├── .gitignore ├── examples └── server.js ├── package.json ├── LICENSE ├── src └── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | const webtcp = require("../src"); 2 | const express = require("express"); 3 | const enableWebsockets = require("express-ws"); 4 | 5 | const PORT = 9999; 6 | 7 | const app = express(); 8 | enableWebsockets(app); 9 | 10 | app.ws("/", webtcp({ debug: true })); 11 | app.listen(PORT, () => console.log(`webtcp Example running on ws port ${PORT}!`)) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webtcp", 3 | "description": "WebSocket/TCP bridge that allows browsers to interact with TCP servers.", 4 | "version": "1.0.0", 5 | "homepage": "https://github.com/PatrickSachs/webtcp", 6 | "author": "PatrickSachs (https://patrick-sachs.de)", 7 | "main": "./src/index.js", 8 | "contributors": [ 9 | "yankov (http://artemyankov.com)" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/PatrickSachs/webtcp.git" 14 | }, 15 | "scripts": { 16 | "example": "node ./examples/server" 17 | }, 18 | "license": "MIT", 19 | "devDependencies": { 20 | "express-ws": "^4.0.0", 21 | "express": "^4.16.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Patrick Sachs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 4 | associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 6 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 7 | so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 13 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 14 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 15 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { Socket } = require("net"); 2 | const { TLSSocket } = require("tls"); 3 | 4 | /** 5 | * The numeric ready states of the WebSocket. 6 | */ 7 | const READY_STATE = { 8 | CONNECTING: 0, 9 | OPEN: 1, 10 | CLOSING: 2, 11 | CLOSED_: 3 12 | } 13 | 14 | const defaultOptions = { 15 | // The options for this webtcp server instance 16 | debug: false, 17 | mayConnect: () => true, 18 | // Creates the connection/session object if you are using a non-default WebSocket implementation. 19 | createConnection: (ws, _req) => ({ 20 | // Sends a JSON object over the WebSocket. 21 | send: data => ws.send(JSON.stringify(data)), 22 | // Checks if the socket is open. If this returns true, the server assumes that calling send will work. 23 | isOpen: () => ws.readyState === READY_STATE.OPEN, 24 | // Placeholder for the TCP socket. Simply set this to null unless you need to get really fancy. 25 | socket: null 26 | }), 27 | // The default options for the TCP socket 28 | defaultTcpOptions: { 29 | host: "localhost", 30 | port: 9998, 31 | ssl: false, 32 | encoding: "utf8", 33 | timeout: 0, 34 | noDelay: false, 35 | keepAlive: false, 36 | initialDelay: 0 37 | } 38 | } 39 | 40 | class WebTCP { 41 | constructor(options = {}) { 42 | this.options = { 43 | ...defaultOptions, 44 | ...options, 45 | // todo: set which of these values the client can adjust 46 | defaultTcpOptions: { 47 | ...defaultOptions.defaultTcpOptions, 48 | ...options.defaultTcpOptions 49 | } 50 | }; 51 | } 52 | 53 | close(connection) { 54 | if (connection.socket) { 55 | this.options.debug && console.log("[webtcp] Closing connection"); 56 | connection.socket.end(); 57 | } 58 | } 59 | 60 | dispatch(connection, message) { 61 | try { 62 | const json = JSON.parse(message); 63 | this.options.debug && console.log("[webtcp] Got message", json, "has socket?", !!connection.socket); 64 | switch (json.type) { 65 | case "connect": { 66 | this.dispatchConnect(connection, json); 67 | break; 68 | } 69 | case "data": { 70 | this.dispatchData(connection, json); 71 | break; 72 | } 73 | case "close": { 74 | this.dispatchClose(connection, json); 75 | break; 76 | } 77 | } 78 | } 79 | catch (e) { 80 | console.log("error", e); 81 | connection.send({ 82 | type: "error", 83 | error: e.message 84 | }); 85 | } 86 | } 87 | 88 | dispatchClose(connection, json) { 89 | if (connection.socket) { 90 | this.close(connection); 91 | } else { 92 | connection.send({ 93 | type: "error", 94 | error: "not connected" 95 | }); 96 | } 97 | } 98 | 99 | dispatchData(connection, json) { 100 | if (connection.socket) { 101 | if (json.payload !== undefined) { 102 | const payload = typeof json.payload === "string" 103 | ? json.payload 104 | : Uint8Array.from(json.payload); 105 | connection.socket.write(payload, this.options.encoding); 106 | } else { 107 | connection.send({ 108 | type: "error", 109 | error: "no payload" 110 | }); 111 | } 112 | } else { 113 | connection.send({ 114 | type: "error", 115 | error: "not connected" 116 | }); 117 | } 118 | } 119 | 120 | dispatchConnect(connection, json) { 121 | if (!connection.socket) { 122 | const tcpOptions = { 123 | ...this.options.defaultTcpOptions, 124 | ...json 125 | } 126 | this.options.debug && console.log("[webtcp] Using connect options", tcpOptions); 127 | if (this.options.mayConnect({ host: tcpOptions.host, port: tcpOptions.port })) { 128 | const socket = connection.socket = tcpOptions.ssl 129 | ? new TLSSocket() 130 | : new Socket(); 131 | socket.connect({ port: tcpOptions.port, host: tcpOptions.host }, () => { 132 | socket.setEncoding(tcpOptions.encoding); 133 | socket.setTimeout(tcpOptions.timeout); 134 | socket.setNoDelay(tcpOptions.noDelay); 135 | socket.setKeepAlive(tcpOptions.keepAlive, tcpOptions.initialDelay); 136 | }); 137 | socket.on("ready", () => { 138 | this.options.debug && console.log("[webtcp] Socket ready"); 139 | connection.send({ type: "connect" }); 140 | }); 141 | socket.on("end", () => { 142 | this.options.debug && console.log("[webtcp] Socket end"); 143 | if (connection.isOpen()) { 144 | connection.send({ type: "end" }); 145 | } 146 | }); 147 | socket.on("close", hadError => { 148 | connection.socket = null; 149 | this.options.debug && console.log("[webtcp] Socket closed", "error?", hadError); 150 | if (connection.isOpen()) { 151 | connection.send({ 152 | type: "close", 153 | hadError 154 | }); 155 | } 156 | }); 157 | socket.on("timeout", () => { 158 | this.options.debug && console.log("[webtcp] Socket timeout"); 159 | socket.destroy(); 160 | connection.send({ type: "timeout" }); 161 | }); 162 | socket.on("error", (error) => { 163 | this.options.debug && console.log("[webtcp] Socket error", error); 164 | if (connection.isOpen()) { 165 | connection.send({ 166 | type: "error", 167 | error: error.errno 168 | }); 169 | } 170 | }); 171 | socket.on("data", payload => { 172 | this.options.debug && console.log("[webtcp] Socket data", payload); 173 | connection.send({ 174 | type: "data", 175 | // todo: forward binary data as array! 176 | payload: ("string" === typeof payload) ? payload : payload.toString(tcpOptions.encoding) 177 | }); 178 | }); 179 | } else { 180 | connection.send({ 181 | type: "error", 182 | error: "not allowed connection" 183 | }); 184 | } 185 | } else { 186 | connection.send({ 187 | type: "error", 188 | error: "already connected" 189 | }); 190 | } 191 | } 192 | 193 | handle(ws, req) { 194 | const connection = this.options.createConnection(ws, req); 195 | ws.on('message', message => this.dispatch(connection, message)); 196 | ws.on("close", () => this.close(connection)); 197 | } 198 | } 199 | 200 | const install = options => { 201 | const tcp = new WebTCP(options); 202 | return (ws, req) => tcp.handle(ws, req); 203 | } 204 | 205 | module.exports = install; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebTCP 1.0.0 # 2 | 3 | Inspired by the original WebTCP: [yankov/webtcp](https://github.com/yankov/webtcp) 4 | 5 | WebTCP allows users to create a raw TCP(With optional SSL support) socket using WebSockets. 6 | 7 | Why a new library? The old library is abandoned, has too much functionality for being a raw TCP socket, and the code was hard to understand for me. 8 | 9 | ## How does it work ## 10 | 11 | Client and server (bridge) communicate through a websocket connection. When the browser wants to create a TCP socket it sends a command to the bridge. The bridge creates a real TCP socket connection and maps all the events that happen on this socket to a client's socket object. For example, when data is received bridge will trigger a data event on according socket object on a browser side. 12 | 13 | ## Why would anyone need that ## 14 | 15 | Sometimes an API does not provide a way to communicate through HTTP or WebSockets, in which case you need to resort to raw TCP. 16 | 17 | ## Can I use this in production? ## 18 | 19 | **Not without adjusting the configuration object.** 20 | 21 | Why? This library allows users to leverage your server to create raw TCP sockets. They can literally do anything with that, all using your servers IP. 22 | 23 | You would have to limit your users ability to connect to certain servers(`options.mayConnect`), properly encrypt the traffic both ways by using the `ssl` option, etc. 24 | 25 | This library is not battle tested and is primarily used for prototyping by me, so be careful. 26 | 27 | ## Installing ## 28 | 29 | Assuming you have `node.js`, `npm` and `git` installed: 30 | 31 | ### Add as a dependency for using in your project ### 32 | 33 | ``` 34 | npm install webtcp 35 | ``` 36 | 37 | ### Clone the repository for testing/contributing ### 38 | 39 | **Clone the repo** 40 | 41 | ``` 42 | git clone https://github.com/PatrickSachs/webtcp 43 | ``` 44 | 45 | **Install dependencies** 46 | 47 | ``` 48 | cd webtcp 49 | npm install 50 | ``` 51 | 52 | **Run WebTCP example server** 53 | 54 | ``` 55 | npm run example 56 | ``` 57 | 58 | Your WebTCP server will now be hosted on localhost:9999. 59 | 60 | ## How to use it ## 61 | 62 | ### Client usage ### 63 | 64 | Connect to the bridge using a WebSocket. 65 | **Important:** Do not connect to the TCP server you want to communicate with here. You need to connect to the TCP bridge first. After connecting to the bridge you can then connect to the TCP server(as seen in the next step). 66 | 67 | ```js 68 | const socket = new WebSocket("localhost", 9999); 69 | ``` 70 | 71 | This WebSocket is now your TCP socket. 72 | 73 | Before we can actually send data we need to connect to a TCP server: 74 | 75 | ```js 76 | socket.send(JSON.stringify({ 77 | type: "connect", 78 | host: "localhost", 79 | port: 8001 80 | })); 81 | ``` 82 | 83 | Assuming everything went smooth the bridge will respond with 84 | 85 | ```json 86 | { 87 | "type": "connect" 88 | } 89 | ``` 90 | Now we are ready to send data: 91 | 92 | ```js 93 | // Binary payload 94 | socket.send(JSON.stringify({ 95 | type: "data", 96 | payload: [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100] 97 | })); 98 | // String payload 99 | socket.send(JSON.stringify({ 100 | type: "data", 101 | payload: "Hello World" 102 | })); 103 | ``` 104 | 105 | Once we are done, let's close the socket again: 106 | 107 | ```js 108 | socket.send(JSON.stringify({ 109 | type: "close" 110 | })); 111 | ``` 112 | 113 | This will also close the websocket. 114 | 115 | ## Events emitted by the bridge ## 116 | 117 | ### connect ### 118 | 119 | Once we have connected to the server the connect event occurs. 120 | 121 | ```json 122 | { 123 | "type": "connect" 124 | } 125 | ``` 126 | 127 | ### data ### 128 | 129 | Sent when the socket recceived data. 130 | 131 | ```json 132 | { 133 | "type": "data", 134 | "payload": "" 135 | } 136 | ``` 137 | ### end ### 138 | 139 | Sent when the other end closed the socket by sending a FIN packet. 140 | 141 | ```json 142 | { 143 | "type": "end" 144 | } 145 | ``` 146 | 147 | ### close ### 148 | 149 | Sent when the socket is closed. If hadError is true an error event will be emitted aswell. 150 | 151 | ```json 152 | { 153 | "type": "close", 154 | "hadError": "" 155 | } 156 | ``` 157 | 158 | ### error ### 159 | 160 | Sent when an error occurred. This typically closes the socket. 161 | 162 | ```json 163 | { 164 | "type": "error", 165 | "error": "" 166 | } 167 | ``` 168 | 169 | ### timeout ### 170 | 171 | Sent when the socket timed out due to inactivity. 172 | 173 | ```json 174 | { 175 | "type": "timeout" 176 | } 177 | ``` 178 | 179 | ## Events handled by the bridge ## 180 | 181 | ### connect ### 182 | 183 | Used to connect to a TCP server. 184 | 185 | ```json 186 | { 187 | "type": "connect", 188 | "host": "", 189 | "port": "", 190 | "encoding": "", 191 | "timeout": "", 192 | "noDelay": "", 193 | "keepAlive": "", 194 | "initialDelay": "", 195 | "ssl": "" 196 | } 197 | ``` 198 | 199 | ### close ### 200 | 201 | Closes the TCP Socket & WebSocket. 202 | 203 | ```json 204 | { 205 | "type": "close" 206 | } 207 | ``` 208 | 209 | ### data ### 210 | 211 | Sends data. The payload can either be a string on an array of bytes(=numbers). 212 | 213 | ```json 214 | { 215 | "type": "data", 216 | "payload": "" 217 | } 218 | ``` 219 | 220 | ## Manually creating a server ## 221 | 222 | This is pretty much a copy of the example included under `/examples/server.js`, but it's always nice to see a code example. 223 | 224 | As you can see WebTCP integrates seamlessly into express using the `express-ws` library. You can of course roll your own solution, which would require you to adjust the `createConnection` function passed in the options to use your WebSocket API. 225 | 226 | ```js 227 | const webtcp = require("webtcp"); 228 | const express = require("express"); 229 | const enableWebsockets = require("express-ws"); 230 | 231 | const PORT = 9999; 232 | 233 | const app = express(); 234 | enableWebsockets(app); 235 | 236 | // All options are optional. The following values are the default ones. 237 | app.ws("/", webtcp({ 238 | // The options for this webtcp server instance 239 | debug: false, 240 | mayConnect: ({host, port}) => true, 241 | // Creates the connection/session object if you are using a non-default WebSocket implementation. 242 | createConnection: (ws, req) => ({ 243 | // Sends a JSON object over the WebSocket. 244 | send: data => ws.send(JSON.stringify(data)), 245 | // Checks if the socket is open. If this returns true, the server assumes that calling send will work. 246 | isOpen: () => ws.readyState === READY_STATE.OPEN, 247 | // Placeholder for the TCP socket. Simply set this to null unless you need to get really fancy. 248 | socket: null 249 | }), 250 | // The default options for the TCP socket 251 | defaultTcpOptions: { 252 | host: "localhost", 253 | port: 9998, 254 | ssl: false, 255 | encoding: "utf8", 256 | timeout: 0, 257 | noDelay: false, 258 | keepAlive: false, 259 | initialDelay: 0 260 | } 261 | }); 262 | app.listen(PORT, () => console.log(`[webtcp] Running on port ${PORT}!`)); 263 | ``` 264 | 265 | ## Contributing ## 266 | 267 | Always welcome! Feel free to open issues and PRs as you wish, then we can talk about possible additions/fixes/changes. 268 | --------------------------------------------------------------------------------