-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 @@ | |||
7 | /RP-db.cc | 7 | /RP-db.cc |
8 | /OP.cgi | ||
9 | /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 @@ | |||
1 | noinst_PROGRAMS = test idiscover RP.cgi | 1 | noinst_PROGRAMS = test idiscover RP.cgi OP.cgi |
2 | 2 | ||
@@ -10,4 +10,4 @@ test_LDADD = ${top_builddir}/lib/libopkele.la | |||
10 | EXTRA_DIST= \ | 10 | EXTRA_DIST= \ |
11 | sqlite.h \ | 11 | sqlite.h kingate_openid_message.h \ |
12 | RP-db.sql | 12 | RP-db.sql OP-db.sql |
13 | 13 | ||
@@ -33,4 +33,16 @@ RP-db.cc: RP-db.sql | |||
33 | 33 | ||
34 | OP_cgi_SOURCES = OP.cc | ||
35 | nodist_OP_cgi_SOURCES = OP-db.cc | ||
36 | OP_cgi_LDADD = ${RP_cgi_LDADD} | ||
37 | OP_cgi_CFLAGS = ${RP_cgi_CFLAGS} | ||
38 | |||
39 | OP-db.cc: OP-db.sql | ||
40 | ( \ | ||
41 | echo 'const char * __OP_db_bootstrap = ' && \ | ||
42 | sed -e 's/^/"/' -e 's/$$/"/' $< && \ | ||
43 | echo ';' \ | ||
44 | ) >$@ | ||
45 | |||
34 | clean-local: | 46 | clean-local: |
35 | rm -f RP-db.cc | 47 | rm -f RP-db.cc OP-db.cc |
36 | 48 | ||
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 @@ | |||
1 | CREATE TABLE assoc ( | ||
2 | a_op text, | ||
3 | a_handle text NOT NULL, | ||
4 | a_type text DEFAULT 'HMAC-SHA1', | ||
5 | a_ctime text NOT NULL, | ||
6 | a_etime text NOT NULL, | ||
7 | a_secret text NOT NULL, | ||
8 | a_stateless integer NOT NULL DEFAULT 0, | ||
9 | a_itime integer, | ||
10 | UNIQUE(a_op,a_handle) | ||
11 | ); | ||
12 | |||
13 | CREATE TABLE nonces ( | ||
14 | n_once text NOT NULL PRIMARY KEY, | ||
15 | n_itime integer | ||
16 | ); | ||
17 | |||
18 | CREATE TABLE setup ( | ||
19 | s_password text | ||
20 | ); | ||
21 | |||
22 | CREATE TABLE ht_sessions ( | ||
23 | hts_id text NOT NULL PRIMARY KEY, | ||
24 | authorized integer NOT NULL DEFAULT 0 | ||
25 | ); | ||
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 @@ | |||
1 | #include <uuid/uuid.h> | ||
2 | #include <iostream> | ||
3 | #include <cassert> | ||
4 | #include <string> | ||
5 | #include <ext/algorithm> | ||
6 | using namespace std; | ||
7 | #include <kingate/exception.h> | ||
8 | #include <kingate/plaincgi.h> | ||
9 | #include <kingate/cgi_gateway.h> | ||
10 | #include <opkele/exception.h> | ||
11 | #include <opkele/util.h> | ||
12 | #include <opkele/uris.h> | ||
13 | #include <opkele/extension.h> | ||
14 | #include <opkele/association.h> | ||
15 | #include <opkele/debug.h> | ||
16 | #include <opkele/verify_op.h> | ||
17 | |||
18 | #include "sqlite.h" | ||
19 | #include "kingate_openid_message.h" | ||
20 | |||
21 | static const string get_self_url(const kingate::cgi_gateway& gw) { | ||
22 | bool s = gw.has_meta("SSL_PROTOCOL_VERSION"); | ||
23 | string rv = s?"https://":"http://"; | ||
24 | rv += gw.http_request_header("Host"); | ||
25 | const string& port = gw.get_meta("SERVER_PORT"); | ||
26 | if( port!=(s?"443":"80") ) { | ||
27 | rv += ':'; rv += port; | ||
28 | } | ||
29 | rv += gw.get_meta("REQUEST_URI"); | ||
30 | string::size_type q = rv.find('?'); | ||
31 | if(q!=string::npos) | ||
32 | rv.erase(q); | ||
33 | return rv; | ||
34 | } | ||
35 | |||
36 | class opdb_t : public sqlite3_t { | ||
37 | public: | ||
38 | opdb_t() | ||
39 | : sqlite3_t("/tmp/OP.db") { | ||
40 | assert(_D); | ||
41 | char **resp; int nr,nc; char *errm; | ||
42 | if(sqlite3_get_table( | ||
43 | _D, "SELECT a_op FROM assoc LIMIT 0", | ||
44 | &resp,&nr,&nc,&errm)!=SQLITE_OK) { | ||
45 | extern const char *__OP_db_bootstrap; | ||
46 | DOUT_("Bootstrapping DB"); | ||
47 | if(sqlite3_exec(_D,__OP_db_bootstrap,NULL,NULL,&errm)!=SQLITE_OK) | ||
48 | throw opkele::exception(OPKELE_CP_ string("Failed to boostrap SQLite database: ")+errm); | ||
49 | }else | ||
50 | sqlite3_free_table(resp); | ||
51 | } | ||
52 | }; | ||
53 | |||
54 | class example_op_t : public opkele::verify_op { | ||
55 | public: | ||
56 | kingate::cgi_gateway& gw; | ||
57 | opdb_t db; | ||
58 | kingate::cookie htc; | ||
59 | |||
60 | |||
61 | example_op_t(kingate::cgi_gateway& gw) | ||
62 | : gw(gw) { | ||
63 | try { | ||
64 | htc = gw.cookies.get_cookie("htop_session"); | ||
65 | sqlite3_mem_t<char*> S = sqlite3_mprintf( | ||
66 | "SELECT 1 FROM ht_sessions WHERE hts_id=%Q", | ||
67 | htc.get_value().c_str()); | ||
68 | sqlite3_table_t T; int nr,nc; | ||
69 | db.get_table(S,T,&nr,&nc); | ||
70 | if(nr<1) | ||
71 | throw kingate::exception_notfound(CODEPOINT,"forcing cookie generation"); | ||
72 | }catch(kingate::exception_notfound& kenf) { | ||
73 | uuid_t uuid; uuid_generate(uuid); | ||
74 | htc = kingate::cookie("htop_session",opkele::util::encode_base64(uuid,sizeof(uuid))); | ||
75 | sqlite3_mem_t<char*> S = sqlite3_mprintf( | ||
76 | "INSERT INTO ht_sessions (hts_id) VALUES (%Q)", | ||
77 | htc.get_value().c_str()); | ||
78 | db.exec(S); | ||
79 | } | ||
80 | } | ||
81 | |||
82 | void set_authorized(bool a) { | ||
83 | sqlite3_mem_t<char*> | ||
84 | S = sqlite3_mprintf( | ||
85 | "UPDATE ht_sessions" | ||
86 | " SET authorized=%d" | ||
87 | " WHERE hts_id=%Q", | ||
88 | (int)a,htc.get_value().c_str()); | ||
89 | db.exec(S); | ||
90 | } | ||
91 | bool get_authorized() { | ||
92 | sqlite3_mem_t<char*> | ||
93 | S = sqlite3_mprintf( | ||
94 | "SELECT authorized" | ||
95 | " FROM ht_sessions" | ||
96 | " WHERE hts_id=%Q", | ||
97 | htc.get_value().c_str()); | ||
98 | sqlite3_table_t T; int nr,nc; | ||
99 | db.get_table(S,T,&nr,&nc); | ||
100 | assert(nr==1); assert(nc=1); | ||
101 | return opkele::util::string_to_long(T.get(1,0,nc)); | ||
102 | } | ||
103 | |||
104 | ostream& cookie_header(ostream& o) const { | ||
105 | o << "Set-Cookie: " << htc.set_cookie_header() << "\n"; | ||
106 | return o; | ||
107 | } | ||
108 | |||
109 | opkele::assoc_t alloc_assoc(const string& type,size_t klength,bool sl) { | ||
110 | uuid_t uuid; uuid_generate(uuid); | ||
111 | string a_handle = opkele::util::encode_base64(uuid,sizeof(uuid)); | ||
112 | opkele::secret_t a_secret; | ||
113 | generate_n( | ||
114 | back_insert_iterator<opkele::secret_t>(a_secret),klength, | ||
115 | rand ); | ||
116 | string ssecret; a_secret.to_base64(ssecret); | ||
117 | time_t now = time(0); | ||
118 | int expires_in = sl?3600*2:3600*24*7*2; | ||
119 | sqlite3_mem_t<char*> | ||
120 | S = sqlite3_mprintf( | ||
121 | "INSERT INTO assoc" | ||
122 | " (a_handle,a_type,a_ctime,a_etime,a_secret,a_stateless)" | ||
123 | " VALUES (" | ||
124 | " %Q,%Q,datetime('now')," | ||
125 | " datetime('now','+%d seconds')," | ||
126 | " %Q,%d );", | ||
127 | a_handle.c_str(), type.c_str(), | ||
128 | expires_in, | ||
129 | ssecret.c_str(), sl ); | ||
130 | db.exec(S); | ||
131 | return opkele::assoc_t(new opkele::association( | ||
132 | "", | ||
133 | a_handle, type, a_secret, | ||
134 | now+expires_in, sl )); | ||
135 | } | ||
136 | |||
137 | opkele::assoc_t retrieve_assoc(const string& h) { | ||
138 | sqlite3_mem_t<char*> | ||
139 | S = sqlite3_mprintf( | ||
140 | "SELECT" | ||
141 | " a_handle,a_type,a_secret,a_stateless," | ||
142 | " strftime('%%s',a_etime) AS a_etime," | ||
143 | " a_itime" | ||
144 | " FROM assoc" | ||
145 | " WHERE a_handle=%Q AND a_itime IS NULL" | ||
146 | " AND datetime('now') < a_etime" | ||
147 | " LIMIT 1", | ||
148 | h.c_str() ); | ||
149 | sqlite3_table_t T; | ||
150 | int nr,nc; | ||
151 | db.get_table(S,T,&nr,&nc); | ||
152 | if(nr<1) | ||
153 | throw opkele::failed_lookup(OPKELE_CP_ | ||
154 | "couldn't retrieve valid unexpired assoc"); | ||
155 | assert(nr==1); assert(nc==6); | ||
156 | opkele::secret_t secret; opkele::util::decode_base64(T.get(1,2,nc),secret); | ||
157 | return opkele::assoc_t(new opkele::association( | ||
158 | "", h, T.get(1,1,nc), secret, | ||
159 | strtol(T.get(1,4,nc),0,0), | ||
160 | strtol(T.get(1,3,nc),0,0) )); | ||
161 | } | ||
162 | |||
163 | string& alloc_nonce(string& nonce,bool stateless) { | ||
164 | uuid_t uuid; uuid_generate(uuid); | ||
165 | nonce += opkele::util::encode_base64(uuid,sizeof(uuid)); | ||
166 | sqlite3_mem_t<char*> | ||
167 | S = sqlite3_mprintf( | ||
168 | "INSERT INTO nonces" | ||
169 | " (n_once) VALUES (%Q)", | ||
170 | nonce.c_str() ); | ||
171 | db.exec(S); | ||
172 | return nonce; | ||
173 | } | ||
174 | bool check_nonce(const string& nonce) { | ||
175 | sqlite3_mem_t<char*> | ||
176 | S = sqlite3_mprintf( | ||
177 | "SELECT 1" | ||
178 | " FROM nonces" | ||
179 | " WHERE n_once=%Q AND n_itime IS NULL", | ||
180 | nonce.c_str()); | ||
181 | sqlite3_table_t T; | ||
182 | int nr,nc; | ||
183 | db.get_table(S,T,&nr,&nc); | ||
184 | return nr>=1; | ||
185 | } | ||
186 | void invalidate_nonce(const string& nonce) { | ||
187 | sqlite3_mem_t<char*> | ||
188 | S = sqlite3_mprintf( | ||
189 | "UPDATE nonces" | ||
190 | " SET n_itime=datetime('now')" | ||
191 | " WHERE n_once=%Q", | ||
192 | nonce.c_str()); | ||
193 | db.exec(S); | ||
194 | } | ||
195 | |||
196 | const string get_op_endpoint() const { | ||
197 | return get_self_url(gw); | ||
198 | } | ||
199 | |||
200 | }; | ||
201 | |||
202 | int main(int argc,char *argv[]) { | ||
203 | try { | ||
204 | kingate::plaincgi_interface ci; | ||
205 | kingate::cgi_gateway gw(ci); | ||
206 | string op; | ||
207 | try { op = gw.get_param("op"); }catch(kingate::exception_notfound&) { } | ||
208 | string message; | ||
209 | if(op=="set_password") { | ||
210 | example_op_t OP(gw); | ||
211 | string password = gw.get_param("password"); | ||
212 | sqlite3_mem_t<char*> | ||
213 | Sget = sqlite3_mprintf("SELECT s_password FROM setup LIMIT 1"); | ||
214 | sqlite3_table_t T; int nr,nc; | ||
215 | OP.db.get_table(Sget,T,&nr,&nc); | ||
216 | if(nr>=1) | ||
217 | throw opkele::exception(OPKELE_CP_ "Password already set"); | ||
218 | sqlite3_mem_t<char*> | ||
219 | Sset = sqlite3_mprintf( | ||
220 | "INSERT INTO setup (s_password) VALUES (%Q)", | ||
221 | password.c_str()); | ||
222 | OP.db.exec(Sset); | ||
223 | op.clear(); | ||
224 | message = "password set"; | ||
225 | }else if(op=="login") { | ||
226 | example_op_t OP(gw); | ||
227 | string password = gw.get_param("password"); | ||
228 | sqlite3_mem_t<char*> | ||
229 | Sget = sqlite3_mprintf("SELECT s_password FROM setup LIMIT 1"); | ||
230 | sqlite3_table_t T; int nr,nc; | ||
231 | OP.db.get_table(Sget,T,&nr,&nc); | ||
232 | if(nr<1) | ||
233 | throw opkele::exception(OPKELE_CP_ "no password set"); | ||
234 | if(password!=T.get(1,0,nc)) | ||
235 | throw opkele::exception(OPKELE_CP_ "wrong password"); | ||
236 | OP.set_authorized(true); | ||
237 | op.clear(); | ||
238 | message = "logged in"; | ||
239 | OP.cookie_header(cout); | ||
240 | }else if(op=="logout") { | ||
241 | example_op_t OP(gw); | ||
242 | OP.set_authorized(false); | ||
243 | op.clear(); | ||
244 | message = "logged out"; | ||
245 | } | ||
246 | string om; | ||
247 | try { om = gw.get_param("openid.mode"); }catch(kingate::exception_notfound&) { } | ||
248 | if(op=="xrds") { | ||
249 | cout << | ||
250 | "Content-type: application/xrds+xml\n\n" | ||
251 | "<?xml version='1.0' encoding='utf-8'?>" | ||
252 | "<xrds:XRDS xmlns:xrds='xri://$xrds' xmlns='xri://$xrd*($v*2.0)'>" | ||
253 | "<XRD>" | ||
254 | "<Service>" | ||
255 | "<Type>" STURI_OPENID20 "</Type>" | ||
256 | "<URI>" << get_self_url(gw) << "</URI>" | ||
257 | "</Service>"; | ||
258 | if(gw.has_param("idsel")){ | ||
259 | cout << | ||
260 | "<Service>" | ||
261 | "<Type>" STURI_OPENID20_OP "</Type>" | ||
262 | "<URI>" << get_self_url(gw) << "</URI>"; | ||
263 | } | ||
264 | cout << | ||
265 | "</XRD>" | ||
266 | "</xrds:XRDS>"; | ||
267 | }else if(op=="id_res" || op=="cancel") { | ||
268 | kingate_openid_message_t inm(gw); | ||
269 | example_op_t OP(gw); | ||
270 | if(gw.get_param("hts_id")!=OP.htc.get_value()) | ||
271 | throw opkele::exception(OPKELE_CP_ "toying around, huh?"); | ||
272 | OP.checkid_(inm,0); | ||
273 | OP.cookie_header(cout); | ||
274 | opkele::openid_message_t om; | ||
275 | if(op=="id_res") { | ||
276 | if(!OP.get_authorized()) | ||
277 | throw opkele::exception(OPKELE_CP_ "not logged in"); | ||
278 | if(OP.is_id_select()) { | ||
279 | OP.select_identity( get_self_url(gw), get_self_url(gw) ); | ||
280 | } | ||
281 | cout << | ||
282 | "Status: 302 Going back to RP with id_res\n" | ||
283 | "Location: " << OP.id_res(om).append_query(OP.get_return_to()) | ||
284 | << "\n\n"; | ||
285 | }else{ | ||
286 | cout << | ||
287 | "Status: 302 Going back to RP with cancel\n" | ||
288 | "Location: " << OP.cancel(om).append_query(OP.get_return_to()) | ||
289 | << "\n\n"; | ||
290 | } | ||
291 | om.to_keyvalues(clog); | ||
292 | }else if(om=="associate") { | ||
293 | kingate_openid_message_t inm(gw); | ||
294 | opkele::openid_message_t oum; | ||
295 | example_op_t OP(gw); | ||
296 | OP.associate(oum,inm); | ||
297 | cout << "Content-type: text/plain\n\n"; | ||
298 | oum.to_keyvalues(cout); | ||
299 | }else if(om=="checkid_setup") { | ||
300 | kingate_openid_message_t inm(gw); | ||
301 | example_op_t OP(gw); | ||
302 | OP.checkid_(inm,0); | ||
303 | OP.cookie_header(cout) << | ||
304 | "Content-type: text/html\n" | ||
305 | "\n" | ||
306 | |||
307 | "<html>" | ||
308 | "<head>" | ||
309 | "<title>test OP: confirm authentication</title>" | ||
310 | "</head>" | ||
311 | "<body>" | ||
312 | "realm: " << OP.get_realm() << "<br/>" | ||
313 | "return_to: " << OP.get_return_to() << "<br/>" | ||
314 | "claimed_id: " << OP.get_claimed_id() << "<br/>" | ||
315 | "identity: " << OP.get_identity() << "<br/>"; | ||
316 | if(OP.is_id_select()) { | ||
317 | OP.select_identity( get_self_url(gw), get_self_url(gw) ); | ||
318 | cout << | ||
319 | "selected claimed_id: " << OP.get_claimed_id() << "<br/>" | ||
320 | "selected identity: " << OP.get_identity() << "<br/>"; | ||
321 | } | ||
322 | cout << | ||
323 | "<form method='post'>"; | ||
324 | inm.to_htmlhiddens(cout); | ||
325 | cout << | ||
326 | "<input type='hidden' name='hts_id'" | ||
327 | " value='" << opkele::util::attr_escape(OP.htc.get_value()) << "'/>" | ||
328 | "<input type='submit' name='op' value='id_res'/>" | ||
329 | "<input type='submit' name='op' value='cancel'/>" | ||
330 | "</form>" | ||
331 | "</body>" | ||
332 | "</html>"; | ||
333 | }else if(om=="check_authentication") { | ||
334 | kingate_openid_message_t inm(gw); | ||
335 | example_op_t OP(gw); | ||
336 | opkele::openid_message_t oum; | ||
337 | OP.check_authentication(oum,inm); | ||
338 | cout << "Content-type: text/plain\n\n"; | ||
339 | oum.to_keyvalues(cout); | ||
340 | oum.to_keyvalues(clog); | ||
341 | }else{ | ||
342 | example_op_t OP(gw); | ||
343 | string idsel; | ||
344 | if(gw.has_param("idsel")) | ||
345 | idsel = "&idsel=idsel"; | ||
346 | OP.cookie_header(cout) << | ||
347 | "Content-type: text/html\n" | ||
348 | "X-XRDS-Location: " << get_self_url(gw) << "?op=xrds" << idsel << "\n" | ||
349 | "\n" | ||
350 | |||
351 | "<html>" | ||
352 | "<head>" | ||
353 | "<title>test OP</title>" | ||
354 | "<link rel='openid.server' href='" << get_self_url(gw) << "'/>" | ||
355 | "</head>" | ||
356 | "<body>" | ||
357 | "test openid 2.0 endpoint" | ||
358 | "<br/>" | ||
359 | "<a href='" << get_self_url(gw) << "?op=xrds" << idsel << "'>XRDS document</a>" | ||
360 | "<br/>" | ||
361 | "<h1>" << message << "</h1>"; | ||
362 | sqlite3_mem_t<char*> | ||
363 | S = sqlite3_mprintf("SELECT s_password FROM setup LIMIT 1"); | ||
364 | sqlite3_table_t T; int nr,nc; | ||
365 | OP.db.get_table(S,T,&nr,&nc); | ||
366 | if(nr<1) { | ||
367 | cout << | ||
368 | "<form method='post'>" | ||
369 | "set password " | ||
370 | "<input type='hidden' name='op' value='set_password'/>" | ||
371 | "<input type='password' name='password' value=''/>" | ||
372 | "<input type='submit' name='submit' value='submit'/>" | ||
373 | "</form>"; | ||
374 | }else if(OP.get_authorized()) { | ||
375 | cout << | ||
376 | "<br/>" | ||
377 | "<a href='" << get_self_url(gw) << "?op=logout'>logout</a>"; | ||
378 | }else{ | ||
379 | cout << | ||
380 | "<form method='post'>" | ||
381 | "login " | ||
382 | "<input type='hidden' name='op' value='login'/>" | ||
383 | "<input type='password' name='password' value=''/>" | ||
384 | "<input type='submit' name='submit' value='submit'/>" | ||
385 | "</form>"; | ||
386 | } | ||
387 | cout << "</body>"; | ||
388 | } | ||
389 | #ifdef OPKELE_HAVE_KONFORKA | ||
390 | }catch(konforka::exception& e) { | ||
391 | #else | ||
392 | }catch(std::exception& e){ | ||
393 | #endif | ||
394 | DOUT_("Oops: " << e.what()); | ||
395 | cout << "Content-Type: text/plain\n\n" | ||
396 | "Exception:\n" | ||
397 | " what: " << e.what() << endl; | ||
398 | #ifdef OPKELE_HAVE_KONFORKA | ||
399 | cout << " where: " << e.where() << endl; | ||
400 | if(!e._seen.empty()) { | ||
401 | cout << " seen:" << endl; | ||
402 | for(list<konforka::code_point>::const_iterator | ||
403 | i=e._seen.begin();i!=e._seen.end();++i) { | ||
404 | cout << " " << i->c_str() << endl; | ||
405 | } | ||
406 | } | ||
407 | #endif | ||
408 | } | ||
409 | } | ||