guacamole-connection-fetch 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +130 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +18 -0
- package/src/index.ts +193 -0
- package/tsconfig.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# guacamole-connection-fetch
|
|
2
|
+
|
|
3
|
+
Isomorphic Guacamole token generation and connection helper for [encrypted JSON authentication](https://guacamole.apache.org/doc/gug/json-auth.html), written in vanilla JS. For use in Node.js, Cloudflare Workers, web browsers, or serverless environments
|
|
4
|
+
|
|
5
|
+
Requires the Fetch API (e.g. `fetch(request)`) to be available
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
```js
|
|
9
|
+
|
|
10
|
+
import { GuacamoleUserConnection } from "guacamole-connection-fetch"
|
|
11
|
+
/**
|
|
12
|
+
makes request to '/api/tokens' via fetch()
|
|
13
|
+
*/
|
|
14
|
+
async function interceptApiTokensRequest (request) {
|
|
15
|
+
// properly formed, minified JSON string as described in https://github.com/ridvanaltun/guacamole-rest-api-documentation/blob/master/docs/AUTHENTICATION.md#post-apitokens
|
|
16
|
+
const conns = {"Ubuntu origin 10.91.0.101":{"protocol":"ssh","parameters":{"dest-port":"22","hostname":"10.91.0.101","username":"UBUNTU_USERNAME","password":"UBUNTU_PASSWORD"}},"Win11 client 10.90.0.103":{"protocol":"rdp","parameters":{"ignore-cert":"true","enable-wallpaper":"true","security":"nla","username":"WINDOWS_USERNAME","password":"WINDOWS_PASSWORD","hostname":"10.90.0.103","dpi":"96","enable-font-smoothing":"true","enable-desktop-composition":"true","enable-menu-animations":"true","port":"3389"}}}
|
|
17
|
+
const res = await new GuacamoleUserConnection({
|
|
18
|
+
req: new Request("/api/tokens", request),
|
|
19
|
+
secretKey: GUAC_JSON_SECRET_KEY,
|
|
20
|
+
conns,
|
|
21
|
+
username: USER_EMAIL,
|
|
22
|
+
expires: 1990964469612,
|
|
23
|
+
headers: new Headers({
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
// if using Cloudflare Access, pass these:
|
|
26
|
+
"cf-access-client-id": CF_ACCESS_SERVICE_TOKEN_CLIENT_ID,
|
|
27
|
+
"cf-access-client-secret": CF_ACCESS_SERVICE_TOKEN_CLIENT_SECRET,
|
|
28
|
+
|
|
29
|
+
...request.headers,
|
|
30
|
+
}),
|
|
31
|
+
}).retrieveToken();
|
|
32
|
+
|
|
33
|
+
return new Response(res.body, res);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interceptApiTokensRequest(request);
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install guacamole-connection --save-dev
|
|
44
|
+
# or
|
|
45
|
+
pnpm add guacamole-connection
|
|
46
|
+
# or
|
|
47
|
+
yarn add guacamole-connection
|
|
48
|
+
```
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var str2ab = (str) => Uint8Array.from(str, (x) => x.charCodeAt(0));
|
|
3
|
+
var ab2str = (buf) => String.fromCharCode(...new Uint8Array(buf));
|
|
4
|
+
var Auth = function(secretKey, jsonMsg) {
|
|
5
|
+
const auth = {
|
|
6
|
+
secretKey,
|
|
7
|
+
jsonMsg,
|
|
8
|
+
set key(val) {
|
|
9
|
+
this.secretKey = val;
|
|
10
|
+
},
|
|
11
|
+
/**
|
|
12
|
+
* Converts a UTF-8 "hex" string to an array buffer suitable for here
|
|
13
|
+
* in SubtleCrypto
|
|
14
|
+
*/
|
|
15
|
+
get key() {
|
|
16
|
+
return new Uint8Array(
|
|
17
|
+
this.secretKey.match(/[\da-f]{2}/gi).map((h) => parseInt(h, 16))
|
|
18
|
+
);
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* @params jsonMsg - a standard Guacamole encrypted JSON object
|
|
22
|
+
* https://guacamole.apache.org/doc/gug/json-auth.html#json-format
|
|
23
|
+
*/
|
|
24
|
+
async createHmac(jsonMsg2) {
|
|
25
|
+
const key = await crypto.subtle.importKey(
|
|
26
|
+
"raw",
|
|
27
|
+
this.key,
|
|
28
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
29
|
+
false,
|
|
30
|
+
["sign"]
|
|
31
|
+
);
|
|
32
|
+
const mac = await crypto.subtle.sign(
|
|
33
|
+
"HMAC",
|
|
34
|
+
key,
|
|
35
|
+
new TextEncoder().encode(jsonMsg2)
|
|
36
|
+
);
|
|
37
|
+
return ab2str(mac);
|
|
38
|
+
},
|
|
39
|
+
/**
|
|
40
|
+
* @params sig - the result of the HMAC signature
|
|
41
|
+
* @params authString - the plaintext JSON value
|
|
42
|
+
*
|
|
43
|
+
* Returns encrypts the concatenated result of sig + authString with AES-128-CBC
|
|
44
|
+
*/
|
|
45
|
+
async encryptMessage(sig, authString) {
|
|
46
|
+
const prependedSig = str2ab(sig + authString);
|
|
47
|
+
const key = await crypto.subtle.importKey("raw", this.key, "AES-CBC", true, [
|
|
48
|
+
"encrypt",
|
|
49
|
+
"decrypt"
|
|
50
|
+
]);
|
|
51
|
+
const ivBin = new ArrayBuffer(16);
|
|
52
|
+
const cipher = await crypto.subtle.encrypt(
|
|
53
|
+
{ name: "AES-CBC", iv: ivBin },
|
|
54
|
+
key,
|
|
55
|
+
prependedSig
|
|
56
|
+
);
|
|
57
|
+
const cipherStr = ab2str(cipher);
|
|
58
|
+
return btoa(cipherStr);
|
|
59
|
+
},
|
|
60
|
+
async createToken() {
|
|
61
|
+
const signature = await this.createHmac(this.jsonMsg);
|
|
62
|
+
const encryptedSignature = await this.encryptMessage(signature, this.jsonMsg);
|
|
63
|
+
const encryptedSignatureEncoded = encodeURIComponent(encryptedSignature);
|
|
64
|
+
return encryptedSignatureEncoded;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
return auth;
|
|
68
|
+
};
|
|
69
|
+
Object.create(Auth.prototype);
|
|
70
|
+
Auth.prototype.constructor = Auth.prototype;
|
|
71
|
+
var GuacamoleUserConnection = function(args) {
|
|
72
|
+
const {
|
|
73
|
+
req,
|
|
74
|
+
secretKey,
|
|
75
|
+
conns,
|
|
76
|
+
username,
|
|
77
|
+
expires = 1990964469612,
|
|
78
|
+
headers
|
|
79
|
+
} = args;
|
|
80
|
+
this.request = req;
|
|
81
|
+
this.expires = expires;
|
|
82
|
+
this.headers;
|
|
83
|
+
this.env = typeof secretKey === "string" ? { GUAC_JSON_SECRET_KEY: secretKey } : secretKey;
|
|
84
|
+
this.username = username ? username : req.headers.has("cf-access-authenticated-user-email") ? req.headers.get("cf-access-authenticated-user-email") : this.env.LOCAL_USER ? this.env.LOCAL_USER : "user@example.com";
|
|
85
|
+
this.url = new URL(req.url);
|
|
86
|
+
this.conns = {};
|
|
87
|
+
const parsedConns = typeof conns === "string" ? JSON.parse(conns) : conns;
|
|
88
|
+
for (const name of Object.keys(parsedConns)) {
|
|
89
|
+
this.conns[name] = {
|
|
90
|
+
protocol: parsedConns[name]["protocol"],
|
|
91
|
+
parameters: parsedConns[name]["parameters"]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
this.authString = {
|
|
95
|
+
expires: this.expires,
|
|
96
|
+
username: this.username,
|
|
97
|
+
connections: this.conns
|
|
98
|
+
};
|
|
99
|
+
this.authenticatedGuacConnection = {};
|
|
100
|
+
this.auth = new Auth(
|
|
101
|
+
this.env.GUAC_JSON_SECRET_KEY,
|
|
102
|
+
JSON.stringify(this.authString)
|
|
103
|
+
);
|
|
104
|
+
this.expiration = new Date(this.expires).toLocaleDateString();
|
|
105
|
+
this.params = new URLSearchParams();
|
|
106
|
+
this.headers = new Headers(headers);
|
|
107
|
+
new Headers(this.request.headers).forEach((value, key) => {
|
|
108
|
+
this.headers.append(key, value);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
GuacamoleUserConnection.prototype = Object.create(GuacamoleUserConnection.prototype);
|
|
112
|
+
GuacamoleUserConnection.prototype.constructor = GuacamoleUserConnection;
|
|
113
|
+
GuacamoleUserConnection.prototype.retrieveToken = async function() {
|
|
114
|
+
this.authenticatedGuacConnection = await this.auth.createToken();
|
|
115
|
+
if (this.request.url.includes("/api/tokens")) {
|
|
116
|
+
return fetch(this.request.url, {
|
|
117
|
+
headers: this.headers,
|
|
118
|
+
method: this.request.method,
|
|
119
|
+
body: new URLSearchParams(`data=${await this.authenticatedGuacConnection}`)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
this.params.set("token", `${await this.authenticatedGuacConnection}`);
|
|
123
|
+
this.params.set("expiration", new Date(this.expires).toLocaleDateString());
|
|
124
|
+
this.url = new URL(this.request.url);
|
|
125
|
+
return fetch(this.url, this.request);
|
|
126
|
+
};
|
|
127
|
+
export {
|
|
128
|
+
GuacamoleUserConnection
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/* String to ArrayBuffer */\nconst str2ab = (str: string): ArrayBufferView<ArrayBuffer> =>\n Uint8Array.from(str, (x) => x.charCodeAt(0));\n\n/* ArrayBuffer to String */\nconst ab2str = (buf: ArrayBuffer): string =>\n String.fromCharCode(...new Uint8Array(buf));\n\ninterface Env {\n GUAC_JSON_SECRET_KEY: string;\n LOCAL_USER?: string;\n}\n\ninterface GuacConnection {\n protocol: string;\n parameters: Record<string, string>;\n}\n\ntype GuacConnections = Record<string, GuacConnection>;\n\nconst Auth = function (secretKey: string, jsonMsg: string) {\n // if (typeof jsonMsg !== 'string') jsonMsg = JSON.stringify(jsonMsg);\n const auth = {\n secretKey,\n jsonMsg,\n\n set key(val: string) {\n this.secretKey = val;\n },\n\n /**\n * Converts a UTF-8 \"hex\" string to an array buffer suitable for here\n * in SubtleCrypto\n */\n get key(): Uint8Array {\n return new Uint8Array(\n this.secretKey.match(/[\\da-f]{2}/gi)!.map((h) => parseInt(h, 16))\n );\n },\n\n /**\n * @params jsonMsg - a standard Guacamole encrypted JSON object\n * https://guacamole.apache.org/doc/gug/json-auth.html#json-format\n */\n async createHmac(jsonMsg: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n this.key,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n\n const mac = await crypto.subtle.sign(\n \"HMAC\",\n key,\n new TextEncoder().encode(jsonMsg)\n );\n return ab2str(mac);\n },\n\n /**\n * @params sig - the result of the HMAC signature\n * @params authString - the plaintext JSON value\n *\n * Returns encrypts the concatenated result of sig + authString with AES-128-CBC\n */\n async encryptMessage(sig: string, authString: string): Promise<string> {\n const prependedSig = str2ab(sig + authString);\n const key = await crypto.subtle.importKey(\"raw\", this.key, \"AES-CBC\", true, [\n \"encrypt\",\n \"decrypt\",\n ]);\n const ivBin = new ArrayBuffer(16);\n const cipher = await crypto.subtle.encrypt(\n { name: \"AES-CBC\", iv: ivBin },\n key,\n prependedSig\n );\n const cipherStr = ab2str(cipher);\n return btoa(cipherStr);\n },\n\n async createToken(): Promise<string> {\n const signature = await this.createHmac(this.jsonMsg);\n const encryptedSignature = await this.encryptMessage(signature, this.jsonMsg);\n const encryptedSignatureEncoded = encodeURIComponent(encryptedSignature);\n return encryptedSignatureEncoded\n },\n };\n return auth;\n};\n\nObject.create(Auth.prototype);\nAuth.prototype.constructor = Auth.prototype;\n\n/**\n * @params request - the request object passed by CF workers\n * @params env - the env object passed by CF workers\n * @params conns - a 'connection' object or array of 'connection' objects conforming to\n * Guacamole's JSON spec: https://guacamole.apache.org/doc/gug/json-auth.html#json-format\n * @params expires - the number of days until token expires\n *\n * Returns a minified JSON string suitable for signing and encryption\n */\n// Create a constructor function\ninterface UserConnectionArgs {\n req: Request;\n secretKey: Env | string;\n conns: string | GuacConnections;\n username?: string;\n expires?: number;\n headers?: Headers;\n}\nconst GuacamoleUserConnection = function (this: any, args: UserConnectionArgs) {\n const {\n req,\n secretKey,\n conns,\n username,\n expires = 1990964469612,\n headers,\n } = args;\n\n this.request = req;\n this.expires = expires;\n this.headers \n this.env =\n typeof secretKey === \"string\"\n ? { GUAC_JSON_SECRET_KEY: secretKey }\n : secretKey;\n this.username = username\n ? username\n : req.headers.has(\"cf-access-authenticated-user-email\")\n ? (req.headers.get(\"cf-access-authenticated-user-email\") as string)\n : this.env.LOCAL_USER\n ? this.env.LOCAL_USER\n : \"user@example.com\";\n\n this.url = new URL(req.url);\n\n this.conns = {};\n\n const parsedConns: GuacConnections =\n typeof conns === \"string\" ? (JSON.parse(conns) as GuacConnections) : conns;\n\n for (const name of Object.keys(parsedConns)) {\n this.conns[name] = {\n protocol: parsedConns[name][\"protocol\"],\n parameters: parsedConns[name][\"parameters\"],\n };\n }\n\n this.authString = {\n expires: this.expires,\n username: this.username,\n connections: this.conns,\n };\n\n this.authenticatedGuacConnection = {};\n\n this.auth = new (Auth as any)(\n this.env.GUAC_JSON_SECRET_KEY,\n JSON.stringify(this.authString)\n );\n this.expiration = new Date(this.expires).toLocaleDateString();\n this.params = new URLSearchParams();\n this.headers = new Headers(headers)\n new Headers(this.request.headers).forEach((value, key) => {\n this.headers.append(key, value);\n });\n} as any;\n\nGuacamoleUserConnection.prototype = Object.create(GuacamoleUserConnection.prototype);\nGuacamoleUserConnection.prototype.constructor = GuacamoleUserConnection;\n\nGuacamoleUserConnection.prototype.retrieveToken = async function () {\n\n this.authenticatedGuacConnection = await this.auth.createToken();\n if (this.request.url.includes(\"/api/tokens\")) {\n return fetch(this.request.url, {\n headers: this.headers,\n method: this.request.method,\n body: new URLSearchParams(`data=${await this.authenticatedGuacConnection}`),\n });\n }\n this.params.set(\"token\", `${await this.authenticatedGuacConnection}`);\n this.params.set(\"expiration\", new Date(this.expires).toLocaleDateString());\n this.url = new URL(this.request.url);\n return fetch(this.url, this.request);\n};\n\nexport { GuacamoleUserConnection };"],"mappings":"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","names":["jsonMsg"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "guacamole-connection-fetch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "npx tsup src/index.ts --format esm --dts --sourcemap --clean",
|
|
8
|
+
"build:esbuild": "npx esbuild src/index.ts --bundle --platform=browser --format=esm --outfile=dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "shagamemnon",
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"esbuild": "^0.27.2",
|
|
15
|
+
"tsup": "^8.5.1",
|
|
16
|
+
"typescript": "^5.9.3"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/* String to ArrayBuffer */
|
|
2
|
+
const str2ab = (str: string): ArrayBufferView<ArrayBuffer> =>
|
|
3
|
+
Uint8Array.from(str, (x) => x.charCodeAt(0));
|
|
4
|
+
|
|
5
|
+
/* ArrayBuffer to String */
|
|
6
|
+
const ab2str = (buf: ArrayBuffer): string =>
|
|
7
|
+
String.fromCharCode(...new Uint8Array(buf));
|
|
8
|
+
|
|
9
|
+
interface Env {
|
|
10
|
+
GUAC_JSON_SECRET_KEY: string;
|
|
11
|
+
LOCAL_USER?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface GuacConnection {
|
|
15
|
+
protocol: string;
|
|
16
|
+
parameters: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type GuacConnections = Record<string, GuacConnection>;
|
|
20
|
+
|
|
21
|
+
const Auth = function (secretKey: string, jsonMsg: string) {
|
|
22
|
+
// if (typeof jsonMsg !== 'string') jsonMsg = JSON.stringify(jsonMsg);
|
|
23
|
+
const auth = {
|
|
24
|
+
secretKey,
|
|
25
|
+
jsonMsg,
|
|
26
|
+
|
|
27
|
+
set key(val: string) {
|
|
28
|
+
this.secretKey = val;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Converts a UTF-8 "hex" string to an array buffer suitable for here
|
|
33
|
+
* in SubtleCrypto
|
|
34
|
+
*/
|
|
35
|
+
get key(): Uint8Array {
|
|
36
|
+
return new Uint8Array(
|
|
37
|
+
this.secretKey.match(/[\da-f]{2}/gi)!.map((h) => parseInt(h, 16))
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @params jsonMsg - a standard Guacamole encrypted JSON object
|
|
43
|
+
* https://guacamole.apache.org/doc/gug/json-auth.html#json-format
|
|
44
|
+
*/
|
|
45
|
+
async createHmac(jsonMsg: string): Promise<string> {
|
|
46
|
+
const key = await crypto.subtle.importKey(
|
|
47
|
+
"raw",
|
|
48
|
+
this.key,
|
|
49
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
50
|
+
false,
|
|
51
|
+
["sign"]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const mac = await crypto.subtle.sign(
|
|
55
|
+
"HMAC",
|
|
56
|
+
key,
|
|
57
|
+
new TextEncoder().encode(jsonMsg)
|
|
58
|
+
);
|
|
59
|
+
return ab2str(mac);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @params sig - the result of the HMAC signature
|
|
64
|
+
* @params authString - the plaintext JSON value
|
|
65
|
+
*
|
|
66
|
+
* Returns encrypts the concatenated result of sig + authString with AES-128-CBC
|
|
67
|
+
*/
|
|
68
|
+
async encryptMessage(sig: string, authString: string): Promise<string> {
|
|
69
|
+
const prependedSig = str2ab(sig + authString);
|
|
70
|
+
const key = await crypto.subtle.importKey("raw", this.key, "AES-CBC", true, [
|
|
71
|
+
"encrypt",
|
|
72
|
+
"decrypt",
|
|
73
|
+
]);
|
|
74
|
+
const ivBin = new ArrayBuffer(16);
|
|
75
|
+
const cipher = await crypto.subtle.encrypt(
|
|
76
|
+
{ name: "AES-CBC", iv: ivBin },
|
|
77
|
+
key,
|
|
78
|
+
prependedSig
|
|
79
|
+
);
|
|
80
|
+
const cipherStr = ab2str(cipher);
|
|
81
|
+
return btoa(cipherStr);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async createToken(): Promise<string> {
|
|
85
|
+
const signature = await this.createHmac(this.jsonMsg);
|
|
86
|
+
const encryptedSignature = await this.encryptMessage(signature, this.jsonMsg);
|
|
87
|
+
const encryptedSignatureEncoded = encodeURIComponent(encryptedSignature);
|
|
88
|
+
return encryptedSignatureEncoded
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
return auth;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
Object.create(Auth.prototype);
|
|
95
|
+
Auth.prototype.constructor = Auth.prototype;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @params request - the request object passed by CF workers
|
|
99
|
+
* @params env - the env object passed by CF workers
|
|
100
|
+
* @params conns - a 'connection' object or array of 'connection' objects conforming to
|
|
101
|
+
* Guacamole's JSON spec: https://guacamole.apache.org/doc/gug/json-auth.html#json-format
|
|
102
|
+
* @params expires - the number of days until token expires
|
|
103
|
+
*
|
|
104
|
+
* Returns a minified JSON string suitable for signing and encryption
|
|
105
|
+
*/
|
|
106
|
+
// Create a constructor function
|
|
107
|
+
interface UserConnectionArgs {
|
|
108
|
+
req: Request;
|
|
109
|
+
secretKey: Env | string;
|
|
110
|
+
conns: string | GuacConnections;
|
|
111
|
+
username?: string;
|
|
112
|
+
expires?: number;
|
|
113
|
+
headers?: Headers;
|
|
114
|
+
}
|
|
115
|
+
const GuacamoleUserConnection = function (this: any, args: UserConnectionArgs) {
|
|
116
|
+
const {
|
|
117
|
+
req,
|
|
118
|
+
secretKey,
|
|
119
|
+
conns,
|
|
120
|
+
username,
|
|
121
|
+
expires = 1990964469612,
|
|
122
|
+
headers,
|
|
123
|
+
} = args;
|
|
124
|
+
|
|
125
|
+
this.request = req;
|
|
126
|
+
this.expires = expires;
|
|
127
|
+
this.headers
|
|
128
|
+
this.env =
|
|
129
|
+
typeof secretKey === "string"
|
|
130
|
+
? { GUAC_JSON_SECRET_KEY: secretKey }
|
|
131
|
+
: secretKey;
|
|
132
|
+
this.username = username
|
|
133
|
+
? username
|
|
134
|
+
: req.headers.has("cf-access-authenticated-user-email")
|
|
135
|
+
? (req.headers.get("cf-access-authenticated-user-email") as string)
|
|
136
|
+
: this.env.LOCAL_USER
|
|
137
|
+
? this.env.LOCAL_USER
|
|
138
|
+
: "user@example.com";
|
|
139
|
+
|
|
140
|
+
this.url = new URL(req.url);
|
|
141
|
+
|
|
142
|
+
this.conns = {};
|
|
143
|
+
|
|
144
|
+
const parsedConns: GuacConnections =
|
|
145
|
+
typeof conns === "string" ? (JSON.parse(conns) as GuacConnections) : conns;
|
|
146
|
+
|
|
147
|
+
for (const name of Object.keys(parsedConns)) {
|
|
148
|
+
this.conns[name] = {
|
|
149
|
+
protocol: parsedConns[name]["protocol"],
|
|
150
|
+
parameters: parsedConns[name]["parameters"],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.authString = {
|
|
155
|
+
expires: this.expires,
|
|
156
|
+
username: this.username,
|
|
157
|
+
connections: this.conns,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
this.authenticatedGuacConnection = {};
|
|
161
|
+
|
|
162
|
+
this.auth = new (Auth as any)(
|
|
163
|
+
this.env.GUAC_JSON_SECRET_KEY,
|
|
164
|
+
JSON.stringify(this.authString)
|
|
165
|
+
);
|
|
166
|
+
this.expiration = new Date(this.expires).toLocaleDateString();
|
|
167
|
+
this.params = new URLSearchParams();
|
|
168
|
+
this.headers = new Headers(headers)
|
|
169
|
+
new Headers(this.request.headers).forEach((value, key) => {
|
|
170
|
+
this.headers.append(key, value);
|
|
171
|
+
});
|
|
172
|
+
} as any;
|
|
173
|
+
|
|
174
|
+
GuacamoleUserConnection.prototype = Object.create(GuacamoleUserConnection.prototype);
|
|
175
|
+
GuacamoleUserConnection.prototype.constructor = GuacamoleUserConnection;
|
|
176
|
+
|
|
177
|
+
GuacamoleUserConnection.prototype.retrieveToken = async function () {
|
|
178
|
+
|
|
179
|
+
this.authenticatedGuacConnection = await this.auth.createToken();
|
|
180
|
+
if (this.request.url.includes("/api/tokens")) {
|
|
181
|
+
return fetch(this.request.url, {
|
|
182
|
+
headers: this.headers,
|
|
183
|
+
method: this.request.method,
|
|
184
|
+
body: new URLSearchParams(`data=${await this.authenticatedGuacConnection}`),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
this.params.set("token", `${await this.authenticatedGuacConnection}`);
|
|
188
|
+
this.params.set("expiration", new Date(this.expires).toLocaleDateString());
|
|
189
|
+
this.url = new URL(this.request.url);
|
|
190
|
+
return fetch(this.url, this.request);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export { GuacamoleUserConnection };
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": false,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"src"
|
|
17
|
+
]
|
|
18
|
+
}
|