jbe/bsw@20: --[[-- jbe/bsw@20: discovery_data, -- table containing "claimed_identifier", "op_endpoint" and "op_local_identifier" jbe/bsw@20: errmsg, -- error message in case of failure jbe/bsw@20: errcode = -- error code in case of failure (TODO: not implemented yet) jbe/bsw@20: auth.openid.discover{ jbe/bsw@20: user_supplied_identifier = user_supplied_identifier, -- string given by user jbe/bsw@20: https_as_default = https_as_default, -- default to https jbe/bsw@20: curl_options = curl_options -- options passed to "curl" binary, when performing discovery jbe/bsw@20: } jbe/bsw@20: jbe/bsw@20: --]]-- jbe/bsw@20: jbe/bsw@20: -- helper function jbe/bsw@20: local function decode_entities(str) jbe/bsw@20: local str = str jbe/bsw@20: str = string.gsub(value, "<", '<') jbe/bsw@20: str = string.gsub(value, ">", '>') jbe/bsw@20: str = string.gsub(value, """, '"') jbe/bsw@20: str = string.gsub(value, "&", '&') jbe/bsw@20: return str jbe/bsw@20: end jbe/bsw@20: jbe/bsw@20: -- helper function jbe/bsw@20: local function get_tag_value( jbe/bsw@20: str, -- HTML document or document snippet jbe/bsw@20: match_tag, -- tag name jbe/bsw@20: match_key, -- attribute key to match jbe/bsw@20: match_value, -- attribute value to match jbe/bsw@20: result_key -- attribute key of value to return jbe/bsw@20: ) jbe/bsw@20: -- NOTE: The following parameters are case insensitive jbe/bsw@20: local match_tag = string.lower(match_tag) jbe/bsw@20: local match_key = string.lower(match_key) jbe/bsw@20: local match_value = string.lower(match_value) jbe/bsw@20: local result_key = string.lower(result_key) jbe/bsw@20: for tag, attributes in jbe/bsw@20: string.gmatch(str, "<([0-9A-Za-z_-]+) ([^>]*)>") jbe/bsw@20: do jbe/bsw@20: local tag = string.lower(tag) jbe/bsw@20: if tag == match_tag then jbe/bsw@20: local matching = false jbe/bsw@20: for key, value in jbe/bsw@20: string.gmatch(attributes, '([0-9A-Za-z_-]+)="([^"<>]*)"') jbe/bsw@20: do jbe/bsw@20: local key = string.lower(key) jbe/bsw@20: local value = decode_entities(value) jbe/bsw@20: if key == match_key then jbe/bsw@20: -- NOTE: match_key must only match one key of space seperated list jbe/bsw@20: for value in string.gmatch(value, "[^ ]+") do jbe/bsw@20: if string.lower(value) == match_value then jbe/bsw@20: matching = true jbe/bsw@20: break jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: if key == result_key then jbe/bsw@20: result_value = value jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: if matching then jbe/bsw@20: return result_value jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: return nil jbe/bsw@20: end jbe/bsw@20: jbe/bsw@20: -- helper function jbe/bsw@20: local function tag_contents(str, match_tag) jbe/bsw@20: local pos = 0 jbe/bsw@20: local tagpos, closing, tag jbe/bsw@20: local function next_tag() jbe/bsw@20: local prefix jbe/bsw@20: tagpos, prefix, tag, pos = string.match( jbe/bsw@20: str, jbe/bsw@20: "()<(/?)([0-9A-Za-z:_-]+)[^>]*>()", jbe/bsw@20: pos jbe/bsw@20: ) jbe/bsw@20: closing = (prefix == "/") jbe/bsw@20: end jbe/bsw@20: return function() jbe/bsw@20: repeat jbe/bsw@20: next_tag() jbe/bsw@20: if not tagpos then return nil end jbe/bsw@20: local stripped_tag jbe/bsw@20: if string.find(tag, ":") then jbe/bsw@20: stripped_tag = string.match(tag, ":([^:]*)$") jbe/bsw@20: else jbe/bsw@20: stripped_tag = tag jbe/bsw@20: end jbe/bsw@20: until stripped_tag == match_tag and not closing jbe/bsw@20: local content_start = pos jbe/bsw@20: local used_tag = tag jbe/bsw@20: local counter = 0 jbe/bsw@20: while true do jbe/bsw@20: repeat jbe/bsw@20: next_tag() jbe/bsw@20: if not tagpos then return nil end jbe/bsw@20: until tag == used_tag jbe/bsw@20: if closing then jbe/bsw@20: if counter > 0 then jbe/bsw@20: counter = counter - 1 jbe/bsw@20: else jbe/bsw@20: return string.sub(str, content_start, tagpos-1) jbe/bsw@20: end jbe/bsw@20: else jbe/bsw@20: counter = counter + 1 jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: local content = string.sub(rest, 1, startpos-1) jbe/bsw@20: str = string.sub(rest, endpos+1) jbe/bsw@20: return content jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: jbe/bsw@20: local function strip(str) jbe/bsw@20: local str = str jbe/bsw@20: string.gsub(str, "^[ \t\r\n]+", "") jbe/bsw@20: string.gsub(str, "[ \t\r\n]+$", "") jbe/bsw@20: return str jbe/bsw@20: end jbe/bsw@20: jbe/bsw@20: function auth.openid.discover(args) jbe/bsw@20: local url = string.match(args.user_supplied_identifier, "[^#]*") jbe/bsw@20: -- NOTE: XRIs are not supported jbe/bsw@20: if jbe/bsw@20: string.find(url, "^[Xx][Rr][Ii]://") or jbe/bsw@20: string.find(url, "^[=@+$!(]") jbe/bsw@20: then jbe/bsw@20: return nil, "XRI identifiers are not supported." jbe/bsw@20: end jbe/bsw@20: -- Prepend http:// or https://, if neccessary: jbe/bsw@20: if not string.find(url, "://") then jbe/bsw@20: if args.default_to_https then jbe/bsw@20: url = "https://" .. url jbe/bsw@20: else jbe/bsw@20: url = "http://" .. url jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: -- Either an xrds_document or an html_document will be fetched jbe/bsw@20: local xrds_document, html_document jbe/bsw@20: -- Repeat max 10 times to avoid endless redirection loops jbe/bsw@20: local redirects = 0 jbe/bsw@20: while true do jbe/bsw@20: local status, headers, body = auth.openid._curl(url, args.curl_options) jbe/bsw@20: if not status then jbe/bsw@20: return nil, "Error while locating XRDS or HTML file for discovery." jbe/bsw@20: end jbe/bsw@20: -- Check, if we are redirected: jbe/bsw@20: local location = string.match( jbe/bsw@20: headers, jbe/bsw@20: "\r?\n[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)" jbe/bsw@20: ) jbe/bsw@20: if location then jbe/bsw@20: -- If we are redirected too often, then return an error. jbe/bsw@20: if redirects >= 10 then jbe/bsw@20: return nil, "Too many redirects." jbe/bsw@20: end jbe/bsw@20: -- Otherwise follow the redirection by changing the variable "url" jbe/bsw@20: -- and by incrementing the redirect counter. jbe/bsw@20: url = location jbe/bsw@20: redirects = redirects + 1 jbe/bsw@20: else jbe/bsw@20: -- Check, if there is an X-XRDS-Location header jbe/bsw@20: -- pointing to an XRDS document: jbe/bsw@20: local xrds_location = string.match( jbe/bsw@20: headers, jbe/bsw@20: "\r?\n[Xx]%-[Xx][Rr][Dd][Ss]%-[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)" jbe/bsw@20: ) jbe/bsw@20: -- If there is no X-XRDS-Location header, there might be an jbe/bsw@20: -- http-equiv meta tag serving the same purpose: jbe/bsw@20: if not xrds_location and status == 200 then jbe/bsw@20: xrds_location = get_tag_value(body, "meta", "http-equiv", "X-XRDS-Location", "content") jbe/bsw@20: end jbe/bsw@20: if xrds_location then jbe/bsw@20: -- If we know the XRDS-Location, we can fetch the XRDS document jbe/bsw@20: -- from that location: jbe/bsw@20: local status, headers, body = auth.openid._curl(xrds_location, args.curl_options) jbe/bsw@20: if not status then jbe/bsw@20: return nil, "XRDS document could not be loaded." jbe/bsw@20: end jbe/bsw@20: if status ~= 200 then jbe/bsw@20: return nil, "XRDS document not found where expected." jbe/bsw@20: end jbe/bsw@20: xrds_document = body jbe/bsw@20: break jbe/bsw@20: elseif jbe/bsw@20: -- If the Content-Type header is set accordingly, then we already jbe/bsw@20: -- should have received an XRDS document: jbe/bsw@20: string.find( jbe/bsw@20: headers, jbe/bsw@20: "\r?\n[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Tt][Yy][Pp][Ee]:[ \t]*application/xrds%+xml\r?\n" jbe/bsw@20: ) jbe/bsw@20: then jbe/bsw@20: if status ~= 200 then jbe/bsw@20: return nil, "XRDS document announced but not found." jbe/bsw@20: end jbe/bsw@20: xrds_document = body jbe/bsw@20: break jbe/bsw@20: else jbe/bsw@20: -- Otherwise we should have received an HTML document: jbe/bsw@20: if status ~= 200 then jbe/bsw@20: return nil, "No XRDS or HTML document found for discovery." jbe/bsw@20: end jbe/bsw@20: html_document = body jbe/bsw@20: break; jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: local claimed_identifier -- OpenID identifier the user claims to own jbe/bsw@20: local op_endpoint -- OpenID provider endpoint URL jbe/bsw@20: local op_local_identifier -- optional user identifier, local to the OpenID provider jbe/bsw@20: if xrds_document then jbe/bsw@20: -- If we got an XRDS document, we look for a matching entry: jbe/bsw@20: for content in tag_contents(xrds_document, "Service") do jbe/bsw@20: local service_uri, service_localid jbe/bsw@20: for content in tag_contents(content, "URI") do jbe/bsw@20: if not string.find(content, "[<>]") then jbe/bsw@20: service_uri = strip(content) jbe/bsw@20: break jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: for content in tag_contents(content, "LocalID") do jbe/bsw@20: if not string.find(content, "[<>]") then jbe/bsw@20: service_localid = strip(content) jbe/bsw@20: break jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: for content in tag_contents(content, "Type") do jbe/bsw@20: if not string.find(content, "[<>]") then jbe/bsw@20: local content = strip(content) jbe/bsw@20: if content == "http://specs.openid.net/auth/2.0/server" then jbe/bsw@20: -- The user entered a provider identifier, thus claimed_identifier jbe/bsw@20: -- and op_local_identifier will be set to nil. jbe/bsw@20: op_endpoint = service_uri jbe/bsw@20: break jbe/bsw@20: elseif content == "http://specs.openid.net/auth/2.0/signon" then jbe/bsw@20: -- The user entered his/her own identifier. jbe/bsw@20: claimed_identifier = url jbe/bsw@20: op_endpoint = service_uri jbe/bsw@20: op_local_identifier = service_localid jbe/bsw@20: break jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: elseif html_document then jbe/bsw@20: -- If we got an HTML document, we look for matching tags: jbe/bsw@20: claimed_identifier = url jbe/bsw@20: op_endpoint = get_tag_value( jbe/bsw@20: html_document, jbe/bsw@20: "link", "rel", "openid2.provider", "href" jbe/bsw@20: ) jbe/bsw@20: op_local_identifier = get_tag_value( jbe/bsw@20: html_document, jbe/bsw@20: "link", "rel", "openid2.local_id", "href" jbe/bsw@20: ) jbe/bsw@20: else jbe/bsw@20: error("Assertion failed") -- should not happen jbe/bsw@20: end jbe/bsw@20: if not op_endpoint then jbe/bsw@20: return nil, "No OpenID endpoint found." jbe/bsw@20: end jbe/bsw@20: if claimed_identifier then jbe/bsw@20: claimed_identifier = auth.openid._normalize_url(claimed_identifier) jbe/bsw@20: if not claimed_identifier then jbe/bsw@20: return nil, "Claimed identifier could not be normalized." jbe/bsw@20: end jbe/bsw@20: end jbe/bsw@20: return { jbe/bsw@20: claimed_identifier = claimed_identifier, jbe/bsw@20: op_endpoint = op_endpoint, jbe/bsw@20: op_local_identifier = op_local_identifier jbe/bsw@20: } jbe/bsw@20: end