summaryrefslogtreecommitdiffabout
Unidiff
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--include/Makefile.am8
-rw-r--r--include/opkele/openid_service_resolver.h118
-rw-r--r--include/opkele/uris.h10
-rw-r--r--lib/Makefile.am3
-rw-r--r--lib/openid_service_resolver.cc294
-rw-r--r--lib/util.cc16
-rw-r--r--test/.gitignore1
-rw-r--r--test/Makefile.am5
-rw-r--r--test/openid_resolve.cc36
9 files changed, 482 insertions, 9 deletions
diff --git a/include/Makefile.am b/include/Makefile.am
index 0385cfb..23c7e0d 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -10,8 +10,10 @@ nobase_include_HEADERS = \
10 opkele/sreg.h \ 10 opkele/sreg.h \
11 opkele/extension_chain.h \ 11 opkele/extension_chain.h \
12 opkele/xconsumer.h \ 12 opkele/xconsumer.h \
13 opkele/xserver.h 13 opkele/xserver.h \
14 opkele/curl.h opkele/expat.h \
15 opkele/openid_service_resolver.h \
16 opkele/uris.h
14EXTRA_DIST = \ 17EXTRA_DIST = \
15 opkele/data.h \ 18 opkele/data.h \
16 opkele/util.h \ 19 opkele/util.h
17 opkele/curl.h opkele/expat.h
diff --git a/include/opkele/openid_service_resolver.h b/include/opkele/openid_service_resolver.h
new file mode 100644
index 0000000..64edd28
--- a/dev/null
+++ b/include/opkele/openid_service_resolver.h
@@ -0,0 +1,118 @@
1#ifndef __OPKELE_OPENID_SERVICE_RESOLVER_H
2#define __OPKELE_OPENID_SERVICE_RESOLVER_H
3
4#include <climits>
5#include <string>
6#include <list>
7#include <set>
8#include <map>
9#include <opkele/curl.h>
10#include <opkele/expat.h>
11
12namespace opkele {
13 using std::list;
14 using std::string;
15 using std::set;
16 using std::map;
17
18 struct openid_auth_SEP_t {
19 long priority;
20 set<string> xrd_Type;
21 string xrd_URI;
22 string openid_Delegate;
23
24 openid_auth_SEP_t() : priority(LONG_MAX) { }
25 };
26
27 struct openid_auth_info_t {
28 string canonical_id;
29 openid_auth_SEP_t auth_SEP;
30 };
31
32
33 class openid_service_resolver_t : public util::curl_t, public util::expat_t {
34 public:
35 string xri_proxy;
36
37 openid_service_resolver_t(const string& xp="");
38 ~openid_service_resolver_t() throw() { }
39
40 const openid_auth_info_t& resolve(const string& id);
41
42 enum state_t {
43 state_parse = 0,
44 state_stopping_head, state_stopping_body,
45 state_stopping_size
46 };
47 state_t state;
48
49 struct parser_node_t {
50 string element;
51 string content;
52 typedef map<string,string> attrs_t;
53 attrs_t attrs;
54 bool skip_text, skip_tags;
55 openid_auth_info_t auth_info;
56
57 parser_node_t(const XML_Char *n,const XML_Char **a)
58 : skip_text(true), skip_tags(true)
59 {
60 element = n;
61 for(;*a;a+=2)
62 attrs[a[0]] = a[1];
63 }
64
65 };
66
67 class parser_tree_t : public list<parser_node_t> {
68 public:
69 const_reference top() const { return back(); }
70 reference top() { return back(); }
71
72 const_reference parent() const {
73 const_reverse_iterator rv = rbegin();
74 return *(++rv); }
75 reference parent() {
76 reverse_iterator rv = rbegin();
77 return *(++rv); }
78
79 inline void pop() { pop_back(); }
80 inline void push(const_reference e) { push_back(e); }
81
82 void push(const XML_Char *n,const XML_Char **a) {
83 parser_node_t nn(n,a);
84 if(empty())
85 nn.skip_text = nn.skip_tags = true;
86 else{
87 const_reference t = top();
88 nn.skip_text = t.skip_text; nn.skip_tags = t.skip_tags;
89 }
90 push(nn);
91 }
92 };
93 parser_tree_t tree;
94
95 void start_element(const XML_Char *n,const XML_Char **a);
96 void end_element(const XML_Char *n);
97 void character_data(const XML_Char *s,int l);
98
99 string xrds_location;
100 openid_auth_SEP_t html_SEP;
101 openid_auth_info_t auth_info;
102
103 void pop_tag();
104
105 size_t write(void *p,size_t s,size_t nm);
106
107 string http_content_type;
108
109 size_t header(void *p,size_t s,size_t nm);
110
111 bool xri_mode;
112
113 void discover_service(const string& url,bool xri=false);
114 };
115
116}
117
118#endif /* __OPKELE_OPENID_SERVICE_RESOLVER_H */
diff --git a/include/opkele/uris.h b/include/opkele/uris.h
new file mode 100644
index 0000000..9a6a3cd
--- a/dev/null
+++ b/include/opkele/uris.h
@@ -0,0 +1,10 @@
1#ifndef __OPKELE_URIS_H
2#define __OPKELE_URIS_H
3
4#define NSURI_XRDS "xri://$xrds"
5#define NSURI_XRD "xri://$xrd*($v*2.0)"
6#define NSURI_OPENID10 "http://openid.net/xmlns/1.0"
7
8#define STURI_OPENID10 "http://openid.net/signon/1.0"
9
10#endif /* __OPKELE_URIS_H */
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 7309353..b278faf 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -24,6 +24,7 @@ libopkele_la_SOURCES = \
24 extension.cc \ 24 extension.cc \
25 sreg.cc \ 25 sreg.cc \
26 extension_chain.cc \ 26 extension_chain.cc \
27 curl.cc expat.cc 27 curl.cc expat.cc \
28 openid_service_resolver.cc
28libopkele_la_LDFLAGS = \ 29libopkele_la_LDFLAGS = \
29 -version-info 2:0:0 30 -version-info 2:0:0
diff --git a/lib/openid_service_resolver.cc b/lib/openid_service_resolver.cc
new file mode 100644
index 0000000..5f82955
--- a/dev/null
+++ b/lib/openid_service_resolver.cc
@@ -0,0 +1,294 @@
1#include <cctype>
2#include <opkele/exception.h>
3#include <opkele/util.h>
4#include <opkele/openid_service_resolver.h>
5#include <opkele/uris.h>
6
7#define LOCATION_HEADER "X-XRDS-Location"
8
9namespace opkele {
10 static const char *whitespace = " \t\r\n";
11
12 openid_service_resolver_t::openid_service_resolver_t(const string& xp)
13 : util::curl_t(easy_init()),
14 util::expat_t(0),
15 xri_proxy(xp.empty()?"http://beta.xri.net/":xp)
16 {
17 CURLcode r;
18 (r=misc_sets())
19 || (r=set_write())
20 || (r==set_header())
21 ;
22 if(r)
23 throw opkele::exception_curl(OPKELE_CP_ "failed to set curly options",r);
24 }
25
26 static bool is_element(const XML_Char *n,const char *en) {
27 if(!strcasecmp(n,en)) return true;
28 int nl = strlen(n), enl = strlen(en);
29 if( (nl>=(enl+1)) && n[nl-enl-1]=='\t'
30 && !strcasecmp(&n[nl-enl],en) )
31 return true;
32 return false;
33 }
34 static inline bool is_qelement(const XML_Char *n,const char *qen) {
35 return !strcasecmp(n,qen);
36 }
37 static inline bool is_element(
38 const openid_service_resolver_t::parser_node_t& n,
39 const char *en) {
40 return is_element(n.element.c_str(),en);
41 }
42 static inline bool is_qelement(
43 const openid_service_resolver_t::parser_node_t& n,
44 const char *qen) {
45 return is_qelement(n.element.c_str(),qen);
46 }
47
48 void openid_service_resolver_t::start_element(const XML_Char *n,const XML_Char **a) {
49 if(state!=state_parse) return;
50 tree.push(n,a);
51 parser_node_t& t = tree.top();
52 if(is_element(n,"html") || is_element(n,"head")
53 || is_qelement(n,NSURI_XRDS "\tXRDS")
54 || is_qelement(n,NSURI_XRD "\tXRD") )
55 t.skip_tags = false;
56 else if(is_qelement(n,NSURI_XRD "\tService")
57 || is_qelement(n,NSURI_XRD "\tType")
58 || is_qelement(n,NSURI_XRD "\tURI")
59 || is_qelement(n,NSURI_OPENID10 "\tDelegate")
60 || is_qelement(n,NSURI_XRD "\tCanonicalID") )
61 t.skip_tags = t.skip_text = false;
62 else if(is_element(n,"body"))
63 state = state_stopping_body;
64 }
65 void openid_service_resolver_t::end_element(const XML_Char *n) {
66 if(state!=state_parse) return;
67 assert(tree.top().element == n);
68 pop_tag();
69 }
70 void openid_service_resolver_t::character_data(const XML_Char *s,int l) {
71 if(state!=state_parse) return;
72 if( !( tree.empty() || tree.top().skip_text ) )
73 tree.top().content.append(s,l);
74 }
75
76 static void copy_trim_whitespace(string& to,const string& from) {
77 string::size_type ns0 = from.find_first_not_of(whitespace);
78 if(ns0==string::npos) {
79 to.clear(); return;
80 }
81 string::size_type ns1 = from.find_last_not_of(whitespace);
82 assert(ns1!=string::npos);
83 to.assign(from,ns0,ns1-ns0+1);
84 }
85
86 void openid_service_resolver_t::pop_tag() {
87 assert(!tree.empty());
88 parser_node_t& t = tree.top();
89 if( is_element(t,"meta")
90 && !strcasecmp(t.attrs["http-equiv"].c_str(),LOCATION_HEADER) ) {
91 xrds_location = t.attrs["content"];
92 }else if( is_element(t,"link") ) {
93 parser_node_t::attrs_t::const_iterator ir = t.attrs.find("rel");
94 if(ir!=t.attrs.end()) {
95 const string& rels = ir->second;
96 for(string::size_type ns = rels.find_first_not_of(whitespace);
97 ns!=string::npos;
98 ns=rels.find_first_not_of(whitespace,ns)) {
99 string::size_type s = rels.find_first_of(whitespace,ns);
100 string rel;
101 if(s==string::npos) {
102 rel.assign(rels,ns,string::npos);
103 ns = string::npos;
104 }else{
105 rel.assign(rels,ns,s-ns);
106 ns = s;
107 }
108 if(rel=="openid.server")
109 copy_trim_whitespace(html_SEP.xrd_URI,t.attrs["href"]);
110 else if(rel=="openid.delegate")
111 copy_trim_whitespace(html_SEP.openid_Delegate,t.attrs["href"]);
112 }
113 }
114 }else if( is_element(t,"head") )
115 state = state_stopping_head;
116 else if( is_qelement(t,NSURI_XRD "\tXRD")) {
117 if( !(
118 (
119 xri_mode
120 && t.auth_info.canonical_id.empty()
121 ) ||
122 t.auth_info.auth_SEP.xrd_Type.empty()
123 ) )
124 auth_info = t.auth_info;
125 }else if( tree.size()>1 ) {
126 parser_node_t& p = tree.parent();
127 if( is_qelement(p,NSURI_XRD "\tService") ) {
128 if( is_qelement(t,NSURI_XRD "\tType") ) {
129 if(t.content==STURI_OPENID10) {
130 string tmp; copy_trim_whitespace(tmp,t.content);
131 p.auth_info.auth_SEP.xrd_Type.insert(tmp);
132 }
133 }else if( is_qelement(t,NSURI_XRD "\tURI") )
134 copy_trim_whitespace(p.auth_info.auth_SEP.xrd_URI,t.content);
135 else if( is_qelement(t,NSURI_OPENID10 "\tDelegate") )
136 copy_trim_whitespace(p.auth_info.auth_SEP.openid_Delegate,t.content);
137 }else if( is_qelement(p,NSURI_XRD "\tXRD") ) {
138 if(is_qelement(t,NSURI_XRD "\tService") ) {
139 if( !t.auth_info.auth_SEP.xrd_Type.empty() ) {
140 parser_node_t::attrs_t::const_iterator ip
141 = t.attrs.find("priority");
142 if(ip!=t.attrs.end()) {
143 const char *nptr = ip->second.c_str();
144 char *eptr = 0;
145 t.auth_info.auth_SEP.priority = strtol(nptr,&eptr,10);
146 if(nptr==eptr)
147 t.auth_info.auth_SEP.priority = LONG_MAX;
148 }
149 if( (t.auth_info.auth_SEP.priority < p.auth_info.auth_SEP.priority)
150 || p.auth_info.auth_SEP.xrd_Type.empty() )
151 p.auth_info.auth_SEP = t.auth_info.auth_SEP;
152 }
153 }else if( is_qelement(t,NSURI_XRD "\tCanonicalID") )
154 copy_trim_whitespace(p.auth_info.canonical_id,t.content);
155 }
156 }
157
158 tree.pop();
159 }
160
161 size_t openid_service_resolver_t::write(void *p,size_t s,size_t nm) {
162 if(state != state_parse)
163 return 0;
164 /* TODO: limit total size */
165 size_t bytes = s*nm;
166 parse((const char *)p,bytes,false);
167 return bytes;
168 }
169
170 size_t openid_service_resolver_t::header(void *p,size_t s,size_t nm) {
171 size_t bytes = s*nm;
172 const char *h = (const char *)p;
173 const char *colon = (const char*)memchr(p,':',bytes);
174 const char *space = (const char*)memchr(p,' ',bytes);
175 if(space && ( (!colon) || space<colon ) ) {
176 xrds_location.clear(); http_content_type.clear();
177 }else if(colon) {
178 const char *hv = ++colon;
179 int hnl = colon-h;
180 int rb;
181 for(rb = bytes-hnl-1;
182 rb>0 && isspace(*hv);
183 ++hv,--rb );
184 while(rb>0 && isspace(hv[rb-1]))
185 --rb;
186 if(rb) {
187 if( (hnl >= sizeof(LOCATION_HEADER))
188 && !strncasecmp(h,LOCATION_HEADER ":",
189 sizeof(LOCATION_HEADER)) ) {
190 xrds_location.assign(hv,rb);
191 }else if( (hnl >= sizeof("Content-Type"))
192 && !strncasecmp(h,"Content-Type:",
193 sizeof("Content-Type")) ) {
194 const char *sc = (const char*)memchr(
195 hv,';',rb);
196 http_content_type.assign(
197 hv,sc?(sc-hv):rb );
198 }
199 }
200 }
201 return curl_t::header(p,s,nm);
202 }
203
204 void openid_service_resolver_t::discover_service(const string& url,bool xri) {
205 CURLcode r = easy_setopt(CURLOPT_URL,url.c_str());
206 if(r)
207 throw opkele::exception_curl(OPKELE_CP_ "failed to set curly urlie",r);
208
209 (*(expat_t*)this) = parser_create_ns();
210 set_user_data(); set_element_handler();
211 set_character_data_handler();
212 tree.clear();
213 state = state_parse;
214
215 r = easy_perform();
216 if(r && r!=CURLE_WRITE_ERROR)
217 throw exception_curl(OPKELE_CP_ "failed to perform curly request",r);
218
219 parse(0,0,true);
220 while(!tree.empty()) pop_tag();
221 }
222
223 const openid_auth_info_t& openid_service_resolver_t::resolve(const string& id) {
224 auth_info = openid_auth_info_t();
225 html_SEP = openid_auth_SEP_t();
226
227 string::size_type fns = id.find_first_not_of(whitespace);
228 if(fns==string::npos)
229 throw opkele::bad_input(OPKELE_CP_ "whitespace-only identity");
230 string::size_type lns = id.find_last_not_of(whitespace);
231 assert(lns!=string::npos);
232 if(!strncasecmp(
233 id.c_str()+fns,"xri://",
234 sizeof("xri://")-1))
235 fns+=sizeof("xri://")-1;
236 string nid(id,fns,lns-fns+1);
237 if(nid.empty())
238 throw opkele::bad_input(OPKELE_CP_ "nothing significant in identity");
239 if(strchr("=@+$!(",*nid.c_str())) {
240 discover_service(
241 xri_proxy + util::url_encode(nid) +
242 "?_xrd_t=" STURI_OPENID10 "&_xrd_r=application/xrd+xml;sep=true",
243 true );
244 if(auth_info.canonical_id.empty()
245 || auth_info.auth_SEP.xrd_Type.empty() )
246 throw opkele::failed_lookup(OPKELE_CP_ "no OpenID service for XRI found");
247 return auth_info;
248 }else{
249 const char *np = nid.c_str();
250 if( (strncasecmp(np,"http",4) || strncmp(
251 tolower(*(np+4))=='s'? np+5 : np+4, "://", 3))
252#ifndef NDEBUG
253 && strncasecmp(np,"file:///",sizeof("file:///")-1)
254 #endif /* XXX: or how do I let tests work? */
255 )
256 nid.insert(0,"http://");
257 string::size_type fp = nid.find('#');
258 if(fp!=string::npos) {
259 string::size_type qp = nid.find('?');
260 if(qp==string::npos || qp<fp) {
261 nid.erase(fp);
262 }else if(qp>fp)
263 nid.erase(fp,qp-fp);
264 }
265 discover_service(nid);
266 const char *eu = 0;
267 CURLcode r = easy_getinfo(CURLINFO_EFFECTIVE_URL,&eu);
268 if(r)
269 throw exception_curl(OPKELE_CP_ "failed to get CURLINFO_EFFECTIVE_URL",r);
270 string canonicalized_id = util::rfc_3986_normalize_uri(eu);
271 if(xrds_location.empty()) {
272 if(auth_info.auth_SEP.xrd_Type.empty()) {
273 if(html_SEP.xrd_URI.empty())
274 throw opkele::failed_lookup(OPKELE_CP_ "no OpenID service discovered");
275 auth_info.auth_SEP = html_SEP;
276 auth_info.auth_SEP.xrd_Type.clear(); auth_info.auth_SEP.xrd_Type.insert( STURI_OPENID10 );
277 auth_info.canonical_id = canonicalized_id;
278 }else{
279 if(auth_info.canonical_id.empty())
280 auth_info.canonical_id = canonicalized_id;
281 }
282 return auth_info;
283 }else{
284 discover_service(xrds_location);
285 if(auth_info.auth_SEP.xrd_Type.empty())
286 throw opkele::failed_lookup(OPKELE_CP_ "no OpenID service found in Yadis document");
287 if(auth_info.canonical_id.empty())
288 auth_info.canonical_id = canonicalized_id;
289 return auth_info;
290 }
291 }
292 }
293
294}
diff --git a/lib/util.cc b/lib/util.cc
index ac70938..69d37b5 100644
--- a/lib/util.cc
+++ b/lib/util.cc
@@ -173,21 +173,29 @@ namespace opkele {
173 * - if there's no path component, add '/' 173 * - if there's no path component, add '/'
174 */ 174 */
175 string rfc_3986_normalize_uri(const string& uri) { 175 string rfc_3986_normalize_uri(const string& uri) {
176 static const char *whitespace = " \t\r\n";
176 string rv; 177 string rv;
177 string::size_type colon = uri.find(':'); 178 string::size_type ns = uri.find_first_not_of(whitespace);
179 if(ns==string::npos)
180 throw bad_input(OPKELE_CP_ "Can't normalize empty URI");
181 string::size_type colon = uri.find(':',ns);
178 if(colon==string::npos) 182 if(colon==string::npos)
179 throw bad_input(OPKELE_CP_ "No scheme specified in URI"); 183 throw bad_input(OPKELE_CP_ "No scheme specified in URI");
180 transform( 184 transform(
181 uri.begin(), uri.begin()+colon+1, 185 uri.begin()+ns, uri.begin()+colon+1,
182 back_inserter(rv), ::tolower ); 186 back_inserter(rv), ::tolower );
183 bool s; 187 bool s;
184 if(rv=="http:") 188 if(rv=="http:")
185 s = false; 189 s = false;
186 else if(rv=="https:") 190 else if(rv=="https:")
187 s = true; 191 s = true;
192#ifndef NDEBUG
193 else if(rv=="file:")
194 s = false;
195#endif /* XXX: or try to make tests work some other way */
188 else 196 else
189 throw not_implemented(OPKELE_CP_ "Only http(s) URIs can be normalized here"); 197 throw not_implemented(OPKELE_CP_ "Only http(s) URIs can be normalized here");
190 string::size_type ul = uri.length(); 198 string::size_type ul = uri.find_last_not_of(whitespace)+1;
191 if(ul <= (colon+3)) 199 if(ul <= (colon+3))
192 throw bad_input(OPKELE_CP_ "Unexpected end of URI being normalized encountered"); 200 throw bad_input(OPKELE_CP_ "Unexpected end of URI being normalized encountered");
193 if(uri[colon+1]!='/' || uri[colon+2]!='/') 201 if(uri[colon+1]!='/' || uri[colon+2]!='/')
@@ -196,7 +204,7 @@ namespace opkele {
196 string::size_type interesting = uri.find_first_of(":/#?",colon+3); 204 string::size_type interesting = uri.find_first_of(":/#?",colon+3);
197 if(interesting==string::npos) { 205 if(interesting==string::npos) {
198 transform( 206 transform(
199 uri.begin()+colon+3,uri.end(), 207 uri.begin()+colon+3,uri.begin()+ul,
200 back_inserter(rv), ::tolower ); 208 back_inserter(rv), ::tolower );
201 rv += '/'; return rv; 209 rv += '/'; return rv;
202 } 210 }
diff --git a/test/.gitignore b/test/.gitignore
index 918b3c9..5ce4dc9 100644
--- a/test/.gitignore
+++ b/test/.gitignore
@@ -2,3 +2,4 @@
2/.libs 2/.libs
3/test 3/test
4*.o 4*.o
5/openid_resolve
diff --git a/test/Makefile.am b/test/Makefile.am
index 4b78087..5aa87b3 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -1,4 +1,4 @@
1noinst_PROGRAMS = test 1noinst_PROGRAMS = test openid_resolve
2 2
3DEFAULT_INCLUDES = -I${top_builddir} 3DEFAULT_INCLUDES = -I${top_builddir}
4INCLUDES = -I${top_srcdir}/include/ ${KONFORKA_CFLAGS} 4INCLUDES = -I${top_srcdir}/include/ ${KONFORKA_CFLAGS}
@@ -10,3 +10,6 @@ EXTRA_DIST=$(addsuffix .html,$(addprefix html/, \
10 empty head-in-body hkn-delegate hkn-server hkn in-body \ 10 empty head-in-body hkn-delegate hkn-server hkn in-body \
11 unclosed-head spaced-links spaced-link-attrs 2rels \ 11 unclosed-head spaced-links spaced-link-attrs 2rels \
12 )) 12 ))
13
14openid_resolve_SOURCES = openid_resolve.cc
15openid_resolve_LDADD = ${top_builddir}/lib/libopkele.la
diff --git a/test/openid_resolve.cc b/test/openid_resolve.cc
new file mode 100644
index 0000000..31c7a6a
--- a/dev/null
+++ b/test/openid_resolve.cc
@@ -0,0 +1,36 @@
1#include <iostream>
2#include <stdexcept>
3#include <iterator>
4#include <algorithm>
5using namespace std;
6#include <opkele/exception.h>
7#include <opkele/openid_service_resolver.h>
8
9int main(int argc,char **argv) {
10 try {
11 if(argc<2)
12 throw opkele::exception(OPKELE_CP_ "Please, give me something to resolve");
13 opkele::openid_service_resolver_t resolver;
14 for(int a=1;a<argc;++a) {
15 const opkele::openid_auth_info_t& iai = resolver.resolve(argv[a]);
16 clog
17 << "====================" << endl
18 << "canonical id is " << iai.canonical_id << endl
19 << endl
20 << "service priority is " << iai.auth_SEP.priority << endl
21 << "service types are " ;
22 copy(
23 iai.auth_SEP.xrd_Type.begin(), iai.auth_SEP.xrd_Type.end(),
24 ostream_iterator<string>(clog," ") );
25 clog << endl
26 << "service URI is " << iai.auth_SEP.xrd_URI << endl;
27 if(!iai.auth_SEP.openid_Delegate.empty())
28 clog << "openid:Delegate is " << iai.auth_SEP.openid_Delegate << endl;
29 clog << endl;
30 }
31 }catch(exception& e) {
32 cerr << "oops: " << e.what() << endl;
33 _exit(1);
34 }
35 _exit(0);
36}