diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..be65583 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +.DS_Store +node_modules diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1ff10c9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +; Works with some editors only +quote_type = single +max_line_length = 120 +spaces_around_brackets = true +spaces_around_operators = true diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e4cc4d9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +sudo: required + +env: + global: + - TAG_PATTERN="^[0-9]+(\.[0-9]+){2}(-(alpha|beta|rc))?$" + - DOCKER_IMAGE=eexit/mirror-http-server:${TRAVIS_TAG:=$TRAVIS_BUILD_NUMBER} + +services: + - docker + +before_install: + - docker login --email=$DOCKER_EMAIL --username=$DOCKER_USER --password=$DOCKER_PASSWD + +install: + - docker build -t $DOCKER_IMAGE . + +script: + - docker run $DOCKER_IMAGE npm test + +after_success: + - if [[ "$TRAVIS_TAG" =~ $TAG_PATTERN ]]; then docker push $DOCKER_IMAGE; fi diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..db8138f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,2 @@ +FROM node:4.2-onbuild +MAINTAINER Joris Berthelot diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..09d44be --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Joris Berthelot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..da8b12f --- /dev/null +++ b/README.md @@ -0,0 +1,195 @@ +![logo](logo.png) + +# Mirror HTTP Server [![Build Status](https://travis-ci.org/eexit/mirror-http-server.svg)](https://travis-ci.org/eexit/mirror-http-server) + +*A dummy HTTP server that responds whatever you told him to.* + +Build to play with HTTP or test your API. Make a HTTP call to the dummy server with the specified headers you want the server responds with. + +## Usage + +Pull the [Docker](https://www.docker.com) container: + + $ docker pull eexit/mirror-http-server + +Start the container: + + $ docker run -itp 80:80 eexit/mirror-http-server + 2015-11-05T20:59:57.353Z] INFO: mirror-http-server/17 on ccc867df5980: Listening on http://0.0.0.0:80 + +For this README examples, I use the great [HTTPie](https://github.com/jkbrzt/httpie) tool. + +Send request againt it: + + http $(docker-machine ip default) + +```http +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 0 +Date: Thu, 05 Nov 2015 21:33:20 GMT +X-Powered-By: Express +``` + +You can use any [HTTP verbs](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods) with any path, any request body and any header. + +### Behavioural request headers + +You can change the server response code and body by setting specific `X-Mirror-*` headers to your request. + +### `X-Mirror-Code` + +Change the server response [status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes). +Here, simulate a server error: + + $ http $(docker-machine ip default) X-Mirror-Code:503 + +```http +HTTP/1.1 503 Service Unavailable +Connection: keep-alive +Content-Length: 0 +Date: Thu, 05 Nov 2015 22:30:11 GMT +X-Powered-By: Express +``` + +Here, simulate a `301` redirection: + + http $(docker-machine ip default) \ + X-Mirror-Code:301 \ + X-Mirror-Location:http://www.eexit.net \ + X-Mirror-Content-Type:"text/plain; charset=ISO-8859-1" + +```http +HTTP/1.1 301 Moved Permanently +Connection: keep-alive +Content-Length: 0 +Content-Type: text/plain; charset=ISO-8859-1 +Date: Thu, 05 Nov 2015 22:40:02 GMT +Location: http://www.eexit.net +X-Powered-By: Express +``` + +If you add the `--follow` option, it will output my website HTML source. + +If you check the container logs: + +```json +[2015-11-05T22:48:59.564Z] INFO: mirror-server/18 on 6cb74ed853b0: + request: { + "ip": "192.168.99.1", + "ips": [], + "method": "GET", + "url": "/", + "headers": { + "host": "192.168.99.100", + "x-mirror-code": "301", + "accept-encoding": "gzip, deflate", + "x-mirror-location": "http://www.eexit.net", + "accept": "*/*", + "user-agent": "HTTPie/0.9.2", + "connection": "keep-alive", + "x-mirror-content-type": "text/plain; charset=ISO-8859-1" + }, + "body": {} + } +``` + +### `X-Mirror-Request` + +If you access to the server logs or want to exploit the what's logged, set the `X-Mirror-Request` to receive what's logged in a JSON format: + + $ http POST $(docker-machine ip default)/resource \ + X-Mirror-Code:201 \ + X-Mirror-Request:true \ + key1=value1 key2=value2 + +```http +HTTP/1.1 201 Created +Connection: keep-alive +Content-Length: 373 +Content-Type: application/json; charset=utf-8 +Date: Thu, 05 Nov 2015 22:57:17 GMT +ETag: W/"175-3rxm7gM5Zwu88cZOABP92A" +X-Powered-By: Express + +{ + "request": { + "body": { + "key1": "value1", + "key2": "value2" + }, + "headers": { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "content-length": "36", + "content-type": "application/json", + "host": "192.168.99.100", + "user-agent": "HTTPie/0.9.2", + "x-mirror-code": "201", + "x-mirror-request": "true" + }, + "ip": "192.168.99.1", + "ips": [], + "method": "POST", + "url": "/resource" + } +} +``` + +### `X-Mirror-Body` + +Instead, if you with the dummy server to return you the same body you requested to it, set the `X-Mirror-Body` header. + +Note: the `X-Mirror-Request` header will override `X-Mirror-Body` header. + + $ http PUT $(docker-machine ip default)/resource \ + X-Mirror-Code:400 \ + X-Mirror-Body:true \ + key1=value1 key2=value2 + +```http +HTTP/1.1 400 Bad Request +Connection: keep-alive +Content-Length: 33 +Content-Type: application/json; charset=utf-8 +Date: Thu, 05 Nov 2015 23:52:34 GMT +ETag: W/"21-/0XMODUWUwfvQUwjyixvZw" +X-Powered-By: Express + +{ + "key1": "value1", + "key2": "value2" +} +``` + +### Works for all headers + +Aside to the previous three special headers, you can set your wanted response header by prepending your header name by `X-Mirror-`. + +In the request: + +```http +Content-Type: application/json +X-Mirror-Content-Type: text/html +``` + +You'll get in your response: + +```http +Content-Type: text/html +``` + +You can even override Express headers or any other default header: + +```http +X-Mirror-X-Powered-By: eexit-engine +X-Mirror-Date: some date +``` + +Will turn into: + +```http +X-Powered-By: eexit-engine +Date: some date +``` diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..e22cb03 Binary files /dev/null and b/logo.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..d6ae89c --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "mirror-http-server", + "version": "1.0.0", + "description": "A dummy HTTP server that responds whatever you told him to", + "scripts": { + "start": "node server.js | npm run bunyan", + "start:dev": "nodemon server.js | npm run bunyan", + "test": "echo \"No test specified yet\"", + "bunyan": "$(npm bin)/bunyan" + }, + "keywords": [ + "node", + "nodejs", + "server", + "http", + "mirror", + "dumb", + "dump", + "test", + "development" + ], + "author": "Joris Berthelot ", + "license": "MIT", + "dependencies": { + "body-parser": "^1.14.1", + "bunyan": "^1.5.1", + "express": "^4.13.3", + "lodash": "^3.10.1", + "nodemon": "^1.8.1" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..8cf2dfd --- /dev/null +++ b/server.js @@ -0,0 +1,88 @@ +'use strict'; + +var host = process.env.HOST || '0.0.0.0'; +var port = process.env.PORT || 80; + +var _ = require('lodash'); +var bunyan = require('bunyan'); +var bodyParser = require('body-parser'); +var pckg = require(__dirname + '/package.json'); +var logger = bunyan.createLogger({name: pckg.name}); +var express = require('express'); +var app = express(); + +app.enable('trust proxy'); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +// Intercepts all HTTP verb requests +app.all('*', function (req, res, next) { + // Returned response headers + var responseHeaders = {}; + + // Parses the wanted response code + var mirrorCode = req.get('X-Mirror-Code') || 200; + + // Finds out if the request should be returned as the response + var mirrorRequest = (req.get('X-Mirror-Request') + && req.get('X-Mirror-Request').toLowerCase() == 'true') + || false; + + // Finds out if the response should be returned + var mirrorBody = (req.get('X-Mirror-Body') + && req.get('X-Mirror-Body').toLowerCase() == 'true') + || false; + + // Parses X-Mirror-* headers, skips app specific headers + var reqHeaders = _.without( + _.filter( + Object.keys(req.headers), function (name) { + return _.startsWith(name, 'x-mirror-'); + } + ), 'x-mirror-code', 'x-mirror-request', 'x-mirror-body' + ); + + // Injects X-Mirror-* headers to response headers + reqHeaders.forEach(function (name) { + var resHeader = _.startCase(_.trimLeft(name, 'x-mirror-')).replace(' ', '-'); + responseHeaders[resHeader] = req.headers[name]; + }); + + // Builds the request object + var request = { + request: { + ip: req.ip, + ips: req.ips, + method: req.method, + url: req.originalUrl, + headers: req.headers, + body: req.body + } + }; + + logger.info(request); + + + // Prepares the response + res.status(mirrorCode).set(responseHeaders); + + // Appends the full request or only the request body if wanted + if (mirrorRequest) { + res.json(request); + } else if (mirrorBody) { + res.send(req.body); + } + + // Flushes! + res.end(); +}); + +// Basic error handler +app.use(function (err, req, res, next) { + logger.fatal(err); + res.status(500).json(err); +}); + +app.listen(port, host, 511, function () { + logger.info('Listening on http://%s:%s', host, port); +});