1
0
Fork 0
liza/src/server/request/UserRequest.js

572 lines
12 KiB
JavaScript

/**
* UserRequest class
*
* 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 <http://www.gnu.org/licenses/>.
*/
var Class = require( 'easejs' ).Class;
/**
* Encapsulates user request and response data in an easy-to-use class
*
* This class doesn't really add any new functionality. It just makes working
* with requests and responses a bit easier.
*/
module.exports = Class.extend( require( 'events' ).EventEmitter,
{
/**
* Request timeout in seconds
*
* In the future, we can make this configurable. Currently, it's
* unnecessary. Feel free to add such a feature if you have need for it.
*
* @type {number}
*/
'const TIMEOUT': 120,
/**
* Requested URI
* @var String
*/
uri: '',
/**
* Query (GET vars)
* @var {Object}
*/
query: {},
/**
* Request object
* @var http.ServerRequest
*/
request: null,
/**
* Response object
* @var http.ServerResponse
*/
response: null,
/**
* Contains post data, when available
* @var undefined|Object
*/
postData: undefined,
/**
* Functions to call when post data is available
* @var Array
*/
postDataCallbacks: [],
/**
* HTTP status code to respond with (default 200 OK)
* @var Integer
*/
responseCode: 200,
/**
* HTTP MIME Content type (text/html by default)
* @var String
*/
contentType: 'text/html; charset=utf-8',
/**
* Headers to send
* @var {Object}
*/
headers: {},
/**
* Whether the headers have been sent
* @var Boolean
*/
headersSent: false,
/**
* Length of response
*
* @var Integer
*/
responseLength: 0,
/**
* User's session
* @var UserSession
*/
session: null,
/**
* Timer that will cause a timeout after TIMEOUT seconds
* @type {!number}
*/
'private _timeout': null,
/**
* String representation of object
*
* @return String
*/
toString: function()
{
return '[object UserRequest]';
},
/**
* Constructor
*
* @return undefined
*/
__construct: function( request, response, session_builder )
{
var request_data = require( 'url' ).parse( request.url, true );
this.uri = request_data.pathname;
this.query = request_data.query || {};
this.request = request;
this.response = response;
this._initRequestPost();
// "session key" used internally for certain scripts
var skey = this.query.skey;
// initialize session
var _self = this;
this.session = session_builder( this.getCookies().PHPSESSID )
.on( 'ready', function( data )
{
// if no data is available, then the session could not be
// initialized; abort the request :( (this should be ignored if
// a session key is provided, since in that case we do not care
// about a session)
if ( ( data === null ) && !skey )
{
_self.setResponseCode( 500 );
_self.end( 'Session initialization failure.' );
return;
}
// now we're ready to roll
_self.emit( 'ready' );
} );
// set timeout in the event we fail to respond due to some bug/uncaught
// exception/etc
this._timeout = setTimeout(
function()
{
_self.setResponseCode( 408 );
_self.end( 'Request timed out.' );
},
( this.__self.$( 'TIMEOUT' ) * 1000 )
);
},
/**
* Watches for post data and returns the data to any waiting callbacks
*
* @return undefined
*/
_initRequestPost: function()
{
var querystring = require( 'querystring' ),
post_raw = '',
request = this;
this.request
.addListener( 'data', function( data )
{
post_raw += data;
})
.addListener( 'end', function()
{
request.postData = querystring.parse( post_raw );
// call any callbacks that are waiting for the data
var func = null;
while ( func = request.postDataCallbacks.pop() )
{
func.call( request, request.postData );
}
});
},
/**
* Performs general initialization tasks (template method)
*
* @return undefined
*/
_init: function( session_builder )
{
var request = this;
this._initRequestPost();
},
/**
* Returns the requested URI
*
* @return String requested URI
*/
getUri: function()
{
return this.uri;
},
/**
* Returns query (GET) data
*
* @return Object GET data
*/
getGetData: function()
{
return this.query;
},
/**
* Requests the post data
*
* This is asynchronous. If the data is already available, the callback will
* be called immediately. If the data is not yet available, it will be
* called as soon as it becomes available.
*
* @param Function( data ) callback function to call when data is available
*
* @return UserRequest self to allow for method chaining
*/
getPostData: function( callback )
{
// if we already have the post data, give it to them immediately
if ( this.postData !== undefined )
{
callback.call( this, this.postData );
return this;
}
// otherwise, we need to call the callback when the data is available
this.postDataCallbacks.push( callback );
return this;
},
/**
* Sets the HTTP status code to respond with
*
* @param Integer code HTTP status code
*
* @return UserRequest self
*/
setResponseCode: function( code )
{
if ( this.headersSent === true )
{
console.error( 'Headers already sent; response code not set' );
return this;
}
this.responseCode = +code;
return this;
},
/**
* Returns the HTTP status code sent to the client
*
* @return Integer HTTP status code
*/
getResponseCode: function()
{
return this.responseCode;
},
/**
* Sets the content type
*
* @param String type content type
*
* @return UserRequest self
*/
setContentType: function( type )
{
this.contentType = ''+type;
return this;
},
/**
* Sets HTTP headers to send to the client
*
* The headers provided will be merged with any existing headers. They will
* be overwritten if they have already been set.
*
* @param {Object} data headers to set (key-value)
*
* @return {UserRequest} self
*/
setHeaders: function( data )
{
for ( header in data )
{
this.headers[ header ] = data[ header ];
}
return this;
},
/**
* Tells the client not to cache the response
*
* @return {UserRequest} self
*/
noCache: function()
{
// the first two are for IE6, the others are HTTP/1.0
this.headers[ 'Cache-Control' ] =
'private, max-age=0, no-store, no-cache, must-revalidate, ' +
'post-check=0, pre-check=0';
return this;
},
/**
* Send headers to the client
*
* @return undefined
*/
_sendHeaders: function()
{
this.headers[ 'Content-Type' ] = this.contentType;
this.response.writeHead( this.responseCode, this.headers );
this.headersSent = true;
// we don't need this function anymore
this._sendHeaders = function() {}
},
/**
* Write data to the client
*
* @param String chunk data to write
* @param String encoding
*
* @return UserRequest self
*/
write: function( chunk, encoding )
{
encoding = encoding || 'utf8';
this.responseLength += chunk.length;
this._sendHeaders();
this.response.write( chunk, encoding );
return this;
},
error: function( error, data, encoding )
{
this.setResponseCode( 503 );
this.end(
JSON.stringify( {
error: error,
data: data
} ),
encoding
);
},
tryAgain: function( data, encoding )
{
this.setResponseCode( 503 );
this.end(
JSON.stringify( {
error: 'EAGAIN',
data: data
} ),
encoding
);
},
ok: function( data, encoding )
{
this.setResponseCode( 200 );
this.end(
JSON.stringify( {
error: null,
data: data,
} )
);
},
accepted: function( data, encoding )
{
this.setResponseCode( 202 );
this.end(
JSON.stringify( {
error: null,
data: data,
} )
);
},
/**
* End client response
*
* @param String chunk data to write
* @param String encoding
*
* @return UserRequest self
*/
end: function( data, encoding )
{
data = data || '';
clearTimeout( this._timeout );
this._timeout = null;
this.responseLength += data.length;
this._sendHeaders();
this.response.end( data, encoding );
this.emit( 'end' );
return this;
},
/**
* Returns the length of the response
*
* Note that this is not very accurate, as this doesn't take into account
* multi-byte characters.
*
* @return Integer response length
*
* @todo multibyte
*/
getResponseLength: function()
{
return this.responseLength;
},
/**
* Returns request cookies as an object
*
* @return Object request cookies
*/
getCookies: function()
{
var cookies = {},
cookie_data = this.request.headers.cookie;
if ( cookie_data === undefined )
{
return {};
}
// parse the cookies into an easily accessible object
cookie_data.split( ';' ).forEach( function( val )
{
var data = val.split( '=', 2 );
cookies[ data[0].trim() ] = data[1];
});
// any future calls to this function will simply return the already
// generated cookies array to save us some time
this.getCookies = function()
{
return cookies;
}
return cookies;
},
/**
* Returns the current session
*
* @return UserSession
*/
getSession: function()
{
return this.session;
},
/**
* Returns the request object
*
* @return http.ServerRequest
*/
getRequest: function()
{
return this.request;
},
'public getRemoteAddr': function()
{
// since we may be proxied, let the proxy forward header take precidence
return this.request.headers['x-forwarded-for']
|| this.request.connection.remoteAddress;
},
'public getUserAgent': function()
{
return this.request.headers['user-agent'];
},
'public getSessionId': function()
{
return this.getCookies().PHPSESSID || null;
},
'public getSessionIdName': function()
{
return 'PHPSESSID';
}
} );