Merge pull request #1 from RealOrangeOne/typescript

Typescript rewrite
This commit is contained in:
Jake Howard 2017-02-19 22:09:44 +00:00 committed by GitHub
commit eed5dadc01
26 changed files with 392 additions and 207 deletions

View file

@ -1,16 +0,0 @@
{
"extends": "eslint-config-dabapps/base/.eslintrc",
"env": {
"node": true,
"browser": false,
"mocha": true,
"es6": true
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 6
},
"rules": {
"no-console": 0
}
}

2
.gitignore vendored
View file

@ -31,3 +31,5 @@ node_modules
# Optional REPL history
.node_repl_history
typings/
dist/

View file

@ -2,29 +2,55 @@
The only static-file server you'll ever need!
### Features:
- Logging
- Basic-Auth _(optional)_
- Logging - [`winston`](https://www.npmjs.com/package/winston)
- Basic-Auth - [`basic-auth`](https://www.npmjs.com/package/basic-auth)
- Custom 404 page
- Optimum Compression - [`compression`](https://www.npmjs.com/package/compression)
- Security checks / headers - [`helmet`](https://www.npmjs.com/package/helmet)
- Opbeat error-reporting - [docs](https://opbeat.com/docs/articles/get-started-with-express/)
- Whitelist IP Addresses - [`express-ip-access-control`](https://www.npmjs.com/package/express-ip-access-control)
- Directory Listing - [`serve-index`](https://www.npmjs.com/package/serve-index)
### Usage / Configuration
### Usage
```bash
tstatic <directory>
tstatic <dir> [options]
-h --help Show this screen.
--version Show version.
-p <port> --port=<port> Port to listen on.
-b <auth> --basic-auth=<auth> Enable basic-auth.
-i <ips> --ips=<ips> Allowed IP addresses.
-l --list-dir List Directory.
--opbeat Enable Opbeat.
-o --open Open in browser after start.
```
`directory` is where your static files are.
`dir` is where your static files are.
404 errors will return with `<directory>/.404.html`, with status code 404. If this file doesnt exist, plain error page will be shown.
404 errors will return with `<dir>/.404.html`, with status code 404. If this file doesnt exist, the default error page will be shown.
#### Environment
Make sure to set `NODE_ENV` to `production`!
### Configuration
`PORT`: The port you want the server to listen on. Default: `5000`.
##### `port`
The port for the server to listen on. Currently supports plain HTTP only
`BASIC_AUTH_USERNAME` / `BASIC_AUTH_PASSWORD`: Credentials for built-in basic auth
##### `basic-auth`
Enable basic-auth for all paths. Currently only supports single credentals.
Opbeat middleware is configured using documented variables [here](https://opbeat.com/docs/articles/opbeat-for-nodejs-api/#appid). _Requires production `NODE_ENV`!_
Format:`-b username:password`
##### `ips`
IP addresses that are allowed to connect to the server.
Format: `-i 192.168.1.100,192.168.1.101`
##### `list-dir`
Enables directory listing. Allow browseing
##### `opbeat`
Enable opbeat error reporting. `--opbeat` only enables this, configuration is done using [environment varables](https://opbeat.com/docs/articles/get-started-with-express/#appId).
##### `open`
Open the server in the browser one started. It will open in your default browser, and use url `http://0.0.0.0:<port>`.

View file

@ -2,15 +2,17 @@
"name": "tstatic",
"version": "1.0.0",
"description": "Container to host simple static applications using a node server, so files can be deployed using rsync",
"main": "./src/server.js",
"main": "node ./dist/index.js",
"bin": {
"tstatic": "./src/server.js"
"tstatic": "node ./dist/index.js"
},
"scripts": {
"test": "npm run mocha && npm run lint && nsp check",
"lint": "eslint src/ tests/",
"start": "./src/server.js site/",
"mocha": "NODE_ENV=test mocha tests/**.test.js"
"start": "node ./dist/index.js",
"postinstall": "typings install",
"build": "tsc",
"test": "npm run build && npm run mocha && nsp check",
"mocha": "mocha --compilers ts:ts-node/register --require scripts/test-helper.js tests/**.test.ts",
"lint": "tslint src/**/*.ts"
},
"engines": {
"node": "6.9.4"
@ -25,22 +27,30 @@
},
"homepage": "https://github.com/RealOrangeOne/tstatic#readme",
"dependencies": {
"basic-auth": "^1.1.0",
"basic-auth": "=1.1.0",
"compression": "=1.6.2",
"connect-static-file": "=1.1.2",
"express": "=4.14.0",
"express-basic-auth": "=0.2.3",
"docopt": "=0.6.2",
"express": "=4.14.1",
"express-basic-auth": "=0.3.2",
"express-ip-access-control": "=1.0.5",
"express-winston": "=2.1.2",
"express-winston": "=2.2.0",
"helmet": "=3.4.0",
"opbeat": "=4.7.0",
"opbeat": "=4.11.0",
"open": "=0.0.5",
"serve-index": "=1.8.0",
"winston": "=2.3.0"
"winston": "=2.3.1"
},
"devDependencies": {
"eslint-config": "dabapps/eslint-config#2.0.5",
"chai": "=3.5.0",
"chai-as-promised": "=6.0.0",
"mocha": "=3.2.0",
"node-fetch": "=1.6.3",
"nsp": "=2.6.2",
"supertest": "=2.0.1"
"supertest": "=3.0.0",
"ts-node": "=2.1.0",
"tslint": "=4.4.2",
"typescript": "=2.1.6",
"typings": "=2.1.0"
}
}

7
scripts/test-helper.js Normal file
View file

@ -0,0 +1,7 @@
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.expect();
chai.use(chaiAsPromised);
process.env.NODE_ENV = 'test';

View file

@ -1,9 +0,0 @@
const staticFile = require('connect-static-file');
const path = require('path');
const { SERVE_DIR } = require('./consts');
const handle404 = staticFile(path.join(SERVE_DIR, '.404.html'));
module.exports = function (request, response, next) {
response.statusCode = 404;
return handle404(request, response, next);
};

View file

@ -1,17 +0,0 @@
const basicAuth = require('express-basic-auth');
const { BASIC_AUTH_ENABLED } = require('./consts');
function basicAuthHandler(username, password) {
return process.env.BASIC_AUTH_USERNAME === username && process.env.BASIC_AUTH_PASSWORD === password;
}
if (BASIC_AUTH_ENABLED) {
module.exports = basicAuth({
authorizer: basicAuthHandler,
challenge: true
});
} else {
module.exports = (req, res, next) => next();
}

40
src/cli.ts Normal file
View file

@ -0,0 +1,40 @@
import { docopt } from 'docopt';
import { Options } from './types';
const PKG = require('../package.json');
const ARG_DATA = `
${PKG.name}.
${PKG.description}
Usage:
tstatic <dir> [options]
tstatic -h | --help
tstatic --version
Options:
-h --help Show this screen.
--version Show version.
-p <port> --port=<port> Port to listen on.
-b <auth> --basic-auth=<auth> Enable basic-auth.
-i <ips> --ips=<ips> Allowed IP addresses.
-l --list-dir List Directory.
--opbeat Enable Opbeat.
-o --open Open in browser after start.
`;
export default function getArgs() : Options {
const rawArgs = docopt(ARG_DATA, {
version: PKG.version,
help: true
});
return {
port: rawArgs['--port'] || process.env.PORT || 5000,
allowed_ips: rawArgs['--ips'] ? rawArgs['--ips'].split(',') : [],
basicAuth: rawArgs['--basic-auth'] ? rawArgs['--basic-auth'].split(':') : [],
dirList: rawArgs['--list-dir'],
serveDir: rawArgs['<dir>'],
opbeat: rawArgs['--opbeat'],
open: rawArgs['--open']
};
}

View file

@ -1,11 +0,0 @@
const IN_TEST = process.env.NODE_ENV === 'test';
module.exports = {
SERVE_DIR: IN_TEST ? 'site/' : process.argv[process.argv.length - 1],
PORT: process.env.PORT || 5000,
ALLOWED_IPS: process.env.ALLOWED_IPS ? process.env.ALLOWED_IPS.split(',') : undefined,
IN_TEST,
IN_PRODUCTION: process.env.NODE_ENV === 'production',
DIR_LIST: process.env.DIR_LIST,
BASIC_AUTH_ENABLED: process.env.BASIC_AUTH_USERNAME && process.env.BASIC_AUTH_PASSWORD
};

17
src/index.ts Normal file
View file

@ -0,0 +1,17 @@
import { docopt } from 'docopt';
import createServer from './server';
import getArgs from './cli';
import * as open from 'open';
console.log("Starting Server...");
const ARGS = getArgs();
const app = createServer(ARGS);
export const server = app.listen(ARGS.port, function () {
const port = server.address().port;
console.log("Server started on port " + port);
if (ARGS.open) {
open('http://0.0.0.0:' + port);
}
});

View file

@ -1,17 +0,0 @@
const winston = require('winston');
const expressWinston = require('express-winston');
module.exports = expressWinston.logger({
transports: [
new winston.transports.Console({
colorize: true
})
],
meta: false,
msg: '{{ req.url }} '
.concat('status:{{ res.statusCode }} ')
.concat('useragent:{{ req.headers["user-agent"] }} ')
.concat('time:{{ res.responseTime }}ms'),
colorize: true,
statusLevels: true
});

11
src/middleware/404.ts Normal file
View file

@ -0,0 +1,11 @@
import { Request, Response } from 'express';
import * as staticFile from 'connect-static-file'
import * as path from 'path';
export default function handle404(serveDir : string) {
const handle404Middleware = staticFile(path.join(serveDir, '.404.html'));
return function (request : Request, response : Response, next : Function) {
response.statusCode = 404;
return handle404Middleware(request, response, next);
}
}

View file

@ -0,0 +1,8 @@
import * as basicAuth from 'express-basic-auth';
export default function basicAuthHandler(username : string, password : string) {
return basicAuth({
authorizer: (req_username : string, req_password : string) => req_username === username && req_password === password,
challenge: true
});
}

17
src/middleware/logging.ts Normal file
View file

@ -0,0 +1,17 @@
import * as winston from 'winston';
import * as expressWinston from 'express-winston';
export default expressWinston.logger({
transports: [
new winston.transports.Console({
colorize: true
})
],
meta: false,
msg: '{{ req.url }} '
.concat('status:{{ res.statusCode }} ')
.concat('useragent:{{ req.headers["user-agent"] }} ')
.concat('time:{{ res.responseTime }}ms'),
colorize: true,
statusLevels: true
});

View file

@ -0,0 +1,28 @@
import * as express from 'express';
import * as serveIndex from 'serve-index';
import * as path from 'path';
function isDirectory(url : string) : boolean {
return /\/$/.test(url);
}
export function indexHandle(request : express.Request, response : express.Response, next : Function) {
if (isDirectory(request.url)) {
request.url = path.join(request.url, 'index.html');
}
return next();
}
export function staticFileHandle(serveDir : string) {
return express.static(serveDir, {
dotfiles: 'ignore',
index: false,
redirect: true
});
}
export function serveIndexHandle(serveDir : string) {
return serveIndex(serveDir, {
icons: true
});
}

View file

@ -1,54 +0,0 @@
#!/usr/bin/env node
console.log('Starting Server...');
const app = require('express')();
const consts = require('./consts');
const compression = require('compression');
const helmet = require('helmet');
const serveIndex = require('serve-index');
const AccessControl = require('express-ip-access-control');
const opbeat = require('opbeat').start({
active: consts.IN_PRODUCTION
});
const logging = require('./logging');
const staticFiles = require('./static-files');
const handle404 = require('./404');
const basicAuth = require('./basic-auth');
if (consts.ALLOWED_IPS) {
app.set('trust proxy', true);
app.use(AccessControl({
mode: 'allow',
allows: consts.ALLOWED_IPS,
statusCode: 404
}));
}
// Custom Middleware
app.use(logging);
app.use(basicAuth);
if (consts.DIR_LIST) {
app.use(serveIndex(consts.SERVE_DIR, {
icons: true
}));
} else {
app.use(staticFiles.indexHandle);
}
app.use(staticFiles.static);
app.use(handle404);
// Library
app.use(compression({ level: 9 }));
app.use(helmet());
app.use(opbeat.middleware.express());
const server = app.listen(consts.PORT, function () {
console.log('Server started on ' + server.address().port);
});
module.exports = server;

53
src/server.ts Normal file
View file

@ -0,0 +1,53 @@
import * as express from 'express';
import * as AccessControl from 'express-ip-access-control';
import * as compression from 'compression';
import * as helmet from 'helmet';
import * as opbeat from 'opbeat';
import logging from './middleware/logging';
import basicAuthHandler from './middleware/basic-auth';
import { serveIndexHandle, indexHandle, staticFileHandle } from './middleware/static-files';
import handle404 from './middleware/404';
import { Options } from './types';
export default function createServer(opts : Options) : express.Application {
const app = express();
if (process.env.NODE_ENV !== 'test') {
app.use(logging);
}
if (opts.allowed_ips.length) {
app.set('trust proxy', true);
app.use(AccessControl({
mode: 'allow',
allows: opts.allowed_ips,
statusCode: 404
}));
}
if (opts.basicAuth.length) {
app.use(basicAuthHandler(opts.basicAuth[0], opts.basicAuth[1]));
}
if (opts.dirList) {
app.use(serveIndexHandle(opts.serveDir));
} else {
app.use(indexHandle);
}
app.use(staticFileHandle(opts.serveDir));
app.use(handle404(opts.serveDir));
app.use(compression({ level: 9 }));
app.use(helmet());
if (opts.opbeat) {
app.use(opbeat.start({
active: opts.opbeat
}).middleware.express());
}
return app;
}

View file

@ -1,16 +0,0 @@
const express = require('express');
const path = require('path');
const { SERVE_DIR } = require('./consts');
module.exports.indexHandle = function (request, response, next) {
if (request.url.endsWith('/')) {
request.url = path.join(request.url, 'index.html');
}
next();
};
module.exports.static = express.static(SERVE_DIR, {
dotfiles: 'ignore',
index: false,
redirect: true
});

12
src/types/fakes.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
/* Mock types that dont exist */
declare module 'express-ip-access-control';
declare module 'connect-static-file';
declare module 'express-basic-auth';
declare module 'winston'; // doesnt like console transport
declare module 'express-winston';
declare module 'opbeat';
declare module 'docopt';
declare module 'open';
declare module 'node-fetch';
declare module 'chai';

10
src/types/index.ts Normal file
View file

@ -0,0 +1,10 @@
export interface Options {
port: number;
allowed_ips: string[];
basicAuth: string[];
dirList: boolean;
serveDir: string;
opbeat: boolean;
open: boolean;
}

14
tests/helpers.ts Normal file
View file

@ -0,0 +1,14 @@
import createServer from '../src/server';
import { Options } from '../src/types';
import fetch from 'node-fetch';
export function runServer(opts: Object, url : string, callback: Function) {
const app = createServer(opts as Options);
const server = app.listen(1234, function () {
return fetch('http://0.0.0.0:1234' + url).then(function (response : any) {
server.close();
callback(response);
});
});
}

View file

@ -1,42 +0,0 @@
const request = require('supertest');
const fs = require('fs');
describe('Server', function () {
var server;
before(function () {
server = require('../src/server');
});
after(function () {
server.close();
});
it('responds to /', function (done) {
request(server)
.get('/')
.expect(200, done);
});
it('returns 404 on bad path', function (done) {
request(server)
.get('/foo/bar')
.expect(404, done);
});
describe('index route', function () {
const body = fs.readFileSync(__dirname + '/../site/index.html').toString();
it('should render /index.html', function (done) {
request(server)
.get('/index.html')
.expect(200, body, done);
});
it('should render /', function (done) {
request(server)
.get('/')
.expect(200, body, done);
});
});
});

61
tests/server.test.ts Normal file
View file

@ -0,0 +1,61 @@
import { expect } from 'chai';
import { runServer } from './helpers';
import * as fs from 'fs';
import * as path from 'path';
describe('Server', function () {
it('should test', function () {
expect(2 + 2).to.equal(4);
});
it('Should be usable', function (done) {
runServer({
allowed_ips: [],
basicAuth: [],
dirList: false,
serveDir: 'site/',
opbeat: false,
open: false
}, '/index.html', function (response : any) {
expect(response.status).to.equal(200);
expect(response.url).to.include('/index.html');
done();
});
});
it('Should respond with 404 on bad path', function (done) {
runServer({
allowed_ips: [],
basicAuth: [],
dirList: false,
serveDir: 'site/',
opbeat: false,
open: false
}, '/foo/bar', function (response : any) {
expect(response.ok).to.be.false;
expect(response.status).to.equal(404);
expect(response.text()).to.eventually.include('Cannot GET').notify(done);
});
});
describe('index route', function () {
const body = fs.readFileSync(path.join(__dirname, '..', 'site', 'index.html')).toString();
['', '/', '/index.html'].forEach(function (path : string) {
it('should render ' + path, function (done) {
runServer({
allowed_ips: [],
basicAuth: [],
dirList: false,
serveDir: 'site/',
opbeat: false,
open: false
}, path, function (response : any) {
expect(response.status).to.equal(200);
expect(response.text()).to.eventually.equal(body).notify(done);
});
});
});
});
});

28
tsconfig.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
"outDir": "dist",
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,
"experimentalDecorators": true,
"preserveConstEnums": true,
"allowJs": true,
"sourceMap": true,
"pretty": true
},
"filesGlob": [
"typings/index.d.ts",
"src/**/*.ts"
],
"exclude": [
"dist",
"node_modules",
"scripts/",
"tests/"
],
"typeRoots": [
"node_modules",
"typings",
"src/types"
],
"compileOnSave": false
}

7
tslint.json Normal file
View file

@ -0,0 +1,7 @@
{
"extends": "tslint:recommended",
"rules": {
"quotemark": [true, "single"],
"trailing-comma": [false]
}
}

16
typings.json Normal file
View file

@ -0,0 +1,16 @@
{
"dependencies": {
"compression": "registry:dt/compression#0.0.0+20160725212620",
"debug": "registry:npm/debug#2.0.0+20160723033700",
"express": "registry:npm/express#4.14.0+20160925001530",
"helmet": "registry:dt/helmet#0.0.0+20161005184000",
"serve-index": "registry:dt/serve-index#1.7.2+20160428043022"
},
"globalDependencies": {
"node": "registry:dt/node#7.0.0+20170204020307"
},
"globalDevDependencies": {
"chai": "registry:dt/chai#3.4.0+20170217154556",
"mocha": "registry:dt/mocha#2.2.5+20170204022515"
}
}