node-http-server

Subversion Repositories:
Compare Path: Rev
With Path: Rev
?path1? @ 37  →  ?path2? @ 38
/server.js
@@ -5,43 +5,44 @@
/*************************************************************************/
 
// Import packages.
const https = require('https');
const http = require('http');
const path = require('path');
const fs = require('fs');
const url = require('url');
const moment = require('moment');
const winston = require('winston');
const yargs = require('yargs');
const dns = require('dns');
const HttpCache = require("http-cache");
const https = require('https')
const http = require('http')
const path = require('path')
const fs = require('fs')
const url = require('url')
const moment = require('moment')
const winston = require('winston')
const yargs = require('yargs')
const dns = require('dns')
const HttpCache = require("http-cache")
 
// Local imports.
const Handler = require(
path
.resolve(
path.dirname(require.main.filename),
'src',
'handler'
)
);
.resolve(
path.dirname(require.main.filename),
'src',
'handler'
)
)
 
const certs = require(
path
.resolve(
path.dirname(require.main.filename),
'src',
'certs'
)
);
.resolve(
path.dirname(require.main.filename),
'src',
'certs'
)
)
 
// Load configuration file.
const config = require(
path
.resolve(
path.dirname(require.main.filename),
'config'
)
);
.resolve(
path.dirname(require.main.filename),
'config'
)
)
 
// Get command-line arguments.
const argv = yargs
@@ -56,7 +57,7 @@
 
// Create various logging mechanisms.
// RFC5424 - { emerg: 0, alert: 1, crit: 2, error: 3, warning: 4, notice: 5, info: 6, debug: 7 }
winston.setLevels(winston.config.syslog.levels);
winston.setLevels(winston.config.syslog.levels)
const log = new winston.Logger({
transports: [
new winston.transports.File({
@@ -83,16 +84,16 @@
})
],
exitOnError: false
});
})
 
fs.realpath(argv.root, (error, root) => {
if (error) {
log.error('Could not find document root: ' + argv.root);
process.exit(1);
log.error('Could not find document root: ' + argv.root)
process.exit(1)
}
 
// Create server-side cache.
const httpcache = new HttpCache();
const httpcache = new HttpCache()
 
// Start HTTP server.
http.createServer(
@@ -99,33 +100,31 @@
// authentication,
(request, response) => {
// Configuration path requested, so send the server configuration if allowed.
if(config.configuration.enable === true &&
url.parse(request.url, true).path ===
if (config.configuration.enable === true &&
url.parse(request.url, true).path ===
config.configuration.path) {
const address = request.socket.address();
const address = request.socket.address()
log.info('HTTP Server configuration requested by: ' +
address.address + ':' +
address.port
);
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify(config));
return;
)
response.setHeader('Content-Type', 'application/json')
response.end(JSON.stringify(config))
return
}
 
// Process and cache the resource.
httpcache(request, response, () => {
new Handler().process(config, request, response, root)
.on('log', (data) => {
log.log(data.severity, data.message);
log.log(data.severity, data.message)
})
.on('data', (result) => {
response.setHeader('Content-Type', result.type);
response.writeHead(result.status);
result.data
.on('readable', () => result.data.pipe(response))
.on('end', () => response.end());
});
});
response.setHeader('Content-Type', result.type)
response.writeHead(result.status)
result.data.pipe(response)
})
})
}
).listen(config.net.port, config.net.address, () => {
log.info('HTTP Server accessible at: http://' +
@@ -134,8 +133,8 @@
config.net.port +
' and serving files from directory: ' +
root
);
});
)
})
 
// Start HTTPs server if enabled.
if (config.ssl.enable) {
@@ -152,34 +151,32 @@
cert: certificates.certificate,
},
(request, response) => {
 
// Configuration path requested, so send the server configuration if allowed.
if(config.configuration.enable === true &&
url.parse(request.url, true).path ===
if (config.configuration.enable === true &&
url.parse(request.url, true).path ===
config.configuration.path) {
const address = request.socket.address();
const address = request.socket.address()
log.info('HTTP Server configuration requested by: ' +
address.address + ':' +
address.port
);
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify(config));
return;
)
response.setHeader('Content-Type', 'application/json')
response.end(JSON.stringify(config))
return
}
 
httpcache(request, response, () => {
new Handler().process(config, request, response, root)
.on('log', (data) => {
log.log(data.severity, data.message);
log.log(data.severity, data.message)
})
.on('data', (result) => {
response.setHeader('Content-Type', result.type);
response.writeHead(result.status);
result.data
.on('readable', () => result.data.pipe(response))
.on('end', () => response.end());
});
});
response.setHeader('Content-Type', result.type)
response.writeHead(result.status)
result.data.pipe(response)
})
})
}
).listen(config.ssl.port, config.ssl.address, () => {
log.info('HTTPs Server accessible at: https://' +
@@ -188,9 +185,9 @@
config.ssl.port +
' and serving files from directory: ' +
root
);
)
})
}
);
)
}
});
})
/src/cache.js
@@ -4,71 +4,71 @@
/* Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 */
/*************************************************************************/
 
const fs = require('fs');
const stream = require('stream');
const util = require('util');
const tz = require('moment-timezone');
const forge = require('node-forge');
const EventEmitter = require('events').EventEmitter;
const fs = require('fs')
const stream = require('stream')
const util = require('util')
const tz = require('moment-timezone')
const forge = require('node-forge')
const EventEmitter = require('events').EventEmitter
 
// Cache constructor.
function Cache(config, client, request, response) {
// Create events emitters for logging and data.
EventEmitter.call(this);
EventEmitter.call(this)
 
// Pass through objects needed for caching.
this.config = config;
this.client = client;
this.request = request;
this.response = response;
};
this.config = config
this.client = client
this.request = request
this.response = response
}
 
// Cache handling.
Cache.prototype.process = function(resource, input, type) {
EventEmitter.call(this);
const self = this;
EventEmitter.call(this)
const self = this
 
// Read the resource and cache or not depending on configuration settings.
fs.stat(resource, (error, stats) => {
var expires = 0;
var expires = 0
Object.keys(self.config.site.cache).forEach((key) => {
self.config.site.cache[key].forEach((expire) => {
if (expire.test(resource)) {
expires = key;
expires = key
}
});
});
})
})
 
switch (self.request.httpVersion) {
case '1.1': // HTTP 1.1
self.response.setHeader('Cache-Control',
"max-age=" + expires + ", public"
);
const sha1 = forge.md.sha1.create();
)
const sha1 = forge.md.sha1.create()
const data = new stream.Readable({
objectMode: true,
read(size) {}
});
})
input
.on('data', (chunk) => {
sha1.update(chunk);
data.push(chunk);
sha1.update(chunk)
data.push(chunk)
})
.on('end', () => {
const etag = sha1.digest().toHex();
const etag = sha1.digest().toHex()
 
// Set the ETag for the resource.
self.response.setHeader('ETag', etag);
self.response.setHeader('ETag', etag)
 
const ifNoneMatch = Object
.getOwnPropertyNames(self.request.headers)
.filter((header) => header.toUpperCase() ===
'If-None-Match'.toUpperCase());
'If-None-Match'.toUpperCase())
 
const ifModifiedSince = Object
.getOwnPropertyNames(self.request.headers)
.filter((header) => header.toUpperCase() ===
'If-Modified-Since'.toUpperCase());
'If-Modified-Since'.toUpperCase())
 
if ((ifNoneMatch.length !== 0 &&
self.request.headers[ifNoneMatch].toUpperCase() === etag.toUpperCase()) ||
@@ -82,17 +82,17 @@
' cached resource: ' +
resource,
severity: 'info'
});
})
self.emit('data', {
status: 304,
data: new stream.Readable({
read(size) {
this.push(null);
this.push(null)
}
}),
type: type
});
return;
})
return
}
 
// Send the resource.
@@ -103,28 +103,28 @@
' sent resource: ' +
resource,
severity: 'info'
});
data.push(null);
})
data.push(null)
self.emit('data', {
status: 200,
data: data,
type: type
});
});
})
})
 
return;
return
default:
self.response.setHeader('Last-Modified',
tz(stats.mtime)
.tz('UTC')
.format("ddd, DD MMM YYYY HH:mm:ss z")
);
)
self.response.setHeader('Expires',
tz()
.tz('UTC')
.add(expires, 'seconds')
.format("ddd, DD MMM YYYY HH:mm:ss z")
);
)
// Send the resource.
self.emit('log', {
message: 'Client: ' +
@@ -133,18 +133,18 @@
' sent resource: ' +
resource,
severity: 'info'
});
})
self.emit('data', {
status: 200,
data: input,
type: type
});
break;
})
break
}
});
})
 
return this;
};
return this
}
 
util.inherits(Cache, EventEmitter);
module.exports = Cache;
util.inherits(Cache, EventEmitter)
module.exports = Cache
/src/handler.js
@@ -4,32 +4,32 @@
/* Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 */
/*************************************************************************/
 
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const auth = require("http-auth");
const stream = require('stream');
const util = require('util');
const EventEmitter = require('events').EventEmitter;
const url = require('url')
const path = require('path')
const fs = require('fs')
const mime = require('mime')
const auth = require("http-auth")
const stream = require('stream')
const util = require('util')
const EventEmitter = require('events').EventEmitter
 
// Local imports.
const Cache = require(
path
.resolve(
path.dirname(require.main.filename),
'src',
'cache'
)
);
.resolve(
path.dirname(require.main.filename),
'src',
'cache'
)
)
const was = require(
path
.resolve(
path.dirname(require.main.filename),
'src',
'was'
)
);
.resolve(
path.dirname(require.main.filename),
'src',
'was'
)
)
 
// Serves files.
function files(self, config, file, client, cache) {
@@ -43,28 +43,28 @@
' requesting inaccessible path: ' +
file,
severity: 'warning'
});
})
self.emit('data', {
status: 403,
data: new stream.Readable({
read(size) {
this.push(null);
this.push(null)
}
}),
type: 'text/plain'
});
return;
})
return
}
 
cache.process(file, fs.createReadStream(file), mime.lookup(file))
.on('data', (result) => self.emit('data', result))
.on('log', (data) => self.emit('log', data));
});
.on('log', (data) => self.emit('log', data))
})
}
 
// Serves a directory listing or the document index in case it exists.
function index(self, config, directory, href, client, cache) {
const root = path.resolve(directory, config.site.index);
const root = path.resolve(directory, config.site.index)
fs.stat(root, (error, stats) => {
if (error) {
if (config.site.indexing
@@ -79,28 +79,28 @@
' could not access directory: ' +
directory,
severity: 'warning'
});
})
self.emit('data', {
status: 500,
data: new stream.Readable({
read(size) {
this.push(null);
this.push(null)
}
}),
type: 'text/plain'
});
return;
})
return
}
cache.process(directory, new stream.Readable({
read(size) {
this.push(JSON.stringify(paths));
this.push(null);
}
}), 'application/json')
read(size) {
this.push(JSON.stringify(paths))
this.push(null)
}
}), 'application/json')
.on('data', (result) => self.emit('data', result))
.on('log', (data) => self.emit('log', data));
});
return;
.on('log', (data) => self.emit('log', data))
})
return
}
// Could not access directory index file and directory listing not allowed.
self.emit('log', {
@@ -110,17 +110,17 @@
' no index file found and accessing forbiden index: ' +
href,
severity: 'warning'
});
})
self.emit('data', {
status: 403,
data: new stream.Readable({
read(size) {
this.push(null);
this.push(null)
}
}),
type: 'text/plain'
});
return;
})
return
}
 
// Serve the document index.
@@ -133,23 +133,23 @@
' unable to access path: ' +
directory,
severity: 'warning'
});
})
self.emit('data', {
status: 403,
data: new stream.Readable({
read(size) {
this.push(null);
this.push(null)
}
}),
type: 'text/plain'
});
return;
})
return
}
cache.process(root, fs.createReadStream(root), mime.lookup(root))
.on('data', (result) => self.emit('data', result))
.on('log', (data) => self.emit('log', data));
});
});
.on('log', (data) => self.emit('log', data))
})
})
}
 
// Determines whether the requested filesystem request path is a directory or a file.
@@ -164,26 +164,26 @@
' accessing non-existent document: ' +
local,
severity: 'warning'
});
})
self.emit('data', {
status: 404,
data: new stream.Readable({
read(size) {
this.push(null);
this.push(null)
}
}),
type: 'text/plain'
});
return;
})
return
}
 
if (stats.isDirectory()) {
// Directory is requested so provide directory indexes.
index(self, config, local, href, address, cache);
return;
index(self, config, local, href, address, cache)
return
}
if (stats.isFile()) {
const file = path.parse(local).base;
const file = path.parse(local).base
 
// If the file matches the reject list or is not in the accept list,
// then there is no file to serve.
@@ -196,61 +196,61 @@
' requested disallowed file: ' +
file,
severity: 'warning'
});
})
self.emit('data', {
status: 404,
data: new stream.Readable({
read(size) {
this.push(null);
this.push(null)
}
}),
type: 'text/plain'
});
return;
})
return
}
 
// A file was requested so provide the file.
files(self, config, local, address, cache);
files(self, config, local, address, cache)
}
});
})
}
 
// Constructor.
function Handler() {
// Create events emitters for logging and data.
EventEmitter.call(this);
};
EventEmitter.call(this)
}
 
// Process a request.
Handler.prototype.process = function(config, request, response, root) {
EventEmitter.call(this);
var self = this;
Handler.prototype.process = function (config, request, response, root) {
EventEmitter.call(this)
var self = this
 
// Get client details.
const address = request.socket.address();
const address = request.socket.address()
// Get requested URL.
const requestURL = url.parse(
request.url, true
);
)
 
// Perform URL re-writes.
Object.keys(config.site.rewrite).forEach((key, index) => {
if (config.site.rewrite[key].test(requestURL.path)) {
const originalPath = requestURL.path;
const originalPath = requestURL.path
requestURL.path = requestURL
.path
.replace(
config.site.rewrite[key], key
);
)
requestURL.pathname = url.parse(
requestURL
requestURL
.pathname
.replace(
config.site.rewrite[key], key
),
true
)
.pathname;
true
)
.pathname
self.emit('log', {
message: 'Rewrite path: ' +
originalPath +
@@ -257,18 +257,18 @@
' to: ' +
requestURL.path,
severity: 'info'
});
})
}
});
})
 
const trimmedPath = requestURL
.pathname
.split('/')
.filter(Boolean)
.join('/');
.join('/')
const requestPath = trimmedPath === '/' ?
path.join(root, trimmedPath) :
path.resolve(root, trimmedPath);
path.resolve(root, trimmedPath)
 
fs.realpath(requestPath, (error, resolvedPath) => {
// If the path does not exist, then return early.
@@ -280,17 +280,17 @@
' requesting: ' +
requestURL.pathname,
severity: 'warning'
});
})
self.emit('data', {
status: 404,
data: new stream.Readable({
read(size) {
this.push(null);
this.push(null)
}
}),
type: 'text/plain'
});
return;
})
return
}
 
// Check for path traversals early on and bail if the requested path does not
@@ -304,22 +304,22 @@
' requesting: ' +
requestURL.pathname,
severity: 'warning'
});
})
self.emit('done', {
status: 404,
data: new stream.Readable({
read(size) {
this.push(null);
this.push(null)
}
}),
type: 'text/plain'
});
return;
})
return
}
 
// If authentication is required for this path then perform authentication.
if (config.auth.locations.some(
(authPath) => authPath.toUpperCase() === requestURL.pathname.toUpperCase())) {
(authPath) => authPath.toUpperCase() === requestURL.pathname.toUpperCase())) {
// Create digest authentication.
const authentication = auth.digest({
realm: config.auth.realm,
@@ -327,7 +327,7 @@
path.dirname(require.main.filename),
config.auth.digest
)
});
})
// Requested location requires authentication.
authentication.check(request, response, (request, response) => {
self.emit('log', {
@@ -337,7 +337,7 @@
' accessing: ' +
requestURL.pathname,
severity: 'info'
});
})
process.nextTick(() =>
serve(self,
config,
@@ -346,9 +346,9 @@
address,
new Cache(config, address, request, response)
)
);
});
return;
)
})
return
}
 
// If no authentication is required then serve the request.
@@ -359,7 +359,7 @@
' accessing: ' +
requestURL.pathname,
severity: 'info'
});
})
process.nextTick(() =>
serve(self,
config,
@@ -368,13 +368,13 @@
address,
new Cache(config, address, request, response)
)
);
});
});
)
})
})
 
return this;
};
return this
}
 
util.inherits(Handler, EventEmitter);
util.inherits(Cache, EventEmitter);
module.exports = Handler;
util.inherits(Handler, EventEmitter)
util.inherits(Cache, EventEmitter)
module.exports = Handler