| 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 |