Skip to content

Commit 48317c7

Browse files
committed
refactor(code.gs): Add type annotation, logging, safety check
1 parent 68241ed commit 48317c7

1 file changed

Lines changed: 110 additions & 23 deletions

File tree

assets/apps_script/Code.gs

Lines changed: 110 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,42 +64,95 @@ const DECOY_HTML =
6464

6565
const URL_PATTERN = /^https?:\/\//i;
6666

67-
// `doGet` is what active scanners hit first (HTTP GET probes are cheaper
68-
// than POSTs). Apps Script defaults to a "Script function not found" page
69-
// here which is a fine-enough decoy on its own, but explicitly returning
70-
// the same harmless placeholder makes the response identical to the
71-
// bad-auth POST decoy — one less fingerprint vector.
67+
/**
68+
* @typedef {Object} ClientRequest
69+
* @property {GoogleAppsScript.URL_Fetch.HttpMethod} m
70+
* @property {string} u - URL to relay
71+
* @property {GoogleAppsScript.URL_Fetch.HttpHeaders} h
72+
* @property {string} b - Request body
73+
* @property {string} ct - Request contentType
74+
* @property {boolean} r - Request goes through exit node
75+
*/
76+
77+
/**
78+
* @typedef {Object} ClientRequestSingle
79+
* @property {string} k - Auth token
80+
* @property {GoogleAppsScript.URL_Fetch.HttpMethod} m
81+
* @property {string} u - URL to relay
82+
* @property {GoogleAppsScript.URL_Fetch.HttpHeaders} h
83+
* @property {string} b - Request body
84+
* @property {string} ct - Request contentType
85+
* @property {boolean} r - Request goes through exit node
86+
*/
87+
88+
/**
89+
* @typedef {Object} ClientRequestBatch
90+
* @property {string} k
91+
* @property {Array<ClientRequest>} q
92+
*/
93+
94+
/**
95+
* @typedef RelayErrorResponse
96+
* @property {string} e
97+
*/
98+
99+
/**
100+
* @typedef RelaySingleResponse
101+
* @property {number} s
102+
* @property {GoogleAppsScript.URL_Fetch.HttpHeaders} h
103+
* @property {string} b
104+
*/
105+
106+
/**
107+
* @typedef RelayBatchResponse
108+
* @property {Array<RelaySingleResponse | RelayErrorResponse>} q
109+
*/
110+
111+
/**
112+
* `doGet` is what active scanners hit first (HTTP GET probes are cheaper
113+
* than POSTs). Apps Script defaults to a "Script function not found" page
114+
* here which is a fine-enough decoy on its own, but explicitly returning
115+
* the same harmless placeholder makes the response identical to the
116+
* bad-auth POST decoy — one less fingerprint vector.
117+
* @param {GoogleAppsScript.Events.DoGet} e
118+
*/
72119
function doGet (e) {
73120
return ContentService
74121
.createTextOutput(DECOY_HTML)
75122
.setMimeType(ContentService.MimeType.XML);
76123
}
77124

125+
/** @param {RelayErrorResponse | RelaySingleResponse | RelayBatchResponse} obj */
78126
function _relayResponse (obj) {
79127
return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(
80128
ContentService.MimeType.JSON
81129
);
82130
}
83131

132+
/** @param {RelayErrorResponse} err */
84133
function _decoyOrError (err) {
85134
if (DIAGNOSTIC_MODE) return _relayResponse(err);
86135
return ContentService
87136
.createTextOutput(DECOY_HTML)
88137
.setMimeType(ContentService.MimeType.XML);
89138
}
90139

140+
/** @param {GoogleAppsScript.URL_Fetch.HTTPResponse} resp */
91141
function _respHeaders (resp) {
92142
return resp.getAllHeaders?.() ?? resp.getHeaders();
93143
}
94144

145+
/** @param {ClientRequest} req */
95146
function _buildOpts (req) {
96147
const method = (req.m?.toLowerCase?.() ?? "get");
97148

98149
if (!VALID_METHODS.has(method)) {
99150
throw new Error(`Invalid HTTP method: ${ method }`);
100151
}
101152

153+
/** @type {GoogleAppsScript.URL_Fetch.URLFetchRequestOptions} */
102154
let opts = {
155+
/** @type {GoogleAppsScript.URL_Fetch.HttpMethod} */
103156
method: method,
104157
muteHttpExceptions: true,
105158
followRedirects: true, // ← always true; r flag now has different meaning
@@ -115,11 +168,12 @@ function _buildOpts (req) {
115168
if (typeof req.b !== "string") {
116169
throw new Error("Payload must be string (base64)");
117170
}
118-
if (req.b.length > 50000000) {
119-
throw new Error("Payload exceeds 50MB limit");
120-
}
121171
try {
122-
opts.payload = Utilities.base64Decode(req.b);
172+
const decoded = Utilities.base64Decode(req.b);
173+
if (decoded.length > 50000000) {
174+
throw new Error("Payload exceeds 50MB limit");
175+
}
176+
opts.payload = decoded;
123177
} catch (decodeErr) {
124178
throw new Error(`Base64 decode failed: ${ String(decodeErr) }`);
125179
}
@@ -128,23 +182,38 @@ function _buildOpts (req) {
128182
return opts;
129183
}
130184

185+
/**
186+
* @param {GoogleAppsScript.URL_Fetch.HTTPResponse} resp
187+
* @return {RelaySingleResponse}
188+
*/
131189
function _buildResponse (resp) {
132-
let respContent = resp.getContent();
133-
if (respContent && respContent.length > 50000000) {
134-
throw new Error("Payload exceeds 50MB limit");
190+
try {
191+
let respContentB64 = Utilities.base64Encode(resp.getContent());
192+
if (respContentB64.length > 50000000) {
193+
throw new Error("Payload exceeds 50MB limit");
194+
}
195+
return {
196+
s: resp.getResponseCode(),
197+
h: _respHeaders(resp),
198+
b: respContentB64,
199+
};
200+
} catch (encodeErr) {
201+
throw new Error(`Base64 encode failed: ${ String(encodeErr) }`);
135202
}
136-
return {
137-
s: resp.getResponseCode(),
138-
h: _respHeaders(resp),
139-
b: Utilities.base64Encode(respContent),
140-
};
141203
}
142204

205+
/**
206+
* @param {ClientRequestSingle} req
207+
* @return {GoogleAppsScript.Content.TextOutput}
208+
*/
143209
function _doSingle (req) {
144210
if (!req.u || typeof req.u !== "string" || !URL_PATTERN.test(req.u)) {
145-
return _relayResponse({ e: "bad url" });
211+
const errMsg = "invalid url: " + String(req.u);
212+
Logger.log(`[ERROR] _doSingle: ${ errMsg }`);
213+
return _relayResponse({ e: errMsg });
146214
}
147215

216+
148217
// ── Normal relay ────────
149218
// Wrap the fetch + body encode in try/catch so any failure surfaces as
150219
// a JSON error envelope the Rust client can parse. Without this, throws
@@ -178,10 +247,16 @@ function _doSingle (req) {
178247

179248
return _relayResponse(_buildResponse(resp));
180249
} catch (err) {
181-
return _relayResponse({ e: "fetch failed: " + String(err) });
250+
const errMsg = `fetch failed: ${ String(err) }`;
251+
Logger.log(`[ERROR] _doSingle(${ req.u }): ${ errMsg }`);
252+
return _relayResponse({ e: errMsg });
182253
}
183254
}
184255

256+
/**
257+
* @param {Array<ClientRequest>} items
258+
* @returns {GoogleAppsScript.Content.TextOutput}
259+
*/
185260
function _doBatch (items) {
186261
let fetchItems = [];
187262
let fetchIndices = [];
@@ -195,15 +270,19 @@ function _doBatch (items) {
195270
continue;
196271
}
197272
if (!item.u || typeof item.u !== "string" || !URL_PATTERN.test(item.u)) {
198-
errorMap.set(i, "bad url");
273+
const errMsg = `bad url: ${ String(item.u) }`;
274+
Logger.log(`[ERROR] _doBatch[${ i }]: ${ errMsg }`);
275+
errorMap.set(i, errMsg);
199276
continue;
200277
}
201278
try {
202279
fetchItems.push({ url: item.u, ..._buildOpts(item) });
203280
fetchIndices.push(i);
204281
fetchMethods.push((item.m?.toLowerCase?.() ?? "get"));
205282
} catch (buildErr) {
206-
errorMap.set(i, String(buildErr));
283+
const errMsg = String(buildErr);
284+
Logger.log(`[ERROR] _doBatch[${ i }] _buildOpts: ${ errMsg }`);
285+
errorMap.set(i, errMsg);
207286
}
208287
}
209288

@@ -234,6 +313,7 @@ function _doBatch (items) {
234313
responses[ j ] = UrlFetchApp.fetch(fallbackReq.url, fallbackReq);
235314
} catch (singleErr) {
236315
const singleErrMsg = String(singleErr);
316+
Logger.log(`[ERROR] _doBatch (fetching single item): ${ fetchIndices[ j ] }`);
237317
errorMap.set(fetchIndices[ j ], singleErrMsg);
238318
responses[ j ] = null;
239319
}
@@ -266,8 +346,12 @@ function doPost (e) {
266346
return _decoyOrError({ e: "AUTH_KEY not configured" });
267347
}
268348
try {
349+
/** @type {ClientRequestSingle | ClientRequestBatch} **/
269350
let req = JSON.parse(e.postData.contents);
270-
if (req.k !== AUTH_KEY) return _decoyOrError({ e: "unauthorized" });
351+
if (req.k !== AUTH_KEY) {
352+
Logger.log("[WARN] doPost: unauthorized attempt");
353+
return _decoyOrError({ e: "unauthorized" });
354+
}
271355

272356
// Batch mode: { k, q: [...] }
273357
if ("q" in req) {
@@ -279,6 +363,9 @@ function doPost (e) {
279363
} catch (err) {
280364
// Parse failures of the request body are also probe-shaped — a real
281365
// mhrv-rs client never sends invalid JSON. Decoy for the same reason.
282-
return _decoyOrError({ e: String(err) });
366+
367+
const errMsg = String(err);
368+
Logger.log(`[ERROR] doPost parse: ${ errMsg }`);
369+
return _decoyOrError({ e: errMsg });
283370
}
284371
}

0 commit comments

Comments
 (0)