]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/c-apps/oils_auth.c
Docs: adding introduction section to webclient offline docs
[Evergreen.git] / Open-ILS / src / c-apps / oils_auth.c
1 #include "opensrf/osrf_app_session.h"
2 #include "opensrf/osrf_application.h"
3 #include "opensrf/osrf_settings.h"
4 #include "opensrf/osrf_json.h"
5 #include "opensrf/log.h"
6 #include "openils/oils_utils.h"
7 #include "openils/oils_constants.h"
8 #include "openils/oils_event.h"
9 #include <pcre.h>
10
11 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
12 #define OILS_AUTH_COUNT_SFFX "_count"
13
14 #define MODULENAME "open-ils.auth"
15
16 #define OILS_AUTH_OPAC "opac"
17 #define OILS_AUTH_STAFF "staff"
18 #define OILS_AUTH_TEMP "temp"
19 #define OILS_AUTH_PERSIST "persist"
20
21 // Default time for extending a persistent session: ten minutes
22 #define DEFAULT_RESET_INTERVAL 10 * 60
23
24 int osrfAppInitialize();
25 int osrfAppChildInit();
26
27 static long _oilsAuthSeedTimeout = 0;
28 static long _oilsAuthBlockTimeout = 0;
29 static long _oilsAuthBlockCount = 0;
30
31
32 /**
33         @brief Initialize the application by registering functions for method calls.
34         @return Zero in all cases.
35 */
36 int osrfAppInitialize() {
37
38         osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
39
40         /* load and parse the IDL */
41         if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
42
43         osrfAppRegisterMethod(
44                 MODULENAME,
45                 "open-ils.auth.authenticate.init",
46                 "oilsAuthInit",
47                 "Start the authentication process and returns the intermediate authentication seed"
48                 " PARAMS( username )", 1, 0 );
49
50     osrfAppRegisterMethod(
51         MODULENAME,
52         "open-ils.auth.authenticate.init.barcode",
53         "oilsAuthInitBarcode",
54         "Start the authentication process using a patron barcode and return "
55         "the intermediate authentication seed. PARAMS(barcode)", 1, 0);
56
57     osrfAppRegisterMethod(
58         MODULENAME,
59         "open-ils.auth.authenticate.init.username",
60         "oilsAuthInitUsername",
61         "Start the authentication process using a patron username and return "
62         "the intermediate authentication seed. PARAMS(username)", 1, 0);
63
64         osrfAppRegisterMethod(
65                 MODULENAME,
66                 "open-ils.auth.authenticate.complete",
67                 "oilsAuthComplete",
68                 "Completes the authentication process.  Returns an object like so: "
69                 "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
70                 "token and authtime is the number of seconds the session will be active"
71                 "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
72                 "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
73                 "org_id is the location at which the login should be considered "
74                 "active for login timeout purposes", 1, 0 );
75
76         osrfAppRegisterMethod(
77                 MODULENAME,
78                 "open-ils.auth.login",
79                 "oilsAuthLogin",
80         "Request an authentication token logging in with username or "
81         "barcode.  Parameter is a keyword arguments hash with keys "
82         "username, barcode, identifier, password, type, org, workstation, "
83         "agent.  The 'identifier' option is used when the caller wants the "
84         "API to determine if an identifier string is a username or barcode "
85         "using the barcode format configuration.",
86         1, 0);
87
88         osrfAppRegisterMethod(
89                 MODULENAME,
90                 "open-ils.auth.authenticate.verify",
91                 "oilsAuthComplete",
92                 "Verifies the user provided a valid username and password."
93                 "Params and are the same as open-ils.auth.authenticate.complete."
94                 "Returns SUCCESS event on success, failure event on failure", 1, 0);
95
96
97         osrfAppRegisterMethod(
98                 MODULENAME,
99                 "open-ils.auth.session.retrieve",
100                 "oilsAuthSessionRetrieve",
101                 "Pass in the auth token and this retrieves the user object.  The auth "
102                 "timeout is reset when this call is made "
103                 "Returns the user object (password blanked) for the given login session "
104                 "PARAMS( authToken )", 1, 0 );
105
106         osrfAppRegisterMethod(
107                 MODULENAME,
108                 "open-ils.auth.session.delete",
109                 "oilsAuthSessionDelete",
110                 "Destroys the given login session "
111                 "PARAMS( authToken )",  1, 0 );
112
113         osrfAppRegisterMethod(
114                 MODULENAME,
115                 "open-ils.auth.session.reset_timeout",
116                 "oilsAuthResetTimeout",
117                 "Resets the login timeout for the given session "
118                 "Returns an ILS Event with payload = session_timeout of session "
119                 "if found, otherwise returns the NO_SESSION event"
120                 "PARAMS( authToken )", 1, 0 );
121
122         if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */
123
124                 jsonObject* value_obj;
125
126                 value_obj = osrf_settings_host_value_object(
127                         "/apps/open-ils.auth/app_settings/auth_limits/seed" );
128                 _oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
129                 jsonObjectFree(value_obj);
130                 if( -1 == _oilsAuthSeedTimeout ) {
131                         osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
132                         _oilsAuthSeedTimeout = 30;
133                 }
134
135                 value_obj = osrf_settings_host_value_object(
136                         "/apps/open-ils.auth/app_settings/auth_limits/block_time" );
137                 _oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
138                 jsonObjectFree(value_obj);
139                 if( -1 == _oilsAuthBlockTimeout ) {
140                         osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
141                         _oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
142                 }
143
144                 value_obj = osrf_settings_host_value_object(
145                         "/apps/open-ils.auth/app_settings/auth_limits/block_count" );
146                 _oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
147                 jsonObjectFree(value_obj);
148                 if( -1 == _oilsAuthBlockCount ) {
149                         osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
150                         _oilsAuthBlockCount = 10;
151                 }
152
153                 osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
154                         "seed => %ld : block_timeout => %ld : block_count => %ld",
155                         _oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
156         }
157
158         return 0;
159 }
160
161 /**
162         @brief Dummy placeholder for initializing a server drone.
163
164         There is nothing to do, so do nothing.
165 */
166 int osrfAppChildInit() {
167         return 0;
168 }
169
170 // free() response
171 static char* oilsAuthGetSalt(int user_id) {
172     char* salt_str = NULL;
173
174     jsonObject* params = jsonParseFmt( // free
175         "{\"from\":[\"actor.get_salt\",%d,\"%s\"]}", user_id, "main");
176
177     jsonObject* salt_obj = // free
178         oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
179
180     jsonObjectFree(params);
181
182     if (salt_obj) {
183
184         if (salt_obj->type != JSON_NULL) {
185
186             const char* salt_val = jsonObjectGetString(
187                 jsonObjectGetKeyConst(salt_obj, "actor.get_salt"));
188
189             // caller expects a free-able string, could be NULL.
190             if (salt_val) { salt_str = strdup(salt_val); } 
191         }
192
193         jsonObjectFree(salt_obj);
194     }
195
196     return salt_str;
197 }
198
199 // ident is either a username or barcode
200 // Returns the init seed -> requires free();
201 static char* oilsAuthBuildInitCache(
202     int user_id, const char* ident, const char* ident_type, const char* nonce) {
203
204     char* cache_key  = va_list_to_string(
205         "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, nonce);
206
207     char* count_key = va_list_to_string(
208         "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, OILS_AUTH_COUNT_SFFX);
209
210     char* auth_seed;
211     if (user_id == -1) {
212         // user does not exist.  Use a dummy seed
213         auth_seed = strdup("x");
214     } else {
215         auth_seed = oilsAuthGetSalt(user_id);
216     }
217
218     jsonObject* seed_object = jsonParseFmt(
219         "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}",
220         ident_type, ident, user_id, auth_seed);
221
222     jsonObject* count_object = osrfCacheGetObject(count_key);
223     if(!count_object) {
224         count_object = jsonNewNumberObject((double) 0);
225     }
226
227     osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout);
228
229     if (user_id != -1) {
230         // Only track login counts for existing users, since a 
231         // login for a nonexistent user will never succeed anyway.
232         osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
233     }
234
235     osrfLogDebug(OSRF_LOG_MARK, 
236         "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key);
237
238     free(cache_key);
239     free(count_key);
240     jsonObjectFree(count_object);
241     jsonObjectFree(seed_object);
242
243     return auth_seed;
244 }
245
246 static int oilsAuthInitUsernameHandler(
247     osrfMethodContext* ctx, const char* username, const char* nonce) {
248
249     osrfLogInfo(OSRF_LOG_MARK, 
250         "User logging in with username %s", username);
251
252     int user_id = -1;
253     jsonObject* resp = NULL; // free
254     jsonObject* user_obj = oilsUtilsFetchUserByUsername(ctx, username); // free
255
256     if (user_obj && user_obj->type != JSON_NULL) 
257         user_id = oilsFMGetObjectId(user_obj);
258
259     jsonObjectFree(user_obj); // NULL OK
260
261     char* seed = oilsAuthBuildInitCache(user_id, username, "username", nonce);
262     resp = jsonNewObject(seed);
263     free(seed);
264
265     osrfAppRespondComplete(ctx, resp);
266     jsonObjectFree(resp);
267     return 0;
268 }
269
270 // open-ils.auth.authenticate.init.username
271 int oilsAuthInitUsername(osrfMethodContext* ctx) {
272     OSRF_METHOD_VERIFY_CONTEXT(ctx);
273
274     char* username =  // free
275         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
276     const char* nonce = 
277         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
278
279     if (!nonce) nonce = "";
280     if (!username) return -1;
281
282     int resp = oilsAuthInitUsernameHandler(ctx, username, nonce);
283
284     free(username);
285     return resp;
286 }
287
288 static int oilsAuthInitBarcodeHandler(
289     osrfMethodContext* ctx, const char* barcode, const char* nonce) {
290
291     osrfLogInfo(OSRF_LOG_MARK, 
292         "User logging in with barcode %s", barcode);
293
294     int user_id = -1;
295     jsonObject* resp = NULL; // free
296     jsonObject* user_obj = oilsUtilsFetchUserByBarcode(ctx, barcode); // free
297
298     if (user_obj && user_obj->type != JSON_NULL) 
299         user_id = oilsFMGetObjectId(user_obj);
300
301     jsonObjectFree(user_obj); // NULL OK
302
303     char* seed = oilsAuthBuildInitCache(user_id, barcode, "barcode", nonce);
304     resp = jsonNewObject(seed);
305     free(seed);
306
307     osrfAppRespondComplete(ctx, resp);
308     jsonObjectFree(resp);
309     return 0;
310 }
311
312
313 // open-ils.auth.authenticate.init.barcode
314 int oilsAuthInitBarcode(osrfMethodContext* ctx) {
315     OSRF_METHOD_VERIFY_CONTEXT(ctx);
316
317     char* barcode = // free
318         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
319     const char* nonce = 
320         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
321
322     if (!nonce) nonce = "";
323     if (!barcode) return -1;
324
325     int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce);
326
327     free(barcode);
328     return resp;
329 }
330
331 // returns true if the provided identifier matches the barcode regex.
332 static int oilsAuthIdentIsBarcode(const char* identifier, int org_id) {
333
334     if (org_id < 1)
335         org_id = oilsUtilsGetRootOrgId();
336
337     char* bc_regex = oilsUtilsFetchOrgSetting(org_id, "opac.barcode_regex");
338
339     if (!bc_regex) {
340         // if no regex is set, assume any identifier starting
341         // with a number is a barcode.
342         bc_regex = strdup("^\\d"); // dupe for later free'ing
343     }
344
345     const char *err_str;
346     int err_offset, match_ret;
347
348     pcre *compiled = pcre_compile(
349         bc_regex, 0, &err_str, &err_offset, NULL);
350
351     if (compiled == NULL) {
352         osrfLogError(OSRF_LOG_MARK,
353             "Could not compile '%s': %s", bc_regex, err_str);
354         free(bc_regex);
355         pcre_free(compiled);
356         return 0;
357     }
358
359     pcre_extra *extra = pcre_study(compiled, 0, &err_str);
360
361     if(err_str != NULL) {
362         osrfLogError(OSRF_LOG_MARK,
363             "Could not study regex '%s': %s", bc_regex, err_str);
364         free(bc_regex);
365         pcre_free(compiled);
366         return 0;
367     } 
368
369     match_ret = pcre_exec(
370         compiled, extra, identifier, strlen(identifier), 0, 0, NULL, 0);       
371
372     free(bc_regex);
373     pcre_free(compiled);
374     if (extra) pcre_free(extra);
375
376     if (match_ret >= 0) return 1; // regex matched
377
378     if (match_ret != PCRE_ERROR_NOMATCH) 
379         osrfLogError(OSRF_LOG_MARK, "Unknown error processing barcode regex");
380
381     return 0; // regex did not match
382 }
383
384
385 /**
386         @brief Implement the "init" method.
387         @param ctx The method context.
388         @return Zero if successful, or -1 if not.
389
390         Method parameters:
391         - username
392         - nonce : optional login seed (string) provided by the caller which
393                 is added to the auth init cache to differentiate between logins
394                 using the same username and thus avoiding cache collisions for
395                 near-simultaneous logins.
396
397         Return to client: Intermediate authentication seed.
398 */
399 int oilsAuthInit(osrfMethodContext* ctx) {
400     OSRF_METHOD_VERIFY_CONTEXT(ctx);
401     int resp = 0;
402
403     char* identifier = // free
404         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
405     const char* nonce = 
406         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
407
408     if (!nonce) nonce = "";
409     if (!identifier) return -1;  // we need an identifier
410
411     if (oilsAuthIdentIsBarcode(identifier, 0)) {
412         resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce);
413     } else {
414         resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce);
415     }
416
417     free(identifier);
418     return resp;
419 }
420
421 /**
422         Returns 1 if the password provided matches the user's real password
423         Returns 0 otherwise
424         Returns -1 on error
425 */
426 /**
427         @brief Verify the password received from the client.
428         @param ctx The method context.
429         @param userObj An object from the database, representing the user.
430         @param password An obfuscated password received from the client.
431         @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
432
433         (None of the so-called "passwords" used here are in plaintext.  All have been passed
434         through at least one layer of hashing to obfuscate them.)
435
436         Take the password from the user object.  Append it to the username seed from memcache,
437         as stored previously by a call to the init method.  Take an md5 hash of the result.
438         Then compare this hash to the password received from the client.
439
440         In order for the two to match, other than by dumb luck, the client had to construct
441         the password it passed in the same way.  That means it neded to know not only the
442         original password (either hashed or plaintext), but also the seed.  The latter requirement
443         means that the client process needs either to be the same process that called the init
444         method or to receive the seed from the process that did so.
445 */
446 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, 
447         const char* identifier, const char* password, const char* nonce) {
448
449     int verified = 0;
450
451     // We won't be needing the seed again, remove it
452     osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
453
454     // Ask the DB to verify the user's password.
455     // Here, the password is md5(md5(password) + salt)
456
457     jsonObject* params = jsonParseFmt( // free
458         "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}", 
459         user_id, password);
460
461     jsonObject* verify_obj = // free 
462         oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
463
464     jsonObjectFree(params);
465
466     if (verify_obj) {
467         verified = oilsUtilsIsDBTrue(
468             jsonObjectGetString(
469                 jsonObjectGetKeyConst(
470                     verify_obj, "actor.verify_passwd")));
471
472         jsonObjectFree(verify_obj);
473     }
474
475     char* countkey = va_list_to_string("%s%s%s", 
476         OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
477     jsonObject* countobject = osrfCacheGetObject( countkey );
478     if(countobject) {
479         long failcount = (long) jsonObjectGetNumber( countobject );
480         if(failcount >= _oilsAuthBlockCount) {
481             verified = 0;
482             osrfLogInfo(OSRF_LOG_MARK, 
483                 "oilsAuth found too many recent failures for '%s' : %i, "
484                 "forcing failure state.", identifier, failcount);
485         }
486         if(verified == 0) {
487             failcount += 1;
488         }
489         jsonObjectSetNumber( countobject, failcount );
490         osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
491         jsonObjectFree(countobject);
492     }
493     free(countkey);
494
495     return verified;
496 }
497
498 /**
499  * Returns true if the provided password is correct.
500  * Turn the password into the nested md5 hash required of migrated
501  * passwords, then check the password in the DB.
502  */
503 static int oilsAuthLoginCheckPassword(int user_id, const char* password) {
504
505     growing_buffer* gb = buffer_init(33); // free me 1
506     char* salt = oilsAuthGetSalt(user_id); // free me 2
507     char* passhash = md5sum(password); // free me 3
508
509     buffer_add(gb, salt); // gb strdup's internally
510     buffer_add(gb, passhash);
511
512     free(salt); // free 2
513     free(passhash); // free 3
514
515     // salt + md5(password)
516     passhash = buffer_release(gb); // free 1 ; free me 4
517     char* finalpass = md5sum(passhash); // free me 5
518
519     free(passhash); // free 4
520
521     jsonObject *arr = jsonNewObjectType(JSON_ARRAY);
522     jsonObjectPush(arr, jsonNewObject("actor.verify_passwd"));
523     jsonObjectPush(arr, jsonNewNumberObject((long) user_id));
524     jsonObjectPush(arr, jsonNewObject("main"));
525     jsonObjectPush(arr, jsonNewObject(finalpass));
526     jsonObject *params = jsonNewObjectType(JSON_HASH); // free me 6
527     jsonObjectSetKey(params, "from", arr);
528
529     free(finalpass); // free 5
530
531     jsonObject* verify_obj = // free 
532         oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
533
534     jsonObjectFree(params); // free 6
535
536     if (!verify_obj) return 0; // error
537
538     int verified = oilsUtilsIsDBTrue(
539         jsonObjectGetString(
540             jsonObjectGetKeyConst(verify_obj, "actor.verify_passwd")
541         )
542     );
543
544     jsonObjectFree(verify_obj);
545
546     return verified;
547 }
548
549 static int oilsAuthLoginVerifyPassword(const osrfMethodContext* ctx, 
550     int user_id, const char* username, const char* password) {
551
552     // build the cache key
553     growing_buffer* gb = buffer_init(64); // free me
554     buffer_add(gb, OILS_AUTH_CACHE_PRFX);
555     buffer_add(gb, username);
556     buffer_add(gb, OILS_AUTH_COUNT_SFFX);
557     char* countkey = buffer_release(gb); // free me
558
559     jsonObject* countobject = osrfCacheGetObject(countkey); // free me
560
561     long failcount = 0;
562     if (countobject) {
563         failcount = (long) jsonObjectGetNumber(countobject);
564
565         if (failcount >= _oilsAuthBlockCount) {
566             // User is blocked.  Don't waste any more CPU cycles on them.
567
568             osrfLogInfo(OSRF_LOG_MARK, 
569                 "oilsAuth found too many recent failures for '%s' : %i, "
570                 "forcing failure state.", username, failcount);
571
572             jsonObjectFree(countobject);
573             free(countkey);   
574             return 0;
575         }
576     }
577
578     int verified = oilsAuthLoginCheckPassword(user_id, password);
579
580     if (!verified) { // login failed.  increment failure counter.
581         failcount++;
582
583         if (countobject) {
584             // append to existing counter
585             jsonObjectSetNumber(countobject, failcount);
586
587         } else { 
588             // first failure, create a new counter
589             countobject = jsonNewNumberObject((double) failcount);
590         }
591
592         osrfCachePutObject(countkey, countobject, _oilsAuthBlockTimeout);
593     }
594
595     jsonObjectFree(countobject); // NULL OK
596     free(countkey);
597
598     return verified;
599 }
600
601
602 /*
603         Adds the authentication token to the user cache.  The timeout for the
604         auth token is based on the type of login as well as (if type=='opac')
605         the org location id.
606         Returns the event that should be returned to the user.
607         Event must be freed
608 */
609 static oilsEvent* oilsAuthHandleLoginOK( osrfMethodContext* ctx, jsonObject* userObj, const char* uname,
610                 const char* type, int orgloc, const char* workstation ) {
611
612         oilsEvent* response = NULL;
613
614     jsonObject* params = jsonNewObject(NULL);
615     jsonObjectSetKey(params, "user_id", 
616         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
617     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
618     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
619     if (workstation) 
620         jsonObjectSetKey(params, "workstation", jsonNewObject(workstation));
621
622     jsonObject* authEvt = oilsUtilsQuickReqCtx(
623         ctx,
624         "open-ils.auth_internal",
625         "open-ils.auth_internal.session.create", params);
626     jsonObjectFree(params);
627
628     if (authEvt) {
629
630         response = oilsNewEvent2(
631             OSRF_LOG_MARK, 
632             jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")),
633             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
634         );
635
636         osrfLogActivity(OSRF_LOG_MARK,
637             "successful login: username=%s, authtoken=%s, workstation=%s",
638             uname,
639             jsonObjectGetString(
640                 jsonObjectGetKeyConst(
641                     jsonObjectGetKeyConst(authEvt, "payload"),
642                     "authtoken"
643                 )
644             ),
645             workstation ? workstation : ""
646         );
647
648         jsonObjectFree(authEvt);
649
650     } else {
651         osrfLogError(OSRF_LOG_MARK, 
652             "Error caching auth session in open-ils.auth_internal");
653     }
654
655     return response;
656 }
657
658
659 /**
660         @brief Implement the "complete" method.
661         @param ctx The method context.
662         @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
663         client to indicate completion; a positive integer if successful but no such STATUS
664         message has been sent.
665
666         Method parameters:
667         - a hash with some combination of the following elements:
668                 - "username"
669                 - "barcode"
670                 - "password" (hashed with the cached seed; not plaintext)
671                 - "type"
672                 - "org"
673                 - "workstation"
674                 - "agent" (what software/interface/3rd-party is making the request)
675                 - "nonce" optional login seed to differentiate logins using the same username.
676
677         The password is required.  Either a username or a barcode must also be present.
678
679         Return to client: Intermediate authentication seed.
680
681         Validate the password, using the username if available, or the barcode if not.  The
682         user must be active, and not barred from logging on.  The barcode, if used for
683         authentication, must be active as well.  The workstation, if specified, must be valid.
684
685         Upon deciding whether to allow the logon, return a corresponding event to the client.
686 */
687 int oilsAuthComplete( osrfMethodContext* ctx ) {
688     OSRF_METHOD_VERIFY_CONTEXT(ctx);
689
690     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
691
692     const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
693     const char* identifier  = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
694     const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
695     const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
696     int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
697     const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
698     const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
699     const char* ewho        = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
700     const char* nonce       = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
701
702     const char* ws = (workstation) ? workstation : "";
703     if (!nonce) nonce = "";
704
705     // we no longer care how the identifier reaches us, 
706     // as long as we have one.
707     if (!identifier) {
708         if (uname) {
709             identifier = uname;
710         } else if (barcode) {
711             identifier = barcode;
712         }
713     }
714
715     if (!identifier) {
716         return osrfAppRequestRespondException(ctx->session, ctx->request,
717             "username/barcode and password required for method: %s", 
718             ctx->method->name);
719     }
720
721     osrfLogInfo(OSRF_LOG_MARK, 
722         "Patron completing authentication with identifer %s", identifier);
723
724     /* Use __FILE__, harmless_line_number for creating
725      * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
726      * giving away information about why an authentication attempt failed.
727      */
728     int harmless_line_number = __LINE__;
729
730     if( !type )
731          type = OILS_AUTH_STAFF;
732
733     oilsEvent* response = NULL; // free
734     jsonObject* userObj = NULL; // free
735
736     char* cache_key = va_list_to_string(
737         "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
738     jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
739
740     if (!cacheObj) {
741         return osrfAppRequestRespondException(ctx->session,
742             ctx->request, "No authentication seed found. "
743             "open-ils.auth.authenticate.init must be called first "
744             " (check that memcached is running and can be connected to) "
745         );
746     }
747
748     int user_id = jsonObjectGetNumber(
749         jsonObjectGetKeyConst(cacheObj, "user_id"));
750
751     if (user_id == -1) {
752         // User was not found during init.  Clean up and exit early.
753         response = oilsNewEvent(
754             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
755         osrfAppRespondComplete(ctx, oilsEventToJSON(response));
756         oilsEventFree(response); // frees event JSON
757         osrfCacheRemove(cache_key);
758         jsonObjectFree(cacheObj);
759         return 0;
760     }
761
762     jsonObject* param = jsonNewNumberObject(user_id); // free
763     userObj = oilsUtilsCStoreReqCtx(
764         ctx, "open-ils.cstore.direct.actor.user.retrieve", param);
765     jsonObjectFree(param);
766
767     char* freeable_uname = NULL;
768     if (!uname) {
769         uname = freeable_uname = oilsFMGetString(userObj, "usrname");
770     }
771
772     // See if the user is allowed to login.
773
774     jsonObject* params = jsonNewObject(NULL);
775     jsonObjectSetKey(params, "user_id", 
776         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
777     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
778     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
779     if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
780
781     jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test
782         ctx,
783         "open-ils.auth_internal",
784         "open-ils.auth_internal.user.validate", params);
785     jsonObjectFree(params);
786
787     if (!authEvt) {
788         // Something went seriously wrong.  Get outta here before 
789         // we start segfaulting.
790         jsonObjectFree(userObj);
791         if(freeable_uname) free(freeable_uname);
792         return -1;
793     }
794
795     const char* authEvtCode = 
796         jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
797
798     if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
799         // Received the generic login failure event.
800
801         osrfLogInfo(OSRF_LOG_MARK,  
802             "failed login: username=%s, barcode=%s, workstation=%s",
803             uname, (barcode ? barcode : "(none)"), ws);
804
805         response = oilsNewEvent(
806             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
807     }
808
809     int passOK = 0;
810     
811     if (!response) {
812         // User exists and is not barred, etc.  Test the password.
813
814         passOK = oilsAuthVerifyPassword(
815             ctx, user_id, identifier, password, nonce);
816
817         if (!passOK) {
818             // Password check failed. Return generic login failure.
819
820             response = oilsNewEvent(
821                 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
822
823             osrfLogInfo(OSRF_LOG_MARK,  
824                 "failed login: username=%s, barcode=%s, workstation=%s",
825                     uname, (barcode ? barcode : "(none)"), ws );
826         }
827     }
828
829
830     // Below here, we know the password check succeeded if no response
831     // object is present.
832
833     if (!response && (
834         !strcmp(authEvtCode, "PATRON_INACTIVE") ||
835         !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
836         // Patron and/or card is inactive but the correct password 
837         // was provided.  Alert the caller to the inactive-ness.
838         response = oilsNewEvent2(
839             OSRF_LOG_MARK, authEvtCode,
840             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
841         );
842     }
843
844     if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
845         // Validate API returned an unexpected non-success event.
846         // To be safe, treat this as a generic login failure.
847
848         response = oilsNewEvent(
849             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
850     }
851
852     if (!response) {
853         // password OK and no other events have prevented login completion.
854
855         char* ewhat = "login";
856
857         if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
858             response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
859             ewhat = "verify";
860
861         } else {
862             response = oilsAuthHandleLoginOK(
863                 ctx, userObj, uname, type, orgloc, workstation);
864         }
865
866         oilsUtilsTrackUserActivity(
867             ctx,
868             oilsFMGetObjectId(userObj), 
869             ewho, ewhat, 
870             osrfAppSessionGetIngress()
871         );
872     }
873
874     // reply
875     osrfAppRespondComplete(ctx, oilsEventToJSON(response));
876
877     // clean up
878     oilsEventFree(response);
879     jsonObjectFree(userObj);
880     jsonObjectFree(authEvt);
881     jsonObjectFree(cacheObj);
882     if(freeable_uname)
883         free(freeable_uname);
884
885     return 0;
886 }
887
888
889 int oilsAuthLogin(osrfMethodContext* ctx) {
890     OSRF_METHOD_VERIFY_CONTEXT(ctx);
891
892     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
893
894     const char* username    = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
895     const char* identifier  = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
896     const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
897     const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
898     int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
899     const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
900     const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
901     const char* ewho        = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
902
903     const char* ws = (workstation) ? workstation : "";
904     if (!type) type = OILS_AUTH_STAFF;
905
906     jsonObject* userObj = NULL; // free me
907     oilsEvent* response = NULL; // free me
908
909     /* Use __FILE__, harmless_line_number for creating
910      * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
911      * giving away information about why an authentication attempt failed.
912      */
913     int harmless_line_number = __LINE__;
914
915     // translate a generic identifier into a username or barcode if necessary.
916     if (identifier && !username && !barcode) {
917         if (oilsAuthIdentIsBarcode(identifier, orgloc)) {
918             barcode = identifier;
919         } else {
920             username = identifier;
921         }
922     }
923
924     if (username) {
925         barcode = NULL; // avoid superfluous identifiers
926         userObj = oilsUtilsFetchUserByUsername(ctx, username);
927
928     } else if (barcode) {
929         userObj = oilsUtilsFetchUserByBarcode(ctx, barcode);
930
931     } else {
932         // not enough params
933         return osrfAppRequestRespondException(ctx->session, ctx->request,
934             "username/barcode and password required for method: %s", 
935             ctx->method->name);
936     }
937
938     if (!userObj) { // user not found.  
939         response = oilsNewEvent(
940             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
941         osrfAppRespondComplete(ctx, oilsEventToJSON(response));
942         oilsEventFree(response); // frees event JSON
943         return 0;
944     }
945
946     long user_id = oilsFMGetObjectId(userObj);
947
948     // username is freed when userObj is freed.
949     // From here we can use the username as the generic identifier
950     // since it's guaranteed to have a value.
951     if (!username) username = oilsFMGetStringConst(userObj, "usrname");
952
953     // See if the user is allowed to login.
954     jsonObject* params = jsonNewObject(NULL);
955     jsonObjectSetKey(params, "user_id", jsonNewNumberObject(user_id));
956     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
957     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
958     if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
959
960     jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test
961         ctx,
962         "open-ils.auth_internal",
963         "open-ils.auth_internal.user.validate", params);
964     jsonObjectFree(params);
965
966     if (!authEvt) { // unknown error
967         jsonObjectFree(userObj);
968         return -1;
969     }
970
971     const char* authEvtCode = 
972         jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
973
974     if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
975         // Received the generic login failure event.
976
977         osrfLogInfo(OSRF_LOG_MARK,  
978             "failed login: username=%s, barcode=%s, workstation=%s",
979             username, (barcode ? barcode : "(none)"), ws);
980
981         response = oilsNewEvent(
982             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
983     }
984
985     if (!response && // user exists and is not barred, etc.
986         !oilsAuthLoginVerifyPassword(ctx, user_id, username, password)) {
987         // User provided the wrong password or is blocked from too 
988         // many previous login failures.
989
990         response = oilsNewEvent(
991             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
992
993         osrfLogInfo(OSRF_LOG_MARK,  
994             "failed login: username=%s, barcode=%s, workstation=%s",
995                 username, (barcode ? barcode : "(none)"), ws );
996     }
997
998     // Below here, we know the password check succeeded if no response
999     // object is present.
1000
1001     if (!response && (
1002         !strcmp(authEvtCode, "PATRON_INACTIVE") ||
1003         !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
1004         // Patron and/or card is inactive but the correct password 
1005         // was provided.  Alert the caller to the inactive-ness.
1006         response = oilsNewEvent2(
1007             OSRF_LOG_MARK, authEvtCode,
1008             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
1009         );
1010     }
1011
1012     if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
1013         // Validate API returned an unexpected non-success event.
1014         // To be safe, treat this as a generic login failure.
1015
1016         response = oilsNewEvent(
1017             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
1018     }
1019
1020     if (!response) {
1021         // password OK and no other events have prevented login completion.
1022
1023         char* ewhat = "login";
1024
1025         if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
1026             response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
1027             ewhat = "verify";
1028
1029         } else {
1030             response = oilsAuthHandleLoginOK(
1031                 ctx, userObj, username, type, orgloc, workstation);
1032         }
1033
1034         oilsUtilsTrackUserActivity(
1035             ctx,
1036             oilsFMGetObjectId(userObj), 
1037             ewho, ewhat, 
1038             osrfAppSessionGetIngress()
1039         );
1040     }
1041
1042     // reply
1043     osrfAppRespondComplete(ctx, oilsEventToJSON(response));
1044
1045     // clean up
1046     oilsEventFree(response);
1047     jsonObjectFree(userObj);
1048     jsonObjectFree(authEvt);
1049
1050         return 0;
1051 }
1052
1053
1054
1055 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
1056         OSRF_METHOD_VERIFY_CONTEXT(ctx);
1057
1058         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
1059         jsonObject* resp = NULL;
1060
1061         if( authToken ) {
1062                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
1063                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
1064                 osrfCacheRemove(key);
1065                 resp = jsonNewObject(authToken); /**/
1066                 free(key);
1067         }
1068
1069         osrfAppRespondComplete( ctx, resp );
1070         jsonObjectFree(resp);
1071         return 0;
1072 }
1073
1074 /**
1075  * Fetches the user object from the database and updates the user object in 
1076  * the cache object, which then has to be re-inserted into the cache.
1077  * User object is retrieved inside a transaction to avoid replication issues.
1078  */
1079 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
1080     int reqid, userId;
1081     osrfAppSession* session;
1082         osrfMessage* omsg;
1083     jsonObject *param, *userObj, *newUserObj = NULL;
1084
1085     userObj = jsonObjectGetKey( cacheObj, "userobj" );
1086     userId = oilsFMGetObjectId( userObj );
1087
1088     session = osrfAppSessionClientInit( "open-ils.cstore" );
1089     osrfAppSessionConnect(session);
1090
1091     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
1092         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1093
1094     if(omsg) {
1095
1096         osrfMessageFree(omsg);
1097         param = jsonNewNumberObject(userId);
1098         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
1099             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1100         jsonObjectFree(param);
1101
1102         if(omsg) {
1103             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
1104             osrfMessageFree(omsg);
1105             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
1106                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
1107             osrfMessageFree(omsg);
1108         }
1109     }
1110
1111     osrfAppSessionFree(session); // calls disconnect internally
1112
1113     if(newUserObj) {
1114
1115         // ws_ou and wsid are ephemeral and need to be manually propagated
1116         // oilsFMSetString dupe()'s internally, no need to clone the string
1117         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
1118         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
1119
1120         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
1121         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
1122         return 1;
1123     } 
1124
1125     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
1126     return 0;
1127 }
1128
1129 /**
1130         Resets the auth login timeout
1131         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
1132 */
1133 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
1134         if(!authToken) return NULL;
1135
1136         oilsEvent* evt = NULL;
1137         time_t timeout;
1138
1139         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
1140         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1141         jsonObject* cacheObj = osrfCacheGetObject( key );
1142
1143         if(!cacheObj) {
1144                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
1145                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1146
1147         } else {
1148
1149         if(reloadUser) {
1150             _oilsAuthReloadUser(cacheObj);
1151         }
1152
1153                 // Determine a new timeout value
1154                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
1155                 if( endtime_obj ) {
1156                         // Extend the current endtime by a fixed amount
1157                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
1158                         int reset_interval = DEFAULT_RESET_INTERVAL;
1159                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
1160                                 cacheObj, "reset_interval" );
1161                         if( reset_interval_obj ) {
1162                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
1163                                 if( reset_interval <= 0 )
1164                                         reset_interval = DEFAULT_RESET_INTERVAL;
1165                         }
1166
1167                         time_t now = time( NULL );
1168                         time_t new_endtime = now + reset_interval;
1169                         if( new_endtime > endtime ) {
1170                                 // Keep the session alive a little longer
1171                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
1172                                 timeout = reset_interval;
1173                                 osrfCachePutObject( key, cacheObj, timeout );
1174                         } else {
1175                                 // The session isn't close to expiring, so don't reset anything.
1176                                 // Just report the time remaining.
1177                                 timeout = endtime - now;
1178                         }
1179                 } else {
1180                         // Reapply the existing timeout from the current time
1181                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
1182                         osrfCachePutObject( key, cacheObj, timeout );
1183                 }
1184
1185                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
1186                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
1187                 jsonObjectFree(payload);
1188                 jsonObjectFree(cacheObj);
1189         }
1190
1191         free(key);
1192         return evt;
1193 }
1194
1195 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
1196         OSRF_METHOD_VERIFY_CONTEXT(ctx);
1197         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1198     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
1199         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
1200         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
1201         oilsEventFree(evt);
1202         return 0;
1203 }
1204
1205
1206 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
1207         OSRF_METHOD_VERIFY_CONTEXT(ctx);
1208     bool returnFull = false;
1209
1210         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
1211
1212     if(ctx->params->size > 1) {
1213         // caller wants full cached object, with authtime, etc.
1214         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
1215         if(rt && strcmp(rt, "0") != 0) 
1216             returnFull = true;
1217     }
1218
1219         jsonObject* cacheObj = NULL;
1220         oilsEvent* evt = NULL;
1221
1222         if( authToken ){
1223
1224                 // Reset the timeout to keep the session alive
1225                 evt = _oilsAuthResetTimeout(authToken, 0);
1226
1227                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
1228                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
1229
1230                 } else {
1231
1232                         // Retrieve the cached session object
1233                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
1234                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
1235                         cacheObj = osrfCacheGetObject( key );
1236                         if(cacheObj) {
1237                                 // Return a copy of the cached user object
1238                 if(returnFull)
1239                                     osrfAppRespondComplete( ctx, cacheObj);
1240                 else
1241                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
1242                                 jsonObjectFree(cacheObj);
1243                         } else {
1244                                 // Auth token is invalid or expired
1245                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1246                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
1247                                 oilsEventFree(evt2);
1248                         }
1249                         free(key);
1250                 }
1251
1252         } else {
1253
1254                 // No session
1255                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
1256                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
1257         }
1258
1259         if(evt)
1260                 oilsEventFree(evt);
1261
1262         return 0;
1263 }