blob: 2d7244c56d955e585b7ee8b3ff497d36e645715c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function() {
'use strict';
/* global myApp */
/* global chrome */
myApp.factory('HttpServer', ['$q', function($q) {
var DEFAULT_PORT = 2424;
var STATE_NEW = 0;
var STATE_REQUEST_DATA_RECEIVED = 1;
var STATE_HEADERS_RECEIVED = 2;
var STATE_REQUEST_RECEIVED = 3;
var STATE_RESPONSE_STARTED = 4;
var STATE_RESPONSE_WAITING_FOR_FLUSH = 5;
var STATE_COMPLETE = 6;
function changeState(requestData, newState) {
if (newState <= requestData.state) {
throw new Error('Socket ' + requestData.socket.socketId + ' state error: ' + requestData.state + '->' + newState);
}
console.log('Socket ' + requestData.socket.socketId + ' state ' + requestData.state + '->' + newState);
requestData.state = newState;
}
function ResponseException(code, /* optional */ responseText) {
this.code = code;
this.responseText = responseText;
}
function HttpRequest(requestData) {
this._requestData = requestData;
this.method = requestData.method;
this.headers = requestData.headers;
this.bytesRemaining = 0;
this._readChunkCalled = false;
if (requestData.method == 'POST' || requestData.method == 'PUT') {
this.bytesRemaining = parseInt(requestData.headers['content-length'] || '0');
}
if (this.bytesRemaining === 0) {
changeState(this._requestData, STATE_REQUEST_RECEIVED);
}
var host = this.headers['host'] || 'localhost';
var queryMatch = /\?.*/.exec(requestData.resource);
this.url = 'http://' + host + requestData.resource;
this.path = requestData.resource.replace(/\?.*/, '');
this.query = queryMatch ? queryMatch[0] : '';
}
HttpRequest.prototype.getQueryParam = function(name) {
var pattern = new RegExp('[\\?&]' + name + '=([^&#]*)');
var m = pattern.exec(this.query);
return m && decodeURIComponent(m[1]);
};
HttpRequest.prototype.readAsJson = function() {
var self = this;
return this.readEntireBody()
.then(function(arrayBuffer) {
var s = arrayBufferToString(arrayBuffer);
return JSON.parse(s);
}).then(null, function(e) {
return self._requestData.httpResponse.sendTextResponse(400, 'Invalid JSON received.\n')
.then(function() {
throw e;
});
});
};
HttpRequest.prototype.readEntireBody = function() {
var byteArray = null;
var soFar = 0;
var self = this;
function handleChunk(chunk) {
if (byteArray) {
byteArray.set(chunk, soFar);
soFar += chunk.byteLength;
}
if (self.bytesRemaining === 0) {
return byteArray ? byteArray.buffer : chunk;
}
return self.readChunk().then(handleChunk);
}
return this.readChunk().then(handleChunk);
};
HttpRequest.prototype.readChunk = function(/* optional */maxChunkSize) {
// Allow readChunk() to be called *once* after request is already received.
// This is convenient for empty payloads.
if (this._requestData.state === STATE_REQUEST_RECEIVED) {
if (this._readChunkCalled) {
throw new Error('readChunk() when request already received.');
}
this._readChunkCalled = true;
if (this.bytesRemaining === 0) {
return $q.when(new ArrayBuffer(0));
}
}
var self = this;
return this._requestData.socket.read(maxChunkSize)
.then(function(chunk) {
var chunkSize = chunk.byteLength;
console.log('Processing request chunk of size ' + chunkSize);
self.bytesRemaining -= chunkSize;
if (self.bytesRemaining < 0) {
throw new Error('Bytes remaining negative: ' + self.bytesRemaining);
}
if (self.bytesRemaining === 0 && self._requestData.state === STATE_HEADERS_RECEIVED) {
changeState(self._requestData, STATE_REQUEST_RECEIVED);
}
return chunk;
});
};
function HttpResponse(requestData) {
this._requestData = requestData;
this.headers = Object.create(null);
var keepAlive = requestData.headers['connection'] === 'keep-alive';
this.headers['Connection'] = keepAlive ? 'keep-alive' : 'close';
var self = this;
requestData.socket.onClose = function(err) {
if (err) {
console.error(err);
}
self._finish(!!err);
};
}
HttpResponse.prototype.sendTextResponse = function(status, message, /* optional */ contentType) {
this.headers['Content-Type'] = contentType || 'text/plain';
this.headers['Content-Length'] = message.length;
this._startResponse(status);
this.writeChunk(stringToArrayBuffer(message));
return this.close();
};
HttpResponse.prototype.sendJsonResponse = function(status, json) {
return this.sendTextResponse(200, JSON.stringify(json, null, 4), 'application/json');
};
HttpResponse.prototype.writeChunk = function(arrayBuffer) {
if (this._requestData.state !== STATE_RESPONSE_STARTED) {
this._startResponse(200);
}
var promise = this._requestData.socket.write(arrayBuffer);
if (!arrayBuffer) {
changeState(this._requestData, STATE_RESPONSE_WAITING_FOR_FLUSH);
var self = this;
promise = promise.then(function() {
self._finish();
});
}
return promise;
};
HttpResponse.prototype.close = function() {
if (this._requestData.state < STATE_RESPONSE_WAITING_FOR_FLUSH) {
return this.writeChunk(null);
}
};
HttpResponse.prototype._startResponse = function(status) {
var headers = this.headers;
// Check if they haven't finished reading the request, and error out.
if (this._requestData.state < STATE_REQUEST_RECEIVED) {
this._requestData.socket.close(new Error('Started to write response before request data was finished.'));
return;
}
changeState(this._requestData, STATE_RESPONSE_STARTED);
var statusMsg = status === 404 ? 'Not Found' :
status === 400 ? 'Bad Request' :
status === 200 ? 'OK' :
'meh';
var lines = ['HTTP/1.1 ' + status + ' ' + statusMsg];
Object.keys(headers).forEach(function(k) {
lines.push(k + ': ' + headers[k]);
});
lines.push('', '');
this.writeChunk(stringToArrayBuffer(lines.join('\r\n')));
};
HttpResponse.prototype._finish = function(disconnect) {
if (this._requestData.state === STATE_COMPLETE) {
return;
}
changeState(this._requestData, STATE_COMPLETE);
this._requestData.socket.onClose = null;
var socketId = this._requestData.socket.socketId;
if (typeof disconnect == 'undefined') {
disconnect = (this.headers['Connection'] || '').toLowerCase() != 'keep-alive';
}
delete this._requestData.httpServer._requests[socketId];
if (disconnect) {
this._requestData.socket.close();
} else {
this._requestData.httpServer._onAccept(socketId);
}
};
function Socket(socketId) {
this.socketId = socketId;
this.alive = true;
this.onClose = null;
this._pendingReadChunk = null;
this._writeQueue = [];
this._readInProgress = false;
}
Socket.prototype.unread = function(chunk) {
if (this._pendingReadChunk) {
throw new Error('Socket.unread called multiple times.');
}
this._pendingReadChunk = chunk;
};
Socket.prototype.read = function(maxLength) {
if (this._readInProgress) {
throw new Error('Read already in progress.');
}
this._readInProgress = true;
maxLength = maxLength || Infinity;
var self = this;
var deferred = $q.defer();
var bufSize = Math.min(200 * 1024, maxLength);
var chunk = this._pendingReadChunk;
if (chunk) {
self._readInProgress = false;
if (chunk.byteLength <= maxLength) {
this._pendingReadChunk = null;
deferred.resolve(chunk);
} else {
this._pendingReadChunk = chunk.slice(maxLength);
deferred.resolve(chunk.slice(0, maxLength));
}
} else {
chrome.socket.read(this.socketId, bufSize, function(readInfo) {
self._readInProgress = false;
if (!readInfo.data) {
var err = new Error('Socket.read() failed with code ' + readInfo.resultCode);
self.close(err);
deferred.reject(err);
} else {
deferred.resolve(readInfo.data);
}
});
}
return deferred.promise;
};
// Multiple writes in are allowed at a time.
// A null arrayBuffer can be used as a synchronization point.
Socket.prototype.write = function(arrayBuffer) {
var deferred = $q.defer();
this._writeQueue.push(arrayBuffer, deferred);
if (this._writeQueue.length === 2) {
this._pokeWriteQueue();
}
return deferred.promise;
};
Socket.prototype.close = function(/*(optional*/ error) {
if (this.alive) {
this.alive = false;
chrome.socket.destroy(this.socketId);
if (this.onClose) {
this.onClose(error);
}
}
};
Socket.prototype._pokeWriteQueue = function() {
if (this._writeQueue.length === 0) {
return;
}
var arrayBuffer = this._writeQueue[0];
var deferred = this._writeQueue[1];
if (arrayBuffer && arrayBuffer.byteLength > 0) {
var self = this;
chrome.socket.write(this.socketId, arrayBuffer, function(writeInfo) {
if (writeInfo.bytesWritten !== arrayBuffer.byteLength) {
console.warn('Failed to write entire ArrayBuffer.');
}
self._writeQueue.shift();
self._writeQueue.shift();
if (writeInfo.bytesWritten < 0) {
var err = new Error('Write error: ' + -writeInfo.bytesWritten);
deferred.reject(err);
self.close(err);
} else {
deferred.resolve();
self._pokeWriteQueue();
}
});
} else {
this._writeQueue.shift();
this._writeQueue.shift();
deferred.resolve();
this._pokeWriteQueue();
}
};
function HttpServer() {
this._requests = Object.create(null); // Map of socketId -> Object
this._handlers = Object.create(null); // Map of resourcePath -> function(httpRequest, httpResponse)
}
HttpServer.prototype.addRoute = function(path, func) {
this._handlers[path] = func;
return this;
};
HttpServer.prototype.start = function(/* optional */ port) {
port = port || DEFAULT_PORT;
var deferred = $q.defer();
var boundAccept = this._onAccept.bind(this);
console.log('Starting web server on port ' + port);
chrome.socket.create('tcp', function(createInfo) {
if (!createInfo) {
console.error('Failed to create socket: ' + chrome.runtime.lastError);
deferred.reject(new Error('Failed to create socket: ' + chrome.runtime.lastError));
return;
}
chrome.socket.listen(createInfo.socketId, '0.0.0.0', port, function(result) {
if (result === 0) {
acceptLoop(createInfo.socketId, boundAccept);
deferred.resolve();
} else {
console.error('Error on socket.listen: ' + result);
deferred.reject(new Error('Error on socket.listen: ' + result));
}
});
});
return deferred.promise;
};
function acceptLoop(socketId, acceptCallback) {
chrome.socket.accept(socketId, function(acceptInfo) {
acceptCallback(acceptInfo.socketId);
acceptLoop(socketId, acceptCallback);
});
}
HttpServer.prototype._onAccept = function(socketId) {
console.log('Connection established on socket ' + socketId);
var requestData = {
state: STATE_NEW,
socket: new Socket(socketId),
dataAsStr: '', // Used only when parsing head of request.
method: null,
resource: null,
httpVersion: null,
headers: null,
httpServer: this,
httpResponse: null,
httpRequest: null
};
this._requests[socketId] = requestData;
var self = this;
return readRequestHeaders(requestData)
.then(function() {
var req = new HttpRequest(requestData);
var resp = new HttpResponse(requestData);
requestData.httpRequest = req;
requestData.httpResponse = resp;
// Strip query params.
var handler = self._handlers[req.path];
if (handler) {
// Wrap to catch exceptions.
return $q.when().then(function() {
return handler(req, resp);
}).then(function() {
if (requestData.state < STATE_RESPONSE_WAITING_FOR_FLUSH) {
if (requestData.state == STATE_REQUEST_RECEIVED) {
console.warn('No response was sent for action ' + requestData.resource);
return resp.sendTextResponse(200, '');
} else {
return requestData.socket.close();
}
}
}, function(err) {
console.error('Error while handling ' + req.path, err);
if (requestData.state !== STATE_RESPONSE_WAITING_FOR_FLUSH) {
if (requestData.state < STATE_RESPONSE_STARTED) {
return req.readEntireBody()
.then(function() {
if (err instanceof ResponseException) {
return resp.sendTextResponse(err.code, (err.responseText || '') + '\n');
}
return resp.sendTextResponse(500, '' + err + '\n');
});
} else {
return requestData.socket.close();
}
}
});
}
return resp.sendTextResponse(404, 'Not Found');
});
};
function stringToArrayBuffer(str) {
var view = new Uint8Array(str.length);
for (var i = 0; i < str.length; i++) {
view[i] = str.charCodeAt(i);
}
return view.buffer;
}
function arrayBufferToString(buffer) {
var str = '';
var uArrayVal = new Uint8Array(buffer);
for (var s = 0; s < uArrayVal.length; s++) {
str += String.fromCharCode(uArrayVal[s]);
}
return str;
}
function readRequestHeaders(requestData) {
return requestData.socket.read()
.then(function(arrayBuffer) {
var oldLen = requestData.dataAsStr.length;
var newData = arrayBufferToString(arrayBuffer);
var splitPoint;
requestData.dataAsStr += newData;
if (requestData.state === STATE_NEW) {
splitPoint = requestData.dataAsStr.indexOf('\r\n');
if (splitPoint > -1) {
var requestDataLine = requestData.dataAsStr.substring(0, splitPoint);
requestData.dataAsStr = '';
arrayBuffer = arrayBuffer.slice(splitPoint + 2 - oldLen);
var requestDataParts = requestDataLine.split(' ');
requestData.method = requestDataParts[0].toUpperCase();
requestData.resource = requestDataParts[1];
requestData.httpVersion = requestDataParts[2];
console.log('Socket ' + requestData.socket.socketId + ': ' + requestData.method + ' ' + requestData.resource);
changeState(requestData, STATE_REQUEST_DATA_RECEIVED);
requestData.socket.unread(arrayBuffer);
return readRequestHeaders(requestData);
}
} else {
splitPoint = requestData.dataAsStr.indexOf('\r\n\r\n');
if (splitPoint > -1) {
requestData.headers = parseHeaders(requestData.dataAsStr.substring(0, splitPoint));
requestData.dataAsStr = '';
arrayBuffer = arrayBuffer.slice(splitPoint + 4 - oldLen);
changeState(requestData, STATE_HEADERS_RECEIVED);
requestData.socket.unread(arrayBuffer);
return requestData;
}
}
return readRequestHeaders(requestData);
});
}
function strip(str) {
return str.replace(/^\s*|\s*$/g, '');
}
function parseHeaders(headerText) {
var headers = Object.create(null);
var headerLines = headerText.split('\r\n');
var currentKey;
for (var i = 0; i < headerLines.length; i++) {
if (/^\s/.test(headerLines[i])) {
if (!currentKey) {
break;
}
headers[currentKey] += ' ' + strip(headerLines[i]);
} else {
var splitPoint = headerLines[i].indexOf(':');
if (splitPoint == -1) {
break;
}
currentKey = strip(headerLines[i].substring(0,splitPoint).toLowerCase());
headers[currentKey] = strip(headerLines[i].substring(splitPoint+1));
}
}
return headers;
}
HttpServer.ResponseException = ResponseException;
return HttpServer;
}]);
})();