diff --git a/Makefile b/Makefile index eee375b..a867b7e 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ all: $(TARGETS) run_prereqs: all test: run_prereqs - $(ERL_CMD) -noshell -s yadis test -s init stop + $(ERL_CMD) -noshell -s openid test -s init stop clean: cleanlog rm -f $(TARGETS) diff --git a/src/openid.erl b/src/openid.erl new file mode 100644 index 0000000..ec3852a --- /dev/null +++ b/src/openid.erl @@ -0,0 +1,98 @@ +%%%------------------------------------------------------------------- +%%% File : openid.erl +%%% Author : Brendon Hogger +%%% Description : +%%% +%%% Created : 18 Sep 2009 by Brendon Hogger +%%%------------------------------------------------------------------- +-module(openid). + +-export([discover/1, test/0]). + +-include("openid.hrl"). + +-define(GV(E, P), proplists:get_value(E, P)). +-define(GVD(E, P, D), proplists:get_value(E, P, D)). +-define(DBG(Term), io:format("~p: ~p~n", [self(), Term])). + + +discover(Identifier) -> + case yadis:retrieve(Identifier) of + {none, Body} -> html_discovery(Body); + XRDS -> extract_identifier(XRDS) + end. + + +extract_identifier(XRDS) -> + case extract_op_id(XRDS) of + none -> extract_claimed_id(XRDS); + OpID -> OpID + end. + +extract_op_id(XRDS) -> + case find_service_type(XRDS#xrds.services, "http://specs.openid.net/auth/2.0/server") of + none -> none; + URL -> #authReq{opURL=URL, version={2,0}} + end. + +extract_claimed_id(XRDS) -> + extract_claimed_id(XRDS, [{"http://specs.openid.net/auth/2.0/signon", {2,0}}, + {"http://openid.net/signon/1.1", {1,1}}, + {"http://openid.net/signon/1.0", {1,0}}]). + +extract_claimed_id(_, []) -> + none; +extract_claimed_id(XRDS, [{Type,Version}|Rest]) -> + case find_service_type(XRDS#xrds.services, Type) of + none -> extract_claimed_id(XRDS, Rest); + URL -> #authReq{opURL=URL, version=Version} + end. + + +find_service_type([], _) -> none; +find_service_type([{Types, []}|Rest], Type) -> find_service_type(Rest, Type); +find_service_type([{Types, [URL|_]}|Rest], Type) -> + case lists:any(fun(X) -> X == Type end, Types) of + true -> URL; + false -> find_service_type(Rest, Type) + end. + +html_discovery(Body) -> + html_discovery(Body, [{"openid2.provider", "openid2.local_id", {2,0}}, + {"openid.server", "openid.delegate", {1,1}}]). + +html_discovery(_, []) -> + none; +html_discovery(Body, [{ProviderRel, LocalIDRel, Version}|Rest]) -> + case openid_utils:get_tags(Body, "link", "rel", ProviderRel) of + [Tag|_] -> + case ?GVD("href", Tag, none) of + none -> html_discovery(Body, Rest); + URL -> + LocalID = html_local_id(Body, LocalIDRel), + #authReq{opURL=URL, version=Version, localID=LocalID} + end; + _ -> html_discovery(Body, Rest) + end. + +html_local_id(Body, RelName) -> + case openid_utils:get_tags(Body, "link", "rel", RelName) of + [Tag|_] -> ?GVD("href", Tag, none); + _ -> none + end. + + +%% ------------------------------------------------------------ +%% Tests +%% ------------------------------------------------------------ + +test() -> + + ?DBG({"Google:", discover("https://www.google.com/accounts/o8/id")}), + ?DBG({"AOL:", discover("http://openid.aol.com/brend")}), + ?DBG({"Flickr:", discover("http://flickr.com/exbrend")}), + ?DBG({"Myspace:", discover("www.myspace.com")}), + ?DBG({"LiveJournal:", discover("http://exbrend.livejournal.com")}), + ?DBG({"XRI Brend:", discover("=brendonh")}), + + application:stop(inets). % Avoid error spam from held-open connections diff --git a/src/openid.hrl b/src/openid.hrl index eca8b89..4c19e77 100644 --- a/src/openid.hrl +++ b/src/openid.hrl @@ -1,5 +1,22 @@ -%% Shorthands +%%%------------------------------------------------------------------- +%%% File : openid.hrl +%%% Author : Brendon Hogger +%%% Description : OpenID-related record definitions +%%% +%%% Created : 18 Sep 2009 by Brendon Hogger +%%%------------------------------------------------------------------- --define(GV(E, P), proplists:get_value(E, P)). --define(GVD(E, P, D), proplists:get_value(E, P, D)). --define(DBG(Term), io:format("~p: ~p~n", [self(), Term])). +-record(xrds, { + origID, + claimedID, + canonicalID, + isXRI, + services +}). + +-record(authReq, { + opURL, + version, + claimedID, + localID +}). diff --git a/src/openid_utils.erl b/src/openid_utils.erl index e984553..fc2c9b3 100644 --- a/src/openid_utils.erl +++ b/src/openid_utils.erl @@ -64,45 +64,3 @@ check_val(V, V, PropList, Tail, {Buffer,Tag,Key,Val})-> find_tags(Tail, {[PropList|Buffer],Tag,Key,Val}); check_val(_, _, _, Tail, State) -> find_tags(Tail, State). - - - -%% 30> openid_utils:get_tags(S, "meta"). -%% [[{"http-equiv","content-type"}, -%% {"content","text/html; charset=utf-8"}], -%% [{"name","mssmarttagspreventparsing"},{"content","true"}], -%% [{"name","generator"},{"content","blogger"}]] -%% 31> openid_utils:get_tags(S, "link", "rel", "icon"). -%% [[{"rel","icon"}, -%% {"type","image/vnd.microsoft.icon"}, -%% {"href","http://www.blogger.com/favicon.ico"}]] -%% 32> openid_utils:get_tags(S, "link"). -%% [[{"rel","icon"}, -%% {"type","image/vnd.microsoft.icon"}, -%% {"href","http://www.blogger.com/favicon.ico"}], -%% [{"rel","alternate"}, -%% {"type","application/atom+xml"}, -%% {"title","brend - atom"}, -%% {"href","http://brend.taizilla.com/atom.xml"}], -%% [{"rel","alternate"}, -%% {"type","application/rss+xml"}, -%% {"title","brend - rss"}, -%% {"href","http://brend.taizilla.com/rss.xml"}], -%% [{"rel","service.post"}, -%% {"type","application/atom+xml"}, -%% {"title","brend - atom"}, -%% {"href", -%% "http://www.blogger.com/feeds/1426264525662754834/posts/default"}], -%% [{"rel","edituri"}, -%% {"type","application/rsd+xml"}, -%% {"title","rsd"}, -%% {"href", -%% "http://www.blogger.com/rsd.g?blogid=1426264525662754834"}], -%% [{"rel","stylesheet"}, -%% {"type","text/css"}, -%% {"href", -%% "http://www.blogger.com/static/v1/v-css/3727950723-blog_controls.css"}], -%% [{"rel","stylesheet"}, -%% {"type","text/css"}, -%% {"href", -%% "http://www.blogger.com/dyn-css/authorization.css?targetblogid=1426264525662754834&zx=2aeefe4a-f5eb-4cc0-a761-58f936965e98"}]] diff --git a/src/yadis.erl b/src/yadis.erl index 7042932..1c7f9b5 100644 --- a/src/yadis.erl +++ b/src/yadis.erl @@ -9,6 +9,7 @@ -export([normalize/1, retrieve/1, test/0]). +-include("openid.hrl"). -include_lib("xmerl/include/xmerl.hrl"). -define(GVD(E, P, D), proplists:get_value(E, P, D)). @@ -24,30 +25,51 @@ %% API %% ------------------------------------------------------------ -normalize("xri://" ++ Identifier) -> normalize(Identifier); -normalize([$=|_]=Identifier) -> resolve(Identifier); -normalize([$@|_]=Identifier) -> resolve(Identifier); -normalize([$+|_]=Identifier) -> resolve(Identifier); -normalize([$$|_]=Identifier) -> resolve(Identifier); -normalize([$!|_]=Identifier) -> resolve(Identifier); -normalize([$(|_]=Identifier) -> resolve(Identifier); -normalize("http://" ++ Tail) -> strip_fragment("http://" ++ Tail); -normalize("https://" ++ Tail) -> strip_fragment("https://" ++ Tail); -normalize(PartialURL) -> strip_fragment("http://" ++ PartialURL). +normalize("xri://" ++ Identifier) -> {Identifier, true}; +normalize([$=|_]=Identifier) -> {Identifier, true}; +normalize([$@|_]=Identifier) -> {Identifier, true}; +normalize([$+|_]=Identifier) -> {Identifier, true}; +normalize([$$|_]=Identifier) -> {Identifier, true}; +normalize([$!|_]=Identifier) -> {Identifier, true}; +normalize([$(|_]=Identifier) -> {Identifier, true}; +normalize("http://" ++ Tail) -> {strip_fragment("http://" ++ Tail), false}; +normalize("https://" ++ Tail) -> {strip_fragment("https://" ++ Tail), false}; +normalize(PartialURL) -> {strip_fragment("http://" ++ PartialURL), false}. -retrieve(YadisURL) -> +retrieve(Identifier) -> application:start(inets), application:start(ssl), - - NormalizedURL = normalize(YadisURL), - - case http:request(get, {NormalizedURL, ?HTTP_HEADERS}, ?HTTP_OPTIONS, ?REQ_OPTIONS) of + + {Normalized, IsXRI} = normalize(Identifier), + + URL = case IsXRI of + true -> resolve(Normalized); + false -> Normalized + end, + + case http:request(get, {URL, ?HTTP_HEADERS}, ?HTTP_OPTIONS, ?REQ_OPTIONS) of {ok, {_Status, Headers, Body}} -> DescriptorURL = get_descriptor_url(Headers, Body), - handle_response(DescriptorURL, Headers, Body); + XRDS = handle_response(DescriptorURL, Headers, Body), + case XRDS of + none -> + {none, Body}; + #xrds{} -> + + % XXX Todo -- Normalize DescriptorURL as claimedID + % (2.0 spec #7.2.4) + + ClaimedID = case IsXRI of + true -> Normalized; + false -> DescriptorURL + end, + XRDS#xrds{origID=Identifier, + isXRI=IsXRI, + claimedID=ClaimedID} + end; Other -> - {error, {http_error, {YadisURL, Other}}} + {error, {http_error, {Normalized, Other}}} end. @@ -62,7 +84,7 @@ strip_fragment(URL) -> strip_fragment(URL, []). strip_fragment([$#|_], SoFar) -> lists:reverse(SoFar); strip_fragment([], SoFar) -> lists:reverse(SoFar); strip_fragment([H|T], SoFar) -> strip_fragment(T, [H|SoFar]). - + handle_response(none, Headers, Body) -> @@ -73,7 +95,7 @@ handle_response(URL, _Headers, _Body) -> get_xrds("application/xrds" ++ _Rest, Body) -> munge_xrds(Body); get_xrds("text/xml" ++ _Rest, Body) -> munge_xrds(Body); % Against the spec, but LiveJournal does it. -get_xrds(Other, _Body) -> {error, {not_xrds, Other}}. +get_xrds(Other, _Body) -> none. try_descriptor_url(none) -> {error, no_descriptor_url}; @@ -106,34 +128,6 @@ get_descriptor_url(Body) -> [] -> none; [Tag|_] -> ?GVD("content", Tag, none) end. - - - -%% get_descriptor_url(" get_meta(Rest); -%% get_descriptor_url("" ++ _Rest) -> none; -%% get_descriptor_url("") -> none; -%% get_descriptor_url([_|Rest]) -> -%% get_descriptor_url(Rest). - - -%% get_meta(Rest) -> -%% Content = get_meta_content(Rest, []), -%% case re:run(string:to_lower(Content), -%% "([a-z0-9-]+)\s*=\s*[\"'](.*?)[\"']", % " -%% [{capture, all_but_first, list}, global]) of -%% {match, Bits} -> check_meta([{K,V} || [K,V] <- Bits], Rest); -%% _ -> get_descriptor_url(Rest) -%% end. - -%% check_meta(PropList, Rest) -> -%% case ?GVD("http-equiv", PropList, none) of -%% "x-xrds-location" -> ?GVD("content", PropList, none); -%% _ -> get_descriptor_url(Rest) -%% end. - - -%% get_meta_content(">" ++ _Rest, Content) -> lists:reverse(Content); -%% get_meta_content([Char|Rest], Bits) -> get_meta_content(Rest, [Char|Bits]). %% ------------------------------------------------------------ @@ -142,9 +136,11 @@ get_descriptor_url(Body) -> munge_xrds(String) -> {Doc, _} = xmerl_scan:string(String), - [{Ts, Us} || {_P, Ts, Us} <- lists:sort( + CanonicalID = get_canonical_id(Doc), + Services = [{Ts, Us} || {_P, Ts, Us} <- lists:sort( fun({P1,_,_},{P2,_,_}) -> P1 < P2 end, - [munge_service(S) || S <- xmerl_xpath:string("XRD/Service", Doc)])]. + [munge_service(S) || S <- xmerl_xpath:string("XRD/Service", Doc)])], + #xrds{canonicalID=CanonicalID, services=Services}. munge_service(Service) -> Priority = get_priority(Service#xmlElement.attributes), @@ -162,6 +158,12 @@ get_priority([#xmlAttribute{name=priority, value=Value}|_]) -> list_to_integer(V get_priority([_|Rest]) -> get_priority(Rest); get_priority([]) -> none. +get_canonical_id(Doc) -> + case xmerl_xpath:string("XRD/CanonicalID", Doc) of + [] -> none; + [#xmlElement{content=[Value|_]}|_] -> Value#xmlText.value + end. + %% ------------------------------------------------------------ %% Tests @@ -172,7 +174,8 @@ get_priority([]) -> none. test() -> ?P({"Google:", retrieve("https://www.google.com/accounts/o8/id")}), % Direct XRDS response - ?P({"AOL:", retrieve("https://api.screenname.aol.com/auth/openidServer")}), % x-xrds-location header + %?P({"AOL:", retrieve("https://api.screenname.aol.com/auth/openidServer")}), % x-xrds-location header + ?P({"Flickr:", retrieve("http://flickr.com/exbrend")}), ?P({"LiveJournal:", retrieve("http://exbrend.livejournal.com")}), % x-xrds-location meta tag ?P({"XRI Brend:", retrieve("=brendonh")}), % Direct XRDS via xri.net