herdr-remote-relay 0.2.10 → 0.2.12
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/package.json +1 -1
- package/src/relay-config.js +4 -1
- package/src/relay-server.js +132 -0
- package/web/dist/assets/index-B2osAs3a.js +41 -0
- package/web/dist/assets/index-BYOV8phW.css +32 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Kt_aycdH.css +0 -32
- package/web/dist/assets/index-N8_SLf_4.js +0 -40
package/package.json
CHANGED
package/src/relay-config.js
CHANGED
|
@@ -31,7 +31,10 @@ const DEFAULTS = {
|
|
|
31
31
|
host: '127.0.0.1',
|
|
32
32
|
port: 8787,
|
|
33
33
|
publicUrl: 'http://127.0.0.1:8787',
|
|
34
|
-
|
|
34
|
+
// Sized for a pasted image, not for keystrokes. `ws` closes a connection
|
|
35
|
+
// that receives an oversized frame (1009) rather than dropping the frame,
|
|
36
|
+
// so this ceiling is what a client's own upload cap has to stay under.
|
|
37
|
+
maxPayloadBytes: 5 * 1024 * 1024,
|
|
35
38
|
maxClientsPerHost: 16,
|
|
36
39
|
maxHosts: 1024,
|
|
37
40
|
maxPendingHandshakes: 1024,
|
package/src/relay-server.js
CHANGED
|
@@ -71,6 +71,57 @@ function tokenMatches(candidate, expected) {
|
|
|
71
71
|
return crypto.timingSafeEqual(candidateHash, expectedHash);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function verifyImageMagicBytes(mime, dataBase64) {
|
|
75
|
+
if (typeof dataBase64 !== 'string' || dataBase64.length === 0) return false;
|
|
76
|
+
let headerBuf;
|
|
77
|
+
try {
|
|
78
|
+
headerBuf = Buffer.from(dataBase64.slice(0, 32), 'base64');
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (headerBuf.length === 0) return false;
|
|
83
|
+
|
|
84
|
+
switch (mime) {
|
|
85
|
+
case 'image/png':
|
|
86
|
+
return (
|
|
87
|
+
headerBuf.length >= 8 &&
|
|
88
|
+
headerBuf[0] === 0x89 &&
|
|
89
|
+
headerBuf[1] === 0x50 &&
|
|
90
|
+
headerBuf[2] === 0x4e &&
|
|
91
|
+
headerBuf[3] === 0x47
|
|
92
|
+
);
|
|
93
|
+
case 'image/jpeg':
|
|
94
|
+
return (
|
|
95
|
+
headerBuf.length >= 3 &&
|
|
96
|
+
headerBuf[0] === 0xff &&
|
|
97
|
+
headerBuf[1] === 0xd8 &&
|
|
98
|
+
headerBuf[2] === 0xff
|
|
99
|
+
);
|
|
100
|
+
case 'image/webp':
|
|
101
|
+
return (
|
|
102
|
+
headerBuf.length >= 12 &&
|
|
103
|
+
headerBuf[0] === 0x52 &&
|
|
104
|
+
headerBuf[1] === 0x49 &&
|
|
105
|
+
headerBuf[2] === 0x46 &&
|
|
106
|
+
headerBuf[3] === 0x46 &&
|
|
107
|
+
headerBuf[8] === 0x57 &&
|
|
108
|
+
headerBuf[9] === 0x45 &&
|
|
109
|
+
headerBuf[10] === 0x42 &&
|
|
110
|
+
headerBuf[11] === 0x50
|
|
111
|
+
);
|
|
112
|
+
case 'image/gif':
|
|
113
|
+
return (
|
|
114
|
+
headerBuf.length >= 6 &&
|
|
115
|
+
headerBuf[0] === 0x47 &&
|
|
116
|
+
headerBuf[1] === 0x49 &&
|
|
117
|
+
headerBuf[2] === 0x46 &&
|
|
118
|
+
headerBuf[3] === 0x38
|
|
119
|
+
);
|
|
120
|
+
default:
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
74
125
|
class RelayServer {
|
|
75
126
|
constructor(config = loadRelayConfig().config, options = {}) {
|
|
76
127
|
this.config = {
|
|
@@ -707,6 +758,11 @@ class RelayServer {
|
|
|
707
758
|
client.session = null;
|
|
708
759
|
}
|
|
709
760
|
this.detachClient(client, { notify: false });
|
|
761
|
+
} else if (message.type === 'paste_file_ready') {
|
|
762
|
+
jsonSend(client.ws, {
|
|
763
|
+
type: 'paste_file_ready',
|
|
764
|
+
path: message.path,
|
|
765
|
+
});
|
|
710
766
|
} else if (message.type === 'error') {
|
|
711
767
|
jsonSend(client.ws, {
|
|
712
768
|
type: 'error',
|
|
@@ -948,6 +1004,82 @@ class RelayServer {
|
|
|
948
1004
|
// so beats a silence an older client would wait on.
|
|
949
1005
|
jsonSend(client.ws, { type: 'control_state', role: 'controller', controllerId: null });
|
|
950
1006
|
}
|
|
1007
|
+
else if (message.type === 'paste_file') {
|
|
1008
|
+
if (client.role === 'viewer') {
|
|
1009
|
+
jsonSend(client.ws, {
|
|
1010
|
+
type: 'error',
|
|
1011
|
+
code: 'viewer_mode',
|
|
1012
|
+
message: 'Viewer mode cannot paste to terminal',
|
|
1013
|
+
});
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const allowedMimes = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
|
|
1017
|
+
if (typeof message.mime !== 'string' || !allowedMimes.has(message.mime)) {
|
|
1018
|
+
jsonSend(client.ws, {
|
|
1019
|
+
type: 'error',
|
|
1020
|
+
code: 'paste_file_unsupported',
|
|
1021
|
+
message: 'Unsupported paste image format',
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (typeof message.dataBase64 !== 'string') {
|
|
1026
|
+
jsonSend(client.ws, {
|
|
1027
|
+
type: 'error',
|
|
1028
|
+
code: 'paste_file_unsupported',
|
|
1029
|
+
message: 'Invalid paste image payload',
|
|
1030
|
+
});
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const MAX_PASTE_BYTES = 3 * 1024 * 1024;
|
|
1034
|
+
const rawLength = Buffer.byteLength(message.dataBase64, 'base64');
|
|
1035
|
+
if (rawLength === 0) {
|
|
1036
|
+
jsonSend(client.ws, {
|
|
1037
|
+
type: 'error',
|
|
1038
|
+
code: 'paste_file_empty',
|
|
1039
|
+
message: 'Pasted image payload is empty',
|
|
1040
|
+
});
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (rawLength > MAX_PASTE_BYTES) {
|
|
1044
|
+
jsonSend(client.ws, {
|
|
1045
|
+
type: 'error',
|
|
1046
|
+
code: 'paste_file_too_large',
|
|
1047
|
+
message: 'Pasted image exceeds 3 MB limit',
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (!verifyImageMagicBytes(message.mime, message.dataBase64)) {
|
|
1052
|
+
jsonSend(client.ws, {
|
|
1053
|
+
type: 'error',
|
|
1054
|
+
code: 'paste_file_unsupported',
|
|
1055
|
+
message: 'Pasted image content does not match declared MIME type',
|
|
1056
|
+
});
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (!isOpen(host?.ws)) {
|
|
1060
|
+
jsonSend(client.ws, {
|
|
1061
|
+
type: 'error',
|
|
1062
|
+
code: 'host_offline',
|
|
1063
|
+
message: 'Host is offline or disconnected',
|
|
1064
|
+
});
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (!client.session) {
|
|
1068
|
+
jsonSend(client.ws, {
|
|
1069
|
+
type: 'error',
|
|
1070
|
+
code: 'no_session',
|
|
1071
|
+
message: 'Terminal session is not ready',
|
|
1072
|
+
});
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
jsonSend(host.ws, {
|
|
1076
|
+
type: 'paste_file',
|
|
1077
|
+
clientId: client.session.streamId,
|
|
1078
|
+
streamId: client.session.streamId,
|
|
1079
|
+
mime: message.mime,
|
|
1080
|
+
dataBase64: message.dataBase64,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
951
1083
|
}
|
|
952
1084
|
|
|
953
1085
|
/**
|