static int osrf_json_gateway_method_handler (request_rec *r) {

	/* make sure we're needed first thing*/
	if (strcmp(r->handler, MODULE_NAME )) return DECLINED;


	osrf_json_gateway_dir_config* dir_conf =
		ap_get_module_config(r->per_dir_config, &osrf_json_gateway_module);


	/* provide 2 different JSON parsers and serializers to support legacy JSON */
	jsonObject* (*parseJSONFunc) (const char*) = legacy_jsonParseString;
	char* (*jsonToStringFunc) (const jsonObject*) = legacy_jsonObjectToJSON;

	if(dir_conf->legacyJSON) {
		ap_log_rerror( APLOG_MARK, APLOG_DEBUG, 0, r, "Using legacy JSON");

	} else {
		parseJSONFunc = jsonParse;
		jsonToStringFunc = jsonObjectToJSON;
	}


	osrfLogDebug(OSRF_LOG_MARK, "osrf gateway: entered request handler");

	/* verify we are connected */
	if( !bootstrapped || !osrfSystemGetTransportClient()) {
		ap_log_rerror( APLOG_MARK, APLOG_ERR, 0, r, "Cannot process request "
				"because the OpenSRF JSON gateway has not been bootstrapped...");
		usleep( 100000 ); /* 100 milliseconds */
		exit(1);
	}

	osrfLogSetAppname("osrf_json_gw");

	char* osrf_locale   = NULL;
	char* param_locale  = NULL;  /* locale for this call */
	char* service       = NULL;  /* service to connect to */
	char* method        = NULL;  /* method to perform */
	char* format        = NULL;  /* method to perform */
	char* a_l           = NULL;  /* request api level */
	char* input_format  = NULL;  /* POST data format, defaults to 'format' */
	int   isXML         = 0;
	int   api_level     = 1;

	r->allowed |= (AP_METHOD_BIT << M_GET);
	r->allowed |= (AP_METHOD_BIT << M_POST);

	osrfLogDebug(OSRF_LOG_MARK, "osrf gateway: parsing URL params");
	osrfStringArray* mparams = NULL;
	osrfStringArray* params  = apacheParseParms(r); /* free me */
	param_locale             = apacheGetFirstParamValue( params, "locale" );
	service                  = apacheGetFirstParamValue( params, "service" );
	method                   = apacheGetFirstParamValue( params, "method" );
	format                   = apacheGetFirstParamValue( params, "format" );
	input_format             = apacheGetFirstParamValue( params, "input_format" );
	a_l                      = apacheGetFirstParamValue( params, "api_level" );
	mparams                  = apacheGetParamValues( params, "param" ); /* free me */

	if(format == NULL)
		format = strdup( "json" );
	if(input_format == NULL)
		input_format = strdup( format );

	/* set the user defined timeout value */
	int timeout = 60;
	char* tout = apacheGetFirstParamValue( params, "timeout" ); /* request timeout in seconds */
	if( tout ) {
		timeout = atoi(tout);
		osrfLogDebug(OSRF_LOG_MARK, "Client supplied timeout of %d", timeout);
		free( tout );
	}

	if (a_l) {
		api_level = atoi(a_l);
		free( a_l );
	}

	if (!strcasecmp(format, "xml")) {
		isXML = 1;
		ap_set_content_type(r, "application/xml");
	} else {
		ap_set_content_type(r, "text/plain");
	}

	free( format );
	int ret = OK;

	/* ----------------------------------------------------------------- */
	/* Grab the requested locale using the Accept-Language header*/


	if ( !param_locale ) {
		if ( apr_table_get(r->headers_in, "X-OpenSRF-Language") ) {
			param_locale = strdup( apr_table_get(r->headers_in, "X-OpenSRF-Language") );
		} else if ( apr_table_get(r->headers_in, "Accept-Language") ) {
			param_locale = strdup( apr_table_get(r->headers_in, "Accept-Language") );
		}
	}


	if (param_locale) {
		growing_buffer* osrf_locale_buf = buffer_init(16);
		if (index(param_locale, ',')) {
			int ind = index(param_locale, ',') - param_locale;
			int i;
			for ( i = 0; i < ind && i < 128; i++ )
				buffer_add_char( osrf_locale_buf, param_locale[i] );
		} else {
			buffer_add( osrf_locale_buf, param_locale );
		}

		free(param_locale);
		osrf_locale = buffer_release( osrf_locale_buf );
	} else {
		osrf_locale = strdup( osrf_json_default_locale );
	}
	/* ----------------------------------------------------------------- */


	if(!(service && method)) {

		osrfLogError(OSRF_LOG_MARK,
			"Service [%s] not found or not allowed", service);
		ret = HTTP_NOT_FOUND;

	} else {

		/* This will log all heaers to the apache error log
		const apr_array_header_t* arr = apr_table_elts(r->headers_in);
		const void* ptr;

		while( (ptr = apr_array_pop(arr)) ) {
			apr_table_entry_t* e = (apr_table_entry_t*) ptr;
			fprintf(stderr, "Table entry: %s : %s\n", e->key, e->val );
		}
		fflush(stderr);
		*/

		osrfAppSession* session = osrfAppSessionClientInit(service);
		osrf_app_session_set_locale(session, osrf_locale);

		double starttime = get_timestamp_millis();
		int req_id = -1;

		if(!strcasecmp(input_format, "json")) {
			jsonObject * arr = jsonNewObject(NULL);

			const char* str;
			int i = 0;

			while( (str = osrfStringArrayGetString(mparams, i++)) )
				jsonObjectPush(arr, parseJSONFunc(str));

			req_id = osrfAppSessionSendRequest( session, arr, method, api_level );
			jsonObjectFree(arr);
		} else {

			/**
			* If we receive XML method params, convert each param to a JSON object
			* and pass the array of JSON object params to the method */
			if(!strcasecmp(input_format, "xml")) {
				jsonObject* jsonParams = jsonNewObject(NULL);

				const char* str;
				int i = 0;
				while( (str = osrfStringArrayGetString(mparams, i++)) ) {
					jsonObjectPush(jsonParams, jsonXMLToJSONObject(str));
				}

				req_id = osrfAppSessionSendRequest( session, jsonParams, method, api_level );
				jsonObjectFree(jsonParams);
			}
		}


		if( req_id == -1 ) {
			osrfLogError(OSRF_LOG_MARK, "I am unable to communicate with opensrf..going away...");
			osrfAppSessionFree(session);
			/* we don't want to spawn an intense re-forking storm
			 * if there is no jabber server.. so give it some time before we die */
			usleep( 100000 ); /* 100 milliseconds */
			exit(1);
		}


		/* ----------------------------------------------------------------- */
		/* log all requests to the activity log */
		const char* authtoken = apr_table_get(r->headers_in, "X-OILS-Authtoken");
		if(!authtoken) authtoken = "";
		growing_buffer* act = buffer_init(128);
		buffer_fadd(act, "[%s] [%s] [%s] %s %s", r->connection->remote_ip,
			authtoken, osrf_locale, service, method );
		const char* str; int i = 0;
		while( (str = osrfStringArrayGetString(mparams, i++)) ) {
			if( i == 1 ) {
				OSRF_BUFFER_ADD(act, " ");
				OSRF_BUFFER_ADD(act, str);
			} else {
				OSRF_BUFFER_ADD(act, ", ");
				OSRF_BUFFER_ADD(act, str);
			}
		}

		osrfLogActivity( OSRF_LOG_MARK, act->buf );
		buffer_free(act);
		/* ----------------------------------------------------------------- */


		osrfMessage* omsg = NULL;

		int statuscode = 200;

		/* kick off the object */
		if (isXML)
			ap_rputs( "<response xmlns=\"http://opensrf.org/-/namespaces/gateway/v1\"><payload>",
				r );
		else
			ap_rputs("{\"payload\":[", r);

		int morethan1       = 0;
		char* statusname    = NULL;
		char* statustext    = NULL;
		char* output        = NULL;

		while((omsg = osrfAppSessionRequestRecv( session, req_id, timeout ))) {

			statuscode = omsg->status_code;
			const jsonObject* res;

			if( ( res = osrfMessageGetResult(omsg)) ) {

				if (isXML) {
					output = jsonObjectToXML( res );
				} else {
					output = jsonToStringFunc( res );
					if( morethan1 ) ap_rputs(",", r); /* comma between JSON array items */
				}
				ap_rputs(output, r);
				free(output);
				morethan1 = 1;

			} else {

				if( statuscode > 299 ) { /* the request returned a low level error */
					statusname = omsg->status_name ? strdup(omsg->status_name)
						: strdup("Unknown Error");
					statustext = omsg->status_text ? strdup(omsg->status_text) 
						: strdup("No Error Message");
					osrfLogError( OSRF_LOG_MARK,  "Gateway received error: %s", statustext );
				}
			}

			osrfMessageFree(omsg);
			if(statusname) break;
		}

		double duration = get_timestamp_millis() - starttime;
		osrfLogDebug(OSRF_LOG_MARK, "gateway request took %f seconds", duration);


		if (isXML)
			ap_rputs("</payload>", r);
		else
			ap_rputs("]",r); /* finish off the payload array */

		if(statusname) {

			/* add a debug field if the request died */
			ap_log_rerror( APLOG_MARK, APLOG_INFO, 0, r,
					"OpenSRF JSON Request returned error: %s -> %s", statusname, statustext );
			int l = strlen(statusname) + strlen(statustext) + 32;
			char buf[l];

			if (isXML)
				snprintf( buf, sizeof(buf), "<debug>\"%s : %s\"</debug>", statusname, statustext );

			else {
				char bb[l];
				snprintf(bb, sizeof(bb),  "%s : %s", statusname, statustext);
				jsonObject* tmp = jsonNewObject(bb);
				char* j = jsonToStringFunc(tmp);
				snprintf( buf, sizeof(buf), ",\"debug\": %s", j);
				free(j);
				jsonObjectFree(tmp);
			}

			ap_rputs(buf, r);

			free(statusname);
			free(statustext);
		}

		/* insert the status code */
		char buf[32];

		if (isXML)
			snprintf(buf, sizeof(buf), "<status>%d</status>", statuscode );
		else
			snprintf(buf, sizeof(buf), ",\"status\":%d", statuscode );

		ap_rputs( buf, r );

		if (isXML)
			ap_rputs("</response>", r);
		else
			ap_rputs( "}", r ); /* finish off the object */

		osrfAppSessionFree(session);
	}

	osrfLogInfo(OSRF_LOG_MARK, "Completed processing service=%s, method=%s", service, method);
	osrfStringArrayFree(params);
	osrfStringArrayFree(mparams);
	free( osrf_locale );
	free( input_format );
	free( method );
	free( service );

	osrfLogDebug(OSRF_LOG_MARK, "Gateway served %d requests", ++numserved);
	osrfLogClearXid();

	return ret;
}
/**
    @brief Implement the session create method
    @param ctx The method context.
    @return -1 upon error; zero if successful, and if a STATUS message has 
    been sent to the client to indicate completion; a positive integer if 
    successful but no such STATUS message has been sent.

    Method parameters:
    - a hash with some combination of the following elements:
        - "user_id"     -- actor.usr (au) ID for the user to cache.
        - "org_unit"    -- actor.org_unit (aou) ID representing the physical 
                           location / context used for timeout, etc. settings.
        - "login_type"  -- login type (opac, staff, temp, persist)
        - "workstation" -- workstation name

*/
int oilsAuthInternalCreateSession(osrfMethodContext* ctx) {
    OSRF_METHOD_VERIFY_CONTEXT(ctx);

    const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);

    const char* user_id     = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id"));
    const char* login_type  = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type"));
    const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
    int org_unit            = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org_unit"));

    if ( !(user_id && login_type) ) {
        return osrfAppRequestRespondException( ctx->session, ctx->request,
            "Missing parameters for method: %s", ctx->method->name );
    }

    oilsEvent* response = NULL;

    // fetch the user object
    jsonObject* idParam = jsonNewNumberStringObject(user_id);
    jsonObject* userObj = oilsUtilsCStoreReqCtx(
        ctx, "open-ils.cstore.direct.actor.user.retrieve", idParam);
    jsonObjectFree(idParam);

    if (!userObj) {
        return osrfAppRequestRespondException(ctx->session, 
            ctx->request, "No user found with ID %s", user_id);
    }

    // If a workstation is defined, add the workstation info
    if (workstation) {
        response = oilsAuthVerifyWorkstation(ctx, userObj, workstation);

        if (response) { // invalid workstation.
            jsonObjectFree(userObj);
            osrfAppRespondComplete(ctx, oilsEventToJSON(response));
            oilsEventFree(response);
            return 0;

        } else { // workstation OK.  

            // The worksation org unit supersedes any org unit value 
            // provided via the API.  oilsAuthVerifyWorkstation() sets the 
            // ws_ou value to the WS owning lib.  A value is guaranteed.
            org_unit = atoi(oilsFMGetStringConst(userObj, "ws_ou"));
        }

    } else { // no workstation

        // For backwards compatibility, when no workstation is provided, use 
        // the users's home org as its workstation org unit, regardless of 
        // any API-level org unit value provided.
        const char* orgid = oilsFMGetStringConst(userObj, "home_ou");
        oilsFMSetString(userObj, "ws_ou", orgid);

        // The context org unit defaults to the user's home library when
        // no workstation is used and no API-level value is provided.
        if (org_unit < 1) org_unit = atoi(orgid);
    }

    // determine the auth/cache timeout
    long timeout = oilsAuthGetTimeout(userObj, login_type, org_unit);

    char* string = va_list_to_string("%d.%ld.%ld", 
        (long) getpid(), time(NULL), oilsFMGetObjectId(userObj));
    char* authToken = md5sum(string);
    char* authKey = va_list_to_string(
        "%s%s", OILS_AUTH_CACHE_PRFX, authToken);

    oilsFMSetString(userObj, "passwd", "");
    jsonObject* cacheObj = jsonParseFmt("{\"authtime\": %ld}", timeout);
    jsonObjectSetKey(cacheObj, "userobj", jsonObjectClone(userObj));

    if( !strcmp(login_type, OILS_AUTH_PERSIST)) {
        // Add entries for endtime and reset_interval, so that we can gracefully
        // extend the session a bit if the user is active toward the end of the 
        // timeout originally specified.
        time_t endtime = time( NULL ) + timeout;
        jsonObjectSetKey(cacheObj, "endtime", 
            jsonNewNumberObject( (double) endtime ));

        // Reset interval is hard-coded for now, but if we ever want to make it
        // configurable, this is the place to do it:
        jsonObjectSetKey(cacheObj, "reset_interval",
            jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL));
    }

    osrfCachePutObject(authKey, cacheObj, (time_t) timeout);
    jsonObjectFree(cacheObj);
    jsonObject* payload = jsonParseFmt(
        "{\"authtoken\": \"%s\", \"authtime\": %ld}", authToken, timeout);

    response = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
    free(string); free(authToken); free(authKey);
    jsonObjectFree(payload);

    jsonObjectFree(userObj);
    osrfAppRespondComplete(ctx, oilsEventToJSON(response));
    oilsEventFree(response);

    return 0;
}
/**
	@brief Parse a hash (JSON object), and create a JSON_HASH for it.
	@param parser Pointer to a Parser.
	@return Pointer to a newly created jsonObject of type JSON_HASH, or NULL upon error.

	Look for a series of name/value pairs, separated by commas and terminated by a right
	curly brace.  Each name/value pair consists of a quoted string, followed by a colon,
	followed a JSON node of any sort.  Parse the value recursively.

	Collect the name/value pairs into a newly created jsonObject of type JSON_ARRAY, and
	return a pointer to it.

	Upon error, log an error message and return NULL.
*/
static jsonObject* get_hash( Parser* parser ) {
	jsonObject* hash = jsonNewObjectType( JSON_HASH );

	char c = skip_white_space( parser );
	if( '}' == c )
		return hash;           // Empty hash

	for( ;; ) {

		// Get the key string
		if( '"' != c ) {
			report_error( parser, c,
						  "Expected quotation mark to begin hash key; didn't find it\n" );
			jsonObjectFree( hash );
			return NULL;
		}

		const char* key = get_string( parser );
		if( ! key ) {
			jsonObjectFree( hash );
			return NULL;
		}
		char* key_copy = strdup( key );

		if( jsonObjectGetKeyConst( hash, key_copy ) ) {
			report_error( parser, '"', "Duplicate key in JSON object" );
			jsonObjectFree( hash );
			return NULL;
		}

		// Get the colon
		c = skip_white_space( parser );
		if( c != ':' ) {
			report_error( parser, c,
						  "Expected colon after hash key; didn't find it\n" );
			free( key_copy );
			jsonObjectFree( hash );
			return NULL;
		}

		// Get the associated value
		jsonObject* obj = get_json_node( parser, skip_white_space( parser ) );
		if( !obj ) {
			free( key_copy );
			jsonObjectFree( hash );
			return NULL;
		}

		// Add a new entry to the hash
		jsonObjectSetKey( hash, key_copy, obj );
		free( key_copy );

		// Look for comma or right brace
		c = skip_white_space( parser );
		if( '}' == c )
			break;
		else if( c != ',' ) {
			report_error( parser, c,
						  "Expected comma or brace in hash, didn't find it" );
			jsonObjectFree( hash );
			return NULL;
		}
		c = skip_white_space( parser );
	}

	return hash;
}
/**
	@brief Parse a hash (JSON object), and create a JSON_HASH for it; decode class hints.
	@param parser Pointer to a Parser.
	@return Pointer to a newly created jsonObject, or NULL upon error.

	This function is similar to get_hash(), @em except:

	If the hash includes a member with a key equal to JSON_CLASS_KEY ("__c" by default),
	then look for a member whose key is JSON_DATA_KEY ("__p" by default).  If you find one,
	return the data associated with it; otherwise return a jsonObject of type JSON_NULL.

	If there is no member with a key equal to JSON_CLASS_KEY, then return the same sort of
	jsonObject as get_hash() would return (except of course that lower levels may be
	decoded as described above).
*/
static jsonObject* get_decoded_hash( Parser* parser ) {
	jsonObject* hash = jsonNewObjectType( JSON_HASH );

	char c = skip_white_space( parser );
	if( '}' == c )
		return hash;           // Empty hash

	char* class_name = NULL;

	for( ;; ) {

		// Get the key string
		if( '"' != c ) {
			report_error( parser, c,
					"Expected quotation mark to begin hash key; didn't find it\n" );
			jsonObjectFree( hash );
			return NULL;
		}

		const char* key = get_string( parser );
		if( ! key ) {
			jsonObjectFree( hash );
			return NULL;
		}
		char* key_copy = strdup( key );

		if( jsonObjectGetKeyConst( hash, key_copy ) ) {
			report_error( parser, '"', "Duplicate key in JSON object" );
			jsonObjectFree( hash );
			return NULL;
		}

		// Get the colon
		c = skip_white_space( parser );
		if( c != ':' ) {
			report_error( parser, c,
					"Expected colon after hash key; didn't find it\n" );
			free( key_copy );
			jsonObjectFree( hash );
			return NULL;
		}

		// Get the associated value
		jsonObject* obj = get_json_node( parser, skip_white_space( parser ) );
		if( !obj ) {
			free( key_copy );
			jsonObjectFree( hash );
			return NULL;
		}

		// Add a new entry to the hash
		jsonObjectSetKey( hash, key_copy, obj );

		// Save info for class hint, if present
		if( !strcmp( key_copy, JSON_CLASS_KEY ) )
			class_name = jsonObjectToSimpleString( obj );

		free( key_copy );

		// Look for comma or right brace
		c = skip_white_space( parser );
		if( '}' == c )
			break;
		else if( c != ',' ) {
			report_error( parser, c,
					"Expected comma or brace in hash, didn't find it" );
			jsonObjectFree( hash );
			return NULL;
		}
		c = skip_white_space( parser );
	}

	if( class_name ) {
		// We found a class hint.  Extract the data node and return it.
		jsonObject* class_data = osrfHashExtract( hash->value.h, JSON_DATA_KEY );
		if( class_data ) {
			class_data->parent = NULL;
			jsonObjectFree( hash );
			hash = class_data;
			hash->parent = NULL;
			hash->classname = class_name;
		} else {
			// Huh?  We have a class name but no data for it.
			// Throw away what we have and return a JSON_NULL.
			jsonObjectFree( hash );
			hash = jsonNewObjectType( JSON_NULL );
		}

	} else {
		if( class_name )
			free( class_name );
	}

	return hash;
}
/**
	Resets the auth login timeout
	@return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
*/
static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
	if(!authToken) return NULL;

	oilsEvent* evt = NULL;
	time_t timeout;

	osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
	char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
	jsonObject* cacheObj = osrfCacheGetObject( key );

	if(!cacheObj) {
		osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
		evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);

	} else {

        if(reloadUser) {
            _oilsAuthReloadUser(cacheObj);
        }

		// Determine a new timeout value
		jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
		if( endtime_obj ) {
			// Extend the current endtime by a fixed amount
			time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
			int reset_interval = DEFAULT_RESET_INTERVAL;
			const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
				cacheObj, "reset_interval" );
			if( reset_interval_obj ) {
				reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
				if( reset_interval <= 0 )
					reset_interval = DEFAULT_RESET_INTERVAL;
			}

			time_t now = time( NULL );
			time_t new_endtime = now + reset_interval;
			if( new_endtime > endtime ) {
				// Keep the session alive a little longer
				jsonObjectSetNumber( endtime_obj, (double) new_endtime );
				timeout = reset_interval;
				osrfCachePutObject( key, cacheObj, timeout );
			} else {
				// The session isn't close to expiring, so don't reset anything.
				// Just report the time remaining.
				timeout = endtime - now;
			}
		} else {
			// Reapply the existing timeout from the current time
			timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
			osrfCachePutObject( key, cacheObj, timeout );
		}

		jsonObject* payload = jsonNewNumberObject( (double) timeout );
		evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
		jsonObjectFree(payload);
		jsonObjectFree(cacheObj);
	}

	free(key);
	return evt;
}
/**
	@brief Call the function that implements a specified method.
	@param appName Name of the application.
	@param methodName Name of the method.
	@param ses Pointer to the current application session.
	@param reqId The request id of the request invoking the method.
	@param params Pointer to a jsonObject encoding the parameters to the method.
	@return Zero if successful, or -1 upon failure.

	If we can't find a function corresponding to the method, or if we call it and it returns
	a negative return code, send a STATUS message to the client to report an exception.

	A return code of -1 means that the @a appName, @a methodName, or @a ses parameter was NULL.
*/
int osrfAppRunMethod( const char* appName, const char* methodName,
		osrfAppSession* ses, int reqId, jsonObject* params ) {

	if( !(appName && methodName && ses) ) return -1;

	// Find the application, and then find the method for it
	osrfApplication* app = _osrfAppFindApplication(appName);
	if( !app )
		return osrfAppRequestRespondException( ses,
				reqId, "Application not found: %s", appName );

	osrfMethod* method = osrfAppFindMethod( app, methodName );
	if( !method )
		return osrfAppRequestRespondException( ses, reqId,
				"Method [%s] not found for service %s", methodName, appName );

	#ifdef OSRF_STRICT_PARAMS
	if( method->argc > 0 ) {
		// Make sure that the client has passed at least the minimum number of arguments.
		if(!params || params->type != JSON_ARRAY || params->size < method->argc )
			return osrfAppRequestRespondException( ses, reqId,
				"Not enough params for method %s / service %s", methodName, appName );
	}
	#endif

	// Build an osrfMethodContext, which we will pass by pointer to the function.
	osrfMethodContext context;

	context.session = ses;
	context.method = method;
	context.params = params;
	context.request = reqId;
	context.responses = NULL;

	int retcode = 0;

	if( method->options & OSRF_METHOD_SYSTEM ) {
		retcode = _osrfAppRunSystemMethod(&context);

	} else {

		// Function pointer through which we will call the function dynamically
		int (*meth) (osrfMethodContext*);

		// Open the function that implements the method
		meth = dlsym(app->handle, method->symbol);

		const char* error = dlerror();
		if( error != NULL ) {
			return osrfAppRequestRespondException( ses, reqId,
				"Unable to execute method [%s] for service %s", methodName, appName );
		}

		// Run it
		retcode = meth( &context );
	}

	if(retcode < 0)
		return osrfAppRequestRespondException(
				ses, reqId, "An unknown server error occurred" );

	retcode = _osrfAppPostProcess( &context, retcode );

	if( context.responses )
		jsonObjectFree( context.responses );
	return retcode;
}
/**
	@brief Initialize the application by registering functions for method calls.
	@return Zero in all cases.
*/
int osrfAppInitialize() {

	osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");

	/* load and parse the IDL */
	if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */

	osrfAppRegisterMethod(
		MODULENAME,
		"open-ils.auth.authenticate.init",
		"oilsAuthInit",
		"Start the authentication process and returns the intermediate authentication seed"
		" PARAMS( username )", 1, 0 );

	osrfAppRegisterMethod(
		MODULENAME,
		"open-ils.auth.authenticate.complete",
		"oilsAuthComplete",
		"Completes the authentication process.  Returns an object like so: "
		"{authtoken : <token>, authtime:<time>}, where authtoken is the login "
		"token and authtime is the number of seconds the session will be active"
		"PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
		"type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
		"org_id is the location at which the login should be considered "
		"active for login timeout purposes", 1, 0 );

	osrfAppRegisterMethod(
		MODULENAME,
		"open-ils.auth.session.retrieve",
		"oilsAuthSessionRetrieve",
		"Pass in the auth token and this retrieves the user object.  The auth "
		"timeout is reset when this call is made "
		"Returns the user object (password blanked) for the given login session "
		"PARAMS( authToken )", 1, 0 );

	osrfAppRegisterMethod(
		MODULENAME,
		"open-ils.auth.session.delete",
		"oilsAuthSessionDelete",
		"Destroys the given login session "
		"PARAMS( authToken )",  1, 0 );

	osrfAppRegisterMethod(
		MODULENAME,
		"open-ils.auth.session.reset_timeout",
		"oilsAuthResetTimeout",
		"Resets the login timeout for the given session "
		"Returns an ILS Event with payload = session_timeout of session "
		"if found, otherwise returns the NO_SESSION event"
		"PARAMS( authToken )", 1, 0 );

	if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */

		jsonObject* value_obj;

		value_obj = osrf_settings_host_value_object(
			"/apps/open-ils.auth/app_settings/auth_limits/seed" );
		_oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
		jsonObjectFree(value_obj);
		if( -1 == _oilsAuthSeedTimeout ) {
			osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
			_oilsAuthSeedTimeout = 30;
		}

		value_obj = osrf_settings_host_value_object(
			"/apps/open-ils.auth/app_settings/auth_limits/block_time" );
		_oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
		jsonObjectFree(value_obj);
		if( -1 == _oilsAuthBlockTimeout ) {
			osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
			_oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
		}

		value_obj = osrf_settings_host_value_object(
			"/apps/open-ils.auth/app_settings/auth_limits/block_count" );
		_oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
		jsonObjectFree(value_obj);
		if( -1 == _oilsAuthBlockCount ) {
			osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
			_oilsAuthBlockCount = 10;
		}

		osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
			"seed => %ld : block_timeout => %ld : block_count => %ld",
			_oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
	}

	return 0;
}
/**
	@brief Implement the "complete" method.
	@param ctx The method context.
	@return -1 upon error; zero if successful, and if a STATUS message has been sent to the
	client to indicate completion; a positive integer if successful but no such STATUS
	message has been sent.

	Method parameters:
	- a hash with some combination of the following elements:
		- "username"
		- "barcode"
		- "password" (hashed with the cached seed; not plaintext)
		- "type"
		- "org"
		- "workstation"

	The password is required.  Either a username or a barcode must also be present.

	Return to client: Intermediate authentication seed.

	Validate the password, using the username if available, or the barcode if not.  The
	user must be active, and not barred from logging on.  The barcode, if used for
	authentication, must be active as well.  The workstation, if specified, must be valid.

	Upon deciding whether to allow the logon, return a corresponding event to the client.
*/
int oilsAuthComplete( osrfMethodContext* ctx ) {
	OSRF_METHOD_VERIFY_CONTEXT(ctx);

	const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);

	const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
	const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
	const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
	int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
	const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
	const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));

	const char* ws = (workstation) ? workstation : "";

	if( !type )
		 type = OILS_AUTH_STAFF;

	if( !( (uname || barcode) && password) ) {
		return osrfAppRequestRespondException( ctx->session, ctx->request,
			"username/barcode and password required for method: %s", ctx->method->name );
	}

	oilsEvent* response = NULL;
	jsonObject* userObj = NULL;
	int card_active     = 1;      // boolean; assume active until proven otherwise

	// Fetch a row from the actor.usr table, by username if available,
	// or by barcode if not.
	if(uname) {
		userObj = oilsUtilsFetchUserByUsername( uname );
		if( userObj && JSON_NULL == userObj->type ) {
			jsonObjectFree( userObj );
			userObj = NULL;         // username not found
		}
	}
	else if(barcode) {
		// Read from actor.card by barcode

		osrfLogInfo( OSRF_LOG_MARK, "Fetching user by barcode %s", barcode );

		jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
		jsonObject* card = oilsUtilsQuickReq(
			"open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
		jsonObjectFree( params );

		if( card && card->type != JSON_NULL ) {
			// Determine whether the card is active
			char* card_active_str = oilsFMGetString( card, "active" );
			card_active = oilsUtilsIsDBTrue( card_active_str );
			free( card_active_str );

			// Look up the user who owns the card
			char* userid = oilsFMGetString( card, "usr" );
			jsonObjectFree( card );
			params = jsonParseFmt( "[%s]", userid );
			free( userid );
			userObj = oilsUtilsQuickReq(
					"open-ils.cstore", "open-ils.cstore.direct.actor.user.retrieve", params );
			jsonObjectFree( params );
			if( userObj && JSON_NULL == userObj->type ) {
				// user not found (shouldn't happen, due to foreign key)
				jsonObjectFree( userObj );
				userObj = NULL;
			}
		}
	}

	if(!userObj) {
		response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
		osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
				uname, (barcode ? barcode : "(none)"), ws );
		osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
		oilsEventFree(response);
		return 0;           // No such user
	}

	// Such a user exists.  Now see if he or she has the right credentials.
	int passOK = -1;
	if(uname)
		passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
	else if (barcode)
		passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password );

	if( passOK < 0 ) {
		jsonObjectFree(userObj);
		return passOK;
	}

	// See if the account is active
	char* active = oilsFMGetString(userObj, "active");
	if( !oilsUtilsIsDBTrue(active) ) {
		if( passOK )
			response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
		else
			response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );

		osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
		oilsEventFree(response);
		jsonObjectFree(userObj);
		free(active);
		return 0;
	}
	free(active);

	osrfLogInfo( OSRF_LOG_MARK, "Fetching card by barcode %s", barcode );

	if( !card_active ) {
		osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
		response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
		osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
		oilsEventFree( response );
		jsonObjectFree( userObj );
		return 0;
	}


	// See if the user is even allowed to log in
	if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
		jsonObjectFree(userObj);
		return 0;
	}

	// If a workstation is defined, add the workstation info
	if( workstation != NULL ) {
		osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
		response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
		if(response) {
			jsonObjectFree(userObj);
			osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
			oilsEventFree(response);
			return 0;
		}

	} else {
		// Otherwise, use the home org as the workstation org on the user
		char* orgid = oilsFMGetString(userObj, "home_ou");
		oilsFMSetString(userObj, "ws_ou", orgid);
		free(orgid);
	}

	char* freeable_uname = NULL;
	if(!uname) {
		uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
	}

	if( passOK ) {
		response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );

	} else {
		response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
		osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
				uname, (barcode ? barcode : "(none)"), ws );
	}

	jsonObjectFree(userObj);
	osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
	oilsEventFree(response);

	if(freeable_uname)
		free(freeable_uname);

	return 0;
}
/**
	@brief Determine the login timeout.
	@param userObj Pointer to an object describing the user.
	@param type Pointer to one of four possible character strings identifying the login type.
	@param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
	@return The length of the timeout, in seconds.

	The default timeout value comes from the configuration file, and depends on the
	login type.

	The default may be overridden by a corresponding org unit setting.  The @a orgloc
	parameter says what org unit to use for the lookup.  If @a orgloc <= 0, or if the
	lookup for @a orgloc yields no result, we look up the setting for the user's home org unit
	instead (except that if it's the same as @a orgloc we don't bother repeating the lookup).

	Whether defined in the config file or in an org unit setting, a timeout value may be
	expressed as a raw number (i.e. all digits, possibly with leading and/or trailing white
	space) or as an interval string to be translated into seconds by PostgreSQL.
*/
static long oilsAuthGetTimeout( const jsonObject* userObj, const char* type, int orgloc ) {

	if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */

		jsonObject* value_obj;

		value_obj = osrf_settings_host_value_object(
			"/apps/open-ils.auth/app_settings/default_timeout/opac" );
		_oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
		jsonObjectFree(value_obj);
		if( -1 == _oilsAuthOPACTimeout ) {
			osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
			_oilsAuthOPACTimeout = 0;
		}

		value_obj = osrf_settings_host_value_object(
			"/apps/open-ils.auth/app_settings/default_timeout/staff" );
		_oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
		jsonObjectFree(value_obj);
		if( -1 == _oilsAuthStaffTimeout ) {
			osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
			_oilsAuthStaffTimeout = 0;
		}

		value_obj = osrf_settings_host_value_object(
			"/apps/open-ils.auth/app_settings/default_timeout/temp" );
		_oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
		jsonObjectFree(value_obj);
		if( -1 == _oilsAuthOverrideTimeout ) {
			osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
			_oilsAuthOverrideTimeout = 0;
		}

		value_obj = osrf_settings_host_value_object(
			"/apps/open-ils.auth/app_settings/default_timeout/persist" );
		_oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
		jsonObjectFree(value_obj);
		if( -1 == _oilsAuthPersistTimeout ) {
			osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
			_oilsAuthPersistTimeout = 0;
		}

		osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
			"opac => %ld : staff => %ld : temp => %ld : persist => %ld",
			_oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
			_oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
	}

	int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
	if(orgloc < 1)
		orgloc = home_ou;

	char* setting = NULL;
	long default_timeout = 0;

	if( !strcmp( type, OILS_AUTH_OPAC )) {
		setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
		default_timeout = _oilsAuthOPACTimeout;
	} else if( !strcmp( type, OILS_AUTH_STAFF )) {
		setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
		default_timeout = _oilsAuthStaffTimeout;
	} else if( !strcmp( type, OILS_AUTH_TEMP )) {
		setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
		default_timeout = _oilsAuthOverrideTimeout;
	} else if( !strcmp( type, OILS_AUTH_PERSIST )) {
		setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
		default_timeout = _oilsAuthPersistTimeout;
	}

	// Get the org unit setting, if there is one.
	char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
	if(!timeout) {
		if( orgloc != home_ou ) {
			osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
				"trying home_ou %d", orgloc, home_ou );
			timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
		}
	}

	if(!timeout)
		return default_timeout;   // No override from org unit setting

	// Translate the org unit setting to a number
	long t;
	if( !*timeout ) {
		osrfLogWarning( OSRF_LOG_MARK,
			"Timeout org unit setting is an empty string for %s login; using default",
			timeout, type );
		t = default_timeout;
	} else {
		// Treat timeout string as an interval, and convert it to seconds
		t = oilsUtilsIntervalToSeconds( timeout );
		if( -1 == t ) {
			// Unable to convert; possibly an invalid interval string
			osrfLogError( OSRF_LOG_MARK,
				"Unable to convert timeout interval \"%s\" for %s login; using default",
				timeout, type );
			t = default_timeout;
		}
	}

	free(timeout);
	return t;
}
/**
	@brief Verify the password received from the client.
	@param ctx The method context.
	@param userObj An object from the database, representing the user.
	@param password An obfuscated password received from the client.
	@return 1 if the password is valid; 0 if it isn't; or -1 upon error.

	(None of the so-called "passwords" used here are in plaintext.  All have been passed
	through at least one layer of hashing to obfuscate them.)

	Take the password from the user object.  Append it to the username seed from memcache,
	as stored previously by a call to the init method.  Take an md5 hash of the result.
	Then compare this hash to the password received from the client.

	In order for the two to match, other than by dumb luck, the client had to construct
	the password it passed in the same way.  That means it neded to know not only the
	original password (either hashed or plaintext), but also the seed.  The latter requirement
	means that the client process needs either to be the same process that called the init
	method or to receive the seed from the process that did so.
*/
static int oilsAuthVerifyPassword( const osrfMethodContext* ctx,
		const jsonObject* userObj, const char* uname, const char* password ) {

	// Get the username seed, as stored previously in memcache by the init method
	char* seed = osrfCacheGetString( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
	if(!seed) {
		return osrfAppRequestRespondException( ctx->session,
			ctx->request, "No authentication seed found. "
			"open-ils.auth.authenticate.init must be called first "
			" (check that memcached is running and can be connected to) "
		);
	}
    
	// We won't be needing the seed again, remove it
	osrfCacheRemove( "%s%s", OILS_AUTH_CACHE_PRFX, uname );

	// Get the hashed password from the user object
	char* realPassword = oilsFMGetString( userObj, "passwd" );

	osrfLogInternal(OSRF_LOG_MARK, "oilsAuth retrieved real password: [%s]", realPassword);
	osrfLogDebug(OSRF_LOG_MARK, "oilsAuth retrieved seed from cache: %s", seed );

	// Concatenate them and take an MD5 hash of the result
	char* maskedPw = md5sum( "%s%s", seed, realPassword );

	free(realPassword);
	free(seed);

	if( !maskedPw ) {
		// This happens only if md5sum() runs out of memory
		free( maskedPw );
		return -1;  // md5sum() ran out of memory
	}

	osrfLogDebug(OSRF_LOG_MARK,  "oilsAuth generated masked password %s. "
			"Testing against provided password %s", maskedPw, password );

	int ret = 0;
	if( !strcmp( maskedPw, password ) )
		ret = 1;

	free(maskedPw);

	char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, OILS_AUTH_COUNT_SFFX );
	jsonObject* countobject = osrfCacheGetObject( countkey );
	if(countobject) {
		long failcount = (long) jsonObjectGetNumber( countobject );
		if(failcount >= _oilsAuthBlockCount) {
			ret = 0;
		    osrfLogInfo(OSRF_LOG_MARK, "oilsAuth found too many recent failures for '%s' : %i, forcing failure state.", uname, failcount);
		}
		if(ret == 0) {
			failcount += 1;
		}
		jsonObjectSetNumber( countobject, failcount );
		osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
		jsonObjectFree(countobject);
	}
	free(countkey);

	return ret;
}
/**
	@brief Replace the jsonObject of an osrfConfig.
	@param cfg The osrfConfig to alter.
	@param obj The jsonObject to install in the osrfConfig.

	This is useful if you have a json object already, rather than an XML configuration file
	to parse.
*/
void osrfConfigReplaceConfig(osrfConfig* cfg, const jsonObject* obj) {
	if(!cfg || !obj) return;
	jsonObjectFree(cfg->config);
	cfg->config = jsonObjectClone(obj);
}
/**
 * Parse opensrf request and relay the request to the opensrf network.
 */
static size_t on_message_handler_body(void *data,
                const WebSocketServer *server, const int type, 
                unsigned char *buffer, const size_t buffer_size) {

    request_rec *r = server->request(server);

    jsonObject *msg_wrapper = NULL; // free me
    const jsonObject *tmp_obj = NULL;
    const jsonObject *osrf_msg = NULL;
    const char *service = NULL;
    const char *thread = NULL;
    const char *log_xid = NULL;
    char *msg_body = NULL;
    char *recipient = NULL;
    int i;

    if (buffer_size <= 0) return OK;

    // generate a new log trace for this request. it 
    // may be replaced by a client-provided trace below.
    osrfLogMkXid();

    osrfLogDebug(OSRF_LOG_MARK, "WS received message size=%d", buffer_size);

    // buffer may not be \0-terminated, which jsonParse requires
    char buf[buffer_size + 1];
    memcpy(buf, buffer, buffer_size);
    buf[buffer_size] = '\0';

    osrfLogInternal(OSRF_LOG_MARK, "WS received inbound message: %s", buf);

    msg_wrapper = jsonParse(buf);

    if (msg_wrapper == NULL) {
        osrfLogWarning(OSRF_LOG_MARK, "WS Invalid JSON: %s", buf);
        return HTTP_BAD_REQUEST;
    }

    osrf_msg = jsonObjectGetKeyConst(msg_wrapper, "osrf_msg");

    if (tmp_obj = jsonObjectGetKeyConst(msg_wrapper, "service")) 
        service = jsonObjectGetString(tmp_obj);

    if (tmp_obj = jsonObjectGetKeyConst(msg_wrapper, "thread")) 
        thread = jsonObjectGetString(tmp_obj);

    if (tmp_obj = jsonObjectGetKeyConst(msg_wrapper, "log_xid")) 
        log_xid = jsonObjectGetString(tmp_obj);

    if (log_xid) {

        // use the caller-provide log trace id
        if (strlen(log_xid) > MAX_THREAD_SIZE) {
            osrfLogWarning(OSRF_LOG_MARK, "WS log_xid exceeds max length");
            return HTTP_BAD_REQUEST;
        }

        osrfLogForceXid(log_xid);
    }

    if (thread) {

        if (strlen(thread) > MAX_THREAD_SIZE) {
            osrfLogWarning(OSRF_LOG_MARK, "WS thread exceeds max length");
            return HTTP_BAD_REQUEST;
        }

        // since clients can provide their own threads at session start time,
        // the presence of a thread does not guarantee a cached recipient
        recipient = (char*) apr_hash_get(
            trans->stateful_session_cache, thread, APR_HASH_KEY_STRING);

        if (recipient) {
            osrfLogDebug(OSRF_LOG_MARK, "WS found cached recipient %s", recipient);
        }
    }

    if (!recipient) {

        if (service) {
            int size = snprintf(recipient_buf, RECIP_BUF_SIZE - 1,
                "%s@%s/%s", trans->osrf_router, trans->osrf_domain, service);                                    
            recipient_buf[size] = '\0';                                          
            recipient = recipient_buf;

        } else {
            osrfLogWarning(OSRF_LOG_MARK, "WS Unable to determine recipient");
            return HTTP_BAD_REQUEST;
        }
    }

    osrfLogDebug(OSRF_LOG_MARK, 
        "WS relaying message to opensrf thread=%s, recipient=%s", 
            thread, recipient);

    msg_body = extract_inbound_messages(
        r, service, thread, recipient, osrf_msg);

    osrfLogInternal(OSRF_LOG_MARK, 
        "WS relaying inbound message: %s", msg_body);

    transport_message *tmsg = message_init(
        msg_body, NULL, thread, recipient, NULL);

    message_set_osrf_xid(tmsg, osrfLogGetXid());
    client_send_message(osrf_handle, tmsg);


    osrfLogClearXid();
    message_free(tmsg);                                                         
    jsonObjectFree(msg_wrapper);
    free(msg_body);

    last_activity_time = time(NULL);
    return OK;
}
void* osrf_responder_thread_main_body(transport_message *tmsg) {

    osrfList *msg_list = NULL;
    osrfMessage *one_msg = NULL;
    int i;

    osrfLogDebug(OSRF_LOG_MARK, 
        "WS received opensrf response for thread=%s", tmsg->thread);

    // first we need to perform some maintenance
    msg_list = osrfMessageDeserialize(tmsg->body, NULL);

    for (i = 0; i < msg_list->size; i++) {
        one_msg = OSRF_LIST_GET_INDEX(msg_list, i);

        osrfLogDebug(OSRF_LOG_MARK, 
            "WS returned response of type %d", one_msg->m_type);

        /*  if our client just successfully connected to an opensrf service,
            cache the sender so that future calls on this thread will use
            the correct recipient. */
        if (one_msg && one_msg->m_type == STATUS) {

            if (one_msg->status_code == OSRF_STATUS_OK) {

                if (!apr_hash_get(trans->stateful_session_cache, 
                        tmsg->thread, APR_HASH_KEY_STRING)) {

                    apr_size_t ses_size = 
                        apr_hash_count(trans->stateful_session_cache);

                    if (ses_size < MAX_ACTIVE_STATEFUL_SESSIONS) {

                        osrfLogDebug(OSRF_LOG_MARK, "WS caching sender "
                            "thread=%s, sender=%s; concurrent=%d", 
                            tmsg->thread, tmsg->sender, ses_size);

                        apr_hash_set(trans->stateful_session_cache, 
                            apr_pstrdup(trans->stateful_session_pool, tmsg->thread),
                            APR_HASH_KEY_STRING, 
                            apr_pstrdup(trans->stateful_session_pool, tmsg->sender));

                    } else {
                        osrfLogWarning(OSRF_LOG_MARK, 
                            "WS max concurrent sessions (%d) reached.  "
                            "Current session will not be tracked",
                            MAX_ACTIVE_STATEFUL_SESSIONS
                        );
                    }
                }

            } else {

                // connection timed out; clear the cached recipient
                if (one_msg->status_code == OSRF_STATUS_TIMEOUT) {
                    clear_cached_recipient(tmsg->thread);

                } else {
                    if (one_msg->status_code == OSRF_STATUS_COMPLETE)
                        requests_in_flight--;
                }
            }
        }
    }

    // osrfMessageDeserialize applies the freeItem handler to the 
    // newly created osrfList.  We only need to free the list and 
    // the individual osrfMessage's will be freed along with it
    osrfListFree(msg_list);
    
    // relay the response messages to the client
    jsonObject *msg_wrapper = NULL;
    char *msg_string = NULL;

    // build the wrapper object
    msg_wrapper = jsonNewObject(NULL);
    jsonObjectSetKey(msg_wrapper, "thread", jsonNewObject(tmsg->thread));
    jsonObjectSetKey(msg_wrapper, "log_xid", jsonNewObject(tmsg->osrf_xid));
    jsonObjectSetKey(msg_wrapper, "osrf_msg", jsonParseRaw(tmsg->body));

    if (tmsg->is_error) {
        osrfLogError(OSRF_LOG_MARK, 
            "WS received jabber error message in response to thread=%s", 
            tmsg->thread);
        jsonObjectSetKey(msg_wrapper, "transport_error", jsonNewBoolObject(1));
    }

    msg_string = jsonObjectToJSONRaw(msg_wrapper);

    // drop the JSON on the outbound wire
    trans->server->send(trans->server, MESSAGE_TYPE_TEXT, 
        (unsigned char*) msg_string, strlen(msg_string));

    free(msg_string);
    jsonObjectFree(msg_wrapper);
}