JS Cookie Monitor/Debugger Hook

用于监控js对cookie的修改,或者在cookie符合给定条件时进入断点

目前为 2022-07-29 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name JS Cookie Monitor/Debugger Hook
  3. // @namespace https://github.com/CC11001100/js-cookie-monitor-debugger-hook
  4. // @version 0.9
  5. // @description 用于监控js对cookie的修改,或者在cookie符合给定条件时进入断点
  6. // @document https://github.com/CC11001100/js-cookie-monitor-debugger-hook
  7. // @author CC11001100
  8. // @match *://*/*
  9. // @run-at document-start
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (() => {
  14.  
  15. // 使用文档: https://github.com/CC11001100/js-cookie-monitor-debugger-hook
  16.  
  17. // @since v0.6 断点规则发生了向后不兼容变化,详情请查阅文档
  18. const debuggerRules = [];
  19. // example:
  20. // const debuggerRules = ["foo", /foo_\d+/];
  21.  
  22. // 设置事件断点是否开启,一般保持默认即可
  23. const enableEventDebugger = {
  24. "add": true,
  25. "update": true,
  26. "delete": true,
  27. "read": true,
  28. }
  29.  
  30. // 在控制台打印日志时字体大小,根据自己喜好调整
  31. // 众所周知,12px是宇宙通用大小
  32. const consoleLogFontSize = 12;
  33.  
  34. // 使用document.cookie更新cookie,但是cookie新的值和原来的值一样,此时要不要忽略这个事件
  35. const ignoreUpdateButNotChanged = false;
  36.  
  37. // 此处实现的反复hook,保证页面流程能够继续往下走下去
  38. (function addCookieHook() {
  39. Object.defineProperty(document, "cookie", {
  40. get: () => {
  41. delete document.cookie;
  42. const currentDocumentCookie = document.cookie;
  43. addCookieHook();
  44. return currentDocumentCookie;
  45. },
  46. set: newValue => {
  47. cc11001100_onSetCookie(newValue);
  48. delete document.cookie;
  49. document.cookie = newValue;
  50. addCookieHook();
  51. },
  52. configurable: true
  53. });
  54. })();
  55.  
  56. /**
  57. * 这个方法的前缀起到命名空间的作用,等下调用栈追溯赋值cookie的代码时需要用这个名字作为终结标志
  58. *
  59. * @param newValue
  60. */
  61. function cc11001100_onSetCookie(newValue) {
  62. const cookiePair = parseSetCookie(newValue);
  63. const currentCookieMap = getCurrentCookieMap();
  64.  
  65. // 如果过期时间为当前时间之前,则为删除,有可能没设置?虽然目前为止没碰到这样的...
  66. if (cookiePair.expires !== null && new Date().getTime() >= cookiePair.expires) {
  67. onDeleteCookie(newValue, cookiePair.name, cookiePair.value || (currentCookieMap.get(cookiePair.name) || {}).value);
  68. return;
  69. }
  70.  
  71. // 如果之前已经存在,则是修改
  72. if (currentCookieMap.has(cookiePair.name)) {
  73. onUpdateCookie(newValue, cookiePair.name, currentCookieMap.get(cookiePair.name).value, cookiePair.value);
  74. return;
  75. }
  76.  
  77. // 否则则为添加
  78. onAddCookie(newValue, cookiePair.name, cookiePair.value);
  79. }
  80.  
  81. function onReadCookie(cookieOriginalValue, cookieName, cookieValue) {
  82.  
  83. }
  84.  
  85. function onDeleteCookie(cookieOriginalValue, cookieName, cookieValue) {
  86. const valueStyle = `color: black; background: #E50000; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
  87. const normalStyle = `color: black; background: #FF6766; font-size: ${consoleLogFontSize}px;`;
  88.  
  89. const message = [
  90.  
  91. normalStyle,
  92. now(),
  93.  
  94. normalStyle,
  95. "JS Cookie Monitor: ",
  96.  
  97. normalStyle,
  98. "delete cookie, cookieName = ",
  99.  
  100. valueStyle,
  101. `${cookieName}`,
  102.  
  103. ...(() => {
  104. if (!cookieValue) {
  105. return [];
  106. }
  107. return [
  108. normalStyle,
  109. ", value = ",
  110.  
  111. valueStyle,
  112. `${cookieValue}`,
  113. ];
  114. })(),
  115.  
  116. normalStyle,
  117. `, code location = ${getCodeLocation()}`
  118. ];
  119. console.log(genFormatArray(message), ...message);
  120.  
  121. testDebuggerRules(cookieOriginalValue, "delete", cookieName, cookieValue);
  122. }
  123.  
  124. function onUpdateCookie(cookieOriginalValue, cookieName, oldCookieValue, newCookieValue) {
  125.  
  126. const cookieValueChanged = oldCookieValue !== newCookieValue;
  127.  
  128. if (ignoreUpdateButNotChanged && !cookieValueChanged) {
  129. return;
  130. }
  131.  
  132. const valueStyle = `color: black; background: #FE9900; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
  133. const normalStyle = `color: black; background: #FFCC00; font-size: ${consoleLogFontSize}px;`;
  134.  
  135. const message = [
  136.  
  137. normalStyle,
  138. now(),
  139.  
  140. normalStyle,
  141. "JS Cookie Monitor: ",
  142.  
  143. normalStyle,
  144. "update cookie, cookieName = ",
  145.  
  146. valueStyle,
  147. `${cookieName}`,
  148.  
  149. ...(() => {
  150. if (cookieValueChanged) {
  151. return [
  152. normalStyle,
  153. `, oldValue = `,
  154.  
  155. valueStyle,
  156. `${oldCookieValue}`,
  157.  
  158. normalStyle,
  159. `, newValue = `,
  160.  
  161. valueStyle,
  162. `${newCookieValue}`
  163. ]
  164. } else {
  165. return [
  166. normalStyle,
  167. `, value = `,
  168.  
  169. valueStyle,
  170. `${newCookieValue}`,
  171. ];
  172. }
  173. })(),
  174.  
  175. normalStyle,
  176. `, valueChanged = `,
  177.  
  178. valueStyle,
  179. `${cookieValueChanged}`,
  180.  
  181. normalStyle,
  182. `, code location = ${getCodeLocation()}`
  183. ];
  184. console.log(genFormatArray(message), ...message);
  185.  
  186. testDebuggerRules(cookieOriginalValue, "update", cookieName, newCookieValue, cookieValueChanged);
  187. }
  188.  
  189. function onAddCookie(cookieOriginalValue, cookieName, cookieValue) {
  190. const valueStyle = `color: black; background: #669934; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
  191. const normalStyle = `color: black; background: #65CC66; font-size: ${consoleLogFontSize}px;`;
  192.  
  193. const message = [
  194.  
  195. normalStyle,
  196. now(),
  197.  
  198. normalStyle,
  199. "JS Cookie Monitor: ",
  200.  
  201. normalStyle,
  202. "add cookie, cookieName = ",
  203.  
  204. valueStyle,
  205. `${cookieName}`,
  206.  
  207. normalStyle,
  208. ", cookieValue = ",
  209.  
  210. valueStyle,
  211. `${cookieValue}`,
  212.  
  213. normalStyle,
  214. `, code location = ${getCodeLocation()}`
  215. ];
  216. console.log(genFormatArray(message), ...message);
  217.  
  218. testDebuggerRules(cookieOriginalValue, "add", cookieName, cookieValue);
  219. }
  220.  
  221. function now() {
  222. // 东八区专属...
  223. return "[" + new Date(new Date().getTime() + 1000 * 60 * 60 * 8).toJSON().replace("T", " ").replace("Z", "") + "] ";
  224. }
  225.  
  226. function genFormatArray(messageAndStyleArray) {
  227. const formatArray = [];
  228. for (let i = 0, end = messageAndStyleArray.length / 2; i < end; i++) {
  229. formatArray.push("%c%s");
  230. }
  231. return formatArray.join("");
  232. }
  233.  
  234. // 解析当前代码的位置,以便能够直接定位到事件触发的代码位置
  235. function getCodeLocation() {
  236. const callstack = new Error().stack.split("\n");
  237. while (callstack.length && callstack[0].indexOf("cc11001100") === -1) {
  238. callstack.shift();
  239. }
  240. callstack.shift();
  241. callstack.shift();
  242.  
  243. return callstack[0].trim();
  244. }
  245.  
  246. /**
  247. * 将本次设置cookie的字符串解析为容易处理的形式
  248. *
  249. * @param cookieString
  250. * @returns {CookiePair}
  251. */
  252. function parseSetCookie(cookieString) {
  253. // uuid_tt_dd=10_37476713480-1609821005397-659114; Expires=Thu, 01 Jan 1025 00:00:00 GMT; Path=/; Domain=.csdn.net;
  254. const cookieStringSplit = cookieString.split(";");
  255. const {key, value} = splitKeyValue(cookieStringSplit.length && cookieStringSplit[0])
  256. const map = new Map();
  257. for (let i = 1; i < cookieStringSplit.length; i++) {
  258. let {key, value} = splitKeyValue(cookieStringSplit[i]);
  259. map.set(key.toLowerCase(), value);
  260. }
  261. // 当不设置expires的时候关闭浏览器就过期
  262. const expires = map.get("expires");
  263. return new CookiePair(key, value, expires ? new Date(expires).getTime() : null)
  264. }
  265.  
  266. /**
  267. * 把按照等号=拼接的key、value字符串切分开
  268. * @param s
  269. * @returns {{value: string, key: string}}
  270. */
  271. function splitKeyValue(s) {
  272. let key = "", value = "";
  273. const keyValueArray = (s || "").split("=");
  274.  
  275. if (keyValueArray.length) {
  276. key = decodeURIComponent(keyValueArray[0].trim());
  277. }
  278.  
  279. if (keyValueArray.length > 1) {
  280. value = decodeURIComponent(keyValueArray.slice(1).join("=").trim());
  281. }
  282.  
  283. return {
  284. key,
  285. value
  286. }
  287. }
  288.  
  289. /**
  290. * 获取当前所有已经设置的cookie
  291. *
  292. * @returns {Map<string, CookiePair>}
  293. */
  294. function getCurrentCookieMap() {
  295. const cookieMap = new Map();
  296. if (!document.cookie) {
  297. return cookieMap;
  298. }
  299. document.cookie.split(";").forEach(x => {
  300. const {key, value} = splitKeyValue(x);
  301. cookieMap.set(key, new CookiePair(key, value));
  302. });
  303. return cookieMap;
  304. }
  305.  
  306. class DebuggerRule {
  307.  
  308. constructor(eventName, cookieNameFilter, cookieValueFilter) {
  309. this.eventName = eventName;
  310. this.cookieNameFilter = cookieNameFilter;
  311. this.cookieValueFilter = cookieValueFilter;
  312. }
  313.  
  314. test(eventName, cookieName, cookieValue) {
  315. return this.testByEventName(eventName) && (this.testByCookieNameFilter(cookieName) || this.testByCookieValueFilter(cookieValue));
  316. }
  317.  
  318. testByEventName(eventName) {
  319. // 如果此类型的事件断点没有开启,则直接返回
  320. if (!enableEventDebugger[eventName]) {
  321. return false;
  322. }
  323. // 事件不设置则匹配任何事件
  324. if (!this.eventName) {
  325. return true;
  326. }
  327. return this.eventName === eventName;
  328. }
  329.  
  330. testByCookieNameFilter(cookieName) {
  331. if (!cookieName || !this.cookieNameFilter) {
  332. return false;
  333. }
  334. if (typeof this.cookieNameFilter === "string") {
  335. return this.cookieNameFilter === cookieName;
  336. }
  337. if (this.cookieNameFilter instanceof RegExp) {
  338. return this.cookieNameFilter.test(cookieName);
  339. }
  340. return false;
  341. }
  342.  
  343. testByCookieValueFilter(cookieValue) {
  344. if (!cookieValue || !this.cookieValueFilter) {
  345. return false;
  346. }
  347. if (typeof this.cookieValueFilter === "string") {
  348. return this.cookieValueFilter === cookieValue;
  349. }
  350. if (this.cookieValueFilter instanceof RegExp) {
  351. return this.cookieValueFilter.test(cookieValue);
  352. }
  353. return false;
  354. }
  355.  
  356. }
  357.  
  358. // 将规则整理为标准规则
  359. // 解析起来并不复杂,但是有点过于灵活,要介绍清楚打的字要远超代码,所以我文档里就随便介绍下完事有缘人会自己读代码的...
  360. (function standardizingRules() {
  361.  
  362. // 用于收集规则配置错误,在解析完所有规则之后一次把事情说完
  363. const ruleConfigErrorMessage = [];
  364.  
  365. const newRules = [];
  366. while (debuggerRules.length) {
  367. const rule = debuggerRules.pop();
  368.  
  369. // 如果是字符串或者正则
  370. if (typeof rule === "string" || rule instanceof RegExp) {
  371. newRules.push(new DebuggerRule(null, rule, null));
  372. continue;
  373. }
  374.  
  375. // 如果是字典对象,则似乎有点麻烦
  376. for (let key in rule) {
  377. let events = null;
  378. let cookieNameFilter = null;
  379. let cookieValueFilter = null;
  380. if (key === "events") {
  381. events = rule["events"] || "add | delete | update";
  382. cookieNameFilter = rule["name"]
  383. cookieValueFilter = rule["value"];
  384. } else if (key !== "name" && key !== "value") {
  385. events = key;
  386. cookieNameFilter = rule[key];
  387. cookieValueFilter = rule["value"];
  388. } else {
  389. // name & value ignore
  390. continue;
  391. }
  392. // cookie的名字是必须配置的
  393. if (!cookieNameFilter) {
  394. const errorMessage = `必须为此条规则 ${JSON.stringify(rule)} 配置一个Cookie Name匹配条件`;
  395. ruleConfigErrorMessage.push(errorMessage);
  396. continue;
  397. }
  398. events.split("|").forEach(eventName => {
  399. eventName = eventName.trim();
  400. if (eventName !== "add" && eventName !== "delete" && eventName !== "update") {
  401. const errorMessage = `此条规则 ${JSON.stringify(rule)} Cookie事件名字配置错误,必须为 adddeleteupdate 三种之一或者|分隔的组合,您配置的是 ${eventName},仅忽略此无效事件`;
  402. ruleConfigErrorMessage.push(errorMessage);
  403. return;
  404. }
  405. newRules.push(new DebuggerRule(eventName, cookieNameFilter, cookieValueFilter));
  406. })
  407. }
  408. }
  409.  
  410. // 配置错误的规则会被忽略,其它规则照常生效
  411. if (ruleConfigErrorMessage.length) {
  412. // 错误打印字号要大1.5倍,不信你注意不到
  413. const errorMessageStyle = `color: black; background: #FF2121; font-size: ${Math.round(consoleLogFontSize * 1.5)}px; font-weight: bold;`;
  414. let errorMessage = now() + "JS Cookie Monitor: 以下Cookie断点规则配置错误,已忽略: \n ";
  415. for (let i = 0; i < ruleConfigErrorMessage.length; i++) {
  416. errorMessage += `${i + 1}. ${ruleConfigErrorMessage[i]}\n`;
  417. }
  418. console.log("%c%s", errorMessageStyle, errorMessage);
  419. }
  420.  
  421. // 是否需要合并重复规则呢?
  422. // 还是不了,而且静态合并对于正则没办法,用户应该知道自己在做什么
  423.  
  424. for (let rule of newRules) {
  425. debuggerRules.push(rule);
  426. }
  427. })();
  428.  
  429. /**
  430. * 当断点停在这里时查看这个方法各个参数的值能够大致了解断点情况
  431. *
  432. * 鼠标移动到变量上查看变量的值
  433. *
  434. * @param setCookieOriginalValue 目标网站使用document.cookie时赋值的原始值是什么,这个值没有 URL decode,
  435. * 如果要分析它请拷贝其值到外面分析,这里只是提供一种可能性
  436. * @param eventName 本次是发生了什么事件,add增加新cookie、update更新cookie的值、delete表示cookie被删除
  437. * @param cookieName 本脚本对setCookieOriginalValue解析出的cookie名字,会被URL decode
  438. * @param cookieValue 本脚本对setCookieOriginalValue解析出的cookie值,会被URL decode
  439. * @param cookieValueChanged 只在update事件时有值,用于帮助快速确定本次update有没有修改cookie的值
  440. */
  441. function testDebuggerRules(setCookieOriginalValue, eventName, cookieName, cookieValue, cookieValueChanged) {
  442. for (let rule of debuggerRules) {
  443. // rule当前的值表示被什么断点规则匹配到了,可以把鼠标移动到rule变量上查看
  444. if (rule.test(eventName, cookieName, cookieValue)) {
  445. debugger;
  446. }
  447. }
  448. }
  449.  
  450. /**
  451. * 用于在本脚本内部表示一条cookie以方便程序处理
  452. * 这里只取了有用的信息,忽略了域名及路径,也许需要加上这两个限制?但现在这个脚本已经够臃肿了...
  453. */
  454. class CookiePair {
  455.  
  456. /**
  457. *
  458. * @param name Cookie的名字
  459. * @param value Cookie的值
  460. * @param expires Cookie的过期时间
  461. */
  462. constructor(name, value, expires) {
  463. this.name = name;
  464. this.value = value;
  465. this.expires = expires;
  466. }
  467.  
  468. }
  469.  
  470. }
  471.  
  472. )();