@@ -64,42 +64,95 @@ const DECOY_HTML =
6464
6565const URL_PATTERN = / ^ h t t p s ? : \/ \/ / 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+ */
72119function doGet ( e ) {
73120 return ContentService
74121 . createTextOutput ( DECOY_HTML )
75122 . setMimeType ( ContentService . MimeType . XML ) ;
76123}
77124
125+ /** @param {RelayErrorResponse | RelaySingleResponse | RelayBatchResponse } obj */
78126function _relayResponse ( obj ) {
79127 return ContentService . createTextOutput ( JSON . stringify ( obj ) ) . setMimeType (
80128 ContentService . MimeType . JSON
81129 ) ;
82130}
83131
132+ /** @param {RelayErrorResponse } err */
84133function _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 */
91141function _respHeaders ( resp ) {
92142 return resp . getAllHeaders ?. ( ) ?? resp . getHeaders ( ) ;
93143}
94144
145+ /** @param {ClientRequest } req */
95146function _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+ */
131189function _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+ */
143209function _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+ */
185260function _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