description = [[
      Looks for text editor backups and swap files of CMS configuration files, e.g. "wp-config.php~"
]];

---
-- @usage
-- nmap --script=http-config-backup <target>
--
-- @output
-- PORT   STATE SERVICE REASON
-- 80/tcp open  http    syn-ack
-- | http-config-backup:
-- |   /%23wp-config.php%23 HTTP/1.1 200 OK
-- |_  /config.php~ HTTP/1.1 200 OK
--
-- @args http-config-backup.path the path where the CMS is installed
-- @args http-config-backup.save save all the valid config files found
--

author = "Riccardo Cecolin";
license = "Same as Nmap--See http://nmap.org/book/man-legal.html";
categories = { "auth", "intrusive" };

require 'http';
require 'shortport';
require 'url';

portrule = shortport.http;

local function make_grep(pattern)
  return function(s)
    return string.match(s, pattern)
  end
end

local grep_php = make_grep("<%?php");

local CONFIGS = {
  { filename = "wp-config.php", check = grep_php }, -- WordPress
  { filename = "config.php", check = grep_php }, -- phpBB, ExpressionEngine
  { filename = "configuration.php", check = grep_php }, -- Joomla
  { filename = "LocalSettings.php", check = grep_php }, -- MediaWiki
  { filename = "mt-config.cgi", check = make_grep("CGIPath") }, -- Movable Type
  { filename = "settings.php", check = grep_php }, -- Drupal
};

-- Return directory, filename pair. directory may be empty.
local function splitdir(path)
  local dir, filename

  dir, filename = string.match(path, "^(.*/)(.*)$")
  if not dir then
    dir = ""
    filename = path
  end

  return dir, filename
end

-- Return basename, extension pair. extension may be empty.
local function splitext(filename)
  local base, ext;

  base, ext = string.match(filename, "^(.*)(%..+)")
  if not base then
    base = filename
    ext = ""
  end

  return base, ext
end

-- Functions mangling filenames.
local TRANSFORMS = {
  function(fn)
    local base, ext = splitext(fn);
    return base .. ".bak" -- generic bak file
  end,
  function(fn) return fn .. "~" end, -- vim, gedit
  function(fn) return "#" .. fn .. "#" end, -- Emacs
  function(fn)
    local base, ext = splitext(fn);
    return base .. " copy" .. ext -- mac copy
  end,
  function(fn) return "Copy of " .. fn end, -- windows copy
  function(fn) return "Copy (2) of " .. fn end, -- windows copy
  function(fn) return fn .. ".1" end, -- generic backup
  function(fn) return fn .. ".save" end, -- nano
  function(fn) return fn .. ".swp" end, -- vim swap
  function(fn) return fn .. ".old" end, -- generic backup
};

---
--Creates combinations of backup names for a given filename
--Taken from: http-backup-finder.nse
local function backupNames (filename)
  local dir, basename;

  dir, basename = splitdir(filename);
  return coroutine.wrap(function()
    for _, transform in ipairs(TRANSFORMS) do
      coroutine.yield(dir .. transform(basename));
    end
  end)
end

---
--Writes string to file
--Taken from: hostmap.nse
-- @param filename Filename to write
-- @param contents Content of file
-- @return True if file was written successfully
local function write_file (filename, contents)
  local f, err = io.open(filename, "w");
  if not f then
    return f, err;
  end
  f:write(contents);
  f:close();
  return true;
end

action = function (host, port)
  local path = stdnse.get_script_args("http-config-backup.path") or "/";
  local save = stdnse.get_script_args("http-config-backup.save");

  local backups = {};

  if not path:match("/$") then
    path = path .. "/";
  end

  -- for each config file
  for _, cfg in ipairs(CONFIGS) do
    -- for each alteration of the filename
    for entry in backupNames(cfg.filename) do
      local url_path

      url_path = url.build({path = path .. entry});
      -- http request
      local response = http.get(host, port, url_path);

      if (response.status == 200) then
        -- check it if is valid before inserting
        if cfg.check(response.body) then
          local filename = ((host.targetname or host.ip) .. path):gsub("/", "-");

          -- save the content
          if save then
            local status, err = write_file(filename, response.body);
            if status then
              stdnse.print_debug(1, "%s saved", filename);
            else
              stdnse.print_debug(1, "error saving %s", err);
            end
          end

          table.insert(backups, url_path .. " " .. response["status-line"]);
        else
          stdnse.print_debug(1, SCRIPT_NAME .. ": %s: found but not matching: %s",
            host.targetname or host.ip, url_path);
        end
      end
    end
  end

  return stdnse.format_output(true, backups);
end;
