├── .gitignore ├── LICENSE ├── README.assets ├── Snipaste_2024-10-26_18-13-07.png └── image-20220729214105718.png ├── README.md ├── banner.txt ├── dev-header.js ├── docs ├── .DS_Store ├── README.md ├── mail.sohu.com-RE │ ├── README.assets │ │ ├── image-20240908175036662.png │ │ ├── image-20240908175134169.png │ │ ├── image-20240908175155333.png │ │ ├── image-20240908175230081.png │ │ ├── image-20240908175510868.png │ │ └── image-20240908175633235.png │ └── README.md ├── old-code-backup │ └── js-cookie-monitor-debugger-hook.js └── q.10jqka.com.cn-RE │ ├── .DS_Store │ ├── README.assets │ ├── image-20240908170233715.png │ ├── image-20240908171817289.png │ ├── image-20240908172113950.png │ ├── image-20240908172355099.png │ ├── image-20240908172500882.png │ ├── image-20240908172827707.png │ └── image-20240908173050817.png │ └── README.md ├── fuck-hot-compile.sh ├── images ├── README_images │ ├── 0cf06995.png │ ├── 10ea2db6.png │ ├── 20f986d7.png │ ├── 2a5b0f6c.png │ ├── 33dc63f1.png │ ├── 35720fae.png │ ├── 36eb394d.png │ ├── 45ecea34.png │ ├── 47c3b465.png │ ├── 5415caa1.png │ ├── 676ecd0d.png │ ├── 82fec90f.png │ ├── 8b47aea4.png │ └── fa06f80c.png ├── img.png ├── img_1.png ├── img_2.png ├── img_3.png ├── img_4.png ├── img_5.png ├── img_6.png ├── img_7.png ├── img_8.png └── img_9.png ├── package-lock.json ├── package.json ├── scripts └── generate-dev-header.js ├── src ├── cookie-monitor │ ├── config.ts │ ├── index.ts │ └── init.ts ├── events │ ├── handlers.ts │ ├── index.ts │ └── parser.ts ├── hooks │ ├── cookie-hooks.ts │ ├── index.ts │ └── property-hooks.ts ├── index.ts ├── logger │ ├── index.ts │ ├── logger.ts │ └── types.ts ├── models │ ├── cookie-pair.ts │ ├── debugger-rule.ts │ └── index.ts ├── rules │ ├── index.ts │ ├── standardize.ts │ └── tester.ts ├── types │ ├── index.ts │ └── tampermonkey.d.ts ├── ui │ ├── index.ts │ ├── menu │ │ ├── index.ts │ │ └── register.ts │ ├── modal │ │ ├── index.ts │ │ ├── modal.css.ts │ │ ├── modal.html.ts │ │ └── modal.ts │ ├── tabs │ │ ├── index.ts │ │ ├── tabs.css.ts │ │ ├── tabs.html.ts │ │ └── tabs.ts │ └── views │ │ ├── about │ │ ├── about.ts │ │ └── index.ts │ │ ├── breakpoints │ │ ├── breakpoints.ts │ │ └── index.ts │ │ └── settings │ │ ├── index.ts │ │ └── settings.ts └── utils │ ├── debugger.ts │ ├── format.ts │ ├── index.ts │ └── time.ts ├── test_cookie ├── page-has-cookie-define-properties.html └── page-has-cookie-define-property.html ├── tsconfig.json ├── userscript-headers.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 4 | 5 | # User-specific stuff 6 | .idea/**/workspace.xml 7 | .idea/**/tasks.xml 8 | .idea/**/usage.statistics.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # Generated files 13 | .idea/**/contentModel.xml 14 | 15 | # Sensitive or high-churn files 16 | .idea/**/dataSources/ 17 | .idea/**/dataSources.ids 18 | .idea/**/dataSources.local.xml 19 | .idea/**/sqlDataSources.xml 20 | .idea/**/dynamic.xml 21 | .idea/**/uiDesigner.xml 22 | .idea/**/dbnavigator.xml 23 | 24 | # Gradle 25 | .idea/**/gradle.xml 26 | .idea/**/libraries 27 | 28 | # Gradle and Maven with auto-import 29 | # When using Gradle or Maven with auto-import, you should exclude module files, 30 | # since they will be recreated, and may cause churn. Uncomment if using 31 | # auto-import. 32 | # .idea/artifacts 33 | # .idea/compiler.xml 34 | # .idea/jarRepositories.xml 35 | # .idea/modules.xml 36 | # .idea/*.iml 37 | # .idea/modules 38 | # *.iml 39 | # *.ipr 40 | 41 | # CMake 42 | cmake-build-*/ 43 | 44 | # Mongo Explorer plugin 45 | .idea/**/mongoSettings.xml 46 | 47 | # File-based project format 48 | *.iws 49 | 50 | # IntelliJ 51 | out/ 52 | 53 | # mpeltonen/sbt-idea plugin 54 | .idea_modules/ 55 | 56 | # JIRA plugin 57 | atlassian-ide-plugin.xml 58 | 59 | # Cursive Clojure plugin 60 | .idea/replstate.xml 61 | 62 | # Crashlytics plugin (for Android Studio and IntelliJ) 63 | com_crashlytics_export_strings.xml 64 | crashlytics.properties 65 | crashlytics-build.properties 66 | fabric.properties 67 | 68 | # Editor-based Rest Client 69 | .idea/httpRequests 70 | 71 | # Android studio 3.1+ serialized cache file 72 | .idea/caches/build_file_checksums.ser 73 | 74 | # Logs 75 | logs 76 | *.log 77 | npm-debug.log* 78 | yarn-debug.log* 79 | yarn-error.log* 80 | lerna-debug.log* 81 | .pnpm-debug.log* 82 | 83 | # Diagnostic reports (https://nodejs.org/api/report.html) 84 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 85 | 86 | # Runtime data 87 | pids 88 | *.pid 89 | *.seed 90 | *.pid.lock 91 | 92 | # Directory for instrumented libs generated by jscoverage/JSCover 93 | lib-cov 94 | 95 | # Coverage directory used by tools like istanbul 96 | coverage 97 | *.lcov 98 | 99 | # nyc test coverage 100 | .nyc_output 101 | 102 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 103 | .grunt 104 | 105 | # Bower dependency directory (https://bower.io/) 106 | bower_components 107 | 108 | # node-waf configuration 109 | .lock-wscript 110 | 111 | # Compiled binary addons (https://nodejs.org/api/addons.html) 112 | build/Release 113 | 114 | # Dependency directories 115 | node_modules/ 116 | jspm_packages/ 117 | 118 | # Snowpack dependency directory (https://snowpack.dev/) 119 | web_modules/ 120 | 121 | # TypeScript cache 122 | *.tsbuildinfo 123 | 124 | # Optional npm cache directory 125 | .npm 126 | 127 | # Optional eslint cache 128 | .eslintcache 129 | 130 | # Optional stylelint cache 131 | .stylelintcache 132 | 133 | # Microbundle cache 134 | .rpt2_cache/ 135 | .rts2_cache_cjs/ 136 | .rts2_cache_es/ 137 | .rts2_cache_umd/ 138 | 139 | # Optional REPL history 140 | .node_repl_history 141 | 142 | # Output of 'npm pack' 143 | *.tgz 144 | 145 | # Yarn Integrity file 146 | .yarn-integrity 147 | 148 | # dotenv environment variable files 149 | .env 150 | .env.development.local 151 | .env.test.local 152 | .env.production.local 153 | .env.local 154 | 155 | # parcel-bundler cache (https://parceljs.org/) 156 | .cache 157 | .parcel-cache 158 | 159 | # Next.js build output 160 | .next 161 | out 162 | 163 | # Nuxt.js build / generate output 164 | .nuxt 165 | dist 166 | 167 | # Gatsby files 168 | .cache/ 169 | # Comment in the public line in if your project uses Gatsby and not Next.js 170 | # https://nextjs.org/blog/next-9-1#public-directory-support 171 | # public 172 | 173 | # vuepress build output 174 | .vuepress/dist 175 | 176 | # vuepress v2.x temp and cache directory 177 | .temp 178 | .cache 179 | 180 | # Docusaurus cache and generated files 181 | .docusaurus 182 | 183 | # Serverless directories 184 | .serverless/ 185 | 186 | # FuseBox cache 187 | .fusebox/ 188 | 189 | # DynamoDB Local files 190 | .dynamodb/ 191 | 192 | # TernJS port file 193 | .tern-port 194 | 195 | # Stores VSCode versions used for testing VSCode extensions 196 | .vscode-test 197 | 198 | # yarn v2 199 | .yarn/cache 200 | .yarn/unplugged 201 | .yarn/build-state.yml 202 | .yarn/install-state.gz 203 | .pnp.* 204 | 205 | # IDE 206 | .idea/ 207 | .vscode/ 208 | 209 | # Build output 210 | /build/ 211 | /dist/ 212 | 213 | .idea 214 | data/ 215 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 CC11001100 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.assets/Snipaste_2024-10-26_18-13-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/README.assets/Snipaste_2024-10-26_18-13-07.png -------------------------------------------------------------------------------- /README.assets/image-20220729214105718.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/README.assets/image-20220729214105718.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JS Cookie Monitor/Debugger Hook (TypeScript版本) 2 | 3 | 这是一个用于监控JavaScript对Cookie操作的工具,同时也支持在Cookie符合特定条件时进入断点进行调试。本项目是原[JS Cookie Monitor/Debugger Hook](https://github.com/CC11001100/js-cookie-monitor-debugger-hook)的TypeScript重构版本。 4 | 5 | ## 功能特点 6 | 7 | 1. 实时监控JavaScript对Cookie的添加、修改和删除操作 8 | 2. 在浏览器控制台中以醒目的颜色显示Cookie操作 9 | 3. 支持根据Cookie名称或值设置断点条件 10 | 4. 支持在特定Cookie事件(添加、修改、删除、读取)时触发断点 11 | 5. 提供TypeScript类型支持,提高开发体验和代码质量 12 | 13 | ## 开发与构建 14 | 15 | ### 安装依赖 16 | 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | ### 开发模式 22 | 23 | ```bash 24 | npm run watch 25 | ``` 26 | 27 | 此命令会启动开发服务器并在代码变更时自动重新编译。 28 | 29 | ### 生产构建 30 | 31 | ```bash 32 | npm run build 33 | ``` 34 | 35 | 此命令会生成压缩优化后的脚本,通常用于发布。 36 | 37 | ## 使用方法 38 | 39 | ### 基本使用 40 | 41 | 在浏览器中安装用户脚本扩展(如Tampermonkey),然后安装构建生成的脚本。当JavaScript代码操作Cookie时,控制台会以不同颜色显示相关信息: 42 | 43 | - 绿色:添加Cookie 44 | - 橙色:修改Cookie 45 | - 红色:删除Cookie 46 | 47 | ### 断点调试 48 | 49 | 可以在脚本中设置断点规则,当Cookie操作符合特定条件时,脚本会自动进入断点,方便开发者进行调试: 50 | 51 | ```typescript 52 | // 示例:匹配名称为"foo"的Cookie 53 | const debuggerRules = ["foo"]; 54 | 55 | // 示例:使用正则表达式匹配名称格式为"foo_数字"的Cookie 56 | const debuggerRules = [/foo_\d+/]; 57 | 58 | // 示例:更复杂的规则配置 59 | const debuggerRules = [ 60 | // 当名为"sessionId"的Cookie被添加时触发断点 61 | { event: "add", name: "sessionId" }, 62 | 63 | // 当任何包含"token"的Cookie被修改时触发断点 64 | { event: "update", name: /token/ }, 65 | 66 | // 当Cookie值包含"admin"字样时触发断点 67 | { value: /admin/ } 68 | ]; 69 | ``` 70 | 71 | ## 安装 72 | 73 | ### 1. 安装Tampermonkey浏览器插件 74 | 75 | 首先安装[Tampermonkey](https://www.tampermonkey.net/)浏览器扩展: 76 | - [Chrome商店](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) 77 | - [Firefox商店](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/) 78 | 79 | ### 2. 安装本用户脚本 80 | 81 | 构建项目后,在浏览器中打开dist目录下生成的.user.js文件,Tampermonkey会提示安装。 82 | 83 | ## 许可证 84 | 85 | MIT 86 | 87 | ## 致谢 88 | 89 | 感谢原项目作者[CC11001100](https://github.com/CC11001100)的贡献。 90 | 91 | # 监控、定位JavaScript操作cookie 92 | 93 | GitHub Repository: https://github.com/JSREI/js-cookie-monitor-debugger-hook 94 | 95 | 简体中文 | [English](./README_en.md) 96 | 97 | ![GitHub Created At](https://img.shields.io/github/created-at/JSREI/js-cookie-monitor-debugger-hook) ![GitHub contributors](https://img.shields.io/github/contributors-anon/JSREI/js-cookie-monitor-debugger-hook) ![GitHub top language](https://img.shields.io/github/languages/top/JSREI/js-cookie-monitor-debugger-hook) ![GitHub commit activity](https://img.shields.io/github/commit-activity/t/JSREI/js-cookie-monitor-debugger-hook) ![GitHub Release](https://img.shields.io/github/v/release/JSREI/js-cookie-monitor-debugger-hook) ![Greasy Fork Downloads](https://img.shields.io/greasyfork/dt/419781) ![Greasy Fork Rating](https://img.shields.io/greasyfork/rating-count/419781) ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/JSREI/js-cookie-monitor-debugger-hook) ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-pr/JSREI/js-cookie-monitor-debugger-hook) ![GitHub License](https://img.shields.io/github/license/JSREI/js-cookie-monitor-debugger-hook) ![GitHub Repo stars](https://img.shields.io/github/stars/JSREI/js-cookie-monitor-debugger-hook) ![GitHub forks](https://img.shields.io/github/forks/JSREI/js-cookie-monitor-debugger-hook) ![GitHub watchers](https://img.shields.io/github/watchers/JSREI/js-cookie-monitor-debugger-hook) 98 | 99 | ![Snipaste_2024-10-26_18-13-07](./README.assets/Snipaste_2024-10-26_18-13-07.png) 100 | 101 | ## 一、脚本说明 102 | 103 | ### 为什么会有这个东西? 104 | 105 | 数据无价的时代,爬虫与反爬的对抗已经进入白热化状态,其中Cookie反爬是`最常见之一`的反爬类型, 网站方通过混淆得亲妈都不认识的JS代码设置Cookie(通常是浏览器指纹、请求时必须带上的Cookie之类的), 106 | 面对请求时必须要带上但是又不知道在哪里生成的Cookie, 你在几万行混淆的亲妈都不认识的JS屎海中苦苦挣扎希望能找到生成Cookie的地方(要是逆向思路不科学兴许还会呛上几口...), 107 | 甚至几度想找个借口骗自己放弃,或者要不干脆用Selenium之类的浏览器模拟方式算了? 怂个球,此脚本就是来助你一臂之力的! (你我都知道,这段只是撑场面的废话,你可以略过,如果你没有不幸读完的话...) 108 | 109 | ### 脚本功能 110 | 111 | 本脚本的功能大致分为两个部分: 112 | 113 | - monitor: 监控所有JS操作cookie变化的动作并打印在控制台上 114 | - debugger: 在cookie符合给定条件并且发生变化时打debugger断点 115 | 116 | ### Hook生效的条件 117 | 118 | - 需要本脚本被成功注入到页面头部最先执行,脚本都未注入成功自然无法Hook 119 | - 需要是JavaScript操作document.cookie赋值来操作Cookie才能够Hook到 (目前还没碰到不是这么赋值的...) 120 | 121 | ### 使用须知 122 | 123 | 本脚本是通过将自己的JS代码注入到页面,Hook住`document.cookie`来完成各种功能, 因此在使用本脚本之前要先确定要搞的Cookie确实是通过JS生成的 124 | (后面介绍了一种非常简单的确定Cookie是JS生成还是服务器返回的方式)。 125 | 126 | ## 二、有何优势? 127 | 128 | ## 2.1 不影响浏览器自带的Cookie管理 129 | 130 | 目前很多Hook脚本Hook姿势并不对,本脚本采用的是一次性、反复Hook,对浏览器自带的Cookie管理无影响: 131 | 132 | ![./images/img.png](./images/img.png) 133 | 134 | ## 2.2 功能更强:监控Cookie变化 135 | 136 | 除了cookie断点功能之外,增加了Cookie修改监控功能,能够在更宏观的角度分析页面上的Cookie: 137 | 138 | ![./images/img_1.png](./images/img_1.png) 139 | 140 | (算了,放弃打码了...) 141 | 142 | 颜色是用于区分操作类型: 143 | 144 | - 绿色是添加Cookie 145 | - 红色是删除Cookie 146 | - 黄色是修改已经存在的Cookie的值 147 | 148 | 每个操作都会跟着一个code location,单击可以定位到做了此操作的JS代码的位置。 149 | 150 | ## 2.3 功能更强:打断点时细分Cookie变化类型 151 | 152 | 从v0.6开始引入了功能更强大并且配置更灵活的断点规则,引入事件机制, 将Cookie修改细分为增加、删除、更新三个事件,支持更细粒度的打断点, 关于Cookie事件,详情请参阅本文第五部分。 153 | 154 | 关于为什么要这样设计? 一种比较常见的情况,目标网站有反爬的Cookie是JS设置的, 但是JS代码的逻辑是先疯狂的删除,然后删除好多次之后才添加真正的值, 这种方式设置Cookie正好能反制一般的Cookie Hook调试。 155 | 156 | 这里是其中一个例子,比如F5的Cookie保护,有一个Cookie `TS51c47c46075`,它就是先被删除好多次,然后再被添加一次: 157 | ![](images/README_images/20f986d7.png) 158 | 这种情况下可以针对**添加**名为`TS51c47c46075`的Cookie事件打一个断点, 就可以避免那些红色的删除事件混淆视听。 159 | 160 | ## 三、 安装 161 | 162 | ### 3.1 安装油猴插件 163 | 164 | 理论上只要本脚本的JS代码能够注入到页面上即可,这里采用的是油猴插件来将JS代码注入到页面上。 165 | 166 | 油猴插件可从Chrome商店安装: 167 | 168 | [https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) 169 | 170 | 如果无法翻墙,可以在百度搜索"Tampermonkey"字样寻找第三方网站下载,但请注意不要安装了虚假的恶意插件,推荐从官方商店安装。 171 | 172 | 其它工具亦可,只要能够将本脚本的JS代码注入到页面最头部执行即可。 173 | 174 | ### 3.2 安装本脚本 175 | 176 | 安装油猴脚本可以从官方商店,也可以拷贝代码自己在本地创建。 177 | 178 | #### 3.2.1 从油猴商店安装本脚本 179 | 180 | 推荐此方式,从油猴商店安装的油猴脚本有后续版本更新时能够自动更新,本脚本已经在油猴商店上架: 181 | 182 | [https://greasyfork.org/zh-CN/scripts/419781-js-cookie-monitor-debugger-hook](https://greasyfork.org/zh-CN/scripts/419781-js-cookie-monitor-debugger-hook) 183 | 184 | #### 3.2.2 手动创建插件 185 | 186 | 如果您觉得自动更新太烦,或者有其它的顾虑,可以在这里复制本脚本的代码: 187 | 188 | [https://github.com/CC11001100/js-cookie-monitor-debugger-hook/blob/main/js-cookie-monitor-debugger-hook.js](https://github.com/CC11001100/js-cookie-monitor-debugger-hook/blob/main/js-cookie-monitor-debugger-hook.js) 189 | 190 | review确认没问题之后在油猴的管理面板添加即可。 191 | 192 | ## 四、监控Cookie的变化(monitor) 193 | 194 | ### 4.1 基本用法 195 | 196 | 注意,监控是为了在宏观上有一个全局的认识,并不是为了定位细节 (通常情况下正确的使用工具才能提高效率哇,当然一个人的认知是有限的,欢迎大家反馈更有意思的玩法), 比如打开一个页面时: 197 | 198 | ![./images/img_1.png](./images/img_1.png) 199 | 200 | 根据这张图,我们就能够对这个网站上哪些cookie是JS操作的,什么时间如何操作的有个大致的了解。 201 | 202 | ### 4.2 基本用法进阶 203 | 204 | 再比如借助monitor观察cookie的变化规律,比如这个页面,根据时间能够看出这个cookie每隔半分钟会被改变一次: 205 | 206 | ![./images/img_2.png](./images/img_2.png) 207 | 208 | ### 4.3 过滤打印信息,只查看某个Cookie的变化 209 | 210 | (2021-1-7 18:27:49更新v0.4添加此功能): 如果控制台打印的信息过多, 可以借助Chrome浏览器自带的过滤来筛选,打印的日志的格式已经统一,只需要`cookieName = Cookie名字`即可, 比如: 211 | 212 | ![./images/img_9.png](./images/img_9.png) 213 | 214 | 请注意,搜索时要保证你的搜索信息是URL解码了的,否则可能会不匹配, 因为控制台的打印信息都是先URL解码再打印的。 215 | 216 | ### 4.4 过滤打印信息,快速确定Cookie是否是JS本地生成的 217 | 218 | 如果你不确定要搞的Cookie是本地生成的还是某个请求服务器`set-cookie`返回的, 则可以把本脚本打开,然后刷新目标网站的页面,然后在控制台搜索Cookie名字即可, 219 | 方法与上一节类似,当Cookie的名字比较短没有标识性的时候可以加`cookieName`辅助定位,比如: 220 | 221 | ```text 222 | cookieName = v 223 | ``` 224 | 225 | ### 4.5 减少冗余信息(不推荐) 226 | 227 | 有时候目标网站可能会反复设置一个cookie,还都是同样的值,这个变量用于忽略此类事件: 228 | 229 | ![./images/img_8.png](./images/img_8.png) 230 | 231 | 一般保持默认即可。 232 | 233 | ## 五、 定位Cookie的变化(debugger) 234 | 235 | ```@since v0.6 ``` 236 | 此部分的文档适用于v0.6+版本,如果您本地的版本小于0.6,请升级版本后再来阅读文档。 237 | 238 | 从v0.6开始,在Cookie的值发生改变时打断点变得很复杂,也变得很简单, 复杂是因为引入了事件机制,简单是因为简化了断点规则配置更灵活。 239 | 240 | 断点规则可以分为`标准规则`和`简化规则`,标准规则是程序底层方便实现处理的, 简化规则是为了用户更方便地配置,通常情况下您只需要了解简化规则就可以了, 当简化规则配置无法满足需求时再来查阅标准规则如何配置。 241 | 242 | #### 5.1 debuggerRules 243 | 244 | 所有的规则都是配置在`debuggerRules`数组中的,在脚本的头部有一个变量: 245 | ![](images/README_images/45ecea34.png) 246 | 如果找不到的话,可以按Ctrl+F按变量的名字搜索: 247 | 248 | ```js 249 | debuggerRules 250 | ``` 251 | 252 | 这个变量是一个数组类型,里面存放着一些规则条件,来决定什么情况下会进入断点。 253 | 254 | 注意,这是一个数组,数组中的规则是或的关系,触发Cookie修改事件时, 会顺序匹配每条规则, 只要有一条规则匹配成功就会进入一次断点。 255 | 256 | ### 5.2 常用配置方式(简化的配置规则) 257 | 258 | #### 5.2.1 Cookie名字过滤 259 | 260 | 当名为`foo`的Cookie发生变化时进入断点: 261 | 262 | ```js 263 | const debuggerRules = ["foo"]; 264 | ``` 265 | 266 | 上面这种方式指定一个字符串,会按照Cookie名字等于给定的字符串去匹配。 267 | 268 | 注意,此处的完全匹配如果有被URL编码的部分也需要先URL解码再粘贴到这里, 其它涉及到字符串的地方都一样后面不再赘述。 269 | 270 | 如果Cookie的名字中包含一直变化的部分,比如时间戳、UUID之类的, 通过名字已经无法定位,则通过正则匹配: 271 | 272 | ```js 273 | const debuggerRules = [/foo.+/]; 274 | ``` 275 | 276 | 绝大多数情况只需要这两种配置就够了。 277 | 278 | 下面来实践一下,当打开这个页面 279 | 280 | [https://www.ishumei.com/trial/captcha.html](https://www.ishumei.com/trial/captcha.html) 281 | 282 | 能看到脚本检测到了一些Cookie操作: 283 | 284 | ![](images/README_images/36eb394d.png) 285 | 286 | 其中有个`smidV2`很可疑,于是我们为它添加一个断点: 287 | 288 | ![](images/README_images/5415caa1.png) 289 | 290 | 修改完`debuggerRules`数组要注意按Ctrl+S保存脚本,然后因为油猴是在页面加载的时候注入JS代码的, 所以要刷新页面重新注入,当刷新页面的时候就自动进入了断点: 291 | 292 | ![](images/README_images/47c3b465.png) 293 | 294 | 上图的红色框A中是专门传进来的一些变量,通过将鼠标移动到这些变量上查看值, 我们能够大概知道当前断点的一些情况: 295 | 296 | ![](images/README_images/0cf06995.png) 297 | 298 | 然后就是红色框B,我们打Cookie断点就是为了追踪调用栈定位生成Cookie的地方, 红色方框内是本脚本的调用栈,有很明显的`userscript.html`标识, 忽略此部分的调用栈即可。 299 | 300 | 然后追溯调用栈,能够看到设置Cookie的地方: 301 | 302 | ![](images/README_images/33dc63f1.png) 303 | 304 | 当然看这个栈对我们没用,我们要做的就是逐步往前定位, 直到定位到真正生成Cookie的地方,但是呢,本脚本只能帮你打个断点, 后面星辰大海的征程就要靠你自己啦! 305 | 306 | #### 5.2.2 Cookie名字和事件结合 307 | 308 | 在名为`foo`的Cookie被`添加`时进入断点: 309 | 310 | ```js 311 | const debuggerRules = [{"add": "foo"}]; 312 | ``` 313 | 314 | 在名为`foo`的Cookie被`删除`时进入断点: 315 | 316 | ```js 317 | const debuggerRules = [{"delete": "foo"}]; 318 | ``` 319 | 320 | 在名为`foo`的Cookie已经存在但是值被`更新`时进入断点: 321 | 322 | ```js 323 | const debuggerRules = [{"update": "foo"}]; 324 | ``` 325 | 326 | 条件可以同时指定多个,在`添加和更新`时进入断点,相当于是把删除排除在外: 327 | 328 | ```js 329 | const debuggerRules = [{"add|update": "foo"}]; 330 | ``` 331 | 332 | 涉及到Cookie名字匹配的地方都可以使用字符串或者正则: 333 | 334 | ```js 335 | const debuggerRules = [{"add": /foo_\d+/}]; 336 | ``` 337 | 338 | ### 5.3 标准的配置规则 339 | 340 | 上面的简化规则会被转化为标准规则,您也可以直接在`debuggerRules`数组中配置标准规则, 一条标准的规则的格式: 341 | 342 | ```text 343 | { 344 | "events": "{add|delete|update}", 345 | "name": {"cookie-name" | /cookie-name-regex/}, 346 | "value": {"cookie-value" | /cookie-value-regex/} 347 | } 348 | ``` 349 | 350 | #### events: 351 | 352 | 字符串类型,表示此条规则匹配的事件类型,可以是单个事件,比如`add`, 也可以是多个事件,多个事件之间使用`|`来分隔,比如`add|update`, 如果觉得挤的话还可以在`|`两侧加空格,比如`add | update` 353 | 当配置了事件类型时只会匹配给定的事件类型,当不配置此选项时,默认匹配所有事件类型。 354 | 355 | #### name: 356 | 357 | 可以是字符串,也可以是正则,当Cookie的名字匹配给定的字符串或者正则时为true, 此条不可忽略必须配置。 358 | 359 | #### value: 360 | 361 | 可以是字符串,也可以是正则,当Cookie的值匹配给定的字符串或者正则时此规则为true, 可以不配置,不配置则会忽略此选项。 362 | 363 | ### 5.4 事件类型详解 364 | 365 | 前面介绍断点规则的配置,多次提到了事件类型, 我们只知道每个事件对应的名字的字符串是啥了, 但是还不知道每种事件意味着底层发生了啥, 本部分就是解释每种事件的实现机制。 366 | 367 | Cookie发生了变化细分为增加Cookie、删除Cookie、更新已有的Cookie的值,其中每个事件对应着一个事件名字: 368 | 369 | - 增加Cookie(add) 370 | - 删除Cookie(delete) 371 | - 更新Cookie(update) 372 | 373 | #### 增加Cookie事件 374 | 375 | Cookie之前在本地不存在,这是第一次添加。 有可能是第一次访问这个网站 ,也有可能是清除了Cookie重新访问,或者是每次访问网站都会生成新的Cookie, 376 | 甚至可能是网站自己的代码把Cookie删了重新添加,这都会触发增加Cookie事件。 377 | 378 | 比如执行下面的代码,这里为了保证Cookie之前不存在,在cookie的名字中加了时间戳: 379 | 380 | ```js 381 | document.cookie = "foo_" + new Date().getTime() + "=bar; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/"; 382 | ``` 383 | 384 | 当我们在控制台运行这行代码的时候,就会触发Cookie添加事件: 385 | 386 | ![](images/README_images/10ea2db6.png) 387 | 388 | #### 更新Cookie事件 389 | 390 | 当一个Cookie在本地已经存在,然后又尝试为它设置值,就会触发更新Cookie事件。 391 | 392 | 比如下面的代码: 393 | 394 | ```js 395 | document.cookie = "foo_10086=blabla; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/"; 396 | document.cookie = "foo_10086=wuawua; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/"; 397 | ``` 398 | 399 | 第一条设置Cookie的语句会触发Cookie新增事件, 第二条设置Cookie的语句因为要设置的Cookie已经存在了, 所以触发了Cookie更新事件。 400 | 401 | ![](images/README_images/fa06f80c.png) 402 | 403 | #### 删除Cookie事件 404 | 405 | 如果前端开发者在设置Cookie的时候,给了一个早于当前时间的expires, 则意味着要删除这个Cookie,比如一种常见的删除Cookie的方式: 406 | 407 | ```js 408 | const expires = new Date(new Date().getTime() - 1000 * 30).toGMTString(); 409 | document.cookie = "foo=; expires=" + expires + "; path=/" 410 | ``` 411 | 412 | 当我们在控制台运行这段代码时,就会触发Cookie删除事件: 413 | 414 | ![](images/README_images/35720fae.png) 415 | 416 | 由上面也可以看出来,触发Cookie删除事件纯粹是检测expires, 并不会真的去检查这个Cookie之前是否存在。 417 | 418 | ### 5.5 控制事件类型断点是否开启的标志位 419 | 420 | 前面介绍了在配置Cookie断点规则的时候有个事件类型, 事实上每个事件类型都对应着一个此事件类型的断点是否开启的标志位, 这个标志位的优先级是最高的,比如没有开启删除Cookie断点的情况下,触发了Cookie删除事件, 421 | 会先检查Cookie删除断点是否开启标志位,如果是关闭的, 则直接忽略本次事件不再尝试匹配断点规则 (开发者工具控制台上是仍然会打印本次删除事件的日志的)。 422 | 423 | 所以现在的情况就变得非常复杂了,让我们再捋一下这一个小小的Cookie断点要走的流程: 424 | 425 | 1. 触发Cookie增加、删除、修改事件,然后检查对应的事件类型断点是否开启 426 | 2. 如果没有开启,则忽略,如果已经开启,则顺序检查是否匹配给定的规则 427 | 3. 每匹配成功一条规则,则进入断点一次. 428 | 429 | 默认情况下只开启了Cookie增加事件和Cookie修改事件的断点: 430 | 431 | ![](images/README_images/2a5b0f6c.png) 432 | 433 | 因为一般情况下,增加Cookie和更新Cookie可以混为一谈,它们都是为Cookie赋了一个值, 而大部分情况下我们不会关注Cookie被删除的事件,所以这里就这么设置了,如果无法满足你的需求, 434 | 可以自行修改`enableEventDebugger`对应的值。 435 | 436 | ## 六、 问题反馈 437 | 438 | 在使用的过程过程中遇到任何问题,可以在GitHub的`Issues`中反馈, 也可以在油猴脚本的评论区反馈,还可以给我发邮件,我看到之后会尽快处理。 439 | 440 | ## 七、FAQ 441 | 442 | ### 7.1 如何调整控制台打印的字体大小? 443 | 444 | 从v0.6版本开始增加了一个变量用于调整本脚本在控制台打印的日志的字体大小,单位为px: 445 | 446 | ![](images/README_images/8b47aea4.png) 447 | 448 | 随着版本迭代,可能不在这个位置了,如果一下找不到,就在代码搜索: 449 | 450 | ```js 451 | consoleLogFontSize 452 | ``` 453 | 454 | 然后修改这个变量的值即可。 455 | 456 | 或者另一种方案,可以在开发者工具控制台按住Ctrl+鼠标滚轮缩放调整整体大小, 这是Chrome浏览器自带的功能。 457 | 458 | ### 7.2 为什么Cookie明明是JS设置的,但是没有Hook到?你个大骗子! :( 459 | 460 | 在本文的最开始就交代了,本脚本要能够成功的注入到页面的开头部分并且执行才能够Hook成功, 对于类似于加速乐第一层那种整个页面只返回一个script,里面是这种逻辑: 461 | 462 | ```html 463 | 464 | 468 | ``` 469 | 470 | 设置了Cookie并且立刻就重定向到了新的页面,对于这种操作,有可能会Hook不到,这是油猴脚本的问题,如果坚持要Hook, 可以采用挂代理将本脚本注入到这个URL的响应的头部。 471 | 472 | # 八、实战示例 473 | 474 | 此页面下是一些使用此脚本逆向的实战例子汇总: 475 | [点我进入导航页](docs) 476 | 477 | # 九、其它 478 | 479 | 本项目拆分自: 480 | [https://github.com/CC11001100/crawler-js-hook-framework-public/tree/master/001-cookie-hook#%E7%9B%91%E6%8E%A7%E5%AE%9A%E4%BD%8Djavascript%E6%93%8D%E4%BD%9Ccookie](https://github.com/CC11001100/crawler-js-hook-framework-public/tree/master/001-cookie-hook#%E7%9B%91%E6%8E%A7%E5%AE%9A%E4%BD%8Djavascript%E6%93%8D%E4%BD%9Ccookie) 481 | 482 | 更改了namespace,可能安装量要清零了,截图纪念下,截止到目前(2022-7-29 21:40:01)安装量破三百了,感觉对于这么窄领域的一个小工具来说很不容易了... 483 | 484 | ![image-20220729214105718](README.assets/image-20220729214105718.png) 485 | 486 | # 十、感谢支持 487 | 感谢热心网友反馈问题,谢谢支持。 488 | 489 |
490 | 502 |
503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | # 十一、Contributors 512 | 513 | 514 | 515 | # 十二、Star History 516 | 517 | 518 | 519 | 520 | # 十三、404星链计划 521 | 522 | 523 | js-cookie-monitor-debugger-hook 现已加入 [404星链计划](https://github.com/knownsec/404StarLink) 524 | 525 | # 十四、逆向技术交流群 526 | 527 | 扫码加入逆向技术交流群: 528 | 529 | 530 | 531 | 如群二维码过期,可以加我个人微信,发送【逆向群】拉你进群: 532 | 533 | 534 | 535 | [点此](https://t.me/jsreijsrei)或扫码加入TG交流群: 536 | 537 | 538 | 539 | -------------------------------------------------------------------------------- /banner.txt: -------------------------------------------------------------------------------- 1 | 2 | ▗▄▄▄▖▗▖ ▗▖▗▄▄▖ ▗▄▄▄▖ ▗▄▄▖ ▗▄▄▖▗▄▄▖ ▗▄▄▄▖▗▄▄▖▗▄▄▄▖ 3 | █ ▝▚▞▘ ▐▌ ▐▌▐▌ ▐▌ ▐▌ ▐▌ ▐▌ █ ▐▌ ▐▌ █ 4 | █ ▐▌ ▐▛▀▘ ▐▛▀▀▘ ▝▀▚▖▐▌ ▐▛▀▚▖ █ ▐▛▀▘ █ 5 | █ ▐▌ ▐▌ ▐▙▄▄▖▗▄▄▞▘▝▚▄▄▖▐▌ ▐▌▗▄█▄▖▐▌ █ 6 | 7 | 8 | 9 | ▗▖ ▗▖ ▗▄▄▖▗▄▄▄▖▗▄▄▖ ▗▄▄▖ ▗▄▄▖▗▄▄▖ ▗▄▄▄▖▗▄▄▖▗▄▄▄▖ 10 | ▐▌ ▐▌▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌ ▐▌ ▐▌ █ ▐▌ ▐▌ █ 11 | ▐▌ ▐▌ ▝▀▚▖▐▛▀▀▘▐▛▀▚▖ ▝▀▚▖▐▌ ▐▛▀▚▖ █ ▐▛▀▘ █ 12 | ▝▚▄▞▘▗▄▄▞▘▐▙▄▄▖▐▌ ▐▌▗▄▄▞▘▝▚▄▄▖▐▌ ▐▌▗▄█▄▖▐▌ █ 13 | 14 | 15 | 16 | ▗▄▄▄▖▗▄▄▄▖▗▖ ▗▖▗▄▄▖ ▗▖ ▗▄▖▗▄▄▄▖▗▄▄▄▖ 17 | █ ▐▌ ▐▛▚▞▜▌▐▌ ▐▌▐▌ ▐▌ ▐▌ █ ▐▌ 18 | █ ▐▛▀▀▘▐▌ ▐▌▐▛▀▘ ▐▌ ▐▛▀▜▌ █ ▐▛▀▀▘ 19 | █ ▐▙▄▄▖▐▌ ▐▌▐▌ ▐▙▄▄▖▐▌ ▐▌ █ ▐▙▄▄▖ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /dev-header.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JS Cookie Monitor/Debugger Hook 3 | // @namespace https://github.com/CC11001100/js-cookie-monitor-debugger-hook 4 | // @version 0.11 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 GM_registerMenuCommand 11 | // @require file:///Users/cc11001100/github/JSREI/js-cookie-monitor-debugger-hook/dist/index.js 12 | // ==/UserScript== 13 | 14 | (() => { 15 | 16 | })() 17 | -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/.DS_Store -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 逆向实例导航 2 | 3 | 欢迎提交使用此脚本进行逆向的例子,可以是一个链接,也可以是一个markdown放在此目录下, 4 | 只要是用到了此脚本即可,后续本人也会陆续添加一些实际例子。 5 | 6 | - [同花顺行情中心列表页Cookie加密](q.10jqka.com.cn-RE) 7 | - [搜狐闪电邮箱登录](mail.sohu.com-RE) 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/mail.sohu.com-RE/README.assets/image-20240908175036662.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/mail.sohu.com-RE/README.assets/image-20240908175036662.png -------------------------------------------------------------------------------- /docs/mail.sohu.com-RE/README.assets/image-20240908175134169.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/mail.sohu.com-RE/README.assets/image-20240908175134169.png -------------------------------------------------------------------------------- /docs/mail.sohu.com-RE/README.assets/image-20240908175155333.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/mail.sohu.com-RE/README.assets/image-20240908175155333.png -------------------------------------------------------------------------------- /docs/mail.sohu.com-RE/README.assets/image-20240908175230081.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/mail.sohu.com-RE/README.assets/image-20240908175230081.png -------------------------------------------------------------------------------- /docs/mail.sohu.com-RE/README.assets/image-20240908175510868.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/mail.sohu.com-RE/README.assets/image-20240908175510868.png -------------------------------------------------------------------------------- /docs/mail.sohu.com-RE/README.assets/image-20240908175633235.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/mail.sohu.com-RE/README.assets/image-20240908175633235.png -------------------------------------------------------------------------------- /docs/mail.sohu.com-RE/README.md: -------------------------------------------------------------------------------- 1 | # 搜狐闪电邮箱登录 2 | 3 | 可以看到在登录的时候有地方悄悄设置了cookie,要携带这个cookie才能登录成功: 4 | 5 | ![image-20240908175510868](./README.assets/image-20240908175510868.png) 6 | 7 | 直接单击日志里的代码链接,跳转到了一个eval方法,看起来也没有太多有用的信息: 8 | 9 | ![image-20240908175633235](./README.assets/image-20240908175633235.png) 10 | 11 | 尝试加断点,在油猴这里编辑脚本: 12 | 13 | ![image-20240908175155333](./README.assets/image-20240908175155333.png) 14 | 15 | 给这个名为`jv`的cookie设置上断点: 16 | 17 | ![image-20240908175230081](./README.assets/image-20240908175230081.png) 18 | 19 | 然后刷新页面再点击登录按钮设置cookie的时候就触发了断点: 20 | 21 | ![image-20240908175134169](./README.assets/image-20240908175134169.png) 22 | 23 | 直接追到了VM里面,可以看到这就是设置的cookie的内容: 24 | 25 | ![image-20240908175036662](./README.assets/image-20240908175036662.png) 26 | 27 | 28 | 29 | # 参考资料 30 | 31 | - https://github.com/JSREP/mail.sohu.com-RE 32 | 33 | - 或者使用这个工具对jsfuck解密 https://github.com/JSREI/eval-decoder -------------------------------------------------------------------------------- /docs/old-code-backup/js-cookie-monitor-debugger-hook.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JS Cookie Monitor/Debugger Hook 3 | // @namespace https://github.com/CC11001100/js-cookie-monitor-debugger-hook 4 | // @version 0.11 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, "update": true, "delete": true, "read": true, 25 | } 26 | 27 | // 在控制台打印日志时字体大小,根据自己喜好调整 28 | // 众所周知,12px是宇宙通用大小 29 | const consoleLogFontSize = 12; 30 | 31 | // 使用document.cookie更新cookie,但是cookie新的值和原来的值一样,此时要不要忽略这个事件 32 | const ignoreUpdateButNotChanged = false; 33 | 34 | // 网站的开发者也可能会使用到Object.,这会与工具内置的冲突,使用这个变量持有者目标网站开发者自己设置的 35 | // 然后在执行的时候使其真正的生效,这样不影响原有的逻辑 36 | let realDocumentCookieProperty = null; 37 | 38 | // 用于区分是本插件自己调用的definePropertyIsMe还是外部调用的 39 | const definePropertyIsMe = "CC11001100-js-cookie-monitor-debugger-hook"; 40 | 41 | // 页面内部的Object.defineProperty需要能够劫持一下 42 | (function () { 43 | 44 | // 把Object.defineProperty给拦截了 45 | Object.defineProperty = new Proxy(Object.defineProperty, { 46 | apply: function (target, thisArg, argArray) { 47 | 48 | // 检查是否是自己调用的 49 | const isMe = argArray && argArray.length >= 3 && argArray[2] && definePropertyIsMe in argArray[2]; 50 | 51 | // 检查是否是定义的document.cookie 52 | const isDocumentCookie = argArray && argArray.length >= 2 && argArray[0] === document && "cookie" === argArray[1]; 53 | 54 | if (!isMe && isDocumentCookie) { 55 | // 检查要定义访问符的是否是document.cookie这个方法的话就包装一下,保证同时多个都能被调用到 56 | if (argArray && argArray.length >= 3) { 57 | // 更新一下real property就不管了, 58 | realDocumentCookieProperty = argArray[2]; 59 | return; 60 | } 61 | } 62 | return target.apply(thisArg, argArray); 63 | } 64 | }); 65 | 66 | Object.defineProperty.toString = function () { 67 | return "function defineProperty() { [native code] }"; 68 | } 69 | 70 | // 把Object.defineProperties也给拦截了 71 | Object.defineProperties = new Proxy(Object.defineProperties, { 72 | apply: function (target, thisArg, argArray) { 73 | // 可能会通过如下代码来调用: 74 | // Object.defineProperties(document, {"cookie": {...}) 75 | const isDocumentCookie = argArray && argArray.length >= 2 && document === argArray[0] && "cookie" in argArray[1]; 76 | if (isDocumentCookie) { 77 | // 把要设置的property描述符持有者 78 | realDocumentCookieProperty = argArray[1]["cookie"]; 79 | // 任务这个cookie的define已经执行完了,将其删除掉 80 | delete argArray[1]["cookie"]; 81 | // 如果只有一个cookie的话,删除完没有其它的属性了,则没必要继续往下了 82 | // 如果有剩余的属性的话,则需要原样继续执行 83 | if (!Object.keys(argArray[1]).length) { 84 | return; 85 | } 86 | } 87 | return target.apply(thisArg, argArray); 88 | } 89 | }); 90 | 91 | Object.defineProperties.toString = function () { 92 | return "function defineProperties() { [native code] }"; 93 | } 94 | 95 | })(); 96 | 97 | // 此处实现的反复hook,保证页面流程能够继续往下走下去 98 | (function addCookieHook() { 99 | const handler = { 100 | get: () => { 101 | 102 | // 先恢复原状 103 | delete document.cookie; 104 | 105 | try { 106 | // 如果网站开发者有设置自己的属性访问符的话,则以他设置的为准,把它的返回值作为此函数最终的返回值,保持其原有逻辑 107 | if (realDocumentCookieProperty && "get" in realDocumentCookieProperty) { 108 | // 在网站执行者自己定义的cookie的property执行期间,我们的工具添加的hook是被下掉的,所以是没有影响的 109 | // fix #13 此处的this需要绑定为document 110 | return realDocumentCookieProperty["get"].apply(document, arguments); 111 | } else { 112 | // 如果网站开发者没有设置自己的property的话,则获取到真正的cookie值返回 113 | return document.cookie; 114 | } 115 | } finally { 116 | // 然后这么获取完之后,还是要把hook加上 117 | addCookieHook(); 118 | } 119 | 120 | }, set: newValue => { 121 | 122 | // 先触发相关的事件 123 | cc11001100_onSetCookie(newValue); 124 | 125 | // 然后恢复原状,把我们设置的hook啥的下掉 126 | delete document.cookie; 127 | 128 | try { 129 | // 如果网站开发者有设置自己的属性访问符的话,则以他设置的为准 130 | if (realDocumentCookieProperty && "set" in realDocumentCookieProperty) { 131 | // 在网站执行者自己定义的cookie的property执行期间,我们的工具添加的hook是被下掉的,所以是没有影响的 132 | // 不过这同时带来一个新的问题,就是如果它在这个property中进行cookie的操作我们无法感知到,那能怎么办呢?有得必有失 133 | // TODO 2023-7-26 22:02:11 那,有没有比较简单的“我全都要”的方案呢? 134 | // fix #13 此处的this需要绑定为document 135 | realDocumentCookieProperty["set"].apply(document, [newValue]); 136 | } else { 137 | // 如果网站开发者没有设置property或者没有设置set的话,则还是走默认的赋值逻辑 138 | document.cookie = newValue; 139 | } 140 | } finally { 141 | // 然后再把hook设置上,加在finally里保证就算出错了也能恢复hook 142 | addCookieHook(); 143 | } 144 | 145 | }, configurable: true, enumerable: false, 146 | }; 147 | handler[definePropertyIsMe] = true; 148 | Object.defineProperty(document, "cookie", handler); 149 | })(); 150 | 151 | /** 152 | * 这个方法的前缀起到命名空间的作用,等下调用栈追溯赋值cookie的代码时需要用这个名字作为终结标志 153 | * 154 | * @param newValue 155 | */ 156 | function cc11001100_onSetCookie(newValue) { 157 | const cookiePair = parseSetCookie(newValue); 158 | const currentCookieMap = getCurrentCookieMap(); 159 | 160 | // 如果过期时间为当前时间之前,则为删除,有可能没设置?虽然目前为止没碰到这样的... 161 | if (cookiePair.expires !== null && new Date().getTime() >= cookiePair.expires) { 162 | onDeleteCookie(newValue, cookiePair.name, cookiePair.value || (currentCookieMap.get(cookiePair.name) || {}).value); 163 | return; 164 | } 165 | 166 | // 如果之前已经存在,则是修改 167 | if (currentCookieMap.has(cookiePair.name)) { 168 | onUpdateCookie(newValue, cookiePair.name, currentCookieMap.get(cookiePair.name).value, cookiePair.value); 169 | return; 170 | } 171 | 172 | // 否则则为添加 173 | onAddCookie(newValue, cookiePair.name, cookiePair.value); 174 | } 175 | 176 | function onReadCookie(cookieOriginalValue, cookieName, cookieValue) { 177 | 178 | } 179 | 180 | function onDeleteCookie(cookieOriginalValue, cookieName, cookieValue) { 181 | const valueStyle = `color: black; background: #E50000; font-size: ${consoleLogFontSize}px; font-weight: bold;`; 182 | const normalStyle = `color: black; background: #FF6766; font-size: ${consoleLogFontSize}px;`; 183 | 184 | const message = [ 185 | 186 | normalStyle, now(), 187 | 188 | normalStyle, "JS Cookie Monitor: ", 189 | 190 | normalStyle, "delete cookie, cookieName = ", 191 | 192 | valueStyle, `${cookieName}`, 193 | 194 | ...(() => { 195 | if (!cookieValue) { 196 | return []; 197 | } 198 | return [normalStyle, ", value = ", 199 | 200 | valueStyle, `${cookieValue}`,]; 201 | })(), 202 | 203 | normalStyle, `, code location = ${getCodeLocation()}`]; 204 | console.log(genFormatArray(message), ...message); 205 | 206 | testDebuggerRules(cookieOriginalValue, "delete", cookieName, cookieValue); 207 | } 208 | 209 | function onUpdateCookie(cookieOriginalValue, cookieName, oldCookieValue, newCookieValue) { 210 | 211 | const cookieValueChanged = oldCookieValue !== newCookieValue; 212 | 213 | if (ignoreUpdateButNotChanged && !cookieValueChanged) { 214 | return; 215 | } 216 | 217 | const valueStyle = `color: black; background: #FE9900; font-size: ${consoleLogFontSize}px; font-weight: bold;`; 218 | const normalStyle = `color: black; background: #FFCC00; font-size: ${consoleLogFontSize}px;`; 219 | 220 | const message = [ 221 | 222 | normalStyle, now(), 223 | 224 | normalStyle, "JS Cookie Monitor: ", 225 | 226 | normalStyle, "update cookie, cookieName = ", 227 | 228 | valueStyle, `${cookieName}`, 229 | 230 | ...(() => { 231 | if (cookieValueChanged) { 232 | return [normalStyle, `, oldValue = `, 233 | 234 | valueStyle, `${oldCookieValue}`, 235 | 236 | normalStyle, `, newValue = `, 237 | 238 | valueStyle, `${newCookieValue}`] 239 | } else { 240 | return [normalStyle, `, value = `, 241 | 242 | valueStyle, `${newCookieValue}`,]; 243 | } 244 | })(), 245 | 246 | normalStyle, `, valueChanged = `, 247 | 248 | valueStyle, `${cookieValueChanged}`, 249 | 250 | normalStyle, `, code location = ${getCodeLocation()}`]; 251 | console.log(genFormatArray(message), ...message); 252 | 253 | testDebuggerRules(cookieOriginalValue, "update", cookieName, newCookieValue, cookieValueChanged); 254 | } 255 | 256 | function onAddCookie(cookieOriginalValue, cookieName, cookieValue) { 257 | const valueStyle = `color: black; background: #669934; font-size: ${consoleLogFontSize}px; font-weight: bold;`; 258 | const normalStyle = `color: black; background: #65CC66; font-size: ${consoleLogFontSize}px;`; 259 | 260 | const message = [ 261 | 262 | normalStyle, now(), 263 | 264 | normalStyle, "JS Cookie Monitor: ", 265 | 266 | normalStyle, "add cookie, cookieName = ", 267 | 268 | valueStyle, `${cookieName}`, 269 | 270 | normalStyle, ", cookieValue = ", 271 | 272 | valueStyle, `${cookieValue}`, 273 | 274 | normalStyle, `, code location = ${getCodeLocation()}`]; 275 | console.log(genFormatArray(message), ...message); 276 | 277 | testDebuggerRules(cookieOriginalValue, "add", cookieName, cookieValue); 278 | } 279 | 280 | function now() { 281 | // 东八区专属... 282 | return "[" + new Date(new Date().getTime() + 1000 * 60 * 60 * 8).toJSON().replace("T", " ").replace("Z", "") + "] "; 283 | } 284 | 285 | function genFormatArray(messageAndStyleArray) { 286 | const formatArray = []; 287 | for (let i = 0, end = messageAndStyleArray.length / 2; i < end; i++) { 288 | formatArray.push("%c%s"); 289 | } 290 | return formatArray.join(""); 291 | } 292 | 293 | // 解析当前代码的位置,以便能够直接定位到事件触发的代码位置 294 | function getCodeLocation() { 295 | const callstack = new Error().stack.split("\n"); 296 | while (callstack.length && callstack[0].indexOf("cc11001100") === -1) { 297 | callstack.shift(); 298 | } 299 | callstack.shift(); 300 | callstack.shift(); 301 | 302 | return callstack[0].trim(); 303 | } 304 | 305 | /** 306 | * 将本次设置cookie的字符串解析为容易处理的形式 307 | * 308 | * @param cookieString 309 | * @returns {CookiePair} 310 | */ 311 | function parseSetCookie(cookieString) { 312 | // uuid_tt_dd=10_37476713480-1609821005397-659114; Expires=Thu, 01 Jan 1025 00:00:00 GMT; Path=/; Domain=.csdn.net; 313 | const cookieStringSplit = cookieString.split(";"); 314 | const {key, value} = splitKeyValue(cookieStringSplit.length && cookieStringSplit[0]) 315 | const map = new Map(); 316 | for (let i = 1; i < cookieStringSplit.length; i++) { 317 | let {key, value} = splitKeyValue(cookieStringSplit[i]); 318 | map.set(key.toLowerCase(), value); 319 | } 320 | // 当不设置expires的时候关闭浏览器就过期 321 | const expires = map.get("expires"); 322 | return new CookiePair(key, value, expires ? new Date(expires).getTime() : null) 323 | } 324 | 325 | /** 326 | * 把按照等号=拼接的key、value字符串切分开 327 | * @param s 328 | * @returns {{value: string, key: string}} 329 | */ 330 | function splitKeyValue(s) { 331 | let key = "", value = ""; 332 | const keyValueArray = (s || "").split("="); 333 | 334 | if (keyValueArray.length) { 335 | key = decodeURIComponent(keyValueArray[0].trim()); 336 | } 337 | 338 | if (keyValueArray.length > 1) { 339 | value = decodeURIComponent(keyValueArray.slice(1).join("=").trim()); 340 | } 341 | 342 | return { 343 | key, value 344 | } 345 | } 346 | 347 | /** 348 | * 获取当前所有已经设置的cookie 349 | * 350 | * @returns {Map} 351 | */ 352 | function getCurrentCookieMap() { 353 | const cookieMap = new Map(); 354 | if (!document.cookie) { 355 | return cookieMap; 356 | } 357 | document.cookie.split(";").forEach(x => { 358 | const {key, value} = splitKeyValue(x); 359 | cookieMap.set(key, new CookiePair(key, value)); 360 | }); 361 | return cookieMap; 362 | } 363 | 364 | class DebuggerRule { 365 | 366 | constructor(eventName, cookieNameFilter, cookieValueFilter) { 367 | this.eventName = eventName; 368 | this.cookieNameFilter = cookieNameFilter; 369 | this.cookieValueFilter = cookieValueFilter; 370 | } 371 | 372 | test(eventName, cookieName, cookieValue) { 373 | return this.testByEventName(eventName) && (this.testByCookieNameFilter(cookieName) || this.testByCookieValueFilter(cookieValue)); 374 | } 375 | 376 | testByEventName(eventName) { 377 | // 如果此类型的事件断点没有开启,则直接返回 378 | if (!enableEventDebugger[eventName]) { 379 | return false; 380 | } 381 | // 事件不设置则匹配任何事件 382 | if (!this.eventName) { 383 | return true; 384 | } 385 | return this.eventName === eventName; 386 | } 387 | 388 | testByCookieNameFilter(cookieName) { 389 | if (!cookieName || !this.cookieNameFilter) { 390 | return false; 391 | } 392 | if (typeof this.cookieNameFilter === "string") { 393 | return this.cookieNameFilter === cookieName; 394 | } 395 | if (this.cookieNameFilter instanceof RegExp) { 396 | return this.cookieNameFilter.test(cookieName); 397 | } 398 | return false; 399 | } 400 | 401 | testByCookieValueFilter(cookieValue) { 402 | if (!cookieValue || !this.cookieValueFilter) { 403 | return false; 404 | } 405 | if (typeof this.cookieValueFilter === "string") { 406 | return this.cookieValueFilter === cookieValue; 407 | } 408 | if (this.cookieValueFilter instanceof RegExp) { 409 | return this.cookieValueFilter.test(cookieValue); 410 | } 411 | return false; 412 | } 413 | 414 | } 415 | 416 | // 将规则整理为标准规则 417 | // 解析起来并不复杂,但是有点过于灵活,要介绍清楚打的字要远超代码,所以我文档里就随便介绍下完事有缘人会自己读代码的... 418 | (function standardizingRules() { 419 | 420 | // 用于收集规则配置错误,在解析完所有规则之后一次把事情说完 421 | const ruleConfigErrorMessage = []; 422 | 423 | const newRules = []; 424 | while (debuggerRules.length) { 425 | const rule = debuggerRules.pop(); 426 | 427 | // 如果是字符串或者正则 428 | if (typeof rule === "string" || rule instanceof RegExp) { 429 | newRules.push(new DebuggerRule(null, rule, null)); 430 | continue; 431 | } 432 | 433 | // 如果是字典对象,则似乎有点麻烦 434 | for (let key in rule) { 435 | let events = null; 436 | let cookieNameFilter = null; 437 | let cookieValueFilter = null; 438 | if (key === "events") { 439 | events = rule["events"] || "add | delete | update"; 440 | cookieNameFilter = rule["name"] 441 | cookieValueFilter = rule["value"]; 442 | } else if (key !== "name" && key !== "value") { 443 | events = key; 444 | cookieNameFilter = rule[key]; 445 | cookieValueFilter = rule["value"]; 446 | } else { 447 | // name & value ignore 448 | continue; 449 | } 450 | // cookie的名字是必须配置的 451 | if (!cookieNameFilter) { 452 | const errorMessage = `必须为此条规则 ${JSON.stringify(rule)} 配置一个Cookie Name匹配条件`; 453 | ruleConfigErrorMessage.push(errorMessage); 454 | continue; 455 | } 456 | events.split("|").forEach(eventName => { 457 | eventName = eventName.trim(); 458 | if (eventName !== "add" && eventName !== "delete" && eventName !== "update") { 459 | const errorMessage = `此条规则 ${JSON.stringify(rule)} 的Cookie事件名字配置错误,必须为 add、delete、update 三种之一或者|分隔的组合,您配置的是 ${eventName},仅忽略此无效事件`; 460 | ruleConfigErrorMessage.push(errorMessage); 461 | return; 462 | } 463 | newRules.push(new DebuggerRule(eventName, cookieNameFilter, cookieValueFilter)); 464 | }) 465 | } 466 | } 467 | 468 | // 配置错误的规则会被忽略,其它规则照常生效 469 | if (ruleConfigErrorMessage.length) { 470 | // 错误打印字号要大1.5倍,不信你注意不到 471 | const errorMessageStyle = `color: black; background: #FF2121; font-size: ${Math.round(consoleLogFontSize * 1.5)}px; font-weight: bold;`; 472 | let errorMessage = now() + "JS Cookie Monitor: 以下Cookie断点规则配置错误,已忽略: \n "; 473 | for (let i = 0; i < ruleConfigErrorMessage.length; i++) { 474 | errorMessage += `${i + 1}. ${ruleConfigErrorMessage[i]}\n`; 475 | } 476 | console.log("%c%s", errorMessageStyle, errorMessage); 477 | } 478 | 479 | // 是否需要合并重复规则呢? 480 | // 还是不了,而且静态合并对于正则没办法,用户应该知道自己在做什么 481 | 482 | for (let rule of newRules) { 483 | debuggerRules.push(rule); 484 | } 485 | })(); 486 | 487 | /** 488 | * 当断点停在这里时查看这个方法各个参数的值能够大致了解断点情况 489 | * 490 | * 鼠标移动到变量上查看变量的值 491 | * 492 | * @param setCookieOriginalValue 目标网站使用document.cookie时赋值的原始值是什么,这个值没有 URL decode, 493 | * 如果要分析它请拷贝其值到外面分析,这里只是提供一种可能性 494 | * @param eventName 本次是发生了什么事件,add增加新cookie、update更新cookie的值、delete表示cookie被删除 495 | * @param cookieName 本脚本对setCookieOriginalValue解析出的cookie名字,会被URL decode 496 | * @param cookieValue 本脚本对setCookieOriginalValue解析出的cookie值,会被URL decode 497 | * @param cookieValueChanged 只在update事件时有值,用于帮助快速确定本次update有没有修改cookie的值 498 | */ 499 | function testDebuggerRules(setCookieOriginalValue, eventName, cookieName, cookieValue, cookieValueChanged) { 500 | for (let rule of debuggerRules) { 501 | // rule当前的值表示被什么断点规则匹配到了,可以把鼠标移动到rule变量上查看 502 | if (rule.test(eventName, cookieName, cookieValue)) { 503 | debugger; 504 | } 505 | } 506 | } 507 | 508 | /** 509 | * 用于在本脚本内部表示一条cookie以方便程序处理 510 | * 这里只取了有用的信息,忽略了域名及路径,也许需要加上这两个限制?但现在这个脚本已经够臃肿了... 511 | */ 512 | class CookiePair { 513 | 514 | /** 515 | * 516 | * @param name Cookie的名字 517 | * @param value Cookie的值 518 | * @param expires Cookie的过期时间 519 | */ 520 | constructor(name, value, expires) { 521 | this.name = name; 522 | this.value = value; 523 | this.expires = expires; 524 | } 525 | 526 | } 527 | 528 | } 529 | 530 | )(); 531 | -------------------------------------------------------------------------------- /docs/q.10jqka.com.cn-RE/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/q.10jqka.com.cn-RE/.DS_Store -------------------------------------------------------------------------------- /docs/q.10jqka.com.cn-RE/README.assets/image-20240908170233715.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/q.10jqka.com.cn-RE/README.assets/image-20240908170233715.png -------------------------------------------------------------------------------- /docs/q.10jqka.com.cn-RE/README.assets/image-20240908171817289.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/q.10jqka.com.cn-RE/README.assets/image-20240908171817289.png -------------------------------------------------------------------------------- /docs/q.10jqka.com.cn-RE/README.assets/image-20240908172113950.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/q.10jqka.com.cn-RE/README.assets/image-20240908172113950.png -------------------------------------------------------------------------------- /docs/q.10jqka.com.cn-RE/README.assets/image-20240908172355099.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/q.10jqka.com.cn-RE/README.assets/image-20240908172355099.png -------------------------------------------------------------------------------- /docs/q.10jqka.com.cn-RE/README.assets/image-20240908172500882.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/q.10jqka.com.cn-RE/README.assets/image-20240908172500882.png -------------------------------------------------------------------------------- /docs/q.10jqka.com.cn-RE/README.assets/image-20240908172827707.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/q.10jqka.com.cn-RE/README.assets/image-20240908172827707.png -------------------------------------------------------------------------------- /docs/q.10jqka.com.cn-RE/README.assets/image-20240908173050817.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/docs/q.10jqka.com.cn-RE/README.assets/image-20240908173050817.png -------------------------------------------------------------------------------- /docs/q.10jqka.com.cn-RE/README.md: -------------------------------------------------------------------------------- 1 | # 同花顺行情中心列表页Cookie加密 2 | 3 | # 一、逆向目标 4 | 5 | 在同花顺行情中心的列表页: 6 | 7 | ```bash 8 | https://q.10jqka.com.cn/thshy/ 9 | ``` 10 | 11 | 翻页的时候发送的请求里有一个自定义的请求头`hexin-v`,这个请求头是必须携带的,接下来就是借助`js-cookie-monitor-debugger-hook`来分析这个请求头是如何生成的: 12 | 13 | ![image-20240908170233715](./README.assets/image-20240908170233715.png) 14 | 15 | # 二、分析过程 16 | 17 | 通过上面的请求头可以看到,这个请求头`hexin-v`和名为`v`的cookie的值是一样的,所以接下来就是分析cookie `v`是如何生成的。 18 | 19 | 安装好``js-cookie-monitor-debugger-hook``插件并开启之后,打开开发者工具刷新页面,可以看到`Console`里打印了一些内容,看起来是插件观察到了`v`的生成过程: 20 | 21 | ![image-20240908171817289](./README.assets/image-20240908171817289.png) 22 | 23 | 在油猴脚本这里展开脚本选项,选择“编辑”: 24 | 25 | ![image-20240908172113950](./README.assets/image-20240908172113950.png) 26 | 27 | 然后加上一个断点规则,`["v"]`表示当名为`v`的cookie被操作的时候会进入断点: 28 | 29 | ![image-20240908172355099](./README.assets/image-20240908172355099.png) 30 | 31 | 然后回到原页面刷新页面,自动命中了断点,这个时候就是向上追踪调用栈: 32 | 33 | ![image-20240908172500882](./README.assets/image-20240908172500882.png) 34 | 35 | 往后向上追踪调用栈,可以看到Cookie的生成主要是调用了`rt.update()`这个方法生成的: 36 | 37 | ![image-20240908173050817](./README.assets/image-20240908173050817.png) 38 | 39 | 跟进去发现主要是这个方法生成的,至此逻辑大概明了了,就是采集了一堆信息,然后编码作为`v`的值: 40 | 41 | ![image-20240908172827707](./README.assets/image-20240908172827707.png) 42 | 43 | # 三、参考资料 44 | 45 | - https://github.com/JSREP/q.10jqka.com.cn-RE -------------------------------------------------------------------------------- /fuck-hot-compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 自动安装依赖的热更新脚本 4 | # 使用方式:chmod +x ./fuck-hot-compile.sh && ./fuck-hot-compile.sh 5 | 6 | # 检测包管理器并安装依赖 7 | init_project() { 8 | if command -v yarn &> /dev/null; then 9 | echo "使用 yarn 安装依赖..." 10 | yarn install --frozen-lockfile 11 | elif command -v npm &> /dev/null; then 12 | echo "使用 npm 安装依赖..." 13 | npm ci 14 | else 15 | echo "错误:未检测到 yarn 或 npm,请先安装 Node.js" 16 | exit 1 17 | fi 18 | } 19 | 20 | # 获取构建命令 21 | detect_build_command() { 22 | if [ -f yarn.lock ]; then 23 | echo "yarn build" 24 | else 25 | echo "npm run build" 26 | fi 27 | } 28 | 29 | # ---------- 主流程 ---------- 30 | init_project 31 | build_command=$(detect_build_command) 32 | 33 | echo "启动热更新监听..." 34 | while true; do 35 | echo "[$(date +'%T')] 开始构建..." 36 | if $build_command; then 37 | echo "[$(date +'%T')] 构建成功 ✅" 38 | else 39 | echo "[$(date +'%T')] 构建失败 ❌,10秒后重试..." 40 | sleep 10 41 | fi 42 | sleep 1 43 | done -------------------------------------------------------------------------------- /images/README_images/0cf06995.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/0cf06995.png -------------------------------------------------------------------------------- /images/README_images/10ea2db6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/10ea2db6.png -------------------------------------------------------------------------------- /images/README_images/20f986d7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/20f986d7.png -------------------------------------------------------------------------------- /images/README_images/2a5b0f6c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/2a5b0f6c.png -------------------------------------------------------------------------------- /images/README_images/33dc63f1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/33dc63f1.png -------------------------------------------------------------------------------- /images/README_images/35720fae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/35720fae.png -------------------------------------------------------------------------------- /images/README_images/36eb394d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/36eb394d.png -------------------------------------------------------------------------------- /images/README_images/45ecea34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/45ecea34.png -------------------------------------------------------------------------------- /images/README_images/47c3b465.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/47c3b465.png -------------------------------------------------------------------------------- /images/README_images/5415caa1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/5415caa1.png -------------------------------------------------------------------------------- /images/README_images/676ecd0d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/676ecd0d.png -------------------------------------------------------------------------------- /images/README_images/82fec90f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/82fec90f.png -------------------------------------------------------------------------------- /images/README_images/8b47aea4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/8b47aea4.png -------------------------------------------------------------------------------- /images/README_images/fa06f80c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/README_images/fa06f80c.png -------------------------------------------------------------------------------- /images/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img.png -------------------------------------------------------------------------------- /images/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img_1.png -------------------------------------------------------------------------------- /images/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img_2.png -------------------------------------------------------------------------------- /images/img_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img_3.png -------------------------------------------------------------------------------- /images/img_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img_4.png -------------------------------------------------------------------------------- /images/img_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img_5.png -------------------------------------------------------------------------------- /images/img_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img_6.png -------------------------------------------------------------------------------- /images/img_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img_7.png -------------------------------------------------------------------------------- /images/img_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img_8.png -------------------------------------------------------------------------------- /images/img_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSREI/js-cookie-monitor-debugger-hook/1d8ede4a83a609a139ddab51dbe1d45af8086b6c/images/img_9.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-cookie-monitor-debugger-hook", 3 | "version": "0.11", 4 | "description": "用于监控js对cookie的修改,或者在cookie符合给定条件时进入断点", 5 | "main": "index.js", 6 | "repository": "https://github.com/CC11001100/js-cookie-monitor-debugger-hook.git", 7 | "namespace": "https://github.com/CC11001100/js-cookie-monitor-debugger-hook", 8 | "document": "https://github.com/CC11001100/js-cookie-monitor-debugger-hook", 9 | "scripts": { 10 | "build": "webpack --config webpack.prod.js", 11 | "watch": "webpack --watch --config webpack.dev.js", 12 | "gen:dev": "node ./scripts/generate-dev-header.js", 13 | "build:dev": "npm run build && npm run gen:dev" 14 | }, 15 | "author": "CC11001100 ", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@types/node": "^22.13.1", 19 | "@types/webpack": "^5.28.5", 20 | "ts-loader": "^9.5.2", 21 | "typescript": "^5.7.3", 22 | "webpack": "^5.88.2", 23 | "webpack-cli": "^5.1.4", 24 | "webpack-merge": "^5.9.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/generate-dev-header.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * 开发头部文件生成工具 4 | * 5 | * 这个脚本会基于userscript-headers.js文件自动生成dev-header.js, 6 | * 并且根据当前操作系统自动添加正确格式的本地文件路径引用。 7 | * 8 | * 使用方法: 9 | * 1. 直接运行: npm run gen:dev 10 | * 2. 构建并生成: npm run build:dev 11 | * 12 | * 生成的dev-header.js文件可以直接复制到Tampermonkey中创建新脚本, 13 | * 实现对本地开发文件的引用,方便调试。 14 | * 15 | * 支持的系统: 16 | * - Windows: 使用file:///C:/path/format 17 | * - Mac/Linux: 使用file:///path/format 18 | */ 19 | 20 | const fs = require('fs'); 21 | const path = require('path'); 22 | const os = require('os'); 23 | 24 | // 项目根目录路径 25 | const rootPath = path.resolve(__dirname, '..'); 26 | 27 | // 读取userscript-headers.js内容 28 | const headerPath = path.join(rootPath, 'userscript-headers.js'); 29 | let headerContent = fs.readFileSync(headerPath, 'utf8'); 30 | 31 | // 获取dist/index.js的绝对路径 32 | let distFilePath = path.join(rootPath, 'dist', 'index.js'); 33 | 34 | // 根据操作系统格式化文件URL 35 | let fileUrl; 36 | if (os.platform() === 'win32') { 37 | // Windows格式: file:///C:/path/to/file.js 38 | fileUrl = 'file:///' + distFilePath.replace(/\\/g, '/'); 39 | } else { 40 | // Mac/Linux格式: file:///path/to/file.js 41 | // 确保路径以单个斜杠开始 42 | if (distFilePath.startsWith('/')) { 43 | fileUrl = 'file://' + distFilePath; 44 | } else { 45 | fileUrl = 'file:///' + distFilePath; 46 | } 47 | } 48 | 49 | // 在头部添加@require行 50 | const lastHeaderLine = '// ==/UserScript=='; 51 | const requireLine = `// @require ${fileUrl}`; 52 | 53 | // 替换最后一行头部,添加@require和最后一行 54 | headerContent = headerContent.replace( 55 | lastHeaderLine, 56 | `${requireLine}\n${lastHeaderLine}` 57 | ); 58 | 59 | // 添加空函数以匹配原始dev-header.js结构 60 | headerContent += `\n(() => {\n\n})()\n`; 61 | 62 | // 写入到dev-header.js 63 | const devHeaderPath = path.join(rootPath, 'dev-header.js'); 64 | fs.writeFileSync(devHeaderPath, headerContent, 'utf8'); 65 | 66 | console.log(`\x1b[32m✓\x1b[0m 已生成开发头部文件: ${devHeaderPath}`); 67 | console.log(`\x1b[34m→\x1b[0m 使用本地文件路径: ${fileUrl}`); -------------------------------------------------------------------------------- /src/cookie-monitor/config.ts: -------------------------------------------------------------------------------- 1 | import { EventDebuggerConfig } from '../types'; 2 | import logger from '../logger/logger'; 3 | 4 | // @ts-ignore - 为与原JS版本保持一致,忽略类型检查 5 | export const debuggerRules: Array = []; 6 | // example: 7 | // const debuggerRules = ["foo", /foo_\d+/]; 8 | 9 | // 设置事件断点是否开启,一般保持默认即可 10 | const enableEventDebugger: EventDebuggerConfig = { 11 | "add": true, "update": true, "delete": true, "read": true, 12 | }; 13 | 14 | // 在控制台打印日志时字体大小,根据自己喜好调整 15 | // 众所周知,12px是宇宙通用大小 16 | const consoleLogFontSize = 12; 17 | 18 | // 使用document.cookie更新cookie,但是cookie新的值和原来的值一样,此时要不要忽略这个事件 19 | const ignoreUpdateButNotChanged = false; 20 | 21 | // 网站的开发者也可能会使用到Object.,这会与工具内置的冲突,使用这个变量持有者目标网站开发者自己设置的 22 | // 然后在执行的时候使其真正的生效,这样不影响原有的逻辑 23 | let realDocumentCookieProperty: PropertyDescriptor | null = null; 24 | 25 | // 用于区分是本插件自己调用的definePropertyIsMe还是外部调用的 26 | export const definePropertyIsMe = "CC11001100-js-cookie-monitor-debugger-hook"; 27 | 28 | // 初始化日志配置 29 | export function initLoggerConfig(): void { 30 | logger.setConfig({ 31 | fontSize: consoleLogFontSize, 32 | prefix: 'JS Cookie Monitor:' 33 | }); 34 | } 35 | 36 | // 导出获取配置的函数 37 | export function getEventDebuggerConfig(): EventDebuggerConfig { 38 | return enableEventDebugger; 39 | } 40 | 41 | export function getConsoleLogFontSize(): number { 42 | return consoleLogFontSize; 43 | } 44 | 45 | export function getIgnoreUpdateButNotChanged(): boolean { 46 | return ignoreUpdateButNotChanged; 47 | } 48 | 49 | export function getRealDocumentCookieProperty(): PropertyDescriptor | null { 50 | return realDocumentCookieProperty; 51 | } 52 | 53 | export function setRealDocumentCookieProperty(property: PropertyDescriptor): void { 54 | realDocumentCookieProperty = property; 55 | } -------------------------------------------------------------------------------- /src/cookie-monitor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './init'; -------------------------------------------------------------------------------- /src/cookie-monitor/init.ts: -------------------------------------------------------------------------------- 1 | import { installPropertyHooks, installCookieHooks } from '../hooks'; 2 | import { standardizingRules } from '../rules'; 3 | import { initLoggerConfig } from './config'; 4 | 5 | /** 6 | * 初始化Cookie监控器 7 | * 安装钩子并标准化规则 8 | */ 9 | export function initCookieMonitor(): void { 10 | // 使用文档: https://github.com/CC11001100/js-cookie-monitor-debugger-hook 11 | 12 | // 初始化日志配置 13 | initLoggerConfig(); 14 | 15 | // 安装属性钩子 16 | installPropertyHooks(); 17 | 18 | // 安装Cookie钩子 19 | installCookieHooks(); 20 | 21 | // 标准化规则配置 22 | standardizingRules(); 23 | } -------------------------------------------------------------------------------- /src/events/handlers.ts: -------------------------------------------------------------------------------- 1 | import { getIgnoreUpdateButNotChanged } from '../cookie-monitor/config'; 2 | import { testDebuggerRules } from '../rules/tester'; 3 | import { getCurrentCookieMap, parseSetCookie } from './parser'; 4 | import logger from '../logger/logger'; 5 | 6 | /** 7 | * 这个方法的前缀起到命名空间的作用,等下调用栈追溯赋值cookie的代码时需要用这个名字作为终结标志 8 | * 9 | * @param newValue Cookie新值 10 | */ 11 | export function cc11001100_onSetCookie(newValue: string): void { 12 | const cookiePair = parseSetCookie(newValue); 13 | const currentCookieMap = getCurrentCookieMap(); 14 | 15 | // 如果过期时间为当前时间之前,则为删除,有可能没设置?虽然目前为止没碰到这样的... 16 | if (cookiePair.expires !== null && new Date().getTime() >= cookiePair.expires) { 17 | onDeleteCookie(newValue, cookiePair.name, cookiePair.value || (currentCookieMap.get(cookiePair.name)?.value || null)); 18 | return; 19 | } 20 | 21 | // 如果之前已经存在,则是修改 22 | if (currentCookieMap.has(cookiePair.name)) { 23 | onUpdateCookie(newValue, cookiePair.name, currentCookieMap.get(cookiePair.name)!.value || "", cookiePair.value); 24 | return; 25 | } 26 | 27 | // 否则则为添加 28 | onAddCookie(newValue, cookiePair.name, cookiePair.value); 29 | } 30 | 31 | /** 32 | * 处理读取Cookie事件 33 | * @param cookieOriginalValue 原始Cookie字符串 34 | * @param cookieName Cookie名称 35 | * @param cookieValue Cookie值 36 | */ 37 | export function onReadCookie(cookieOriginalValue: string, cookieName: string, cookieValue: string): void { 38 | // 虽然原JS版本中这个函数是空的,但为了完整性添加实现 39 | // 需要触发读取Cookie功能时,取消以下代码的注释 40 | 41 | /* 42 | logger.custom('#4169E1', 'black', 43 | "read cookie, cookieName = ", cookieName, 44 | ", value = ", cookieValue 45 | ); 46 | 47 | testDebuggerRules(cookieOriginalValue, "read", cookieName, cookieValue, null); 48 | */ 49 | } 50 | 51 | /** 52 | * 处理删除Cookie事件 53 | * @param cookieOriginalValue 原始Cookie字符串 54 | * @param cookieName Cookie名称 55 | * @param cookieValue Cookie值 56 | */ 57 | export function onDeleteCookie(cookieOriginalValue: string, cookieName: string, cookieValue: string | null): void { 58 | logger.custom('#E50000', 'black', 59 | "delete cookie, cookieName = ", cookieName, 60 | ...(cookieValue ? [", value = ", cookieValue] : []) 61 | ); 62 | 63 | // @ts-ignore - 保持与原JS版本一致的调用方式 64 | testDebuggerRules(cookieOriginalValue, "delete", cookieName, cookieValue); 65 | } 66 | 67 | /** 68 | * 处理更新Cookie事件 69 | * @param cookieOriginalValue 原始Cookie字符串 70 | * @param cookieName Cookie名称 71 | * @param oldCookieValue 旧Cookie值 72 | * @param newCookieValue 新Cookie值 73 | */ 74 | export function onUpdateCookie(cookieOriginalValue: string, cookieName: string, oldCookieValue: string, newCookieValue: string | null): void { 75 | const cookieValueChanged = oldCookieValue !== newCookieValue; 76 | 77 | // 忽略cookie更新但值没变的情况 78 | if (getIgnoreUpdateButNotChanged() && !cookieValueChanged) { 79 | return; 80 | } 81 | 82 | logger.custom('#FE9900', 'black', 83 | "update cookie, cookieName = ", cookieName, 84 | ...(cookieValueChanged 85 | ? [", oldValue = ", oldCookieValue, ", newValue = ", newCookieValue] 86 | : [", value = ", newCookieValue] 87 | ), 88 | ", valueChanged = ", String(cookieValueChanged) 89 | ); 90 | 91 | testDebuggerRules(cookieOriginalValue, "update", cookieName, newCookieValue, cookieValueChanged); 92 | } 93 | 94 | /** 95 | * 处理添加Cookie事件 96 | * @param cookieOriginalValue 原始Cookie字符串 97 | * @param cookieName Cookie名称 98 | * @param cookieValue Cookie值 99 | */ 100 | export function onAddCookie(cookieOriginalValue: string, cookieName: string, cookieValue: string | null): void { 101 | logger.custom('#669934', 'black', 102 | "add cookie, cookieName = ", cookieName, 103 | ", cookieValue = ", cookieValue 104 | ); 105 | 106 | // @ts-ignore - 保持与原JS版本一致的调用方式 107 | testDebuggerRules(cookieOriginalValue, "add", cookieName, cookieValue); 108 | } -------------------------------------------------------------------------------- /src/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './handlers'; 2 | export * from './parser'; -------------------------------------------------------------------------------- /src/events/parser.ts: -------------------------------------------------------------------------------- 1 | import { CookiePairType } from '../types'; 2 | import { CookiePair } from '../models'; 3 | import { splitKeyValue } from '../utils'; 4 | 5 | /** 6 | * 将本次设置cookie的字符串解析为容易处理的形式 7 | * 8 | * @param cookieString 原始Cookie字符串 9 | * @returns 解析后的Cookie对象 10 | */ 11 | export function parseSetCookie(cookieString: string): CookiePairType { 12 | // uuid_tt_dd=10_37476713480-1609821005397-659114; Expires=Thu, 01 Jan 1025 00:00:00 GMT; Path=/; Domain=.csdn.net; 13 | const cookieStringSplit = cookieString.split(";"); 14 | // @ts-ignore - 与原JS版本保持一致的短路操作 15 | const {key, value} = splitKeyValue(cookieStringSplit.length && cookieStringSplit[0]); 16 | const map = new Map(); 17 | for (let i = 1; i < cookieStringSplit.length; i++) { 18 | const {key, value} = splitKeyValue(cookieStringSplit[i]); 19 | map.set(key.toLowerCase(), value); 20 | } 21 | // 当不设置expires的时候关闭浏览器就过期 22 | const expires = map.get("expires"); 23 | return new CookiePair(key, value, expires ? new Date(expires).getTime() : null); 24 | } 25 | 26 | /** 27 | * 获取当前所有已经设置的cookie 28 | * 29 | * @returns Cookie映射表 30 | */ 31 | export function getCurrentCookieMap(): Map { 32 | const cookieMap = new Map(); 33 | if (!document.cookie) { 34 | return cookieMap; 35 | } 36 | document.cookie.split(";").forEach(x => { 37 | const {key, value} = splitKeyValue(x); 38 | // @ts-ignore - 保持与原JS版本一致,不传第三个参数 39 | cookieMap.set(key, new CookiePair(key, value)); 40 | }); 41 | return cookieMap; 42 | } -------------------------------------------------------------------------------- /src/hooks/cookie-hooks.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyDescriptor } from '../types'; 2 | import { definePropertyIsMe, getRealDocumentCookieProperty } from '../cookie-monitor/config'; 3 | import { cc11001100_onSetCookie } from '../events/handlers'; 4 | 5 | /** 6 | * 安装Cookie钩子 7 | * 拦截document.cookie属性的访问和设置 8 | */ 9 | export function installCookieHooks(): void { 10 | // 此处实现的反复hook,保证页面流程能够继续往下走下去 11 | (function addCookieHook() { 12 | const handler: CustomPropertyDescriptor = { 13 | get: () => { 14 | // 先恢复原状 15 | // @ts-ignore - 允许delete操作 16 | delete document.cookie; 17 | 18 | try { 19 | // 如果网站开发者有设置自己的属性访问符的话,则以他设置的为准,把它的返回值作为此函数最终的返回值,保持其原有逻辑 20 | const realProperty = getRealDocumentCookieProperty(); 21 | if (realProperty && "get" in realProperty && realProperty["get"]) { 22 | // 在网站执行者自己定义的cookie的property执行期间,我们的工具添加的hook是被下掉的,所以是没有影响的 23 | // fix #13 此处的this需要绑定为document 24 | // @ts-ignore - 允许将IArguments传递给apply方法 25 | return realProperty["get"].apply(document, arguments); 26 | } else { 27 | // 如果网站开发者没有设置自己的property的话,则获取到真正的cookie值返回 28 | return document.cookie; 29 | } 30 | } finally { 31 | // 然后这么获取完之后,还是要把hook加上 32 | addCookieHook(); 33 | } 34 | 35 | }, set: (newValue: string) => { 36 | // 先触发相关的事件 37 | cc11001100_onSetCookie(newValue); 38 | 39 | // 然后恢复原状,把我们设置的hook啥的下掉 40 | // @ts-ignore - 允许delete操作 41 | delete document.cookie; 42 | 43 | try { 44 | // 如果网站开发者有设置自己的属性访问符的话,则以他设置的为准 45 | const realProperty = getRealDocumentCookieProperty(); 46 | if (realProperty && "set" in realProperty && realProperty["set"]) { 47 | // 在网站执行者自己定义的cookie的property执行期间,我们的工具添加的hook是被下掉的,所以是没有影响的 48 | // 不过这同时带来一个新的问题,就是如果它在这个property中进行cookie的操作我们无法感知到,那能怎么办呢?有得必有失 49 | // TODO 2023-7-26 22:02:11 那,有没有比较简单的"我全都要"的方案呢? 50 | // fix #13 此处的this需要绑定为document 51 | realProperty["set"].apply(document, [newValue]); 52 | } else { 53 | // 如果网站开发者没有设置property或者没有设置set的话,则还是走默认的赋值逻辑 54 | document.cookie = newValue; 55 | } 56 | } finally { 57 | // 然后再把hook设置上,加在finally里保证就算出错了也能恢复hook 58 | addCookieHook(); 59 | } 60 | 61 | }, configurable: true, enumerable: false, 62 | }; 63 | handler[definePropertyIsMe] = true; 64 | Object.defineProperty(document, "cookie", handler); 65 | })(); 66 | } -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './property-hooks'; 2 | export * from './cookie-hooks'; -------------------------------------------------------------------------------- /src/hooks/property-hooks.ts: -------------------------------------------------------------------------------- 1 | import { definePropertyIsMe, getRealDocumentCookieProperty, setRealDocumentCookieProperty } from '../cookie-monitor/config'; 2 | 3 | /** 4 | * 安装属性钩子 5 | * 拦截 Object.defineProperty 和 Object.defineProperties 的调用 6 | */ 7 | export function installPropertyHooks(): void { 8 | // 页面内部的Object.defineProperty需要能够劫持一下 9 | (function () { 10 | // 把Object.defineProperty给拦截了 11 | Object.defineProperty = new Proxy(Object.defineProperty, { 12 | apply: function (target, thisArg, argArray) { 13 | // 检查是否是自己调用的 14 | const isMe = argArray && argArray.length >= 3 && argArray[2] && definePropertyIsMe in argArray[2]; 15 | 16 | // 检查是否是定义的document.cookie 17 | const isDocumentCookie = argArray && argArray.length >= 2 && argArray[0] === document && "cookie" === argArray[1]; 18 | 19 | if (!isMe && isDocumentCookie) { 20 | // 检查要定义访问符的是否是document.cookie这个方法的话就包装一下,保证同时多个都能被调用到 21 | if (argArray && argArray.length >= 3) { 22 | // 更新一下real property就不管了, 23 | setRealDocumentCookieProperty(argArray[2] as PropertyDescriptor); 24 | return; 25 | } 26 | } 27 | // @ts-ignore - 允许动态参数 28 | return target.apply(thisArg, argArray); 29 | } 30 | }); 31 | 32 | Object.defineProperty.toString = function () { 33 | return "function defineProperty() { [native code] }"; 34 | } 35 | 36 | // 把Object.defineProperties也给拦截了 37 | Object.defineProperties = new Proxy(Object.defineProperties, { 38 | apply: function (target, thisArg, argArray) { 39 | // 可能会通过如下代码来调用: 40 | // Object.defineProperties(document, {"cookie": {...}) 41 | const isDocumentCookie = argArray && argArray.length >= 2 && document === argArray[0] && "cookie" in argArray[1]; 42 | if (isDocumentCookie) { 43 | // 把要设置的property描述符持有者 44 | setRealDocumentCookieProperty(argArray[1]["cookie"] as PropertyDescriptor); 45 | // 任务这个cookie的define已经执行完了,将其删除掉 46 | delete argArray[1]["cookie"]; 47 | // 如果只有一个cookie的话,删除完没有其它的属性了,则没必要继续往下了 48 | // 如果有剩余的属性的话,则需要原样继续执行 49 | if (!Object.keys(argArray[1]).length) { 50 | return; 51 | } 52 | } 53 | // @ts-ignore - 允许动态参数 54 | return target.apply(thisArg, argArray); 55 | } 56 | }); 57 | 58 | Object.defineProperties.toString = function () { 59 | return "function defineProperties() { [native code] }"; 60 | } 61 | })(); 62 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { initCookieMonitor } from './cookie-monitor'; 2 | import { initUI } from './ui'; 3 | 4 | // 初始化Cookie监控器 5 | initCookieMonitor(); 6 | 7 | // 初始化UI界面 8 | initUI(); 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './types'; -------------------------------------------------------------------------------- /src/logger/logger.ts: -------------------------------------------------------------------------------- 1 | import { LoggerConfig, LogLevel } from './types'; 2 | import { getCodeLocation } from '../utils/debugger'; 3 | import { now } from '../utils/time'; 4 | 5 | /** 6 | * 默认日志配置 7 | */ 8 | const defaultConfig: LoggerConfig = { 9 | level: LogLevel.INFO, 10 | showTimestamp: true, 11 | showLogLevel: true, 12 | showCodeLocation: true, 13 | prefix: 'JS Cookie Monitor:', 14 | fontSize: 12 15 | }; 16 | 17 | /** 18 | * 当前日志配置 19 | */ 20 | let config: LoggerConfig = { ...defaultConfig }; 21 | 22 | /** 23 | * 日志级别对应的样式 24 | */ 25 | const LOG_LEVEL_STYLES: Record = { 26 | [LogLevel.DEBUG]: { background: '#E8F5E9', textColor: '#2E7D32' }, 27 | [LogLevel.INFO]: { background: '#E3F2FD', textColor: '#1565C0' }, 28 | [LogLevel.WARN]: { background: '#FFF8E1', textColor: '#F57F17' }, 29 | [LogLevel.ERROR]: { background: '#FFEBEE', textColor: '#C62828' }, 30 | [LogLevel.NONE]: { background: '#FFFFFF', textColor: '#000000' } 31 | }; 32 | 33 | /** 34 | * 获取当前日志配置 35 | * @returns 当前日志配置 36 | */ 37 | export function getLoggerConfig(): LoggerConfig { 38 | return { ...config }; 39 | } 40 | 41 | /** 42 | * 设置日志配置 43 | * @param newConfig 新的日志配置(部分) 44 | */ 45 | export function setLoggerConfig(newConfig: Partial): void { 46 | config = { 47 | ...config, 48 | ...newConfig 49 | }; 50 | } 51 | 52 | /** 53 | * 生成格式化字符串数组 54 | * @param partsCount 部分数量 55 | * @returns 格式化字符串数组拼接后的字符串 56 | */ 57 | function genFormatArray(partsCount: number): string { 58 | return Array(partsCount).fill('%c%s').join(''); 59 | } 60 | 61 | /** 62 | * 构建带样式的消息 63 | * @param level 日志级别 64 | * @param message 消息内容 (可以有多个参数) 65 | * @returns [格式化字符串, ...样式和消息交替排列的数组] 66 | */ 67 | function buildStyledMessage(level: LogLevel, ...messages: any[]): [string, ...any[]] { 68 | const { background, textColor } = LOG_LEVEL_STYLES[level]; 69 | const baseStyle = `color: ${textColor}; background: ${background}; font-size: ${config.fontSize}px;`; 70 | const boldStyle = `${baseStyle} font-weight: bold;`; 71 | 72 | const parts: any[] = []; 73 | 74 | // 添加时间戳 75 | if (config.showTimestamp) { 76 | parts.push(baseStyle, now()); 77 | } 78 | 79 | // 添加前缀 80 | parts.push(baseStyle, `${config.prefix} `); 81 | 82 | // 添加日志级别 83 | if (config.showLogLevel) { 84 | parts.push(boldStyle, `[${level.toUpperCase()}]`); 85 | parts.push(baseStyle, ' '); 86 | } 87 | 88 | // 添加消息 89 | for (let i = 0; i < messages.length; i++) { 90 | const message = messages[i]; 91 | // 对象和数组使用JSON格式化 92 | if (typeof message === 'object' && message !== null) { 93 | try { 94 | parts.push(boldStyle, JSON.stringify(message, null, 2)); 95 | } catch (e) { 96 | parts.push(boldStyle, String(message)); 97 | } 98 | } else { 99 | parts.push(i % 2 === 0 ? baseStyle : boldStyle, String(message)); 100 | } 101 | 102 | if (i < messages.length - 1) { 103 | parts.push(baseStyle, ' '); 104 | } 105 | } 106 | 107 | // 添加代码位置 108 | if (config.showCodeLocation) { 109 | parts.push(baseStyle, ` [at ${getCodeLocation()}]`); 110 | } 111 | 112 | return [genFormatArray(parts.length / 2), ...parts]; 113 | } 114 | 115 | /** 116 | * 检查是否应该打印指定级别的日志 117 | * @param level 日志级别 118 | * @returns 是否应该打印 119 | */ 120 | function shouldLog(level: LogLevel): boolean { 121 | const levels = [ 122 | LogLevel.DEBUG, 123 | LogLevel.INFO, 124 | LogLevel.WARN, 125 | LogLevel.ERROR 126 | ]; 127 | 128 | // NONE 级别不打印任何日志 129 | if (config.level === LogLevel.NONE) { 130 | return false; 131 | } 132 | 133 | return levels.indexOf(level) >= levels.indexOf(config.level); 134 | } 135 | 136 | /** 137 | * 打印调试日志 138 | * @param messages 消息参数 (可以有多个) 139 | */ 140 | export function debug(...messages: any[]): void { 141 | if (!shouldLog(LogLevel.DEBUG)) return; 142 | const [format, ...args] = buildStyledMessage(LogLevel.DEBUG, ...messages); 143 | console.log(format, ...args); 144 | } 145 | 146 | /** 147 | * 打印信息日志 148 | * @param messages 消息参数 (可以有多个) 149 | */ 150 | export function info(...messages: any[]): void { 151 | if (!shouldLog(LogLevel.INFO)) return; 152 | const [format, ...args] = buildStyledMessage(LogLevel.INFO, ...messages); 153 | console.log(format, ...args); 154 | } 155 | 156 | /** 157 | * 打印警告日志 158 | * @param messages 消息参数 (可以有多个) 159 | */ 160 | export function warn(...messages: any[]): void { 161 | if (!shouldLog(LogLevel.WARN)) return; 162 | const [format, ...args] = buildStyledMessage(LogLevel.WARN, ...messages); 163 | console.warn(format, ...args); 164 | } 165 | 166 | /** 167 | * 打印错误日志 168 | * @param messages 消息参数 (可以有多个) 169 | */ 170 | export function error(...messages: any[]): void { 171 | if (!shouldLog(LogLevel.ERROR)) return; 172 | const [format, ...args] = buildStyledMessage(LogLevel.ERROR, ...messages); 173 | console.error(format, ...args); 174 | } 175 | 176 | /** 177 | * 打印自定义背景颜色的日志 178 | * @param backgroundColor 背景颜色 179 | * @param textColor 文本颜色 180 | * @param messages 消息参数 (可以有多个) 181 | */ 182 | export function custom(backgroundColor: string, textColor: string, ...messages: any[]): void { 183 | const baseStyle = `color: ${textColor}; background: ${backgroundColor}; font-size: ${config.fontSize}px;`; 184 | const boldStyle = `${baseStyle} font-weight: bold;`; 185 | 186 | const parts: any[] = []; 187 | 188 | // 添加时间戳 189 | if (config.showTimestamp) { 190 | parts.push(baseStyle, now()); 191 | } 192 | 193 | // 添加前缀 194 | parts.push(baseStyle, `${config.prefix} `); 195 | 196 | // 添加消息 197 | for (let i = 0; i < messages.length; i++) { 198 | const message = messages[i]; 199 | if (typeof message === 'object' && message !== null) { 200 | try { 201 | parts.push(boldStyle, JSON.stringify(message, null, 2)); 202 | } catch (e) { 203 | parts.push(boldStyle, String(message)); 204 | } 205 | } else { 206 | parts.push(i % 2 === 0 ? baseStyle : boldStyle, String(message)); 207 | } 208 | 209 | if (i < messages.length - 1) { 210 | parts.push(baseStyle, ' '); 211 | } 212 | } 213 | 214 | // 添加代码位置 215 | if (config.showCodeLocation) { 216 | parts.push(baseStyle, ` [at ${getCodeLocation()}]`); 217 | } 218 | 219 | console.log(genFormatArray(parts.length / 2), ...parts); 220 | } 221 | 222 | // 导出默认日志实例 223 | export default { 224 | debug, 225 | info, 226 | warn, 227 | error, 228 | custom, 229 | getConfig: getLoggerConfig, 230 | setConfig: setLoggerConfig 231 | }; -------------------------------------------------------------------------------- /src/logger/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 日志级别枚举 3 | */ 4 | export enum LogLevel { 5 | DEBUG = 'debug', 6 | INFO = 'info', 7 | WARN = 'warn', 8 | ERROR = 'error', 9 | NONE = 'none' 10 | } 11 | 12 | /** 13 | * 日志配置接口 14 | */ 15 | export interface LoggerConfig { 16 | /** 17 | * 日志级别,低于该级别的日志将不会输出 18 | */ 19 | level: LogLevel; 20 | 21 | /** 22 | * 是否显示时间戳 23 | */ 24 | showTimestamp: boolean; 25 | 26 | /** 27 | * 是否显示日志级别 28 | */ 29 | showLogLevel: boolean; 30 | 31 | /** 32 | * 是否显示代码位置 33 | */ 34 | showCodeLocation: boolean; 35 | 36 | /** 37 | * 脚本标识前缀 38 | */ 39 | prefix: string; 40 | 41 | /** 42 | * 字体大小(px) 43 | */ 44 | fontSize: number; 45 | } -------------------------------------------------------------------------------- /src/models/cookie-pair.ts: -------------------------------------------------------------------------------- 1 | import { CookiePairType } from '../types'; 2 | 3 | /** 4 | * 用于在本脚本内部表示一条cookie以方便程序处理 5 | * 这里只取了有用的信息,忽略了域名及路径,也许需要加上这两个限制?但现在这个脚本已经够臃肿了... 6 | */ 7 | export class CookiePair implements CookiePairType { 8 | name: string; 9 | value: string | null; 10 | expires: number | null; 11 | 12 | /** 13 | * 14 | * @param name Cookie的名字 15 | * @param value Cookie的值 16 | * @param expires Cookie的过期时间 17 | */ 18 | constructor(name: string, value: string | null, expires: number | null = null) { 19 | this.name = name; 20 | this.value = value; 21 | this.expires = expires; 22 | } 23 | } -------------------------------------------------------------------------------- /src/models/debugger-rule.ts: -------------------------------------------------------------------------------- 1 | import { DebuggerRuleType } from '../types'; 2 | import { getEventDebuggerConfig } from '../cookie-monitor/config'; 3 | 4 | export class DebuggerRule implements DebuggerRuleType { 5 | private eventName: string | null; 6 | private cookieNameFilter: string | RegExp | null; 7 | private cookieValueFilter: string | RegExp | null; 8 | 9 | constructor(eventName: string | null, cookieNameFilter: string | RegExp | null, cookieValueFilter: string | RegExp | null) { 10 | this.eventName = eventName; 11 | this.cookieNameFilter = cookieNameFilter; 12 | this.cookieValueFilter = cookieValueFilter; 13 | } 14 | 15 | test(eventName: string, cookieName: string, cookieValue: string | null): boolean { 16 | return this.testByEventName(eventName) && 17 | (this.testByCookieNameFilter(cookieName) || this.testByCookieValueFilter(cookieValue)); 18 | } 19 | 20 | testByEventName(eventName: string): boolean { 21 | // 如果此类型的事件断点没有开启,则直接返回 22 | const enableEventDebugger = getEventDebuggerConfig(); 23 | if (!enableEventDebugger[eventName]) { 24 | return false; 25 | } 26 | // 事件不设置则匹配任何事件 27 | if (!this.eventName) { 28 | return true; 29 | } 30 | return this.eventName === eventName; 31 | } 32 | 33 | testByCookieNameFilter(cookieName: string): boolean { 34 | if (!cookieName || !this.cookieNameFilter) { 35 | return false; 36 | } 37 | if (typeof this.cookieNameFilter === "string") { 38 | return this.cookieNameFilter === cookieName; 39 | } 40 | if (this.cookieNameFilter instanceof RegExp) { 41 | return this.cookieNameFilter.test(cookieName); 42 | } 43 | return false; 44 | } 45 | 46 | testByCookieValueFilter(cookieValue: string | null): boolean { 47 | if (!cookieValue || !this.cookieValueFilter) { 48 | return false; 49 | } 50 | if (typeof this.cookieValueFilter === "string") { 51 | return this.cookieValueFilter === cookieValue; 52 | } 53 | if (this.cookieValueFilter instanceof RegExp) { 54 | return this.cookieValueFilter.test(cookieValue); 55 | } 56 | return false; 57 | } 58 | } -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cookie-pair'; 2 | export * from './debugger-rule'; -------------------------------------------------------------------------------- /src/rules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tester'; 2 | export * from './standardize'; -------------------------------------------------------------------------------- /src/rules/standardize.ts: -------------------------------------------------------------------------------- 1 | import { DebuggerRule } from '../models'; 2 | import { debuggerRules } from '../cookie-monitor/config'; 3 | import logger from '../logger/logger'; 4 | 5 | /** 6 | * 将规则整理为标准规则 7 | */ 8 | export function standardizingRules(): void { 9 | // 用于收集规则配置错误 10 | const ruleConfigErrorMessage: string[] = []; 11 | const newRules: DebuggerRule[] = []; 12 | 13 | while (debuggerRules.length) { 14 | const rule = debuggerRules.pop(); 15 | 16 | // 字符串或正则处理 17 | if (typeof rule === "string" || rule instanceof RegExp) { 18 | newRules.push(new DebuggerRule(null, rule, null)); 19 | continue; 20 | } 21 | 22 | // 对象处理 23 | if (typeof rule === "object") { 24 | for (let key in rule as Record) { 25 | let events: string | null = null; 26 | let cookieNameFilter = null; 27 | let cookieValueFilter = null; 28 | 29 | if (key === "events") { 30 | events = (rule as any)["events"] || "add | delete | update"; 31 | cookieNameFilter = (rule as any)["name"]; 32 | cookieValueFilter = (rule as any)["value"]; 33 | } else if (key !== "name" && key !== "value") { 34 | events = key; 35 | cookieNameFilter = (rule as any)[key]; 36 | cookieValueFilter = (rule as any)["value"]; 37 | } else { 38 | continue; 39 | } 40 | 41 | // 名字必须配置 42 | if (!cookieNameFilter) { 43 | ruleConfigErrorMessage.push(`必须为此条规则 ${JSON.stringify(rule)} 配置一个Cookie Name匹配条件`); 44 | continue; 45 | } 46 | 47 | if (events) { 48 | events.split("|").forEach((eventName: string) => { 49 | eventName = eventName.trim(); 50 | if (eventName !== "add" && eventName !== "delete" && eventName !== "update") { 51 | ruleConfigErrorMessage.push(`此条规则 ${JSON.stringify(rule)} 的Cookie事件名字配置错误,必须为 add、delete、update 三种之一或者|分隔的组合,您配置的是 ${eventName},仅忽略此无效事件`); 52 | return; 53 | } 54 | newRules.push(new DebuggerRule(eventName, cookieNameFilter, cookieValueFilter)); 55 | }); 56 | } 57 | } 58 | } 59 | } 60 | 61 | // 处理错误 62 | if (ruleConfigErrorMessage.length) { 63 | let errorMessage = "以下Cookie断点规则配置错误,已忽略: \n "; 64 | for (let i = 0; i < ruleConfigErrorMessage.length; i++) { 65 | errorMessage += `${i + 1}. ${ruleConfigErrorMessage[i]}\n`; 66 | } 67 | logger.error(errorMessage); 68 | } 69 | 70 | // 将新规则添加到规则列表 71 | for (let rule of newRules) { 72 | debuggerRules.push(rule); 73 | } 74 | } -------------------------------------------------------------------------------- /src/rules/tester.ts: -------------------------------------------------------------------------------- 1 | import { debuggerRules } from '../cookie-monitor/config'; 2 | 3 | /** 4 | * 当断点停在这里时查看这个方法各个参数的值能够大致了解断点情况 5 | * 6 | * 鼠标移动到变量上查看变量的值 7 | * 8 | * @param setCookieOriginalValue 目标网站使用document.cookie时赋值的原始值是什么,这个值没有 URL decode, 9 | * 如果要分析它请拷贝其值到外面分析,这里只是提供一种可能性 10 | * @param eventName 本次是发生了什么事件,add增加新cookie、update更新cookie的值、delete表示cookie被删除 11 | * @param cookieName 本脚本对setCookieOriginalValue解析出的cookie名字,会被URL decode 12 | * @param cookieValue 本脚本对setCookieOriginalValue解析出的cookie值,会被URL decode 13 | * @param cookieValueChanged 只在update事件时有值,用于帮助快速确定本次update有没有修改cookie的值 14 | */ 15 | export function testDebuggerRules(setCookieOriginalValue: string, eventName: string, cookieName: string, cookieValue: string | null, cookieValueChanged?: boolean): void { 16 | for (let rule of debuggerRules) { 17 | // rule当前的值表示被什么断点规则匹配到了,可以把鼠标移动到rule变量上查看 18 | if (rule.test(eventName, cookieName, cookieValue)) { 19 | // 如果规则匹配的话则进入断点 20 | debugger; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // 核心类型定义 2 | 3 | // Cookie键值对类型 4 | export interface CookiePairType { 5 | name: string; 6 | value: string | null; 7 | expires: number | null; 8 | } 9 | 10 | // 调试规则类型 11 | export interface DebuggerRuleType { 12 | test(eventName: string, cookieName: string, cookieValue: string | null): boolean; 13 | testByEventName(eventName: string): boolean; 14 | testByCookieNameFilter(cookieName: string): boolean; 15 | testByCookieValueFilter(cookieValue: string | null): boolean; 16 | } 17 | 18 | // 为definePropertyIsMe创建类型扩展 19 | export interface CustomPropertyDescriptor extends PropertyDescriptor { 20 | [key: string]: any; 21 | } 22 | // Cookie事件类型 23 | export type CookieEventType = 'add' | 'update' | 'delete' | 'read'; 24 | 25 | // 存储事件调试是否开启的配置 26 | export type EventDebuggerConfig = Record; 27 | 28 | // 键值对拆分结果 29 | export interface KeyValuePair { 30 | key: string; 31 | value: string; 32 | } -------------------------------------------------------------------------------- /src/types/tampermonkey.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tampermonkey API类型声明 3 | */ 4 | 5 | declare function GM_registerMenuCommand(caption: string, commandFunc: () => void, accessKey?: string): void; 6 | declare function GM_setValue(name: string, value: any): void; 7 | declare function GM_getValue(name: string, defaultValue?: any): any; 8 | declare function GM_deleteValue(name: string): void; 9 | declare function GM_listValues(): string[]; 10 | declare function GM_addStyle(css: string): HTMLStyleElement; 11 | declare function GM_getResourceText(resourceName: string): string; 12 | declare function GM_getResourceURL(resourceName: string): string; 13 | declare function GM_openInTab(url: string, options?: { active?: boolean, insert?: boolean, setParent?: boolean }): void; 14 | declare function GM_notification(details: any, ondone?: () => void): void; 15 | declare function GM_xmlhttpRequest(details: any): void; -------------------------------------------------------------------------------- /src/ui/index.ts: -------------------------------------------------------------------------------- 1 | import { registerMenu } from './menu'; 2 | import { showModal } from './modal'; 3 | 4 | /** 5 | * 初始化UI界面 6 | * 注册油猴菜单并准备UI组件 7 | */ 8 | export function initUI(): void { 9 | // 注册油猴菜单 10 | registerMenu(); 11 | 12 | // 注入全局样式 13 | injectGlobalStyles(); 14 | } 15 | 16 | /** 17 | * 注入全局样式 18 | */ 19 | function injectGlobalStyles(): void { 20 | const style = document.createElement('style'); 21 | style.textContent = ` 22 | /* 重置样式 */ 23 | .jscookie-ui * { 24 | box-sizing: border-box; 25 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 26 | } 27 | 28 | /* 动画 */ 29 | @keyframes jscookie-fadeIn { 30 | from { opacity: 0; } 31 | to { opacity: 1; } 32 | } 33 | `; 34 | document.head.appendChild(style); 35 | } 36 | 37 | // 导出所有UI组件 38 | export { registerMenu } from './menu'; 39 | export { showModal, hideModal } from './modal'; -------------------------------------------------------------------------------- /src/ui/menu/index.ts: -------------------------------------------------------------------------------- 1 | export { registerMenu } from './register'; -------------------------------------------------------------------------------- /src/ui/menu/register.ts: -------------------------------------------------------------------------------- 1 | import { showModal } from '../modal'; 2 | import logger from '../../logger/logger'; 3 | 4 | /** 5 | * 注册油猴菜单 6 | */ 7 | export function registerMenu(): void { 8 | try { 9 | if (typeof GM_registerMenuCommand !== 'undefined') { 10 | // 注册主菜单项 - 打开配置面板 11 | GM_registerMenuCommand('打开配置面板', () => { 12 | showModal(); 13 | }); 14 | 15 | // 注册调试菜单项 16 | GM_registerMenuCommand('开启/关闭调试日志', toggleDebugMode); 17 | 18 | logger.info('油猴菜单注册成功'); 19 | } else { 20 | logger.warn('当前环境不支持GM_registerMenuCommand,无法注册油猴菜单'); 21 | 22 | // 备用方案:添加键盘快捷键唤起界面 23 | addKeyboardShortcut(); 24 | } 25 | } catch (error) { 26 | logger.error('注册油猴菜单失败', error); 27 | } 28 | } 29 | 30 | /** 31 | * 切换调试模式 32 | */ 33 | function toggleDebugMode(): void { 34 | // 实现调试模式切换逻辑 35 | const isDebugEnabled = localStorage.getItem('jscookie-debug-mode') === 'true'; 36 | localStorage.setItem('jscookie-debug-mode', (!isDebugEnabled).toString()); 37 | 38 | logger.info(`调试模式已${!isDebugEnabled ? '开启' : '关闭'}`); 39 | } 40 | 41 | /** 42 | * 添加键盘快捷键 43 | * 在不支持GM_registerMenuCommand的环境下使用 44 | */ 45 | function addKeyboardShortcut(): void { 46 | document.addEventListener('keydown', (event) => { 47 | // Ctrl/Cmd + Shift + J 打开配置面板 48 | if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'j') { 49 | showModal(); 50 | event.preventDefault(); 51 | } 52 | }); 53 | 54 | logger.info('已添加键盘快捷键: Ctrl/Cmd + Shift + J 打开配置面板'); 55 | } -------------------------------------------------------------------------------- /src/ui/modal/index.ts: -------------------------------------------------------------------------------- 1 | export { showModal, hideModal } from './modal'; -------------------------------------------------------------------------------- /src/ui/modal/modal.css.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 模态框样式 3 | */ 4 | export const modalStyles = ` 5 | /* 模态框背景遮罩 */ 6 | .jscookie-modal-overlay { 7 | position: fixed; 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | height: 100%; 12 | background-color: rgba(0, 0, 0, 0.5); 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | z-index: 9999; 17 | animation: jscookie-fadeIn 0.2s ease-out; 18 | } 19 | 20 | /* 模态框容器 */ 21 | .jscookie-modal { 22 | background-color: #fff; 23 | border-radius: 8px; 24 | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); 25 | width: 90%; 26 | max-width: 800px; 27 | max-height: 90vh; 28 | display: flex; 29 | flex-direction: column; 30 | overflow: hidden; 31 | position: relative; 32 | } 33 | 34 | /* 模态框头部 */ 35 | .jscookie-modal-header { 36 | display: flex; 37 | justify-content: space-between; 38 | align-items: center; 39 | padding: 16px 20px; 40 | border-bottom: 1px solid #eee; 41 | } 42 | 43 | .jscookie-modal-title { 44 | font-size: 18px; 45 | font-weight: 600; 46 | color: #333; 47 | margin: 0; 48 | } 49 | 50 | .jscookie-modal-close { 51 | background: none; 52 | border: none; 53 | cursor: pointer; 54 | font-size: 20px; 55 | color: #999; 56 | padding: 0; 57 | width: 24px; 58 | height: 24px; 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | border-radius: 50%; 63 | transition: background-color 0.2s; 64 | } 65 | 66 | .jscookie-modal-close:hover { 67 | background-color: #f5f5f5; 68 | color: #333; 69 | } 70 | 71 | /* 模态框内容区 */ 72 | .jscookie-modal-content { 73 | flex: 1; 74 | overflow: auto; 75 | padding: 0; 76 | } 77 | 78 | /* 模态框底部 */ 79 | .jscookie-modal-footer { 80 | display: flex; 81 | justify-content: flex-end; 82 | padding: 16px 20px; 83 | border-top: 1px solid #eee; 84 | gap: 12px; 85 | } 86 | 87 | /* 按钮样式 */ 88 | .jscookie-btn { 89 | padding: 8px 16px; 90 | border-radius: 4px; 91 | font-size: 14px; 92 | font-weight: 500; 93 | cursor: pointer; 94 | border: none; 95 | transition: background-color 0.2s, color 0.2s; 96 | } 97 | 98 | .jscookie-btn-primary { 99 | background-color: #4f7dff; 100 | color: white; 101 | } 102 | 103 | .jscookie-btn-primary:hover { 104 | background-color: #3b6af8; 105 | } 106 | 107 | .jscookie-btn-secondary { 108 | background-color: #f5f5f5; 109 | color: #333; 110 | } 111 | 112 | .jscookie-btn-secondary:hover { 113 | background-color: #e5e5e5; 114 | } 115 | 116 | /* 响应式调整 */ 117 | @media (max-width: 576px) { 118 | .jscookie-modal { 119 | width: 95%; 120 | max-height: 95vh; 121 | } 122 | 123 | .jscookie-modal-header, 124 | .jscookie-modal-footer { 125 | padding: 12px 16px; 126 | } 127 | } 128 | `; -------------------------------------------------------------------------------- /src/ui/modal/modal.html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成模态框HTML 3 | * @param title 模态框标题 4 | * @param content 模态框内容 5 | * @returns 模态框HTML字符串 6 | */ 7 | export function createModalHTML(title: string = 'JS Cookie Monitor 配置', content: string = ''): string { 8 | return ` 9 |
10 |
11 |
12 |

${title}

13 | 14 |
15 |
16 | ${content} 17 |
18 | 22 |
23 |
24 | `; 25 | } -------------------------------------------------------------------------------- /src/ui/modal/modal.ts: -------------------------------------------------------------------------------- 1 | import { modalStyles } from './modal.css'; 2 | import { createModalHTML } from './modal.html'; 3 | import { Tabs } from '../tabs'; 4 | import { createBreakpointsView } from '../views/breakpoints'; 5 | import { createSettingsView } from '../views/settings'; 6 | import { createAboutView } from '../views/about'; 7 | import logger from '../../logger/logger'; 8 | 9 | // 保存模态框实例 10 | let modalInstance: HTMLElement | null = null; 11 | 12 | /** 13 | * 注入模态框样式 14 | */ 15 | function injectStyles(): void { 16 | if (!document.querySelector('#jscookie-modal-styles')) { 17 | const style = document.createElement('style'); 18 | style.id = 'jscookie-modal-styles'; 19 | style.textContent = modalStyles; 20 | document.head.appendChild(style); 21 | } 22 | } 23 | 24 | /** 25 | * 创建模态框内容 26 | * @returns 模态框内容HTML 27 | */ 28 | function createModalContent(): string { 29 | // 创建各个标签页的内容 30 | const breakpointsContent = createBreakpointsView(); 31 | const settingsContent = createSettingsView(); 32 | const aboutContent = createAboutView(); 33 | 34 | // 使用标签页组件创建内容 35 | const container = document.createElement('div'); 36 | const tabs = new Tabs(container, [ 37 | { id: 'jscookie-tab-breakpoints', title: '断点列表', content: breakpointsContent, active: true }, 38 | { id: 'jscookie-tab-settings', title: '全局设置', content: settingsContent }, 39 | { id: 'jscookie-tab-about', title: '关于', content: aboutContent } 40 | ]); 41 | 42 | // 获取生成的HTML 43 | return container.innerHTML || ''; 44 | } 45 | 46 | /** 47 | * 绑定模态框事件 48 | * @param modal 模态框元素 49 | */ 50 | function bindModalEvents(modal: HTMLElement): void { 51 | // 关闭按钮 52 | const closeBtn = modal.querySelector('.jscookie-modal-close'); 53 | if (closeBtn) { 54 | closeBtn.addEventListener('click', hideModal); 55 | } 56 | 57 | // 取消按钮 58 | const cancelBtn = modal.querySelector('.jscookie-cancel-btn'); 59 | if (cancelBtn) { 60 | cancelBtn.addEventListener('click', hideModal); 61 | } 62 | 63 | // 保存按钮 64 | const saveBtn = modal.querySelector('.jscookie-save-btn'); 65 | if (saveBtn) { 66 | saveBtn.addEventListener('click', saveSettings); 67 | } 68 | 69 | // 点击遮罩关闭 70 | modal.addEventListener('click', (event) => { 71 | if (event.target === modal) { 72 | hideModal(); 73 | } 74 | }); 75 | 76 | // ESC按键关闭 77 | document.addEventListener('keydown', (event) => { 78 | if (event.key === 'Escape' && isModalVisible()) { 79 | hideModal(); 80 | } 81 | }); 82 | } 83 | 84 | /** 85 | * 保存配置 86 | */ 87 | function saveSettings(): void { 88 | try { 89 | // 遍历表单并收集数据 90 | const form = document.querySelector('.jscookie-settings-form'); 91 | if (form) { 92 | // 收集表单数据的逻辑... 93 | 94 | logger.info('设置已保存'); 95 | hideModal(); 96 | } 97 | } catch (error) { 98 | logger.error('保存设置失败', error); 99 | } 100 | } 101 | 102 | /** 103 | * 显示模态框 104 | */ 105 | export function showModal(): void { 106 | // 防止重复创建 107 | if (isModalVisible()) { 108 | return; 109 | } 110 | 111 | try { 112 | // 注入样式 113 | injectStyles(); 114 | 115 | // 创建模态框内容 116 | const content = createModalContent(); 117 | 118 | // 创建模态框 119 | const modalHTML = createModalHTML('JS Cookie Monitor 配置', content); 120 | const tempDiv = document.createElement('div'); 121 | tempDiv.innerHTML = modalHTML.trim(); 122 | 123 | // 获取模态框元素 124 | modalInstance = tempDiv.firstChild as HTMLElement; 125 | 126 | // 添加到文档 127 | document.body.appendChild(modalInstance); 128 | 129 | // 防止body滚动 130 | document.body.style.overflow = 'hidden'; 131 | 132 | // 绑定事件 133 | bindModalEvents(modalInstance); 134 | 135 | // 初始化标签页 136 | initTabs(); 137 | } catch (error) { 138 | logger.error('显示模态框失败', error); 139 | } 140 | } 141 | 142 | /** 143 | * 初始化标签页 144 | */ 145 | function initTabs(): void { 146 | try { 147 | // 查找模态框内容容器 148 | const content = document.querySelector('.jscookie-modal-content'); 149 | if (content && content instanceof HTMLElement) { 150 | // 创建标签页 151 | new Tabs(content, [ 152 | { id: 'jscookie-tab-breakpoints', title: '断点列表', content: createBreakpointsView(), active: true }, 153 | { id: 'jscookie-tab-settings', title: '全局设置', content: createSettingsView() }, 154 | { id: 'jscookie-tab-about', title: '关于', content: createAboutView() } 155 | ]); 156 | } 157 | } catch (error) { 158 | logger.error('初始化标签页失败', error); 159 | } 160 | } 161 | 162 | /** 163 | * 隐藏模态框 164 | */ 165 | export function hideModal(): void { 166 | if (modalInstance) { 167 | // 恢复body滚动 168 | document.body.style.overflow = ''; 169 | 170 | // 移除模态框 171 | modalInstance.remove(); 172 | modalInstance = null; 173 | } 174 | } 175 | 176 | /** 177 | * 检查模态框是否可见 178 | */ 179 | export function isModalVisible(): boolean { 180 | return !!modalInstance && document.body.contains(modalInstance); 181 | } -------------------------------------------------------------------------------- /src/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { Tabs } from './tabs'; 2 | export type { TabConfig } from './tabs.html'; -------------------------------------------------------------------------------- /src/ui/tabs/tabs.css.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 标签页样式 3 | */ 4 | export const tabsStyles = ` 5 | /* 标签页导航 */ 6 | .jscookie-tabs-nav { 7 | display: flex; 8 | overflow-x: auto; 9 | background-color: #f7f8fb; 10 | border-bottom: 1px solid #eee; 11 | } 12 | 13 | /* 隐藏滚动条但保留功能 */ 14 | .jscookie-tabs-nav::-webkit-scrollbar { 15 | height: 0; 16 | } 17 | 18 | /* 标签按钮 */ 19 | .jscookie-tab-btn { 20 | padding: 12px 16px; 21 | background: none; 22 | border: none; 23 | font-size: 14px; 24 | font-weight: 500; 25 | color: #666; 26 | cursor: pointer; 27 | white-space: nowrap; 28 | position: relative; 29 | transition: color 0.2s; 30 | } 31 | 32 | .jscookie-tab-btn:hover { 33 | color: #4f7dff; 34 | } 35 | 36 | /* 活动标签按钮 */ 37 | .jscookie-tab-btn.active { 38 | color: #4f7dff; 39 | } 40 | 41 | /* 活动标签下划线 */ 42 | .jscookie-tab-btn.active::after { 43 | content: ''; 44 | position: absolute; 45 | bottom: 0; 46 | left: 0; 47 | width: 100%; 48 | height: 2px; 49 | background-color: #4f7dff; 50 | } 51 | 52 | /* 标签内容容器 */ 53 | .jscookie-tabs-content { 54 | height: 100%; 55 | overflow: auto; 56 | } 57 | 58 | /* 标签面板 */ 59 | .jscookie-tab-panel { 60 | padding: 16px 20px; 61 | display: none; 62 | height: 100%; 63 | overflow: auto; 64 | } 65 | 66 | /* 活动标签面板 */ 67 | .jscookie-tab-panel.active { 68 | display: block; 69 | } 70 | `; -------------------------------------------------------------------------------- /src/ui/tabs/tabs.html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 标签页配置接口 3 | */ 4 | export interface TabConfig { 5 | id: string; 6 | title: string; 7 | content: string; 8 | active?: boolean; 9 | } 10 | 11 | /** 12 | * 生成标签页HTML 13 | * @param tabs 标签页配置数组 14 | * @returns 标签页HTML字符串 15 | */ 16 | export function createTabsHTML(tabs: TabConfig[]): string { 17 | // 确保至少有一个标签被设置为活动状态 18 | const hasActiveTab = tabs.some(tab => tab.active); 19 | if (!hasActiveTab && tabs.length > 0) { 20 | tabs[0].active = true; 21 | } 22 | 23 | return ` 24 |
25 |
26 | ${tabs.map(tab => ` 27 | 33 | `).join('')} 34 |
35 |
36 | ${tabs.map(tab => ` 37 |
41 | ${tab.content} 42 |
43 | `).join('')} 44 |
45 |
46 | `; 47 | } -------------------------------------------------------------------------------- /src/ui/tabs/tabs.ts: -------------------------------------------------------------------------------- 1 | import { tabsStyles } from './tabs.css'; 2 | import { createTabsHTML, TabConfig } from './tabs.html'; 3 | 4 | /** 5 | * 标签页组件 6 | */ 7 | export class Tabs { 8 | private container: HTMLElement | null = null; 9 | private tabs: TabConfig[] = []; 10 | private activeTabId: string = ''; 11 | 12 | /** 13 | * 创建标签页组件 14 | * @param container 容器元素或选择器 15 | * @param tabs 标签页配置数组 16 | */ 17 | constructor(container: HTMLElement | string, tabs: TabConfig[] = []) { 18 | this.tabs = tabs; 19 | 20 | if (typeof container === 'string') { 21 | this.container = document.querySelector(container); 22 | } else { 23 | this.container = container; 24 | } 25 | 26 | if (!this.container) { 27 | throw new Error('无法找到标签页容器元素'); 28 | } 29 | 30 | this.init(); 31 | } 32 | 33 | /** 34 | * 初始化标签页 35 | */ 36 | private init(): void { 37 | // 注入样式 38 | this.injectStyles(); 39 | 40 | // 渲染HTML 41 | this.render(); 42 | 43 | // 绑定事件 44 | this.bindEvents(); 45 | 46 | // 设置初始活动标签 47 | const activeTab = this.tabs.find(tab => tab.active); 48 | if (activeTab) { 49 | this.activeTabId = activeTab.id; 50 | } else if (this.tabs.length > 0) { 51 | this.activeTabId = this.tabs[0].id; 52 | } 53 | } 54 | 55 | /** 56 | * 注入样式 57 | */ 58 | private injectStyles(): void { 59 | if (!document.querySelector('#jscookie-tabs-styles')) { 60 | const style = document.createElement('style'); 61 | style.id = 'jscookie-tabs-styles'; 62 | style.textContent = tabsStyles; 63 | document.head.appendChild(style); 64 | } 65 | } 66 | 67 | /** 68 | * 渲染标签页HTML 69 | */ 70 | private render(): void { 71 | if (this.container) { 72 | this.container.innerHTML = createTabsHTML(this.tabs); 73 | } 74 | } 75 | 76 | /** 77 | * 绑定事件 78 | */ 79 | private bindEvents(): void { 80 | if (!this.container) return; 81 | 82 | // 获取所有标签按钮 83 | const tabButtons = this.container.querySelectorAll('.jscookie-tab-btn'); 84 | 85 | // 为每个标签按钮添加点击事件 86 | tabButtons.forEach(button => { 87 | button.addEventListener('click', () => { 88 | const tabId = button.getAttribute('data-tab-id'); 89 | if (tabId) { 90 | this.activateTab(tabId); 91 | } 92 | }); 93 | }); 94 | } 95 | 96 | /** 97 | * 激活指定标签 98 | * @param tabId 标签ID 99 | */ 100 | public activateTab(tabId: string): void { 101 | if (!this.container) return; 102 | 103 | // 设置当前活动标签ID 104 | this.activeTabId = tabId; 105 | 106 | // 获取所有标签按钮和面板 107 | const tabButtons = this.container.querySelectorAll('.jscookie-tab-btn'); 108 | const tabPanels = this.container.querySelectorAll('.jscookie-tab-panel'); 109 | 110 | // 取消所有标签的活动状态 111 | tabButtons.forEach(button => { 112 | button.classList.remove('active'); 113 | button.setAttribute('aria-selected', 'false'); 114 | }); 115 | 116 | tabPanels.forEach(panel => { 117 | panel.classList.remove('active'); 118 | panel.setAttribute('aria-hidden', 'true'); 119 | }); 120 | 121 | // 激活当前标签 122 | const activeButton = this.container.querySelector(`.jscookie-tab-btn[data-tab-id="${tabId}"]`); 123 | const activePanel = this.container.querySelector(`#${tabId}`); 124 | 125 | if (activeButton) { 126 | activeButton.classList.add('active'); 127 | activeButton.setAttribute('aria-selected', 'true'); 128 | } 129 | 130 | if (activePanel) { 131 | activePanel.classList.add('active'); 132 | activePanel.setAttribute('aria-hidden', 'false'); 133 | } 134 | } 135 | 136 | /** 137 | * 获取当前活动标签ID 138 | */ 139 | public getActiveTabId(): string { 140 | return this.activeTabId; 141 | } 142 | 143 | /** 144 | * 获取容器元素 145 | */ 146 | public getContainer(): HTMLElement | null { 147 | return this.container; 148 | } 149 | } 150 | 151 | /** 152 | * 导出标签页相关函数和类型 153 | */ 154 | export { createTabsHTML } from './tabs.html'; 155 | export type { TabConfig } from './tabs.html'; -------------------------------------------------------------------------------- /src/ui/views/about/about.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 创建关于视图 3 | * @returns 关于视图HTML字符串 4 | */ 5 | export function createAboutView(): string { 6 | const version = '0.11'; 7 | 8 | return ` 9 |
10 |
11 |

关于

12 |
13 | 14 |
15 | 20 | 21 |

22 | JS Cookie Monitor/Debugger Hook是一个用于监控和调试网页中JavaScript对Cookie操作的工具。 23 | 它可以帮助开发人员跟踪Cookie的读取、添加、修改和删除操作,并支持在特定条件下设置断点。 24 |

25 | 26 |
27 |

主要功能

28 |
    29 |
  • 实时监控Cookie的添加、修改和删除操作
  • 30 |
  • 支持根据Cookie名称和值设置断点
  • 31 |
  • 支持使用正则表达式匹配Cookie
  • 32 |
  • 彩色日志输出,便于区分不同操作
  • 33 |
  • 支持配置忽略值未变化的Cookie更新
  • 34 |
35 |
36 | 37 |
38 |

使用说明

39 |

40 | 通过油猴菜单或快捷键(Ctrl/Cmd + Shift + J)打开配置面板, 41 | 在"断点列表"标签页中添加或管理断点规则,在"全局设置"中调整工具配置。 42 |

43 |
44 | 45 | 52 |
53 |
54 | `; 55 | } -------------------------------------------------------------------------------- /src/ui/views/about/index.ts: -------------------------------------------------------------------------------- 1 | export { createAboutView } from './about'; -------------------------------------------------------------------------------- /src/ui/views/breakpoints/breakpoints.ts: -------------------------------------------------------------------------------- 1 | import { debuggerRules } from '../../../cookie-monitor/config'; 2 | import logger from '../../../logger/logger'; 3 | 4 | /** 5 | * 创建断点列表视图 6 | * @returns 断点列表HTML字符串 7 | */ 8 | export function createBreakpointsView(): string { 9 | try { 10 | return ` 11 |
12 |
13 |

Cookie断点规则配置

14 |

15 | 在这里配置触发断点的Cookie规则,支持字符串匹配和正则表达式。 16 |

17 |
18 | 19 |
20 | ${generateBreakpointsList()} 21 |
22 | 23 |
24 |
25 | 28 |
29 |
30 |
31 | `; 32 | } catch (error) { 33 | logger.error('创建断点列表视图失败', error); 34 | return '
加载断点列表失败
'; 35 | } 36 | } 37 | 38 | /** 39 | * 生成断点规则列表HTML 40 | * @returns 断点规则列表HTML字符串 41 | */ 42 | function generateBreakpointsList(): string { 43 | if (!debuggerRules || debuggerRules.length === 0) { 44 | return ` 45 |
46 |

暂无断点规则,点击下方按钮添加。

47 |
48 | `; 49 | } 50 | 51 | const ruleItems = debuggerRules.map((rule, index) => { 52 | let ruleDisplayText = ''; 53 | 54 | // 根据规则类型生成显示文本 55 | if (typeof rule === 'string') { 56 | ruleDisplayText = `Cookie名称包含: "${rule}"`; 57 | } else if (rule instanceof RegExp) { 58 | ruleDisplayText = `Cookie名称匹配正则: ${rule.toString()}`; 59 | } else if (typeof rule === 'object') { 60 | // 对象类型规则 61 | ruleDisplayText = formatObjectRule(rule); 62 | } 63 | 64 | return ` 65 |
66 |
67 | ${ruleDisplayText} 68 |
69 |
70 | 73 | 76 |
77 |
78 | `; 79 | }).join(''); 80 | 81 | return ` 82 |
83 | ${ruleItems} 84 |
85 | `; 86 | } 87 | 88 | /** 89 | * 格式化对象类型的规则为可读文本 90 | * @param rule 规则对象 91 | * @returns 格式化后的规则文本 92 | */ 93 | function formatObjectRule(rule: any): string { 94 | try { 95 | const parts: string[] = []; 96 | 97 | // 检查事件类型 98 | if (rule.events) { 99 | parts.push(`事件: ${rule.events}`); 100 | } 101 | 102 | // 检查Cookie名称 103 | if (rule.name) { 104 | if (typeof rule.name === 'string') { 105 | parts.push(`Cookie名称: "${rule.name}"`); 106 | } else if (rule.name instanceof RegExp) { 107 | parts.push(`Cookie名称匹配: ${rule.name.toString()}`); 108 | } 109 | } 110 | 111 | // 检查Cookie值 112 | if (rule.value) { 113 | if (typeof rule.value === 'string') { 114 | parts.push(`Cookie值: "${rule.value}"`); 115 | } else if (rule.value instanceof RegExp) { 116 | parts.push(`Cookie值匹配: ${rule.value.toString()}`); 117 | } 118 | } 119 | 120 | return parts.join(', '); 121 | } catch (error) { 122 | return '无效规则'; 123 | } 124 | } -------------------------------------------------------------------------------- /src/ui/views/breakpoints/index.ts: -------------------------------------------------------------------------------- 1 | export { createBreakpointsView } from './breakpoints'; -------------------------------------------------------------------------------- /src/ui/views/settings/index.ts: -------------------------------------------------------------------------------- 1 | export { createSettingsView } from './settings'; -------------------------------------------------------------------------------- /src/ui/views/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import logger from '../../../logger/logger'; 2 | import { getConsoleLogFontSize, getIgnoreUpdateButNotChanged } from '../../../cookie-monitor/config'; 3 | 4 | /** 5 | * 创建设置视图 6 | * @returns 设置视图HTML字符串 7 | */ 8 | export function createSettingsView(): string { 9 | try { 10 | const fontSize = getConsoleLogFontSize(); 11 | const ignoreUnchanged = getIgnoreUpdateButNotChanged(); 12 | 13 | return ` 14 |
15 |
16 |

全局设置

17 |

18 | 配置JS Cookie Monitor的全局设置。 19 |

20 |
21 | 22 |
23 |
24 |

日志设置

25 | 26 |
27 | 28 | 29 | px 30 |
31 | 32 |
33 | 34 | 41 |
42 |
43 | 44 |
45 |

Cookie监控设置

46 | 47 |
48 | 49 | 50 |
51 | 52 |
53 | 54 | 55 |
56 |
57 |
58 |
59 | `; 60 | } catch (error) { 61 | logger.error('创建设置视图失败', error); 62 | return '
加载设置失败
'; 63 | } 64 | } -------------------------------------------------------------------------------- /src/utils/debugger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取代码调用位置 3 | * @returns 代码位置字符串 4 | */ 5 | export function getCodeLocation(): string { 6 | try { 7 | const callstack = new Error().stack?.split("\n") || []; 8 | 9 | // 尝试查找包含特定标识的行 10 | const index = callstack.findIndex(line => line && line.indexOf("cc11001100") !== -1); 11 | 12 | if (index !== -1 && index + 2 < callstack.length) { 13 | // 如果找到标识,返回标识后的第三行 14 | return callstack[index + 2]?.trim() || "未知位置"; 15 | } 16 | 17 | // 如果没找到标识或堆栈不足,返回堆栈中有意义的一行 18 | for (let i = 0; i < callstack.length; i++) { 19 | const line = callstack[i]; 20 | if (line && !line.includes("getCodeLocation") && !line.includes("Error") && !line.includes("at Object.") && !line.includes("at Module.")) { 21 | return line.trim(); 22 | } 23 | } 24 | 25 | // 如果实在找不到合适的行,至少返回堆栈的第一行 26 | return callstack[0]?.trim() || "未知位置"; 27 | } catch (e) { 28 | // 出错时返回安全值 29 | return "位置获取失败"; 30 | } 31 | } -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | import { KeyValuePair } from '../types'; 2 | 3 | /** 4 | * 生成日志输出的格式化数组 5 | * @param messageAndStyleArray 消息和样式数组 6 | * @returns 格式化数组 7 | */ 8 | export function genFormatArray(messageAndStyleArray: string[]): string { 9 | const formatArray: string[] = []; 10 | for (let i = 0, end = messageAndStyleArray.length / 2; i < end; i++) { 11 | formatArray.push("%c%s"); 12 | } 13 | return formatArray.join(""); 14 | } 15 | 16 | /** 17 | * 把按照等号=拼接的key、value字符串切分开 18 | * @param s 包含键值对的字符串 19 | * @returns 切分后的键值对对象 20 | */ 21 | export function splitKeyValue(s: string): KeyValuePair { 22 | let key = "", value = ""; 23 | const keyValueArray = (s || "").split("="); 24 | 25 | if (keyValueArray.length) { 26 | key = decodeURIComponent(keyValueArray[0].trim()); 27 | } 28 | 29 | if (keyValueArray.length > 1) { 30 | value = decodeURIComponent(keyValueArray.slice(1).join("=").trim()); 31 | } 32 | 33 | return { 34 | key, value 35 | }; 36 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debugger'; 2 | export * from './format'; 3 | export * from './time'; -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 时间工具函数集合 3 | * 提供了各种时间格式化、解析和操作函数 4 | */ 5 | 6 | /** 7 | * 格式化当前时间为指定格式的字符串 8 | * @param format 输出格式,可选值: 9 | * - 'default': "[YYYY-MM-DD HH:mm:ss.SSS]" 10 | * - 'date': "YYYY-MM-DD" 11 | * - 'time': "HH:mm:ss" 12 | * - 'datetime': "YYYY-MM-DD HH:mm:ss" 13 | * - 'iso': ISO 8601格式 14 | * @returns 格式化后的时间字符串 15 | */ 16 | export function now(format: 'default' | 'date' | 'time' | 'datetime' | 'iso' = 'default'): string { 17 | const date = new Date(); 18 | 19 | switch (format) { 20 | case 'date': 21 | return formatDate(date); 22 | case 'time': 23 | return formatTime(date); 24 | case 'datetime': 25 | return formatDateTime(date); 26 | case 'iso': 27 | return date.toISOString(); 28 | case 'default': 29 | default: 30 | return `[${formatDateTime(date, true)}]`; 31 | } 32 | } 33 | 34 | /** 35 | * 格式化日期部分 (YYYY-MM-DD) 36 | * @param date Date对象 37 | * @returns 格式化后的日期字符串 38 | */ 39 | export function formatDate(date: Date): string { 40 | return date.toLocaleDateString('zh-CN', { 41 | year: 'numeric', 42 | month: '2-digit', 43 | day: '2-digit' 44 | }).replace(/\//g, '-'); 45 | } 46 | 47 | /** 48 | * 格式化时间部分 (HH:mm:ss) 49 | * @param date Date对象 50 | * @param withMilliseconds 是否包含毫秒 51 | * @returns 格式化后的时间字符串 52 | */ 53 | export function formatTime(date: Date, withMilliseconds = false): string { 54 | const time = date.toLocaleTimeString('zh-CN', { 55 | hour: '2-digit', 56 | minute: '2-digit', 57 | second: '2-digit', 58 | hour12: false 59 | }); 60 | 61 | return withMilliseconds 62 | ? `${time}.${String(date.getMilliseconds()).padStart(3, '0')}` 63 | : time; 64 | } 65 | 66 | /** 67 | * 格式化日期和时间 (YYYY-MM-DD HH:mm:ss) 68 | * @param date Date对象 69 | * @param withMilliseconds 是否包含毫秒 70 | * @returns 格式化后的日期时间字符串 71 | */ 72 | export function formatDateTime(date: Date, withMilliseconds = false): string { 73 | return `${formatDate(date)} ${formatTime(date, withMilliseconds)}`; 74 | } 75 | 76 | /** 77 | * 获取用户浏览器的当前时区偏移(分钟) 78 | * @returns 时区偏移(分钟) 79 | */ 80 | export function getTimezoneOffset(): number { 81 | return new Date().getTimezoneOffset(); 82 | } 83 | 84 | /** 85 | * 获取用户浏览器的时区名称 86 | * @returns 时区名称字符串,如 "Asia/Shanghai" 或 "UTC+8" 87 | */ 88 | export function getTimezoneName(): string { 89 | try { 90 | return Intl.DateTimeFormat().resolvedOptions().timeZone; 91 | } catch (e) { 92 | // 如果Intl API不可用,返回基于偏移量的时区名称 93 | const offset = -getTimezoneOffset() / 60; // 转换为小时且反转符号 94 | return `UTC${offset >= 0 ? '+' : ''}${offset}`; 95 | } 96 | } -------------------------------------------------------------------------------- /test_cookie/page-has-cookie-define-properties.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 页面中有自定义的cookie的property 6 | 7 | 8 | 9 | 10 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test_cookie/page-has-cookie-define-property.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 页面中有自定义的cookie的property 6 | 7 | 8 | 9 | 10 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "ESNext", 5 | "lib": ["DOM", "ES6"], 6 | "allowJs": true, 7 | "checkJs": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": false, 18 | "sourceMap": true, 19 | "noImplicitAny": false 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules"] 23 | } -------------------------------------------------------------------------------- /userscript-headers.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JS Cookie Monitor/Debugger Hook 3 | // @namespace https://github.com/CC11001100/js-cookie-monitor-debugger-hook 4 | // @version 0.11 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 GM_registerMenuCommand 11 | // ==/UserScript== 12 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const webpackPackageJson = require("./package.json"); 4 | const fs = require("fs"); 5 | 6 | module.exports = { 7 | entry: { 8 | index: "./src/index.ts" // 修改入口文件为 .ts 9 | }, 10 | output: { 11 | // filename: "[name]-[hash].js", 12 | filename: "[name].js", 13 | path: path.resolve(__dirname, "dist"), 14 | }, 15 | resolve: { 16 | extensions: [".ts", ".js"] // 添加 .ts 扩展名 17 | }, 18 | optimization: {}, 19 | plugins: [ 20 | // 在打包后的文件头插入一些banner信息,官方插件: 21 | // https://webpack.js.org/plugins/banner-plugin/ 22 | new webpack.BannerPlugin({ 23 | // 是否仅在入口包中输出 banner 信息 24 | entryOnly: true, 25 | // 保持原样 26 | raw: true, 27 | banner: () => { 28 | // 渲染文件头,目前只支持这些变量,有点丑,先凑活着用... 29 | let userscriptHeaders = fs.readFileSync("./userscript-headers.js").toString("utf-8"); 30 | userscriptHeaders = userscriptHeaders.replaceAll("${name}", webpackPackageJson["name"] || ""); 31 | userscriptHeaders = userscriptHeaders.replaceAll("${namespace}", webpackPackageJson["namespace"] || ""); 32 | userscriptHeaders = userscriptHeaders.replaceAll("${version}", webpackPackageJson["version"] || ""); 33 | userscriptHeaders = userscriptHeaders.replaceAll("${description}", webpackPackageJson["description"] || ""); 34 | userscriptHeaders = userscriptHeaders.replaceAll("${document}", webpackPackageJson["document"] || ""); 35 | userscriptHeaders = userscriptHeaders.replaceAll("${author}", webpackPackageJson["author"] || ""); 36 | userscriptHeaders = userscriptHeaders.replaceAll("${repository}", webpackPackageJson["repository"] || ""); 37 | 38 | // 如果存在 banner 的话,则读取插入 39 | const bannerFilePath = "./banner.txt"; 40 | if (fs.existsSync(bannerFilePath)) { 41 | let banner = fs.readFileSync(bannerFilePath).toString("utf-8"); 42 | banner = banner.replaceAll("${name}", webpackPackageJson["name"] || ""); 43 | banner = banner.replaceAll("${namespace}", webpackPackageJson["namespace"] || ""); 44 | banner = banner.replaceAll("${version}", webpackPackageJson["version"] || ""); 45 | banner = banner.replaceAll("${description}", webpackPackageJson["description"] || ""); 46 | banner = banner.replaceAll("${document}", webpackPackageJson["document"] || ""); 47 | banner = banner.replaceAll("${author}", webpackPackageJson["author"] || ""); 48 | banner = banner.replaceAll("${repository}", webpackPackageJson["repository"] || ""); 49 | userscriptHeaders += "\n" + banner.split("\n").join("\n// ") + "\n"; 50 | } 51 | 52 | return userscriptHeaders; 53 | } 54 | }), 55 | ], 56 | module: { 57 | rules: [ 58 | { 59 | test: /\.ts$/, // 添加 TypeScript 文件的处理规则 60 | use: 'ts-loader', 61 | exclude: /node_modules/ 62 | }, 63 | { 64 | test: /\.css$/, 65 | use: ['style-loader', 'css-loader'] 66 | }, 67 | { 68 | test: /\.(png|svg|jpg|gif)$/, 69 | use: ['file-loader'] 70 | }, 71 | { 72 | test: /\.(woff|woff2|eot|ttf|otf)$/, 73 | use: ['file-loader'] 74 | } 75 | ] 76 | } 77 | }; -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const common = require("./webpack.common.js"); 2 | const {merge} = require("webpack-merge"); 3 | const webpack = require("webpack"); 4 | const path = require("path"); 5 | 6 | module.exports = env => { 7 | return merge(common, { 8 | mode: "none", 9 | //开启这个可以在开发环境中调试代码 10 | devtool: "source-map", 11 | devServer: { 12 | static: false, 13 | allowedHosts: "all", 14 | compress: false, 15 | port: 10086, 16 | hot: true 17 | }, 18 | plugins: [ 19 | //这两个插件用于开发环境时,修改保存代码之后页面自动刷新 20 | new webpack.HotModuleReplacementPlugin() 21 | ] 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const common = require("./webpack.common.js"); 2 | const {merge} = require("webpack-merge"); 3 | module.exports = merge(common, { 4 | // 禁用source map减少输出文件 5 | devtool: false, 6 | // 两个原因: 7 | // 1. 在油猴商店上架的脚本不允许混淆和压缩 8 | // 2. 不混淆不压缩保留注释能够稍微增加一点用户的信任度 9 | mode: "none", 10 | // 额外配置 11 | output: { 12 | // 设置输出为单文件 13 | filename: "index.js", 14 | // 禁止生成目录结构 15 | clean: true 16 | } 17 | }); 18 | 19 | --------------------------------------------------------------------------------