archive
/
tstatic
Archived
1
Fork 0

Compare commits

...

34 Commits

Author SHA1 Message Date
Jake Howard b86b542998
Document the index file 2018-05-05 12:02:10 +01:00
Jake Howard 3caa4a51db
Move install location to `/opt` and install globally 2018-05-05 12:01:07 +01:00
Jake Howard 0d311ac19c
Build container on CI 2018-05-04 22:00:13 +01:00
Jake Howard a90feaffdf
Change serve dir in container to /var/www 2018-05-04 21:01:01 +01:00
Jake Howard ee45331b4d
Correct prepublish script 2018-05-04 20:56:34 +01:00
Jake Howard 0c763cafd3
Use any version of node 8 2018-05-04 20:55:56 +01:00
Jake Howard 0ec819e232
Cleanup container and reorder 2018-05-04 20:47:51 +01:00
Jake Howard c74f486b7e
Repair docker file and run in prod env 2018-03-21 11:28:30 +00:00
Jake Howard cc5d07595b
Cleanup readme 2018-03-21 10:38:21 +00:00
Jake Howard c9004a8be5
Release 1.2.0 2018-03-15 22:07:57 +00:00
Jake Howard 18c4ae38a0
Remove reference to opbeat in readme 2018-03-15 22:05:05 +00:00
Jake Howard d8ba44143d
Add more type definitions from types 2018-03-15 22:01:07 +00:00
Jake Howard 3232eba22a
Remove opbeat 2018-03-15 21:51:49 +00:00
Jake Howard 25c973303d
Swap out linter 2018-03-15 21:40:35 +00:00
Jake Howard 2e2e4c8b8f
Update typescript and linter 2018-03-15 21:31:01 +00:00
Jake Howard e53846e71f
Update test dependencies 2018-03-15 21:29:04 +00:00
Jake Howard 28b1c4f51c
Update key dependencies 2018-03-15 21:23:59 +00:00
Jake Howard c35760d95e
Use express logger compatible with nginx 2018-03-15 20:58:16 +00:00
Jake Howard 28eec3e28c
Upgrade node to 8.10.0 2018-03-15 20:37:56 +00:00
Jake Howard 38dcc3e5fa
Remove reference to typings in docker file 2018-01-28 20:04:34 +00:00
Jake Howard b86ce01aa2
Test missing HSTS header 2018-01-28 11:52:19 +00:00
Jake Howard ac29b7f932
Test static file middleware 2018-01-28 11:49:08 +00:00
Jake Howard 45904be707
Add additional demo file 2018-01-28 11:37:04 +00:00
Jake Howard 5d5f69c79e
Change default page from some error page 2018-01-28 11:33:11 +00:00
Jake Howard 92a4fc3cda
Update to CircleCI 2.0 2018-01-28 11:29:54 +00:00
Jake Howard 08a15c97ab
Replace typings with @types 2018-01-28 11:24:43 +00:00
Jake Howard bdcff47ba4
Update versions to node 8 2018-01-28 11:10:42 +00:00
Jake Howard 44c64d4f05
Add some docker components 2018-01-26 20:01:25 +00:00
Jake Howard fb7764b3c7
Remove nsp
GitHub does this now
2018-01-17 21:38:49 +00:00
Jake Howard e16075a702
Test HSTS header 2018-01-17 20:59:16 +00:00
Jake Howard 42f5af27f0
Check powered by header isnt set 2018-01-17 20:40:09 +00:00
Jake Howard 251d46b43f
Use HSTS all the time, unless serving over HTTP
Not serving the HSTS header over HTTP is fairly bad
2018-01-17 20:31:12 +00:00
Jake Howard ef9b100dd1
Remove `X-Powered-By` header 2018-01-17 20:03:41 +00:00
Jake Howard e3d5406d27
Fix test name 2017-07-08 14:39:13 +01:00
22 changed files with 1883 additions and 161 deletions

21
.circleci/config.yml Normal file
View File

@ -0,0 +1,21 @@
version: 2.0
jobs:
build:
docker:
- image: circleci/node:8
steps:
- checkout
- run:
name: Install dependencies
command: npm install
- run:
name: Build Project
command: npm run build
- run:
name: Run Tests
command: npm test
- setup_remote_docker
- run:
name: Build docker container
command: docker build .

27
Dockerfile Normal file
View File

@ -0,0 +1,27 @@
FROM node:8-alpine
COPY ./src /opt/tstatic/src
COPY ./package.json opt/tstatic/package.json
COPY ./package-lock.json opt/tstatic/package-lock.json
COPY ./tsconfig.json opt/tstatic/tsconfig.json
COPY ./site /var/www
WORKDIR /opt/tstatic
RUN apk add --no-cache git
RUN npm install
ENV NODE_ENV production
RUN npm run build
RUN npm prune --production
RUN npm install -g .
WORKDIR /
CMD tstatic /var/www
EXPOSE 5000

View File

@ -7,12 +7,10 @@
The only static-file server you'll ever need!
### Features:
- 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)
@ -26,7 +24,6 @@ The only static-file server you'll ever need!
-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.
```
`dir` is where your static files are.
@ -40,7 +37,7 @@ The only static-file server you'll ever need!
The port for the server to listen on. Currently supports plain HTTP only
##### `basic-auth`
Enable basic-auth for all paths. Currently only supports single credentals.
Enable basic-auth for all paths. Currently only supports single credentals.
Format:`-b username:password`
@ -52,8 +49,24 @@ 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>`.
### Docker
Included in this repo is a `Dockerfile` to use. The default setup requires being run from the project directory, and will serve `/var/www` in the container on port `5000`. By default, this directory is contains a simple index file, however can be overriden.
Below is an example `docker-compose.yml` file you can use with it:
```yml
version: "2"
services:
tstatic:
image: "tstatic"
build:
context: .
dockerfile: Dockerfile
volumes:
- ./site/:/var/www
ports:
- "5000:5000"
```

View File

@ -1,3 +0,0 @@
machine:
node:
version: 6.11.0

1637
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "tstatic",
"version": "1.1.0",
"version": "1.2.0",
"description": "The only static-file server you'll ever need!",
"main": "./dist/server.js",
"bin": {
@ -8,14 +8,14 @@
},
"scripts": {
"start": "node ./dist/index.js",
"prepublish": "typings install && npm run build",
"prepublishOnly": "npm run build",
"build": "tsc",
"test": "npm run build && npm run lint && npm run mocha && nsp check",
"mocha": "mocha --compilers ts:ts-node/register --require scripts/test-helper.js --recursive --bail tests/**/*.test.ts tests/*.test.ts",
"lint": "tslint src/**/*.ts --type-check --project tsconfig.json"
"test": "npm run lint && npm run mocha",
"mocha": "mocha --require scripts/test-helper.js --recursive --bail tests/**/*.test.ts tests/*.test.ts",
"lint": "tslint src/**/*.ts --project tsconfig.json"
},
"engines": {
"node": "6.9.4"
"node": "8"
},
"repository": {
"type": "git",
@ -27,29 +27,36 @@
},
"homepage": "https://github.com/RealOrangeOne/tstatic#readme",
"dependencies": {
"compression": "1.6.2",
"connect-static-file": "1.2.0",
"compression": "1.7.2",
"connect-static-file": "2.0.0",
"docopt": "0.6.2",
"express": "4.15.3",
"express-basic-auth": "1.0.1",
"express": "4.16.3",
"express-basic-auth": "1.1.4",
"express-ip-access-control": "1.0.5",
"express-winston": "2.4.0",
"helmet": "3.6.1",
"opbeat": "4.14.0",
"helmet": "3.12.0",
"morgan": "1.9.0",
"open": "0.0.5",
"serve-index": "1.9.0",
"winston": "2.3.1"
"serve-index": "1.9.1"
},
"devDependencies": {
"chai": "4.0.2",
"@types/docopt": "0.6.31",
"@types/node-fetch": "1.6.7",
"@types/open": "0.0.29",
"@types/chai": "4.1.2",
"@types/compression": "0.0.35",
"@types/express": "4.11.1",
"@types/helmet": "0.0.37",
"@types/mocha": "2.2.48",
"@types/morgan": "1.7.35",
"@types/serve-index": "1.7.29",
"chai": "4.1.2",
"chai-as-promised": "7.1.1",
"mocha": "3.4.2",
"node-fetch": "1.7.1",
"nsp": "2.6.3",
"mocha": "5.0.4",
"node-fetch": "2.1.1",
"supertest": "3.0.0",
"ts-node": "3.2.0",
"tslint": "5.5.0",
"typescript": "2.2.1",
"typings": "2.1.0"
"ts-node": "5.0.1",
"tslint": "5.9.1",
"tslint-config-dabapps": "dabapps/tslint-config-dabapps#v0.3.0",
"typescript": "2.7.2"
}
}

View File

@ -1,3 +1,5 @@
require('ts-node/register');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');

View File

@ -2,24 +2,10 @@
<html>
<head>
<title>Oops!</title>
<style type="text/css">
body {
width: 75%;
padding-top: 50px;
margin: 0 auto;
text-align: center;
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
font-size: 80%;
color: #333;
}
h1 {
margin: 30px 0;
}
</style>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Something's not right!</h1>
<p><strong>Sorry!</strong> Something has gone wrong while handling your request. Please try again later.</p>
<h1>TStatic File Server</h1>
<p>Add some content to your serve directory to get started</p>
</body>
</html>

13
site/style.css Normal file
View File

@ -0,0 +1,13 @@
body {
width: 75%;
padding-top: 50px;
margin: 0 auto;
text-align: center;
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
font-size: 80%;
color: #333;
}
h1 {
margin: 30px 0;
}

View File

@ -1,5 +1,5 @@
import { docopt } from 'docopt';
import { Options } from './types';
import { IOptions } from './types';
const PKG = require('../package.json');
@ -19,11 +19,11 @@ Options:
-b <auth> --basic-auth=<auth> Enable basic-auth.
-i <ips> --ips=<ips> Allowed IP addresses.
-l --list-dir List Directory.
--opbeat Enable Opbeat.
-s --allow-http Allow connection over HTTP.
-o --open Open in browser after start.
`;
export default function getArgs() : Options {
export default function getArgs() : IOptions {
const rawArgs = docopt(ARG_DATA, {
version: PKG.version,
help: true
@ -34,7 +34,7 @@ export default function getArgs() : Options {
basicAuth: rawArgs['--basic-auth'] ? rawArgs['--basic-auth'].split(':') : [],
dirList: rawArgs['--list-dir'],
serveDir: rawArgs['<dir>'],
opbeat: rawArgs['--opbeat'],
open: rawArgs['--open']
open: rawArgs['--open'],
allowHttp: rawArgs['--allow-http']
};
}

View File

@ -2,7 +2,7 @@
import { docopt } from 'docopt';
import createServer from './server';
import getArgs from './cli';
import * as open from 'open';
import open from 'open';
const ARGS = getArgs();

View File

@ -4,7 +4,7 @@ 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: () => void) {
return (request: Request, response: Response, next: () => void) => {
response.statusCode = 404;
return handle404Middleware(request, response, next);
};

View File

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

View File

@ -3,39 +3,34 @@ 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 * as expectCt from 'expect-ct';
import * as referrerPolicy from 'referrer-policy';
import * as morgan from 'morgan';
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';
import { IOptions } from './types';
const PKG = require('../package.json');
export default function createServer(opts : Options) : express.Application {
export default function createServer(opts : IOptions) : express.Application {
const app = express();
app.use(helmet());
app.use(helmet.hidePoweredBy({setTo: `tstatic ${PKG.version}`}));
app.use(helmet.ieNoOpen());
app.use(helmet.noCache());
app.use(helmet({
hsts: {
maxAge: 5184000,
setIf: () => !opts.allowHttp,
includeSubdomains: false
},
noCache: true,
expectCt: {
enforce: false,
maxAge: 1000
}
}));
app.use(referrerPolicy({ policy: 'same-origin' }));
app.use(expectCt({
enforce: false,
maxAge: 1000
}));
app.use(helmet.hsts({
maxAge: 5184000,
setIf: (req, res) => req.secure,
}));
if (process.env.NODE_ENV !== 'test') {
app.use(logging);
app.use(morgan('combined'));
}
if (opts.allowed_ips.length) {
@ -61,11 +56,6 @@ export default function createServer(opts : Options) : express.Application {
app.use(handle404(opts.serveDir));
app.use(compression({ level: 9 }));
if (opts.opbeat) {
app.use(opbeat.start({
active: opts.opbeat
}).middleware.express());
}
return app;
}

View File

@ -3,12 +3,5 @@
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';
declare module 'expect-ct';
declare module 'referrer-policy';

View File

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

View File

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

View File

@ -0,0 +1,77 @@
import { expect } from 'chai';
import { runServer } from '../helpers';
import * as fs from 'fs';
import * as path from 'path';
import { IOptions } from '../../src/types';
describe('Static Files', function () {
const body = fs.readFileSync(path.join(__dirname, '../..', 'site', 'index.html')).toString();
const cssBody = fs.readFileSync(path.join(__dirname, '../..', 'site', 'style.css')).toString();
describe('index route', function () {
['', '/', '/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);
});
});
});
});
it('Should return static files', function (done) {
runServer({
allowed_ips: [],
basicAuth: [],
dirList: false,
serveDir: 'site/',
opbeat: false,
open: false
}, '/style.css', function (response : any) {
expect(response.status).to.equal(200);
expect(response.text()).to.eventually.equal(cssBody).notify(done);
});
});
describe('Directory listing', function () {
const SERVER_CONFIG = {
allowed_ips: [],
basicAuth: [],
dirList: true,
serveDir: 'site/',
opbeat: false,
open: false
} as IOptions;
it('Should allow directory listing', function (done) {
runServer(SERVER_CONFIG, '/', function (response : any) {
expect(response.status).to.equal(200);
expect(response.text()).to.eventually.not.equal(body).notify(done);
});
});
it('Should show index file in directory listing', function (done) {
runServer(SERVER_CONFIG, '/', function (response : any) {
expect(response.status).to.equal(200);
expect(response.text()).to.eventually.contain('style.css').notify(done);
});
});
it('Should show css file in directory listing', function (done) {
runServer(SERVER_CONFIG, '/', function (response : any) {
expect(response.status).to.equal(200);
expect(response.text()).to.eventually.contain('index.html').notify(done);
});
});
});
});

View File

@ -2,7 +2,7 @@ import { expect } from 'chai';
import { runServer } from './helpers';
import * as fs from 'fs';
import * as path from 'path';
import { Options } from '../src/types';
import { IOptions } from '../src/types';
const PKG = require('../package.json');
@ -22,26 +22,6 @@ describe('Server', function () {
});
});
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);
});
});
});
});
describe('secure headers', function () {
const SERVER_SETTINGS = {
allowed_ips: [],
@ -49,14 +29,14 @@ describe('Server', function () {
dirList: false,
serveDir: 'site/',
opbeat: false,
open: false
} as Options;
open: false,
allowHttp: false
} as IOptions;
it('Should have no powered by header', function (done) {
runServer(SERVER_SETTINGS, '/index.html', function (response : any) {
expect(response.status).to.equal(200);
expect(response.headers.get('x-powered-by')).to.contain('tstatic');
expect(response.headers.get('x-powered-by')).to.contain(PKG.version);
expect(response.headers.get('x-powered-by')).to.be.null;
done();
});
});
@ -113,12 +93,28 @@ describe('Server', function () {
});
});
it('Should block cache', function (done) {
it('Should block referrer transfer', function (done) {
runServer(SERVER_SETTINGS, '/index.html', function (response : any) {
expect(response.status).to.equal(200);
expect(response.headers.get('referrer-policy')).to.contain('same-origin');
done();
});
});
it('Should have HSTS header', function (done) {
runServer(SERVER_SETTINGS, '/index.html', function (response : any) {
expect(response.status).to.equal(200);
expect(response.headers.get('strict-transport-security')).to.contain('5184000');
done();
});
});
it('Should not have HSTS header if HTTP is allowed', function (done) {
runServer({...SERVER_SETTINGS, allowHttp: true}, '/index.html', function (response : any) {
expect(response.status).to.equal(200);
expect(response.headers.get('strict-transport-security')).to.be.null;
done();
});
});
});
});

View File

@ -21,7 +21,7 @@
],
"typeRoots": [
"node_modules",
"typings",
"node_modules/@types/",
"src/types"
],
"compileOnSave": false

View File

@ -1,9 +1,5 @@
{
"extends": "tslint:recommended",
"rules": {
"quotemark": [true, "single"],
"trailing-comma": false,
"only-arrow-functions": false,
"interface-name": false
}
"extends": [
"tslint-config-dabapps"
]
}

View File

@ -1,16 +0,0 @@
{
"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+20170310054102",
"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"
}
}