-rw-r--r-- | lib/Makefile.am | 11 | ||||
-rw-r--r-- | lib/consumer.cc | 20 | ||||
-rw-r--r-- | lib/discovery.cc | 446 | ||||
-rw-r--r-- | lib/exception.cc | 22 | ||||
-rw-r--r-- | lib/expat.cc | 96 | ||||
-rw-r--r-- | lib/params.cc | 13 | ||||
-rw-r--r-- | lib/secret.cc | 12 | ||||
-rw-r--r-- | lib/sreg.cc | 2 | ||||
-rw-r--r-- | lib/util.cc | 55 |
9 files changed, 629 insertions, 48 deletions
diff --git a/lib/Makefile.am b/lib/Makefile.am index 0fe705a..989de28 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -1,28 +1,31 @@ lib_LTLIBRARIES = libopkele.la +AM_CPPFLAGS = ${CPPFLAGS_DEBUG} +DEFAULT_INCLUDES = -I${top_builddir} INCLUDES = \ -I${top_srcdir}/include/ \ ${KONFORKA_CFLAGS} \ ${OPENSSL_CFLAGS} \ ${LIBCURL_CPPFLAGS} \ - ${PCRE_CFLAGS} + ${PCRE_CFLAGS} ${EXPAT_CFLAGS} ${TIDY_CFLAGS} libopkele_la_LIBADD = \ ${LIBCURL} \ - ${PCRE_LIBS} \ + ${PCRE_LIBS} ${EXPAT_LIBS} \ ${OPENSSL_LIBS} \ - ${KONFORKA_LIBS} + ${KONFORKA_LIBS} ${TIDY_LIBS} libopkele_la_SOURCES = \ params.cc \ util.cc \ server.cc \ secret.cc \ data.cc \ consumer.cc \ exception.cc \ extension.cc \ sreg.cc \ extension_chain.cc \ - curl.cc + curl.cc expat.cc \ + discovery.cc libopkele_la_LDFLAGS = \ -version-info 2:0:0 diff --git a/lib/consumer.cc b/lib/consumer.cc index 9f7530f..3c3b4f8 100644 --- a/lib/consumer.cc +++ b/lib/consumer.cc @@ -1,230 +1,212 @@ #include <algorithm> #include <cassert> #include <cstring> #include <opkele/util.h> #include <opkele/curl.h> #include <opkele/exception.h> #include <opkele/data.h> #include <opkele/consumer.h> #include <openssl/sha.h> #include <openssl/hmac.h> #include <iostream> #include "config.h" #include <pcre.h> namespace opkele { using namespace std; using util::curl_t; - - template<int lim> - class curl_fetch_string_t : public curl_t { - public: - curl_fetch_string_t(CURL *c) - : curl_t(c) { } - ~curl_fetch_string_t() throw() { } - - string response; - - size_t write(void *p,size_t size,size_t nmemb) { - size_t bytes = size*nmemb; - size_t get = min(lim-response.length(),bytes); - response.append((const char *)p,get); - return get; - } - }; - - typedef curl_fetch_string_t<16384> curl_pick_t; + using util::curl_pick_t; class pcre_matches_t { public: int *_ov; int _s; pcre_matches_t() : _ov(0), _s(0) { } pcre_matches_t(int s) : _ov(0), _s(s) { if(_s&1) ++_s; _s += _s>>1; _ov = new int[_s]; } ~pcre_matches_t() throw() { if(_ov) delete[] _ov; } int begin(int i) const { return _ov[i<<1]; } int end(int i) const { return _ov[(i<<1)+1]; } int length(int i) const { int t=i<<1; return _ov[t+1]-_ov[t]; } }; class pcre_t { public: pcre *_p; pcre_t() : _p(0) { } pcre_t(pcre *p) : _p(p) { } pcre_t(const char *re,int opts) : _p(0) { static const char *errptr; static int erroffset; _p = pcre_compile(re,opts,&errptr,&erroffset,NULL); if(!_p) throw internal_error(OPKELE_CP_ string("Failed to compile regexp: ")+errptr); } ~pcre_t() throw() { if(_p) (*pcre_free)(_p); } pcre_t& operator=(pcre *p) { if(_p) (*pcre_free)(_p); _p=p; return *this; } operator const pcre*(void) const { return _p; } operator pcre*(void) { return _p; } int exec(const string& s,pcre_matches_t& m) { if(!_p) throw internal_error(OPKELE_CP_ "Trying to execute absent regexp"); return pcre_exec(_p,NULL,s.c_str(),s.length(),0,0,m._ov,m._s); } }; assoc_t consumer_t::associate(const string& server) { util::dh_t dh = DH_new(); if(!dh) throw exception_openssl(OPKELE_CP_ "failed to DH_new()"); dh->p = util::dec_to_bignum(data::_default_p); dh->g = util::dec_to_bignum(data::_default_g); if(!DH_generate_key(dh)) throw exception_openssl(OPKELE_CP_ "failed to DH_generate_key()"); string request = "openid.mode=associate" "&openid.assoc_type=HMAC-SHA1" "&openid.session_type=DH-SHA1" "&openid.dh_consumer_public="; request += util::url_encode(util::bignum_to_base64(dh->pub_key)); curl_pick_t curl = curl_pick_t::easy_init(); if(!curl) throw exception_curl(OPKELE_CP_ "failed to initialize curl"); CURLcode r; (r=curl.misc_sets()) || (r=curl.easy_setopt(CURLOPT_URL,server.c_str())) || (r=curl.easy_setopt(CURLOPT_POST,1)) || (r=curl.easy_setopt(CURLOPT_POSTFIELDS,request.data())) || (r=curl.easy_setopt(CURLOPT_POSTFIELDSIZE,request.length())) || (r=curl.set_write()) ; if(r) throw exception_curl(OPKELE_CP_ "failed to set curly options",r); if( (r=curl.easy_perform()) ) throw exception_curl(OPKELE_CP_ "failed to perform curly request",r); params_t p; p.parse_keyvalues(curl.response); if(p.has_param("assoc_type") && p.get_param("assoc_type")!="HMAC-SHA1") throw bad_input(OPKELE_CP_ "unsupported assoc_type"); string st; if(p.has_param("session_type")) st = p.get_param("session_type"); if((!st.empty()) && st!="DH-SHA1") throw bad_input(OPKELE_CP_ "unsupported session_type"); secret_t secret; if(st.empty()) { secret.from_base64(p.get_param("mac_key")); }else{ util::bignum_t s_pub = util::base64_to_bignum(p.get_param("dh_server_public")); vector<unsigned char> ck(DH_size(dh)+1); unsigned char *ckptr = &(ck.front())+1; int cklen = DH_compute_key(ckptr,s_pub,dh); if(cklen<0) throw exception_openssl(OPKELE_CP_ "failed to DH_compute_key()"); if(cklen && (*ckptr)&0x80) { (*(--ckptr)) = 0; ++cklen; } unsigned char key_sha1[SHA_DIGEST_LENGTH]; SHA1(ckptr,cklen,key_sha1); secret.enxor_from_base64(key_sha1,p.get_param("enc_mac_key")); } int expires_in = 0; if(p.has_param("expires_in")) { expires_in = util::string_to_long(p.get_param("expires_in")); }else if(p.has_param("issued") && p.has_param("expiry")) { expires_in = util::w3c_to_time(p.get_param("expiry"))-util::w3c_to_time(p.get_param("issued")); }else throw bad_input(OPKELE_CP_ "no expiration information"); return store_assoc(server,p.get_param("assoc_handle"),secret,expires_in); } string consumer_t::checkid_immediate(const string& identity,const string& return_to,const string& trust_root,extension_t *ext) { return checkid_(mode_checkid_immediate,identity,return_to,trust_root,ext); } string consumer_t::checkid_setup(const string& identity,const string& return_to,const string& trust_root,extension_t *ext) { return checkid_(mode_checkid_setup,identity,return_to,trust_root,ext); } string consumer_t::checkid_(mode_t mode,const string& identity,const string& return_to,const string& trust_root,extension_t *ext) { params_t p; if(mode==mode_checkid_immediate) p["mode"]="checkid_immediate"; else if(mode==mode_checkid_setup) p["mode"]="checkid_setup"; else throw bad_input(OPKELE_CP_ "unknown checkid_* mode"); string iurl = canonicalize(identity); string server, delegate; retrieve_links(iurl,server,delegate); p["identity"] = delegate.empty()?iurl:delegate; if(!trust_root.empty()) p["trust_root"] = trust_root; p["return_to"] = return_to; try { string ah = find_assoc(server)->handle(); p["assoc_handle"] = ah; }catch(failed_lookup& fl) { string ah = associate(server)->handle(); p["assoc_handle"] = ah; } if(ext) ext->checkid_hook(p,identity); return p.append_query(server); } void consumer_t::id_res(const params_t& pin,const string& identity,extension_t *ext) { if(pin.has_param("openid.user_setup_url")) throw id_res_setup(OPKELE_CP_ "assertion failed, setup url provided",pin.get_param("openid.user_setup_url")); string server,delegate; retrieve_links(identity.empty()?pin.get_param("openid.identity"):canonicalize(identity),server,delegate); params_t ps; try { assoc_t assoc = retrieve_assoc(server,pin.get_param("openid.assoc_handle")); if(assoc->is_expired()) throw id_res_expired_on_delivery(OPKELE_CP_ "retrieve_assoc() has returned expired handle"); const string& sigenc = pin.get_param("openid.sig"); vector<unsigned char> sig; util::decode_base64(sigenc,sig); const string& slist = pin.get_param("openid.signed"); string kv; string::size_type p = 0; while(true) { string::size_type co = slist.find(',',p); string f = (co==string::npos)?slist.substr(p):slist.substr(p,co-p); kv += f; kv += ':'; f.insert(0,"openid."); kv += pin.get_param(f); kv += '\n'; if(ext) ps[f.substr(sizeof("openid.")-1)] = pin.get_param(f); if(co==string::npos) break; p = co+1; } secret_t secret = assoc->secret(); unsigned int md_len = 0; unsigned char *md = HMAC( EVP_sha1(), &(secret.front()),secret.size(), (const unsigned char *)kv.data(),kv.length(), 0,&md_len); if(sig.size()!=md_len || memcmp(&(sig.front()),md,md_len)) throw id_res_mismatch(OPKELE_CP_ "signature mismatch"); }catch(failed_lookup& e) { const string& slist = pin.get_param("openid.signed"); string::size_type pp = 0; params_t p; while(true) { string::size_type co = slist.find(',',pp); string f = "openid."; f += (co==string::npos)?slist.substr(pp):slist.substr(pp,co-pp); p[f] = pin.get_param(f); if(co==string::npos) break; pp = co+1; } p["openid.assoc_handle"] = pin.get_param("openid.assoc_handle"); diff --git a/lib/discovery.cc b/lib/discovery.cc new file mode 100644 index 0000000..d868308 --- a/dev/null +++ b/lib/discovery.cc @@ -0,0 +1,446 @@ +#include <list> +#include <opkele/curl.h> +#include <opkele/expat.h> +#include <opkele/uris.h> +#include <opkele/discovery.h> +#include <opkele/exception.h> +#include <opkele/util.h> +#include <opkele/tidy.h> +#include <opkele/debug.h> + +#include "config.h" + +#define XRDS_HEADER "X-XRDS-Location" +#define CT_HEADER "Content-Type" + +namespace opkele { + using std::list; + using xrd::XRD_t; + using xrd::service_t; + + static const char *whitespace = " \t\r\n"; + static const char *i_leaders = "=@+$!("; + static const size_t max_html = 16384; + + static inline bool is_qelement(const XML_Char *n,const char *qen) { + return !strcasecmp(n,qen); + } + static inline bool is_element(const XML_Char *n,const char *en) { + if(!strcasecmp(n,en)) return true; + int nl = strlen(n), enl = strlen(en); + if( (nl>=(enl+1)) && n[nl-enl-1]=='\t' + && !strcasecmp(&n[nl-enl],en) ) + return true; + return false; + } + + static long element_priority(const XML_Char **a) { + for(;*a;++a) + if(!strcasecmp(*(a++),"priority")) { + long rv; + return (sscanf(*a,"%ld",&rv)==1)?rv:-1; + } + return -1; + } + + class idigger_t : public util::curl_t, public util::expat_t { + public: + string xri_proxy; + + enum { + xmode_html = 1, xmode_xrd = 2 + }; + int xmode; + + string xrds_location; + string http_content_type; + service_t html_openid1; + service_t html_openid2; + string cdata_buf; + long status_code; + string status_string; + + typedef list<string> pt_stack_t; + pt_stack_t pt_stack; + int skipping; + bool parser_choked; + string save_html; + + XRD_t *xrd; + service_t *xrd_service; + string* cdata; + + idigger_t() + : util::curl_t(easy_init()), + util::expat_t(0), + xri_proxy(XRI_PROXY_URL) { + CURLcode r; + (r=misc_sets()) + || (r=set_write()) + || (r=set_header()) + ; + if(r) + throw exception_curl(OPKELE_CP_ "failed to set curly options",r); + } + ~idigger_t() throw() { } + + void discover(idiscovery_t& result,const string& identity) { + result.clear(); + string::size_type fsc = identity.find_first_not_of(whitespace); + if(fsc==string::npos) + throw bad_input(OPKELE_CP_ "whtiespace-only identity"); + string::size_type lsc = identity.find_last_not_of(whitespace); + assert(lsc!=string::npos); + if(!strncasecmp(identity.c_str()+fsc,"xri://",sizeof("xri://")-1)) + fsc += sizeof("xri://")-1; + if((fsc+1)>=lsc) + throw bad_input(OPKELE_CP_ "not a character of importance in identity"); + string id(identity,fsc,lsc-fsc+1); + if(strchr(i_leaders,id[0])) { + result.normalized_id = id; + result.xri_identity = true; + /* TODO: further canonicalize xri identity? Like folding case or whatever... */ + discover_at( + result, + xri_proxy + util::url_encode(id)+ + "?_xrd_r=application/xrd+xml;sep=false", xmode_xrd); + if(status_code!=100) + throw failed_xri_resolution(OPKELE_CP_ + "XRI resolution failed with '"+status_string+"' message",status_code); + if(result.xrd.canonical_ids.empty()) + throw opkele::failed_discovery(OPKELE_CP_ "No CanonicalID for XRI identity found"); + result.canonicalized_id = result.xrd.canonical_ids.begin()->second; + }else{ + result.xri_identity = false; + if(id.find("://")==string::npos) + id.insert(0,"http://"); + string::size_type fp = id.find('#'); + if(fp!=string::npos) { + string::size_type qp = id.find('?'); + if(qp==string::npos || qp<fp) + id.erase(fp); + else if(qp>fp) + id.erase(fp,qp-fp); + } + result.normalized_id = util::rfc_3986_normalize_uri(id); + discover_at(result,id,xmode_html|xmode_xrd); + const char * eu = 0; + CURLcode r = easy_getinfo(CURLINFO_EFFECTIVE_URL,&eu); + if(r) + throw exception_curl(OPKELE_CP_ "failed to get CURLINFO_EFFECTIVE_URL",r); + result.canonicalized_id = util::rfc_3986_normalize_uri(eu); /* XXX: strip fragment part? */ + if(xrds_location.empty()) { + html2xrd(result.xrd); + }else{ + discover_at(result,xrds_location,xmode_xrd); + if(result.xrd.empty()) + html2xrd(result.xrd); + } + } + } + + void discover_at(idiscovery_t& result,const string& url,int xm) { + CURLcode r = easy_setopt(CURLOPT_URL,url.c_str()); + if(r) + throw exception_curl(OPKELE_CP_ "failed to set culry urlie",r); + + http_content_type.clear(); + xmode = xm; + prepare_to_parse(); + if(xmode&xmode_html) { + xrds_location.clear(); + save_html.clear(); + save_html.reserve(max_html); + } + xrd = &result.xrd; + + r = easy_perform(); + if(r && r!=CURLE_WRITE_ERROR) + throw exception_curl(OPKELE_CP_ "failed to perform curly request",r); + + if(!parser_choked) { + parse(0,0,true); + }else{ + /* TODO: do not bother if we've seen xml */ + try { + util::tidy_doc_t td = util::tidy_doc_t::create(); + if(!td) + throw exception_tidy(OPKELE_CP_ "failed to create htmltidy document"); +#ifndef NDEBUG + td.opt_set(TidyQuiet,false); + td.opt_set(TidyShowWarnings,false); +#endif /* NDEBUG */ + td.opt_set(TidyForceOutput,true); + td.opt_set(TidyXhtmlOut,true); + td.opt_set(TidyDoctypeMode,TidyDoctypeOmit); + td.opt_set(TidyMark,false); + if(td.parse_string(save_html)<=0) + throw exception_tidy(OPKELE_CP_ "tidy failed to parse document"); + if(td.clean_and_repair()<=0) + throw exception_tidy(OPKELE_CP_ "tidy failed to clean and repair"); + util::tidy_buf_t tide; + if(td.save_buffer(tide)<=0) + throw exception_tidy(OPKELE_CP_ "tidy failed to save buffer"); + prepare_to_parse(); + parse(tide.c_str(),tide.size(),true); + }catch(exception_tidy& et) { } + } + save_html.clear(); + } + + void prepare_to_parse() { + (*(expat_t*)this) = parser_create_ns(); + set_user_data(); set_element_handler(); + set_character_data_handler(); + + if(xmode&xmode_html) { + html_openid1.clear(); html_openid2.clear(); + parser_choked = false; + } + + cdata = 0; xrd_service = 0; skipping = 0; + status_code = 100; status_string.clear(); + } + + void html2xrd(XRD_t& x) { + if(!html_openid1.uris.empty()) { + html_openid1.types.insert(STURI_OPENID11); + x.services.add(-1,html_openid1); + } + if(!html_openid2.uris.empty()) { + html_openid2.types.insert(STURI_OPENID20); + x.services.add(-1,html_openid2); + } + } + + size_t write(void *p,size_t s,size_t nm) { + /* TODO: limit total size */ + size_t bytes = s*nm; + const char *inbuf = (const char*)p; + if(xmode&xmode_html) { + size_t mbts = save_html.capacity()-save_html.size(); + size_t bts = 0; + if(mbts>0) { + bts = (bytes>mbts)?mbts:bytes; + save_html.append(inbuf,bts); + } + if(skipping<0) return bts; + } + if(skipping<0) return 0; + bool rp = parse(inbuf,bytes,false); + if(!rp) { + parser_choked = true; + skipping = -1; + if(!(xmode&xmode_html)) + bytes = 0; + } + return bytes; + } + size_t header(void *p,size_t s,size_t nm) { + size_t bytes = s*nm; + const char *h = (const char*)p; + const char *colon = (const char*)memchr(p,':',bytes); + const char *space = (const char*)memchr(p,' ',bytes); + if(space && ( (!colon) || space<colon ) ) { + xrds_location.clear(); http_content_type.clear(); + }else if(colon) { + const char *hv = ++colon; + int hnl = colon-h; + int rb; + for(rb = bytes-hnl-1;rb>0 && isspace(*hv);++hv,--rb); + while(rb>0 && isspace(hv[rb-1])) --rb; + if(rb) { + if( (hnl>=sizeof(XRDS_HEADER)) + && !strncasecmp(h,XRDS_HEADER":", + sizeof(XRDS_HEADER)) ) { + xrds_location.assign(hv,rb); + }else if( (hnl>=sizeof(CT_HEADER)) + && !strncasecmp(h,CT_HEADER":", + sizeof(CT_HEADER)) ) { + const char *sc = (const char*)memchr( + hv,';',rb); + http_content_type.assign(hv,sc?(sc-hv):rb); + } + } + } + return curl_t::header(p,s,nm); + } + + void start_element(const XML_Char *n,const XML_Char **a) { + if(skipping<0) return; + if(skipping) { + if(xmode&xmode_html) + html_start_element(n,a); + ++skipping; return; + } + if(pt_stack.empty()) { + if(is_qelement(n,NSURI_XRDS "\tXRDS")) + return; + if(is_qelement(n,NSURI_XRD "\tXRD")) { + assert(xrd); + xrd->clear(); + pt_stack.push_back(n); + }else if(xmode&xmode_html) { + html_start_element(n,a); + }else{ + skipping = -1; + } + }else{ + int pt_s = pt_stack.size(); + if(pt_s==1) { + if(is_qelement(n,NSURI_XRD "\tCanonicalID")) { + assert(xrd); + cdata = &(xrd->canonical_ids.add(element_priority(a),string())); + }else if(is_qelement(n,NSURI_XRD "\tLocalID")) { + assert(xrd); + cdata = &(xrd->local_ids.add(element_priority(a),string())); + }else if(is_qelement(n,NSURI_XRD "\tProviderID")) { + assert(xrd); + cdata = &(xrd->provider_id); + }else if(is_qelement(n,NSURI_XRD "\tService")) { + assert(xrd); + xrd_service = &(xrd->services.add(element_priority(a), + service_t())); + pt_stack.push_back(n); + }else if(is_qelement(n,NSURI_XRD "\tStatus")) { + for(;*a;) { + if(!strcasecmp(*(a++),"code")) { + if(sscanf(*(a++),"%ld",&status_code)==1 && status_code!=100) { + cdata = &status_string; + pt_stack.push_back(n); + break; + } + } + } + }else if(is_qelement(n,NSURI_XRD "\tExpires")) { + assert(xrd); + cdata_buf.clear(); + cdata = &cdata_buf; + }else if(xmode&xmode_html) { + html_start_element(n,a); + }else{ + skipping = 1; + } + }else if(pt_s==2) { + if(is_qelement(pt_stack.back().c_str(), NSURI_XRD "\tService")) { + if(is_qelement(n,NSURI_XRD "\tType")) { + assert(xrd); assert(xrd_service); + cdata_buf.clear(); + cdata = &cdata_buf; + }else if(is_qelement(n,NSURI_XRD "\tURI")) { + assert(xrd); assert(xrd_service); + cdata = &(xrd_service->uris.add(element_priority(a),string())); + }else if(is_qelement(n,NSURI_XRD "\tLocalID") + || is_qelement(n,NSURI_OPENID10 "\tDelegate") ) { + assert(xrd); assert(xrd_service); + cdata = &(xrd_service->local_ids.add(element_priority(a),string())); + }else if(is_qelement(n,NSURI_XRD "\tProviderID")) { + assert(xrd); assert(xrd_service); + cdata = &(xrd_service->provider_id); + }else{ + skipping = 1; + } + }else + skipping = 1; + }else if(xmode&xmode_html) { + html_start_element(n,a); + }else{ + skipping = 1; + } + } + } + void end_element(const XML_Char *n) { + if(skipping<0) return; + if(skipping) { + --skipping; return; + } + if(is_qelement(n,NSURI_XRD "\tType")) { + assert(xrd); assert(xrd_service); assert(cdata==&cdata_buf); + xrd_service->types.insert(cdata_buf); + }else if(is_qelement(n,NSURI_XRD "\tService")) { + assert(xrd); assert(xrd_service); + assert(!pt_stack.empty()); + assert(pt_stack.back()==(NSURI_XRD "\tService")); + pt_stack.pop_back(); + xrd_service = 0; + }else if(is_qelement(n,NSURI_XRD "\tStatus")) { + assert(xrd); + if(is_qelement(pt_stack.back().c_str(),n)) { + assert(cdata==&status_string); + pt_stack.pop_back(); + if(status_code!=100) + skipping = -1; + } + }else if(is_qelement(n,NSURI_XRD "\tExpires")) { + assert(xrd); + xrd->expires = util::w3c_to_time(cdata_buf); + }else if((xmode&xmode_html) && is_element(n,"head")) { + skipping = -1; + } + cdata = 0; + } + void character_data(const XML_Char *s,int l) { + if(skipping) return; + if(cdata) cdata->append(s,l); + } + + void html_start_element(const XML_Char *n,const XML_Char **a) { + if(is_element(n,"meta")) { + bool heq = false; + string l; + for(;*a;a+=2) { + if(!( strcasecmp(a[0],"http-equiv") + || strcasecmp(a[1],XRDS_HEADER) )) + heq = true; + else if(!strcasecmp(a[0],"content")) + l.assign(a[1]); + } + if(heq) + xrds_location = l; + }else if(is_element(n,"link")) { + string rels; + string href; + for(;*a;a+=2) { + if( !strcasecmp(a[0],"rel") ) { + rels.assign(a[1]); + }else if( !strcasecmp(a[0],"href") ) { + const char *ns = a[1]; + for(;*ns && isspace(*ns);++ns); + href.assign(ns); + string::size_type lns=href.find_last_not_of(whitespace); + href.erase(lns+1); + } + } + for(string::size_type ns=rels.find_first_not_of(whitespace); + ns!=string::npos; ns=rels.find_first_not_of(whitespace,ns)) { + string::size_type s = rels.find_first_of(whitespace,ns); + string rel; + if(s==string::npos) { + rel.assign(rels,ns,string::npos); + ns = string::npos; + }else{ + rel.assign(rels,ns,s-ns); + ns = s; + } + if(rel=="openid.server") + html_openid1.uris.add(-1,href); + else if(rel=="openid.delegate") + html_openid1.local_ids.add(-1,href); + else if(rel=="openid2.provider") + html_openid2.uris.add(-1,href); + else if(rel=="openid2.local_id") + html_openid2.local_ids.add(-1,href); + } + }else if(is_element(n,"body")) { + skipping = -1; + } + } + + }; + + void idiscover(idiscovery_t& result,const string& identity) { + idigger_t idigger; + idigger.discover(result,identity); + } + +} diff --git a/lib/exception.cc b/lib/exception.cc index 510982e..e32594b 100644 --- a/lib/exception.cc +++ b/lib/exception.cc @@ -1,29 +1,51 @@ #include <openssl/err.h> #include <curl/curl.h> #include <opkele/exception.h> +#include <opkele/debug.h> namespace opkele { # ifndef OPKELE_HAVE_KONFORKA + exception::exception(const string& w) + : _what(w) + { + DOUT_("throwing exception(\""<<w<<"\")"); + } + exception::~exception() throw() { } const char *exception::what() const throw() { return _what.c_str(); } + +# else + exception::exception(const string& fi,const string& fu,int l,const string& w) + : konforka::exception(fi,fu,l,w) + { + DOUT_("throwing exception(\""<<w<<"\")"); + DOUT_(" from "<<fi<<':'<<fu<<':'<<l); + } + # endif exception_openssl::exception_openssl(OPKELE_E_PARS) : exception(OPKELE_E_CONS_ w+" ["+ERR_error_string(ERR_peek_last_error(),0)+']'), _error(ERR_peek_last_error()), _ssl_string(ERR_error_string(_error,0)) { } exception_curl::exception_curl(OPKELE_E_PARS) : exception_network(OPKELE_E_CONS), _error(CURLE_OK) { } exception_curl::exception_curl(OPKELE_E_PARS,CURLcode e) : exception_network(OPKELE_E_CONS_ w+" ["+curl_easy_strerror(e)+']'), _error(e), _curl_string(curl_easy_strerror(e)) { } + exception_tidy::exception_tidy(OPKELE_E_PARS) + : exception(OPKELE_E_CONS), _rc(0) { } + exception_tidy::exception_tidy(OPKELE_E_PARS,int r) + : exception(OPKELE_E_CONS), + _rc(r) { } + } diff --git a/lib/expat.cc b/lib/expat.cc new file mode 100644 index 0000000..fa6fdde --- a/dev/null +++ b/lib/expat.cc @@ -0,0 +1,96 @@ +#include <opkele/expat.h> + +namespace opkele { + + namespace util { + + expat_t::~expat_t() throw() { + if(_x) + XML_ParserFree(_x); + } + + expat_t& expat_t::operator=(XML_Parser x) { + if(_x) + XML_ParserFree(_x); + _x = x; + } + + static void _start_element(void* ud,const XML_Char *n,const XML_Char **a) { + ((expat_t*)ud)->start_element(n,a); + } + static void _end_element(void *ud,const XML_Char *n) { + ((expat_t*)ud)->end_element(n); + } + + void expat_t::set_element_handler() { + assert(_x); + XML_SetElementHandler(_x,_start_element,_end_element); + } + + static void _character_data(void *ud,const XML_Char *s,int l) { + ((expat_t*)ud)->character_data(s,l); + } + + void expat_t::set_character_data_handler() { + assert(_x); + XML_SetCharacterDataHandler(_x,_character_data); + } + + static void _processing_instruction(void *ud,const XML_Char *t,const XML_Char *d) { + ((expat_t*)ud)->processing_instruction(t,d); + } + + void expat_t::set_processing_instruction_handler() { + assert(_x); + XML_SetProcessingInstructionHandler(_x,_processing_instruction); + } + + static void _comment(void *ud,const XML_Char *d) { + ((expat_t*)ud)->comment(d); + } + + void expat_t::set_comment_handler() { + assert(_x); + XML_SetCommentHandler(_x,_comment); + } + + static void _start_cdata_section(void *ud) { + ((expat_t*)ud)->start_cdata_section(); + } + static void _end_cdata_section(void *ud) { + ((expat_t*)ud)->end_cdata_section(); + } + + void expat_t::set_cdata_section_handler() { + assert(_x); + XML_SetCdataSectionHandler(_x,_start_cdata_section,_end_cdata_section); + } + + static void _default_handler(void *ud,const XML_Char *s,int l) { + ((expat_t*)ud)->default_handler(s,l); + } + + void expat_t::set_default_handler() { + assert(_x); + XML_SetDefaultHandler(_x,_default_handler); + } + void expat_t::set_default_handler_expand() { + assert(_x); + XML_SetDefaultHandlerExpand(_x,_default_handler); + } + + static void _start_namespace_decl(void *ud,const XML_Char *p,const XML_Char *u) { + ((expat_t*)ud)->start_namespace_decl(p,u); + } + static void _end_namespace_decl(void *ud,const XML_Char *p) { + ((expat_t*)ud)->end_namespace_decl(p); + } + + void expat_t::set_namespace_decl_handler() { + assert(_x); + XML_SetNamespaceDeclHandler(_x,_start_namespace_decl,_end_namespace_decl); + } + + } + +} diff --git a/lib/params.cc b/lib/params.cc index ea86d3a..7a572c1 100644 --- a/lib/params.cc +++ b/lib/params.cc @@ -1,108 +1,121 @@ #include <opkele/types.h> #include <opkele/exception.h> #include <opkele/util.h> #include <openssl/sha.h> #include <openssl/hmac.h> #include "config.h" namespace opkele { using namespace std; bool params_t::has_param(const string& n) const { return find(n)!=end(); } const string& params_t::get_param(const string& n) const { const_iterator i = find(n); if(i==end()) throw failed_lookup(OPKELE_CP_ n+": no such parameter"); return i->second; } string& params_t::get_param(const string& n) { iterator i = find(n); if(i==end()) throw failed_lookup(OPKELE_CP_ n+": no such parameter"); return i->second; } void params_t::parse_keyvalues(const string& kv) { clear(); string::size_type p = 0; while(true) { string::size_type co = kv.find(':',p); if(co==string::npos) break; #ifndef POSTELS_LAW string::size_type nl = kv.find('\n',co+1); if(nl==string::npos) throw bad_input(OPKELE_CP_ "malformed input"); if(nl>co) insert(value_type(kv.substr(p,co-p),kv.substr(co+1,nl-co-1))); p = nl+1; #else /* POSTELS_LAW */ string::size_type lb = kv.find_first_of("\r\n",co+1); if(lb==string::npos) { insert(value_type(kv.substr(p,co-p),kv.substr(co+1))); break; } if(lb>co) insert(value_type(kv.substr(p,co-p),kv.substr(co+1,lb-co-1))); string::size_type nolb = kv.find_first_not_of("\r\n",lb); if(nolb==string::npos) break; p = nolb; #endif /* POSTELS_LAW */ } } void params_t::sign(secret_t secret,string& sig,const string& slist,const char *prefix) const { string kv; string::size_type p = 0; while(true) { string::size_type co = slist.find(',',p); string f = (co==string::npos)?slist.substr(p):slist.substr(p,co-p); kv += f; kv += ':'; if(prefix) f.insert(0,prefix); kv += get_param(f); kv += '\n'; if(co==string::npos) break; p = co+1; } unsigned int md_len = 0; unsigned char *md = HMAC( EVP_sha1(), &(secret.front()),secret.size(), (const unsigned char *)kv.data(),kv.length(), 0,&md_len); sig = util::encode_base64(md,md_len); } string params_t::append_query(const string& url,const char *prefix) const { string rv = url; bool p = true; if(rv.find('?')==string::npos) { rv += '?'; p = false; } for(const_iterator i=begin();i!=end();++i) { if(p) rv += '&'; else p = true; rv += prefix; rv += i->first; rv += '='; rv += util::url_encode(i->second); } return rv; } + string params_t::query_string(const char *prefix) const { + string rv; + for(const_iterator i=begin();i!=end();++i) { + if(!rv.empty()) + rv += '&'; + rv += prefix; + rv += i->first; + rv += '='; + rv += util::url_encode(i->second); + } + return rv; + } + ostream& operator << (ostream& o,const params_t& p) { for(params_t::const_iterator i=p.begin();i!=p.end();++i) o << i->first << ':' << i->second << '\n'; return o; } } diff --git a/lib/secret.cc b/lib/secret.cc index 632a2ca..d538890 100644 --- a/lib/secret.cc +++ b/lib/secret.cc @@ -1,49 +1,45 @@ #include <algorithm> #include <functional> #include <opkele/types.h> #include <opkele/exception.h> #include <opkele/util.h> namespace opkele { using namespace std; template<class __a1,class __a2,class __r> struct bitwise_xor : public binary_function<__a1,__a2,__r> { __r operator() (const __a1& a1,const __a2& a2) const { return a1^a2; } }; - void secret_t::enxor_to_base64(const unsigned char *key_sha1,string& rv) const { - if(size()!=20) - throw bad_input(OPKELE_CP_ "wrong secret size"); + void secret_t::enxor_to_base64(const unsigned char *key_d,string& rv) const { vector<unsigned char> tmp; transform( begin(), end(), - key_sha1, + key_d, back_insert_iterator<vector<unsigned char> >(tmp), bitwise_xor<unsigned char,unsigned char,unsigned char>() ); rv = util::encode_base64(&(tmp.front()),tmp.size()); } - void secret_t::enxor_from_base64(const unsigned char *key_sha1,const string& b64) { + void secret_t::enxor_from_base64(const unsigned char *key_d,const string& b64) { clear(); util::decode_base64(b64,*this); transform( begin(), end(), - key_sha1, + key_d, begin(), bitwise_xor<unsigned char,unsigned char,unsigned char>() ); } void secret_t::to_base64(string& rv) const { - if(size()!=20) - throw bad_input(OPKELE_CP_ "wrong secret size"); rv = util::encode_base64(&(front()),size()); } void secret_t::from_base64(const string& b64) { util::decode_base64(b64,*this); } } diff --git a/lib/sreg.cc b/lib/sreg.cc index 60dc691..03edf57 100644 --- a/lib/sreg.cc +++ b/lib/sreg.cc @@ -1,124 +1,126 @@ #include <opkele/exception.h> #include <opkele/sreg.h> +#include <opkele/uris.h> #include <algorithm> namespace opkele { using std::find; static const struct _sreg_field { const char *fieldname; sreg_t::fieldbit_t fieldbit; } fields[] = { { "nickname", sreg_t::field_nickname }, { "email", sreg_t::field_email }, { "fullname", sreg_t::field_fullname }, { "dob", sreg_t::field_dob }, { "gender", sreg_t::field_gender }, { "postcode", sreg_t::field_postcode }, { "country", sreg_t::field_country }, { "language", sreg_t::field_language }, { "timezone", sreg_t::field_timezone } }; # define fields_BEGIN fields # define fields_END &fields[sizeof(fields)/sizeof(*fields)] typedef const struct _sreg_field *fields_iterator; bool operator==(const struct _sreg_field& fd,const string& fn) { return fd.fieldname==fn; } void sreg_t::checkid_hook(params_t& p,const string& /* identity */) { string fr, fo; for(fields_iterator f=fields_BEGIN;f<fields_END;++f) { if(f->fieldbit&fields_required) { if(!fr.empty()) fr+=","; fr += f->fieldname; } if(f->fieldbit&fields_optional) { if(!fo.empty()) fo+=","; fo += f->fieldname; } } + p["ns.sreg"] = OIURI_SREG11; if(!fr.empty()) p["sreg.required"]=fr; if(!fo.empty()) p["sreg.optional"]=fo; if(!policy_url.empty()) p["sreg.policy_url"]=policy_url; } void sreg_t::id_res_hook(const params_t& /* p */,const params_t& sp,const string& /* identity */) { clear(); for(fields_iterator f=fields_BEGIN;f<fields_END;++f) { string fn = "sreg."; fn+=f->fieldname; if(!sp.has_param(fn)) continue; has_fields |= f->fieldbit; response[f->fieldbit]=sp.get_param(fn); } } const string& sreg_t::get_field(fieldbit_t fb) const { response_t::const_iterator i = response.find(fb); if(i==response.end()) throw failed_lookup(OPKELE_CP_ "no field data available"); return i->second; } void sreg_t::set_field(fieldbit_t fb,const string& fv) { response[fb] = fv; has_fields |= fb; } void sreg_t::reset_field(fieldbit_t fb) { has_fields &= ~fb; response.erase(fb); } void sreg_t::clear() { has_fields = 0; response.clear(); } static long fields_list_to_bitmask(string& fl) { long rv = 0; while(!fl.empty()) { string::size_type co = fl.find(','); string fn; if(co==string::npos) { fn = fl; fl.erase(); }else{ fn = fl.substr(0,co); fl.erase(0,co+1); } fields_iterator f = find(fields_BEGIN,fields_END,fn); if(f!=fields_END) rv |= f->fieldbit; } return rv; } void sreg_t::checkid_hook(const params_t& pin,params_t& pout) { fields_optional = 0; fields_required = 0; policy_url.erase(); fields_response = 0; try { string fl = pin.get_param("openid.sreg.required"); fields_required = fields_list_to_bitmask(fl); }catch(failed_lookup&) { } try { string fl = pin.get_param("openid.sreg.optional"); fields_optional = fields_list_to_bitmask(fl); }catch(failed_lookup&) { } try { policy_url = pin.get_param("openid.sreg.policy_url"); }catch(failed_lookup&) { } setup_response(pin,pout); fields_response &= has_fields; for(fields_iterator f=fields_BEGIN;f<fields_END;++f) { if(!(f->fieldbit&fields_response)) continue; if(!pout["signed"].empty()) pout["signed"] +=','; string pn = "sreg."; pn += f->fieldname; pout["signed"] += pn; pout[pn] = get_field(f->fieldbit); } } void sreg_t::setup_response(const params_t& /* pin */,params_t& /* pout */) { fields_response = (fields_required|fields_optional)&has_fields; } } diff --git a/lib/util.cc b/lib/util.cc index 416e2cc..a9b9bed 100644 --- a/lib/util.cc +++ b/lib/util.cc @@ -1,295 +1,316 @@ #include <errno.h> #include <cassert> #include <cctype> #include <cstring> #include <vector> #include <string> #include <stack> #include <openssl/bio.h> #include <openssl/evp.h> #include <curl/curl.h> #include "opkele/util.h" #include "opkele/exception.h" namespace opkele { using namespace std; namespace util { /* * base64 */ string encode_base64(const void *data,size_t length) { BIO *b64 = 0, *bmem = 0; try { b64 = BIO_new(BIO_f_base64()); if(!b64) throw exception_openssl(OPKELE_CP_ "failed to BIO_new() base64 encoder"); BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL); bmem = BIO_new(BIO_s_mem()); BIO_set_flags(b64,BIO_CLOSE); if(!bmem) throw exception_openssl(OPKELE_CP_ "failed to BIO_new() memory buffer"); BIO_push(b64,bmem); if(((size_t)BIO_write(b64,data,length))!=length) throw exception_openssl(OPKELE_CP_ "failed to BIO_write()"); if(BIO_flush(b64)!=1) throw exception_openssl(OPKELE_CP_ "failed to BIO_flush()"); char *rvd; long rvl = BIO_get_mem_data(bmem,&rvd); string rv(rvd,rvl); BIO_free_all(b64); return rv; }catch(...) { if(b64) BIO_free_all(b64); throw; } } void decode_base64(const string& data,vector<unsigned char>& rv) { BIO *b64 = 0, *bmem = 0; rv.clear(); try { bmem = BIO_new_mem_buf((void*)data.data(),data.size()); if(!bmem) throw exception_openssl(OPKELE_CP_ "failed to BIO_new_mem_buf()"); b64 = BIO_new(BIO_f_base64()); if(!b64) throw exception_openssl(OPKELE_CP_ "failed to BIO_new() base64 decoder"); BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL); BIO_push(b64,bmem); unsigned char tmp[512]; size_t rb = 0; while((rb=BIO_read(b64,tmp,sizeof(tmp)))>0) rv.insert(rv.end(),tmp,&tmp[rb]); BIO_free_all(b64); }catch(...) { if(b64) BIO_free_all(b64); throw; } } /* * big numerics */ BIGNUM *base64_to_bignum(const string& b64) { vector<unsigned char> bin; decode_base64(b64,bin); BIGNUM *rv = BN_bin2bn(&(bin.front()),bin.size(),0); if(!rv) throw failed_conversion(OPKELE_CP_ "failed to BN_bin2bn()"); return rv; } BIGNUM *dec_to_bignum(const string& dec) { BIGNUM *rv = 0; if(!BN_dec2bn(&rv,dec.c_str())) throw failed_conversion(OPKELE_CP_ "failed to BN_dec2bn()"); return rv; } string bignum_to_base64(const BIGNUM *bn) { vector<unsigned char> bin(BN_num_bytes(bn)+1); unsigned char *binptr = &(bin.front())+1; int l = BN_bn2bin(bn,binptr); if(l && (*binptr)&0x80){ (*(--binptr)) = 0; ++l; } return encode_base64(binptr,l); } /* * w3c times */ string time_to_w3c(time_t t) { struct tm tm_t; if(!gmtime_r(&t,&tm_t)) throw failed_conversion(OPKELE_CP_ "failed to BN_dec2bn()"); char rv[25]; if(!strftime(rv,sizeof(rv)-1,"%Y-%m-%dT%H:%M:%SZ",&tm_t)) throw failed_conversion(OPKELE_CP_ "failed to strftime()"); return rv; } time_t w3c_to_time(const string& w) { + int fraction; struct tm tm_t; memset(&tm_t,0,sizeof(tm_t)); - if( + if( ( + sscanf( + w.c_str(), + "%04d-%02d-%02dT%02d:%02d:%02dZ", + &tm_t.tm_year,&tm_t.tm_mon,&tm_t.tm_mday, + &tm_t.tm_hour,&tm_t.tm_min,&tm_t.tm_sec + ) != 6 + ) && ( sscanf( w.c_str(), - "%04d-%02d-%02dT%02d:%02d:%02dZ", + "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", &tm_t.tm_year,&tm_t.tm_mon,&tm_t.tm_mday, - &tm_t.tm_hour,&tm_t.tm_min,&tm_t.tm_sec - ) != 6 ) + &tm_t.tm_hour,&tm_t.tm_min,&tm_t.tm_sec, + &fraction + ) != 7 + ) ) throw failed_conversion(OPKELE_CP_ "failed to sscanf()"); tm_t.tm_mon--; tm_t.tm_year-=1900; time_t rv = mktime(&tm_t); if(rv==(time_t)-1) throw failed_conversion(OPKELE_CP_ "failed to mktime()"); - return rv; + return rv-timezone; } /* * */ string url_encode(const string& str) { char * t = curl_escape(str.c_str(),str.length()); if(!t) throw failed_conversion(OPKELE_CP_ "failed to curl_escape()"); string rv(t); curl_free(t); return rv; } string long_to_string(long l) { char rv[32]; int r=snprintf(rv,sizeof(rv),"%ld",l); if(r<0 || r>=(int)sizeof(rv)) throw failed_conversion(OPKELE_CP_ "failed to snprintf()"); return rv; } long string_to_long(const string& s) { char *endptr = 0; long rv = strtol(s.c_str(),&endptr,10); if((!endptr) || endptr==s.c_str()) throw failed_conversion(OPKELE_CP_ "failed to strtol()"); return rv; } /* * Normalize URL according to the rules, described in rfc 3986, section 6 * - * - uppercase hext triplets (e.g. %ab -> %AB) + * - uppercase hex triplets (e.g. %ab -> %AB) * - lowercase scheme and host * - decode %-encoded characters, specified as unreserved in rfc 3986, section 2.3, * that is - [:alpha:][:digit:]._~- * - remove dot segments * - remove empty and default ports * - if there's no path component, add '/' */ string rfc_3986_normalize_uri(const string& uri) { + static const char *whitespace = " \t\r\n"; string rv; - string::size_type colon = uri.find(':'); + string::size_type ns = uri.find_first_not_of(whitespace); + if(ns==string::npos) + throw bad_input(OPKELE_CP_ "Can't normalize empty URI"); + string::size_type colon = uri.find(':',ns); if(colon==string::npos) throw bad_input(OPKELE_CP_ "No scheme specified in URI"); transform( - uri.begin(), uri.begin()+colon+1, + uri.begin()+ns, uri.begin()+colon+1, back_inserter(rv), ::tolower ); bool s; - if(rv=="http:") - s = false; - else if(rv=="https:") - s = true; - else - throw not_implemented(OPKELE_CP_ "Only http(s) URIs can be normalized here"); - string::size_type ul = uri.length(); + string::size_type ul = uri.find_last_not_of(whitespace)+1; if(ul <= (colon+3)) throw bad_input(OPKELE_CP_ "Unexpected end of URI being normalized encountered"); if(uri[colon+1]!='/' || uri[colon+2]!='/') throw bad_input(OPKELE_CP_ "Unexpected input in URI being normalized after scheme component"); + if(rv=="http:") + s = false; + else if(rv=="https:") + s = true; + else{ + /* TODO: support more schemes. + * e.g. xri. How do we normalize + * xri? + */ + rv.append(uri,colon+1,ul-colon-1); + return rv; + } rv += "//"; string::size_type interesting = uri.find_first_of(":/#?",colon+3); if(interesting==string::npos) { transform( - uri.begin()+colon+3,uri.end(), + uri.begin()+colon+3,uri.begin()+ul, back_inserter(rv), ::tolower ); rv += '/'; return rv; } transform( uri.begin()+colon+3,uri.begin()+interesting, back_inserter(rv), ::tolower ); bool qf = false; char ic = uri[interesting]; if(ic==':') { string::size_type ni = uri.find_first_of("/#?%",interesting+1); const char *nptr = uri.data()+interesting+1; char *eptr = 0; long port = strtol(nptr,&eptr,10); if( (port>0) && (port<65535) && port!=(s?443:80) ) { char tmp[8]; snprintf(tmp,sizeof(tmp),":%ld",port); rv += tmp; } if(ni==string::npos) { rv += '/'; return rv; } interesting = ni; }else if(ic!='/') { rv += '/'; rv += ic; qf = true; ++interesting; } string::size_type n = interesting; char tmp[3] = { 0,0,0 }; stack<string::size_type> psegs; psegs.push(rv.length()); string pseg; for(;n<ul;) { string::size_type unsafe = uri.find_first_of(qf?"%":"%/?#",n); if(unsafe==string::npos) { pseg.append(uri,n,ul-n-1); n = ul-1; }else{ pseg.append(uri,n,unsafe-n); n = unsafe; } char c = uri[n++]; if(c=='%') { if((n+1)>=ul) throw bad_input(OPKELE_CP_ "Unexpected end of URI encountered while parsing percent-encoded character"); tmp[0] = uri[n++]; tmp[1] = uri[n++]; if(!( isxdigit(tmp[0]) && isxdigit(tmp[1]) )) throw bad_input(OPKELE_CP_ "Invalid percent-encoded character in URI being normalized"); int cc = strtol(tmp,0,16); if( isalpha(cc) || isdigit(cc) || strchr("._~-",cc) ) pseg += cc; else{ pseg += '%'; pseg += toupper(tmp[0]); pseg += toupper(tmp[1]); } }else if(qf) { rv += pseg; rv += c; pseg.clear(); }else if(n>=ul || strchr("?/#",c)) { if(pseg.empty() || pseg==".") { }else if(pseg=="..") { if(psegs.size()>1) { rv.resize(psegs.top()); psegs.pop(); } }else{ psegs.push(rv.length()); if(c!='/') { pseg += c; qf = true; } rv += '/'; rv += pseg; } if(c=='/' && (n>=ul || strchr("?#",uri[n])) ) { rv += '/'; if(n<ul) qf = true; }else if(strchr("?#",c)) { if(psegs.size()==1 && psegs.top()==rv.length()) rv += '/'; if(pseg.empty()) rv += c; qf = true; } pseg.clear(); }else{ pseg += c; } } if(!pseg.empty()) { - rv += '/'; rv += pseg; + if(!qf) rv += '/'; + rv += pseg; } return rv; } } } |