MiddleMan Sandbox

A test script for the middleman library. See https://gf.qytechs.cn/en/scripts/472943-itsnotlupus-middleman for details.

  1. // ==UserScript==
  2. // @name MiddleMan Sandbox
  3. // @namespace Itsnotlupus Industries
  4. // @description A test script for the middleman library. See https://gf.qytechs.cn/en/scripts/472943-itsnotlupus-middleman for details.
  5. // @author Itsnotlupus
  6. // @version 1.4.1
  7. // @license MIT
  8. // @run-at document-start
  9. // @match *://*/*
  10. // @require https://gf.qytechs.cn/scripts/468394-itsnotlupus-tiny-utilities/code/utils.js
  11. // @require https://gf.qytechs.cn/scripts/472943-itsnotlupus-middleman/code/middleman.js
  12. // @grant unsafeWindow
  13. // ==/UserScript==
  14.  
  15. // standard Web APIs eshint doesn't know about
  16. /* global DecompressionStream */
  17. // things defined from @require scripts
  18. /* global crel, logGroup, middleMan */
  19.  
  20. // If you see some sites complaining loudly about TrustedHTML issues caused by the hook below,
  21. // you may choose to uncomment the next line to make them go away. Be wary, this has.. implications.
  22. // unsafeWindow.trustedTypes.createPolicy('default', {createHTML: (string, sink) => string})
  23.  
  24. // adapted from https://www.bram.us/2022/02/13/log-images-to-the-devtools-console-with-console-image/
  25. // tweaked because Chrome now only accepts data: URIs as background urls in console.
  26. async function blobToImageLog(blob, scale = 1) {
  27. const src = URL.createObjectURL(blob);
  28. try {
  29. let {target: img, target: { width, height } } = await new Promise((onload, onerror) =>crel('img', { src, onload, onerror }));
  30. const canvas = crel("canvas", { width, height });
  31. canvas.getContext('2d').drawImage(img, 0, 0);
  32. width *= scale; height *= scale;
  33. return ["%c .", `font-size:1px;padding:${~~(height/2)}px ${~~(width/2)}px;background:url("${canvas.toDataURL()}");background-size:${width}px ${height}px;color: transparent;`];
  34. } catch {
  35. return ["Invalid image", blob];
  36. } finally {
  37. URL.revokeObjectURL(src);
  38. }
  39. }
  40.  
  41. function hexdump(buffer, blockSize = 16) {
  42. const lines = [];
  43. const array = new Uint8Array(buffer);
  44. for (let i = 0; i < array.length; i += blockSize) {
  45. const addr = i.toString(16).padStart(4, '0');
  46. let hex = '';
  47. let chars = '';
  48. for (let j = 0; j < blockSize ; j++) {
  49. const v = array[i+j];
  50. if (j%16==0) { hex += ' '; chars += ' '; }
  51. hex += ' ' + (v!=null ? v.toString(16).padStart(2, '0') : ' ');
  52. chars += v!=null ? v<32?'.':String.fromCharCode(v) : ' ';
  53. }
  54. lines.push(addr + ' ' + hex + ' ' + chars);
  55. }
  56. return lines.join('\n');
  57. }
  58.  
  59. const urlParamsToObject = params => [...new URLSearchParams(params).entries()].reduce((obj, [key, val])=>((obj[key] ? !Array.isArray(obj[key])?obj[key] = [obj[key],val]:obj[key].push(val):obj[key]=val),obj),{});
  60.  
  61. const domainFromHostname = str => str.split('.').reduceRight((domain, chunk)=> domain.length<7&&chunk!='www' ? domain=chunk+'.'+domain : domain, '').slice(0,-1);
  62.  
  63. /**
  64. * Inspect the object passed and try to derive the most
  65. * immediately usable data representation from its body.
  66. *
  67. * @param {Request|Resource} r
  68. * @returns {{type: 'text'|'json'|'doc'|'image'|'binary', operations: string[], payload: any}}
  69. */
  70. async function autoParseBody(r) {
  71. const unzip = (r,encoding="gzip") => new Response(r.body.pipeThrough(new DecompressionStream(encoding)));
  72. const toJSON = str => { try { return JSON.parse(str); } catch {} };
  73. const toText = buffer => new TextDecoder(charset).decode(buffer);
  74. const toArray = obj => Array.isArray(obj) ? obj : Object.keys(obj).reduce((a,k)=>((a[k]=obj[k]),a),[]);
  75.  
  76. const isBinary = async blob => { try {new TextDecoder(charset, {fatal:true}).decode(await blob.arrayBuffer());return false} catch (e) { return true}};
  77. const isArrayShaped = obj => Array.isArray(obj) || Object.keys(obj).every(key => key==parseInt(key));
  78. const isArrayOfBytes = arr => arr.every(value => (value & 255) == value);
  79. const isURLEncoded = str => /^([a-z0-9_.~-]|%[0-9a-f]{2})+=([a-z0-9_.~-]|%[0-9a-f]{2})*(&([a-z0-9_.~-]|%[0-9a-f]{2})+=([a-z0-9_.~-]|%[0-9a-f]{2})*)*$/i.test(str);
  80. const isPerhapsURLEncoded = str => /[&%]/.test(str) || /^[a-z0-9_-]+=[a-z0-9_-]+$/g.test(str);
  81. const mayBeHTML = str => /<\/\s*html\s*>/i.test(str);
  82. const mayBeXML = str => /<[a-z]+.*?(>.*?<\/[a-z]+>|\/>)/i.test(str);
  83.  
  84. const contentType = r.headers.get('content-type')?.split(';')[0] ?? '';
  85. const charset = r.headers.get('content-type')?.match(/charset=(?<charset>[^()<>@,;:\"/[\]?.=\s]*)/i)?.groups?.charset ?? "utf-8";
  86. const encoding = r.headers.get('content-encoding');
  87. let ops = [];
  88. // 1. unzip any compressed content.
  89. if (r instanceof Request && ['gzip', 'deflate'].includes(encoding)) {
  90. // A web app went out of its way to compress a Request payload. cool.
  91. r = unzip(r, encoding);
  92. ops.push(encoding);
  93. }
  94. let body, type;
  95.  
  96. // devour the body, leaving only a blob behind. j/k. we cloned it so you can still grab a working response in the console.
  97. const blob = await r.clone().blob();
  98.  
  99. // 2. get rid of binary formats: images.
  100. if (contentType.startsWith("image/")) {
  101. return {
  102. type: 'image',
  103. operations: ops.concat('raw'),
  104. payload: blob
  105. };
  106. }
  107. // 3. get rid of other binary formats.
  108. if (await isBinary(blob)) {
  109. return {
  110. type: 'binary',
  111. operations: ops.concat('raw'),
  112. payload: blob
  113. };
  114. }
  115. // 4. from here on, everything is text-based. more or less.
  116. async function decodeText(text, operations, hint = '') {
  117. // explicit url-encoded content, with a guardrail for mis-typed payloads
  118. if (hint == "application/x-www-form-urlencoded" && isPerhapsURLEncoded(text)) {
  119. const obj = urlParamsToObject(text);
  120. operations.push('urlparams');
  121. return await decodeJSON(obj, operations);
  122. }
  123. // explicit json content
  124. if (hint.includes('json')) {
  125. // dumb loop to skip over security-minded folks that add junk characters at the beginning of their json payloads.
  126. for (let i=0;i<10;i++) {
  127. const obj = toJSON(text.slice(i));
  128. if (obj !== undefined) {
  129. operations.push('json');
  130. return await decodeJSON(obj, operations);
  131. }
  132. }
  133. }
  134. // explicit html or xml content
  135. if (hint.includes('html') || hint.includes('xml')) {
  136. try {
  137. const doc = new DOMParser().parseFromString(text, hint);
  138. operations.push(hint.includes('html')?'html':'xml');
  139. return {
  140. type: 'doc',
  141. operations,
  142. payload: doc
  143. }
  144. } catch {}
  145. }
  146. // implicit json content
  147. if (text[0]=='[' || text[0]=='{') { // "1" is not an interesting JSON content.
  148. // dumb loop to skip over security-minded folks that add junk characters at the beginning of their json payloads.
  149. for (let i=0;i<10;i++) {
  150. const obj = toJSON(text.slice(i));
  151. if (obj !== undefined) {
  152. operations.push('json');
  153. return await decodeJSON(obj, operations);
  154. }
  155. }
  156. }
  157. // implicit HTML content
  158. if (mayBeHTML(text)) {
  159. try {
  160. let node = new DOMParser().parseFromString(text, 'text/html');
  161. if (node.childElementCount ==1) node = node.firstChild;
  162. operations.push('html');
  163. return {
  164. type: 'doc',
  165. operations,
  166. payload: node
  167. }
  168. } catch {}
  169. }
  170. if (mayBeXML(text)) {
  171. try {
  172. let node = new DOMParser().parseFromString(text, 'text/xml');
  173. if (node.childElementCount ==1) node = node.firstChild;
  174. operations.push('xml');
  175. return {
  176. type: 'doc',
  177. operations,
  178. payload: node
  179. }
  180. } catch {}
  181. }
  182.  
  183. // implicit url-encoded content
  184. if (isURLEncoded(text) && isPerhapsURLEncoded(text)) {
  185. const obj = urlParamsToObject(text);
  186. operations.push('urlparams');
  187. return await decodeJSON(obj, operations);
  188. }
  189.  
  190. // implicit base64 of non-empty US ASCII strings
  191. if (text.length) {
  192. try {
  193. const decoded = atob(unescape(text.replace(/_/g,'/').replace(/-/g,'+'))); // handles URI-escaped strings, as well as "web-safe" base64.
  194. if (/^[0x0d0x0a0x20-0x7f]*$/.test(decoded)) { // but only keep ascii results.
  195. operations.push('base64');
  196. return {
  197. type: 'base64',
  198. operations,
  199. payload: decoded
  200. }
  201. }
  202. } catch {}
  203. }
  204.  
  205. // sometimes a chunk of text is just a chunk of text.
  206. return {
  207. type: 'text',
  208. operations,
  209. payload: text
  210. };
  211. }
  212.  
  213. async function decodeJSON(obj, operations) {
  214. if (obj) {
  215. // 1. is our object an array?
  216. if (isArrayShaped(obj)) {
  217. const array = toArray(obj);
  218. // 1.1 is our array an array of bytes
  219. if (array.length> 10 && isArrayOfBytes(array)) {
  220. let buffer = Uint8Array.from(array).buffer;
  221. //operations.push('binary');
  222. // how high are the odds of ever seeing this in the wild? The answer may surprise you (youtube/log_event)
  223. if (buffer.byteLength > 10 && new DataView(buffer).getInt16() == 0x1f8b) { // gzip magic number
  224. buffer = await unzip(new Response(buffer)).arrayBuffer();
  225. operations.push('gzip');
  226. const text = toText(buffer);
  227. operations.push('text');
  228. return await decodeText(text, operations);
  229. }
  230. }
  231. }
  232. // 2. dig into the object fields. XXX this might be a terrible idea.
  233. if (typeof obj == 'object') {
  234. const sub_ops = Object.assign([], { toString() { return `[ ${this.join()} ]`; }});
  235. // XXX this messes with `operations` a lot. tweak how operations track things.
  236. await Promise.all(Object.keys(obj).map(async key => obj[key] = typeof obj[key] == 'string' ? (await decodeText(obj[key], sub_ops)).payload : obj[key] )); //(await decodeJSON(obj[key], sub_ops)).payload));
  237. if (sub_ops.length) operations.push(sub_ops);
  238. }
  239. }
  240. return {
  241. type: 'json',
  242. operations,
  243. payload: obj
  244. };
  245. }
  246.  
  247. if (blob.size == 0) {
  248. return { type: 'empty', operations: ['empty'], payload: '' }
  249. }
  250. const text = toText(await blob.arrayBuffer()); // this is charset aware, unlike r.text().
  251. return await decodeText(text, ['text'], contentType);
  252. }
  253.  
  254. // logging hook. tries to show what's going on, decoding bodies in potentially convoluted ways.
  255. const logHook = async (req, res, err) => {
  256.  
  257. // used to prefix an object in the console.
  258. function QueryString(obj) { Object.assign(this, obj); }
  259. function Body(obj) { return typeof obj == 'string' || obj instanceof Blob ? obj : Object.assign(this, obj); }
  260.  
  261. async function logHalf(r) {
  262. const t = Date.now();
  263. const headers = [...r.headers.entries()].map(a=>a.join(": ")).join('\n');
  264. const { type, operations, payload } = await autoParseBody(r);
  265. let body, size;
  266. switch (type) {
  267. case 'image':
  268. size = payload.size;
  269. body = await blobToImageLog(payload);
  270. break;
  271. case 'empty':
  272. case 'text':
  273. size = payload.length;
  274. body = payload;
  275. break;
  276. case 'json':
  277. size = JSON.stringify(payload).length;
  278. body = payload;
  279. break;
  280. case 'doc':
  281. size = new XMLSerializer().serializeToString(payload).length;
  282. body = payload;
  283. break;
  284. case 'binary':
  285. // body = hexdump(await payload.arrayBuffer(), 32); // expensive, and not really useful
  286. size = payload.size;
  287. body = payload;
  288. break;
  289. }
  290. const method = r.method ?? 'GET';
  291. return { size, type, method, ops: operations.join(' => '), headers, body, cost: Date.now()-t };
  292. };
  293. const url = new URL(req.url);
  294. const short = domainFromHostname(url.hostname) + url.pathname;
  295. const reqObj = await logHalf(req);
  296. const query = await logHalf(new Response(url.searchParams, { headers: { 'content-type': 'application/x-www-form-urlencoded' }}));
  297. const type = reqObj.type == 'empty' ? query.type : reqObj.type;
  298. const size = reqObj.type == 'empty' ? query.size : reqObj.size;
  299. const ops = reqObj.type == 'empty' ? query.ops : reqObj.ops;
  300. const opsCount = ops.split(' => ').length;
  301. logGroup("Request " + reqObj.method + ' ' + short + ' '+size+'B ['+type+'] ('+opsCount+')', (reqObj.cost+query.cost)+"ms - "+ops, reqObj.headers, req, new QueryString(query.body), typeof reqObj.body !== "json" ? reqObj.body : new Body(reqObj.body));
  302. if (res) {
  303. const resObj = await logHalf(res);
  304. const resOpsCount = resObj.ops.split(' => ').length;
  305. logGroup("Response " + resObj.method + ' ' + short + ' '+resObj.size+'B ['+resObj.type+'] ('+resOpsCount+')', resObj.cost+"ms - "+resObj.ops, resObj.headers, res, typeof resObj.body !== "json" ? resObj.body : new Body(resObj.body));
  306. } else {
  307. logGroup("Response " + reqObj.method + ' ' + short + " error: "+err.message, err);
  308. }
  309. };
  310.  
  311. // The actual middleman call: Snoop into everything, log all requests and responses.
  312. middleMan.addHook("*", {
  313. responseHandler: logHook
  314. });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址