├── .gitignore ├── Dockerfile ├── bin └── deelay.js ├── package.json ├── README.md ├── .circleci └── config.yml ├── LICENSE ├── index.js └── test └── indexTest.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | ENV APP_DIR /app 4 | RUN mkdir ${APP_DIR} 5 | WORKDIR ${APP_DIR} 6 | ADD . ${APP_DIR} 7 | CMD ["./bin/deelay.js"] 8 | -------------------------------------------------------------------------------- /bin/deelay.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const deelay = require('../index.js'); 4 | const http = require('http'); 5 | const port = process.env.PORT || 4567; 6 | 7 | http.createServer((req, res) => deelay(req, res, process.stdout)).listen(4567, () => console.log(`Starting delay on port ${port}`)); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deelay", 3 | "version": "2.0.0", 4 | "description": "Delay proxy for http resources", 5 | "repository": "https://github.com/biesiad/deelay", 6 | "homepage": "http://www.deelay.me/", 7 | "main": "index.js", 8 | "bin": { 9 | "deelay": "bin/deelay.js" 10 | }, 11 | "scripts": { 12 | "test": "node test/indexTest.js" 13 | }, 14 | "author": "Grzegorz Biesiadecki", 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | If you are looking for deelay ruby gem, you can find it [here](https://github.com/biesiad/deelay-ruby). 2 | 3 | # [Deelay.me](http://deelay.me) 4 | 5 | [![CircleCI](https://circleci.com/gh/biesiad/deelay.svg?style=svg)](https://circleci.com/gh/biesiad/deelay) 6 | 7 | **Inline delay proxy for http resources** 8 | Slow loading resources (images, scripts, etc) can break your code. Test it simulating unexpected network conditions applied to specific resource. 9 | 10 | ## Local installation 11 | ```sh 12 | $ npm install -g deelay 13 | $ deelay 14 | Starting delay on port 4567 15 | ``` 16 | 17 | ## Docker installation 18 | ```sh 19 | $ docker build -t deelay . 20 | $ docker run -p 4567:4567 deelay 21 | Starting delay on port 4567 22 | ``` 23 | 24 | ## Usage 25 | ```sh 26 | curl localhost:4567/1000/http://mysite.com/image.gif 27 | ``` 28 | 29 | ### Tests 30 | ```sh 31 | node test/indexTest.js 32 | ``` 33 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:8.9.4 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/deelay 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: npm test 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Grzegorz Biesiadecki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = (request, response, stdout) => { 2 | const path = request.url.split('/'); 3 | const delay = path[1]; 4 | let redirectUrl = path.slice(2).join('/'); 5 | 6 | if (request.method === 'GET' && !isNaN(delay) && path.length > 2) { 7 | if (!redirectUrl.match(/^(http|https):/)) { 8 | redirectUrl = `https://${redirectUrl}`; 9 | } 10 | stdout && stdout.write(`${delay}... `); 11 | 12 | setTimeout(() => { 13 | stdout && stdout.write(`${redirectUrl}\n`); 14 | response.statusCode = 302; 15 | response.setHeader('Location', redirectUrl); 16 | response.setHeader('Access-Control-Allow-Origin', '*'); 17 | response.end(); 18 | }, delay); 19 | } else if (request.method === 'OPTIONS') { 20 | response.statusCode = 200; 21 | response.setHeader('Access-Control-Allow-Origin', '*'); 22 | response.setHeader('Access-Control-Allow-Headers', request.headers['access-control-request-headers'] || ''); 23 | response.setHeader('Access-Control-Allow-Methods', request.headers['access-control-request-methods'] || ''); 24 | response.end(); 25 | } else { 26 | response.statusCode = 404; 27 | response.end(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /test/indexTest.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const http = require('http'); 3 | const deelay = require('../index.js'); 4 | 5 | const test = async (message, options, callback) => { 6 | const server = http.createServer(deelay).listen(1234); 7 | return new Promise(resolve => { 8 | http.get( 9 | { 10 | host: 'localhost', 11 | port: 1234, 12 | ...options, 13 | }, 14 | response => { 15 | response.on('data', () => null); 16 | response.on('end', () => { 17 | callback(response); 18 | server.close(); 19 | resolve(); 20 | }); 21 | }, 22 | ); 23 | }); 24 | }; 25 | 26 | (async () => { 27 | await test('redirects to url with http://', { path: '/10/http://testurl.com' }, response => { 28 | assert.equal(response.statusCode, 302); 29 | assert.equal(response.headers['location'], 'http://testurl.com'); 30 | }); 31 | 32 | await test('add https to url without protocol', { path: '/10/testurl.com' }, response => { 33 | assert.equal(response.statusCode, 302); 34 | assert.equal(response.headers['location'], 'https://testurl.com'); 35 | }); 36 | 37 | await test('redirects to url with https://', { path: '/10/https://testurl.com' }, response => { 38 | assert.equal(response.statusCode, 302); 39 | assert.equal(response.headers['location'], 'https://testurl.com'); 40 | }); 41 | 42 | await test('redirects to url with path', { path: '/10/http://testurl.com/path' }, response => { 43 | assert.equal(response.statusCode, 302); 44 | assert.equal(response.headers['location'], 'http://testurl.com/path'); 45 | }); 46 | 47 | await test('redirects to url with query string', { path: '/10/http://testurl.com?key=value' }, response => { 48 | assert.equal(response.statusCode, 302); 49 | assert.equal(response.headers['location'], 'http://testurl.com?key=value'); 50 | }); 51 | 52 | await test('redirects to url with hostname:port', { path: '/10/http://testurl.com:1234' }, response => { 53 | assert.equal(response.statusCode, 302); 54 | assert.equal(response.headers['location'], 'http://testurl.com:1234'); 55 | }); 56 | 57 | await test('returns 404 when delay param missing', { path: '/http://testurl.com' }, response => { 58 | assert.equal(response.statusCode, 404, 'This sucks'); 59 | }); 60 | 61 | await test('responds with 404 when delay param malformed', { path: '/1000%20/http://testurl.com' }, response => { 62 | assert.equal(response.statusCode, 404); 63 | }); 64 | 65 | await test('returns 404 when url missing', { path: '/10' }, response => { 66 | assert.equal(response.statusCode, 404); 67 | }); 68 | 69 | await test('returns 404 on / call', { path: '' }, response => { 70 | assert.equal(response.statusCode, 404); 71 | }); 72 | 73 | await test('responds with CORS headers for GET requests', { path: '/10/http://testurl.com' }, response => { 74 | assert.equal(response.headers['access-control-allow-origin'], '*'); 75 | }); 76 | 77 | await test('responds and forwards CORS headers for OPTIONS requests', 78 | { 79 | path: '/10/http://testurl.com', 80 | method: 'options', 81 | headers: { 82 | 'Access-Control-Request-Methods': 'GET', 83 | 'Access-Control-Request-Headers': 'Content-Type', 84 | }, 85 | }, 86 | response => { 87 | assert.equal( 88 | response.headers['access-control-allow-headers'], 89 | 'Content-Type', 90 | ); 91 | assert.equal(response.headers['access-control-allow-methods'], 'GET'); 92 | }, 93 | ); 94 | })(); 95 | --------------------------------------------------------------------------------