/** * Remote rater over HTTP * * Copyright (C) 2017 LoVullo Associates, Inc. * * This file is part of the Liza Data Collection Framework. * * liza is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * @todo it's used with a PHP endpoint, but doesn't have to be called * "HttpRater" */ var Class = require( 'easejs' ).Class, Rater = require( './Rater' ), querystring = require( 'querystring' ) ; /** * Rates using one of the PHP raters */ module.exports = Class( 'HttpRater' ) .implement( Rater ) .extend( { /** * Used to communicate with PHP web server * @type {http} */ 'private _client': null, /** * Currently logged in session (performing rating) * @type {UserSession} */ 'private _session': null, /** * Alias of rater to use if only a single rate is desired * @type {string} */ 'private _only': '', /** * Number of seconds before rating will abort with a timeout error * * This is intended to provide more useful information, rather than waiting * for a socket timeout. * * @var number */ 'private _timeout': 100, /** * Initialize rater with the id of the program * * @param {http} http_client Node.js http client * @param {UserSession} session current session (performing rating) * @param {string} host hostname, if different from host addr * @param {string} remote_path endpoint path on server */ __construct: function( http_client, session, host, remote_path ) { this._client = http_client; this._session = session; this._host = ( host ) ? ''+host : this._client.host; this._path = ''+remote_path; }, /** * Indicate that only the given alias should be used when rating * * @param {string} alias alias to rate with * * @return {HttpRater} self */ 'public only': function( alias ) { if ( !alias ) { return this; } this._only = ''+( alias ); return this; }, /** * Rate the provided quote * * The agent id is appended to the arguments for each request. This is * necessary, since we are making the request internally, not the broker. As * such, the session data will not be populated within PHP as it would * normally. We may wish for a better solution in the future (e.g. passing * the actual session id so we can simply load all of the data from within * PHP). * * Please note: args will be modified to append agent_id. It is not removed, * so be careful when passing objects to this method. * * @param {Quote} quote quote to rate * @param {function(err,data)} callback to call when complete * * @return {HttpRater} self */ 'public rate': function( quote, args, callback ) { var _self = this, // data to include in GET request (errdetail will allow us to see // the actual error message) data = querystring.stringify( { quoteId: quote.getId(), errdetail: 1, } ), path = ( this._path + '?' + data ) ; // append agent_id of current session and agent id associated with the // quote args.agent_id = this._session.agentId(); args.quote_agent_id = quote.getAgentId(); // we may wish to only retrieve the rates for a single alias if ( this._only ) { args.only = this._only; args.noautodefer = true; } // return an error if we do not receive a response within a // certain period of time var http_resp = null, req = null, timeout = setTimeout( function() { // if we have a response, close the connection http_resp && http_resp.end(); req && req.end(); callback( Error( 'Rating timeout' ), null ); }, ( _self._timeout * 1000 ) ); function _clientErr( err ) { clearTimeout( timeout ); callback( err, null ); }; function _cleanup() { clearTimeout( timeout ); _self._client.removeListener( 'error', _clientErr ); } this._client.once( 'error', _clientErr ); req = this._client.request( 'POST', path, { host: this._host } ) .on( 'response', function( response ) { var data = ''; http_resp = response; response .on( 'data', function( chunk ) { data += chunk.toString( 'utf8' ); } ) .on( 'end', function() { _cleanup(); _self._parseResponse( data, callback ); } ) .on( 'error', function( err ) { _cleanup(); callback( err, null ); } ) } ) .on( 'error', function( err ) { _cleanup(); callback( err, null ); } ) .end( JSON.stringify( args ) ); }, /** * Parses response from webserver * * The callback function will be called with any error (or null) as the * first argument and the response data (or null if error) as the second. * This is a common convention for callbacks since thorwing exceptions does * not work well with asynchronous calls (since async only executed after * stack has been cleared). * * @param {string} data_str response string from webserver * @param {function(err, data)} callback function to call with response * * @return {undefined} */ 'private _parseResponse': function( data_str, callback ) { var error = null, retdata = null; try { data = JSON.parse( data_str ); // if the server responded with an error, return it if ( data.hasError !== false ) { error = Error( 'Server responded with error: ' + ( data.content || '(empty)' ) ); } else { // return the content of the response rather than the entire // response (we treat the rest a bit like one would treat a // header) retdata = data.content; } } catch ( e ) { error = TypeError( 'Invalid JSON provided: ' + data_str ); } callback( error, retdata ); }, /** * Sets number of seconds before rating attempt will time out and trigger a * rating error * * @param {number} seconds timeout in seconds * * @return {HttpRater} self */ 'public setTimeout': function( seconds ) { this._timeout = +seconds; return this; }, } );