Skip to content

Commit

Permalink
revisited parsing of http requests & responses using HttpMirror
Browse files Browse the repository at this point in the history
  • Loading branch information
gr3p1p3 committed Sep 14, 2023
1 parent b09dc4c commit 1db6be4
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 142 deletions.
3 changes: 2 additions & 1 deletion ProxyServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ class ProxyServer extends net.createServer {
auth, intercept, keys,
handleSni, interceptOptions
},
logger || new Logger(verbose))
logger || new Logger(verbose));
});

this.bridgedConnections = bridgedConnections;

//TODO this is an horrible workaround, but extending doesn't work
Expand Down
156 changes: 156 additions & 0 deletions core/HttpMirror.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
const http = require('http');
const net = require('net');

const {STRINGS} = require('../lib/constants');
const {CRLF, TRANSFER_ENCODING, CONTENT_LENGTH, CHUNKED, ZERO} = STRINGS;

module.exports = class HttpMirror {
constructor(session) {
this.server = http.createServer();
// this.client = http.request;
this.isListening = false;
this._session = session;
this._mirror = {
server: {request: false, response: false},
client: {request: false, response: false}
};
}

async close() {
return new Promise((resolve) => {
if (this.isListening) {
this.server.close(resolve);
return true;
}
resolve(false);
return false;
});

}

listen() {
return new Promise((resolve, reject) => {
if (this.isListening) {
resolve(true);
return;
}
//starting http-server on random port
this.server.listen(0, 'localhost', () => {
this.isListening = true;
resolve(true);
});
});
}

waitForRequest(data) {
const session = this._session;

return new Promise(async (resolve) => {
const serverInfo = this.server.address();
const config = {host: serverInfo.address, port: serverInfo.port};

if (!this._mirror?.client?.request) {
this._mirror.client.request = net.createConnection(config);

this.server
.once('connect', (request, clientSocket) => {
const {method, url, headers, httpVersion, trailers} = request;
this._mirror.client.request = false;
clientSocket.destroy();
resolve({method, url, headers, httpVersion, trailers});
})
.once('request', (request, response) => {
this._mirror.client.requesthandler = request;
this._mirror.client.response = response;
const {method, url, headers, httpVersion, trailers} = request;

request.on('data', (chunk) => {
session._requestCounter++;
session._rawRequestBodyChunks.push(chunk);
})
.once('data', () => {
resolve({method, url, headers, httpVersion, trailers});
})
.once('end', () => {
this._mirror.client.response.end();
this._mirror.client.request.destroy();
this._mirror.client.requesthandler.destroy();
session._request.complete = true;
resolve({method, url, headers, httpVersion, trailers});
});
});
return this._mirror.client.request.write(data);
}
else {
const {method, url, headers, httpVersion, trailers} = this._mirror.client.requestHandler;

this._mirror.client.requestHandler
.once('data', () => {
resolve({method, url, headers, httpVersion, trailers});
})
.once('end', () => {
resolve({method, url, headers, httpVersion, trailers});
});
return this._mirror.client.request.write(data);
}
});
}

waitForResponse(data) {
const session = this._session;
return new Promise((resolve) => {
if (!this._mirror?.server?.response) {
this.server.once('request', (request, response) => {
this._mirror.server.response = response;
response.socket.write(data); //dumping TCP data
});

const {address, port} = this.server.address();
const request = session._request;
const options = {method: request.method, headers: request.headers, path: '/', host: address, port};
delete options.headers.host;

http.request(options, (response) => {
const {headers, httpVersion, statusCode} = response;
this._mirror.server.request = response;

response
.on('data', (chunk) => {
session._responseCounter++;
session._rawResponseBodyChunks.push(chunk);
if (headers?.[CONTENT_LENGTH]) {
const bodyBytes = Buffer.byteLength(session.rawResponse);
session._response.complete = parseInt(headers[CONTENT_LENGTH]) <= bodyBytes;
}
})
.once('data', (chunk) => {
resolve({headers, httpVersion, statusCode});
})
.once('close', async () => {
session._response.complete = true;
this._mirror?.server?.request?.destroy();
this._mirror?.server?.response?.end();
await this.close(); //closing this instance at the end of response
resolve({headers, httpVersion, statusCode});
});

setTimeout(() => {
resolve({headers, httpVersion, statusCode}); //resolving in case of no data after 10ms
}, 10);
})
.end();
}
else {
const {headers, httpVersion, statusCode} = this._mirror.server.request;
this._mirror.server.request
.once('data', () => {
resolve({headers, httpVersion, statusCode});
})
.once('close', () => {
resolve({headers, httpVersion, statusCode});
});
return this._mirror.server.response.socket.write(data); //dumping TCP data to existing response-Object
}
});
}
};
149 changes: 41 additions & 108 deletions core/Session.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
const net = require('net');
const tls = require('tls');
const {EVENTS, DEFAULT_KEYS, STRINGS, HTTP_METHODS} = require('../lib/constants');
const parseDataToObject = require('../lib/parseDataToObject');
const {EVENTS, DEFAULT_KEYS} = require('../lib/constants');
const {CLOSE, DATA, ERROR} = EVENTS;
const {BINARY_ENCODING, CRLF, TRANSFER_ENCODING, CONTENT_LENGTH, CHUNKED, ZERO} = STRINGS;
const NOT_HEX_VALUE = /[^0-9A-Fa-f]/g;

/**
* Write data of given socket
* @param {net.Socket} socket
* @param data
*/
function socketWrite(socket, data) {
return new Promise(function (resolve, reject) {
if (socket && !socket.destroyed && data) {
return socket.write(data, null, resolve);
}
return resolve(false);
});
}
const socketWrite = require('../lib/socketWrite');
const HttpMirror = require('./HttpMirror');

/**
* Destroy the socket
Expand Down Expand Up @@ -52,7 +39,10 @@ class Session {
this._isResponsePaused = false;

this._rawResponseBodyChunks = [];
this._rawRequestBodyChunks = [];
this._interceptOptions = interceptOptions;

this._httpMirror = new HttpMirror(this);
}

/**
Expand Down Expand Up @@ -114,6 +104,9 @@ class Session {
* @returns {Session}
*/
destroy() {
if (this._httpMirror.isListening) {
this._httpMirror.close();
}
if (this._dst) {
socketDestroy(this._dst);
}
Expand Down Expand Up @@ -151,6 +144,23 @@ class Session {
return this;
}

async sendToMirror(data, isResponse = false) {
await this._httpMirror.listen(); //this will happen only once

if (!this.isHttps || this._updated) {
if (!isResponse) {
const request = await this._httpMirror.waitForRequest(data); //waiting for parsed request data
this._request = Object.assign(this._request, request);
}
else {
const response = await this._httpMirror.waitForResponse(data); //waiting for parsed response data
this._response = Object.assign(this._response, response);
}
return true;
}
return false;
}

/**
* Get own id
* @returns {string}
Expand All @@ -159,96 +169,19 @@ class Session {
return this._id;
}

set request(buffer) {
if (!this.isHttps || this._updated) { //parse only if data is not encrypted
const parsedRequest = parseDataToObject(buffer, null, this._requestCounter > 0);
const body = parsedRequest.body;
delete parsedRequest.body;

++this._requestCounter;
if (parsedRequest.headers) {
this._request = parsedRequest;
}
if (this._request.method === HTTP_METHODS.CONNECT) { //ignore CONNECT method
--this._requestCounter;
}

if (body) {
this._request.body = (this._request.body || '') + body;
}
}

return this._request;
get response() {
return JSON.parse(JSON.stringify(Object.assign({}, this._response, {
body: this.rawResponse?.toString() || undefined,
})));
}

get request() {
return this._request;
}

set response(buffer) {
if (!this.isHttps || this._updated) { //parse only if data is not encrypted
const parsedResponse = parseDataToObject(buffer, true, this._responseCounter > 0);

if (!parsedResponse.headers) {
this.rawResponse = buffer; //pushing whole buffer, because there aren't headers here
}
else {
//found body from buffer without converting
const DOUBLE_CRLF = CRLF + CRLF;
const splitAt = buffer.indexOf(DOUBLE_CRLF, 0);
this.rawResponse = buffer.slice(splitAt + DOUBLE_CRLF.length);
}

++this._responseCounter;
this._response = Object.assign({}, this._response, parsedResponse);

if (this._response?.statusCode >= 300
&& this._response?.statusCode < 400) {
//redirects will use same session to do next requests
--this._requestCounter; //resetting request
--this._responseCounter; //resetting response
}

if (this._response?.headers?.[CONTENT_LENGTH]
&& this.rawResponse.length) {
const bodyBytes = Buffer.byteLength(this.rawResponse);
this._response.complete = parseInt(this._response.headers[CONTENT_LENGTH]) <= bodyBytes;
}
if (this._response?.headers?.[TRANSFER_ENCODING] === CHUNKED
&& this.rawResponse.length) {
this._response.complete = buffer.indexOf(ZERO + CRLF + CRLF) > -1;
}
}
return this._response;
}

set rawResponse(buffer) {
if (this._responseCounter === 0) {
this._rawResponseBodyChunks = []; //need to reset all possible body-chunks
}
const bufferToPush = Buffer.from(buffer, BINARY_ENCODING);
const splitAt = bufferToPush.indexOf(CRLF);
if (splitAt > -1) {
// handling transfer-encoding: chunked
// each chunk contains info like:
// chunk length in hex\r\n
// chunk\r\n
const [chunkLengthHex, chunk] = [bufferToPush.slice(0, splitAt), bufferToPush.slice(splitAt + CRLF.length)];
const chunkLength = parseInt(chunkLengthHex, 16);
if (Number.isInteger(chunkLength)
&& chunkLength !== 0) {
const [thisChunk, nextChunk] = [chunk.slice(0, chunkLength), chunk.slice(chunkLength + CRLF.length)];
this._rawResponseBodyChunks.push(thisChunk);
if (nextChunk.length > 0) {
return this.rawResponse = nextChunk; //process next chunk in recursion
}
}
else if (!Number.isInteger(chunkLength)) {
return this.rawResponse = chunkLengthHex; //valid chunk is what we think could be the hex-number
}
return; //go out from this function
}
this._rawResponseBodyChunks.push(bufferToPush);
return JSON.parse(JSON.stringify(Object.assign({}, this._request, {
body: this.rawRequest?.toString() || undefined,
trailers: Object.keys(this._request.trailers || {}).length > 0
? this._request.trailers
: undefined
})));
}

/**
Expand All @@ -260,11 +193,11 @@ class Session {
}

/**
* Get response object.
* @returns {Object}
* Get the response body as Buffer.
* @returns {Buffer}
*/
get response() {
return Object.assign({}, this._response, {body: this.rawResponse.toString()});
get rawRequest() {
return Buffer.concat(this._rawRequestBodyChunks);
}

/**
Expand Down
Loading

0 comments on commit 1db6be4

Please sign in to comment.