-rw-r--r-- | test/.gitignore | 2 | ||||
-rw-r--r-- | test/Makefile.am | 20 | ||||
-rw-r--r-- | test/OP-db.sql | 25 | ||||
-rw-r--r-- | test/OP.cc | 409 |
4 files changed, 452 insertions, 4 deletions
diff --git a/test/.gitignore b/test/.gitignore index d07884c..3d88495 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -7 +7,3 @@ /RP-db.cc +/OP.cgi +/OP-db.cc diff --git a/test/Makefile.am b/test/Makefile.am index 61e3787..8fedf48 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -1,2 +1,2 @@ -noinst_PROGRAMS = test idiscover RP.cgi +noinst_PROGRAMS = test idiscover RP.cgi OP.cgi @@ -10,4 +10,4 @@ test_LDADD = ${top_builddir}/lib/libopkele.la EXTRA_DIST= \ - sqlite.h \ - RP-db.sql + sqlite.h kingate_openid_message.h \ + RP-db.sql OP-db.sql @@ -33,4 +33,16 @@ RP-db.cc: RP-db.sql +OP_cgi_SOURCES = OP.cc +nodist_OP_cgi_SOURCES = OP-db.cc +OP_cgi_LDADD = ${RP_cgi_LDADD} +OP_cgi_CFLAGS = ${RP_cgi_CFLAGS} + +OP-db.cc: OP-db.sql + ( \ + echo 'const char * __OP_db_bootstrap = ' && \ + sed -e 's/^/"/' -e 's/$$/"/' $< && \ + echo ';' \ + ) >$@ + clean-local: - rm -f RP-db.cc + rm -f RP-db.cc OP-db.cc diff --git a/test/OP-db.sql b/test/OP-db.sql new file mode 100644 index 0000000..424b5ff --- a/dev/null +++ b/test/OP-db.sql @@ -0,0 +1,25 @@ +CREATE TABLE assoc ( + a_op text, + a_handle text NOT NULL, + a_type text DEFAULT 'HMAC-SHA1', + a_ctime text NOT NULL, + a_etime text NOT NULL, + a_secret text NOT NULL, + a_stateless integer NOT NULL DEFAULT 0, + a_itime integer, + UNIQUE(a_op,a_handle) +); + +CREATE TABLE nonces ( + n_once text NOT NULL PRIMARY KEY, + n_itime integer +); + +CREATE TABLE setup ( + s_password text +); + +CREATE TABLE ht_sessions ( + hts_id text NOT NULL PRIMARY KEY, + authorized integer NOT NULL DEFAULT 0 +); diff --git a/test/OP.cc b/test/OP.cc new file mode 100644 index 0000000..1196c0c --- a/dev/null +++ b/test/OP.cc @@ -0,0 +1,409 @@ +#include <uuid/uuid.h> +#include <iostream> +#include <cassert> +#include <string> +#include <ext/algorithm> +using namespace std; +#include <kingate/exception.h> +#include <kingate/plaincgi.h> +#include <kingate/cgi_gateway.h> +#include <opkele/exception.h> +#include <opkele/util.h> +#include <opkele/uris.h> +#include <opkele/extension.h> +#include <opkele/association.h> +#include <opkele/debug.h> +#include <opkele/verify_op.h> + +#include "sqlite.h" +#include "kingate_openid_message.h" + +static const string get_self_url(const kingate::cgi_gateway& gw) { + bool s = gw.has_meta("SSL_PROTOCOL_VERSION"); + string rv = s?"https://":"http://"; + rv += gw.http_request_header("Host"); + const string& port = gw.get_meta("SERVER_PORT"); + if( port!=(s?"443":"80") ) { + rv += ':'; rv += port; + } + rv += gw.get_meta("REQUEST_URI"); + string::size_type q = rv.find('?'); + if(q!=string::npos) + rv.erase(q); + return rv; +} + +class opdb_t : public sqlite3_t { + public: + opdb_t() + : sqlite3_t("/tmp/OP.db") { + assert(_D); + char **resp; int nr,nc; char *errm; + if(sqlite3_get_table( + _D, "SELECT a_op FROM assoc LIMIT 0", + &resp,&nr,&nc,&errm)!=SQLITE_OK) { + extern const char *__OP_db_bootstrap; + DOUT_("Bootstrapping DB"); + if(sqlite3_exec(_D,__OP_db_bootstrap,NULL,NULL,&errm)!=SQLITE_OK) + throw opkele::exception(OPKELE_CP_ string("Failed to boostrap SQLite database: ")+errm); + }else + sqlite3_free_table(resp); + } +}; + +class example_op_t : public opkele::verify_op { + public: + kingate::cgi_gateway& gw; + opdb_t db; + kingate::cookie htc; + + + example_op_t(kingate::cgi_gateway& gw) + : gw(gw) { + try { + htc = gw.cookies.get_cookie("htop_session"); + sqlite3_mem_t<char*> S = sqlite3_mprintf( + "SELECT 1 FROM ht_sessions WHERE hts_id=%Q", + htc.get_value().c_str()); + sqlite3_table_t T; int nr,nc; + db.get_table(S,T,&nr,&nc); + if(nr<1) + throw kingate::exception_notfound(CODEPOINT,"forcing cookie generation"); + }catch(kingate::exception_notfound& kenf) { + uuid_t uuid; uuid_generate(uuid); + htc = kingate::cookie("htop_session",opkele::util::encode_base64(uuid,sizeof(uuid))); + sqlite3_mem_t<char*> S = sqlite3_mprintf( + "INSERT INTO ht_sessions (hts_id) VALUES (%Q)", + htc.get_value().c_str()); + db.exec(S); + } + } + + void set_authorized(bool a) { + sqlite3_mem_t<char*> + S = sqlite3_mprintf( + "UPDATE ht_sessions" + " SET authorized=%d" + " WHERE hts_id=%Q", + (int)a,htc.get_value().c_str()); + db.exec(S); + } + bool get_authorized() { + sqlite3_mem_t<char*> + S = sqlite3_mprintf( + "SELECT authorized" + " FROM ht_sessions" + " WHERE hts_id=%Q", + htc.get_value().c_str()); + sqlite3_table_t T; int nr,nc; + db.get_table(S,T,&nr,&nc); + assert(nr==1); assert(nc=1); + return opkele::util::string_to_long(T.get(1,0,nc)); + } + + ostream& cookie_header(ostream& o) const { + o << "Set-Cookie: " << htc.set_cookie_header() << "\n"; + return o; + } + + opkele::assoc_t alloc_assoc(const string& type,size_t klength,bool sl) { + uuid_t uuid; uuid_generate(uuid); + string a_handle = opkele::util::encode_base64(uuid,sizeof(uuid)); + opkele::secret_t a_secret; + generate_n( + back_insert_iterator<opkele::secret_t>(a_secret),klength, + rand ); + string ssecret; a_secret.to_base64(ssecret); + time_t now = time(0); + int expires_in = sl?3600*2:3600*24*7*2; + sqlite3_mem_t<char*> + S = sqlite3_mprintf( + "INSERT INTO assoc" + " (a_handle,a_type,a_ctime,a_etime,a_secret,a_stateless)" + " VALUES (" + " %Q,%Q,datetime('now')," + " datetime('now','+%d seconds')," + " %Q,%d );", + a_handle.c_str(), type.c_str(), + expires_in, + ssecret.c_str(), sl ); + db.exec(S); + return opkele::assoc_t(new opkele::association( + "", + a_handle, type, a_secret, + now+expires_in, sl )); + } + + opkele::assoc_t retrieve_assoc(const string& h) { + sqlite3_mem_t<char*> + S = sqlite3_mprintf( + "SELECT" + " a_handle,a_type,a_secret,a_stateless," + " strftime('%%s',a_etime) AS a_etime," + " a_itime" + " FROM assoc" + " WHERE a_handle=%Q AND a_itime IS NULL" + " AND datetime('now') < a_etime" + " LIMIT 1", + h.c_str() ); + sqlite3_table_t T; + int nr,nc; + db.get_table(S,T,&nr,&nc); + if(nr<1) + throw opkele::failed_lookup(OPKELE_CP_ + "couldn't retrieve valid unexpired assoc"); + assert(nr==1); assert(nc==6); + opkele::secret_t secret; opkele::util::decode_base64(T.get(1,2,nc),secret); + return opkele::assoc_t(new opkele::association( + "", h, T.get(1,1,nc), secret, + strtol(T.get(1,4,nc),0,0), + strtol(T.get(1,3,nc),0,0) )); + } + + string& alloc_nonce(string& nonce,bool stateless) { + uuid_t uuid; uuid_generate(uuid); + nonce += opkele::util::encode_base64(uuid,sizeof(uuid)); + sqlite3_mem_t<char*> + S = sqlite3_mprintf( + "INSERT INTO nonces" + " (n_once) VALUES (%Q)", + nonce.c_str() ); + db.exec(S); + return nonce; + } + bool check_nonce(const string& nonce) { + sqlite3_mem_t<char*> + S = sqlite3_mprintf( + "SELECT 1" + " FROM nonces" + " WHERE n_once=%Q AND n_itime IS NULL", + nonce.c_str()); + sqlite3_table_t T; + int nr,nc; + db.get_table(S,T,&nr,&nc); + return nr>=1; + } + void invalidate_nonce(const string& nonce) { + sqlite3_mem_t<char*> + S = sqlite3_mprintf( + "UPDATE nonces" + " SET n_itime=datetime('now')" + " WHERE n_once=%Q", + nonce.c_str()); + db.exec(S); + } + + const string get_op_endpoint() const { + return get_self_url(gw); + } + +}; + +int main(int argc,char *argv[]) { + try { + kingate::plaincgi_interface ci; + kingate::cgi_gateway gw(ci); + string op; + try { op = gw.get_param("op"); }catch(kingate::exception_notfound&) { } + string message; + if(op=="set_password") { + example_op_t OP(gw); + string password = gw.get_param("password"); + sqlite3_mem_t<char*> + Sget = sqlite3_mprintf("SELECT s_password FROM setup LIMIT 1"); + sqlite3_table_t T; int nr,nc; + OP.db.get_table(Sget,T,&nr,&nc); + if(nr>=1) + throw opkele::exception(OPKELE_CP_ "Password already set"); + sqlite3_mem_t<char*> + Sset = sqlite3_mprintf( + "INSERT INTO setup (s_password) VALUES (%Q)", + password.c_str()); + OP.db.exec(Sset); + op.clear(); + message = "password set"; + }else if(op=="login") { + example_op_t OP(gw); + string password = gw.get_param("password"); + sqlite3_mem_t<char*> + Sget = sqlite3_mprintf("SELECT s_password FROM setup LIMIT 1"); + sqlite3_table_t T; int nr,nc; + OP.db.get_table(Sget,T,&nr,&nc); + if(nr<1) + throw opkele::exception(OPKELE_CP_ "no password set"); + if(password!=T.get(1,0,nc)) + throw opkele::exception(OPKELE_CP_ "wrong password"); + OP.set_authorized(true); + op.clear(); + message = "logged in"; + OP.cookie_header(cout); + }else if(op=="logout") { + example_op_t OP(gw); + OP.set_authorized(false); + op.clear(); + message = "logged out"; + } + string om; + try { om = gw.get_param("openid.mode"); }catch(kingate::exception_notfound&) { } + if(op=="xrds") { + cout << + "Content-type: application/xrds+xml\n\n" + "<?xml version='1.0' encoding='utf-8'?>" + "<xrds:XRDS xmlns:xrds='xri://$xrds' xmlns='xri://$xrd*($v*2.0)'>" + "<XRD>" + "<Service>" + "<Type>" STURI_OPENID20 "</Type>" + "<URI>" << get_self_url(gw) << "</URI>" + "</Service>"; + if(gw.has_param("idsel")){ + cout << + "<Service>" + "<Type>" STURI_OPENID20_OP "</Type>" + "<URI>" << get_self_url(gw) << "</URI>"; + } + cout << + "</XRD>" + "</xrds:XRDS>"; + }else if(op=="id_res" || op=="cancel") { + kingate_openid_message_t inm(gw); + example_op_t OP(gw); + if(gw.get_param("hts_id")!=OP.htc.get_value()) + throw opkele::exception(OPKELE_CP_ "toying around, huh?"); + OP.checkid_(inm,0); + OP.cookie_header(cout); + opkele::openid_message_t om; + if(op=="id_res") { + if(!OP.get_authorized()) + throw opkele::exception(OPKELE_CP_ "not logged in"); + if(OP.is_id_select()) { + OP.select_identity( get_self_url(gw), get_self_url(gw) ); + } + cout << + "Status: 302 Going back to RP with id_res\n" + "Location: " << OP.id_res(om).append_query(OP.get_return_to()) + << "\n\n"; + }else{ + cout << + "Status: 302 Going back to RP with cancel\n" + "Location: " << OP.cancel(om).append_query(OP.get_return_to()) + << "\n\n"; + } + om.to_keyvalues(clog); + }else if(om=="associate") { + kingate_openid_message_t inm(gw); + opkele::openid_message_t oum; + example_op_t OP(gw); + OP.associate(oum,inm); + cout << "Content-type: text/plain\n\n"; + oum.to_keyvalues(cout); + }else if(om=="checkid_setup") { + kingate_openid_message_t inm(gw); + example_op_t OP(gw); + OP.checkid_(inm,0); + OP.cookie_header(cout) << + "Content-type: text/html\n" + "\n" + + "<html>" + "<head>" + "<title>test OP: confirm authentication</title>" + "</head>" + "<body>" + "realm: " << OP.get_realm() << "<br/>" + "return_to: " << OP.get_return_to() << "<br/>" + "claimed_id: " << OP.get_claimed_id() << "<br/>" + "identity: " << OP.get_identity() << "<br/>"; + if(OP.is_id_select()) { + OP.select_identity( get_self_url(gw), get_self_url(gw) ); + cout << + "selected claimed_id: " << OP.get_claimed_id() << "<br/>" + "selected identity: " << OP.get_identity() << "<br/>"; + } + cout << + "<form method='post'>"; + inm.to_htmlhiddens(cout); + cout << + "<input type='hidden' name='hts_id'" + " value='" << opkele::util::attr_escape(OP.htc.get_value()) << "'/>" + "<input type='submit' name='op' value='id_res'/>" + "<input type='submit' name='op' value='cancel'/>" + "</form>" + "</body>" + "</html>"; + }else if(om=="check_authentication") { + kingate_openid_message_t inm(gw); + example_op_t OP(gw); + opkele::openid_message_t oum; + OP.check_authentication(oum,inm); + cout << "Content-type: text/plain\n\n"; + oum.to_keyvalues(cout); + oum.to_keyvalues(clog); + }else{ + example_op_t OP(gw); + string idsel; + if(gw.has_param("idsel")) + idsel = "&idsel=idsel"; + OP.cookie_header(cout) << + "Content-type: text/html\n" + "X-XRDS-Location: " << get_self_url(gw) << "?op=xrds" << idsel << "\n" + "\n" + + "<html>" + "<head>" + "<title>test OP</title>" + "<link rel='openid.server' href='" << get_self_url(gw) << "'/>" + "</head>" + "<body>" + "test openid 2.0 endpoint" + "<br/>" + "<a href='" << get_self_url(gw) << "?op=xrds" << idsel << "'>XRDS document</a>" + "<br/>" + "<h1>" << message << "</h1>"; + sqlite3_mem_t<char*> + S = sqlite3_mprintf("SELECT s_password FROM setup LIMIT 1"); + sqlite3_table_t T; int nr,nc; + OP.db.get_table(S,T,&nr,&nc); + if(nr<1) { + cout << + "<form method='post'>" + "set password " + "<input type='hidden' name='op' value='set_password'/>" + "<input type='password' name='password' value=''/>" + "<input type='submit' name='submit' value='submit'/>" + "</form>"; + }else if(OP.get_authorized()) { + cout << + "<br/>" + "<a href='" << get_self_url(gw) << "?op=logout'>logout</a>"; + }else{ + cout << + "<form method='post'>" + "login " + "<input type='hidden' name='op' value='login'/>" + "<input type='password' name='password' value=''/>" + "<input type='submit' name='submit' value='submit'/>" + "</form>"; + } + cout << "</body>"; + } +#ifdef OPKELE_HAVE_KONFORKA + }catch(konforka::exception& e) { +#else + }catch(std::exception& e){ +#endif + DOUT_("Oops: " << e.what()); + cout << "Content-Type: text/plain\n\n" + "Exception:\n" + " what: " << e.what() << endl; +#ifdef OPKELE_HAVE_KONFORKA + cout << " where: " << e.where() << endl; + if(!e._seen.empty()) { + cout << " seen:" << endl; + for(list<konforka::code_point>::const_iterator + i=e._seen.begin();i!=e._seen.end();++i) { + cout << " " << i->c_str() << endl; + } + } +#endif + } +} |