webmcp
annotate framework/env/auth/openid/discover.lua @ 34:e19b0b5c46ba
allow execptions an slot.reset_all()
this is usefull when an error is detected deep into a view where you want redirect
to something usefull, but still save some slots like notice and error slots
this is usefull when an error is detected deep into a view where you want redirect
to something usefull, but still save some slots like notice and error slots
| author | Daniel Poelzleithner <poelzi@poelzi.org> | 
|---|---|
| date | Wed Oct 06 16:14:33 2010 +0200 (2010-10-06) | 
| parents | 47ddf0f86009 | 
| children | 
| 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 |