--[[--
url =                        -- normalized URL or nil
auth.openid._normalize_url(
  url                        -- unnormalized URL
)

This function normalizes an URL, and returns nil if the given URL is not a
valid absolute URL. For security reasons only a restricted set of URLs is
valid.

--]]--

function auth.openid._normalize_url(url)
  local url = string.match(url, "^(.-)??$")  -- remove "?" at end
  local proto, host, path = string.match(
    url,
    "([A-Za-z]+)://([0-9A-Za-z.:_-]+)/?([0-9A-Za-z%%/._~-]*)$"
  )
  if not proto then
    return nil
  end
  proto = string.lower(proto)
  host  = string.lower(host)
  local port = string.match(host, ":(.*)")
  if port then
    if string.find(port, "^[0-9]+$") then
      port = tonumber(port)
      host = string.match(host, "^(.-):")
      if port < 1 or port > 65535 then
        return nil
      end
    else
      return nil
    end
  end
  if proto == "http" then
    if port == 80 then port = nil end
  elseif proto == "https" then
    if port == 443 then port = nil end
  else
    return nil
  end
  if
    string.find(host, "^%.") or
    string.find(host, "%.$") or
    string.find(host, "%.%.")
  then
    return nil
  end
  for part in string.gmatch(host, "[^.]+") do
    if not string.find(part, "[A-Za-z]") then
      return nil
    end
  end
  local path_parts = {}
  for part in string.gmatch(path, "[^/]+") do
    if part == "." then
      -- do nothing
    elseif part == ".." then
      path_parts[#path_parts] = nil
    else
      local fail = false
      local part = string.gsub(
        part,
        "%%([0-9A-Fa-f]?[0-9A-Fa-f]?)",
        function (hex)
          if #hex ~= 2 then
            fail = true
            return
          end
          local char = string.char(tonumber("0x" .. hex))
          if string.find(char, "[0-9A-Za-z._~-]") then
            return char
          else
            return "%" .. string.upper(hex)
          end
        end
      )
      if fail then
        return nil
      end
      path_parts[#path_parts+1] = part
    end
  end
  if string.find(path, "/$") then
    path_parts[#path_parts+1] = ""
  end
  path = table.concat(path_parts, "/")
  if port then
    host = host .. ":" .. tostring(port)
  end
  return proto .. "://" .. host .. "/" .. path
end
