From c5c521fabdf5739bea819d4d47781584f8e7ea6c Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 19:48:37 +0000 Subject: [PATCH 01/72] feat: add web RDP module --- .icons/desktop.svg | 5 +++ windows-rdp/README.md | 28 +++++++++++++ windows-rdp/main.tf | 98 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 .icons/desktop.svg create mode 100644 windows-rdp/README.md create mode 100644 windows-rdp/main.tf diff --git a/.icons/desktop.svg b/.icons/desktop.svg new file mode 100644 index 00000000..77d231ce --- /dev/null +++ b/.icons/desktop.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows-rdp/README.md b/windows-rdp/README.md new file mode 100644 index 00000000..ecede44d --- /dev/null +++ b/windows-rdp/README.md @@ -0,0 +1,28 @@ +--- +display_name: Windows RDP +description: RDP Server and Web Client powered by Devolutions +icon: ../.icons/desktop.svg +maintainer_github: coder +verified: false +tags: [windows, ide, web] +--- + +# Windows RDP + +Enable Remote Desktop + a web based client on Windows workspaces + + + +## Usage + +```tf +module "code-server" { + source = "registry.coder.com/modules/code-server/coder" + version = "1.0.10" + agent_id = coder_agent.example.id +} +``` + +## Tested on + +- ✅ GCP with Windows Server 2022: [Example template](#TODO) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf new file mode 100644 index 00000000..81ac95f4 --- /dev/null +++ b/windows-rdp/main.tf @@ -0,0 +1,98 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +resource "coder_script" "windows-rdp" { + agent_id = var.agent_id + display_name = "web-rdp" + icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons + script = < Date: Sat, 6 Apr 2024 19:51:51 +0000 Subject: [PATCH 02/72] fix port typo --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 81ac95f4..0e7d6791 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -91,7 +91,7 @@ resource "coder_app" "windows-rdp" { subdomain = true healthcheck { - url = "http://localhost:${var.port}" + url = "http://localhost:7171" interval = 5 threshold = 15 } From 12fd16f701197ad718274791a9d26bd8ad0dbbb1 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:01:57 +0000 Subject: [PATCH 03/72] add metadata and local instructions --- windows-rdp/main.tf | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 0e7d6791..1ca13722 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -14,6 +14,17 @@ variable "agent_id" { description = "The ID of a Coder agent." } +variable "resource_id" { + type = string + description = "The ID of the primary Coder resource (e.g. VM)." +} + +variable "admin_password" { + type = string + default = "coderRDP!" + sensitive = true +} + resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "web-rdp" @@ -73,7 +84,7 @@ resource "coder_script" "windows-rdp" { Start-Service 'DevolutionsGateway' } - Set-AdminPassword -adminPassword "coderRDP!" + Set-AdminPassword -adminPassword "${var.admin_password}" Configure-RDP Install-DevolutionsGateway @@ -96,3 +107,35 @@ resource "coder_app" "windows-rdp" { threshold = 15 } } + +resource "coder_app" "rdp-docs" { + agent_id = coder_agent.main.id + display_name = "Local RDP" + slug = "rdp-docs" + icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg" + url = "https://coder.com/docs/v2/latest/ides/remote-desktops#rdp-desktop" + external = true +} + +resource "coder_metadata" "rdp_details" { + count = data.coder_workspace.me.start_count + resource_id = var.resource_id + daily_cost = 0 + item { + key = "Host" + value = "localhost" + } + item { + key = "Port" + value = "3389" + } + item { + key = "Username" + value = "Administrator" + } + item { + key = "Password" + value = var.admin_password + sensitive = true + } +} From bf06e8d3ac0f4b988e37ec63af65f03e58db0488 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:04:28 +0000 Subject: [PATCH 04/72] fix agent id --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 1ca13722..4211059f 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -109,7 +109,7 @@ resource "coder_app" "windows-rdp" { } resource "coder_app" "rdp-docs" { - agent_id = coder_agent.main.id + agent_id = var.agent_id display_name = "Local RDP" slug = "rdp-docs" icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg" From 0e7644b284d6ef809add791714f1f3639516547e Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:05:57 +0000 Subject: [PATCH 05/72] remove count --- windows-rdp/main.tf | 1 - 1 file changed, 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 4211059f..4b420507 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -118,7 +118,6 @@ resource "coder_app" "rdp-docs" { } resource "coder_metadata" "rdp_details" { - count = data.coder_workspace.me.start_count resource_id = var.resource_id daily_cost = 0 item { From 9f8eee55b2c3b612737421d8c96048baa175c750 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:11:59 +0000 Subject: [PATCH 06/72] rename script --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 4b420507..fd086f03 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -27,7 +27,7 @@ variable "admin_password" { resource "coder_script" "windows-rdp" { agent_id = var.agent_id - display_name = "web-rdp" + display_name = "windows-rdp" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons script = < Date: Sat, 6 Apr 2024 20:13:50 +0000 Subject: [PATCH 07/72] remove metadata for now --- windows-rdp/main.tf | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index fd086f03..5507018e 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -117,24 +117,24 @@ resource "coder_app" "rdp-docs" { external = true } -resource "coder_metadata" "rdp_details" { - resource_id = var.resource_id - daily_cost = 0 - item { - key = "Host" - value = "localhost" - } - item { - key = "Port" - value = "3389" - } - item { - key = "Username" - value = "Administrator" - } - item { - key = "Password" - value = var.admin_password - sensitive = true - } -} +# resource "coder_metadata" "rdp_details" { +# resource_id = var.resource_id +# daily_cost = 0 +# item { +# key = "Host" +# value = "localhost" +# } +# item { +# key = "Port" +# value = "3389" +# } +# item { +# key = "Username" +# value = "Administrator" +# } +# item { +# key = "Password" +# value = var.admin_password +# sensitive = true +# } +# } From 748a180ac3ac7e84be052052a97c759c456a8a1d Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:18:58 +0000 Subject: [PATCH 08/72] add temp link to example template --- windows-rdp/README.md | 2 +- windows-rdp/main.tf | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index ecede44d..71a48735 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -25,4 +25,4 @@ module "code-server" { ## Tested on -- ✅ GCP with Windows Server 2022: [Example template](#TODO) +- ✅ GCP with Windows Server 2022: [Example template](https://gist.github.com/bpmct/18918b8cab9f20295e5c4039b92b5143) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 5507018e..1b557eba 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -117,6 +117,7 @@ resource "coder_app" "rdp-docs" { external = true } +# For some reason this is not rendering, commented out for now # resource "coder_metadata" "rdp_details" { # resource_id = var.resource_id # daily_cost = 0 From ac648cc0a90bcb7e4413b357f96bff3fb1c10fe0 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:32:52 +0000 Subject: [PATCH 09/72] add thumbnail --- windows-rdp/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 71a48735..d7b016c5 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -11,7 +11,7 @@ tags: [windows, ide, web] Enable Remote Desktop + a web based client on Windows workspaces - +[![Web RDP on Windows](https://cdn.loom.com/sessions/thumbnails/a5d98c7007a7417fb572aba1acf8d538-with-play.gif)](https://www.loom.com/share/a5d98c7007a7417fb572aba1acf8d538) ## Usage @@ -26,3 +26,9 @@ module "code-server" { ## Tested on - ✅ GCP with Windows Server 2022: [Example template](https://gist.github.com/bpmct/18918b8cab9f20295e5c4039b92b5143) + +## Roadmap + +- [ ] Test on additional cloud providers +- [ ] Automatically establish web RDP session + > This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp) \ No newline at end of file From 89135671b26f1f5e7cb28feead37ebcf60fd72a3 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:34:06 +0000 Subject: [PATCH 10/72] fix module usage --- windows-rdp/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index d7b016c5..15290845 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -16,10 +16,11 @@ Enable Remote Desktop + a web based client on Windows workspaces ## Usage ```tf -module "code-server" { - source = "registry.coder.com/modules/code-server/coder" - version = "1.0.10" - agent_id = coder_agent.example.id +module "windows_rdp" { + count = data.coder_workspace.me.start_count + source = "github.com/coder/modules//windows-rdp?ref=web-rdp" + agent_id = resource.coder_agent.main.id + resource_id = resource.google_compute_instance.dev[0].id } ``` From 7de78d2ef5392adc809666ca590748c905ed151e Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:36:55 +0000 Subject: [PATCH 11/72] add tags --- windows-rdp/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 15290845..32ec0917 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -4,12 +4,12 @@ description: RDP Server and Web Client powered by Devolutions icon: ../.icons/desktop.svg maintainer_github: coder verified: false -tags: [windows, ide, web] +tags: [windows, rdp, web, desktop] --- # Windows RDP -Enable Remote Desktop + a web based client on Windows workspaces +Enable Remote Desktop + a web based client on Windows workspaces, powered by [devolutions-gateway](https://github.com/Devolutions/devolutions-gateway) [![Web RDP on Windows](https://cdn.loom.com/sessions/thumbnails/a5d98c7007a7417fb572aba1acf8d538-with-play.gif)](https://www.loom.com/share/a5d98c7007a7417fb572aba1acf8d538) From 53083a5718c557e80021ac28e53d41b699ea7cd1 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:46:50 +0000 Subject: [PATCH 12/72] add more context on auto login --- windows-rdp/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 32ec0917..6320ca64 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -31,5 +31,5 @@ module "windows_rdp" { ## Roadmap - [ ] Test on additional cloud providers -- [ ] Automatically establish web RDP session - > This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp) \ No newline at end of file +- [ ] Automatically establish web RDP session when users click "web RDP" + > This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp), modifying `webapp/`, building, and specifying a new [static root path](https://github.com/Devolutions/devolutions-gateway/blob/a884cbb8ff313496fb3d4072e67ef75350c40c03/devolutions-gateway/tests/config.rs#L271). Ideally we can upstream this functionality. \ No newline at end of file From b93471a3814a7be91b43d86c22b9ad266fe5e0ab Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 24 Apr 2024 22:39:24 +0000 Subject: [PATCH 13/72] chore: add admin username --- windows-rdp/main.tf | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 1b557eba..9e535214 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -19,6 +19,11 @@ variable "resource_id" { description = "The ID of the primary Coder resource (e.g. VM)." } +variable "admin_username" { + type = string + default = "Administrator" +} + variable "admin_password" { type = string default = "coderRDP!" @@ -35,9 +40,9 @@ resource "coder_script" "windows-rdp" { [string]$adminPassword ) # Set admin password - Get-LocalUser -Name "Administrator" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) + Get-LocalUser -Name "${var.admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) # Enable admin user - Get-LocalUser -Name "Administrator" | Enable-LocalUser + Get-LocalUser -Name "${var.admin_username}" | Enable-LocalUser } function Configure-RDP { From 20795aa2b642ecf26db634e5c9cfd823e10e43aa Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 19:37:31 +0000 Subject: [PATCH 14/72] chore: add script file for overriding Devolutions --- windows-rdp/devolutions-patch.js | 361 +++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 windows-rdp/devolutions-patch.js diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js new file mode 100644 index 00000000..00260d21 --- /dev/null +++ b/windows-rdp/devolutions-patch.js @@ -0,0 +1,361 @@ +// @ts-check +/** + * @file Defines the custom logic for patching in UI changes/behavior into the + * base Devolutions Gateway Angular app. + * + * Defined as a JS file to remove the need to have a separate compilation step. + * It is highly recommended that you work on this file from within VS Code so + * that you can take advantage of the @ts-check directive and get some type- + * checking still. + * + * A lot of the HTML selectors in this file will look nonstandard. This is + * because they are actually custom Angular components. + * + * @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry + * @typedef {Readonly>} FormFieldEntries + */ + +/** + * The communication protocol to set Devolutions to. + */ +const PROTOCOL = "RDP"; + +/** + * The hostname to use with Devolutions. + */ +const HOSTNAME = "localhost"; + +/** + * How often to poll the screen for the main Devolutions form. + */ +const SCREEN_POLL_INTERVAL_MS = 500; + +/** + * The fields in the Devolutions sign-in form that should be populated with + * values from the Coder workspace. + * + * All properties should be defined as placeholder templates in the form + * {{ VALUE_NAME }}. The Coder module, when spun up, should then run some logic + * to replace the template slots with actual values. These values should never + * change from within JavaScript itself. + * + * @satisfies {FormFieldEntries} + */ +const formFieldEntries = { + /** @readonly */ + username: { + /** @readonly */ + querySelector: "web-client-username-control input", + + /** @readonly */ + value: "{{ CODER_USERNAME }}", + }, + + /** @readonly */ + password: { + /** @readonly */ + querySelector: "web-client-password-control input", + + /** @readonly */ + value: "{{ CODER_PASSWORD }}", + }, +}; + +/** + * Handles typing in the values for the input form, dispatching each character + * as an event. This function assumes that all characters in the input will be + * UTF-8. + * + * Note: this code will never break, but you might get warnings in the console + * from Angular about unexpected value changes. Angular patches over a lot of + * the built-in browser APIs to support its component change detection system. + * As part of that, it has validations for checking whether an input it + * previously had control over changed without it doing anything. + * + * But the only way to simulate a keyboard input is by setting the input's + * .value property, and then firing an input event. So basically, the inner + * value will change, which Angular won't be happy about, but then the input + * event will fire and sync everything back together. + * + * @param {HTMLInputElement} inputField + * @param {string} inputText + * @returns {Promise} + */ +function setInputValue(inputField, inputText) { + const continueEventName = "coder-patch--continue"; + + const promise = /** @type {Promise} */ ( + new Promise((resolve, reject) => { + if (inputText === "") { + resolve(); + return; + } + + // -1 indicates a "pre-write" for clearing out the input before trying to + // write new text to it + let i = -1; + + // requestAnimationFrame is not capable of giving back values of 0 for its + // task IDs. Good default value to ensure that we don't need if statements + // when trying to cancel anything + let currentAnimationId = 0; + + // Super easy to pool the same event objects, because the events don't + // have any custom, context-specific values on them, and they're + // restricted to this one callback. + const continueEvent = new CustomEvent(continueEventName); + const inputEvent = new Event("input", { + bubbles: true, + cancelable: true, + }); + + /** @returns {void} */ + const handleNextCharIndex = () => { + if (i === inputText.length) { + resolve(); + return; + } + + const currentChar = inputText[i]; + if (i !== -1 && currentChar === undefined) { + throw new Error("Went out of bounds"); + } + + try { + inputField.addEventListener( + continueEventName, + () => { + i++; + currentAnimationId = + window.requestAnimationFrame(handleNextCharIndex); + }, + { once: true }, + ); + + if (i === -1) { + inputField.value = ""; + } else { + inputField.value = `${inputField.value}${currentChar}`; + } + + inputField.dispatchEvent(inputEvent); + inputField.dispatchEvent(continueEvent); + } catch (err) { + cancelAnimationFrame(currentAnimationId); + reject(err); + } + }; + + currentAnimationId = window.requestAnimationFrame(handleNextCharIndex); + }) + ); + + return promise; +} + +/** + * Takes a Devolutions remote session form, auto-fills it with data, and then + * submits it. + * + * The logic here is more convoluted than it should be for two main reasons: + * 1. Devolutions' HTML markup has errors. There are labels, but they aren't + * bound to the inputs they're supposed to describe. This means no easy hooks + * for selecting the elements, unfortunately. + * 2. Trying to modify the .value properties on some of the inputs doesn't + * work. Probably some combo of Angular data-binding and some inputs having + * the readonly attribute. Have to simulate user input to get around this. + * + * @param {HTMLFormElement} myForm + * @returns {Promise} + */ +async function autoSubmitForm(myForm) { + const setProtocolValue = () => { + /** @type {HTMLDivElement | null} */ + const protocolDropdownTrigger = myForm.querySelector(`div[role="button"]`); + if (protocolDropdownTrigger === null) { + throw new Error("No clickable trigger for setting protocol value"); + } + + protocolDropdownTrigger.click(); + + // Can't use form as container for querying the list of dropdown options, + // because the elements don't actually exist inside the form. They're placed + // in the top level of the HTML doc, and repositioned to make it look like + // they're part of the form. Avoids CSS stacking context issues, maybe? + /** @type {HTMLLIElement | null} */ + const protocolOption = document.querySelector( + `p-dropdownitem[ng-reflect-label="${PROTOCOL}"] li`, + ); + + if (protocolOption === null) { + throw new Error( + "Unable to find protocol option on screen that matches desired protocol", + ); + } + + protocolOption.click(); + }; + + const setHostname = () => { + /** @type {HTMLInputElement | null} */ + const hostnameInput = myForm.querySelector("p-autocomplete#hostname input"); + + if (hostnameInput === null) { + throw new Error("Unable to find field for adding hostname"); + } + + return setInputValue(hostnameInput, HOSTNAME); + }; + + const setCoderFormFieldValues = async () => { + // The RDP form will not appear on screen unless the dropdown is set to use + // the RDP protocol + const rdpSubsection = myForm.querySelector("rdp-form"); + if (rdpSubsection === null) { + throw new Error( + "Unable to find RDP subsection. Is the value of the protocol set to RDP?", + ); + } + + for (const { value, querySelector } of Object.values(formFieldEntries)) { + /** @type {HTMLInputElement | null} */ + const input = document.querySelector(querySelector); + + if (input === null) { + throw new Error( + `Unable to element that matches query "${querySelector}"`, + ); + } + + await setInputValue(input, value); + } + }; + + const triggerSubmission = () => { + /** @type {HTMLButtonElement | null} */ + const submitButton = myForm.querySelector( + 'p-button[ng-reflect-type="submit"] button', + ); + + if (submitButton === null) { + throw new Error("Unable to find submission button"); + } + + if (submitButton.disabled) { + throw new Error( + "Unable to submit form because submit button is disabled. Are all fields filled out correctly?", + ); + } + + submitButton.click(); + }; + + setProtocolValue(); + await setHostname(); + await setCoderFormFieldValues(); + triggerSubmission(); +} + +/** + * Sets up logic for auto-populating the form data when the form appears on + * screen. + * + * @returns {void} + */ +function setupFormDetection() { + /** @type {HTMLFormElement | null} */ + let formValueFromLastMutation = null; + + /** @returns {void} */ + const onDynamicTabMutation = () => { + console.log("Ran on mutation!"); + + /** @type {HTMLFormElement | null} */ + const latestForm = document.querySelector("web-client-form > form"); + + if (latestForm === null) { + formValueFromLastMutation = null; + return; + } + + // Only try to auto-fill if we went from having no form on screen to + // having a form on screen. That way, we don't accidentally override the + // form if the user is trying to customize values, and this essentially + // makes the script values function as default values + if (formValueFromLastMutation === null) { + autoSubmitForm(latestForm); + } + + formValueFromLastMutation = latestForm; + }; + + /** @type {number | undefined} */ + let pollingId = undefined; + + /** @returns {void} */ + const checkScreenForDynamicTab = () => { + const dynamicTab = document.querySelector("web-client-dynamic-tab"); + + // Keep polling until the main content container is on screen + if (dynamicTab === null) { + return; + } + + window.clearInterval(pollingId); + + // Call the mutation callback manually, to ensure it runs at least once + onDynamicTabMutation(); + + // Having the mutation observer is kind of an extra safety net that isn't + // really expected to run that often. Most of the content in the dynamic + // tab is being rendered through Canvas, which won't trigger any mutations + // that the observer can detect + const dynamicTabObserver = new MutationObserver(onDynamicTabMutation); + dynamicTabObserver.observe(dynamicTab, { + subtree: true, + childList: true, + }); + }; + + pollingId = window.setInterval( + checkScreenForDynamicTab, + SCREEN_POLL_INTERVAL_MS, + ); +} + +/** + * Sets up custom styles for hiding default Devolutions elements that Coder + * users shouldn't need to care about. + * + * @returns {void} + */ +function setupObscuringStyles() { + const styleId = "coder-patch--styles"; + + const existingContainer = document.querySelector(`#${styleId}`); + if (existingContainer) { + return; + } + + const styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` + /* app-menu corresponds to the sidebar of the default view. */ + app-menu { + display: none !important; + } + `; + + document.head.appendChild(styleContainer); +} + +// Always safe to call setupObscuringStyles immediately because even if the +// Angular app isn't loaded by the time the function gets called, the CSS will +// always be globally available for when Angular is finally ready +setupObscuringStyles(); + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", setupFormDetection); +} else { + setupFormDetection(); +} From ff96b3f65397f327a83321b3fe1c5b79a886a347 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:07:39 +0000 Subject: [PATCH 15/72] wip: commit current progress for devolutions patch --- package-lock.json | 263 +++++++++++++++++++++++++++++++ windows-rdp/devolutions-patch.js | 8 +- windows-rdp/main.tf | 16 ++ 3 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..4828ced1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,263 @@ +{ + "name": "modules", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "modules", + "devDependencies": { + "bun-types": "^1.0.18", + "gray-matter": "^4.0.3", + "marked": "^12.0.0", + "prettier-plugin-sh": "^0.13.1", + "prettier-plugin-terraform-formatter": "^1.2.1" + }, + "peerDependencies": { + "typescript": "^5.3.3" + } + }, + "node_modules/@types/node": { + "version": "20.12.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz", + "integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/bun-types": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.16.tgz", + "integrity": "sha512-LpAh8dQe4NKvhSW390Rkftw0ume0moSkRm575e1JZ1PwI/dXjbXyjpntq+2F0bVW1FV7V6B8EfWx088b+dNurw==", + "dev": true, + "dependencies": { + "@types/node": "~20.12.8", + "@types/ws": "~8.5.10" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mvdan-sh": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz", + "integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==", + "dev": true + }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-sh": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.13.1.tgz", + "integrity": "sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==", + "dev": true, + "dependencies": { + "mvdan-sh": "^0.10.1", + "sh-syntax": "^0.4.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "node_modules/prettier-plugin-terraform-formatter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-terraform-formatter/-/prettier-plugin-terraform-formatter-1.2.1.tgz", + "integrity": "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==", + "dev": true, + "peerDependencies": { + "prettier": ">= 1.16.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sh-syntax": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz", + "integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", + "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 00260d21..cf64b425 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -35,8 +35,8 @@ const SCREEN_POLL_INTERVAL_MS = 500; * values from the Coder workspace. * * All properties should be defined as placeholder templates in the form - * {{ VALUE_NAME }}. The Coder module, when spun up, should then run some logic - * to replace the template slots with actual values. These values should never + * VALUE_NAME. The Coder module, when spun up, should then run some logic to + * replace the template slots with actual values. These values should never * change from within JavaScript itself. * * @satisfies {FormFieldEntries} @@ -48,7 +48,7 @@ const formFieldEntries = { querySelector: "web-client-username-control input", /** @readonly */ - value: "{{ CODER_USERNAME }}", + value: "CODER_USERNAME", }, /** @readonly */ @@ -57,7 +57,7 @@ const formFieldEntries = { querySelector: "web-client-password-control input", /** @readonly */ - value: "{{ CODER_PASSWORD }}", + value: "CODER_PASSWORD", }, }; diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 9e535214..de3d408f 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -89,9 +89,25 @@ resource "coder_script" "windows-rdp" { Start-Service 'DevolutionsGateway' } + function Patch-Devolutions-HTML { + $root = "C:\Program Files\Devolutions\Gateway\webapp\client" + $devolutionsHtml = "$root\index.html" + $patch = '' + $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" + if ($isPatched -e $null) { + "templatefile("${path.module}/devolutions-patch.js", { + CODER_USERNAME : var.admin_username, + CODER_PASSWORD : var.admin_password, + }" | Set-Content "$root\coder.js" + + (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml + } + } + Set-AdminPassword -adminPassword "${var.admin_password}" Configure-RDP Install-DevolutionsGateway + Patch-Devolutions-HTML EOF From aab5e55663f951c1f0c1f27119bd6c4db582b80d Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:10:22 +0000 Subject: [PATCH 16/72] fix: update script frequency --- windows-rdp/main.tf | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index de3d408f..99de1938 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -94,12 +94,13 @@ resource "coder_script" "windows-rdp" { $devolutionsHtml = "$root\index.html" $patch = '' $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" + # Always copy the file in case we change it. + "templatefile("${path.module}/devolutions-patch.js", { + CODER_USERNAME : var.admin_username, + CODER_PASSWORD : var.admin_password, + }" | Set-Content "$root\coder.js" + # Only inject the src if we have not before. if ($isPatched -e $null) { - "templatefile("${path.module}/devolutions-patch.js", { - CODER_USERNAME : var.admin_username, - CODER_PASSWORD : var.admin_password, - }" | Set-Content "$root\coder.js" - (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml } } From 29209d546edc24346dba1200cd675b7c874cb96b Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:13:11 +0000 Subject: [PATCH 17/72] fix: update typo in powershell script Co-authored-by: Asher --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 99de1938..30f1a1e2 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -100,7 +100,7 @@ resource "coder_script" "windows-rdp" { CODER_PASSWORD : var.admin_password, }" | Set-Content "$root\coder.js" # Only inject the src if we have not before. - if ($isPatched -e $null) { + if ($isPatched -eq $null) { (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml } } From 452f41aa86d498eae90fb72b6bd71cc586d203f6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:17:31 +0000 Subject: [PATCH 18/72] fix: add parenthesis --- windows-rdp/main.tf | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 30f1a1e2..a098669b 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -93,13 +93,15 @@ resource "coder_script" "windows-rdp" { $root = "C:\Program Files\Devolutions\Gateway\webapp\client" $devolutionsHtml = "$root\index.html" $patch = '' - $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" + # Always copy the file in case we change it. - "templatefile("${path.module}/devolutions-patch.js", { + "${templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - }" | Set-Content "$root\coder.js" + })}" | Set-Content "$root\coder.js" + # Only inject the src if we have not before. + $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" if ($isPatched -eq $null) { (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml } From c7aa8253e3a366401a60eaadf51fdd0efc3c4fec Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:22:25 +0000 Subject: [PATCH 19/72] fix: dolla dolla --- windows-rdp/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index a098669b..594d79f8 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -95,10 +95,10 @@ resource "coder_script" "windows-rdp" { $patch = '' # Always copy the file in case we change it. - "${templatefile("${path.module}/devolutions-patch.js", { + "${replace(templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - })}" | Set-Content "$root\coder.js" + }), "$", "\\$")}" | Set-Content "$root\coder.js" # Only inject the src if we have not before. $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" From 047ccd67ca9f57c03b1d0cf98ea41f4ff6645ea6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:24:49 +0000 Subject: [PATCH 20/72] fix: dolla dolla --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 594d79f8..a4897680 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -98,7 +98,7 @@ resource "coder_script" "windows-rdp" { "${replace(templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - }), "$", "\\$")}" | Set-Content "$root\coder.js" + }), "$", "$$")}" | Set-Content "$root\coder.js" # Only inject the src if we have not before. $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" From d530d68b12a5ca33779e8cbf666ac972b25fbce6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:28:44 +0000 Subject: [PATCH 21/72] fix: more money, more problems --- windows-rdp/devolutions-patch.js | 12 ++++++------ windows-rdp/main.tf | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index cf64b425..ecb16c19 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -48,7 +48,7 @@ const formFieldEntries = { querySelector: "web-client-username-control input", /** @readonly */ - value: "CODER_USERNAME", + value: "${CODER_USERNAME}", }, /** @readonly */ @@ -57,7 +57,7 @@ const formFieldEntries = { querySelector: "web-client-password-control input", /** @readonly */ - value: "CODER_PASSWORD", + value: "${CODER_PASSWORD}", }, }; @@ -135,7 +135,7 @@ function setInputValue(inputField, inputText) { if (i === -1) { inputField.value = ""; } else { - inputField.value = `${inputField.value}${currentChar}`; + inputField.value = `$${inputField.value}$${currentChar}`; } inputField.dispatchEvent(inputEvent); @@ -184,7 +184,7 @@ async function autoSubmitForm(myForm) { // they're part of the form. Avoids CSS stacking context issues, maybe? /** @type {HTMLLIElement | null} */ const protocolOption = document.querySelector( - `p-dropdownitem[ng-reflect-label="${PROTOCOL}"] li`, + `p-dropdownitem[ng-reflect-label="$${PROTOCOL}"] li`, ); if (protocolOption === null) { @@ -223,7 +223,7 @@ async function autoSubmitForm(myForm) { if (input === null) { throw new Error( - `Unable to element that matches query "${querySelector}"`, + `Unable to element that matches query "$${querySelector}"`, ); } @@ -332,7 +332,7 @@ function setupFormDetection() { function setupObscuringStyles() { const styleId = "coder-patch--styles"; - const existingContainer = document.querySelector(`#${styleId}`); + const existingContainer = document.querySelector(`#$${styleId}`); if (existingContainer) { return; } diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index a4897680..a098669b 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -95,10 +95,10 @@ resource "coder_script" "windows-rdp" { $patch = '' # Always copy the file in case we change it. - "${replace(templatefile("${path.module}/devolutions-patch.js", { + "${templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - }), "$", "$$")}" | Set-Content "$root\coder.js" + })}" | Set-Content "$root\coder.js" # Only inject the src if we have not before. $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" From 0b6975c266c3f280576b354d46bf439f2bb273ed Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:41:45 +0000 Subject: [PATCH 22/72] fix: escape quotes --- windows-rdp/main.tf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index a098669b..dafae70f 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -95,10 +95,12 @@ resource "coder_script" "windows-rdp" { $patch = '' # Always copy the file in case we change it. - "${templatefile("${path.module}/devolutions-patch.js", { + @' + ${templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - })}" | Set-Content "$root\coder.js" + })} + '@ | Set-Content "$root\coder.js" # Only inject the src if we have not before. $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" From 14e3fc5b6bedb71ea9d87876f91d9b3f227de279 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 21:13:15 +0000 Subject: [PATCH 23/72] fix: whitespace --- windows-rdp/main.tf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index dafae70f..7d95ec23 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -96,11 +96,11 @@ resource "coder_script" "windows-rdp" { # Always copy the file in case we change it. @' - ${templatefile("${path.module}/devolutions-patch.js", { - CODER_USERNAME : var.admin_username, - CODER_PASSWORD : var.admin_password, - })} - '@ | Set-Content "$root\coder.js" +${templatefile("${path.module}/devolutions-patch.js", { + CODER_USERNAME : var.admin_username, + CODER_PASSWORD : var.admin_password, +})} +'@ | Set-Content "$root\coder.js" # Only inject the src if we have not before. $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" From fba0f842a9e7c4badbff6a9d3f1e21c85d428978 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 21:47:01 +0000 Subject: [PATCH 24/72] fix: remove regex search from Select-String --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 7d95ec23..9e1d8fb9 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -103,7 +103,7 @@ ${templatefile("${path.module}/devolutions-patch.js", { '@ | Set-Content "$root\coder.js" # Only inject the src if we have not before. - $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" + $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" -SimpleMatch if ($isPatched -eq $null) { (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml } From d5cfadb4e7dccb8fc32f9fa652e95444f4e0b5aa Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 17:03:54 +0000 Subject: [PATCH 25/72] fix: remove template literal dollar signs --- windows-rdp/devolutions-patch.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index ecb16c19..cd1284f1 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -8,8 +8,14 @@ * that you can take advantage of the @ts-check directive and get some type- * checking still. * - * A lot of the HTML selectors in this file will look nonstandard. This is - * because they are actually custom Angular components. + * Other notes about the weird ways this file is set up: + * - A lot of the HTML selectors in this file will look nonstandard. This is + * because they are actually custom Angular components. + * - It is strongly advised that you avoid template literals that use the + * placeholder syntax via the dollar sign. The Terraform script looks for + * these characters so that it can inject Coder-specific values, so any + * template literal that uses the character actually needs to double up each + * of them * * @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry * @typedef {Readonly>} FormFieldEntries @@ -135,7 +141,7 @@ function setInputValue(inputField, inputText) { if (i === -1) { inputField.value = ""; } else { - inputField.value = `$${inputField.value}$${currentChar}`; + inputField.value = inputField.value + currentChar; } inputField.dispatchEvent(inputEvent); @@ -171,7 +177,7 @@ function setInputValue(inputField, inputText) { async function autoSubmitForm(myForm) { const setProtocolValue = () => { /** @type {HTMLDivElement | null} */ - const protocolDropdownTrigger = myForm.querySelector(`div[role="button"]`); + const protocolDropdownTrigger = myForm.querySelector('div[role="button"]'); if (protocolDropdownTrigger === null) { throw new Error("No clickable trigger for setting protocol value"); } @@ -184,7 +190,7 @@ async function autoSubmitForm(myForm) { // they're part of the form. Avoids CSS stacking context issues, maybe? /** @type {HTMLLIElement | null} */ const protocolOption = document.querySelector( - `p-dropdownitem[ng-reflect-label="$${PROTOCOL}"] li`, + 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '" li', ); if (protocolOption === null) { @@ -223,7 +229,7 @@ async function autoSubmitForm(myForm) { if (input === null) { throw new Error( - `Unable to element that matches query "$${querySelector}"`, + 'Unable to element that matches query "' + querySelector + '"', ); } @@ -332,7 +338,7 @@ function setupFormDetection() { function setupObscuringStyles() { const styleId = "coder-patch--styles"; - const existingContainer = document.querySelector(`#$${styleId}`); + const existingContainer = document.querySelector("#" + styleId); if (existingContainer) { return; } From 8195cf445381044b92f9039262712ce044412f49 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 19:48:44 +0000 Subject: [PATCH 26/72] wip: add current code for hiding Devolutions form --- windows-rdp/devolutions-patch.js | 113 ++++++++++++++++++++++++++++--- 1 file changed, 105 insertions(+), 8 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index cd1284f1..aa001ac0 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -16,6 +16,11 @@ * these characters so that it can inject Coder-specific values, so any * template literal that uses the character actually needs to double up each * of them + * - All the CSS should be written via custom style tags and the !important + * directive (as much as that is a bad idea most of the time). We do not + * control the Angular app, so we have to modify things from afar to ensure + * that as Angular's internal state changes, it doesn't modify its HTML nodes + * in a way that causes our custom styles to get wiped away. * * @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry * @typedef {Readonly>} FormFieldEntries @@ -90,7 +95,7 @@ const formFieldEntries = { function setInputValue(inputField, inputText) { const continueEventName = "coder-patch--continue"; - const promise = /** @type {Promise} */ ( + const keyboardInputPromise = /** @type {Promise} */ ( new Promise((resolve, reject) => { if (inputText === "") { resolve(); @@ -156,7 +161,7 @@ function setInputValue(inputField, inputText) { }) ); - return promise; + return keyboardInputPromise; } /** @@ -274,8 +279,6 @@ function setupFormDetection() { /** @returns {void} */ const onDynamicTabMutation = () => { - console.log("Ran on mutation!"); - /** @type {HTMLFormElement | null} */ const latestForm = document.querySelector("web-client-form > form"); @@ -335,9 +338,8 @@ function setupFormDetection() { * * @returns {void} */ -function setupObscuringStyles() { - const styleId = "coder-patch--styles"; - +function setupAlwaysOnStyles() { + const styleId = "coder-patch--styles-always-on"; const existingContainer = document.querySelector("#" + styleId); if (existingContainer) { return; @@ -355,10 +357,105 @@ function setupObscuringStyles() { document.head.appendChild(styleContainer); } +function hideFormForInitialSubmission() { + const styleId = "coder-patch--styles-initial-submission"; + const existingContainer = document.querySelector("#" + styleId); + if (existingContainer) { + return; + } + + const styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` + /* + Have to use opacity instead of visibility, because the element still + needs to be interactive via the script so that it can be auto-filled. + */ + :root { + /* + Can be 0 or 1. Start off invisible to avoid risks of UI flickering, but + the rest of the function should be in charge of making the form + container visible again if something goes wrong during setup. + */ + --coder-opacity-multiplier: 1; + } + + /* web-client-form is the container for the main session form */ + web-client-form { + opacity: calc(100% * var(--coder-opacity-multiplier)) !important; + } + `; + + document.head.appendChild(styleContainer); + + // The root node being undefined should be physically impossible (if it's + // undefined, the browser itself is busted), but we need to do a type check + // here so that the rest of the function doesn't need to do type checks over + // and over. + const rootNode = document.querySelector(":root"); + if (!(rootNode instanceof HTMLElement)) { + styleContainer.innerHTML = ""; + return; + } + + /** @type {number | undefined} */ + let intervalId = undefined; + const maxScreenPolls = 3; + let pollAttempts = 0; + + const checkIfSafeToHideForm = () => { + /** @type {HTMLFormElement | null} */ + const form = document.querySelector("web-client-form > form"); + if (form === null) { + pollAttempts++; + if (pollAttempts === maxScreenPolls) { + window.clearInterval(intervalId); + } + + return; + } + + // Now that we know the container exists, it's safe to hide it + rootNode.style.setProperty("--coder-opacity-multiplier", "0"); + + // It's safe to make the form visible preemptively because Devolutions + // outputs the Windows view through an HTML canvas that it overlays on top + // of the rest of the app. Even if the form isn't hidden at the style level, + // it will still be covered up. + const restoreOpacity = () => { + rootNode.style.setProperty("--coder-opacity-multiplier", "1"); + }; + + const timeoutId = window.setTimeout(() => { + restoreOpacity(); + form.removeEventListener("submit", restoreOpacity); + }, 5_000); + + form.addEventListener( + "submit", + () => { + restoreOpacity(); + window.clearTimeout(timeoutId); + }, + { once: true }, + ); + }; + + intervalId = window.setInterval( + checkIfSafeToHideForm, + SCREEN_POLL_INTERVAL_MS, + ); +} + +function setupFormOverrides() { + hideFormForInitialSubmission(); + setupFormDetection(); +} + // Always safe to call setupObscuringStyles immediately because even if the // Angular app isn't loaded by the time the function gets called, the CSS will // always be globally available for when Angular is finally ready -setupObscuringStyles(); +setupAlwaysOnStyles(); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", setupFormDetection); From 652fc6b84fcc73a846a5755822979448338bd40b Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 19:55:14 +0000 Subject: [PATCH 27/72] refactor: clean up form code --- windows-rdp/devolutions-patch.js | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index aa001ac0..582f7917 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -426,19 +426,12 @@ function hideFormForInitialSubmission() { rootNode.style.setProperty("--coder-opacity-multiplier", "1"); }; - const timeoutId = window.setTimeout(() => { - restoreOpacity(); - form.removeEventListener("submit", restoreOpacity); - }, 5_000); - - form.addEventListener( - "submit", - () => { - restoreOpacity(); - window.clearTimeout(timeoutId); - }, - { once: true }, - ); + // If this file gets more complicated, it might make sense to set up the + // timeout and event listener so that if one triggers, it cancels the other, + // but having restoreOpacity run more than once is a no-op for right now. + // Not a big deal if these don't get cleaned up. + window.setTimeout(restoreOpacity, 5_000); + form.addEventListener("submit", restoreOpacity, { once: true }); }; intervalId = window.setInterval( From 702271133f3739d1421470890c3fdbb808148789 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 19:57:48 +0000 Subject: [PATCH 28/72] fix: update HTML query selector --- windows-rdp/devolutions-patch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 582f7917..d5e1e257 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -195,7 +195,7 @@ async function autoSubmitForm(myForm) { // they're part of the form. Avoids CSS stacking context issues, maybe? /** @type {HTMLLIElement | null} */ const protocolOption = document.querySelector( - 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '" li', + 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li', ); if (protocolOption === null) { @@ -451,7 +451,7 @@ function setupFormOverrides() { setupAlwaysOnStyles(); if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", setupFormDetection); + document.addEventListener("DOMContentLoaded", setupFormOverrides); } else { - setupFormDetection(); + setupFormOverrides(); } From 5ec1b207d13d2f2cf20a52e71154ac7d13e61856 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 19:58:56 +0000 Subject: [PATCH 29/72] docs: remove now-inaccurate comment --- windows-rdp/devolutions-patch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index d5e1e257..f7673ed9 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -445,9 +445,9 @@ function setupFormOverrides() { setupFormDetection(); } -// Always safe to call setupObscuringStyles immediately because even if the -// Angular app isn't loaded by the time the function gets called, the CSS will -// always be globally available for when Angular is finally ready +// Always safe to call this immediately because even if the Angular app isn't +// loaded by the time the function gets called, the CSS will always be globally +// available for when Angular is finally ready setupAlwaysOnStyles(); if (document.readyState === "loading") { From c7a4fced4c1ef91b8952b2c980225de79c5d9f05 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 20:15:18 +0000 Subject: [PATCH 30/72] fix: update instanceof check --- windows-rdp/devolutions-patch.js | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index f7673ed9..b4d49d1a 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -359,41 +359,41 @@ function setupAlwaysOnStyles() { function hideFormForInitialSubmission() { const styleId = "coder-patch--styles-initial-submission"; - const existingContainer = document.querySelector("#" + styleId); - if (existingContainer) { - return; - } - const styleContainer = document.createElement("style"); - styleContainer.id = styleId; - styleContainer.innerHTML = ` - /* - Have to use opacity instead of visibility, because the element still - needs to be interactive via the script so that it can be auto-filled. - */ - :root { + /** @type {HTMLStyleElement | null} */ + let styleContainer = document.querySelector("#" + styleId); + if (!styleContainer) { + styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` /* - Can be 0 or 1. Start off invisible to avoid risks of UI flickering, but - the rest of the function should be in charge of making the form - container visible again if something goes wrong during setup. + Have to use opacity instead of visibility, because the element still + needs to be interactive via the script so that it can be auto-filled. */ - --coder-opacity-multiplier: 1; - } + :root { + /* + Can be 0 or 1. Start off invisible to avoid risks of UI flickering, + but the rest of the function should be in charge of making the form + container visible again if something goes wrong during setup. + */ + --coder-opacity-multiplier: 1; + } - /* web-client-form is the container for the main session form */ - web-client-form { - opacity: calc(100% * var(--coder-opacity-multiplier)) !important; - } - `; + /* web-client-form is the container for the main session form */ + web-client-form { + opacity: calc(100% * var(--coder-opacity-multiplier)) !important; + } + `; - document.head.appendChild(styleContainer); + document.head.appendChild(styleContainer); + } // The root node being undefined should be physically impossible (if it's // undefined, the browser itself is busted), but we need to do a type check // here so that the rest of the function doesn't need to do type checks over // and over. const rootNode = document.querySelector(":root"); - if (!(rootNode instanceof HTMLElement)) { + if (!(rootNode instanceof HTMLHtmlElement)) { styleContainer.innerHTML = ""; return; } From 1a0a8659ccd43b50bef5d5d9e588dbec52de6366 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 20:40:44 +0000 Subject: [PATCH 31/72] wip: update logic for hiding form to avoid whiffs --- windows-rdp/devolutions-patch.js | 34 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index b4d49d1a..fd97cfbd 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -15,7 +15,8 @@ * placeholder syntax via the dollar sign. The Terraform script looks for * these characters so that it can inject Coder-specific values, so any * template literal that uses the character actually needs to double up each - * of them + * of them. There are already a few places in this file where it couldn't be + * avoided, but it will save you some headache. * - All the CSS should be written via custom style tags and the !important * directive (as much as that is a bad idea most of the time). We do not * control the Angular app, so we have to modify things from afar to ensure @@ -359,6 +360,7 @@ function setupAlwaysOnStyles() { function hideFormForInitialSubmission() { const styleId = "coder-patch--styles-initial-submission"; + const opacityVariableName = "--coder-opacity-multiplier"; /** @type {HTMLStyleElement | null} */ let styleContainer = document.querySelector("#" + styleId); @@ -376,12 +378,12 @@ function hideFormForInitialSubmission() { but the rest of the function should be in charge of making the form container visible again if something goes wrong during setup. */ - --coder-opacity-multiplier: 1; + $${opacityVariableName}: 0; } /* web-client-form is the container for the main session form */ web-client-form { - opacity: calc(100% * var(--coder-opacity-multiplier)) !important; + opacity: calc(100% * var($${opacityVariableName})) !important; } `; @@ -398,9 +400,17 @@ function hideFormForInitialSubmission() { return; } + // It's safe to make the form visible preemptively because Devolutions + // outputs the Windows view through an HTML canvas that it overlays on top + // of the rest of the app. Even if the form isn't hidden at the style level, + // it will still be covered up. + const restoreOpacity = () => { + rootNode.style.setProperty(opacityVariableName, "1"); + }; + /** @type {number | undefined} */ let intervalId = undefined; - const maxScreenPolls = 3; + const pollingTimeoutMs = 5_000; let pollAttempts = 0; const checkIfSafeToHideForm = () => { @@ -408,24 +418,16 @@ function hideFormForInitialSubmission() { const form = document.querySelector("web-client-form > form"); if (form === null) { pollAttempts++; - if (pollAttempts === maxScreenPolls) { + const elapsedTime = pollAttempts * SCREEN_POLL_INTERVAL_MS; + + if (elapsedTime >= pollingTimeoutMs) { + restoreOpacity(); window.clearInterval(intervalId); } return; } - // Now that we know the container exists, it's safe to hide it - rootNode.style.setProperty("--coder-opacity-multiplier", "0"); - - // It's safe to make the form visible preemptively because Devolutions - // outputs the Windows view through an HTML canvas that it overlays on top - // of the rest of the app. Even if the form isn't hidden at the style level, - // it will still be covered up. - const restoreOpacity = () => { - rootNode.style.setProperty("--coder-opacity-multiplier", "1"); - }; - // If this file gets more complicated, it might make sense to set up the // timeout and event listener so that if one triggers, it cancels the other, // but having restoreOpacity run more than once is a no-op for right now. From ef4c87e48e3e515ed10948101492a0fad6abdb0f Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 20:45:39 +0000 Subject: [PATCH 32/72] fix: simplify code for hiding form --- windows-rdp/devolutions-patch.js | 39 +++++++------------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index fd97cfbd..5adcd38c 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -408,38 +408,15 @@ function hideFormForInitialSubmission() { rootNode.style.setProperty(opacityVariableName, "1"); }; - /** @type {number | undefined} */ - let intervalId = undefined; - const pollingTimeoutMs = 5_000; - let pollAttempts = 0; - - const checkIfSafeToHideForm = () => { - /** @type {HTMLFormElement | null} */ - const form = document.querySelector("web-client-form > form"); - if (form === null) { - pollAttempts++; - const elapsedTime = pollAttempts * SCREEN_POLL_INTERVAL_MS; - - if (elapsedTime >= pollingTimeoutMs) { - restoreOpacity(); - window.clearInterval(intervalId); - } - - return; - } - - // If this file gets more complicated, it might make sense to set up the - // timeout and event listener so that if one triggers, it cancels the other, - // but having restoreOpacity run more than once is a no-op for right now. - // Not a big deal if these don't get cleaned up. - window.setTimeout(restoreOpacity, 5_000); - form.addEventListener("submit", restoreOpacity, { once: true }); - }; + // If this file gets more complicated, it might make sense to set up the + // timeout and event listener so that if one triggers, it cancels the other, + // but having restoreOpacity run more than once is a no-op for right now. + // Not a big deal if these don't get cleaned up. - intervalId = window.setInterval( - checkIfSafeToHideForm, - SCREEN_POLL_INTERVAL_MS, - ); + /** @type {HTMLFormElement | null} */ + const form = document.querySelector("web-client-form > form"); + form?.addEventListener("submit", restoreOpacity, { once: true }); + window.setTimeout(restoreOpacity, 5_000); } function setupFormOverrides() { From a9a75b675faeeb00c7ee4d655fc1bbc211942a47 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:01:11 +0000 Subject: [PATCH 33/72] fix: add more changes to opacity logic --- windows-rdp/devolutions-patch.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 5adcd38c..3e08f762 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -360,7 +360,7 @@ function setupAlwaysOnStyles() { function hideFormForInitialSubmission() { const styleId = "coder-patch--styles-initial-submission"; - const opacityVariableName = "--coder-opacity-multiplier"; + const cssOpacityVariableName = "--coder-opacity-multiplier"; /** @type {HTMLStyleElement | null} */ let styleContainer = document.querySelector("#" + styleId); @@ -378,12 +378,12 @@ function hideFormForInitialSubmission() { but the rest of the function should be in charge of making the form container visible again if something goes wrong during setup. */ - $${opacityVariableName}: 0; + $${cssOpacityVariableName}: 0; } /* web-client-form is the container for the main session form */ web-client-form { - opacity: calc(100% * var($${opacityVariableName})) !important; + opacity: calc(100% * var($${cssOpacityVariableName})) !important; } `; @@ -405,18 +405,26 @@ function hideFormForInitialSubmission() { // of the rest of the app. Even if the form isn't hidden at the style level, // it will still be covered up. const restoreOpacity = () => { - rootNode.style.setProperty(opacityVariableName, "1"); + rootNode.style.setProperty(cssOpacityVariableName, "1"); }; // If this file gets more complicated, it might make sense to set up the // timeout and event listener so that if one triggers, it cancels the other, // but having restoreOpacity run more than once is a no-op for right now. // Not a big deal if these don't get cleaned up. + window.setTimeout(restoreOpacity, 5_000); /** @type {HTMLFormElement | null} */ const form = document.querySelector("web-client-form > form"); - form?.addEventListener("submit", restoreOpacity, { once: true }); - window.setTimeout(restoreOpacity, 5_000); + form?.addEventListener( + "submit", + () => { + // Not restoring opacity right away just to give the HTML canvas a little + // bit of time to get spun up and cover up the main form + window.setTimeout(restoreOpacity, 1_000); + }, + { once: true }, + ); } function setupFormOverrides() { From f3c30abeb431baef522409cd5ec26e65d76a5bfb Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:03:02 +0000 Subject: [PATCH 34/72] fix: make form hiding logic run on webpage opening --- windows-rdp/devolutions-patch.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 3e08f762..e700330e 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -427,18 +427,14 @@ function hideFormForInitialSubmission() { ); } -function setupFormOverrides() { - hideFormForInitialSubmission(); - setupFormDetection(); -} - // Always safe to call this immediately because even if the Angular app isn't // loaded by the time the function gets called, the CSS will always be globally // available for when Angular is finally ready setupAlwaysOnStyles(); +hideFormForInitialSubmission(); if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", setupFormOverrides); + document.addEventListener("DOMContentLoaded", setupFormDetection); } else { - setupFormOverrides(); + setupFormDetection(); } From 8aff87fdf79ffc1ae9f03dfae92f7a9ecc819a95 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:20:42 +0000 Subject: [PATCH 35/72] fix: add logic for hiding the dropdown of protocol options --- windows-rdp/devolutions-patch.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index e700330e..c9e25da9 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -381,8 +381,14 @@ function hideFormForInitialSubmission() { $${cssOpacityVariableName}: 0; } - /* web-client-form is the container for the main session form */ - web-client-form { + /* + web-client-form is the container for the main session form, while + the div is for the dropdown that is used for selecting the protocol. + The dropdown is not inside of the form for CSS styling reasons, so we + need to select both. + */ + web-client-form, + body > div.p-overlay { opacity: calc(100% * var($${cssOpacityVariableName})) !important; } `; From b09c4cb0841e927a4fb82c262d9a81630d5685a7 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:35:53 +0000 Subject: [PATCH 36/72] fix: speed up code for filling in form --- windows-rdp/devolutions-patch.js | 99 +++++++++----------------------- 1 file changed, 28 insertions(+), 71 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index c9e25da9..86198a86 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -74,9 +74,9 @@ const formFieldEntries = { }; /** - * Handles typing in the values for the input form, dispatching each character - * as an event. This function assumes that all characters in the input will be - * UTF-8. + * Handles typing in the values for the input form. All values are written + * immediately, even though that would be physically impossible with a real + * keyboard. * * Note: this code will never break, but you might get warnings in the console * from Angular about unexpected value changes. Angular patches over a lot of @@ -94,75 +94,32 @@ const formFieldEntries = { * @returns {Promise} */ function setInputValue(inputField, inputText) { - const continueEventName = "coder-patch--continue"; - - const keyboardInputPromise = /** @type {Promise} */ ( - new Promise((resolve, reject) => { - if (inputText === "") { - resolve(); - return; - } - - // -1 indicates a "pre-write" for clearing out the input before trying to - // write new text to it - let i = -1; - - // requestAnimationFrame is not capable of giving back values of 0 for its - // task IDs. Good default value to ensure that we don't need if statements - // when trying to cancel anything - let currentAnimationId = 0; - - // Super easy to pool the same event objects, because the events don't - // have any custom, context-specific values on them, and they're - // restricted to this one callback. - const continueEvent = new CustomEvent(continueEventName); - const inputEvent = new Event("input", { - bubbles: true, - cancelable: true, - }); - - /** @returns {void} */ - const handleNextCharIndex = () => { - if (i === inputText.length) { - resolve(); - return; - } - - const currentChar = inputText[i]; - if (i !== -1 && currentChar === undefined) { - throw new Error("Went out of bounds"); - } - - try { - inputField.addEventListener( - continueEventName, - () => { - i++; - currentAnimationId = - window.requestAnimationFrame(handleNextCharIndex); - }, - { once: true }, - ); - - if (i === -1) { - inputField.value = ""; - } else { - inputField.value = inputField.value + currentChar; - } - - inputField.dispatchEvent(inputEvent); - inputField.dispatchEvent(continueEvent); - } catch (err) { - cancelAnimationFrame(currentAnimationId); - reject(err); - } - }; - - currentAnimationId = window.requestAnimationFrame(handleNextCharIndex); - }) - ); + return new Promise((resolve, reject) => { + // Adding timeout for input event, even though we'll be dispatching it + // immediately, just in the off chance that something in the Angular app + // intercepts it or stops it from propagating properly + const timeoutId = window.setTimeout(() => { + reject(new Error("Input event did not get processed correctly in time.")); + }, 3_000); + + const handleSuccessfulDispatch = () => { + resolve(); + window.clearTimeout(timeoutId); + inputField.removeEventListener("input", handleSuccessfulDispatch); + }; + + inputField.addEventListener("input", handleSuccessfulDispatch); + + // Code assumes that Angular will have an event handler in place to handle + // the new event + const inputEvent = new Event("input", { + bubbles: true, + cancelable: true, + }); - return keyboardInputPromise; + inputField.value = inputText; + inputField.dispatchEvent(inputEvent); + }); } /** From 5f418c325321f88f93a6b6132e60e4739ebc78d0 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:51:21 +0000 Subject: [PATCH 37/72] docs: add comments about necessary double dollar signs --- windows-rdp/devolutions-patch.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 86198a86..9b77a8ef 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -103,9 +103,9 @@ function setInputValue(inputField, inputText) { }, 3_000); const handleSuccessfulDispatch = () => { - resolve(); window.clearTimeout(timeoutId); inputField.removeEventListener("input", handleSuccessfulDispatch); + resolve(); }; inputField.addEventListener("input", handleSuccessfulDispatch); @@ -334,6 +334,8 @@ function hideFormForInitialSubmission() { Can be 0 or 1. Start off invisible to avoid risks of UI flickering, but the rest of the function should be in charge of making the form container visible again if something goes wrong during setup. + + Double dollar sign needed to avoid Terraform script false positives */ $${cssOpacityVariableName}: 0; } @@ -346,6 +348,9 @@ function hideFormForInitialSubmission() { */ web-client-form, body > div.p-overlay { + /* + Double dollar sign needed to avoid Terraform script false positives + */ opacity: calc(100% * var($${cssOpacityVariableName})) !important; } `; From b283ac3129f7f5c946f9788be440c69549d024a5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:54:13 +0000 Subject: [PATCH 38/72] docs: fix misleading typo in comment --- windows-rdp/devolutions-patch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 9b77a8ef..0bd91e40 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -395,7 +395,7 @@ function hideFormForInitialSubmission() { ); } -// Always safe to call this immediately because even if the Angular app isn't +// Always safe to call these immediately because even if the Angular app isn't // loaded by the time the function gets called, the CSS will always be globally // available for when Angular is finally ready setupAlwaysOnStyles(); From aebf095075902036ac4141c86d567cf00fcdaff0 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 14:37:14 +0000 Subject: [PATCH 39/72] refactor: clean up patch logic for clarity --- windows-rdp/devolutions-patch.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 0bd91e40..a1e9da40 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -16,7 +16,7 @@ * these characters so that it can inject Coder-specific values, so any * template literal that uses the character actually needs to double up each * of them. There are already a few places in this file where it couldn't be - * avoided, but it will save you some headache. + * avoided, but avoiding this as much as possible will save you some headache. * - All the CSS should be written via custom style tags and the !important * directive (as much as that is a bad idea most of the time). We do not * control the Angular app, so we have to modify things from afar to ensure @@ -240,16 +240,12 @@ function setupFormDetection() { /** @type {HTMLFormElement | null} */ const latestForm = document.querySelector("web-client-form > form"); - if (latestForm === null) { - formValueFromLastMutation = null; - return; - } - // Only try to auto-fill if we went from having no form on screen to // having a form on screen. That way, we don't accidentally override the // form if the user is trying to customize values, and this essentially // makes the script values function as default values - if (formValueFromLastMutation === null) { + const mounted = formValueFromLastMutation === null && latestForm !== null; + if (mounted) { autoSubmitForm(latestForm); } @@ -364,7 +360,10 @@ function hideFormForInitialSubmission() { // and over. const rootNode = document.querySelector(":root"); if (!(rootNode instanceof HTMLHtmlElement)) { - styleContainer.innerHTML = ""; + // Remove the container entirely because if the browser is busted, who knows + // if the CSS variables can be applied correctly. Better to have something + // be a bit more ugly/painful to use, than have it be impossible to use + styleContainer.remove(); return; } @@ -380,6 +379,9 @@ function hideFormForInitialSubmission() { // timeout and event listener so that if one triggers, it cancels the other, // but having restoreOpacity run more than once is a no-op for right now. // Not a big deal if these don't get cleaned up. + + // Have the form automatically reappear no matter what, so that if something + // does break, the user isn't left out to dry window.setTimeout(restoreOpacity, 5_000); /** @type {HTMLFormElement | null} */ From f335cd343d6b4f2a97f172aef1dee8ccda62876e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 16:00:40 +0000 Subject: [PATCH 40/72] fix: update type definitions for helpers --- test.ts | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/test.ts b/test.ts index 37e0805f..faac2535 100644 --- a/test.ts +++ b/test.ts @@ -76,27 +76,22 @@ export const execContainer = async ( }; }; +type TerraformStateResource = { + type: string; + name: string; + provider: string; + instances: [{ attributes: Record }]; +}; + export interface TerraformState { outputs: { [key: string]: { type: string; value: any; }; - } - resources: [ - { - type: string; - name: string; - provider: string; - instances: [ - { - attributes: { - [key: string]: any; - }; - }, - ]; - }, - ]; + }; + + resources: [TerraformStateResource, ...TerraformStateResource[]]; } export interface CoderScriptAttributes { @@ -168,9 +163,11 @@ export const testRequiredVariables = ( // runTerraformApply runs terraform apply in the given directory // with the given variables. It is fine to run in parallel with // other instances of this function, as it uses a random state file. -export const runTerraformApply = async ( +export const runTerraformApply = async < + TVars extends Readonly>, +>( dir: string, - vars: Record, + vars: TVars, ): Promise => { const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; const env = {}; @@ -221,5 +218,5 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => { "Content-Type": "application/json", }, status: statusCode, - }) -} \ No newline at end of file + }); +}; From 33d44fdf17816ccde964271a6a313fa2ff431585 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 16:00:57 +0000 Subject: [PATCH 41/72] fix: remove unneeded any types --- vscode-desktop/main.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode-desktop/main.test.ts b/vscode-desktop/main.test.ts index 53fba967..a3ab6bb4 100644 --- a/vscode-desktop/main.test.ts +++ b/vscode-desktop/main.test.ts @@ -21,7 +21,7 @@ describe("vscode-desktop", async () => { "vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN", ); - const resources: any = state.resources; + const resources = state.resources; expect(resources[1].instances[0].attributes.order).toBeNull(); }); @@ -31,7 +31,7 @@ describe("vscode-desktop", async () => { order: "22", }); - const resources: any = state.resources; + const resources = state.resources; expect(resources[1].instances[0].attributes.order).toBe(22); }); }); From b2807640aaee8ccdb79de2891281e0c9c2bc01d6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 16:01:08 +0000 Subject: [PATCH 42/72] wip: commit progress on main test file --- windows-rdp/main.test.ts | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 windows-rdp/main.test.ts diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts new file mode 100644 index 00000000..b6d0e092 --- /dev/null +++ b/windows-rdp/main.test.ts @@ -0,0 +1,61 @@ +import { beforeAll, describe, expect, it, test } from "bun:test"; +import { + executeScriptInContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +type TestVariables = Readonly<{ + agent_id: string; + resource_id: string; + admin_username?: string; + admin_password?: string; +}>; + +describe("Web RDP", () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + resource_id: "bar", + }); + }); + + it("Patches the Devolutions Angular app's .html file (after it has been bundled) to include an import for the custom JS file", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + resource_id: "bar", + }); + + throw new Error("Not implemented yet"); + }); + + it("Injects the Terraform username and password into the JS patch file", async () => { + throw new Error("Not implemented yet"); + + // Test that things work with the default username/password + const defaultState = await runTerraformApply( + import.meta.dir, + { + agent_id: "foo", + resource_id: "bar", + }, + ); + + const output = await executeScriptInContainer(defaultState, "alpine"); + + // Test that custom usernames/passwords are also forwarded correctly + const customUsername = "crouton"; + const customPassword = "VeryVeryVeryVeryVerySecurePassword97!"; + const customizedState = await runTerraformApply( + import.meta.dir, + { + agent_id: "foo", + resource_id: "bar", + admin_username: customUsername, + admin_password: customPassword, + }, + ); + }); +}); From 83ecba2293e4bdbcf662af124baa78c145f02202 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 17:21:39 +0000 Subject: [PATCH 43/72] wip: commit current progress --- windows-rdp/main.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index b6d0e092..402df53f 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -22,7 +22,18 @@ describe("Web RDP", () => { }); }); - it("Patches the Devolutions Angular app's .html file (after it has been bundled) to include an import for the custom JS file", async () => { + it("Installs the Devolutions Gateway Angular app locally on the machine", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + resource_id: "bar", + }); + }); + + /** + * @todo Verify that the HTML file has been modified, and that the JS file is + * also part of the file system + */ + it("Patches the Devolutions Angular app's .html file to include an import for the custom JS file", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", resource_id: "bar", @@ -31,7 +42,7 @@ describe("Web RDP", () => { throw new Error("Not implemented yet"); }); - it("Injects the Terraform username and password into the JS patch file", async () => { + it("Injects Terraform's username and password into the JS patch file", async () => { throw new Error("Not implemented yet"); // Test that things work with the default username/password From 264584e673f6d6c5b31f74fe24fb90119fe245fd Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 17:59:12 +0000 Subject: [PATCH 44/72] fix: make comments for test helpers exportable --- test.ts | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/test.ts b/test.ts index faac2535..2601d0eb 100644 --- a/test.ts +++ b/test.ts @@ -29,8 +29,10 @@ export const runContainer = async ( return containerID.trim(); }; -// executeScriptInContainer finds the only "coder_script" -// resource in the given state and runs it in a container. +/** + * Finds the only "coder_script" resource in the given state and runs it in a + * container. + */ export const executeScriptInContainer = async ( state: TerraformState, image: string, @@ -100,10 +102,11 @@ export interface CoderScriptAttributes { url: string; } -// findResourceInstance finds the first instance of the given resource -// type in the given state. If name is specified, it will only find -// the instance with the given name. -export const findResourceInstance = ( +/** + * finds the first instance of the given resource type in the given state. If + * name is specified, it will only find the instance with the given name. + */ +export const findResourceInstance = ( state: TerraformState, type: T, name?: string, @@ -126,9 +129,10 @@ export const findResourceInstance = ( return resource.instances[0].attributes as any; }; -// testRequiredVariables creates a test-case -// for each variable provided and ensures that -// the apply fails without it. +/** + * Creates a test-case for each variable provided and ensures that the apply + * fails without it. + */ export const testRequiredVariables = ( dir: string, vars: Record, @@ -160,9 +164,11 @@ export const testRequiredVariables = ( }); }; -// runTerraformApply runs terraform apply in the given directory -// with the given variables. It is fine to run in parallel with -// other instances of this function, as it uses a random state file. +/** + * Runs terraform apply in the given directory with the given variables. It is + * fine to run in parallel with other instances of this function, as it uses a + * random state file. + */ export const runTerraformApply = async < TVars extends Readonly>, >( @@ -200,7 +206,9 @@ export const runTerraformApply = async < return JSON.parse(content); }; -// runTerraformInit runs terraform init in the given directory. +/** + * Runs terraform init in the given directory. + */ export const runTerraformInit = async (dir: string) => { const proc = spawn(["terraform", "init"], { cwd: dir, From de00f6334f6c79af6f22ff59958dd012802b2837 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 19:00:42 +0000 Subject: [PATCH 45/72] chore: add type parameter for testRequiredVariables --- test.ts | 4 ++-- windows-rdp/main.test.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test.ts b/test.ts index 2601d0eb..80524041 100644 --- a/test.ts +++ b/test.ts @@ -133,9 +133,9 @@ export const findResourceInstance = ( * Creates a test-case for each variable provided and ensures that the apply * fails without it. */ -export const testRequiredVariables = ( +export const testRequiredVariables = >( dir: string, - vars: Record, + vars: TVars, ) => { // Ensures that all required variables are provided. it("required variables", async () => { diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 402df53f..4e342852 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it, test } from "bun:test"; +import { describe, expect, it, test } from "bun:test"; import { executeScriptInContainer, runTerraformApply, @@ -13,13 +13,11 @@ type TestVariables = Readonly<{ admin_password?: string; }>; -describe("Web RDP", () => { - beforeAll(async () => { - await runTerraformInit(import.meta.dir); - testRequiredVariables(import.meta.dir, { - agent_id: "foo", - resource_id: "bar", - }); +describe("Web RDP", async () => { + await runTerraformInit(import.meta.dir); + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + resource_id: "bar", }); it("Installs the Devolutions Gateway Angular app locally on the machine", async () => { @@ -27,6 +25,8 @@ describe("Web RDP", () => { agent_id: "foo", resource_id: "bar", }); + + throw new Error("Not implemented yet"); }); /** From 7d366ff92aaaa1c55710cd72ee19094f22e126cd Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 17:20:00 +0000 Subject: [PATCH 46/72] chore: add first finished test --- windows-rdp/main.test.ts | 76 ++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 4e342852..64738e0f 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it, test } from "bun:test"; import { + JsonValue, + TerraformState, executeScriptInContainer, runTerraformApply, runTerraformInit, @@ -13,6 +15,11 @@ type TestVariables = Readonly<{ admin_password?: string; }>; +/** + * @todo It would be nice if we had a way to verify that the Devolutions root + * HTML file is modified to include the import for the patched Coder script, + * but the current test setup doesn't really make that viable + */ describe("Web RDP", async () => { await runTerraformInit(import.meta.dir); testRequiredVariables(import.meta.dir, { @@ -29,21 +36,38 @@ describe("Web RDP", async () => { throw new Error("Not implemented yet"); }); - /** - * @todo Verify that the HTML file has been modified, and that the JS file is - * also part of the file system - */ - it("Patches the Devolutions Angular app's .html file to include an import for the custom JS file", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - resource_id: "bar", - }); + it("Injects Terraform's username and password into the JS patch file", async () => { + const findInstancesScript = (state: TerraformState): string | null => { + let instancesScript: string | null = null; + for (const resource of state.resources) { + if (resource.type !== "coder_script") { + continue; + } - throw new Error("Not implemented yet"); - }); + for (const instance of resource.instances) { + if (instance.attributes.display_name === "windows-rdp") { + instancesScript = instance.attributes.script; + } + } + } - it("Injects Terraform's username and password into the JS patch file", async () => { - throw new Error("Not implemented yet"); + return instancesScript; + }; + + /** + * Using a regex as a quick-and-dirty way to get at the username and + * password values. + * + * Tried going through the trouble of extracting out the form entries + * variable from the main output, converting it from Prettier/JS-based JSON + * text to universal JSON text, and exposing it as a parsed JSON value. That + * got to be too much, though. + * + * Written and tested via Regex101 + * @see {@link https://regex101.com/r/UMgQpv/2} + */ + const formEntryValuesRe = + /^const formFieldEntries = \{$.*?^\s+username: \{$.*?^\s*?querySelector.*?,$.*?^\s*value: "(?.+?)",$.*?password: \{$.*?^\s+querySelector: .*?,$.*?^\s*value: "(?.+?)",$.*?^};$/ms; // Test that things work with the default username/password const defaultState = await runTerraformApply( @@ -54,19 +78,35 @@ describe("Web RDP", async () => { }, ); - const output = await executeScriptInContainer(defaultState, "alpine"); + const defaultInstancesScript = findInstancesScript(defaultState); + expect(defaultInstancesScript).toBeString(); + + const { username: defaultUsername, password: defaultPassword } = + formEntryValuesRe.exec(defaultInstancesScript)?.groups ?? {}; + + expect(defaultUsername).toBe("Administrator"); + expect(defaultPassword).toBe("coderRDP!"); // Test that custom usernames/passwords are also forwarded correctly - const customUsername = "crouton"; - const customPassword = "VeryVeryVeryVeryVerySecurePassword97!"; + const userDefinedUsername = "crouton"; + const userDefinedPassword = "VeryVeryVeryVeryVerySecurePassword97!"; const customizedState = await runTerraformApply( import.meta.dir, { agent_id: "foo", resource_id: "bar", - admin_username: customUsername, - admin_password: customPassword, + admin_username: userDefinedUsername, + admin_password: userDefinedPassword, }, ); + + const customInstancesScript = findInstancesScript(customizedState); + expect(customInstancesScript).toBeString(); + + const { username: customUsername, password: customPassword } = + formEntryValuesRe.exec(customInstancesScript)?.groups ?? {}; + + expect(customUsername).toBe(userDefinedUsername); + expect(customPassword).toBe(userDefinedPassword); }); }); From 6409ee2bbaf59ab5a441a4b00a5615f98bc5a039 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 17:23:01 +0000 Subject: [PATCH 47/72] refactor: clean up current code --- windows-rdp/main.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 64738e0f..16703e32 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, test } from "bun:test"; import { - JsonValue, TerraformState, executeScriptInContainer, runTerraformApply, @@ -38,7 +37,6 @@ describe("Web RDP", async () => { it("Injects Terraform's username and password into the JS patch file", async () => { const findInstancesScript = (state: TerraformState): string | null => { - let instancesScript: string | null = null; for (const resource of state.resources) { if (resource.type !== "coder_script") { continue; @@ -46,12 +44,12 @@ describe("Web RDP", async () => { for (const instance of resource.instances) { if (instance.attributes.display_name === "windows-rdp") { - instancesScript = instance.attributes.script; + return instance.attributes.script as string; } } } - return instancesScript; + return null; }; /** @@ -61,7 +59,7 @@ describe("Web RDP", async () => { * Tried going through the trouble of extracting out the form entries * variable from the main output, converting it from Prettier/JS-based JSON * text to universal JSON text, and exposing it as a parsed JSON value. That - * got to be too much, though. + * got to be a bit too much, though. * * Written and tested via Regex101 * @see {@link https://regex101.com/r/UMgQpv/2} From 25c90001f4d49d44bc81dc38555ee9b5f2285eff Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 17:28:13 +0000 Subject: [PATCH 48/72] docs: add comment about how regex is set up --- windows-rdp/main.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 16703e32..0a1452ed 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -61,6 +61,12 @@ describe("Web RDP", async () => { * text to universal JSON text, and exposing it as a parsed JSON value. That * got to be a bit too much, though. * + * Regex is a little bit more verbose and pedantic than normal. Want to + * have some basic safety nets for validating the structure of the form + * entries variable after the JS file has had values injected. Really do + * not want the wildcard classes to overshoot and grab too much content, + * even if they're all set to lazy mode. + * * Written and tested via Regex101 * @see {@link https://regex101.com/r/UMgQpv/2} */ From 5869eb86d47b13b1369665b70b56cd4b77354c2a Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 19:42:23 +0000 Subject: [PATCH 49/72] chore: finish all initial tests --- windows-rdp/main.test.ts | 78 +++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 0a1452ed..58e0544a 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -1,7 +1,6 @@ -import { describe, expect, it, test } from "bun:test"; +import { describe, expect, it } from "bun:test"; import { TerraformState, - executeScriptInContainer, runTerraformApply, runTerraformInit, testRequiredVariables, @@ -14,6 +13,25 @@ type TestVariables = Readonly<{ admin_password?: string; }>; +function findWindowsRpdScript(state: TerraformState): string | null { + for (const resource of state.resources) { + const isRdpScriptResource = + resource.type === "coder_script" && resource.name === "windows-rdp"; + + if (!isRdpScriptResource) { + continue; + } + + for (const instance of resource.instances) { + if (instance.attributes.display_name === "windows-rdp") { + return instance.attributes.script; + } + } + } + + return null; +} + /** * @todo It would be nice if we had a way to verify that the Devolutions root * HTML file is modified to include the import for the patched Coder script, @@ -26,32 +44,28 @@ describe("Web RDP", async () => { resource_id: "bar", }); - it("Installs the Devolutions Gateway Angular app locally on the machine", async () => { + it("Has the PowerShell script install Devolutions Gateway", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", resource_id: "bar", }); - throw new Error("Not implemented yet"); + const lines = findWindowsRpdScript(state) + .split("\n") + .filter(Boolean) + .map((line) => line.trimStart()); + + expect(lines).toEqual( + expect.arrayContaining([ + '$moduleName = "DevolutionsGateway"', + // Devolutions does versioning in the format year.minor.patch + expect.stringMatching(/^\$moduleVersion = "\d{4}\.\d+\.\d+"$/), + "Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force", + ]), + ); }); it("Injects Terraform's username and password into the JS patch file", async () => { - const findInstancesScript = (state: TerraformState): string | null => { - for (const resource of state.resources) { - if (resource.type !== "coder_script") { - continue; - } - - for (const instance of resource.instances) { - if (instance.attributes.display_name === "windows-rdp") { - return instance.attributes.script as string; - } - } - } - - return null; - }; - /** * Using a regex as a quick-and-dirty way to get at the username and * password values. @@ -82,35 +96,35 @@ describe("Web RDP", async () => { }, ); - const defaultInstancesScript = findInstancesScript(defaultState); - expect(defaultInstancesScript).toBeString(); + const defaultRdpScript = findWindowsRpdScript(defaultState); + expect(defaultRdpScript).toBeString(); const { username: defaultUsername, password: defaultPassword } = - formEntryValuesRe.exec(defaultInstancesScript)?.groups ?? {}; + formEntryValuesRe.exec(defaultRdpScript)?.groups ?? {}; expect(defaultUsername).toBe("Administrator"); expect(defaultPassword).toBe("coderRDP!"); // Test that custom usernames/passwords are also forwarded correctly - const userDefinedUsername = "crouton"; - const userDefinedPassword = "VeryVeryVeryVeryVerySecurePassword97!"; + const customAdminUsername = "crouton"; + const customAdminPassword = "VeryVeryVeryVeryVerySecurePassword97!"; const customizedState = await runTerraformApply( import.meta.dir, { agent_id: "foo", resource_id: "bar", - admin_username: userDefinedUsername, - admin_password: userDefinedPassword, + admin_username: customAdminUsername, + admin_password: customAdminPassword, }, ); - const customInstancesScript = findInstancesScript(customizedState); - expect(customInstancesScript).toBeString(); + const customRdpScript = findWindowsRpdScript(customizedState); + expect(customRdpScript).toBeString(); const { username: customUsername, password: customPassword } = - formEntryValuesRe.exec(customInstancesScript)?.groups ?? {}; + formEntryValuesRe.exec(customRdpScript)?.groups ?? {}; - expect(customUsername).toBe(userDefinedUsername); - expect(customPassword).toBe(userDefinedPassword); + expect(customUsername).toBe(customAdminUsername); + expect(customPassword).toBe(customAdminPassword); }); }); From 90e15cd90c249616cc3d08340ce8cdb7f471c705 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 19:49:16 +0000 Subject: [PATCH 50/72] fix: update string formatting logic to make tests less likely to flake from modifications --- windows-rdp/main.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 58e0544a..a8b4eb2f 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -53,7 +53,7 @@ describe("Web RDP", async () => { const lines = findWindowsRpdScript(state) .split("\n") .filter(Boolean) - .map((line) => line.trimStart()); + .map((line) => line.trim()); expect(lines).toEqual( expect.arrayContaining([ From 05a20a9e1fa9b0ac7871089633b40070b44e2cf9 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 20:00:44 +0000 Subject: [PATCH 51/72] docs: rewrite comment for clarity --- windows-rdp/main.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index a8b4eb2f..1c739b88 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -77,9 +77,9 @@ describe("Web RDP", async () => { * * Regex is a little bit more verbose and pedantic than normal. Want to * have some basic safety nets for validating the structure of the form - * entries variable after the JS file has had values injected. Really do - * not want the wildcard classes to overshoot and grab too much content, - * even if they're all set to lazy mode. + * entries variable after the JS file has had values injected. Even with all + * the wildcard classes set to lazy mode, we want to make sure that they + * don't overshoot and grab too much content. * * Written and tested via Regex101 * @see {@link https://regex101.com/r/UMgQpv/2} From f82c7fd7a1ba08e6328a6fed9d12fb1b64579e4d Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 16:51:03 +0000 Subject: [PATCH 52/72] test: set up NuGet in advance --- windows-rdp/main.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 9e1d8fb9..ffa22563 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,7 +34,7 @@ resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "windows-rdp" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons - script = < Date: Fri, 28 Jun 2024 17:21:24 +0000 Subject: [PATCH 53/72] wip: add try/catch block --- windows-rdp/main.tf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index ffa22563..09622a70 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -62,6 +62,12 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges + try { + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } catch { + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force From 4ab72575ac048a93f3c8854f3a41b936830fab73 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 17:23:58 +0000 Subject: [PATCH 54/72] fix: remove accidental uncaught code --- windows-rdp/main.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 09622a70..eaac9cba 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -63,13 +63,14 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges try { + # Install-PackageProvider is required for AWS Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force } catch { + # If the first command failed, assume that we're on GCP and run + # Install-Module only Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force } - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force # Construct the module path for system-wide installation $moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" From 8262b290635f77d1162c0d2385813dc7ba907d2e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 17:34:36 +0000 Subject: [PATCH 55/72] wip: try reformatting try/catch --- windows-rdp/main.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index eaac9cba..8e52088a 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -66,7 +66,8 @@ resource "coder_script" "windows-rdp" { # Install-PackageProvider is required for AWS Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - } catch { + } + catch { # If the first command failed, assume that we're on GCP and run # Install-Module only Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force From 16f96d3693571edab9847e3d0666e75b1312acec Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 17:49:55 +0000 Subject: [PATCH 56/72] wip: add code for triggering try/catch --- windows-rdp/main.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 8e52088a..273ad20d 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -63,8 +63,9 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges try { - # Install-PackageProvider is required for AWS - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force + # Install-PackageProvider is required for AWS. Need to set command to + # terminate on failure so that try/catch actually triggers + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force } catch { From 78c948094ddb5de8f69fab272072b0e8ebf39321 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 18:20:46 +0000 Subject: [PATCH 57/72] wip: try reverting temporarily --- windows-rdp/main.tf | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 273ad20d..919b5e40 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -62,17 +62,19 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges - try { - # Install-PackageProvider is required for AWS. Need to set command to - # terminate on failure so that try/catch actually triggers - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - } - catch { - # If the first command failed, assume that we're on GCP and run - # Install-Module only - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - } + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + + # try { + # # Install-PackageProvider is required for AWS. Need to set command to + # # terminate on failure so that try/catch actually triggers + # Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop + # Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + # } + # catch { + # # If the first command failed, assume that we're on GCP and run + # # Install-Module only + # Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + # } # Construct the module path for system-wide installation $moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" From 78f91a542abe3e5fb917e2ba9f5304f904fce686 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 18:25:59 +0000 Subject: [PATCH 58/72] wip: revert back --- windows-rdp/main.tf | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 919b5e40..273ad20d 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -62,19 +62,17 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - - # try { - # # Install-PackageProvider is required for AWS. Need to set command to - # # terminate on failure so that try/catch actually triggers - # Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop - # Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - # } - # catch { - # # If the first command failed, assume that we're on GCP and run - # # Install-Module only - # Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - # } + try { + # Install-PackageProvider is required for AWS. Need to set command to + # terminate on failure so that try/catch actually triggers + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } + catch { + # If the first command failed, assume that we're on GCP and run + # Install-Module only + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } # Construct the module path for system-wide installation $moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" From ec2c8edfb2bcc94e9fbdb1a5335f5a6a8602a4e5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 21:06:08 +0000 Subject: [PATCH 59/72] fix: update null check and remove typo --- windows-rdp/main.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 1c739b88..24ce1049 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -13,7 +13,7 @@ type TestVariables = Readonly<{ admin_password?: string; }>; -function findWindowsRpdScript(state: TerraformState): string | null { +function findWindowsRdpScript(state: TerraformState): string | null { for (const resource of state.resources) { const isRdpScriptResource = resource.type === "coder_script" && resource.name === "windows-rdp"; @@ -50,8 +50,8 @@ describe("Web RDP", async () => { resource_id: "bar", }); - const lines = findWindowsRpdScript(state) - .split("\n") + const lines = findWindowsRdpScript(state) + ?.split("\n") .filter(Boolean) .map((line) => line.trim()); @@ -96,7 +96,7 @@ describe("Web RDP", async () => { }, ); - const defaultRdpScript = findWindowsRpdScript(defaultState); + const defaultRdpScript = findWindowsRdpScript(defaultState); expect(defaultRdpScript).toBeString(); const { username: defaultUsername, password: defaultPassword } = @@ -118,7 +118,7 @@ describe("Web RDP", async () => { }, ); - const customRdpScript = findWindowsRpdScript(customizedState); + const customRdpScript = findWindowsRdpScript(customizedState); expect(customRdpScript).toBeString(); const { username: customUsername, password: customPassword } = From d9d1be08a320ae72baedd2b88daaf3afd8629d73 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 14:05:40 +0000 Subject: [PATCH 60/72] fix: update README for RDP --- windows-rdp/README.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 6320ca64..c00e4223 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -1,25 +1,40 @@ --- display_name: Windows RDP -description: RDP Server and Web Client powered by Devolutions +description: RDP Server and Web Client, powered by Devolutions Gateway icon: ../.icons/desktop.svg maintainer_github: coder -verified: false +verified: true tags: [windows, rdp, web, desktop] --- # Windows RDP -Enable Remote Desktop + a web based client on Windows workspaces, powered by [devolutions-gateway](https://github.com/Devolutions/devolutions-gateway) +Enable Remote Desktop + a web based client on Windows workspaces, powered by [devolutions-gateway](https://github.com/Devolutions/devolutions-gateway). -[![Web RDP on Windows](https://cdn.loom.com/sessions/thumbnails/a5d98c7007a7417fb572aba1acf8d538-with-play.gif)](https://www.loom.com/share/a5d98c7007a7417fb572aba1acf8d538) +## Video + +<-- Insert demo video here --> ## Usage +For AWS: + +```tf +module "windows_rdp" { + count = data.coder_workspace.me.start_count + source = "github.com/coder/modules//windows-rdp" + agent_id = resource.coder_agent.main.id + resource_id = resource.aws_instance.dev.id +} +``` + +For Google Cloud: + ```tf module "windows_rdp" { - count = data.coder_workspace.me.start_count - source = "github.com/coder/modules//windows-rdp?ref=web-rdp" - agent_id = resource.coder_agent.main.id + count = data.coder_workspace.me.start_count + source = "github.com/coder/modules//windows-rdp" + agent_id = resource.coder_agent.main.id resource_id = resource.google_compute_instance.dev[0].id } ``` @@ -30,6 +45,4 @@ module "windows_rdp" { ## Roadmap -- [ ] Test on additional cloud providers -- [ ] Automatically establish web RDP session when users click "web RDP" - > This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp), modifying `webapp/`, building, and specifying a new [static root path](https://github.com/Devolutions/devolutions-gateway/blob/a884cbb8ff313496fb3d4072e67ef75350c40c03/devolutions-gateway/tests/config.rs#L271). Ideally we can upstream this functionality. \ No newline at end of file +- [ ] Test on Microsoft Azure. From a381c3ee29c750b01829024b06a295552e7de324 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 14:14:53 +0000 Subject: [PATCH 61/72] fix: update structure of README for linter --- windows-rdp/README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index c00e4223..5d860824 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -11,13 +11,29 @@ tags: [windows, rdp, web, desktop] Enable Remote Desktop + a web based client on Windows workspaces, powered by [devolutions-gateway](https://github.com/Devolutions/devolutions-gateway). +```tf +# AWS example. See below for examples of using this module with other providers +module "windows_rdp" { + count = data.coder_workspace.me.start_count + source = "github.com/coder/modules//windows-rdp" + agent_id = resource.coder_agent.main.id + resource_id = resource.aws_instance.dev.id +} +module "windows_rdp" { + count = data.coder_workspace.me.start_count + source = "github.com/coder/modules//windows-rdp" + agent_id = resource.coder_agent.main.id + resource_id = resource.google_compute_instance.dev[0].id +} +``` + ## Video <-- Insert demo video here --> -## Usage +## Examples -For AWS: +### With AWS ```tf module "windows_rdp" { @@ -28,7 +44,7 @@ module "windows_rdp" { } ``` -For Google Cloud: +### With Google Cloud ```tf module "windows_rdp" { @@ -39,10 +55,6 @@ module "windows_rdp" { } ``` -## Tested on - -- ✅ GCP with Windows Server 2022: [Example template](https://gist.github.com/bpmct/18918b8cab9f20295e5c4039b92b5143) - ## Roadmap - [ ] Test on Microsoft Azure. From c59eb0c0cc0093c8c3e69b22bc334c07b181af3d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 1 Jul 2024 10:22:22 -0400 Subject: [PATCH 62/72] chore: add new video to README --- windows-rdp/README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 5d860824..a0508543 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -19,17 +19,11 @@ module "windows_rdp" { agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id } -module "windows_rdp" { - count = data.coder_workspace.me.start_count - source = "github.com/coder/modules//windows-rdp" - agent_id = resource.coder_agent.main.id - resource_id = resource.google_compute_instance.dev[0].id -} ``` ## Video -<-- Insert demo video here --> +https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02 ## Examples From fd2f91c0434f69db656d20e95714e00b48b38c75 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 18:56:42 +0000 Subject: [PATCH 63/72] fix: remove commented-out code --- windows-rdp/main.tf | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 273ad20d..9de47836 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -152,26 +152,3 @@ resource "coder_app" "rdp-docs" { url = "https://coder.com/docs/v2/latest/ides/remote-desktops#rdp-desktop" external = true } - -# For some reason this is not rendering, commented out for now -# resource "coder_metadata" "rdp_details" { -# resource_id = var.resource_id -# daily_cost = 0 -# item { -# key = "Host" -# value = "localhost" -# } -# item { -# key = "Port" -# value = "3389" -# } -# item { -# key = "Username" -# value = "Administrator" -# } -# item { -# key = "Password" -# value = var.admin_password -# sensitive = true -# } -# } From b4153a6aaa5414479cde9cdc48662d6288be89ea Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 19:09:43 +0000 Subject: [PATCH 64/72] refactor: split off Windows script logic into separate file --- windows-rdp/main.tf | 97 ++------------------------ windows-rdp/windows-installation.tftpl | 88 +++++++++++++++++++++++ 2 files changed, 93 insertions(+), 92 deletions(-) create mode 100644 windows-rdp/windows-installation.tftpl diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 9de47836..cd52c675 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,99 +34,12 @@ resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "windows-rdp" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons - script = <', "$patch") | Set-Content $devolutionsHtml - } - } - - Set-AdminPassword -adminPassword "${var.admin_password}" - Configure-RDP - Install-DevolutionsGateway - Patch-Devolutions-HTML - - EOF + script = templatefile("./windows-installation.tftpl", { + CODER_USERNAME : var.admin_username, + CODER_PASSWORD : var.admin_password, + }) -run_on_start = true + run_on_start = true } resource "coder_app" "windows-rdp" { diff --git a/windows-rdp/windows-installation.tftpl b/windows-rdp/windows-installation.tftpl new file mode 100644 index 00000000..fc0404a2 --- /dev/null +++ b/windows-rdp/windows-installation.tftpl @@ -0,0 +1,88 @@ +function Set-AdminPassword { + param ( + [string]$adminPassword + ) + # Set admin password + Get-LocalUser -Name "${var.admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) + # Enable admin user + Get-LocalUser -Name "${var.admin_username}" | Enable-LocalUser +} + +function Configure-RDP { + # Enable RDP + New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -PropertyType DWORD -Force + # Disable NLA + New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -PropertyType DWORD -Force + New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -PropertyType DWORD -Force + # Enable RDP through Windows Firewall + Enable-NetFirewallRule -DisplayGroup "Remote Desktop" +} + +function Install-DevolutionsGateway { +# Define the module name and version +$moduleName = "DevolutionsGateway" +$moduleVersion = "2024.1.5" + +# Install the module with the specified version for all users +# This requires administrator privileges +try { + # Install-PackageProvider is required for AWS. Need to set command to + # terminate on failure so that try/catch actually triggers + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force +} +catch { + # If the first command failed, assume that we're on GCP and run + # Install-Module only + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force +} + +# Construct the module path for system-wide installation +$moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" +$modulePath = Join-Path -Path $moduleBasePath -ChildPath "$moduleName.psd1" + +# Import the module using the full path +Import-Module $modulePath +Install-DGatewayPackage + +# Configure Devolutions Gateway +$Hostname = "localhost" +$HttpListener = New-DGatewayListener 'http://*:7171' 'http://*:7171' +$WebApp = New-DGatewayWebAppConfig -Enabled $true -Authentication None +$ConfigParams = @{ + Hostname = $Hostname + Listeners = @($HttpListener) + WebApp = $WebApp +} +Set-DGatewayConfig @ConfigParams +New-DGatewayProvisionerKeyPair -Force + +# Configure and start the Windows service +Set-Service 'DevolutionsGateway' -StartupType 'Automatic' +Start-Service 'DevolutionsGateway' +} + +function Patch-Devolutions-HTML { +$root = "C:\Program Files\Devolutions\Gateway\webapp\client" +$devolutionsHtml = "$root\index.html" +$patch = '' + +# Always copy the file in case we change it. +@' +${templatefile("${path.module}/devolutions-patch.js", { +CODER_USERNAME : var.admin_username, +CODER_PASSWORD : var.admin_password, +})} +'@ | Set-Content "$root\coder.js" + +# Only inject the src if we have not before. +$isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" -SimpleMatch +if ($isPatched -eq $null) { + (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml +} +} + +Set-AdminPassword -adminPassword "${var.admin_password}" +Configure-RDP +Install-DevolutionsGateway +Patch-Devolutions-HTML From 49f060549ee48b0e305b7b2039fa52d8c6cc730e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 19:14:05 +0000 Subject: [PATCH 65/72] fix: update TF import --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index cd52c675..cccaf85f 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,7 +34,7 @@ resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "windows-rdp" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons - script = templatefile("./windows-installation.tftpl", { + script = templatefile("${path.module}/./windows-installation.tftpl", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, }) From a8580fe6b92389f2e985136301b83a167ebfe826 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 19:24:47 +0000 Subject: [PATCH 66/72] fix: update object definition for top-level templatefile --- windows-rdp/main.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index cccaf85f..06f2c175 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,9 +34,9 @@ resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "windows-rdp" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons - script = templatefile("${path.module}/./windows-installation.tftpl", { - CODER_USERNAME : var.admin_username, - CODER_PASSWORD : var.admin_password, + script = templatefile("${path.module}/windows-installation.tftpl", { + CODER_USERNAME = var.admin_username, + CODER_PASSWORD = var.admin_password, }) run_on_start = true From b23d85327ceb56707bc2a62cf3ce8dc488484c31 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 20:11:40 +0000 Subject: [PATCH 67/72] refactor: try extracting main script into separate template file --- windows-rdp/main.tf | 9 +++++++-- windows-rdp/windows-installation.tftpl | 11 ++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 06f2c175..f47e94e9 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,9 +34,14 @@ resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "windows-rdp" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons + script = templatefile("${path.module}/windows-installation.tftpl", { - CODER_USERNAME = var.admin_username, - CODER_PASSWORD = var.admin_password, + admin_username = var.admin_username + admin_password = var.admin_password + patch_file_contents = templatefile("${path.module}/devolutions-patch.js", { + CODER_USERNAME = var.admin_username + CODER_PASSWORD = var.admin_password + }) }) run_on_start = true diff --git a/windows-rdp/windows-installation.tftpl b/windows-rdp/windows-installation.tftpl index fc0404a2..1b7ab487 100644 --- a/windows-rdp/windows-installation.tftpl +++ b/windows-rdp/windows-installation.tftpl @@ -3,9 +3,9 @@ function Set-AdminPassword { [string]$adminPassword ) # Set admin password - Get-LocalUser -Name "${var.admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) + Get-LocalUser -Name "${admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) # Enable admin user - Get-LocalUser -Name "${var.admin_username}" | Enable-LocalUser + Get-LocalUser -Name "${admin_username}" | Enable-LocalUser } function Configure-RDP { @@ -69,10 +69,7 @@ $patch = '' # Always copy the file in case we change it. @' -${templatefile("${path.module}/devolutions-patch.js", { -CODER_USERNAME : var.admin_username, -CODER_PASSWORD : var.admin_password, -})} +${patch_file_contents} '@ | Set-Content "$root\coder.js" # Only inject the src if we have not before. @@ -82,7 +79,7 @@ if ($isPatched -eq $null) { } } -Set-AdminPassword -adminPassword "${var.admin_password}" +Set-AdminPassword -adminPassword "${admin_password}" Configure-RDP Install-DevolutionsGateway Patch-Devolutions-HTML From 3f8f6181e0a67115145cfd8cf00c8bc58f291600 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 20:31:43 +0000 Subject: [PATCH 68/72] refactor: clean up final code --- windows-rdp/devolutions-patch.js | 9 ++++----- windows-rdp/main.tf | 6 +++++- ...lation.tftpl => powershell-installation-script.tftpl} | 0 3 files changed, 9 insertions(+), 6 deletions(-) rename windows-rdp/{windows-installation.tftpl => powershell-installation-script.tftpl} (100%) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index a1e9da40..020a40f1 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -12,11 +12,10 @@ * - A lot of the HTML selectors in this file will look nonstandard. This is * because they are actually custom Angular components. * - It is strongly advised that you avoid template literals that use the - * placeholder syntax via the dollar sign. The Terraform script looks for - * these characters so that it can inject Coder-specific values, so any - * template literal that uses the character actually needs to double up each - * of them. There are already a few places in this file where it couldn't be - * avoided, but avoiding this as much as possible will save you some headache. + * placeholder syntax via the dollar sign. The Terraform file is treating this + * as a template file, and because it also uses a similar syntax, there's a + * risk that some values will trigger false positives. If a template literal + * must be used, be sure to use a double dollar sign to escape things. * - All the CSS should be written via custom style tags and the !important * directive (as much as that is a bad idea most of the time). We do not * control the Angular app, so we have to modify things from afar to ensure diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index f47e94e9..563e10f1 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -35,9 +35,13 @@ resource "coder_script" "windows-rdp" { display_name = "windows-rdp" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons - script = templatefile("${path.module}/windows-installation.tftpl", { + script = templatefile("${path.module}/powershell-installation-script.tftpl", { admin_username = var.admin_username admin_password = var.admin_password + + # Wanted to have this be in the powershell template file, but Terraform + # doesn't allow recursive calls to the templatefile function. Have to feed + # results of the JS template replace into the powershell template patch_file_contents = templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME = var.admin_username CODER_PASSWORD = var.admin_password diff --git a/windows-rdp/windows-installation.tftpl b/windows-rdp/powershell-installation-script.tftpl similarity index 100% rename from windows-rdp/windows-installation.tftpl rename to windows-rdp/powershell-installation-script.tftpl From 894e507bb36d9a169e4eba790d31bed168167560 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 2 Jul 2024 15:19:16 +0000 Subject: [PATCH 69/72] fix: add verison number to rdp script --- windows-rdp/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index a0508543..2ddf3f81 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -14,8 +14,9 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de ```tf # AWS example. See below for examples of using this module with other providers module "windows_rdp" { + source = "github.com/coder/modules/windows-rdp" + version = "1.0.15" count = data.coder_workspace.me.start_count - source = "github.com/coder/modules//windows-rdp" agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id } From d98bfcb20b75b5eb85b9dab87a49379fc446695c Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 2 Jul 2024 15:21:45 +0000 Subject: [PATCH 70/72] fix: add versioning to all code snippets --- windows-rdp/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 2ddf3f81..d80fc42f 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -14,8 +14,8 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de ```tf # AWS example. See below for examples of using this module with other providers module "windows_rdp" { - source = "github.com/coder/modules/windows-rdp" - version = "1.0.15" + source = "registry.coder.com/coder/module/windows-rdp" + version = "1.0.16" count = data.coder_workspace.me.start_count agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id @@ -32,8 +32,9 @@ https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a ```tf module "windows_rdp" { + source = "registry.coder.com/coder/module/windows-rdp" + version = "1.0.16" count = data.coder_workspace.me.start_count - source = "github.com/coder/modules//windows-rdp" agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id } @@ -43,8 +44,9 @@ module "windows_rdp" { ```tf module "windows_rdp" { + source = "registry.coder.com/coder/module/windows-rdp" + version = "1.0.16" count = data.coder_workspace.me.start_count - source = "github.com/coder/modules//windows-rdp" agent_id = resource.coder_agent.main.id resource_id = resource.google_compute_instance.dev[0].id } From aebdc9b434cbaf768edf29a21c67e8b442495825 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 2 Jul 2024 15:26:40 +0000 Subject: [PATCH 71/72] fix: update docs link --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 563e10f1..1308ac0b 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -71,6 +71,6 @@ resource "coder_app" "rdp-docs" { display_name = "Local RDP" slug = "rdp-docs" icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg" - url = "https://coder.com/docs/v2/latest/ides/remote-desktops#rdp-desktop" + url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop" external = true } From e8ee02c0445e7ab18a1c963f49946469b51697be Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 2 Jul 2024 16:02:50 +0000 Subject: [PATCH 72/72] fix: update URL for RDP icon --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 1308ac0b..8d874fa3 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -33,7 +33,7 @@ variable "admin_password" { resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "windows-rdp" - icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons + icon = "/icon/desktop.svg" script = templatefile("${path.module}/powershell-installation-script.tftpl", { admin_username = var.admin_username