| rev | 
   line source | 
| 
jbe/bsw@20
 | 
     1 --[[--
 | 
| 
jbe/bsw@20
 | 
     2 discovery_data,                                         -- table containing "claimed_identifier", "op_endpoint" and "op_local_identifier"
 | 
| 
jbe/bsw@20
 | 
     3 errmsg,                                                 -- error message in case of failure
 | 
| 
jbe/bsw@20
 | 
     4 errcode =                                               -- error code in case of failure (TODO: not implemented yet)
 | 
| 
jbe/bsw@20
 | 
     5 auth.openid.discover{
 | 
| 
jbe/bsw@20
 | 
     6   user_supplied_identifier = user_supplied_identifier,  -- string given by user
 | 
| 
jbe/bsw@20
 | 
     7   https_as_default         = https_as_default,          -- default to https
 | 
| 
jbe/bsw@20
 | 
     8   curl_options             = curl_options               -- options passed to "curl" binary, when performing discovery
 | 
| 
jbe/bsw@20
 | 
     9 }
 | 
| 
jbe/bsw@20
 | 
    10 
 | 
| 
jbe/bsw@20
 | 
    11 --]]--
 | 
| 
jbe/bsw@20
 | 
    12 
 | 
| 
jbe/bsw@20
 | 
    13 -- helper function
 | 
| 
jbe/bsw@20
 | 
    14 local function decode_entities(str)
 | 
| 
jbe/bsw@20
 | 
    15   local str = str
 | 
| 
jbe/bsw@20
 | 
    16   str = string.gsub(value, "<", '<')
 | 
| 
jbe/bsw@20
 | 
    17   str = string.gsub(value, ">", '>')
 | 
| 
jbe/bsw@20
 | 
    18   str = string.gsub(value, """, '"')
 | 
| 
jbe/bsw@20
 | 
    19   str = string.gsub(value, "&", '&')
 | 
| 
jbe/bsw@20
 | 
    20   return str
 | 
| 
jbe/bsw@20
 | 
    21 end
 | 
| 
jbe/bsw@20
 | 
    22 
 | 
| 
jbe/bsw@20
 | 
    23 -- helper function
 | 
| 
jbe/bsw@20
 | 
    24 local function get_tag_value(
 | 
| 
jbe/bsw@20
 | 
    25   str,          -- HTML document or document snippet
 | 
| 
jbe/bsw@20
 | 
    26   match_tag,    -- tag name
 | 
| 
jbe/bsw@20
 | 
    27   match_key,    -- attribute key to match
 | 
| 
jbe/bsw@20
 | 
    28   match_value,  -- attribute value to match
 | 
| 
jbe/bsw@20
 | 
    29   result_key    -- attribute key of value to return
 | 
| 
jbe/bsw@20
 | 
    30 )
 | 
| 
jbe/bsw@20
 | 
    31   -- NOTE: The following parameters are case insensitive
 | 
| 
jbe/bsw@20
 | 
    32   local match_tag   = string.lower(match_tag)
 | 
| 
jbe/bsw@20
 | 
    33   local match_key   = string.lower(match_key)
 | 
| 
jbe/bsw@20
 | 
    34   local match_value = string.lower(match_value)
 | 
| 
jbe/bsw@20
 | 
    35   local result_key  = string.lower(result_key)
 | 
| 
jbe/bsw@20
 | 
    36   for tag, attributes in
 | 
| 
jbe/bsw@20
 | 
    37     string.gmatch(str, "<([0-9A-Za-z_-]+) ([^>]*)>")
 | 
| 
jbe/bsw@20
 | 
    38   do
 | 
| 
jbe/bsw@20
 | 
    39     local tag = string.lower(tag)
 | 
| 
jbe/bsw@20
 | 
    40     if tag == match_tag then
 | 
| 
jbe/bsw@20
 | 
    41       local matching = false
 | 
| 
jbe/bsw@20
 | 
    42       for key, value in
 | 
| 
jbe/bsw@20
 | 
    43         string.gmatch(attributes, '([0-9A-Za-z_-]+)="([^"<>]*)"')
 | 
| 
jbe/bsw@20
 | 
    44       do
 | 
| 
jbe/bsw@20
 | 
    45         local key = string.lower(key)
 | 
| 
jbe/bsw@20
 | 
    46         local value = decode_entities(value)
 | 
| 
jbe/bsw@20
 | 
    47         if key == match_key then
 | 
| 
jbe/bsw@20
 | 
    48           -- NOTE: match_key must only match one key of space seperated list
 | 
| 
jbe/bsw@20
 | 
    49           for value in string.gmatch(value, "[^ ]+") do
 | 
| 
jbe/bsw@20
 | 
    50             if string.lower(value) == match_value then
 | 
| 
jbe/bsw@20
 | 
    51               matching = true
 | 
| 
jbe/bsw@20
 | 
    52               break
 | 
| 
jbe/bsw@20
 | 
    53             end
 | 
| 
jbe/bsw@20
 | 
    54           end
 | 
| 
jbe/bsw@20
 | 
    55         end
 | 
| 
jbe/bsw@20
 | 
    56         if key == result_key then
 | 
| 
jbe/bsw@20
 | 
    57           result_value = value
 | 
| 
jbe/bsw@20
 | 
    58         end
 | 
| 
jbe/bsw@20
 | 
    59       end
 | 
| 
jbe/bsw@20
 | 
    60       if matching then
 | 
| 
jbe/bsw@20
 | 
    61         return result_value
 | 
| 
jbe/bsw@20
 | 
    62       end
 | 
| 
jbe/bsw@20
 | 
    63     end
 | 
| 
jbe/bsw@20
 | 
    64   end
 | 
| 
jbe/bsw@20
 | 
    65   return nil
 | 
| 
jbe/bsw@20
 | 
    66 end
 | 
| 
jbe/bsw@20
 | 
    67 
 | 
| 
jbe/bsw@20
 | 
    68 -- helper function
 | 
| 
jbe/bsw@20
 | 
    69 local function tag_contents(str, match_tag)
 | 
| 
jbe/bsw@20
 | 
    70   local pos = 0
 | 
| 
jbe/bsw@20
 | 
    71   local tagpos, closing, tag
 | 
| 
jbe/bsw@20
 | 
    72   local function next_tag()
 | 
| 
jbe/bsw@20
 | 
    73     local prefix
 | 
| 
jbe/bsw@20
 | 
    74     tagpos, prefix, tag, pos = string.match(
 | 
| 
jbe/bsw@20
 | 
    75       str,
 | 
| 
jbe/bsw@20
 | 
    76       "()<(/?)([0-9A-Za-z:_-]+)[^>]*>()",
 | 
| 
jbe/bsw@20
 | 
    77       pos
 | 
| 
jbe/bsw@20
 | 
    78     )
 | 
| 
jbe/bsw@20
 | 
    79     closing = (prefix == "/")
 | 
| 
jbe/bsw@20
 | 
    80   end
 | 
| 
jbe/bsw@20
 | 
    81   return function()
 | 
| 
jbe/bsw@20
 | 
    82     repeat
 | 
| 
jbe/bsw@20
 | 
    83       next_tag()
 | 
| 
jbe/bsw@20
 | 
    84       if not tagpos then return nil end
 | 
| 
jbe/bsw@20
 | 
    85       local stripped_tag
 | 
| 
jbe/bsw@20
 | 
    86       if string.find(tag, ":") then
 | 
| 
jbe/bsw@20
 | 
    87         stripped_tag = string.match(tag, ":([^:]*)$")
 | 
| 
jbe/bsw@20
 | 
    88       else
 | 
| 
jbe/bsw@20
 | 
    89         stripped_tag = tag
 | 
| 
jbe/bsw@20
 | 
    90       end
 | 
| 
jbe/bsw@20
 | 
    91     until stripped_tag == match_tag and not closing
 | 
| 
jbe/bsw@20
 | 
    92     local content_start = pos
 | 
| 
jbe/bsw@20
 | 
    93     local used_tag = tag
 | 
| 
jbe/bsw@20
 | 
    94     local counter = 0
 | 
| 
jbe/bsw@20
 | 
    95     while true do
 | 
| 
jbe/bsw@20
 | 
    96       repeat
 | 
| 
jbe/bsw@20
 | 
    97         next_tag()
 | 
| 
jbe/bsw@20
 | 
    98         if not tagpos then return nil end
 | 
| 
jbe/bsw@20
 | 
    99       until tag == used_tag
 | 
| 
jbe/bsw@20
 | 
   100       if closing then
 | 
| 
jbe/bsw@20
 | 
   101         if counter > 0 then
 | 
| 
jbe/bsw@20
 | 
   102           counter = counter - 1
 | 
| 
jbe/bsw@20
 | 
   103         else
 | 
| 
jbe/bsw@20
 | 
   104           return string.sub(str, content_start, tagpos-1)
 | 
| 
jbe/bsw@20
 | 
   105         end
 | 
| 
jbe/bsw@20
 | 
   106       else
 | 
| 
jbe/bsw@20
 | 
   107         counter = counter + 1
 | 
| 
jbe/bsw@20
 | 
   108       end
 | 
| 
jbe/bsw@20
 | 
   109     end
 | 
| 
jbe/bsw@20
 | 
   110     local content = string.sub(rest, 1, startpos-1)
 | 
| 
jbe/bsw@20
 | 
   111     str = string.sub(rest, endpos+1)
 | 
| 
jbe/bsw@20
 | 
   112     return content
 | 
| 
jbe/bsw@20
 | 
   113   end
 | 
| 
jbe/bsw@20
 | 
   114 end
 | 
| 
jbe/bsw@20
 | 
   115 
 | 
| 
jbe/bsw@20
 | 
   116 local function strip(str)
 | 
| 
jbe/bsw@20
 | 
   117   local str = str
 | 
| 
jbe/bsw@20
 | 
   118   string.gsub(str, "^[ \t\r\n]+", "")
 | 
| 
jbe/bsw@20
 | 
   119   string.gsub(str, "[ \t\r\n]+$", "")
 | 
| 
jbe/bsw@20
 | 
   120   return str
 | 
| 
jbe/bsw@20
 | 
   121 end
 | 
| 
jbe/bsw@20
 | 
   122 
 | 
| 
jbe/bsw@20
 | 
   123 function auth.openid.discover(args)
 | 
| 
jbe/bsw@20
 | 
   124   local url = string.match(args.user_supplied_identifier, "[^#]*")
 | 
| 
jbe/bsw@20
 | 
   125   -- NOTE: XRIs are not supported
 | 
| 
jbe/bsw@20
 | 
   126   if
 | 
| 
jbe/bsw@20
 | 
   127     string.find(url, "^[Xx][Rr][Ii]://") or
 | 
| 
jbe/bsw@20
 | 
   128     string.find(url, "^[=@+$!(]")
 | 
| 
jbe/bsw@20
 | 
   129   then
 | 
| 
jbe/bsw@20
 | 
   130     return nil, "XRI identifiers are not supported."
 | 
| 
jbe/bsw@20
 | 
   131   end
 | 
| 
jbe/bsw@20
 | 
   132   -- Prepend http:// or https://, if neccessary:
 | 
| 
jbe/bsw@20
 | 
   133   if not string.find(url, "://") then
 | 
| 
jbe/bsw@20
 | 
   134     if args.default_to_https then
 | 
| 
jbe/bsw@20
 | 
   135       url = "https://" .. url
 | 
| 
jbe/bsw@20
 | 
   136     else
 | 
| 
jbe/bsw@20
 | 
   137       url = "http://" .. url
 | 
| 
jbe/bsw@20
 | 
   138     end
 | 
| 
jbe/bsw@20
 | 
   139   end
 | 
| 
jbe/bsw@20
 | 
   140   -- Either an xrds_document or an html_document will be fetched
 | 
| 
jbe/bsw@20
 | 
   141   local xrds_document, html_document
 | 
| 
jbe/bsw@20
 | 
   142   -- Repeat max 10 times to avoid endless redirection loops
 | 
| 
jbe/bsw@20
 | 
   143   local redirects = 0
 | 
| 
jbe/bsw@20
 | 
   144   while true do
 | 
| 
jbe/bsw@20
 | 
   145     local status, headers, body = auth.openid._curl(url, args.curl_options)
 | 
| 
jbe/bsw@20
 | 
   146     if not status then
 | 
| 
jbe/bsw@20
 | 
   147       return nil, "Error while locating XRDS or HTML file for discovery."
 | 
| 
jbe/bsw@20
 | 
   148     end
 | 
| 
jbe/bsw@20
 | 
   149     -- Check, if we are redirected:
 | 
| 
jbe/bsw@20
 | 
   150     local location = string.match(
 | 
| 
jbe/bsw@20
 | 
   151       headers,
 | 
| 
jbe/bsw@20
 | 
   152       "\r?\n[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
 | 
| 
jbe/bsw@20
 | 
   153     )
 | 
| 
jbe/bsw@20
 | 
   154     if location then
 | 
| 
jbe/bsw@20
 | 
   155       -- If we are redirected too often, then return an error.
 | 
| 
jbe/bsw@20
 | 
   156       if redirects >= 10 then
 | 
| 
jbe/bsw@20
 | 
   157         return nil, "Too many redirects."
 | 
| 
jbe/bsw@20
 | 
   158       end
 | 
| 
jbe/bsw@20
 | 
   159       -- Otherwise follow the redirection by changing the variable "url"
 | 
| 
jbe/bsw@20
 | 
   160       -- and by incrementing the redirect counter.
 | 
| 
jbe/bsw@20
 | 
   161       url = location
 | 
| 
jbe/bsw@20
 | 
   162       redirects = redirects + 1
 | 
| 
jbe/bsw@20
 | 
   163     else
 | 
| 
jbe/bsw@20
 | 
   164       -- Check, if there is an X-XRDS-Location header
 | 
| 
jbe/bsw@20
 | 
   165       -- pointing to an XRDS document:
 | 
| 
jbe/bsw@20
 | 
   166       local xrds_location = string.match(
 | 
| 
jbe/bsw@20
 | 
   167         headers,
 | 
| 
jbe/bsw@20
 | 
   168         "\r?\n[Xx]%-[Xx][Rr][Dd][Ss]%-[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
 | 
| 
jbe/bsw@20
 | 
   169       )
 | 
| 
jbe/bsw@20
 | 
   170       -- If there is no X-XRDS-Location header, there might be an
 | 
| 
jbe/bsw@20
 | 
   171       -- http-equiv meta tag serving the same purpose:
 | 
| 
jbe/bsw@20
 | 
   172       if not xrds_location and status == 200 then
 | 
| 
jbe/bsw@20
 | 
   173         xrds_location = get_tag_value(body, "meta", "http-equiv", "X-XRDS-Location", "content")
 | 
| 
jbe/bsw@20
 | 
   174       end
 | 
| 
jbe/bsw@20
 | 
   175       if xrds_location then
 | 
| 
jbe/bsw@20
 | 
   176         -- If we know the XRDS-Location, we can fetch the XRDS document
 | 
| 
jbe/bsw@20
 | 
   177         -- from that location:
 | 
| 
jbe/bsw@20
 | 
   178         local status, headers, body = auth.openid._curl(xrds_location, args.curl_options)
 | 
| 
jbe/bsw@20
 | 
   179         if not status then
 | 
| 
jbe/bsw@20
 | 
   180           return nil, "XRDS document could not be loaded."
 | 
| 
jbe/bsw@20
 | 
   181         end
 | 
| 
jbe/bsw@20
 | 
   182         if status ~= 200 then
 | 
| 
jbe/bsw@20
 | 
   183           return nil, "XRDS document not found where expected."
 | 
| 
jbe/bsw@20
 | 
   184         end
 | 
| 
jbe/bsw@20
 | 
   185         xrds_document = body
 | 
| 
jbe/bsw@20
 | 
   186         break
 | 
| 
jbe/bsw@20
 | 
   187       elseif
 | 
| 
jbe/bsw@20
 | 
   188         -- If the Content-Type header is set accordingly, then we already
 | 
| 
jbe/bsw@20
 | 
   189         -- should have received an XRDS document:
 | 
| 
jbe/bsw@20
 | 
   190         string.find(
 | 
| 
jbe/bsw@20
 | 
   191           headers,
 | 
| 
jbe/bsw@20
 | 
   192           "\r?\n[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Tt][Yy][Pp][Ee]:[ \t]*application/xrds%+xml\r?\n"
 | 
| 
jbe/bsw@20
 | 
   193         )
 | 
| 
jbe/bsw@20
 | 
   194       then
 | 
| 
jbe/bsw@20
 | 
   195         if status ~= 200 then
 | 
| 
jbe/bsw@20
 | 
   196           return nil, "XRDS document announced but not found."
 | 
| 
jbe/bsw@20
 | 
   197         end
 | 
| 
jbe/bsw@20
 | 
   198         xrds_document = body
 | 
| 
jbe/bsw@20
 | 
   199         break
 | 
| 
jbe/bsw@20
 | 
   200       else
 | 
| 
jbe/bsw@20
 | 
   201         -- Otherwise we should have received an HTML document:
 | 
| 
jbe/bsw@20
 | 
   202         if status ~= 200 then
 | 
| 
jbe/bsw@20
 | 
   203           return nil, "No XRDS or HTML document found for discovery."
 | 
| 
jbe/bsw@20
 | 
   204         end
 | 
| 
jbe/bsw@20
 | 
   205         html_document = body
 | 
| 
jbe/bsw@20
 | 
   206         break;
 | 
| 
jbe/bsw@20
 | 
   207       end
 | 
| 
jbe/bsw@20
 | 
   208     end
 | 
| 
jbe/bsw@20
 | 
   209   end
 | 
| 
jbe/bsw@20
 | 
   210   local claimed_identifier   -- OpenID identifier the user claims to own
 | 
| 
jbe/bsw@20
 | 
   211   local op_endpoint          -- OpenID provider endpoint URL
 | 
| 
jbe/bsw@20
 | 
   212   local op_local_identifier  -- optional user identifier, local to the OpenID provider
 | 
| 
jbe/bsw@20
 | 
   213   if xrds_document then
 | 
| 
jbe/bsw@20
 | 
   214     -- If we got an XRDS document, we look for a matching <Service> entry:
 | 
| 
jbe/bsw@20
 | 
   215     for content in tag_contents(xrds_document, "Service") do
 | 
| 
jbe/bsw@20
 | 
   216       local service_uri, service_localid
 | 
| 
jbe/bsw@20
 | 
   217       for content in tag_contents(content, "URI") do
 | 
| 
jbe/bsw@20
 | 
   218         if not string.find(content, "[<>]") then
 | 
| 
jbe/bsw@20
 | 
   219           service_uri = strip(content)
 | 
| 
jbe/bsw@20
 | 
   220           break
 | 
| 
jbe/bsw@20
 | 
   221         end
 | 
| 
jbe/bsw@20
 | 
   222       end
 | 
| 
jbe/bsw@20
 | 
   223       for content in tag_contents(content, "LocalID") do
 | 
| 
jbe/bsw@20
 | 
   224         if not string.find(content, "[<>]") then
 | 
| 
jbe/bsw@20
 | 
   225           service_localid = strip(content)
 | 
| 
jbe/bsw@20
 | 
   226           break
 | 
| 
jbe/bsw@20
 | 
   227         end
 | 
| 
jbe/bsw@20
 | 
   228       end
 | 
| 
jbe/bsw@20
 | 
   229       for content in tag_contents(content, "Type") do
 | 
| 
jbe/bsw@20
 | 
   230         if not string.find(content, "[<>]") then
 | 
| 
jbe/bsw@20
 | 
   231           local content = strip(content)
 | 
| 
jbe/bsw@20
 | 
   232           if content == "http://specs.openid.net/auth/2.0/server" then
 | 
| 
jbe/bsw@20
 | 
   233             -- The user entered a provider identifier, thus claimed_identifier
 | 
| 
jbe/bsw@20
 | 
   234             -- and op_local_identifier will be set to nil.
 | 
| 
jbe/bsw@20
 | 
   235             op_endpoint = service_uri
 | 
| 
jbe/bsw@20
 | 
   236             break
 | 
| 
jbe/bsw@20
 | 
   237           elseif content == "http://specs.openid.net/auth/2.0/signon" then
 | 
| 
jbe/bsw@20
 | 
   238             -- The user entered his/her own identifier.
 | 
| 
jbe/bsw@20
 | 
   239             claimed_identifier  = url
 | 
| 
jbe/bsw@20
 | 
   240             op_endpoint         = service_uri
 | 
| 
jbe/bsw@20
 | 
   241             op_local_identifier = service_localid
 | 
| 
jbe/bsw@20
 | 
   242             break
 | 
| 
jbe/bsw@20
 | 
   243           end
 | 
| 
jbe/bsw@20
 | 
   244         end
 | 
| 
jbe/bsw@20
 | 
   245       end
 | 
| 
jbe/bsw@20
 | 
   246     end
 | 
| 
jbe/bsw@20
 | 
   247   elseif html_document then
 | 
| 
jbe/bsw@20
 | 
   248     -- If we got an HTML document, we look for matching <link .../> tags:
 | 
| 
jbe/bsw@20
 | 
   249     claimed_identifier = url
 | 
| 
jbe/bsw@20
 | 
   250     op_endpoint = get_tag_value(
 | 
| 
jbe/bsw@20
 | 
   251       html_document,
 | 
| 
jbe/bsw@20
 | 
   252       "link", "rel", "openid2.provider", "href"
 | 
| 
jbe/bsw@20
 | 
   253     )
 | 
| 
jbe/bsw@20
 | 
   254     op_local_identifier = get_tag_value(
 | 
| 
jbe/bsw@20
 | 
   255       html_document,
 | 
| 
jbe/bsw@20
 | 
   256       "link", "rel", "openid2.local_id", "href"
 | 
| 
jbe/bsw@20
 | 
   257     )
 | 
| 
jbe/bsw@20
 | 
   258   else
 | 
| 
jbe/bsw@20
 | 
   259     error("Assertion failed")  -- should not happen
 | 
| 
jbe/bsw@20
 | 
   260   end
 | 
| 
jbe/bsw@20
 | 
   261   if not op_endpoint then
 | 
| 
jbe/bsw@20
 | 
   262     return nil, "No OpenID endpoint found."
 | 
| 
jbe/bsw@20
 | 
   263   end
 | 
| 
jbe/bsw@20
 | 
   264   if claimed_identifier then
 | 
| 
jbe/bsw@20
 | 
   265     claimed_identifier = auth.openid._normalize_url(claimed_identifier)
 | 
| 
jbe/bsw@20
 | 
   266     if not claimed_identifier then
 | 
| 
jbe/bsw@20
 | 
   267       return nil, "Claimed identifier could not be normalized."
 | 
| 
jbe/bsw@20
 | 
   268     end
 | 
| 
jbe/bsw@20
 | 
   269   end
 | 
| 
jbe/bsw@20
 | 
   270   return {
 | 
| 
jbe/bsw@20
 | 
   271     claimed_identifier  = claimed_identifier,
 | 
| 
jbe/bsw@20
 | 
   272     op_endpoint         = op_endpoint,
 | 
| 
jbe/bsw@20
 | 
   273     op_local_identifier = op_local_identifier
 | 
| 
jbe/bsw@20
 | 
   274   }
 | 
| 
jbe/bsw@20
 | 
   275 end
 |