├── .gitignore ├── .kbignore ├── .npmignore ├── README.md ├── config.js ├── examples ├── 4000.json ├── 4001.json ├── 4002.json ├── 400x.cmd └── example.js ├── externs └── http-proxy.js ├── index.js ├── package.json ├── project.clj ├── resources └── version.js ├── server.js ├── src └── com │ └── softekinc │ ├── dynamic_reverse_proxy.cljs │ └── proxy.cljs ├── test.js └── test └── com └── softekinc └── dynamic_reverse_proxy_test.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pom.xml 3 | pom.xml.asc 4 | *jar 5 | /classes/ 6 | /target/ 7 | /checkouts/ 8 | .lein-deps-sum 9 | .lein-repl-history 10 | .lein-plugins/ 11 | .lein-failures 12 | .nrepl-port 13 | target 14 | out 15 | npm-debug.log 16 | *.tgz 17 | SIGNED.md 18 | 19 | 20 | 21 | lib/debug/cljs 22 | lib/debug/clojure 23 | lib/debug/com 24 | lib/debug/goog 25 | lib/debug/constants_table.* 26 | lib/debug/d*.* 27 | lib/debug/[0-9A-F][0-9A-F]*js 28 | lib/release/cljs 29 | lib/release/clojure 30 | lib/release/com 31 | lib/release/goog 32 | lib/release/constants_table.* 33 | lib/release/d*.* 34 | lib/release/[0-9A-F][0-9A-F]*js 35 | -------------------------------------------------------------------------------- /.kbignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .npmignore 3 | node_modules 4 | target 5 | .lein-repl-history 6 | .nrepl-port 7 | out 8 | npm-debug.log 9 | .git 10 | cljs 11 | clojure 12 | com 13 | goog 14 | 0*js 15 | 1*js 16 | 2*js 17 | 3*js 18 | 4*js 19 | 5*js 20 | 6*js 21 | 6*js 22 | 7*js 23 | 8*js 24 | 9*js 25 | A*js 26 | B*js 27 | C*js 28 | D*js 29 | E*js 30 | F*js 31 | constants_table* 32 | *.tgz 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | target 3 | .lein-repl-history 4 | .nrepl-port 5 | out 6 | npm-debug.log 7 | lib/**/* 8 | !lib/debug/dynamic-proxy.js 9 | !lib/debug/dynamic-proxy.js.map 10 | !lib/release/dynamic-proxy.js 11 | !lib/release/dynamic-proxy.js.map 12 | *.tgz 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dynamic-reverse-proxy 2 | 3 | A reverse proxy built on [http-proxy](https://github.com/nodejitsu/node-http-proxy) that is configured by REST. 4 | 5 | [Dynamic-reverse-proxy](http://github.com/softek/dynamic-reverse-proxy) exposes several web apps on a single port so you can: 6 | * **Use the right language for the job.** Maybe you want to use the best parts of [Clojure](http://clojure.org/), [Node.js](https://nodejs.org/about/), [Erlang](http://www.erlang.org/), [Ruby](https://www.ruby-lang.org/). Put each project on its own port and use dynamic-reverse-proxy to expose a unified front to the world. 7 | * **Partition parts of the web app for stability**. Put experimental features in their own process and relay the traffic. 8 | * **Only bother with HTTPS in one place**. You can expose HTTPS to the world, but your "behind the proxy" apps don't need to worry about HTTPS. 9 | 10 | ##### Latest stable release: 0.6.0 11 | [0.6.0 Documentation](https://github.com/softek/dynamic-reverse-proxy/blob/v0.6.0/README.md) 12 | 13 | `npm install dynamic-reverse-proxy` 14 | 15 | ##### Latest unstable release: 0.7.0-alpha3 16 | `npm install dynamic-reverse-proxy@0.7.0-alpha3` 17 | 18 | ## Starting the server 19 | 20 | #### Stand-alone (available starting 0.7.0) 21 | ```dos 22 | npm install dynamic-reverse-proxy 23 | cd node_modules\dynamic-reverse-proxy 24 | SET port=3000 25 | npm start 26 | ``` 27 | 28 | #### With code 29 | ```javascript 30 | var http = require("http"), 31 | server = http.createServer(), 32 | dynamicProxy = require("dynamic-reverse-proxy")(); 33 | 34 | server.on("request", function (req, res) { 35 | if (req.url.match(/^\/register/i)) { 36 | dynamicProxy.registerRouteRequest(req, res); 37 | } 38 | else { 39 | dynamicProxy.proxyRequest(req, res); 40 | } 41 | }); 42 | 43 | server.listen(3000, function () { 44 | console.log("Reverse Proxy started, listening on port 3000"); 45 | }); 46 | ``` 47 | 48 | ## Configuring the proxy 49 | 50 | The reverse proxy is configured to route based on the first segment of the path. For example: 51 | - `/` would route to the host registered at `/` 52 | - `/application1` would route to the host registered at `/application1` 53 | - `/application2/test/index.html` would route to the host registered at `/application2` 54 | 55 | To register the a host with the proxy: 56 | 57 | ```HTTP 58 | POST /register HTTP/1.1 59 | Host: localhost:3000 60 | Content-Length: 28 61 | Content-Type: application/json 62 | 63 | {"prefix": "/", "port":1234} 64 | ``` 65 | 66 | Now, any request made to `http://localhost:3000/` will be sent to `http://localhost:1234/`. 67 | 68 | To register another host: 69 | 70 | ```HTTP 71 | POST /register HTTP/1.1 72 | Host: localhost:3000 73 | Content-Length: 32 74 | Content-Type: application/json 75 | 76 | {"prefix": "/test", "port":4321} 77 | ``` 78 | 79 | Now, any request made to `http://localhost:3000/test` will be sent to `http://localhost:4321/test`. 80 | 81 | ### Wait, what about security? 82 | 83 | Well, it's pretty lame (but functional) at the moment. Only requests originating from the same machine as the proxy are allowed to register. 84 | 85 | ## Events 86 | 87 | The dynamic proxy object that is returned is an EventEmitter with the following events: 88 | 89 | - `proxyError` is passed `(error, host, request, response)` and is emitted when: 90 | - A request is sent to a known host but the request could not be proxied (likely the host was unreachable). If no handler ends the response back to the original client, `500 Internal Server Error` will be returned. 91 | - No host could be found to handle the request. In this case, the `error` will be `NOT_FOUND`. If no handler ends the response back to the original client, `501 Not Implemented` will be returned. 92 | 93 | - `registerError` is passed `(error, request, response)` and is emitted when a request is sent to `/register` but it could not be handled correctly. Error will be one of the following: 94 | - `FORBIDDEN` (not allowed) 95 | - `METHOD_NOT_ALLOWED` (must be a POST) 96 | - `BAD_REQUEST` (not parsable as JSON) 97 | - `INCOMPLETE_REQUEST` (path and port were not supplied) 98 | 99 | - `routeRegistered` is passed `(host)` and is emitted when a request is sent to `/register` and it was successful. 100 | 101 | ## Methods 102 | 103 | - `dynamicProxy.addRoutes(routes)` adds an object of routes in the following format: 104 | 105 | ```JSON 106 | { 107 | "/": { 108 | "prefix": "", 109 | "port": 1234 110 | }, 111 | "/test": { 112 | "prefix": "test", 113 | "port": 4321 114 | } 115 | } 116 | ``` 117 | 118 | ## Troubleshooting 119 | This package comes with both an optimized/minified "release" version, and a more-readable "debug" version. To use the debug version, set `debug: true` in ./config.js. 120 | 121 | ## Development 122 | 123 | ### Roadmap 124 | * Allowing HOST-specific routes (`http://example.com/` gets a different route than `http://subdomain.example.com/` depending on the host header) 125 | * Requiring encryption for some routes (for example, force the `/login` route to use HTTPS) 126 | * Performance improvements for proxies with many routes. Before v0.7.0, the complexity was o(n) and O(n) because it uses the longest prefix that works. This may become more important when certain areas of sites force HTTPS - that may use more routes, depending on your URL scheme. 127 | 128 | ### Scripts 129 | * To set the version (in package.json, project.clj, resources/version.js): `lein set-version 0.x.x-alphaX`. You can also `:dry-run true` to [see what changes would be made](https://github.com/pallet/lein-set-version#dry-run-mode). 130 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dev: true, 3 | debug: false 4 | }; -------------------------------------------------------------------------------- /examples/4000.json: -------------------------------------------------------------------------------- 1 | {"prefix":"/4000", "reqHostNames":["localhost","drp-example.com"], "host":"localhost", "port":4000} -------------------------------------------------------------------------------- /examples/4001.json: -------------------------------------------------------------------------------- 1 | {"prefix":"/4001", "reqHostNames":["localhost","drp-example.com"], "host":"localhost", "port":4001} -------------------------------------------------------------------------------- /examples/4002.json: -------------------------------------------------------------------------------- 1 | {"prefix":"/4002", "reqHostNames":["localhost","drp-example.com"], "host":"localhost", "port":4002} -------------------------------------------------------------------------------- /examples/400x.cmd: -------------------------------------------------------------------------------- 1 | curl -H "Content-Type: application/json" -d @4000.json http://localhost/register 2 | curl -H "Content-Type: application/json" -d @4001.json http://localhost/register 3 | curl -H "Content-Type: application/json" -d @4002.json http://localhost/register 4 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | var http = require("http"), 2 | server = http.createServer(), 3 | dynamicProxy = require("../index")(); 4 | 5 | server.on("request", function (req, res) { 6 | if (req.url.match(/^\/register/i)) { 7 | dynamicProxy.registerRouteRequest(req, res); 8 | } 9 | else { 10 | dynamicProxy.proxyRequest(req, res); 11 | } 12 | }); 13 | 14 | server.listen(3000, function () { 15 | console.log("Reverse Proxy started, listening on port 3000"); 16 | }); 17 | 18 | // Create a server we can proxy to. 19 | var server = http.createServer(function (req, res) { 20 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 21 | res.write(JSON.stringify(req.headers, true, 2)); 22 | res.end(); 23 | }); 24 | 25 | server.listen(3005, function () { 26 | console.log("Target http server started, listening on port 3005"); 27 | }); 28 | 29 | // Now register the target server for /foo 30 | dynamicProxy.registerRoute({ 31 | host: 'localhost', 32 | prefix: '/foo', 33 | port: '3005' 34 | }); -------------------------------------------------------------------------------- /externs/http-proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | BEGIN_NODE_INCLUDE 3 | var httpProxy = require('http-proxy'); 4 | var events = require('events'); 5 | END_NODE_INCLUDE 6 | */ 7 | 8 | /** 9 | * @type {Object.} 10 | */ 11 | var httpProxy = {}; 12 | 13 | /** 14 | * @param {Object.} 15 | * @return {httpProxy.ProxyServer} 16 | */ 17 | httpProxy.createProxyServer = function createProxyServer(options) {}; 18 | 19 | /** 20 | * @param {Object.} 21 | * @return {httpProxy.ProxyServer} 22 | */ 23 | httpProxy.createServer = function createProxyServer(options) {}; 24 | 25 | /** 26 | * @param {Object.} 27 | * @return {httpProxy.ProxyServer} 28 | */ 29 | httpProxy.createProxy = function createProxyServer(options) {}; 30 | 31 | 32 | /** 33 | * @constructor 34 | * @extends events.EventEmitter 35 | */ 36 | var ProxyServer = {}; 37 | ProxyServer.emit = function (){}; 38 | 39 | ProxyServer.web = function (req, res, data){}; 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var config = require("./config") 2 | var isDev = config.dev ? true : false; 3 | var isDebug = config.debug ? true : false; 4 | module.exports = function () { 5 | if (isDev) { 6 | try { 7 | require("source-map-support").install(); 8 | } catch(err) { 9 | } 10 | var DynamicProxy = require("./out/dev/dynamic-proxy.js"); 11 | var pxy = new DynamicProxy(); 12 | pxy.debug = true; 13 | return pxy; 14 | } else if (isDebug) { 15 | var pxy = new (require("./lib/debug/dynamic-proxy.js"))(); 16 | pxy.debug = true; 17 | return pxy; 18 | } else { 19 | return new (require("./lib/release/dynamic-proxy.js"))(); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Andrew Dunkman ", 3 | "name": "dynamic-reverse-proxy", 4 | "contributors": [ 5 | { 6 | "name": "Andrew Dunkman", 7 | "email": "andrew@dunkman.org" 8 | }, 9 | { 10 | "name": "Joe Andaverde" 11 | }, 12 | { 13 | "name": "Jeremy Sellars", 14 | "email": "jeremy.sellars@softekinc.com" 15 | } 16 | ], 17 | "license": "MIT", 18 | "version": "0.7.0-alpha3", 19 | "scripts": { 20 | "test": "node test.js", 21 | "prepublish": "lein build-release & keybase dir sign" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/softek/dynamic-reverse-proxy" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/softek/dynamic-reverse-proxy/issues" 29 | }, 30 | "keywords": [ 31 | "reverse proxy" 32 | ], 33 | "dependencies": { 34 | "http-proxy": "^1.9.0" 35 | }, 36 | "devDependencies": { 37 | "closurecompiler-externs": "^1.0.4", 38 | "keybase": "^0.7.7", 39 | "source-map-support": "^0.2.10" 40 | }, 41 | "engines": { 42 | "node": "*" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject dynamic-reverse-proxy "0.7.0-alpha3" 2 | :description "Dynamic reverse proxy" 3 | :url "http://github.com/softek/dynamic-reverse-proxy" 4 | 5 | :dependencies [[org.clojure/clojure "1.6.0"] 6 | [org.clojure/clojurescript "0.0-3126"]] 7 | 8 | :profiles {:dev {:dependencies [[org.clojure/clojurescript "0.0-3126"] 9 | [lein-set-version "0.4.1"]]}} 10 | :plugins [[lein-cljsbuild "1.0.5"] 11 | [lein-simpleton "1.3.0"]] 12 | :set-version 13 | {:updates [{:path "resources/version.js"} 14 | {:path "README.md" 15 | :search-regex #"Latest unstable release[^`]+`[^`]+"} 16 | {:path "package.json" 17 | :search-regex #"\"version\"\s*:\s*\"(\\\"|[^\"])*\""}]} ;" 18 | 19 | :clean-targets ["out" "lib"] 20 | 21 | :cljsbuild { 22 | :builds [{:id "dev" 23 | :source-paths ["src" "test"] 24 | :notify-command ["npm.cmd" "test"] 25 | :compiler { 26 | :output-to "out/dev/dynamic-proxy.js" 27 | :output-dir "out/dev/" 28 | :optimizations :simple 29 | :source-map "out/dev/dynamic-proxy.js.map" 30 | :pretty-print true 31 | :preamble ["version.js"] 32 | :target :nodejs 33 | :main "com.softekinc.dynamic-reverse-proxy" 34 | :externs []}} 35 | {:id "debug" 36 | :source-paths ["src"] 37 | :compiler { 38 | :output-to "lib/debug/dynamic-proxy.js" 39 | :output-dir "lib/debug" 40 | :optimizations :simple 41 | :source-map "lib/debug/dynamic-proxy.js.map" 42 | :pretty-print true 43 | :preamble ["version.js"] 44 | :target :nodejs}} 45 | {:id "release" 46 | :source-paths ["src"] 47 | :compiler { 48 | :output-to "lib/release/dynamic-proxy.js" 49 | :output-dir "lib/release" 50 | :optimizations :advanced 51 | :source-map "lib/release/dynamic-proxy.js.map" 52 | :pretty-print false 53 | :preamble ["version.js"] 54 | :target :nodejs 55 | :externs ["node_modules/closurecompiler-externs/events.js" 56 | "node_modules/closurecompiler-externs/stream.js" 57 | "node_modules/closurecompiler-externs/net.js" 58 | "node_modules/closurecompiler-externs/http.js" 59 | "node_modules/closurecompiler-externs/https.js" 60 | "externs/http-proxy.js"]}}]} 61 | 62 | :aliases { 63 | "build-release" ["do" 64 | "clean" 65 | ["cljsbuild" "once" "debug"] 66 | ["cljsbuild" "once" "release"]] 67 | }) 68 | -------------------------------------------------------------------------------- /resources/version.js: -------------------------------------------------------------------------------- 1 | // Dynamic Reverse Proxy version 0.7.0-alpha3 2 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require("http"), 2 | server = http.createServer(), 3 | dynamicProxy = require("./index")(); 4 | 5 | server.on("request", function (req, res) { 6 | if (req.url.match(/^\/register/i)) { 7 | dynamicProxy.registerRouteRequest(req, res); 8 | } 9 | else { 10 | dynamicProxy.proxyRequest(req, res); 11 | } 12 | }); 13 | 14 | var port = process.env.port || 3000; 15 | server.listen(port, function () { 16 | console.log("Reverse Proxy started, listening on port " + port + 17 | (dynamicProxy.debug ? " \x1b[33m(DEBUG build)\x1b[0m" : "")); 18 | }); 19 | -------------------------------------------------------------------------------- /src/com/softekinc/dynamic_reverse_proxy.cljs: -------------------------------------------------------------------------------- 1 | (ns com.softekinc.dynamic-reverse-proxy 2 | (:require [cljs.nodejs :as nodejs] 3 | [com.softekinc.proxy :refer [create-proxy]] 4 | [clojure.string :refer [blank?]])) 5 | 6 | (nodejs/enable-util-print!) 7 | 8 | (def require (js* "require")) 9 | 10 | (def events (require "events")) 11 | 12 | (def allow-registration-from #{"::1" "127.0.0.1"}) 13 | 14 | #_(defschema Url 15 | {:encrypted s/Bool 16 | :host-name s/Str 17 | :path s/Str}) 18 | 19 | (defn longest-string-first [a b] 20 | (let [al (.-length a) 21 | bl (.-length b)] 22 | (if (== al bl) 23 | (.localeCompare a b) 24 | (- bl al)))) 25 | 26 | (def empty-route-map 27 | (apply sorted-map-by longest-string-first [])) 28 | 29 | (defn url-matches-route? 30 | "returns true when 31 | (.-host route) is nil or is the same as (:host-name url)) 32 | AND (:path url) starts with (.-prefix route) 33 | 34 | In both criteria, the string comparisons ignore case." 35 | [{:keys [path host] :as url} route] 36 | (let [pfx (aget route "prefix")] 37 | (.startsWith path pfx))) 38 | 39 | (defn route-for-url 40 | "Gets the first route that matches the url map." 41 | [routes url] 42 | (->> routes 43 | (filter (partial url-matches-route? url)) 44 | first)) 45 | 46 | (defn- set-prototype! 47 | ([f prototype] 48 | (aset f "prototype" prototype)) 49 | ([f prototype-key value] 50 | (-> (aget f "prototype") 51 | (aset prototype-key value)))) 52 | 53 | (defn normalize-prefix [p] 54 | (as-> p prefix 55 | (.replace prefix #"/+$" "") ; trim trailing / slashes 56 | (if (= "" prefix) p prefix) ; undo if string is empty 57 | (if (.test #"^/" prefix) prefix (str "/" prefix)) 58 | (.toLowerCase prefix))) 59 | 60 | (defn normalize-path [^String path] 61 | (.toLowerCase path)) 62 | 63 | (defn complete-route? [route] 64 | (and route 65 | (not (blank? (aget route "prefix"))) 66 | (integer? (aget route "port")) 67 | (not= 0 (aget route "port")))) 68 | 69 | (defn get-host-routes [dproxy] 70 | (let [routes-atom (-> dproxy (aget "routes-atom"))] 71 | @routes-atom)) 72 | 73 | (defn routes-for-host-name [dproxy host-name] 74 | (let [routes (get-host-routes dproxy)] 75 | (lazy-cat (-> routes (get host-name) vals) 76 | (-> routes :any vals)))) 77 | 78 | (defn expand-route [route] 79 | (let [prefix (aget route "prefix") 80 | r {:route route, :req-host-name :any, :prefix prefix} 81 | rhns (-> route (aget "reqHostNames") js->clj)] 82 | (if rhns 83 | (map #(assoc r :req-host-name %) rhns) 84 | [r]))) 85 | 86 | (defn add-route [host-name->routes r] 87 | (let [expanded-routes (expand-route r)] 88 | (reduce 89 | (fn [routes {:keys [req-host-name prefix route]}] 90 | (update routes req-host-name #(assoc (or % empty-route-map) prefix route))) 91 | (update host-name->routes :raw conj r) 92 | expanded-routes))) 93 | 94 | (defn add-routes! [dproxy rs] 95 | (let [routes-atom (-> dproxy (aget "routes-atom"))] 96 | (swap! routes-atom #(add-route % rs)))) 97 | 98 | (defn register-route! [dproxy route] 99 | (aset route "prefix" (-> (aget route "prefix") normalize-prefix)) 100 | (when-not (aget route "host") 101 | (aset route "host" "localhost")) 102 | (add-routes! dproxy route) 103 | (.emit dproxy "routeRegistered" route)) 104 | 105 | (defn end-response 106 | ([res status-code] 107 | (end-response res status-code nil)) 108 | ([res status-code data] 109 | (when (aget res "writable") 110 | (aset res "statusCode" status-code) 111 | (when data 112 | (.write res (.stringify js/JSON data))) 113 | (.end res)))) 114 | 115 | (defn register! 116 | ([dproxy req res] 117 | (letfn [ 118 | (reg-status! [{:keys [code error-msg data]}] 119 | (when error-msg 120 | (.emit dproxy "registerError" (js/Error. error-msg) req res)) 121 | (end-response res code data) 122 | (or error-msg data)) 123 | (success-result [route] #js { 124 | :message "Registered." 125 | :host (aget route "host") 126 | :port (aget route "port") 127 | :prefix (aget route "prefix")}) 128 | (parse-json-or-exception 129 | [json] 130 | (try 131 | {:parsed (.parse js/JSON json)} 132 | (catch :default ex 133 | {:exception ex})))] 134 | (let [remote-address (-> req .-connection .-remoteAddress) 135 | method (-> req .-method .toUpperCase)] 136 | (or (when-not (allow-registration-from remote-address) 137 | (reg-status! {:code 403, :error-msg "FORBIDDEN"})) 138 | (when (not= "POST" method) 139 | (reg-status! {:code 405, :error-msg "METHOD_NOT_ALLOWED"})) 140 | (let [{route :parsed, ex :exception} 141 | (parse-json-or-exception (.-body req))] 142 | (if ex 143 | (reg-status! {:code 400, :error-msg "BAD_REQUEST"}) 144 | (if-not (complete-route? route) 145 | (reg-status! {:code 400, :error-msg "INCOMPLETE_REQUEST"}) 146 | (do (register-route! dproxy route) 147 | (reg-status! {:code 200, 148 | :data (success-result route)})))))))))) 149 | 150 | (defn proxy-error [dproxy error req res {:keys [host code]}] 151 | (let [err (if (string? error) (js/Error. error) error)] 152 | (.emit dproxy "proxyError" err host req res) 153 | (end-response res code))) 154 | 155 | (defn on-proxy-error [dproxy error req res] 156 | (proxy-error dproxy error req res {:code 500, :host (.-host req)})) 157 | 158 | (defn proxy-request [dproxy req res] 159 | (let [uri (-> req .-url js/decodeURI normalize-path) 160 | host (-> req .-headers .-host) 161 | host-name (or (re-find #"^[^:]+" (or host "")) host) 162 | url {:path uri 163 | :host host 164 | :host-name host-name 165 | :encrypted (boolean (-> req .-connection .-encrypted))} 166 | routes (routes-for-host-name dproxy host-name) 167 | route (route-for-url routes url)] 168 | (if-not route 169 | (proxy-error dproxy "NOT_FOUND" req res {:code 501}) 170 | (do 171 | (aset req "host" (.create js/Object route)) 172 | (let [h (aget route "host") 173 | target (str "http://" h ":" (aget route "port"))] 174 | (.setHeader res "x-served-by", (str target (aget route "prefix"))) 175 | (-> dproxy 176 | (aget "proxy") 177 | (.web req, res, #js { :target target }))))))) 178 | 179 | (defn register-route-request [dproxy req res] 180 | (.setEncoding req "utf-8") 181 | (let [body (atom [])] 182 | (.on req "data" (fn [data] (swap! body conj data))) 183 | (.on req "end" 184 | (fn [] 185 | (aset req "body" (apply str @body)) 186 | (register! dproxy req res))))) 187 | 188 | ;; js interop 189 | 190 | (defn ^:export DynamicProxy [] 191 | (let [routes (atom {:any empty-route-map 192 | :raw []}) 193 | proxy (create-proxy #js {"xfwd" true})] 194 | (this-as dproxy 195 | (.on proxy "error" (partial on-proxy-error dproxy)) 196 | (doto dproxy 197 | (aset "routes-atom" routes) 198 | (aset "proxy" proxy)) 199 | dproxy))) 200 | 201 | (set-prototype! DynamicProxy 202 | (.create js/Object (-> events (aget "EventEmitter") (aget "prototype")))) 203 | 204 | (set-prototype! DynamicProxy "addRoutes" 205 | (fn addRoutes [routes] 206 | (this-as dproxy 207 | (add-routes! dproxy routes) 208 | dproxy))) 209 | 210 | (set-prototype! DynamicProxy "getRoutes" 211 | (fn getRoutes [] 212 | (this-as dproxy 213 | (-> dproxy 214 | get-host-routes 215 | :raw 216 | clj->js)))) 217 | 218 | (set-prototype! DynamicProxy "registerRouteRequest" 219 | (fn ^:export registerRouteRequest [req res] 220 | (this-as dproxy 221 | (register-route-request dproxy req res)))) 222 | 223 | (set-prototype! DynamicProxy "proxyRequest" 224 | (fn registerRouteRequest [req res] 225 | (this-as dproxy 226 | (proxy-request dproxy req res)))) 227 | 228 | (set-prototype! DynamicProxy "registerRoute" 229 | (fn registerRoute [route] 230 | (this-as dproxy 231 | (register-route! dproxy route)))) 232 | 233 | (defn -main [] 234 | (aset js/module "exports" DynamicProxy)) 235 | 236 | (set! *main-cli-fn* -main) 237 | -------------------------------------------------------------------------------- /src/com/softekinc/proxy.cljs: -------------------------------------------------------------------------------- 1 | (ns com.softekinc.proxy 2 | (:require [cljs.nodejs :as nodejs])) 3 | 4 | (nodejs/enable-util-print!) 5 | 6 | (def http-proxy (nodejs/require "http-proxy")) 7 | 8 | (defn create-proxy [opts] 9 | (let [options (clj->js (or opts {}))] 10 | (.createProxyServer http-proxy options))) 11 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var dynamicProxy = require("./index")(); 2 | -------------------------------------------------------------------------------- /test/com/softekinc/dynamic_reverse_proxy_test.cljs: -------------------------------------------------------------------------------- 1 | (ns com.softekinc.dynamic-reverse-proxy-test 2 | (:require [cljs.nodejs :as nodejs] 3 | [cljs.test :as t :refer-macros [deftest testing is]] 4 | [com.softekinc.dynamic-reverse-proxy 5 | :refer [url-matches-route? normalize-prefix normalize-path 6 | route-for-url expand-route longest-string-first]] 7 | [clojure.string :refer [blank?]])) 8 | 9 | (deftest test_normalize-prefix 10 | (is (= "/" (normalize-prefix ""))) 11 | (is (= "/" (normalize-prefix "/"))) 12 | (is (= "/a" (normalize-prefix "/a"))) 13 | (is (= "/a" (normalize-prefix "/a/"))) 14 | (is (= "/case" (normalize-prefix "/CASE/")))) 15 | 16 | (deftest test_normalize-path 17 | (is (= "" (normalize-path ""))) 18 | (is (= "/" (normalize-path "/"))) 19 | (is (= "/a" (normalize-path "/a"))) 20 | (is (= "/case" (normalize-path "/CASE")))) 21 | 22 | (deftest test_longest-string-first 23 | (is (== 0 (longest-string-first "" ""))) 24 | (is (== 0 (longest-string-first "1" "1"))) 25 | (is (== 0 (longest-string-first "22" "22"))) 26 | (is (< 0 (longest-string-first "1" "22"))) 27 | (is (> 0 (longest-string-first "22" "1"))) 28 | ; same-length strings are compared alphabetically 29 | (is (> 0 (longest-string-first "a" "b"))) 30 | (is (< 0 (longest-string-first "b" "a"))) 31 | ; put it all together 32 | (is (= ["4444" "22" "bb" "A" "a" "b" "c" ""] 33 | (sort-by identity longest-string-first 34 | ["" "a" "b" "c" "A" "22" "bb" "4444"])))) 35 | 36 | (defn route 37 | ([prefix] 38 | (clj->js 39 | {:prefix (normalize-prefix prefix)})) 40 | ([prefix & kvs] 41 | (clj->js 42 | (apply assoc {:prefix (normalize-prefix prefix)} kvs)))) 43 | 44 | (defn http-url [path & kvs] 45 | (apply 46 | assoc 47 | {:encrypted false 48 | :host "unit.test"} 49 | :path (normalize-path path) 50 | kvs)) 51 | 52 | (deftest test_url-matches-route? 53 | (testing "Route matches if prefix matches at start of uri" 54 | (is (not (url-matches-route? (http-url "/") (route "/a")))) 55 | (is (url-matches-route? (http-url "/") (route "/"))) 56 | (is (url-matches-route? (http-url "/a") (route "/"))) 57 | ;; normalization trims the trailing / from route, making the / optional 58 | (is (url-matches-route? (http-url "/some/page") (route "/some/page/"))) 59 | (is (url-matches-route? (http-url "/SOme/page") (route "/soME/page/"))) 60 | (is (url-matches-route? (http-url "/some/page?p=1") (route "/some/page/"))))) 61 | 62 | (deftest test_route-for-url 63 | (testing "route-for-url returns first matching route. (The sequence of routes matters!)" 64 | (is (= nil (route-for-url [] (http-url "/")))) 65 | (is (= nil (route-for-url [(route "/deeper")] (http-url "/")))) 66 | (is (let [root-route (route "/")] 67 | (= root-route 68 | (route-for-url [(route "/deeper/still") 69 | root-route 70 | (route "/deeper")] 71 | (http-url "/deeper"))))))) 72 | 73 | (deftest test_expand-routes 74 | (testing "When host-names aren't specified, yield 1 route that applies to any host" 75 | (let [js-route #js {:prefix "/", :host "localhost"}] 76 | (is (= [{:prefix "/", :req-host-name :any, :route js-route}] 77 | (expand-route js-route))))) 78 | (testing "When host-names are specified, yield a route for each" 79 | (let [js-route #js {:prefix "/", :host "localhost", :reqHostNames #js ["api.example.com" "v1.api.example.com"]}] 80 | (is (= [{:prefix "/", :req-host-name "api.example.com", :route js-route} 81 | {:prefix "/", :req-host-name "v1.api.example.com", :route js-route}] 82 | (expand-route js-route)))))) 83 | 84 | 85 | (t/run-tests) 86 | --------------------------------------------------------------------------------