websocket-text-relay 1.0.0 → 1.1.1

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,9 +1,5 @@
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.
@@ -15,26 +11,27 @@ of your personalized plugins instead of having to use an in browser code editor.
15
11
  ### 1. Install the extension for your text editor
16
12
 
17
13
  WTR currently has plugins for Neovim and VSCode.
18
- * Installation instructions for Neovim
19
- * Installation instructions for VSCode
14
+ - [websocket-text-relay.nvim](https://github.com/niels4/websocket-text-relay.nvim)
15
+ - [websocket-text-relay-vscode](https://github.com/niels4/websocket-text-relay-vscode)
20
16
 
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)
17
+ ### 2. Verify the webserver is running with the status UI
23
18
 
24
- ### 2. Connect to the websocket server from the front end application
19
+ The websocket server hosts its own status UI on the same port as the websocket server. You can view
20
+ 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)
21
+
22
+ ### 3. Connect to the websocket server from the front end application
25
23
 
26
24
  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
25
+ 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
26
  the live text updates into Vite's hot module reload system.
29
27
 
30
28
  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.
29
+ 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
30
 
33
- And finally, the [status UI for this project](http://localhost:38378) was also created using live updates from websocket-text-relay.
31
+ And finally, the status UI for this project was also created using live updates from websocket-text-relay.
34
32
  In addition to giving you live feedback on the status and activity of the application, it is also meant to serve as a
35
33
  reference UI project that is more complicated than a single javascript file, but still doesn't use any external dependencies.
36
34
 
37
-
38
35
  ## Developing
39
36
 
40
37
  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.1",
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"
@@ -1,7 +1,7 @@
1
1
  const { exportDeps, drawSvgElement, drawCircle, drawLinearPath, coordsToPathData, drawText, drawToolTip } = window.__WTR__
2
2
 
3
3
  const drawValueWithTooltip = ({x, y, label, direction, parentNode}) => {
4
- const tooltipWrapperGroup = drawSvgElement("g", undefined, "tooltip_wrapper_group", parentNode)
4
+ const tooltipWrapperGroup = drawSvgElement({tag: "g", className: "tooltip_wrapper_group", parent: parentNode})
5
5
  drawToolTip({x: x, y: y - 0.0032, text: label, direction, parentNode: tooltipWrapperGroup})
6
6
  return drawText({x: x, y: y, dominantBaseline: "middle", text: "0", className: "timeseries_value", parentNode: tooltipWrapperGroup})
7
7
  }
@@ -67,9 +67,9 @@ class ActivityTimeseriesGraph {
67
67
  const minY = -height / 2
68
68
  const maxY = minY + height
69
69
 
70
- const clipPath = drawSvgElement("clipPath", {id: innerCircleClipPathId}, undefined, this.parentNode)
70
+ const clipPath = drawSvgElement({tag: "clipPath", attributes: {id: innerCircleClipPathId}, parent: this.parentNode})
71
71
  drawCircle({cx: 0, cy: 0, r: this.innerRingRadius - 0.005, parentNode: clipPath})
72
- drawSvgElement("rect", {"clip-path": `url(#${innerCircleClipPathId})`, x: minX, y: minY, height, width}, "timeseries_bg", this.parentNode)
72
+ drawSvgElement({tag: "rect", attributes: {"clip-path": `url(#${innerCircleClipPathId})`, x: minX, y: minY, height, width}, className: "timeseries_bg", parent: this.parentNode})
73
73
 
74
74
  const series = this.dataWindow
75
75
  const {startTime, endTime, maxValue} = getSeriesWindowInfo(series)
@@ -90,13 +90,12 @@ class ActivityTimeseriesGraph {
90
90
  const onTickUpdate = () => {
91
91
  scheduleNextTick()
92
92
 
93
- console.log("data tick", Date.now())
94
93
  const series = this.dataWindow
95
94
  const prevEndTime = series.at(-1).time
96
95
  const newTime = prevEndTime + dataWindowInterval
97
96
  series.shift()
98
97
  series.push({time: newTime, value: window.currentActivityCount})
99
- currentValueElement.innerHTML = window.currentActivityCount
98
+ currentValueElement.textContent = window.currentActivityCount
100
99
  window.currentActivityCount = 0
101
100
 
102
101
  const pathCoords = series.map(({time, value}) => {
@@ -110,7 +109,7 @@ class ActivityTimeseriesGraph {
110
109
  requestAnimationFrame(() => {
111
110
  requestAnimationFrame(() => {
112
111
  const {startTime, endTime, maxValue} = getSeriesWindowInfo(series)
113
- maxValueElement.innerHTML = maxValue
112
+ maxValueElement.textContent = maxValue
114
113
 
115
114
  valueScale = createLinearScale(0, maxValue, maxY, minY) // in svg, y increases as it goes down, so we need to flip max and min in the range
116
115
  timeScale = createLinearScale(startTime, endTime, minX, maxX)
@@ -138,12 +137,7 @@ class ActivityTimeseriesGraph {
138
137
 
139
138
  triggerActivity () {
140
139
  window.currentActivityCount++
141
- console.log("time series graph activity")
142
140
  }
143
141
  }
144
142
 
145
143
  exportDeps({ActivityTimeseriesGraph})
146
-
147
- /*
148
- *
149
- */
@@ -9,14 +9,7 @@ class HeaderSummary {
9
9
  draw () {
10
10
  this.parentNode.innerHTML = ""
11
11
  drawText({x: -.86, y: -.73, text: "editors", parentNode: this.parentNode})
12
- // this.editorCountNode = drawText({x: -.96, y: -.62, text: "0", className: "header_number", parentNode: this.parentNode})
13
12
  drawText({x: .86, y: -.73, text: "clients", className: "right_header", parentNode: this.parentNode})
14
- // this.clientCountNode = drawText({x: .96, y: -.62, text: "0", className: ["right_header", "header_number"], parentNode: this.parentNode})
15
- }
16
-
17
- update (/* data */) {
18
- // this.editorCountNode.innerHTML = data.editors.length
19
- // this.clientCountNode.innerHTML = data.clients.length
20
13
  }
21
14
  }
22
15
 
@@ -21,7 +21,7 @@ class ServerStatus {
21
21
  this.valueElement.classList.remove(valueTextClass)
22
22
  this.offlineElement.classList.add(offlineTextClass)
23
23
  } else {
24
- this.valueElement.innerHTML = pid
24
+ this.valueElement.textContent = pid
25
25
  this.valueElement.classList.add(valueTextClass)
26
26
  this.offlineElement.classList.remove(offlineTextClass)
27
27
  }
@@ -2,7 +2,7 @@ const { exportDeps, polarToCartesian, coordsToPathData, drawLinearPath, drawCirc
2
2
 
3
3
  // a value with a colored circle and tooltip
4
4
  const drawSummaryValue = ({x, y, label, circleClass, parentNode}) => {
5
- const tooltipWrapperGroup = drawSvgElement("g", undefined, "tooltip_wrapper_group", parentNode)
5
+ const tooltipWrapperGroup = drawSvgElement({tag: "g", className: "tooltip_wrapper_group", parent: parentNode})
6
6
  drawCircle({cx: x, cy: y - 0.0032, r: summaryCircleRadius, className: circleClass, parentNode: tooltipWrapperGroup})
7
7
  drawToolTip({x: x, y: y - 0.0032, text: label, parentNode: tooltipWrapperGroup})
8
8
  return drawText({x: x + summaryCircleRadius * 2, y: y, dominantBaseline: "middle", text: "0", className: "summary_text_value", parentNode: tooltipWrapperGroup})
@@ -10,13 +10,13 @@ const drawSummaryValue = ({x, y, label, circleClass, parentNode}) => {
10
10
 
11
11
  // similar to summary value, but with an update function that automatically handles moving the circle on the left as the text size changes
12
12
  const drawRightAlignedSummaryValue = ({x, y, label, circleClass, parentNode}) => {
13
- const tooltipWrapperGroup = drawSvgElement("g", undefined, "tooltip_wrapper_group", parentNode)
13
+ const tooltipWrapperGroup = drawSvgElement({tag: "g", className: "tooltip_wrapper_group", parent: parentNode})
14
14
  drawToolTip({x: x, y: y - 0.0032, text: label, parentNode: tooltipWrapperGroup})
15
15
  const textElement = drawText({x: x + summaryCircleRadius * 2, y: y, dominantBaseline: "middle", text: "0", textAnchor: "end", className: "summary_text_value", parentNode: tooltipWrapperGroup})
16
16
  const xDiff = textElement.getBBox().width
17
17
  const labelCircle = drawCircle({cx: x - xDiff, cy: y - 0.0032, r: summaryCircleRadius, className: circleClass, parentNode: tooltipWrapperGroup})
18
18
  const update = (value) => {
19
- textElement.innerHTML = value
19
+ textElement.textContent = value
20
20
  const xDiff = textElement.getBBox().width
21
21
  labelCircle.setAttribute("cx", x - xDiff)
22
22
  }
@@ -67,7 +67,7 @@ class ClientLabel {
67
67
  const watchedCountBbox = this.watchedCountElement.getBBox()
68
68
 
69
69
  const xDiff = summaryValueSpacing + watchedCountBbox.width
70
- this.activeCountTranslateWrapper = drawSvgElement("g", {transform: `translate(${xDiff}, 0)`}, undefined, this.parentNode)
70
+ this.activeCountTranslateWrapper = drawSvgElement({tag: "g", attributes: {transform: `translate(${xDiff}, 0)`}, parent: this.parentNode})
71
71
  this.activeCountElement = drawSummaryValue({x: summaryStartX, y: summaryMidY, label: "Active Files", circleClass: "summary_active_circle", parentNode: this.activeCountTranslateWrapper})
72
72
  }
73
73
 
@@ -81,13 +81,13 @@ class ClientLabel {
81
81
  const halfIndex = Math.floor(name.length / 2)
82
82
  const nameFirstHalf = name.substring(0, halfIndex)
83
83
  const nameSecondHalf = name.substring(halfIndex, name.length)
84
- this.topNameElement.innerHTML = nameFirstHalf
85
- this.nameTextElement.innerHTML = nameSecondHalf
84
+ this.topNameElement.textContent = nameFirstHalf
85
+ this.nameTextElement.textContent = nameSecondHalf
86
86
  this.nameTextElement.classList.add("small_text")
87
87
  } else {
88
88
  this.topNameElement.innerHTML = ""
89
89
  this.nameTextElement.classList.remove("small_text")
90
- this.nameTextElement.innerHTML = name
90
+ this.nameTextElement.textContent = name
91
91
  }
92
92
 
93
93
  const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
@@ -95,8 +95,8 @@ class ClientLabel {
95
95
  const newUnderlinePathData = coordsToPathData(underlineCoords)
96
96
  this.underlinePath.setAttribute("d", newUnderlinePathData)
97
97
 
98
- this.watchedCountElement.innerHTML = client.watchCount
99
- this.activeCountElement.innerHTML = client.activeWatchCount
98
+ this.watchedCountElement.textContent = client.watchCount
99
+ this.activeCountElement.textContent = client.activeWatchCount
100
100
 
101
101
  const xDiff = summaryValueSpacing + this.watchedCountElement.getBBox().width
102
102
  this.activeCountTranslateWrapper.setAttribute("transform", `translate(${xDiff}, 0)`)
@@ -129,7 +129,7 @@ class EditorLabel {
129
129
 
130
130
  this.activeCountSummaryValue = drawRightAlignedSummaryValue({x: summaryStartX, y: summaryMidY, label: "Active Files", circleClass: "summary_active_circle", parentNode: this.parentNode})
131
131
  const xDiff = this.activeCountSummaryValue.textElement.getBBox().width + summaryCircleRadius + editorSummaryPadding * 2
132
- this.openFilesTransformGroup = drawSvgElement('g', {transform: `translate(${-xDiff})`}, undefined, this.parentNode)
132
+ this.openFilesTransformGroup = drawSvgElement({tag: 'g', attributes: {transform: `translate(${-xDiff})`}, parent: this.parentNode})
133
133
  this.openCountSummaryValue = drawRightAlignedSummaryValue({x: summaryStartX, y: summaryMidY, label: "Open Files", circleClass: "summary_watched_circle", parentNode: this.openFilesTransformGroup})
134
134
  }
135
135
 
@@ -147,13 +147,13 @@ class EditorLabel {
147
147
  const halfIndex = Math.floor(name.length / 2)
148
148
  const nameFirstHalf = name.substring(0, halfIndex)
149
149
  const nameSecondHalf = name.substring(halfIndex, name.length)
150
- this.topNameElement.innerHTML = nameFirstHalf
151
- this.nameTextElement.innerHTML = nameSecondHalf
150
+ this.topNameElement.textContent = nameFirstHalf
151
+ this.nameTextElement.textContent = nameSecondHalf
152
152
  this.nameTextElement.classList.add("small_text")
153
153
  } else {
154
154
  this.topNameElement.innerHTML = ""
155
155
  this.nameTextElement.classList.remove("small_text")
156
- this.nameTextElement.innerHTML = name
156
+ this.nameTextElement.textContent = name
157
157
  }
158
158
 
159
159
  const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
@@ -46,7 +46,7 @@ class SessionWedges {
46
46
  const wedgeAngleDelta = (totalAngleDelta / numWedges) - wedgeSpacing
47
47
  const innerRadius = this.outerRingRadius - wedgeWidth / 2
48
48
  for (let i = 0; i < numWedges; i++) {
49
- const group = drawSvgElement("g", undefined, "single_wedge_group", this.parentNode)
49
+ const group = drawSvgElement({tag: "g", className: "single_wedge_group", parent: this.parentNode})
50
50
 
51
51
  let startAngle = totalStartAngle + this.direction * (i + 1) * wedgeSpacing + this.direction * i * wedgeAngleDelta
52
52
  if (this.direction === -1) {
@@ -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
package/src/ui/js/main.js CHANGED
@@ -9,7 +9,7 @@ gridGroup.innerHTML = ""
9
9
  // drawGrid(gridGroup)
10
10
 
11
11
  const headerSummaryNode = document.getElementById('header_summary_group')
12
- const headerSummary = new HeaderSummary({parentNode: headerSummaryNode})
12
+ new HeaderSummary({parentNode: headerSummaryNode})
13
13
 
14
14
  const statusRingNode = document.getElementById('status_ring_group')
15
15
  const statusRing = new StatusRing({innerRingRadius, outerRingRadius, outerArcSize, parentNode: statusRingNode})
@@ -70,7 +70,6 @@ const handleStatusData = (rawData) => {
70
70
  window.__WTR__.lastStatusData = rawData
71
71
  const data = statusDataTranform(rawData)
72
72
  console.log("status data updated", data)
73
- headerSummary.update(data)
74
73
  statusRing.update(data)
75
74
  clientWedges.update(data.clients)
76
75
  editorWedges.update(data.editors)
@@ -3,8 +3,8 @@ const { exportDeps } = window.__WTR__
3
3
  const TWO_PI = 2 * Math.PI
4
4
  const MAX_ANGLE_DELTA = .99999
5
5
 
6
- const drawSvgElement = (tagName, attributes = {}, className, parentNode) => {
7
- const element = document.createElementNS("http://www.w3.org/2000/svg", tagName)
6
+ const drawSvgElement = ({tag, attributes = {}, className, parent}) => {
7
+ const element = document.createElementNS("http://www.w3.org/2000/svg", tag)
8
8
 
9
9
  if (className && className.length > 0) {
10
10
  if (Array.isArray(className)) {
@@ -20,51 +20,32 @@ const drawSvgElement = (tagName, attributes = {}, className, parentNode) => {
20
20
  }
21
21
  })
22
22
 
23
- if (parentNode) {
24
- parentNode.append(element)
23
+ if (parent) {
24
+ parent.append(element)
25
25
  }
26
26
 
27
27
  return element
28
28
  }
29
29
 
30
- const drawText = ({x, y, text, dominantBaseline, textAnchor, className, parentNode}) => {
31
- const textElement = drawSvgElement("text", {x, y, "dominant-baseline": dominantBaseline, "text-anchor": textAnchor}, className, parentNode)
32
- textElement.innerHTML = text
30
+ const drawText = ({x, y, text, dominantBaseline, textAnchor, className, parentNode: parent}) => {
31
+ const textElement = drawSvgElement({tag: "text", attributes: {x, y, "dominant-baseline": dominantBaseline, "text-anchor": textAnchor}, className, parent})
32
+ textElement.textContent = text
33
33
  return textElement
34
34
  }
35
35
 
36
- const drawLine = ({x1, y1, x2, y2, className, parentNode}) => {
37
- return drawSvgElement("line", {x1, y1, x2, y2}, className, parentNode)
36
+ const drawLine = ({x1, y1, x2, y2, className, parentNode: parent}) => {
37
+ return drawSvgElement({tag: "line", attributes: {x1, y1, x2, y2}, className, parent})
38
38
  }
39
39
 
40
- const drawCircle = ({cx, cy, r, className, parentNode}) => {
41
- return drawSvgElement("circle", {cx, cy, r}, className, parentNode)
40
+ const drawCircle = ({cx, cy, r, className, parentNode: parent}) => {
41
+ return drawSvgElement({tag: "circle", attributes: {cx, cy, r}, className, parent})
42
42
  }
43
43
 
44
44
  const coordsToPathData = (coords) => "M " + coords.map(coord => coord.join(',')).join(" L ")
45
45
 
46
- const drawLinearPath = ({coords, className, parentNode}) => {
46
+ const drawLinearPath = ({coords, className, parentNode: parent}) => {
47
47
  const d = coordsToPathData(coords)
48
- return drawSvgElement("path", {d}, className, parentNode)
49
- }
50
-
51
- const drawPolarLine = ({startAngle, startRadius, endAngle, endRadius, className, parentNode}) => {
52
- const startAngleRadians = (startAngle % 1) * TWO_PI
53
- const endAngleRadians = (endAngle % 1) * TWO_PI
54
- const x1 = Math.cos(startAngleRadians) * startRadius
55
- const y1 = -Math.sin(startAngleRadians) * startRadius
56
- const x2 = Math.cos(endAngleRadians) * endRadius
57
- const y2 = -Math.sin(endAngleRadians) * endRadius
58
-
59
- return drawSvgElement("line", {x1, y1, x2, y2}, className, parentNode)
60
- }
61
-
62
- const drawPolarCircle = ({angle, radius, r, className, parentNode}) => {
63
- const angleRadians = (angle % 1) * TWO_PI
64
- const cx = Math.cos(angleRadians) * radius
65
- const cy = -Math.sin(angleRadians) * radius
66
-
67
- return drawSvgElement("circle", {cx, cy, r}, className, parentNode)
48
+ return drawSvgElement({tag: "path", attributes: {d}, className, parent})
68
49
  }
69
50
 
70
51
  const polarToCartesian = (angle, radius) => {
@@ -74,23 +55,29 @@ const polarToCartesian = (angle, radius) => {
74
55
  return [x, y]
75
56
  }
76
57
 
77
- const drawWedge = ({startAngle, angleDelta, innerRadius, radiusDelta, className, parentNode}) => {
58
+ const drawPolarLine = ({startAngle, startRadius, endAngle, endRadius, className, parentNode: parent}) => {
59
+ const [x1, y1] = polarToCartesian(startAngle, startRadius)
60
+ const [x2, y2] = polarToCartesian(endAngle, endRadius)
61
+
62
+ return drawSvgElement({tag: "line", attributes: {x1, y1, x2, y2}, className, parent})
63
+ }
64
+
65
+ const drawPolarCircle = ({angle, radius, r, className, parentNode: parent}) => {
66
+ const [cx, cy] = polarToCartesian(angle, radius)
67
+ return drawSvgElement({tag: "circle", attributes: {cx, cy, r}, className, parent})
68
+ }
69
+
70
+ const drawWedge = ({startAngle, angleDelta, innerRadius, radiusDelta, className, parentNode: parent}) => {
78
71
  if (angleDelta < 0) { angleDelta = 0 }
79
72
  if (angleDelta > MAX_ANGLE_DELTA) {angleDelta = MAX_ANGLE_DELTA }
80
-
81
- const startAngleRadians = (startAngle % 1) * TWO_PI
82
- const endAngleRadians = ((startAngle + angleDelta) % 1) * TWO_PI
73
+ const endAngle = startAngle + angleDelta
83
74
  const outerRadius = innerRadius + radiusDelta
84
75
  const largeArcFlag = (angleDelta % 1) > .5 ? "1" : "0"
85
76
 
86
- const startX1 = Math.cos(startAngleRadians) * innerRadius
87
- const startY1 = -Math.sin(startAngleRadians) * innerRadius
88
- const startX2 = Math.cos(startAngleRadians) * outerRadius
89
- const startY2 = -Math.sin(startAngleRadians) * outerRadius
90
- const endX1 = Math.cos(endAngleRadians) * innerRadius
91
- const endY1 = -Math.sin(endAngleRadians) * innerRadius
92
- const endX2 = Math.cos(endAngleRadians) * outerRadius
93
- const endY2 = -Math.sin(endAngleRadians) * outerRadius
77
+ const [startX1, startY1] = polarToCartesian(startAngle, innerRadius)
78
+ const [startX2, startY2] = polarToCartesian(startAngle, outerRadius)
79
+ const [endX1, endY1] = polarToCartesian(endAngle, innerRadius)
80
+ const [endX2, endY2] = polarToCartesian(endAngle, outerRadius)
94
81
 
95
82
  const d = `
96
83
  M ${startX1} ${startY1},
@@ -100,28 +87,28 @@ A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${startX2} ${startY2},
100
87
  Z
101
88
  `
102
89
 
103
- return drawSvgElement("path", {d}, className, parentNode)
90
+ return drawSvgElement({tag: "path", attributes: {d}, className, parent})
104
91
  }
105
92
 
106
93
  const triangleHeight = 0.06
107
94
  const verticalPadding = 0.01
108
95
  const horizontalPadding = 0.04
109
96
 
110
- const drawToolTip = ({x, y, text, direction = "below", parentNode}) => {
97
+ const drawToolTip = ({x, y, text, direction = "below", parentNode: parent}) => {
111
98
  const directionMultiplier = direction === "above" ? -1 : 1
112
- const tooltipDisplayGroup = drawSvgElement("g", undefined, "tooltip_display_group", parentNode)
99
+ const tooltipDisplayGroup = drawSvgElement("g", undefined, "tooltip_display_group", parent)
113
100
  const bgPlaceholder = drawSvgElement("g", undefined, undefined, tooltipDisplayGroup)
114
101
  const textY = y + (triangleHeight + verticalPadding) * directionMultiplier
115
102
  const textElement = drawText({x, y: textY, text, className: "tooltip_text", parentNode: tooltipDisplayGroup})
116
103
  const textBbox = textElement.getBBox()
117
- const rectAttributes = {
104
+ const attributes = {
118
105
  x: textBbox.x - horizontalPadding,
119
106
  y: textBbox.y - verticalPadding,
120
107
  width: textBbox.width + horizontalPadding * 2,
121
108
  height: textBbox.height + verticalPadding * 2,
122
109
  rx: 0.015
123
110
  }
124
- drawSvgElement("rect", rectAttributes, "tooltip_outline", bgPlaceholder)
111
+ drawSvgElement({tag: "rect", attributes, className: "tooltip_outline", parent: bgPlaceholder})
125
112
  }
126
113
 
127
114
  exportDeps({drawSvgElement, drawLine, drawCircle, drawLinearPath, drawPolarLine, drawPolarCircle, drawWedge,
@@ -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