├── .gitignore ├── demo ├── example.pdf └── index.html ├── CHANGELOG.md ├── index.js ├── package.json ├── LICENSE ├── swopr-responses.js ├── swopr-responses.example.js ├── swopr.js ├── README.md └── .eslintrc.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | sw-proxy-responses.js 4 | 5 | -------------------------------------------------------------------------------- /demo/example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DannyMoerkerke/swopr/HEAD/demo/example.pdf -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.0.2] - 2019-18-11 9 | ### Added 10 | - repo url 11 | 12 | ## [0.0.1] - 2019-18-11 13 | - First version 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const registration = await navigator.serviceWorker.register('./swopr.js'); 3 | 4 | try { 5 | await navigator.serviceWorker.ready 6 | 7 | console.log('[SWOPR] proxy server ready'); 8 | } 9 | catch(err) { 10 | console.error('error registering SWOPR:', err) 11 | } 12 | 13 | window.addEventListener('beforeunload', async () => { 14 | await registration.unregister(); 15 | }); 16 | })(); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swopr", 3 | "version": "0.0.1", 4 | "description": "Service Worker based proxy server", 5 | "author": "Danny Moerkerke ", 6 | "license": "ISC", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+ssh://git@github.com:DannyMoerkerke/swopr.git" 10 | }, 11 | "homepage": "https://github.com/DannyMoerkerke/swopr", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "main": "swopr.js", 16 | "scripts": { 17 | "start": "ws -p 8080", 18 | "lint": "eslint ." 19 | }, 20 | "devDependencies": { 21 | "local-web-server": "5.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (c) 2019 Danny Moerkerke and other contributors 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /swopr-responses.js: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { 3 | id: '1', 4 | name: 'User 1' 5 | }, 6 | { 7 | id: '2', 8 | name: 'User 2' 9 | }, 10 | { 11 | id: '3', 12 | name: 'User 3' 13 | } 14 | ]; 15 | 16 | const getUserById = ({id}) => users.find((user) => user.id === id); 17 | 18 | /* eslint-disable no-unused-vars */ 19 | const responses = [ 20 | { 21 | url: 'https://api.example.com/json', 22 | method: 'GET', 23 | headers: { 24 | 'Content-Type': 'application/json' 25 | }, 26 | body: { 27 | message: 'a json response' 28 | } 29 | }, 30 | { 31 | url: 'https://api.example.com/users/:id', 32 | method: 'GET', 33 | headers: { 34 | 'Content-Type': 'application/json' 35 | }, 36 | body: params => getUserById(params), 37 | }, 38 | { 39 | url: 'https://api.example.com/text', 40 | method: 'GET', 41 | body: 'plain text body' 42 | }, 43 | { 44 | url: 'https://api.example.com/pdf', 45 | method: 'GET', 46 | headers: { 47 | 'Content-Type': 'application/pdf' 48 | }, 49 | file: 'demo/example.pdf' 50 | }, 51 | { 52 | url: 'https://api.example.com/post', 53 | method: 'POST', 54 | status: 201, 55 | statusText: 'Created', 56 | headers: { 57 | 'Content-Type': 'application/json' 58 | }, 59 | body: { 60 | message: 'created' 61 | } 62 | }, 63 | { 64 | url: 'https://api.example.com/notfound', 65 | method: 'GET', 66 | status: 404, 67 | statusText: 'Not found', 68 | headers: { 69 | 'Content-Type': 'application/json' 70 | }, 71 | body: { 72 | message: 'Not found' 73 | } 74 | }, 75 | { 76 | url: 'https://localhost/*', 77 | method: 'GET', 78 | status: 302, 79 | redirectUrl: 'http://localhost:8080/$1' 80 | } 81 | ]; 82 | -------------------------------------------------------------------------------- /swopr-responses.example.js: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { 3 | id: '1', 4 | name: 'User 1' 5 | }, 6 | { 7 | id: '2', 8 | name: 'User 2' 9 | }, 10 | { 11 | id: '3', 12 | name: 'User 3' 13 | } 14 | ]; 15 | 16 | const getUserById = ({id}) => users.find((user) => user.id === id); 17 | 18 | /* eslint-disable no-unused-vars */ 19 | const responses = [ 20 | { 21 | url: 'https://api.example.com/json', 22 | method: 'GET', 23 | headers: { 24 | 'Content-Type': 'application/json' 25 | }, 26 | body: { 27 | message: 'a json response' 28 | } 29 | }, 30 | { 31 | url: 'https://api.example.com/users/:id', 32 | method: 'GET', 33 | headers: { 34 | 'Content-Type': 'application/json' 35 | }, 36 | body: params => getUserById(params), 37 | }, 38 | { 39 | url: 'https://api.example.com/text', 40 | method: 'GET', 41 | body: 'plain text body' 42 | }, 43 | { 44 | url: 'https://api.example.com/pdf', 45 | method: 'GET', 46 | headers: { 47 | 'Content-Type': 'application/pdf' 48 | }, 49 | file: 'demo/example.pdf' 50 | }, 51 | { 52 | url: 'https://api.example.com/post', 53 | method: 'POST', 54 | status: 201, 55 | statusText: 'Created', 56 | headers: { 57 | 'Content-Type': 'application/json' 58 | }, 59 | body: { 60 | message: 'created' 61 | } 62 | }, 63 | { 64 | url: 'https://api.example.com/notfound', 65 | method: 'GET', 66 | status: 404, 67 | statusText: 'Not found', 68 | headers: { 69 | 'Content-Type': 'application/json' 70 | }, 71 | body: { 72 | message: 'Not found' 73 | } 74 | }, 75 | { 76 | url: 'https://localhost/*', 77 | method: 'GET', 78 | status: 302, 79 | redirectUrl: 'http://localhost:8080/$1' 80 | } 81 | ]; 82 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | swopr 6 | 21 | 22 | 23 |

swopr demo

24 |

25 | Click the buttons below to generate the corresponding type of response from swopr. Check the console for 26 | output. 27 |

28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 60 | 61 | -------------------------------------------------------------------------------- /swopr.js: -------------------------------------------------------------------------------- 1 | self.importScripts('./swopr-responses.js'); 2 | 3 | const handleInstall = () => { 4 | console.log('[SWOPR] service worker installed'); 5 | 6 | self.skipWaiting(); 7 | }; 8 | 9 | const handleActivate = () => { 10 | console.log('[SWOPR] service worker activated'); 11 | 12 | return self.clients.claim(); 13 | }; 14 | 15 | const delayResponse = (time, response) => new Promise(resolve => setTimeout(() => resolve(response), time)); 16 | const compose = (...fns) => x => fns.reduce((res, f) => res || f(x), false); 17 | 18 | const getResponseFor = ({url: reqUrl, method: reqMethod}) => { 19 | const exactUrlMatch = ({url, method}) => url === reqUrl && method === reqMethod; 20 | const patternUrlMatch = ({url, method}) => { 21 | return url.includes('*') && new RegExp(url.replace('*', '(.+)')).test(reqUrl) && method === reqMethod; 22 | }; 23 | 24 | const paramsUrlMatch = ({url, method}) => { 25 | return url.includes(':') && new RegExp(`${url.replaceAll(/(:[a-z-A-Z0-9]+)/g, '([0-9]+)')}$`).test(reqUrl) && method === reqMethod; 26 | }; 27 | 28 | 29 | const exactOrPatternMatch = compose(exactUrlMatch, patternUrlMatch, paramsUrlMatch); 30 | 31 | /* eslint-disable no-undef */ 32 | return responses.find(exactOrPatternMatch); 33 | /* eslint-enable no-undef */ 34 | }; 35 | 36 | const getRequestParams = (matchedUrl, reqUrl) => { 37 | const params = [...matchedUrl.matchAll(/(:[a-z-A-Z0-9]+)/g)].map(([m]) => m); 38 | const values = [...reqUrl.matchAll(/(?<=\/)[0-9]+/g)].map(([m]) => m); 39 | 40 | return params.reduce((acc, key, index) => ({...acc, [key.substring(1)]: values[index]}), {}); 41 | }; 42 | 43 | const getResponseBody = (response, params) => typeof response.body === 'function' ? response.body(params) : response.body; 44 | 45 | const handleFetch = async (e) => { 46 | const {request} = e; 47 | const {method: reqMethod, url: reqUrl} = request; 48 | const response = getResponseFor(request); 49 | 50 | if(response) { 51 | const {headers, status, statusText, delay, resMethod, url: matchedUrl} = response; 52 | const params = matchedUrl.includes(':') ? getRequestParams(matchedUrl, reqUrl) : null; 53 | 54 | const redirectUrl = matchedUrl.includes('*') ? reqUrl.replace(new RegExp(matchedUrl.replace('*', '(.+)')), response.redirectUrl) : response.redirectUrl; 55 | const init = {headers, status, statusText, url: reqUrl, method: resMethod ? resMethod : reqMethod}; 56 | 57 | const proxyResponse = response.file ? fetch(`${self.origin}/${response.file}`) : 58 | redirectUrl ? fetch(redirectUrl, init) : 59 | Promise.resolve(new Response(JSON.stringify(getResponseBody(response, params)), init)); 60 | 61 | 62 | const msg = `[SWOPR] proxying request ${reqMethod}: ${reqUrl}`; 63 | console.log(`${msg} ${redirectUrl ? `-> ${redirectUrl}` : ``} ${response.file ? `-> serving: ${response.file}` : ``}`); 64 | 65 | e.waitUntil(e.respondWith(delay ? delayResponse(delay, proxyResponse) : proxyResponse)); 66 | } 67 | }; 68 | 69 | self.addEventListener('install', handleInstall); 70 | self.addEventListener('activate', handleActivate); 71 | self.addEventListener('fetch', handleFetch); 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swopr 2 | 3 | `swopr` is a really tiny proxy server which utilizes a service worker. 4 | A [service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) 5 | sits between web applications and the network (when available). 6 | 7 | Service workers are able to intercept requests and take an action, like serving a custom response. 8 | 9 | This enables you to use `swopr` as a proxy server inside your browser or as a mock server to test a REST API without 10 | having to run a local backend. 11 | 12 | 13 | ## Installation 14 | Run `npm install swopr --save-dev` 15 | 16 | ## Usage 17 | The Service Worker file needed by `swopr` needs to be in the root folder of your application, so you need to copy 18 | `node_modules/swopr/swopr.js` to the root folder. 19 | 20 | Then simply add `swopr` to your web app using a script tag: 21 | 22 | `