summaryrefslogtreecommitdiffabout
path: root/lib
authorMichael Krelin <hacker@klever.net>2007-12-02 21:48:18 (UTC)
committer Michael Krelin <hacker@klever.net>2007-12-02 21:51:08 (UTC)
commit262f1579f0a9138a01f06afea06d00155cefd4b5 (patch) (unidiff)
treefb4db0ee7b679a1957c63abbe6f6af1d2fa82531 /lib
parent73d98f3652b498b9a74b183bef395714c7d73fda (diff)
downloadlibopkele-262f1579f0a9138a01f06afea06d00155cefd4b5.zip
libopkele-262f1579f0a9138a01f06afea06d00155cefd4b5.tar.gz
libopkele-262f1579f0a9138a01f06afea06d00155cefd4b5.tar.bz2
first cut on XRI resolver
This commit adds openid service resolver that does discovery using XRI (proxy only), Yadis protocol and html-based discovery. It uses expat as xml parsing engine, which makes it a bit more strict about html it receives, but I think failing to discover links in *severely* broken html is better than misdetecting links, hidden in comments or such. This is highly experimental code and needs more thoughts and testing. Thanks everyone pushing me towards this development. Namely Joseph, John, Gen. Signed-off-by: Michael Krelin <hacker@klever.net>
Diffstat (limited to 'lib') (more/less context) (ignore whitespace changes)
-rw-r--r--lib/Makefile.am3
-rw-r--r--lib/openid_service_resolver.cc294
-rw-r--r--lib/util.cc16
3 files changed, 308 insertions, 5 deletions
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 }