/server.js |
@@ -1,49 +1,19 @@ |
#!/usr/bin/env node |
/////////////////////////////////////////////////////////////////////////// |
// Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 // |
/////////////////////////////////////////////////////////////////////////// |
|
/*************************************************************************/ |
/* Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 */ |
/*************************************************************************/ |
|
// 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 auth = require("http-auth"); |
const https = require('https'); |
const path = require('path'); |
const fs = require('fs'); |
const mime = require('mime'); |
const url = require('url'); |
const moment = require('moment'); |
const winston = require('winston'); |
const yargs = require('yargs'); |
|
// Local imports. |
const Handler = require( |
path |
.resolve( |
path.dirname(require.main.filename), |
'src', |
'handler' |
) |
) |
|
const certs = require( |
path |
.resolve( |
path.dirname(require.main.filename), |
'src', |
'certs' |
) |
) |
|
// Load configuration file. |
const config = require( |
path |
.resolve( |
path.dirname(require.main.filename), |
'config' |
) |
) |
|
// Get command-line arguments. |
const argv = yargs |
.version() |
@@ -55,24 +25,29 @@ |
.help() |
.argv |
|
// Configuration file. |
const config = require(path.resolve(__dirname, 'config')); |
|
// Check for path traversal. |
function isRooted(userPath, rootPath, separator) { |
userPath = userPath.split(separator).filter(Boolean); |
rootPath = rootPath.split(separator).filter(Boolean); |
return userPath.length >= rootPath.length && |
rootPath.every((e, i) => e === userPath[i]); |
} |
|
// 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) |
const log = new winston.Logger({ |
transports: [ |
new winston.transports.File({ |
level: 'info', |
filename: path.resolve( |
path.dirname(require.main.filename), |
config.log.file |
), |
filename: path.resolve(__dirname, config.server_log), |
handleExceptions: true, |
json: false, |
maxsize: 1048576, // 1MiB. |
maxFiles: 10, // Ten rotations. |
colorize: false, |
timestamp: () => moment() |
.format('YYYYMMDDTHHmmss') |
timestamp: () => moment().format('YYYYMMDDTHHmmss') |
}), |
new winston.transports.Console({ |
level: 'info', |
@@ -79,115 +54,127 @@ |
handleExceptions: true, |
json: false, |
colorize: true, |
timestamp: () => moment() |
.format('YYYYMMDDTHHmmss') |
timestamp: () => moment().format('YYYYMMDDTHHmmss') |
}) |
], |
exitOnError: false |
}) |
}); |
|
fs.realpath(argv.root, (error, root) => { |
fs.realpath(argv.root, (error, documentRoot) => { |
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() |
var authentication = auth.digest({ |
realm: "was", |
file: path.resolve(__dirname, config.password_file) |
}); |
|
// Start HTTP server. |
http.createServer( |
// authentication, |
(request, response) => { |
// Configuration path requested, so send the server configuration if allowed. |
if (config.configuration.enable === true && |
url.parse(request.url, true).path === |
config.configuration.path) { |
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 |
} |
// HTTPs server using digest authentication. |
https.createServer(authentication, { |
key: fs.readFileSync(path.resolve(__dirname, config.key)), |
cert: fs.readFileSync(path.resolve(__dirname, config.certificate)), |
ca: fs.readFileSync(path.resolve(__dirname, config.ca)), |
}, (request, response) => { |
const requestAddress = request.socket.address(); |
const requestedURL = url.parse(request.url, true); |
|
// Process and cache the resource. |
httpcache(request, response, () => { |
new Handler().process(config, request, response, root) |
.on('log', (data) => { |
log.log(data.severity, data.message) |
}) |
.on('data', (result) => { |
response.setHeader('Content-Type', result.type) |
response.writeHead(result.status) |
result.data.pipe(response) |
}) |
}) |
log.info('Client: ' + requestAddress.address + ':' + requestAddress.port + ' accessing: ' + requestedURL.pathname); |
|
const trimmedPath = requestedURL.pathname.split('/').filter(Boolean).join('/'); |
const filesystemPath = trimmedPath === '/' ? |
path.join(documentRoot, trimmedPath) : |
path.resolve(documentRoot, trimmedPath); |
|
if (!isRooted(filesystemPath, documentRoot, path.sep)) { |
log.warn('Attempted path traversal: ' + requestAddress.address + ':' + requestAddress.port + ' requesting: ' + requestedURL.pathname); |
response.statusCode = 403; |
response.end(); |
return; |
} |
).listen(config.net.port, config.net.address, () => { |
log.info('HTTP Server accessible at: http://' + |
config.net.address + |
':' + |
config.net.port + |
' and serving files from directory: ' + |
root |
) |
}) |
|
// Start HTTPs server if enabled. |
if (config.ssl.enable) { |
// Generate certificates for HTTPs. |
certs.generate( |
config.site.name, |
config.net.address, |
config.ssl.privateKeySize, |
(certificates) => { |
https.createServer( |
// authentication, |
{ |
key: certificates.privateKey, |
cert: certificates.certificate, |
}, |
(request, response) => { |
fs.stat(filesystemPath, (error, stats) => { |
// Document does not exist. |
if (error) { |
response.statusCode = 404; |
response.end(); |
return; |
} |
|
// Configuration path requested, so send the server configuration if allowed. |
if (config.configuration.enable === true && |
url.parse(request.url, true).path === |
config.configuration.path) { |
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 |
switch (stats.isDirectory()) { |
case true: // Browser requesting directory. |
const documentRoot = path.resolve(filesystemPath, config.default_document); |
fs.stat(documentRoot, (error, stats) => { |
if (error) { |
fs.readdir(filesystemPath, (error, paths) => { |
if (error) { |
log.warn('Could not list directory: ' + filesystemPath); |
response.statusCode = 500; |
response.end(); |
return; |
} |
log.info('Directory listing requested for: ' + filesystemPath); |
response.statusCode = 200; |
response.write(JSON.stringify(paths)); |
response.end(); |
}); |
|
return; |
} |
|
httpcache(request, response, () => { |
new Handler().process(config, request, response, root) |
.on('log', (data) => { |
log.log(data.severity, data.message) |
fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
if (error) { |
log.warn('The server was unable to access the filesystem path: ' + filesystemPath); |
response.statusCode = 403; |
response.end(); |
return; |
} |
|
// Set MIME content type. |
response.setHeader('Content-Type', mime.lookup(documentRoot)); |
|
var readStream = fs.createReadStream(documentRoot) |
.on('open', () => { |
response.statusCode = 200; |
readStream.pipe(response); |
}) |
.on('data', (result) => { |
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://' + |
config.ssl.address + |
':' + |
config.ssl.port + |
' and serving files from directory: ' + |
root |
) |
}) |
.on('error', () => { |
response.statusCode = 500; |
response.end(); |
}); |
|
}); |
|
}); |
break; |
default: // Browser requesting file. |
// Check if the file is accessible. |
fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
if (error) { |
response.statusCode = 403; |
response.end(); |
return; |
} |
|
response.setHeader('Content-Type', mime.lookup(filesystemPath)); |
|
var readStream = fs.createReadStream(filesystemPath) |
.on('open', () => { |
response.statusCode = 200; |
readStream.pipe(response); |
}) |
.on('error', () => { |
response.statusCode = 500; |
response.end(); |
}); |
|
}); |
break; |
} |
) |
} |
}) |
}); |
|
}).listen(config.port, config.address, () => { |
log.info('Server is listening on: ' + config.address + ' and port: ' + config.port + ' whilst serving files from: ' + documentRoot); |
}); |
}); |