websocket-text-relay 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,40 +1,41 @@
1
1
  # websocket-text-relay (WTR)
2
2
 
3
- ## Alpha
4
-
5
- Note: This tool is still in alpha state. Some features like the VsCode extension and vite plugin have not been published yet. Documentation is still a work in progress
6
-
7
3
  This application connects to your text editor using the Language Server Protocol (LSP) and then starts a websocket
8
4
  server that front end clients can connect to and subscribe to file change events. This allows users to see their
9
5
  front end changes evaluated immediately as they type, without having to first save the file to disk or reload the browser.
10
6
  It's a similar concept to sites like CodePen and JsFiddle, except you can develop locally using your own text editor with all
11
7
  of your personalized plugins instead of having to use an in browser code editor.
12
8
 
9
+ ## Alpha
10
+
11
+ Note: This tool is still in alpha state. Some features like the VsCode extension have not been published yet. Documentation is still a work in progress
12
+
13
13
  ## Usage
14
14
 
15
15
  ### 1. Install the extension for your text editor
16
16
 
17
17
  WTR currently has plugins for Neovim and VSCode.
18
- * Installation instructions for Neovim
19
- * Installation instructions for VSCode
18
+ - [websocket-text-relay.nvim](https://github.com/niels4/websocket-text-relay.nvim)
19
+ - VSCode extension coming soon!
20
20
 
21
- If you wish to create a plugin for a new editor, the process is fairly straight forward if the editor has
22
- LSP support. See the [developer guide to creating a WTR text editor plugin](./docs/creating-text-editor-plugin.md)
21
+ ### 2. Verify the webserver is running with the status UI
23
22
 
24
- ### 2. Connect to the websocket server from the front end application
23
+ The websocket server hosts its own status UI on the same port as the websocket server. You can view
24
+ the status UI and verify everything is running by first starting up your text editor and then opening your browser to [http://localhost:38378](http://localhost:38378)
25
+
26
+ ### 3. Connect to the websocket server from the front end application
25
27
 
26
28
  To use this on a professional level project that gives you the option to use modules, typescript, and react, I recommend using vite along with
27
- the plugin **vite-plugin-websocket-text-relay**. This plugin gives you all the power of vite when developing while also hooking
29
+ the plugin [vite-plugin-websocket-text-relay](https://github.com/niels4/vite-plugin-websocket-text-relay). This plugin gives you all the power of vite when developing while also hooking
28
30
  the live text updates into Vite's hot module reload system.
29
31
 
30
32
  If you want to use this as a learning tool to play around with UI concepts using simple projects involving 1 html, css, and javascript file,
31
- then check out this **Simple WTR reference project**. This is a great setup for following along with any short and focused web development tutorials.
33
+ then check out this [simple reference project](https://github.com/niels4/wtr-simple-example). This is a great setup for following along with any short and focused web development tutorials.
32
34
 
33
- And finally, the [status UI for this project](http://localhost:38378) was also created using live updates from websocket-text-relay.
35
+ And finally, the status UI for this project was also created using live updates from websocket-text-relay.
34
36
  In addition to giving you live feedback on the status and activity of the application, it is also meant to serve as a
35
37
  reference UI project that is more complicated than a single javascript file, but still doesn't use any external dependencies.
36
38
 
37
-
38
39
  ## Developing
39
40
 
40
41
  If you are a developer looking to run websocket-text-relay from source and make modifications, follow the [Developer Getting Started Guide](./docs/dev-getting-started.md)
package/changelog.md ADDED
@@ -0,0 +1,25 @@
1
+ ## 1.1.0 - 2024/04/07
2
+
3
+ Increased default security.
4
+
5
+ By default, the http and websocket server will only accept incoming connections from your local machine. If you
6
+ wish to allow network access you must set the `allow_network_access` option to true when configuring your editor plugin.
7
+
8
+ ### For Neovim clients
9
+
10
+ ```lua
11
+ { 'niels4/websocket-text-relay.nvim', opts = {
12
+ allow_network_access = true
13
+ }},
14
+ ```
15
+
16
+ By default, the http and websocket server will only accept connections where the hostname is `localhost`. If you wish
17
+ to allow other hosts to connect to the websocket server, you must explicitly allow them using the `allowed_hosts` option when configuring your editor plugin.
18
+
19
+ ### For Neovim clients
20
+
21
+ ```lua
22
+ { 'niels4/websocket-text-relay.nvim', opts = {
23
+ allowed_hosts = { "some-allowed-host.test", "some-other-host.test" },
24
+ }},
25
+ ```
@@ -75,3 +75,50 @@ to start the http server will be the new main server, the rest of the instances
75
75
  proceed to connect to the new server in client mode again.
76
76
 
77
77
  All init messages are resent when a websocket client reconnects.
78
+
79
+ ## Status UI
80
+
81
+ The HTTP server created in `src/websocket-interface/httpServer.js` is also used to host the static files that make up the status UI.
82
+
83
+ The root of the static site is the `src/ui` directory.
84
+
85
+ ### index.html
86
+
87
+ The UI is entirely made up of SVG elements, so the only thing the index.html file has to do is set up the root
88
+ SVG element with some groups to act as containers for the different components to use.
89
+
90
+ ### js/index.js
91
+
92
+ - Sets up the websocket-text-relay client with handlers for css and javascript files.
93
+ - initializes the simple dependency management system that allows the UI to be split into several javascript files.
94
+ - Hooks up events to handle resizing the SVG element on window resize.
95
+ - emit data and activity events that the UI components can hook into
96
+
97
+ Whenever a javascript file is edited, its exports are updated and the main.js file is rerun.
98
+
99
+ ### js/util/DependencyManager.js
100
+
101
+ This is a very quick and simple dependency management system. The dependency container is just an object in the global scope.
102
+ An exportDeps function is created to make it easy to specify which objects in scope are to be exported.
103
+
104
+ An onEvent function is also created and exported here, it prevents event leaks by cleaning up any registered event handlers whenever the javascript is reevaluated.
105
+
106
+ ### js/util/drawing.js
107
+
108
+ This is the base utility class for drawing different shapes in SVG. It contains functions to help with drawing
109
+ wedges and other shapes using polar coordinates.
110
+
111
+ When dealing with polar coordinates, instead of angles being from 0 to 2 * PI,
112
+ the angle is scaled from 0 to 1, angles 0 and 1 pointing down the positive direciton of the x axis. 0.25 points straight up, 0.5 straight left and 0.75 straight down.
113
+
114
+ The center of the UI is at (0, 0) with a minimum height and width of 2. Having a good understanding of the unit circle makes dealing with polar coordinates fairly simple.
115
+
116
+ ### js/components/
117
+
118
+ The components directory contains the javascript classes that render the different elements on the screen.
119
+
120
+ Each class handles the state for its component and has a draw function that renders it to the screen.
121
+
122
+ Any components affected by data updates will also have an update function. When called the component will update the elements it manages with the new data.
123
+
124
+ Some components also respond to text update activity, these components will have a triggerActivity function as well.
@@ -1,8 +1,64 @@
1
1
  # Developers Guide: Getting Started
2
2
 
3
- Placeholder for instructions on how to:
4
- * check out repo
5
- * run tests
6
- * point your text editor plugin to use your checked out code
7
- * run the chrome debugger and set breakpoints
8
- * understanding the code structure
3
+ In this tutorial you will learn how to:
4
+ - Check out the repo and run unit tests
5
+ - Configure your editor to run the language server from local source
6
+ - Start in debugging mode and use the chrome inspector to set breakpoints and step through code
7
+ - Live edit the status UI
8
+
9
+ ## Check out the repo
10
+
11
+ In this example we will be checking out the repo into the `~/dev/src` directory.
12
+
13
+ ```
14
+ mkdir -p ~/dev/src
15
+ cd ~/dev/src
16
+ git clone https://github.com/niels4/websocket-text-relay.git
17
+ cd websocket-text-relay
18
+ npm install
19
+ npm test
20
+ ```
21
+
22
+ ## Configure your editor
23
+
24
+ ### Neovim
25
+
26
+ You can override the command to start up the language server using the setup options.
27
+
28
+ ```lua
29
+
30
+ local home_dir = vim.fn.resolve(os.getenv("HOME"))
31
+
32
+ require('lazy').setup {
33
+
34
+ { 'niels4/websocket-text-relay.nvim', opts = {
35
+ cmd = { "node", "--inspect", home_dir .. "/dev/src/websocket-text-relay/start.js" }
36
+ }}
37
+
38
+ }
39
+
40
+ ```
41
+
42
+ If you wish to pause the language server on startup so you can set break points on the init functions,
43
+ you should use the `--inspect-brk` option instead. You will need to open the chrome debugger and choose
44
+ to continue the execution before the language server will start up if you choose this option.
45
+
46
+ ```lua
47
+ cmd = { "node", "--inspect-brk", home_dir .. "/dev/src/websocket-text-relay/start.js" }
48
+ ```
49
+
50
+ ## Edit Status UI
51
+
52
+ With the language server running, you can connect to the status UI by opening your browser to [http://localhost:38378](http://localhost:38378)
53
+
54
+ You should be able to see at least 1 editor and 1 client (the status UI itself)
55
+
56
+ The UI was built using websocket-text-relay, so we can live edit the UI as its running.
57
+
58
+ Open up the file `src/ui/js/util/constants.js`. Make changes to the outer and inner ring radius variables. You should see the UI update as you make changes.
59
+
60
+ You can do the same with the main.css file. Try changing around some of the colors.
61
+
62
+ ## Understanding the code structure
63
+
64
+ To effectively make any changes to the application, you will need to understand the [overall code structure](./code-structure.md)
package/index.js CHANGED
@@ -34,6 +34,7 @@ const lspInitializeResponse = {
34
34
 
35
35
  export const startLanguageServer = (options = {}) => {
36
36
  const {inputStream: inputStreamParam, outputStream: outputStreamParam, port: portParam} = options
37
+
37
38
  const jsonRpc = new JsonRpcInterface(resolveIoStreams({inputStreamParam, outputStreamParam}))
38
39
 
39
40
  const wsInterface = new WebsocketInterface({port: resolvePort(portParam)})
@@ -52,6 +53,10 @@ export const startLanguageServer = (options = {}) => {
52
53
  lsPid: process.pid
53
54
  }
54
55
 
56
+ const initOptions = params.initializationOptions || {}
57
+ wsInterface.setAllowedHosts(initOptions.allowedHosts)
58
+ wsInterface.setAllowNetworkAccess(initOptions.allowNetworkAccess)
59
+ wsInterface.startInterface()
55
60
  wsInterface.sendInitMessage(wsInitMessage)
56
61
 
57
62
  return lspInitializeResponse
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "websocket-text-relay",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "An LSP server for sending live file updates from your text editor to the front end via websockets.",
5
5
  "bin": {
6
6
  "websocket-text-relay": "./start.js"
@@ -3,8 +3,6 @@ import "./util/DependencyManager.js"
3
3
 
4
4
  const { cleanupEventHandlers, statusDataEmitter } = window.__WTR__
5
5
 
6
- const PORT = 38378
7
-
8
6
  const FILE_PREFIX = "websocket-text-relay/src/ui/"
9
7
  const CSS_FILE = "css/main.css"
10
8
  const cssEndsWith = FILE_PREFIX + CSS_FILE
@@ -28,7 +26,9 @@ const jsFiles = [
28
26
  const svgRoot = document.getElementById('svg_root')
29
27
  const cssElement = document.getElementById('main_style')
30
28
 
31
- const ws = new WebsocketClient({port: PORT})
29
+ const {hostname, port, protocol} = window.location
30
+ const wsProtocol = protocol === "http:" ? "ws" : "wss"
31
+ const ws = new WebsocketClient({port: port, host: hostname, protocol: wsProtocol})
32
32
 
33
33
  const handleCss = (contents) => {
34
34
  cssElement.innerText = contents
@@ -94,7 +94,7 @@ const subscribeWatchers = () => {
94
94
  ws.sendMessage({method: "watch-log-messages"})
95
95
  ws.sendMessage({method: "watch-wtr-status"})
96
96
  ws.sendMessage({method: "watch-wtr-activity"})
97
- ws.sendMessage({ method: "init", name: "beta-status-ui" })
97
+ ws.sendMessage({ method: "init", name: "status-ui" })
98
98
  ws.sendMessage({ method: "watch-file", endsWith: cssEndsWith })
99
99
  jsFiles.forEach((jsFile) => {
100
100
  const jsEndsWith = FILE_PREFIX + jsFile
@@ -14,11 +14,10 @@ export class WebsocketInterface {
14
14
  this.openFileListMessage = null
15
15
  this.serverSession = null
16
16
  this.wsClient = null
17
+ this.allowNetworkAccess = false
17
18
 
18
19
  this._onSocketClose = this._onSocketClose.bind(this)
19
20
  this._sendQueuedMessages = this._sendQueuedMessages.bind(this)
20
-
21
- this._startInterface()
22
21
  }
23
22
 
24
23
  sendInitMessage (initMessage) {
@@ -36,9 +35,19 @@ export class WebsocketInterface {
36
35
  this._sendMessageToServer(sendTextMessage)
37
36
  }
38
37
 
39
- async _startInterface () {
38
+ setAllowedHosts (allowedHostsList) {
39
+ this.allowedHosts = new Set(allowedHostsList)
40
+ }
41
+
42
+ setAllowNetworkAccess (allowNetworkAccessParam) {
43
+ if (allowNetworkAccessParam != null) {
44
+ this.allowNetworkAccess = allowNetworkAccessParam
45
+ }
46
+ }
47
+
48
+ async startInterface () {
40
49
  try {
41
- await createWebsocketServer(this.port)
50
+ await createWebsocketServer({port: this.port, allowedHosts: this.allowedHosts, allowNetworkAccess: this.allowNetworkAccess })
42
51
  this.serverSession = new WtrSession({apiMethods, wsInterfaceEmitter: this.emitter})
43
52
  this._sendQueuedMessages()
44
53
  } catch (e) {
@@ -52,7 +61,7 @@ export class WebsocketInterface {
52
61
  this.wsClient.socket.removeEventListener("close", this._onSocketClose)
53
62
  this.wsClient.socket.removeEventListener("open", this._sendQueuedMessages)
54
63
  this.wsClient = null
55
- this._startInterface()
64
+ this.startInterface()
56
65
  }
57
66
 
58
67
  _sendQueuedMessages () {
@@ -2,6 +2,7 @@ import { createServer } from 'node:http'
2
2
  import path from 'node:path'
3
3
  import fs from 'node:fs'
4
4
  import * as url from 'node:url'
5
+ import { isValidOrigin } from './util.js'
5
6
  const parentDir = url.fileURLToPath(new URL('..', import.meta.url))
6
7
 
7
8
  const uiDirName = "ui"
@@ -27,7 +28,12 @@ const getFileType = (fileUrl) => {
27
28
  return fileUrl.substring(lastDotIndex + 1)
28
29
  }
29
30
 
30
- const requestHandler = (req, res) => {
31
+ const requestHandler = (allowedHosts) => (req, res) => {
32
+ if (!isValidOrigin(allowedHosts, req)) {
33
+ res.writeHead(403)
34
+ res.end("FORBIDDEN!")
35
+ return
36
+ }
31
37
  const filePath = getFilePath(req.url)
32
38
  const fileType = getFileType(filePath)
33
39
  const contentType = allowedFileTypes.get(fileType)
@@ -42,16 +48,18 @@ const requestHandler = (req, res) => {
42
48
  fileStream.pipe(res)
43
49
  }
44
50
 
45
- export const createHttpServer = (port) => {
51
+ export const createHttpServer = ({port, allowedHosts, allowNetworkAccess}) => {
46
52
  return new Promise((resolve, reject) => {
47
53
 
48
- const server = createServer(requestHandler)
54
+ const server = createServer(requestHandler(allowedHosts))
49
55
 
50
56
  server.on("error", (e) => {
51
57
  reject(e)
52
58
  })
53
59
 
54
- server.listen(port, () => {
60
+ const listenAddress = allowNetworkAccess ? "0.0.0.0" : "127.0.0.1"
61
+
62
+ server.listen(port, listenAddress, () => {
55
63
  resolve(server)
56
64
  })
57
65
  })
@@ -10,3 +10,19 @@ export const debounce = (timeout, func) => {
10
10
  // first ID returned is 0 and increases every time function is called
11
11
  let currentId = 0
12
12
  export const getNextId = () => currentId++
13
+
14
+ const defaultAllowedHost = "localhost"
15
+
16
+ export const isValidOrigin = (allowedHosts, req) => {
17
+ const {host, origin} = req.headers
18
+ let hostname
19
+
20
+ if (origin == null || origin.length === 0) {
21
+ hostname = host.split(":")[0]
22
+ } else {
23
+ const url = new URL(origin)
24
+ hostname = url.hostname
25
+ }
26
+
27
+ return hostname === defaultAllowedHost || allowedHosts.has(hostname)
28
+ }
@@ -2,15 +2,20 @@ import { WebSocketServer } from 'ws'
2
2
  import { createHttpServer } from "./httpServer.js"
3
3
  import { apiMethods } from "./websocketApi.js"
4
4
  import { WtrSession } from './WtrSession.js'
5
+ import { isValidOrigin } from './util.js'
5
6
 
6
- export const createWebsocketServer = async (port) => {
7
- const httpServer = await createHttpServer(port) // promise will reject if can't start HTTP server on specified port
7
+ export const createWebsocketServer = async ({port, allowedHosts, allowNetworkAccess}) => {
8
+ const httpServer = await createHttpServer({port, allowedHosts, allowNetworkAccess}) // promise will reject if can't start HTTP server on specified port
8
9
 
9
10
  const websocketServer = new WebSocketServer({ server: httpServer })
10
11
 
11
12
  websocketServer.on('error', () => { })
12
13
 
13
- websocketServer.on('connection', (wsConnection) => {
14
+ websocketServer.on('connection', (wsConnection, request) => {
15
+ if (!isValidOrigin(allowedHosts, request)) {
16
+ wsConnection.close()
17
+ return
18
+ }
14
19
  new WtrSession({apiMethods, wsConnection})
15
20
  })
16
21