Node.js: a shim for overriding "http" and "websocket" modules.

Also added stubs for Server.address()
This was done to prevent crashes in some popular frameworks like express

Supports both CommonJS and the new ES Modules system syntax e.g:

app.js:
const http = require('http')

app.mjs:
import http from "http"

Usage on Node 14.16.x and higher:
{
    "type": "external",
    "processes": {"spare": 0},
    "working_directory": '/project',
    "executable": "/usr/bin/env",
    "arguments": [
        "node",
        "--loader",
        "unit-http/require_shim.mjs"
        "--require",
        "unit-http/require_shim",
        "app.js"
    ]
}

Usage on Node 14.15.x and lower:
{
    "type": "external",
    "processes": {"spare": 0},
    "working_directory": '/project',
    "executable": "/usr/bin/env",
    "arguments": [
        "node",
        "--require",
        "unit-http/require_shim",
        "app.js"
    ]
}
This commit is contained in:
Oisin Canty
2021-05-12 09:26:55 +00:00
parent 07c6bf165d
commit a0c083af20
44 changed files with 288 additions and 78 deletions

View File

@@ -31,6 +31,12 @@ NGINX Unit updated to 1.24.0.
date="" time=""
packager="Andrei Belov <defan@nginx.com>">
<change type="feature">
<para>
a shim for automatic overriding "http" and "websocket" modules in Node.js.
</para>
</change>
<change type="feature">
<para>
ability to limit serving of static files by MIME types.

View File

@@ -5,19 +5,22 @@
'use strict';
const server = require('unit-http/http_server');
const { Server } = server;
const {
Server,
ServerRequest,
ServerResponse,
} = require('./http_server');
function createServer (requestHandler) {
return new Server(requestHandler);
}
const http = require("http")
module.exports = {
...http,
Server,
STATUS_CODES: server.STATUS_CODES,
createServer,
IncomingMessage: server.ServerRequest,
ServerResponse: server.ServerResponse
IncomingMessage: ServerRequest,
ServerResponse,
};

View File

@@ -444,17 +444,30 @@ Server.prototype.setTimeout = function setTimeout(msecs, callback) {
Server.prototype.listen = function (...args) {
this.unit.listen();
const cb = args.pop();
if (typeof cb === 'function') {
this.once('listening', cb);
if (typeof args[args.length - 1] === 'function') {
this.once('listening', args[args.length - 1]);
}
this.emit('listening');
/*
* Some express.js apps use the returned server object inside the listening
* callback, so we timeout the listening event to occur after this function
* returns.
*/
setImmediate(function() {
this.emit('listening')
}.bind(this))
return this;
};
Server.prototype.address = function () {
return {
family: "IPv4",
address: "127.0.0.1",
port: 80
}
}
Server.prototype.emit_request = function (req, res) {
if (req._websocket_handshake && this._upgradeListenerCount > 0) {
this.emit('upgrade', req, req.socket);
@@ -530,7 +543,6 @@ function connectionListener(socket) {
}
module.exports = {
STATUS_CODES: http.STATUS_CODES,
Server,
ServerResponse,
ServerRequest,

View File

@@ -0,0 +1,27 @@
// can only be ran as part of a --require param on the node process
if (module.parent && module.parent.id === "internal/preload") {
const { Module } = require("module")
if (!Module.prototype.require.__unit_shim) {
const http = require("./http")
const websocket = require("./websocket")
const original = Module.prototype.require;
Module.prototype.require = function (id) {
switch(id) {
case "http":
case "unit-http":
return http
case "websocket":
case "unit-http/websocket":
return websocket
}
return original.apply(this, arguments);
}
Module.prototype.require.__unit_shim = true;
}
}

View File

@@ -0,0 +1,18 @@
// must be ran as part of a --loader or --experimental-loader param
export async function resolve(specifier, context, defaultResolver) {
switch (specifier) {
case "websocket":
return {
url: new URL("./websocket.js", import.meta.url).href,
format: "cjs"
}
case "http":
return {
url: new URL("./http.js", import.meta.url).href,
format: "cjs"
}
}
return defaultResolver(specifier, context, defaultResolver)
}

3
test/node/404/app.js Executable file → Normal file
View File

@@ -1,7 +1,6 @@
#!/usr/bin/env node
var fs = require('fs');
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.writeHead(404, {}).end(fs.readFileSync('404.html'));
}).listen(7080);

3
test/node/basic/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'})
.end('Hello World\n');
}).listen(7080);

3
test/node/double_end/app.js Executable file → Normal file
View File

@@ -1,5 +1,4 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.end().end();
}).listen(7080);

3
test/node/get_header_names/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.setHeader('DATE', ['date1', 'date2']);
res.setHeader('X-Header', 'blah');
res.setHeader('X-Names', res.getHeaderNames());

3
test/node/get_header_type/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.setHeader('X-Number', 100);
res.setHeader('X-Type', typeof(res.getHeader('X-Number')));
res.end();

3
test/node/get_variables/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
let query = require('url').parse(req.url, true).query;
res.setHeader('X-Var-1', query.var1);
res.setHeader('X-Var-2', query.var2);

3
test/node/has_header/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.setHeader('X-Has-Header', res.hasHeader(req.headers['x-header']) + '');
res.end();
}).listen(7080);

3
test/node/header_name_case/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.setHeader('X-Header', '1');
res.setHeader('X-header', '2');
res.setHeader('X-HEADER', '3');

3
test/node/header_name_valid/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.writeHead(200, {});
res.setHeader('@$', 'test');
res.end();

3
test/node/header_value_object/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.setHeader('X-Header', {});
res.end();
}).listen(7080);

3
test/node/mirror/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
let body = '';
req.on('data', chunk => {
body += chunk.toString();

3
test/node/post_variables/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
let body = '';
req.on('data', chunk => {
body += chunk.toString();

3
test/node/promise_end/app.js Executable file → Normal file
View File

@@ -1,8 +1,7 @@
#!/usr/bin/env node
var fs = require('fs');
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.write('blah');
Promise.resolve().then(() => {

3
test/node/promise_handler/app.js Executable file → Normal file
View File

@@ -1,8 +1,7 @@
#!/usr/bin/env node
var fs = require('fs');
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.end();
if (req.headers['x-write-call']) {

3
test/node/remove_header/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.setHeader('X-Header', 'test');
res.setHeader('Was-Header', res.hasHeader('X-Header').toString());

View File

@@ -0,0 +1,6 @@
import http from "http"
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'})
.end('Hello World\n');
}).listen(7080);

View File

@@ -0,0 +1 @@
import("./module.mjs")

View File

@@ -0,0 +1,6 @@
import http from "http"
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'})
.end('Hello World\n');
}).listen(7080);

View File

@@ -0,0 +1,30 @@
import http from "http"
import websocket from "websocket"
let server = http.createServer(function() {});
let webSocketServer = websocket.server;
server.listen(7080, function() {});
var wsServer = new webSocketServer({
maxReceivedMessageSize: 0x1000000000,
maxReceivedFrameSize: 0x1000000000,
fragmentOutgoingMessages: false,
fragmentationThreshold: 0x1000000000,
httpServer: server,
});
wsServer.on('request', function(request) {
var connection = request.accept(null);
connection.on('message', function(message) {
if (message.type === 'utf8') {
connection.send(message.utf8Data);
} else if (message.type === 'binary') {
connection.send(message.binaryData);
}
});
connection.on('close', function(r) {});
});

View File

@@ -0,0 +1 @@
import("./module.mjs")

View File

@@ -0,0 +1,30 @@
import http from "http"
import websocket from "websocket"
let server = http.createServer(function() {});
let webSocketServer = websocket.server;
server.listen(7080, function() {});
var wsServer = new webSocketServer({
maxReceivedMessageSize: 0x1000000000,
maxReceivedFrameSize: 0x1000000000,
fragmentOutgoingMessages: false,
fragmentationThreshold: 0x1000000000,
httpServer: server,
});
wsServer.on('request', function(request) {
var connection = request.accept(null);
connection.on('message', function(message) {
if (message.type === 'utf8') {
connection.send(message.utf8Data);
} else if (message.type === 'binary') {
connection.send(message.binaryData);
}
});
connection.on('close', function(r) {});
});

View File

@@ -0,0 +1 @@
require("./transitive_http")

View File

@@ -0,0 +1,8 @@
const http = require("http");
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'})
.end('Hello World\n');
}).listen(7080);
module.exports = http;

View File

@@ -0,0 +1,4 @@
require("unit-http").createServer(function (req, res) {
res.writeHead(200, {'Content-Length': 12, 'Content-Type': 'text/plain'})
.end('Hello World\n');
}).listen(7080);

3
test/node/set_header_array/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.setHeader('Set-Cookie', ['tc=one,two,three', 'tc=four,five,six']);
res.end();
}).listen(7080);

3
test/node/status_message/app.js Executable file → Normal file
View File

@@ -1,5 +1,4 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.writeHead(200, 'blah', {'Content-Type': 'text/plain'}).end();
}).listen(7080);

3
test/node/update_header/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.setHeader('X-Header', 'test');
res.setHeader('X-Header', 'new');
res.end();

3
test/node/variables/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
let body = '';
req.on('data', chunk => {
body += chunk.toString();

7
test/node/websockets/mirror/app.js Executable file → Normal file
View File

@@ -1,9 +1,6 @@
#!/usr/bin/env node
server = require('unit-http').createServer(function() {});
webSocketServer = require('unit-http/websocket').server;
//server = require('http').createServer(function() {});
//webSocketServer = require('websocket').server;
server = require('http').createServer(function() {});
webSocketServer = require('websocket').server;
server.listen(7080, function() {});

7
test/node/websockets/mirror_fragmentation/app.js Executable file → Normal file
View File

@@ -1,9 +1,6 @@
#!/usr/bin/env node
server = require('unit-http').createServer(function() {});
webSocketServer = require('unit-http/websocket').server;
//server = require('http').createServer(function() {});
//webSocketServer = require('websocket').server;
server = require('http').createServer(function() {});
webSocketServer = require('websocket').server;
server.listen(7080, function() {});

3
test/node/write_before_write_head/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.write('blah');
res.writeHead(200, {'Content-Type': 'text/plain'}).end();
}).listen(7080);

3
test/node/write_buffer/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'})
.end(new Buffer([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]));
}).listen(7080);

3
test/node/write_callback/app.js Executable file → Normal file
View File

@@ -1,8 +1,7 @@
#!/usr/bin/env node
var fs = require('fs');
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
var a = 'world';
res.write('hello', 'utf8', function() {

3
test/node/write_multiple/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain', 'Content-Length': 14});
res.write('write');
res.write('write2');

3
test/node/write_return/app.js Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node
require('unit-http').createServer(function (req, res) {
require('http').createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'})
.end(res.write('body').toString());
}).listen(7080);

View File

@@ -9,13 +9,26 @@ from unit.utils import waitforfiles
class TestNodeApplication(TestApplicationNode):
prerequisites = {'modules': {'node': 'all'}}
def test_node_application_basic(self):
self.load('basic')
def assert_basic_application(self):
resp = self.get()
assert resp['headers']['Content-Type'] == 'text/plain', 'basic header'
assert resp['body'] == 'Hello World\n', 'basic body'
def test_node_application_basic(self):
self.load('basic')
self.assert_basic_application()
def test_node_application_require_shim_unit_http(self):
self.load('require_shim/unit_http')
self.assert_basic_application()
def test_node_application_require_shim_transitive_dependency(self):
self.load('require_shim/transitive_dependency')
self.assert_basic_application()
def test_node_application_seq(self):
self.load('basic')

View File

@@ -0,0 +1,50 @@
import pytest
from distutils.version import LooseVersion
from unit.applications.lang.node import TestApplicationNode
from unit.applications.websockets import TestApplicationWebsocket
class TestNodeESModules(TestApplicationNode):
prerequisites = {
'modules': {
'node': lambda v: LooseVersion(v) >= LooseVersion("14.16.0")
}
}
es_modules = True
ws = TestApplicationWebsocket()
def assert_basic_application(self):
resp = self.get()
assert resp['headers']['Content-Type'] == 'text/plain', 'basic header'
assert resp['body'] == 'Hello World\n', 'basic body'
def test_node_es_modules_require_shim_http(self):
self.load('require_shim/es_modules_http', name="app.mjs")
self.assert_basic_application()
def test_node_es_modules_require_shim_http_indirect(self):
self.load('require_shim/es_modules_http_indirect', name="app.js")
self.assert_basic_application()
def test_node_es_modules_require_shim_websockets(self):
self.load('require_shim/es_modules_websocket', name="app.mjs")
message = 'blah'
_, sock, _ = self.ws.upgrade()
self.ws.frame_write(sock, self.ws.OP_TEXT, message)
frame = self.ws.frame_read(sock)
assert message == frame['data'].decode('utf-8'), 'mirror'
self.ws.frame_write(sock, self.ws.OP_TEXT, message)
frame = self.ws.frame_read(sock)
assert message == frame['data'].decode('utf-8'), 'mirror 2'
sock.close()

View File

@@ -7,15 +7,16 @@ from unit.utils import public_dir
class TestApplicationNode(TestApplicationProto):
application_type = "node"
es_modules = False
def prepare_env(self, script):
# copy application
shutil.copytree(
option.test_dir + '/node/' + script, option.temp_dir + '/node'
)
# copy modules
shutil.copytree(
option.current_dir + '/node/node_modules',
option.temp_dir + '/node/node_modules',
@@ -26,6 +27,19 @@ class TestApplicationNode(TestApplicationProto):
def load(self, script, name='app.js', **kwargs):
self.prepare_env(script)
if self.es_modules:
arguments = [
"node",
"--loader",
"unit-http/require_shim.mjs",
"--require",
"unit-http/require_shim",
name,
]
else:
arguments = ["node", "--require", "unit-http/require_shim", name]
self._load_conf(
{
"listeners": {
@@ -36,7 +50,8 @@ class TestApplicationNode(TestApplicationProto):
"type": "external",
"processes": {"spare": 0},
"working_directory": option.temp_dir + '/node',
"executable": name,
"executable": '/usr/bin/env',
"arguments": arguments,
}
},
},

View File

@@ -1,6 +1,15 @@
import os
import subprocess
def check_node(current_dir):
if os.path.exists(current_dir + '/node/node_modules'):
return True
if not os.path.exists(current_dir + '/node/node_modules'):
return None
try:
v_bytes = subprocess.check_output(['/usr/bin/env', 'node', '-v'])
return [str(v_bytes, 'utf-8').lstrip('v').rstrip()]
except subprocess.CalledProcessError:
return None