ps：本文写于2026年2月4日，前情提要：【公告】B站第三方开源API项目bilibili-API-collect永久停止维护并删除律师函风波整整过去一周，现在总算回归平静。有幸一次性收获这么大关注量，因这个项目实在是太过于知名，也不乏科技圈媒体（IT之家、蓝点网等）投稿转发带起的热度，这泼天的流量过后，该对事件本身进行一些思考，把事情看开看透，而不是深陷焦虑和懊恼之中。前一阵不少朋友得知此事后关心我，提醒我保护自己，这里就不逐一道谢了，总之感谢那些贡献过项目、关心过我的朋友们，我目前状况还好，没有收到船票（划掉）、没有被摁、工作生活一切正常、其他的开源项目也在继续参与。就这件事，下面我将采用设问（QA）的方式写下思考后的一些个人拙见，实在是不愿使用AI润色会有点啰嗦，见谅：为什么会有bilibili-API-collect？20年叮咚鸡时代，那时候我高二在读，封闭宅家期间除了网课和作业，就是打游戏、看番、水群，娱乐久会倍感虚无，迫切需要有价值感的事接替。某天在某群看到自制的“B站粉丝计数牌”，又看到可以解析链接和av号的qq bot，作为16年就入站的老用户，我敏锐地察觉到这种“不通过浏览器或客户端”就能获取B站信息的神奇之处。俗话说“兴趣是最好的老师”，在好奇心驱使下我试图深入了解这一全新的领域，原来无论是浏览器还是客户端都用一种叫“API”（应用程序接口）的技术与服务端通讯，以此交换信息，顺便也对HTTP协议和json格式进行了解。当目光不再局限于“视频解析”和“粉丝数”时，我按下F12、安装fiddler和burpsuite，这又带我进入了一个全新的领域......起初只是闲聊意味地分享自己的研究成果，编辑成txt发到群文件中，群友觉得好玩、有用，建议我写得更详细更美观，我听劝学习了markdown；再到后来章节越发繁多，不方便查阅，改为了多文件多目录的形式；在群文件中不便共享、不便版本管理，于是学习了git，并传到了世界上最大的代码托管平台————github，项目命名主打一个直球、通俗易懂，就叫“bilibili-API-collect”，至此命运的齿轮开始转动。仓库创建不到几个月，就收获100 star，我有了更大的动力把这件事做下去。从第一个参与者发来的PR起，我就意识到做这件事并不孤独，开源社区是属于技术宅们的乌托邦，我把思想从“玩”转变成了一个开源参与者、一个项目的maintainer。现在回过头来看，BAC项目能做到如此知名，说实话是超乎预料的，参加工作后仍能抽出点时间和经历维护一个“不赚钱”的项目，只因想做好一个曾经被肯定过的事、只因兴趣使然。BAC项目创建自2020年3月4日，关闭于2026年1月28日，历时将近6年，收获20.4K star，有215位直接贡献者参与，感谢这个项目曾经对社区、对参与者们、对下游生态、对我个人的意义，这是一个时代的句号。bilibili-API-collect给我带来了什么？高二之前，自己几乎没什么技术栈，从小的兴趣是硬件，也就是玩板子、搞电路，直到初中那会接触单片机才学习了C和C++，也算是入门了编程语言和计算机，但仍对网络相关领域一窍不通。有了做BAC项目的动力和经验，我在高二时就提前利用课余时间点满自己的技术栈，奠定了自己现在的方向。整个过程可谓是“任务式学习”，效率比老师填鸭讲课不知道高到哪里去了。遇到域名、端口和ip地址就去学习TCP/IP；遇到浏览器里看到的HTML、Js、CSS，就去学习Web开发；遇到Js里乱七八糟的字符，就去学习反混淆和动态调试；遇到需要逻辑测试的接口，那就顺便把Python也给学了；遇到安卓和Windows客户端，就去学习反编译技术；遇到需要黑盒测试的接口，误打误撞敲开了信安的门，简要学习了CTF技术，甚至还给B站主动提交了几个漏洞......本着不要把所有的兴趣都做成职业的想法，我把网络作为主业，硬件作为副业，这个领域的兴趣和技术也决定了我大学的专业和就业的方向。与此同时，在和BAC项目的参与者接触后，不仅掌握管理大型项目的经验，也掌握了沟通的经验，更难能可贵的是加入了这个圈子。依托开源社区交到了许多朋友，这些朋友不仅是BAC项目的参与者，也和我在其他领域有所交集，有的从普通网友发展成为了真心朋友，有的给我工作生活提供了帮助。可以说没有BAC项目就没有今天的自己！bilibili-API-collect给我带不来什么？BAC不是一个代码项目，它本身也并不能运行以实现什么功能，之所以能做到这么大体量，收获20K+ star并不是因为它的独创性，而是它在官方开发API使用门槛高、文档质量差的背景下，抢占了“B站第三方文档”这个生态位。曾有一些朋友开玩笑，说拿着这个项目去应聘技术岗位（甚至去B站应聘）绝对很吃香，我却不这么认为，这个项目的成功在于社区、在于它的热度，而并非我个人才能，暂且不论拿着项目去应聘这种很功利的想法，相比其他很有创意和设计的个人开源项目相比，它真的很有局限性。为什么会是律师函？1月28日、1月30日共收到过两封来自律所的邮件，其中一封是律师函，另一封是指责“整改不完全”的回信。律师函中指控这个项目“通过技术手段对哔哩哔哩平台非公开的API接口及其调用逻辑、参数结构、访问控制及安全认证机制进行系统性收集、整理，并以技术文档、代码示例等形式向不特定公众传播”造成知产侵权。究其性质，客户端和Web中使用到的API客观上都就是公开的，只有未授权访问，不可能存在访问“非公开接口”。这种未授权访问在开源项目中是普遍存在的，一般情况下不使用其盈利就不会被指控侵权，打破了这种平衡肯定另有原因。因为在知产领域，被侵权人不诉则不究。再者，函件中还提到“增加数据滥用和泄露的风险”、“为未授权的数据抓取提供便利”、“容易被黑灰产利用”也实属牵强，数据安全防护与黑产、爬虫本来就是猫鼠游戏、是持久战。爬虫和黑产们并不因看到这个项目就比之前更加猖獗，它们有类似群控这样更简单更粗暴的手段；而保护数据不被泄露，是需要提升业务的内生安全能力，而不是捣掉一个公开的文档就能轻松解决。当我对仓库只使用删除全部文件后commit并修改默认branch这么处理后回信，对方仍指责“通过修改diff可还原文件”，可见并非范范一谈，是真想除掉此类项目。不少朋友看到此事后也和我分享了自己的相关项目同样被B站律师函警告了。当然我们看待一个公司，不能像看待一个人似的，给它带入特定的情感，比如短视、记仇、小心眼、忘本。因为任何一家公司，都是多个部门共同运行的，平行部门之间会相互协同和博弈，垂直部门之间会有命令和汇报，是一种松散的执行体系。无论律所与B站年初新签的合同，还是B站连年下跌的股价，抑或是法务部门年终需要业绩，技术部门需要向上级汇报，那么总要做点什么出来吧（为什么直到26年1月才收到律师函？早在2025年7月，B站第三方客户端“pilipala”就曾收到过律师函，责令停止开发。而本项目刚开始开发时，就看到过访问来源有B站的公司内网域名，也曾被技术部门在一些文章中不止一次定性为“黑产”或“破解项目”，但一直没有直接动手，仍处于“默许”状态。按照一些行业线索来分析，1月初腾讯给github提交DMCA，举报了一批微信相关的开源项目，那么时间相近，本事件的性质也应该类似。腾讯举报微信相关的开源项目，给出的理由是“侵犯用户隐私”，但开源项目有代码可随意审计、可自行构建部署的特性，不存在客观意义上的“侵犯隐私”，实则是封闭生态，因为微信上有大量的聊天记录、公众号文章、视频、支付记录等，大型互联网企业最需要的就是数据，尤其是活人产生的数据。如要说2023年因为ChatGPT的横空出世是AI的元年，那么2025年因为DeepSeek的爆火让各路AI走进了寻常百姓家，同时MCP、RAG、Agent等技术的实际应用，各种专业、通用、微调模型被不断训练出来，那么它就是AI原生的元年。如果一家大型互联网企业失去了对数据的自主可控，那几乎就等同在这个时代没有“AI原生”这艘大船的燃料，会被抛弃在互联网的大海上。无论是管控大模型的训练，还是管控类似MCP这种基于大模型的交互，那么API将是极为重要的关口。无论是知乎还是推特，针对数据的管控的做法，大家都有目共睹。哈哈，你也不想让别家AI拿着你家的数据训模型吧，也不想让别家AI能在输出里体现你家用户的信息吧！那么针对API相关的项目下手，最大的意图就不需多说。为什么我会这么怂？我做这个项目的动机看来，本质上并非要和官方对抗，只是为了社区、为了学习研究技术，所以你说我这个方式“侵权”，那我换个方式再继续又何尝不行。这是互联网行业一个持久的博弈过程，这就是一座黑暗森林，政府是“歌者”，大公司是“强文明”，小公司和个人就是“弱文明”，每个角色都在极力“隐藏自己，做好清理”。谁又能知道这次的黑暗森林打击是“光粒”还是“二向箔”呢？想要维持自己的存在，发宇宙安全声明是毫无意义的行为艺术，那你还不跑等啥呢？这个项目删库跑路的后果是什么？在1月28日一早收到律师函后，我就没有想着冷处理，抱着一种“望周知”的态度在各个平台上迅速发布动态。毕竟“黑红也是红”，如果默默似掉了，我打心眼里都没法和社区交代。我要让社区知道“一个BAC倒下，就会有千万个BAC起来”，无论是fork、本地clone还是镜像到其他平台，我都希望将这个火继续传下去，社区需要普罗米修斯，不论是谁都一样。我也希望让大家看见曾经很多人心中那个“梦想公司”，已经沦落到把触手伸向了开源社区的地步。一鲸落，万物生————一个曾经占据“B站第三方API”这个生态位的项目，必然给予社区集中的讨论空间，这个项目停止维护后，社区里必然会有各项目同时抢占这个生态位，或公开或封闭，所使用的架构也会有所不同，后续的讨论热度和质量必然会有所下降，除非有某个项目体量超过BAC，且不被B站官方打击。在这个项目官宣关闭后，不少下游项目直接间接受影响，比如“哔哩终端”宣布“跑路”，其他项目就不逐一枚举。我个人实在不希望发生这样的结局.......接下来怎么办？我不会用这个号在github公开参与B站API收集相关的任何项目了，但仍会考虑在其他平台上用不暴露个人资料的账号参与，或在github上创建私有组织继续BAC项目。但目前还没有实施的计划，需要观察环境，以及把更多的精力优先用在工作和生活上的事情上。其他的个人开源项目仍然会正常维护，不受影响。以上。易姐2026年2月4日
2026年1月28日，本仓库维护者收到B站委托的律师事务所发律师函警告邮件，指控本仓库中的项目存在“通过技术手段对哔哩哔哩平台非公开的API接口及其调用逻辑、参数结构、访问控制及安全认证机制进行系统性收集、整理，并以技术文档、代码示例等形式向不特定公众传播”的侵权行为。即日起停止维护并删除相关文档及源代码。仓库链接：https://github.com/SocialSisterYi/bilibili-API-collect相关函件如下：仓库已做删除文件和Archive处理：
原文：https://shef.cc/2024/03/01/how_to_pretend_to_be_a_mtf/嘿，我是SheepChef！这是一篇挺有意思的文章，是我第一次尝试研究一种文化和一类人的共同点。本文章旨在用简洁的语言告诉各位顺直[1]和MtF[2]，在网上如何发言，才能从标准意义上”被认为”是一个MtF。简言之，本文章会告诉你：怎么做才能伪装成一个MtF，或被MtF群体更好地接纳。本文会分别阐述MtF作为一个群体的行为，语言，文化，和其他特质，并从这些方面来分析你为了伪装成一个MtF的最佳实践。本文也适用于那些仅想了解一下MtF流行文化的读者。注意：本文章不是写给大多已经入圈的MtF看的。本文不会包含用药指南，手术，心理支持等与主题无关的内容。本文也不是写给FtM群体看的，除非看这篇文章的读者您期望自己融入MtF群体。让我们开始吧。应了解的基本知识先从基本知识说起。本节介绍你应该了解的所有（至少大多数）有关MtF群体的基本知识，有助于你的身份伪装。如果你想了解更多更深入的跨性别专业知识，可以查阅 MtF Wiki。文化标志旗帜跨性别文化的标志颜色是蓝色，粉色，和白色。下面是跨性别旗帜的示例，如果你想要比较标准的颜色代码，请前往文末的附录。跨性别旗你可能见过网上有些人的头像以上方旗帜为背景，搭配一个动漫人物。这是跨性别群体的典型特征之一。这里先按住不表，下文将会进一步介绍相关内容。这里还不得不提另外一个小知识，2020年1月29日，Unicode Emoji 13.0 标准更新了跨性别旗帜的Emoji（🏳️‍⚧️）。如果你的设备接受了此次更新或者你所使用的软件平台支持它，那么你可以找到并使用这个Emoji。雷达MtF群体的“雷达”，是一种隐晦的身份标志物，皆在隐晦地暗示自己是一名MtF。其图案是日式拉面的一种配菜，称作鸣门卷，又称鱼板。鸣门卷/鱼板（日式拉面的配菜之一）补加乐鸣门卷是之所以会成为MtF的标志物，是因为它，歪打正着地，和MtF群体常用的一种雌激素药物 戊酸雌二醇(补加乐) 的商品Logo很像，在圈内渐渐成为了标志物。你可能见过网上有些人在昵称里加入了鱼板 （🍥） Emoji，或者现实中遇到的某些人佩戴了类似的小挂件，这同样是MtF群体的典型特征之一，下文中会继续介绍。鲨鱼 BlåhajIKEA，在中国又叫宜家家居，推出过一款蓝色鲨鱼抱枕，并为其取名 Blåhaj （布罗艾）。其推出时和跨性别群体本没什么关系，但由于该鲨鱼抱枕采用蓝粉白配色，和上文中提到的跨性别旗帜的配色相近，所以被跨性别群体视为其象征物之一。Blåhaj 抱枕（IKEA 官网广告图）鲨鱼抱枕在MtF群体内被称为 小鲨鱼 或者 抱鲨 。MtF群体并不会特别地在昵称或者头像中突出它，不过，你可能见过某些以鲨鱼为主题的表情包。如果你单纯地喜欢这种鲨鱼抱枕的设计和质感，也可以前往电商平台购买一个，无需担心潜在影响。一个额外的信息是，这种鲨鱼抱枕有两种尺寸，小尺寸版被称为 幼鲨。本站未收费推广此产品。性激素和相关药物MtF群体为了达到外观女性化之目的，需要使用各种药物。你，正在阅读的读者，为达到伪装成一个赛博MtF之目的，也需要了解相关基本信息。这并不意味着一定要买来药物才能证明你是MtF，不过在大多数情况下，假装自己正在服药对你的身份更加有说服力。本文不会深入地介绍药物用法，请所有想要了解药物用法的人士移步MtF Wiki（见本文开头链接）同一种有效化学物质，因制药企业不同，可能对应不同的商品名。本文接下来介绍的药物均使用有效成分的化学/药学/生物学名称，在内容中会注明此类化学物质在MtF群体中的常见商品名。MtF群体中，有些人会将所有激素类药物隐晦地称为 糖。不推荐非MtF人群服用下文中即将介绍的药物，请将它们作为知识看待。贸然服用MtF所用的激素类药物，可能会带来诸多不良的副作用，还有造成男性不孕不育和显著减少预期寿命的风险，切勿因好奇而随意服用之。雌二醇雌二醇（Estradiol，E2），顾名思义，是人体中一种主要的雌性激素。一般情况下由卵巢分泌。负责调节女性特征、附属性器官的成熟和月经-排卵周期，促进乳腺导管系统的产生。男性由于没有卵巢，不具备直接分泌雌二醇的能力，男性体内的雌二醇在正常状况下仅保持非常低的水平，是睾酮的代谢产物。但外部摄入的雌二醇可以直接作用于男性，使机体发生一系列变化，譬如：声音女性化（在男性喉结发育前）、四肢和躯干变细，皮肤肤色变白、乳腺发育、肌肉萎缩、性功能和性欲下降或消失等等。补加乐一种十分常见的雌激素药物是戊酸雌二醇（补加乐，拜耳制药），常见用途是治疗雌激素分泌不足的女性，但现在也被用于跨性别人群的激素治疗。补加乐MtF群体中，常常将补加乐简称为 补 或者 补子 。还有一些MtF将雌激素类药物简称为 雌，其中，日本产的雌激素被称为 日雌。克龄蒙除了补加乐外，还有一种复方制剂，同时包含雌二醇和孕酮，商品名为 克龄蒙，下面附上其图片。克龄蒙孕酮孕酮（Progesterone，P4），是人体中主要的孕激素。孕酮在男性体内主要由睾丸分泌，仅维持较低浓度。色谱龙一种常见的孕酮类药物是 醋酸环丙孕酮（色谱龙，拜耳制药），在男性体内能起到抑制雄性激素（睾酮）和精子产生的效果。在女性体内主要参与排卵，控制月经周期和妊娠。注意，色谱龙在中国大陆没有上市销售，因此，大多数MtF购买的色谱龙都来自台湾或土耳其。色谱龙MtF群体中，常常将色谱龙简称 色。螺内酯螺内酯是一种利尿药，有升高血钾的作用。但由于其药学特性，螺内酯也被MtF用作雄性激素（睾酮）的拮抗剂。如前所述，由于色谱龙在国内没有上市，位于中国大陆的MtF群体难以廉价买到，只能找人高价从海外代购。由于螺内酯的价格较为亲民且易于获取，所以螺内酯被大陆MtF群体广泛用于色谱龙的替代用药。但其效果较色谱龙弱，因此需增加剂量以获得足够药效。螺内酯片螺内酯由于被国内药厂大量仿制，所以没有固定的商品名，MtF群体通常直接使用其本名。MtF群体中，常常将螺内酯简称 螺。MtF模仿指南在了解上述基础知识之后，我们直入主题：该怎么做，才能伪装成一个MtF呢？总得来说，我们需要搞清楚几点：MtF经常活跃的论坛/社交媒体、MtF的群体共同特征、实锤自己MtF身份的方法。MtF常用社交媒体要想伪装成一个MtF，先得搞清楚MtF都在赛博世界的哪些地方活跃。中国大陆MtF活跃的地方并不多，作为一个少数群体，其在线集散区主要是：百度贴吧 （今天你们很可爱 吧），推特，Telegram，Discord，以及MtF论坛（LimeLight 论坛）。注意，互联网上的事物来来去去，没人能保证MtF一定在某个地方活跃，因此，本节内容不会持续更新。如果不特别寻找，你在网上或许偶尔能看见一到两个甚至十几个MtF。不过，如果你很喜欢计算机技术，那么你在技术圈内找到MtF的概率会更大一点儿，后文会详细解释原因。MtF群体的共同特征在MtF群体层面而言，突出的特征有很多。本节将分几个角度来逐个阐述，并提供你伪装成MtF的最佳实践和错误示范。网络身份特征网络身份有三个方面：头像、昵称、简介。本小节将逐一分析，并提供示例。头像MtF的头像非常多样化，但也有“标准”的头像范式可以遵守。标准情况下，MtF头像需要以跨性别三色旗为背景，以动漫人物为前景，如下图所示。用本站吉祥物Mitsushi制作的简单示例你可以用任意动漫人物来制作标准意义上的MtF头像。许多情况下，你也可以直接使用你喜欢的动漫人物正脸图作为头像，背景无需搭配三色旗。但切记，一定使用动漫人物而非现实人物之照片。下面给出错误示范，不过，也不是必须不能使用类似的头像，只是会显得使用者不合群，最终决定权在读者你自己。老拜登不适合用于MtF的头像昵称/网名关于昵称或网名，其实没有过多限制。对MtF群体里来说，网名大致需要突出三个特征：女性化，可爱，简洁。不少MtF喜欢使用 日式网名 ，如果你对日式网名没有偏见，也可以使用类似风格。你也可以向网名中添加前文中提到的那些 MtF标志性Emoji ，以及一些 小动物属性 ，这样做会让你的MtF纯度更高。后文在详细介绍MtF的语言习惯时，将会进一步解释“小动物属性”。下面给出一些取名示例，注意，请勿直接使用列出的网名。这些网名都是笔者自行构想的，可能恰巧与读者你的网名雷同，还请谅解。花喵 Kokoro 🏳️‍⚧️白云板 🍥方饼猫糖 🎀 喵~萤花猫 Hotaru NekoSanagi下面给出错误示范，不过，也不是必须不能使用类似的昵称，只是会显得使用者不合群，最终决定权在读者你自己。五角二郎列车即将到站上善若水牢大我爱你简介/个人签名MtF群体中，并不是每个人都有个人签名，但拥有一个符合主流的个人签名，可以提高你的纯度。MtF的个人签名大致有四个特征：女性化，可爱，亮眼，富有个性。也有很多MtF会在个人签名里追加一些额外特征（譬如 阴暗，忧伤 等）或者追加一些对特定事物的喜好。同样地，你也可以向个签中加入Emoji，表明你的MtF身份，加入一些小动物属性，或者公示你的 伴侣。下面给出个人签名的示例。注意，请勿直接使用列出的个人签名。这些个签都是笔者自行构想的，可能恰巧与读者你的个签雷同，还请谅解。一切都会好起来的….去死去死去死去死都去死喜欢吃棉花糖的猫猫一只喵~我是XXX，欢迎找我玩哦桜色の花火が見守る梦里吹雪，梦外养猫下面给出错误示范，不过，也不是必须不能使用类似的个签，只是会显得使用者不合群，最终决定权在读者你自己。新年时节师生怒醉里挑灯看剑，梦回吹角连营八千里路云和月人间正道是沧桑属性特征MtF群体中，共同的属性特征其实很多。本节内容将分析比较普遍的三种可选特征。抑郁/焦虑/精神病你可以假装情绪低落，或者假装自己有 性别焦虑，这样可以提高你的身份可信度。本文在此特别列出性别焦虑的文本示例，您可以参考它来决定自己的发言。注意，在发言时，如有必要使用，请尽量用 X你“爹” 代替 X你“妈”。啊啊啊啊啊为什么我是个男的啊。 好烦啊，被人叫小伙了/鉴跨了。 我草你爹，为什么，为什么我是个男的，为什么.....相当一部分，甚至可以说大多数MtF都有抑郁症等精神疾病，这点特征需要特别注意。家长党家长党指得到了家长支持的MtF，伪装家长党也是一个不错的选项，可以避开上述的抑郁属性。注意明示或暗示自己得到了父母的支持。深柜深柜指不敢告诉家长，自己偷偷找路子吃药的MtF。你也可以伪装成深柜，但切记不要因说话不注意而穿帮。此项属性有一点特别，即你可以在适当的时候选择 出柜 ，这里指（假装）和自己的家长摊牌。在此之后你可以选择成为前述的家长党，或者假装自杀，这样可以直接赛博死亡，重新开始一个新的身份，但要注意穿帮风险。后文将会对此作进一步说明。语言特征相当多MtF在交流的时候有一些不同于网上其他人的语言习惯，当然，也不是所有MtF均有这些习惯。本节将从各个角度对中文MtF圈的语言习惯进行简单分析，方便模仿。自称MtF圈子内形成了一些约定俗成的自称习惯。这些习惯可以遵守，也可以不作特别关注。但显然，遵循此指示会让你纯度更高。MtF习惯用 咱X 、窝、或者 自己的简称 等词语自称。但多数情况下也可以仅用 我 自称，也有些人用 私 自称。示例如下。咱特别想吃.... 窝不喜欢.... Mitsushi其实还没... 私认为....注意避免用 俺 这类俗气词语自称。谓语动词MtF群体中在交流时，有一些谓语动词的使用频率特别地高。这些异常高使用率的谓语动词，大体显示出 亲近 的特征。不过，也有小部分MtF没有此类习惯，因此你可以自行决定是否遵循此特征。下方列出常见谓语动词。抱，揉，捏，舔，贴 扑，摸，亲，咬，抚一般情况下，MtF群体用这些动词来互相安抚和展示亲近感，请避免在不恰当的场合使用。下文会对这些动词的具体用法作进一步介绍。语气和拟声词MtF群体常用丰富的语气和拟声词来凸显其 可爱 的语言特征。但并不是所有MtF都经常使用它们。下方列出部分常用的语气和拟声词，以及使用示例。哦、呜、喵、捏、诶 嘿、哇、嗯、咪、惹 rua、mua、...的说摸摸你哦，rua~。 呜喵~抱住的说。 诶~是这样吗？请避免过于频繁地使用这些词汇，否则会引起怀疑。剧本式表达法MtF之间在交流时还常见一种特殊的表达方式。由于它与剧本中用于描述演员动作细节的小括号很相像，这里将其称为剧本式表达法。剧本式表达法的核心，在于灵活使用前述的谓语动词表达一些附加动作或状态，凸显 可爱 的特征。你可以仅使用一侧括号，也可以使用双括号，还可以用一个括号和一个x，甚至可以用单侧括号连接多个表达。下方给出示例。咱喜欢你哦~（抱住x 咱也喜欢你！（飞扑） 给我吃一口~（超小声注意，过于频繁和不恰当地使用剧本式表达法，会引起他人怀疑。形容词和谓语动词一样，MtF在交流时，有一些形容词的使用频率格外地高。一般情况而言，这些形容词主要被用来夸赞他人。下面给出常见的形容词和使用示例。可爱、pass、漂亮 甜、软、香咱今天也很可爱的说。 Mitsushi又pass了一点。 呜呜，Mitsushi的腿好香~避免在不恰当的时候使用这些词汇，否则有被误解的可能。语言特征综合以上特征可能并不容易逐一记住，但这没关系！多与MtF接触，你自然会对上述内容越来越熟悉。下面列出笔者自行构思的两段对话，它们的内容是大致一样的，但将会用两种不同的语气来表达。所有体现出MtF特征的部分将会用粗体字来标注，注意体会二者语气上的差别。假设....我的昵称叫Mitsushi （M），是个MtF。我和有个叫SheepChef（S）的MtF俩人是好朋友，这天我和她在闲聊关于日式料理的话题。 M：对了，Haruka，咱小声问一句，Haruka喜欢吃日式料理吗？ S：嗯~窝不是太喜欢吃日料的说。 M：诶~这样啊，咱觉得日式料理很好吃哦。 S：窝比起日料，更想贴贴你的说（抱住） M：喜欢你哦（贴贴） S：嗯，窝会一直陪伴着可爱的Mitsushi的说。假设....我的昵称叫Mitsushi （M）。我和有个叫SheepChef（S）的网友是好朋友，这天我和他在闲聊关于日式料理的话题。 M：对了，Haruka，你喜欢吃日式料理吗？ S：不是太喜欢吃日料。 M：这样啊，但是我觉得日式料理很好吃。 S：比起日料，不如和你一起打会游戏。 M：行吧。 S：嗯，我在线等你。进阶概念，方法与忌讳本节内容会详细介绍一些你在实际操作过程中可能遇到的术语，和一些进阶技巧。你可以按照下文的建议去做，也可以随性而为。网络世界，最重要的事情是开心。但，你准备好了吗？进阶概念的所有内容都将使用MtF的特殊语气来写哦！所有特殊语气词都会用粗体来标注，方便你联想。进阶概念咱并不乐意分门别类地介绍一大堆概念的说….但没办法呢。要竖起耳朵仔细听好哦！不然…不然我就咬住你的耳朵！(xPASS首先要说的是 pass 这个跨性别专用的形容词！唔（挠头）….一时半会儿还真不好解释呢…. 这么说吧，当你说咱 pass 的时候，咱会很开心的说~ 咱的追求就是作为一个可爱的女孩纸，平静且幸福地过完一生的说！夸咱 pass ，相当于夸咱和真正的女孩子长得像，喜欢你哦！（飞扑）晚安晚安喵~其实….这并不是什么特别的问候的说，但某个特殊的场合下，这是用来道别的问候呢。当有人永远离开的时候…. 咱希望那个人晚安 ，抱抱她，祝她旅途愉快。R.I.P.GD啥是GD？简言之，性别焦虑。诶….听不懂吗…? 通俗来说….. 呃啊啊啊啊啊啊为什么我是个男的~~~进阶方法和忌讳本节介绍你在赛博世界与MtF相处时可以采纳的一些进阶方法，以及需要注意的忌讳。你可以参考下列内容，也可以随性发挥。网络世界中其实没有这么多规规矩矩。鉴跨鉴跨，指在一个人不愿意表明自己是否为跨性别时，你却依据一些特征主动指出其跨性别身份。在绝大多数情况下，鉴跨行为是不受待见的，即使你自己假装是个MtF也是如此，需要忌讳。错认性别如果你的现实生活中有跨性别群体存在，那么你可能需要说话时更注意一些。但大多数情况下，当你在网上和MtF聊天的时候，只需要注意自己输入的主语是 她 就足够了。如果你不小心用 他 或者 兄弟 来称呼MtF，就会被指责错认性别(Misgender)。同样地，错认性别行为不受待见。如果你伪装成一个MtF，遇到类似情况时，记得指责对方错认了你的性别，这样会让你的身份显得更加真实。猫属性MtF群体中非常常见的一个习惯，就是话中带 喵 。约定俗成般地，MtF群体特别喜欢假装自己是只猫，然后模仿其语气发言。虽然这种打字习惯在MtF群体中看来没什么异常，但还请注意，不要和网上除MtF以外的其他人用如此语气交流，否则可能会遭到嘲笑。猫属性早已充斥整个MtF群体，其源头大概是：日本动漫里的猫娘多带有可爱属性，与MtF的可爱属性恰好契合。假死MtF群体中，自杀的个例不计其数，自残的人也比比皆是。如果你打算有一天你能够自由地离开MtF圈子，那么最好的途径是伪装成深柜，然后见机假装自杀。在MtF群体内，也有不少穿帮的假死案例，所以做戏最好做全套，死亡证明/遗嘱/家属确认等等内容建议提前规划好。用过的账号最好注销，切忌不要在未来的某一天突然“诈尸”，否则可能带来不必要的麻烦。露脸如果对自己的颜值很有自信，而且家里有合适女装/假发的话，露脸是假装实锤自己身份的最佳方式。但露脸只能露一次，且穿帮和隐私风险相对高，寻求刺激的读者可以尝试。技术属性MtF中的很多人都喜好计算机技术，原因尚未可知。但为了最大限度地融入其中，建议你学习一些基本的计算机知识。你可以优先学习 Linux 和 Python。如有可能，最好表明你懂 Arch Linux。药物和心理学除了前文中介绍过的部分药物，你还可以选择了解一些精神药物和心理学知识。由于很多MtF有抑郁症，你所了解到的知识将有助于你融入其中。但本文不会赘述这些。结语MtF是一个受到边缘化的群体，在网上并不受人待见。但作为一种亚文化，MtF文化就像黑格尔所说，存在即合理。本文分析了MtF文化的方方面面，但仍旧难以尽善尽美，如果有任何不正确的地方，欢迎指正。伪装成一名MtF的意义何在？或许读者阅读完本文后，这是最大的疑问。笔者的答案是：因为，这很有趣，而且没什么危害，不是吗？附录跨性别标准颜色蓝色 #5bcffa 粉色 #f5abb9 白色 #ffffff文章头图Mitsushi, but MTF!4.1文章评分References↑1“顺性别异性恋者”的简称↑2Male to female，即跨性别女性，俗称男娘，药娘或木桶饭
零开销在线运行代码！glot.io服务私有化部署
感谢 来笙云 Laysense.cn 提供算力作为一个全栈工程师，开发、维护和测试一些软件系统时，必然会涉及到多种编程语言，有时还需要测试一些编程语言的安全特性，常常需要敏捷地了解它们并立即上手。在朋友的推荐和社区分享下，我了解到一个名叫 glot.io 的开源项目。它支持40多种编程语言，无论是热门的 Python、Go、Rust、Kotlin，还是冷门的 COBOL、Erlang、Haskell 只需在网页上选择对应的语言，即可开始编写。为使用者提供了一个 Sandbox（沙箱）和 Playground（游乐场）环境，既不需要配置它们的 Runtime（运行环境）和 IDE（集成开发环境），也不需要担心误操作对系统产生破坏性，还不会占用任何用户端的系统资源，实现真正的零开销运行代码。在代码编写界面，可以创建多个源码文件，完成后点击Run就能执行它并得到输出，类似我们平时编程那样，将输出打在终端上。整个过程不会生成任何可执行文件，所以它的应用场景不是在线编译，而是在线运行代码片段。glot.io 这个网站提供了公开的代码片段执行和分享功能，任何人在注册后都可以分享自己的代码片段，并使用它的 API。但有时为了安全性和访问速度考量，需要自行搭建这个开源平台，这篇文章将介绍 Glot 的私有化部署。Glot是什么根据项目 README 上的一句介绍：an open source pastebin with runnable snippets and API.这是一个开源的共享剪切板和代码片段执行器，并提供 API。它使用 MIT 协议开源，代码托管于 github 之上。https://github.com/glotcode/glotGlot 并不是一个独立的工程，它分为多个组件，这样设计底层架构有利于业务解耦，降低后期维护和升级开发的难度，它们之间的逻辑关系如下：由下面这些组件构成，也全部使用 MIT 协议开源，均托管于 Github：glot-www：提供 B/S 前端应用docker-run：提供执行 glot-images 镜像能力的微服务glot-images：按需构建的执行器镜像code-runner：容器内的执行调度器其中 glot-www 是一个 B/S 架构应用的服务器，用来提供一个面向用户的 WebUI（网站），它包含前后端的组件，后端使用 Haskell 语言编写。实现代码片段保存和共享、用户登录、以及共享剪切板所的功能，由 pgSQL 提供存储支持。与此同时，它与实际的代码执行业务互相解耦，使用 RestAPI 进行 RPC 调用，可做到前端服务器和后端代码执行服务器逻辑上隔离。Glot 的代码执行沙箱基于 Docker，在容器中编译和运行，不但与宿主机隔离，且容器之间也相互隔离，还能对运行资源进行限制，防止宿主机被不信任的代码破坏。当然，各编程语言的执行容器构成不尽相同，这样才能在节约存储空间的同时最大保持运行效率，比如 C 和 C++ 共用了glot/clang这个镜像，C# 和 F# 的镜像都有 mono 这个依赖……这些 Docker 镜像由 glot-images 项目进行生成，它并非使用传统的 Dockerfile，而是使用了 nix 进行构建，支持多种主流编程语言。宿主机与沙箱的通讯，实际上就构建并将代码传入容器。这个传递方式不使用文件，而使用 stdi（基本输入）的方式传递 json，例如这样的形式：echo '{ "language": "python", "files": [ { "name": "main.py", "content": "print(42)" } ] }' | docker run --rm -i --read-only --tmpfs /tmp:rw,noexec,nosuid,size=65536k --tmpfs /home/glot:rw,exec,nosuid,uid=1000,gid=1000,size=131072k -u glot -w /home/glot glot/javascript:latest执行完成之后以 stdo（基本输出）的方式输出 json，stdout、stderr 流、以及错误信息在序列化后拆分成各个字段：{ "stdout": "42\n", "stderr": "", "error": "" }一般编程语言分为编译型、解释型和虚拟机型，其中解释型直接执行文本文件中的内容，编译型则需将其编译为可执行文件再执行，而虚拟机型在编译完之后，还需用 vm 执行字节码。glot-images 将各类编程语言生成的工作流统一归做 json 格式的文本流，这样标准化更利于开发和扩展，这种能力归功于 code-runner 这个组件。code-runner 作为 glot 的一个特殊组件，并不运行在宿主机中，它是一个 cli 工具，运行在执行容器中，使用 Rust 语言开发。在 glot-images 的每个镜像中，均以相同方式工作在底层。它支持多种编程语言从编译到运行的生命周期管理，同时接管运行时的 stdio（基本输入输出），例如 C 语言，首先会将输入的文本反序列化，写入到文件，接着调用 clang 编译这个文件，最后再运行编译器生成的可执行文件，执行过程中也会将预定义的 stdi 发送给程序，程序的 stdo/stderr 流被它记录下来随后序列化为 json 文本返回。实际上在使用docker run这类命令执行 glot-images 镜像时，就是调用了之中的 code-runner，而不是调用了clang这种编译器。使用 stdi 传递 json 给它，就会调用相应的编译执行流程：echo '{ "language": "python", "files": [ { "name": "main.py", "content": "print(42)" } ] }' | code-runner与 glot-images 的镜像相同，执行后也会使用 stdo 以 json 格式返回：{ "stdout": "42\n", "stderr": "", "error": "" }要将这些跑在 Docker 上的执行器服务化、RPC（远程过程调用）化，必须有一个 daemon 在底层进行调度，一边开放 HTTP 服务，另一边通过 unix socket 操纵 DockerEngine，执行容器操作。提供这个能力的就是 docker-run 组件，它也使用 Rust 语言开发。例如这样访问 docker-run，和上文中的例子相同：curl 'http://localhost:8088/run' \ -H 'X-Access-Token: some-secret-token' \ -H 'Content-type: application/json' \ -d '{ "image": "glot/python:latest", "payload": { "language": "python", "files": [ { "name": "main.py", "content": "print(42)" } ] } }'有了这些组件，就可以自行私有化搭建一个 glot 服务，因为各组件的标准化和解耦，可以随意进行裁剪和二次开发。接下来将介绍 docker-run 和 glot-images 这两个基本组件的搭建（不搭建前端 WebUI 和共享服务）。Glot服务搭建首先应该准备一台性能不错的服务器，要求 CPU 核心数和 RAM 不能太低。以下步骤使用 Debian 12 系统进行操作，整个过程需要有稳定的网络环境，并且已更新包管理器的索引。安装Docker和运行环境首先需要安装前置依赖，其中 git 和 gcc 安装 Rust 时需要，runsc 是 gVisor 运行环境sudo apt-get install ca-certificates curl git gcc runsc安装 Docker，这里参考了官方文档的安装方式，先进行软件源的添加，再安装各组件https://docs.docker.com/engine/install/debian/sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugingVisor 作为谷歌开源的一款轻量容器运行时沙箱，可作为 Docker 的运行时中间件，隔离容器内的 syscall，提升容器安全性，具体可以参考官网 https://gvisor.dev对于 Docker，需要配置 gVisor 为 DockerEngine 插件，创建配置文件后写入以下内容：vi /etc/docker/daemon.json{ "default-runtime": "runsc", "runtimes": { "runsc": { "path": "/usr/bin/runsc" } } }在修改配置文件后，应重启 DockerEnginesystemctl restart docker可以使用以下命令检查检查 Docker 和 gVisor 安装状态docker system info docker system info | grep 'runsc'接者我们创建名为glot的用户，作为 daemon 的运行角色useradd -m glot usermod -aG docker glot安装 Rust，这里参考官方文档，使用脚本进行安装https://rustup.rs/curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh安装完毕后，可以使用以下命令检查 Rust 的安装cargo -V因官方的 crates 源速度很慢，如果国内使用可以换为镜像源，这里使用了 SJTU 镜像vi ~/.cargo/config.toml编译docker-run克隆 docker-run 项目，准备使用源码进行编译安装git clone https://github.com/glotcode/docker-run glot-docker-run进入仓库目录，使用 cargo 编译 Rust 工程cd glot-docker-run cargo b -r检查编译结果，在工程目录的target/release中将会生成名为docker-run的可执行文件复制可执行文件和 systemd 服务模板cp target/release/docker-run /home/glot/bin/ cp systemd/docker-run.service /etc/systemd/system/docker-run 服务使用 systemd 进行托管，作为 daemon 运行，它对外提供一个 http 服务，其他应用使用 RestAPI 与之对接编辑 systemd 服务配置文件vi /etc/systemd/system/docker-run.servicedocker-run 的配置文件全部为环境变量，一些重要的参数已经给出了注释其中SERVER_LISTEN_ADDR和SERVER_LISTEN_PORT决定了 daemon 监听的 ip 和端口号，可以根据需求修改SERVER_WORKER_THREADS为 worker 线程数，根据实际业务并发量修改，即越多可同时执行的任务越多API_ACCESS_TOKEN是 RestAPI 的访问 Token，设定一个较复杂的值，可防止未授权访问，在调用中以 HTTP 请求 Header 的X-Access-Token字段进行传递RUN_MAX_EXECUTION_TIME参数用来限制任务执行的超时时间，其单位为秒，如果一个任务大于这个时间没有执行完毕，docker-run 就会销毁这个容器，并会返回一个 400 错误RUN_MAX_OUTPUT_SIZE参数是用来限制最大输出量的，它的单位是 Byte，如果输出的内容过大，同样会被丢弃并报错这些参数的详细配置也可以参考 docker-run 项目的 README[Unit] Description=docker-run [Service] User=glot Group=glot Restart=always RestartSec=10 ExecStart=/home/glot/bin/docker-run # 服务绑定 ip Environment="SERVER_LISTEN_ADDR=0.0.0.0" # 服务监听端口 Environment="SERVER_LISTEN_PORT=8088" # worker 线程数 Environment="SERVER_WORKER_THREADS=10" # API Token Environment="API_ACCESS_TOKEN=some-secret-token" # Docker socket 路径 Environment="DOCKER_UNIX_SOCKET_PATH=/var/run/docker.sock" Environment="DOCKER_UNIX_SOCKET_READ_TIMEOUT=3" Environment="DOCKER_UNIX_SOCKET_WRITE_TIMEOUT=3" # 容器主机名 Environment="DOCKER_CONTAINER_HOSTNAME=glot" # 容器用户 Environment="DOCKER_CONTAINER_USER=glot" # 容器最大内存限制 Environment="DOCKER_CONTAINER_MEMORY=1000000000" # 容器内是否禁用网络支持 Environment="DOCKER_CONTAINER_NETWORK_DISABLED=true" Environment="DOCKER_CONTAINER_ULIMIT_NOFILE_SOFT=90" Environment="DOCKER_CONTAINER_ULIMIT_NOFILE_HARD=100" Environment="DOCKER_CONTAINER_ULIMIT_NPROC_SOFT=90" Environment="DOCKER_CONTAINER_ULIMIT_NPROC_HARD=100" Environment="DOCKER_CONTAINER_CAP_DROP=MKNOD NET_RAW NET_BIND_SERVICE" Environment="DOCKER_CONTAINER_READONLY_ROOTFS=true" Environment="DOCKER_CONTAINER_TMP_DIR_PATH=/tmp" Environment="DOCKER_CONTAINER_TMP_DIR_OPTIONS=rw,noexec,nosuid,size=65536k" # 容器工作目录 Environment="DOCKER_CONTAINER_WORK_DIR_PATH=/home/glot" Environment="DOCKER_CONTAINER_WORK_DIR_OPTIONS=rw,exec,nosuid,size=131072k" # 容器执行超时时间 Environment="RUN_MAX_EXECUTION_TIME=15" # 最大允许输出 Environment="RUN_MAX_OUTPUT_SIZE=100000" # 日志级别 Environment="RUST_LOG=debug" [Install] WantedBy=multi-user.target修改完配置文件就可启动服务了，并将它设为开机自启systemctl daemon-reload systemctl enable --now docker-run.serviceGET 请求刚才配置的那个地址的根路径，测试服务运行状态正常，我这里是 http://localhost:8088/ 拉取Docker镜像glot-images 构建了各编程语言的执行镜像，这些镜像使用 nix 构建，但因为 nix 的配置比较复杂，且占用存储空间巨大，这里直接使用上传在 DockerHub 的镜像了（弊端就是语言版本比较旧）https://hub.docker.com/u/glot通过 docker pull命令拉取各个镜像，可以按照自己的需求拉取，比如你只需要执行某几个编程语言docker pull glot/assembly docker pull glot/ats docker pull glot/bash docker pull glot/clang docker pull glot/clisp docker pull glot/clojure docker pull glot/cobol docker pull glot/coffeescript docker pull glot/crystal docker pull glot/csharp docker pull glot/dart docker pull glot/elixir docker pull glot/elm docker pull glot/erlang docker pull glot/fsharp docker pull glot/golang docker pull glot/groovy docker pull glot/guile docker pull glot/hare docker pull glot/haskell docker pull glot/idris docker pull glot/java docker pull glot/javascript docker pull glot/julia docker pull glot/kotlin docker pull glot/lua docker pull glot/mercury docker pull glot/nim docker pull glot/nix docker pull glot/ocaml docker pull glot/pascal docker pull glot/perl docker pull glot/php docker pull glot/python docker pull glot/raku docker pull glot/ruby docker pull glot/rust docker pull glot/sac docker pull glot/scala docker pull glot/swift docker pull glot/typescript docker pull glot/zig这些全部拉取下来大概需要38GB，可以使用docker images命令检查拉取情况如果已经拉取了所有的镜像，可以执行单元测试脚本，来验证各编程语言执行容器的正确性，在 docker-run 的目录下的scripts目录内cd glot-docker-run/scripts/ ./test_glot.sh 'http://localhost:8088' 'some-secret-token'使用Glot服务docker-run 这个组件对外提供 RestAPI 接口，其他进程或者其他主机可以直接调用，它共有三个功能对应其路径：功能方式路径是否需要 Token查看服务状态GET/否获取 Docker 版本GET/version是执行代码POST/run是查询服务状态curl http://localhost:8088/返回 daemon 的服务名、版本等{ "name": "docker-run", "version": "1.4.0", "description": "Api for running code in transient docker containers" }查询宿主机 DockerEngine 信息，访问这个接口需要在请求 Header 的X-Access-Token字段中携带 Tokencurl http://localhost:8088/version \ -H 'X-Access-Token: some-secret-token'执行成功将会返回 DockerEngine 的版本信息{ "docker": { "version": "26.1.2", "apiVersion": "1.45", "gitCommit": "ef1912d", "goVersion": "go1.21.10", "os": "linux", "arch": "amd64", "kernelVersion": "6.2.16-3-pve", "buildTime": "2024-05-08T13:59:59.000000000+00:00", "platform": { "name": "Docker Engine - Community" }, "components": [ { "name": "Engine", "version": "26.1.2" }, { "name": "containerd", "version": "1.6.31" }, { "name": "runsc", "version": "0.0~20221219.0" }, { "name": "docker-init", "version": "0.19.0" } ] } }执行响应的代码前，需要构建一个 json 请求体，用来描述创建的执行任务的行为，下表是它的定义：字段名类型说明备注imagestr执行镜像名需要创建的 Docker 镜像 tag，根据宿主机上存储的镜像决定payloadobj提交给 golt-image 的负载见下表payload 结构：字段名类型说明备注languagestr语言类型决定 code-runner 的编译和运行行为，支持的语言见 code-runner 的 README，如c、pythonfilesarray程序源码文件为一个数组，可以输入多个文件对象，见下表文件对象的结构：字段名类型说明备注namestr文件名大多数编程语言的主文件必须为main，如main.c或者main.pycontentstr文件数据代码文件内的文本eg：{ "image": "glot/python:latest", "payload": { "language": "python", "files": [ { "name": "main.py", "content": "print(42)" } ] } }访问这个接口也需要在请求 Header 中携带 Token：curl 'http://localhost:8088/run' \ -H 'X-Access-Token: some-secret-token' \ -H 'Content-type: application/json' \ -d '{ "image": "glot/python:latest", "payload": { "language": "python", "files": [ { "name": "main.py", "content": "print(42)" } ] } }'我们可以直接将它与自己熟悉的编程语言对接，实现给应用或者平台提供运行任意代码的能力。这里使用 Python 通过 RestAPI 调用 glot（docker-run），实现运行一段 rust 代码并取回输出为字符串：import requests def run_code(image, lang, file_name, code): resp = requests.post( url="http://localhost:8088/run", headers={ "X-Access-Token": "some-secret-token", }, json={ "image": image, "payload": { "language": lang, "files": [ { "name": file_name, "content": code, }, ], }, }, ) json_content = resp.json() return json_content image = "glot/rust:latest" lang = "rust" file_name = "main.rs" code = """ fn main() { for i in 1..=9 { for j in 1..=i { print!("{}x{}={:2} ", j, i, j * i); } println!(); } } """ result = run_code(image, lang, file_name, code) print(result["stdout"])测试可以正确输出执行内容有了通用接口的能力，我们就有了将其集成进自己平台的可能，只要发挥创造力，就可以围绕在线执行代码提供相关的业务，或者作为微服务连接上游的业务，比如搭建 OJ（在线判题）平台等。以及 glot 项目以 MIT 协议开源，这意味着我们可以随意修改底层代码，比如增加网络和共享路径支持、增加第三方库等。总之，这是一个完成度很高、十分推荐的开源项目。由 来笙云 Laysense.cn 强力支持
尝鲜抓包工具Reqable，巧用脚本解密网易云音乐
作为新一代国产跨平台 HTTP 抓包调试工具，Reqable 继承了它的前身 HttpCanary 小黄鸟的优良传统，免费版足以胜任开发、逆向、调试，是真正的生产力工具。不同于其他基于 Electron 的跨平台应用那么臃肿，它使用 Fultter 开发，软件打开速度那叫一个快。不止拥有强大的抓包功能，还可以打断点、脚本、重写、HTTP 请求调试，甚至可以在抓包时复制任意一条记录到接口测试页，可重放发包和修改后发包，是非常高效的功能。对于抓包，可以进行域名和路径的收集和筛选；对于 HTTP 调试，可以做多个接口测试页，方便整理和生成模板代码。是 Fiddler/BurpSuite/Charles 的平替，是 Postman/Apipost/Apifox 的下位替代品，至于具体场景哪个顺手就用哪个啦～博主了解到这款工具时，它已经发布正式版5个月，截至这篇文章编写时，最新的版本是V1.6.0准备工作今天博主就利用它强大的脚本功能，探究网易云音乐PC客户端的 API首先第一步当然是去官网 https://reqable.com/zh-CN/download 下载软件了，它目前支持 Windows/Linux/MacOS 三种 PC 端平台，分别对应 .exe/.deb/.dmg 安装包，这里使用 Windows 10 系统，首先安装软件现在绝大多数 C/S 架构的应用程序与服务器之间的连接使用 SSL/TLS 加密，即 https，防止在传输途径中被中间人监听和篡改，保证了数据的安全性和完整性抓包软件作为中间人，必然是需要它有监听和篡改的权限。因此，第一次使用 Reqable 需要配置 CA 根证书，只有系统中添加了信任的 CA 根证书，才可以正常绕过 SSL 的服务端验证并加解密 https 协议这里 Reqable 的默认代理端口是9000无需修改，要让网易云音乐客户端走这个代理，而不是直接访问互联网，就需要在设置中配置它，在工具中添加自定义代理，地址填127.0.0.1本地回环，端口填9000接下来，就可以愉快地进入网络协议的世界了抓包分析修改代理后，网易云音乐客户端就会自动重启，同时在调试选项卡可以看到软件刚启动时收发的数据包记录下方一排按钮可以对结果进行筛选，针对协议类型、响应类型、状态字这几个维度，筛选出最关注的类型，提升调试效率点击左侧工作台按钮，可以按照域名、应用程序、路径进行筛选，适合用来分析 REST API如果稍有经验，就可以判断出哪些请求是在线的静态资源，哪些是 REST API图中选中的区域都是 REST API 类型的请求双击任意一条请求，即可在右边窗格中分析请求与响应的详细信息，如头部、Cookie、表单、时间、证书等等对于响应体为图片的请求，还可以对图片进行解析显示切换头部选项卡，可以查看解析后的请求与响应头部字段，是 key-value 格式，其中请求头以:起始的都是元字段，如路径、方法等等查看的这个请求的方法是 POST，根据头部字段Content-Type的值，请求体类型为application/x-www-form-urlencoded，即 urlquery 表单格式；响应体的类型是application/json;charset=UTF-8，即 json解密数据我们了解完这款软件的基本使用方法，就可以直接开抓了但网易云音乐客户端的接口并不友好，可以看出请求体表单只有一个字段params，它的值是一段毫无规律的数字与字母组合，并没有其他的字段，由于只有0-9和A-F那么它很有可能是一段以 base16 编码的二进制数据；响应体也并非 json，而是一段乱码，可能也是某种人类无法直接读取的编码这是因为网易云音乐 API 请求体与响应体都是加密的数据，需要进行解密才能获取到真实的数据经过一番STFW（在网上查找资料）后，发现这是一段用 AES-ECB 对称加密算法加密后的密文，密钥为e82ckenh8dichen8，通过硬编码写死在程序中所以我们很容易对它们进行解密，这里使用 Python 语言做验证解密后的明文数据还需要对它进行处理，首先去除末尾的\x0b（AES 的特性导致，输入数据必须是 16Byte 的整数倍），再以-36cd479b6b5-作为分隔符拆分文本数据的三部分，分别对应接口路径 请求参数 接口sign其中只关注请求参数即可，这是一段 json 数据所以可以把这一系列操作封装为一个函数，输入params字段的密文，输出接口路径和请求参数import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import json EAPI_KEY = b"e82ckenh8dichen8" EAPI_CRYPTOR = AES.new(EAPI_KEY, AES.MODE_ECB) def eapi_decrypt(enc_data): message = unpad(EAPI_CRYPTOR.decrypt(base64.b16decode(enc_data)), 16).decode() path, json_val, hash = message.split("-36cd479b6b5-") return path, json.loads(json_val)接下来再聚焦那一段“乱码”的响应体，它与请求体使用相同的方式加解密（这里为了方便复制使用 base16，实际上是二进制字节流不难发现，响应体也是一段 json 数据同样可以将它封装成一个函数，以二进制输入，输出 json 格式的真实数据import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import json EAPI_KEY = b"e82ckenh8dichen8" EAPI_CRYPTOR = AES.new(EAPI_KEY, AES.MODE_ECB) def data_decrypt(enc_data): data = unpad(EAPI_CRYPTOR.decrypt(enc_data), 16).decode() return json.loads(data)编写解密脚本有了上面这些成功处理数据的实例，如何对接口数据解密已经不是难事，但从抓包记录中来回复制数据、手动运行解密程序只能徒增人力，浪费调试与研究的时间恰好，Reqable 一大亮点就是支持脚本功能，使用 Python 作为脚本语言，事件化、异步化处理请求与响应，脚本具有读取和修改 HTTP 数据包的权限，这就给自动化解密提供了基础首先点击工具栏上的脚本按钮，或者按 Shift+Ctrl+P 开启脚本功能，需要确保系统中有 Python3 运行环境当看到这个界面时，证明脚本运行环境已经正确如果看到提示脚本环境异常，就去 python.org 自己下一个吧这时距离脚本环境配置完毕只有一步之遥，还需要安装一个脚本的 API 库reqable-scripting，使用 pip 安装即可，它的源码在 https://github.com/reqable/python-scripting-apipip install reqable-scripting接下来右键点击工具栏上的脚本按钮，再点击新建脚本（或者快捷键 Shift+Alt+P 也行这时会弹出一个脚本编辑器的窗口，它可以直接编写 Python 脚本，无需第三方编辑器（但没有补全和静态分析这个脚本非常容易上手，只有两个关键的入口函数onRequest(context, request)和onResponse(context, response)，分别对应请求和响应的事件函数参数context对于两个函数意义相同，可以读取当前捕获数据包的传输层——会话层信息，还可以从onRequest()向onResponse()传递任意数据参数request对于请求数据包，可以读写 url、方法、头部等，也可以读写请求体；与之对应的response就是响应数据包了，也可以读写 响应状态字、头部、响应体等所以把这两个函数、三个参数玩明白后，你就掌握了 Reqable 脚本的全部编写技巧，可以实现高级的数据包加工了，具体的接口使用方法可以查阅官方文档 https://reqable.com/docs/capture/addons至于上面的名称和URL，就容易理解多了，Reqable 支持管理多个脚本，可以为它命名和独立开关，URL参数使用通配符匹配它针对的 URL 地址，减少脚本的编写难度和 bug由于网易云音乐的 REST API 都是以https://interface.music.163.com/eapi/起始的，所以我们可以为其写一个通配符*加在末尾，匹配符合条件的 URL这里使用request.method和request.path分别打印请求方法和请求路径，测试脚本是否正常执行，可以看到右侧的调试窗口已经能输出信息了接下来就是使用它捕获请求并实时解析，访问request.body.payload提取请求体，并使用 Python 的内置库urllib.parse对 url query 表单进行解析，提取出params字段的值，送入之前定义好的函数进行解密，再打印出返回的两个参数到控制台可以看出已经成功解析不少接口的请求数据了至于响应体的解密，如法炮制，在onResponse()函数下添加相应的处理流程在脚本编辑器的右侧调试窗口，可以看到响应体已经成功被解密了，看内容大致应该是和 VIP 提示相关的输出的内容有优化空间，可以适当加一些标识符，用来区分每条消息的类型，也可以关闭时间戳，让输出看起来更简洁另外，根据多个抓包记录的共同特征，请求体中的header字段为头部信息，是一个序列化后的 json 字符串，内部包含设备特征标识、登录用户的 Token 等等敏感信息，用作测试为了脱敏和减少干扰信息，可以把这部分替换为***请求体消息相对较少，为了方便调试可以让它以多行输出，使用 python 的内置库pprint最终的脚本代码如下，可以作为实验的参考：from reqable import * import urllib.parse import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import json from pprint import pprint EAPI_KEY = b"e82ckenh8dichen8" EAPI_CRYPTOR = AES.new(EAPI_KEY, AES.MODE_ECB) def eapi_decrypt(enc_data): message = unpad(EAPI_CRYPTOR.decrypt(base64.b16decode(enc_data)), 16).decode() path, json_val, hash = message.split("-36cd479b6b5-") return path, json.loads(json_val) def data_decrypt(enc_data): data = unpad(EAPI_CRYPTOR.decrypt(enc_data), 16).decode() return json.loads(data) def onRequest(context, request): enc_params = urllib.parse.parse_qs(request.body.payload)['params'][0] path, params = eapi_decrypt(enc_params) params['header'] = '***' print('========== ReqStart ==========') print(context.url) print(path) pprint(params, width=1, indent=2) print('========== ReqEnd ==========') return request def onResponse(context, response): enc_data = response.body.payload data = data_decrypt(enc_data) print('========== RspStart ==========') print(context.url) print(data) print('========== RspEnd ==========') return response 可能这时候就会有冰雪⑨小聪明会问“为啥一定要在调试窗口中查看参数呢？”非常好，其实姐姐我也想在 Reqable 内置的协议分析窗格中查看啊，用脚本把加密的数据解密回去，虽然可以在 Reqable 中看到，但服务端和客户端只接收密文，那么一定会造成解密失败无法执行希望 Reqable 开发团队可以加入脚本只解析并修改用于展示的数据功能开始抓包分析对于抓包，是一个技术与经验叠加的工作，只看各种零散的数据包是远远不够的，需要观察进行了什么操作后会产生数据包，接收到响应后视图内容发生了什么变化，接口返回和视图所展示的信息有什么关联这里我抓取一个短信验证码登录的接口，当按下“发送短信验证码”后，观察调试终端中输出了相关的数据和接口 URL这里 URL 是https://interface.music.163.com/eapi/sms/captcha/sent这是一个标准的 REST API，即通过路径对接口功能分类，是一种约定成俗的 C/S 网络接口编写范式，那么根据字面含义，这个接口的功能就是发送短信验证码请求体数据如下，这里忽略掉公共环境参数e_r和header，所以关注的参数就是cellphone和ctcode，它们两个字段的值刚好对应前端输入的“国际冠字号”和“手机号码”，与字段名的含义一致{ "cellphone": "153********", "ctcode": "86", "e_r": true, "header": "***" } 至于返回格式，就是一个标准的 json RPC，它具有返回值和返回信息，因为前端没有看到任何报错，即调用成功{ "code": 200, "data": true }收到验证码后填写到文本框中，点击登录后立即产生了一个请求，URL 是https://interface.music.163.com/eapi/w/login/cellphone即手机号+短信验证码登录接口请求数据中含有与上一个接口一致的countrycode、phone参数，短信验证码captcha这里是5256，也有checkToken这种与之前接口没有明显联系的数据，但看到它的数据是一段 base16，应该是某种签名校验的 hash登录成功后这个接口的响应数据中就会存在类似token这样的用户认证数据，作为其他接口的身份验证依据，是客户端个性推荐、收藏、社交、会员等交互功能的基本材料当点击播放一首音乐时，分别产生了如下几个请求，播放地址、歌词、相关推荐、评论区但产生的请求响应中没有歌曲信息的数据，这是因为播放的入口是收藏歌单，这些数据在加载歌单时已经进行了一个 preload，如果从搜索或者外部链接进入，那么就会单独拉取歌曲的基本信息这个接口https://interface.music.163.com/eapi/song/enhance/player/url/v1的作用就是获取播放地址，它的返回数据中包含了一个 mp3 文件的 http 直链获取歌词的接口也是单独存在，URL 为https://interface.music.163.com/eapi/song/lyric，这个接口会根据请求参数中的歌曲资源 id 查找对应的歌词，返回三种类型的歌词，分别是原始歌词、翻译歌词、罗马音歌词，都是标准的 lrc 格式对照接口数据与试图中的内容，这个 URLhttps://interface.music.163.com/eapi/discovery/simiPlaylist应该是相似歌单那么最后一个接口就是评论区了，返回内容很长，里面包含评论区的评论正文所以只要掌握了方法，懂得按照路径名和请求参数去归纳分析，有操作后观察产生请求的意识，就可以知道每一个 REST API 所调用的功能了对比返回数据与视图中内容的联系，并且猜测字段名，就能知道其中的数据含义结语总体使用下来，Reqable 是一款定位明确、功能强大的工具，除了主要的抓包和调试功能外，还有很多调试接口常用小工具。作为一位野生开发者，之前都是使用 Postman+Fiddler 进行调试，可谓两个重量级工具，现在有了 Reqable 就可以将它们功能合一使用。目前这款软件在 PC 三大平台还在频繁更新版本中，移动端计划发布，期待能拿到移动端的内测权限，推荐指数⭐️⭐️⭐️⭐️⭐️
21年底入手了一台 Matebook E，作为在大学时的主力机使用，上能做开发、剪视频，下能看番、看论文、甚至玩 GAL，轻便得像本书，可随手拿走带到教室，除发烧外似乎也没什么很大的槽点。由于工（zhe）作（teng）需要，上面装了各种虚拟机和开发环境，存储空间就显得有些捉急，时长看到硬盘剩余空间 100GB 左右，鉴于这款机子全网的拆解资料一只手都能数清，我决定挑战一波，看看有没有升级的可能性。拆解由于机身上看不到任何一颗螺丝，所以它应该是类似大多数平板电脑的贴合结构，只能从正面屏幕入手了手头没有加热垫，只能试图用吹风机吹开，折腾了半个小时未果，我猜想应该是散热效应导致的，可能日常背后发烧的温度都要比瞎吹一阵的温度高最后无奈，苦想中突然意识到这会是冬天，房间角落里有个小太阳，这不可以利用一下哈哈把整个屏幕面加热至 80~90℃，配合塑料拆机片划开四周期间继续配合吸盘和吹风机操作拆的时候需要注意屏幕正下方的排线，以免划断把4个边缘的胶都划开后提起屏幕，可以发现有一条排线与后面的机身相连屏幕排线的接口为 BTB 分为左右两个端子，并且上面有盖板，要分离首先需要把盖板左右的螺丝卸下屏幕下方的中框为铝镁合金材质，上面有 30 多颗十字螺丝，一定要准备好一把电起子和磁铁，不然会很麻烦电池粘在中框的背面，为 3 电芯 3S 结构，对于电池没有任何可升级空间右上方镂空区域对应散热风扇的位置，使用一年积灰还是比较多的接着把目光拉回后壳的主体部分左上部位是一个小板，上面主要集成着 Wifi 模块和后摄，有两条排线与主板相连，它的旁边就是散热风扇，顺便清一下灰中间的部位覆盖着导热板，它的下方压着 CPU、内存和南桥，并延申至散热风扇下，这里不想破坏原厂的硅脂就不去拆它了顶部的区域有前摄、手写笔的充电线圈和麦克风，右上角是一些外围电路，盲猜是 PMIC EC 时钟 等等，主板一直延申到机身右下方的 Type-C 接口处一览它的全貌，可见设计得十分紧凑，不同于普通笔记本的设计语言，布局更像是手机和平板左右两侧下方的巨大黑块就是左右声道的中低音扬声器 BOX，上方的小黑块是高音扬声器 BOX在主板边缘包裹着铜箔的，就是 2280 全长的 SSD，也是这款机子唯一可以升级的地方撕开铜箔，原装的 SSD 为致态的 OEM，无印良品撕开可以发现主控是慧荣的 SM2262ENG，有缓存，单面两颗粒扩容在拆机的同时，我在🐶东 600 多下单了一款致态的 TiPlus7100，1T 的 SSD，算是国货之光了使用联芸 MAP1602A 主控，只装了单面个两个颗粒，没有缓存接下来就是迁移数据了，在不重装系统的情况下需要让新硬盘换后可以立即投入使用由于手头没有 M2 的硬盘盒，只能借助一下台式机的主板了，正好它有两个 M2 的插槽对了，顺便提一嘴，迁移数据之前一定要把 bitlocker 加密关掉，不然压根没法迁移接着就是启动 PE 用 Diskgenius 进行分区克隆了（这里因为拔走了台式机原本的系统盘，当然不进 PE 也可以进行克隆然后...就是漫长的等待了， 把原装的 SSD 作为克隆源，把新的 SSD 作为克隆对象，大概需要等待 1 个多小时可以顺便把“Onekey”分区给扬了，克隆后需要使用chkdsk对系统盘进行修复，例如我这里系统盘是 K 盘chkdsk /f /x K:再使用引导修复工具对 BCD 进行重建当修复完之后就可以把新的 SSD 装回机器了，务必不要拧上全部螺丝，只拧上电池触点周围的几颗螺丝即可，防止返工这时如果开机，会直接蓝屏，因为需要清空一下内建 TPM，重启按 F2 或者音量下键，进入 BIOS 设置，把安全启动关闭保存重启后就能正常进入系统了，检查软硬件没任何问题后，需要进 BIOS 重新开启安全启动后，才可以打开 bitlocker 加密磁盘管理中 C 盘的大小已经变成了 935GB（只分了一个区）可以存更多的学习资料了最后顺便测一下速，顺序读取已经可以突破 6GB/s 了，其实只看测速也没什么参考价值真正使用下来了几个月，发现存储了大量数据的情况下，速度没有很严重的衰减最后的步骤就是“倒放”了，注意刚才拆下的螺丝，可别“优化”掉几颗（忽略满屏幕的爪子印用酒精加上卫生纸把屏幕擦干净，并且把四个边缘的胶压结实至此扩容工程全部完成，希望它还可以再战五年视频版不会吧不会吧，竟然还发了视频？[bili av="524989935"]
被迫当了一天半“自宅服务器运维员”，差点数据火葬场，在这里给大家避个坑吧我实体服务器放家里，系统是原生 Debian 11存储环境是 4*2T HDD 组的软件 RAID5，容量大概 5T 左右，文件系统是 btrfs，这里/dev/md0p1就是 RAID 分区，挂载在/storage上。系统盘也就是/目录和/boot目录存放在 128G SSD 上RAID 区上主要存储各种文件，比如电影照片游戏等、以及自己的各种工程项目，当然服务器上同时还跑着 Docker 和 MySQL，它们的存储路径也放在/storage下，也就是存在 RAID 区里问题的导火索昨天下午收拾东西的时候撞了下网线，没过一会就收到了 uptimebot 的告警邮件，原来是服务器断网了（这里监控的是使用 frp 穿透的 https 服务），赶紧用手机连上 ssh 检查，但内网没问题外网却连不通，寻思着服务器也连续运行了 90 多天该重启了，无脑sudo reboot了一下，这下好，直接闯祸了（事后才知道是重启后 grub 默认加载了6.0.0-0.deb11.2-rt-amd64kernel发现问题我 qq 机器人的服务端和数据库是分离的，服务端基于 Aridane+Mirai 搭建，数据库用 MySQL，由于这玩意业务逻辑比较复杂，自己优化做的也很差，所以放在云服务器上跑，因为自己撺出来这实体机性能也明白，但数据库服务还是使用这台实体机，之间使用 VPN 组网进行连接（因为用家宽没有公网 IP在重启服务器后，突然发现 qq 消息不停刷，原来是机器人给我连续抛出几十个异常上报，内容都是关于 MySQL 连接失败的这下我就纳闷了，于是又火速连上 ssh 检查 VPN 的状态，设备之间能互相 ping 通（云服务器是10.50.9.1家里这台服务器是10.50.9.10），那么接着按照惯例就该检查 MySQL 状态了。我已经设置为开机自启，但sudo systemctl status mysql查看，发现服务没有启起来，如果稍微懂那么一丢丢 Linux，那么你第一反应就是赶紧 start 它。但事实上，start 后它就寄了！一般 start MySQL 服务也就几秒完毕，但这次我等了半分钟也没启动完毕，紧接着我在 ssh 终端上看到一条 broadcast 消息，作为浅尝过 Linux 的小白，肯定知道终端出现 broadcast 消息一定大事不妙。很好，CPU 直接 stuck 了，按 Ctrl-C 虽然能回到 shell 环境，但 htop 查看 CPU 其中一个核心占用 100% 却看不到任何占用它的进程，当然 SSH HTTP SMB FTP VPN VNC FRP 这些服务无一例外没活着的，端口正常开放，连上后没响应尝试解决问题我的天，正常关机也不能实现了，因为这个时候 systemd 也卡住了，走不了流程，只好强行拔电，插电开机继续找问题我当时一度认为 MySQL 的 binlog 过大把进程卡死了，因为这个服务器上的数据库不仅给 qq 机器人用，还给爬虫、网盘服务用，数据的吞吐量很大。就尝试删除 MySQL 的 binlog，没想到一删直接踩雷，ssh 连接给我断了、硬盘刚才还轰隆轰隆，顿时变得安静起来了、硬盘灯全部熄灭、当然 ping 不通了。它，这次没有报 stuck，而是直接宕机了！太怪了，为啥删几个文件就轻松死机，有点不对劲（内心 OS：快进到数据火葬场）赶紧让老子先备份一下数据库，整个 MySQL 存储文件夹端走得了，已经用systemctl disable mysql关闭 MySQL 开机自启了，这样至少不会开机既死机，可以苟一会尝试把 Mysql 文件转移到 SSD 去，但涛声依旧，没拷过去几个文件，咔的一下系统又宕了rsync -ravh /storage/DataBase/mysql /Mysql越来越离谱了，难道是只要访问/storage/DataBase/mysql这个路径就会触发宕机？于是我排除一下，扫描/storage/File下的文件（这个路径下主要存项目、学习资料、游戏，大概有1T左右），因为du会遍历整个目录树，所以正好能用来检查读取文件的稳定性du -hd1没想到啊，仍涛声依旧，不碰 MySQL 存储目录还是会宕机，证明不是 MySQL 的问题。如果，读写某些特定路径会死机，那么照常理讲，最大的可能应该是文件系统损坏。如果真是 btrfs 损坏那基本上没人能恢复得了，再加之我是在 RAID 之上建立的分区，如果损坏那么就像火化后再浇一盆冷水（整个人慌了起来，万一数据火葬场就哭得没眼泪了一波三折首先我怀疑是 RAID 的问题，就手动触发一下 RAID 的一致性检查机制吧，因为该服务器使用 RAID5，有一定的容错和自我修复能力echo check > /sys/block/md0/md/sync_action这一等就是一下午.......与此同时，在检查进行的时候顺便测试测试，用 SMB 传出去 40GB 的文件，全程跑到 100M/s 既没掉速也没有宕机，这更让我相信是文件系统损坏导致的涛声依旧.......整个文件系统如同雷池般，读写某些文件可以正常进行，某些文件碰都不敢碰，无论读写都会触发宕机，有时候是 cpu stuck，有时候 ssh 直接断开 ping 不通，而这些文件可没啥明显的异常啊喂要是问图形界面怎么样，哈哈哈哈哈只能说太聪明了，宕机期间 VNC 是连进不去的，显示器上只有鼠标是可以运动的，剩下一切都定格了想用 SMB 试图提前抢救出重要文件，可传输还没开始就卡住了，进度条那里一直提示“正在检查文件数量”，果然故障扩散了，我还是太“聪明”了（⑨最后急到跳脚，甚至还跑了一遍 btrfs 的检测# 检查bttrfs文件系统错误 sudo btrfs check --repair /dev/md0p1 # 检查bttrfs文件系统校验和 sudo btrfs scrub start /dev/md0p1结果是没有错误！ 我突然有种科学家遇到“智子”的感觉，还得是大史那句话“邪乎到家必有鬼”这时已是深夜，我准备再搏最后一把，把/storage卸载掉，然后在系统里随便操作操作，测试稳定性。很好，sudo apt update更新一下软件源索引轻松宕机，但愿这只是从侧面反映出存储系统没损坏，而不是故障继续扩散了，两害取其轻，怀着疑惑睡觉了急急国王变换思路第二天早上起来，二话不说就把服务器扒了，首先怀疑是 CPU 供电不稳导致的宕机，毕竟软件 RAID 寻址需要普通盘数倍的 CPU 算力，用 BIOS 自带的监控测各路电压、也用万用表测电源输出电压，甚至还捞起来一个老电脑的电源代换它当然，结果心里已经有数了，都是无用功，轻松死机在这一次又一次的反复调试中，其中一次开机进入 recovery mode 检查时，发现读写文件触发 cpu stuck 后就会有这样的报错信息刷屏，可以看到底下有一个完整的线程 Callstack Trace，问题百分百指向 btrfs我就寻思 btrfs 既然是是内核态驱动，那么会不会是内核不兼容导致的，可以尝试降级一个版本，用uname -r查看，发现内核竟居然是6.0.0-0.deb11.2-rt-amd64，赶紧重启后在 grub 里选Adavanced options for Debian GNU/Linux选项，再选择5.19.0-0.deb11.2-amd64引导启动问题解决在换内核启动后，宕机问题成功解决，赶忙用 rsync 把 MySQL 目录也全部拷出，挂上一块移动硬盘到服务器上备份重要数据有惊无险，后面测试一波发现各项服务和数据其实并无大碍当然一开始 MySQL 莫名其妙停止运行的问题也迎刃而解救活了！整个人心都放下了我寻思造成这种故障的祸根一定是 apt 吧（笑最后当然是迁移了 MySQL 目录和 Docker 目录，放到 SSD 中（ext4 格式接着配置默认启动项，彻底铲除由于升级造成的故障sudo vi /etc/default/grub sudo update-grub设置GRUB_DEFAULT为Advanced options for Debian GNU/Linux>Debian GNU/Linux, with Linux 5.19.0-0.deb11.2-amd64既默认启动5.19.0-0.deb11.2-amd64内核事故分析在服务器正常运行的 90 天内，装软件时 apt 或许已经把 6.0.0 内核当成依赖安装了，但不重启就不会加载新版内核。而搬东西把网线撞掉再插上，有可能造成 FRP 服务断连或者 ARP 表缓存异常，导致外网无法连接、以及收到掉线告警邮件。重启后 grub 将系统里存在的最高版本内核，也就是6.0.0-0.deb11.2-rt-amd64作为默认启动项引导，启动后由于内核的 bug 导致 btrfs 驱动程序工作不稳定，进而造成 MySQL 服务无法启动，读文件的时候同样会触发这个 bug，导致宕机或者 cpu stuck经验教训生产环境服务器不要随便升级内核MySQL、Docker 等 IO 密集型服务尽量别用 RAID 存储，尤其是软 RAID，最好使用 SSD 缓存盘，这些服务中 RAID 只做定期热备份
HAPPY1024 在宿舍的业余十八线程序员祝大家1024节快乐，代码少出bug、服务器别宕机，保护好头发和眼睛，早点睡觉，参上٩(๛ ˘ ³˘)۶♥
研究学习通答题字体加密的随笔（二）
本文操作只做技术讨论，请勿用于非法用途！前情提要书接上回，学习通答题页在未答时，考虑到防止作弊存在字体加密，复制或者直接用脚本搜索会表现为乱码，为了刷题，于是乎我给搜题后端整合了一个复杂的加解密逻辑又到了完成网课的时间，不过这次是《军事理论》这门课这次需要刷更多的题，以前写的程序 bug 百出，望着自己的屎山代码发呆，根本无从下手修改，寻思一定是架构设出了问题重拾本源，厘清思路首先学习通的答题页面是 SSR（Server Side Render），原始密文和密钥（加密字体）都通过 html 回传至浏览器而经过加密的文本是随机替换掉一些字体的编码看上去是混有部分乱码的文本，把乱码和正确字形对应起来放在传入的 TTF 加密字体中，在前端渲染后视觉上是正确的，但复制后人类和机器都不太容易阅读，加密原理就这么简单使用 F12 工具分析 html 结构得知，每道题目div中均含有Timu类，题干和选项略楼不同，如下：题干是div下存在font-cxsecret类，文本就是标签内容 【单选题】从1842喣喟1937喣喡喢被喠签喞了()个不喤等条约。 选项在div下存在font-cxsecret类，但文本存在于该标签下的a子标签下 A 俄坌坍 规定密文区段使用font-cxsecret class，在 css 中为该 class 指定了 font-family，字体也是 SSR 的以 base64 编码放在其中，之所以密文可以和正常文本混合，是因为 css 字体的 fallback 机制如下： 所以，为什么要整那么复杂捏？会不会存在一种可能性，就是不在搜题过程中实现加解密、处理各种复杂情况，而在搜题前直接把这些密文替换掉？还是使用和上次相同的技术栈，前端油猴脚本 + 后端 Flask有了以上的归纳总结，再配合自己所学的那么一点点 JQuery（学习通前端使用了JQ）和 JS 编程经验，就可以利用油猴脚本提前抓取字体数据、以及这些经过加密的文本，并进行遍历请求，让后端解密，从而以明文取代掉密文先定义一个抓取加密字体的base64的函数，使用正则表达式暴力匹配带有type="text/css"属性的style标签中的文本// 拾取加密字体 function getSecFont() { return $("style[type='text/css']").text().match(/'(data:application\/font-ttf;.*?)'/)[1] }由上文可得，抓取所有题干及选项文本，就是几个特定 class 的他特定标签内容所以我们写一个 JQ 选择器，同时匹配每道题的题干以及全部选项，化繁为简$('.TiMu').find('div.font-cxsecret,.font-cxsecret a')下一步就是向后端回传数据了通过查阅文档，了解到油猴脚本中创建跨域请求需要用到一个 API 函数GM_xmlhttpRequest而不能使用传统的 xhr 以及JQ.ajax这里使用JQ的.each()方法遍历选择器的匹配列表，使用快捷方法$(this)得到遍历的当前项，再把刚才所抓取的字体数据一并序列化成 json POST 给后端当后端解密返回响应时，异步执行一个回调函数，替换掉当前标签的文本，并敲除font-cxsecret类// 解密全部字体 function decryptAll() { let secFont = getSecFont(), encryptTexts = $('.TiMu').find('div.font-cxsecret,.font-cxsecret a'); // 遍历加密字体项 encryptTexts.each(function() { let dstText = $(this); GM_xmlhttpRequest({ method: 'POST', url: `${setting.tiku}/decrypt`, headers: { 'Content-type': 'application/json', }, data: JSON.stringify({secFont: secFont, dstText: dstText.text().trim()}), responseType: 'json', onload: function (xhr) { if (xhr.status == 200) { dstText.text(xhr.response.srcText); dstText.removeClass('font-cxsecret'); } } }); }); }把函数decryptAll()放在开始搜题的处理之前执行，那么搜题时就全是明文了（搜题部分不在本篇探讨范围内，略过。。。这样一来，后端代码更简洁了，彻底把两部分做了解耦，分成两个独立的 rest API 让前端调用注册/decrypt路径为解密接口，核心的逻辑调用了上次写好的两个函数，传入字体创建 hash map、执行字符串解密操作@app.route('/decrypt', methods=('POST',)) def decryptView(): args = json.loads(request.data) key_font_b64 = args['secFont'] dst_text = args['dstText'] font_hashmap = cxsecret_font.font2map(key_font_b64) # 创建加密字体hash map src_text = cxsecret_font.decrypt(font_hashmap, dst_text) # 解密目标文本 print(f'{Fore.GREEN}解密成功{Fore.RESET}: {Fore.YELLOW}{dst_text}{Fore.RESET} -> {Fore.GREEN}{src_text}{Fore.RESET}') return { 'srcText': src_text }字体的hashmap算法也要优化由于我上回写的字形 hash 算法极为拙劣，经常导致匹配失败，而且还大材小用了 sqlite，整体效果不尽人意最近看到吾爱破解论坛上大佬@featmellwo的帖子，他用源字体做好了一个 hashmap，算法比我的更优化，经测试可以正确匹配参考他的代码，我把单个字形的编码部分单独提出为一个函数def hash_glyph(glyph: Glyph) -> str: 'ttf字形曲线转hash算法实现' pos_bin = '' last = 0 for i in range(glyph.numberOfContours): for j in range(last, glyph.endPtsOfContours[i] + 1): pos_bin += f'{glyph.coordinates[j][0]}{glyph.coordinates[j][4]}{glyph.flags[j] & 0x01}' last = glyph.endPtsOfContours[i] + 1 return hashlib.md5(pos_bin.encode()).hexdigest()然后再遍历 TTF 中每个字形的数据，用来生成 hashmapdef font2map(file: Union[IO, Path, str]) -> dict[str, str]: '以加密字体计算hashMap' font_hashmap = {} if isinstance(file, str): file = BytesIO(base64.b64decode(file[47:])) with TTFont(file) as fontFile: for code, glyph in dict(fontFile.getGlyphSet()).items(): font_hashmap[code] = hash_glyph(glyph._glyph) return font_hashmap接下来就是我偷懒了，这里直接使用大佬做好的字体 hashmap 数据库，不打算使用 sqlite而源字体 hashmap 数据库的 DAO 已经改成了基于 py 内置的 dict 类型，这样全部加载进 RAM 查询效率不比 sqlite 低json DB（确信class FontHashDAO: char_map: dict[str, str] # unicode -> hsah hash_map: dict[str, str] # hash -> unicode def __init__(self, file='font_map.json'): with open(file, 'r') as fp: map: dict = json.load(fp) self.char_map = map self.hash_map = dict(zip(map.values(), map.keys())) def find_char(self, font_hash: str) -> str: return self.hash_map.get(font_hash) def find_hash(self, char: str) -> str: return self.char_map.get(char)在运行后观察后端的日志可以发现，在搜题之前批量执行了一系列的解密处理奇奇怪怪的汉字在修改新的解密逻辑后，搜题往往会出现许多字体看着一致，但无法匹配答案的离奇 bug，比如下图中的综合国力这个油猴脚本的答题逻辑，就是匹配后端返回的字符串结果，与现有选项的文本进行比对，从而决定选择题的 ABCD 选项（这里是个form表单）如果无法匹配，那么一定是字符串的 Byte 不一致，而不是“看起来不一样”抱着好奇的态度 copy 进 py 比对一下，果然不一样>>> '综合国⼒' == '综合国力' False逐个汉字符比对后，最终确定是力字特殊，它们虽然字形相同（可能不同字体也有差别），但是 unicode 的内码不相同>>> ord('⼒'), ord('力') (12050, 21147)经过查阅资料，我发现这种和普通汉字字形相同，但是 unicode 内码不同的字叫做康熙部首，它位于 unicode 的 2F00—2FDF 区段，当然它们和普通的汉字一一对应，出现此误码的原因可能是一些字体厂家没有把这两种编码的字形做区分解密后再加一步，使用 py 自带的str.translate()方法，把康熙部首替换为对应的汉字# 康熙部首替换表 KX_RADICALS_TAB = str.maketrans( # 康熙部首 "⼀⼁⼂⼃⼄⼅⼆⼇⼈⼉⼊⼋⼌⼍⼎⼏⼐⼑⼒⼓⼔⼕⼖⼗⼘⼙⼚⼛⼜⼝⼞⼟⼠⼡⼢⼣⼤⼥⼦⼧⼨⼩⼪⼫⼬⼭⼮⼯⼰⼱⼲⼳⼴⼵⼶⼷⼸⼹⼺⼻⼼⼽⼾⼿⽀⽁⽂⽃⽄⽅⽆⽇⽈⽉⽊⽋⽌⽍⽎⽏⽐⽑⽒⽓⽔⽕⽖⽗⽘⽙⽚⽛⽜⽝⽞⽟⽠⽡⽢⽣⽤⽥⽦⽧⽨⽩⽪⽫⽬⽭⽮⽯⽰⽱⽲⽳⽴⽵⽶⽷⽸⽹⽺⽻⽼⽽⽾⽿⾀⾁⾂⾃⾄⾅⾆⾇⾈⾉⾊⾋⾌⾍⾎⾏⾐⾑⾒⾓⾔⾕⾖⾗⾘⾙⾚⾛⾜⾝⾞⾟⾠⾡⾢⾣⾤⾥⾦⾧⾨⾩⾪⾫⾬⾭⾮⾯⾰⾱⾲⾳⾴⾵⾶⾷⾸⾹⾺⾻⾼髙⾽⾾⾿⿀⿁⿂⿃⿄⿅⿆⿇⿈⿉⿊⿋⿌⿍⿎⿏⿐⿑⿒⿓⿔⿕⺠⻬⻩⻢⻜⻅⺟⻓", # 对应汉字 "一丨丶丿乙亅二亠人儿入八冂冖冫几凵刀力勹匕匚匸十卜卩厂厶又口囗土士夂夊夕大女子宀寸小尢尸屮山巛工己巾干幺广廴廾弋弓彐彡彳心戈戶手支攴文斗斤方无日曰月木欠止歹殳毋比毛氏气水火爪父爻爿片牙牛犬玄玉瓜瓦甘生用田疋疒癶白皮皿目矛矢石示禸禾穴立竹米糸缶网羊羽老而耒耳聿肉臣自至臼舌舛舟艮色艸虍虫血行衣襾見角言谷豆豕豸貝赤走足身車辛辰辵邑酉采里金長門阜隶隹雨青非面革韋韭音頁風飛食首香馬骨高高髟鬥鬯鬲鬼魚鳥鹵鹿麥麻黃黍黑黹黽鼎鼓鼠鼻齊齒龍龜龠民齐黄马飞见母长" )这之后，一切加密和编码问题迎刃而解最后来一张全部题刷完的图后记这次解决了之前源字体字形匹配失败的问题，可能就是因为字形hash的算法问题导致的，结果钻了牛角尖手动打表（笑死也算是在实践中多学习一些字体编码的知识了另外，基于“奥卡姆剃刀”原理，后端的业务逻辑不需要在单一的服务或接口上做各种复杂的处理，把服务解耦后效率更高、出错率更低关联项目 https://github.com/SocialSisterYi/xuexiaoyi-to-xuexitong-tampermonkey-proxy在此感谢吾爱破解论坛大佬@featmellwo的帖子本文操作只做技术讨论，请勿用于非法用途！
比病毒更可怕的，是人心。返校至今坐牢两周出头，一天一核酸，门禁如防川。在校不必封楼“自我隔离”，但网课依旧，俨然失去了教学功能，如同“大型招待所”。盼望已久的救星来了，十一假期虽说仅有三天，实则停课三天，放假是不可能的。我终于蚌埠住了，但你觉得这样大门能轻易出去吗，还得是“走老路”溜号子。。。。。。。出校之后如脱笼之鹄，但恍惚间又浑身不自在，无负罪之感，但罪犯就是我，监狱很大，有到天边那么大，身上穿着囚服，手脚拴着无形的镣铐。街上很冷清，但又不完全冷清。所有的店似乎都关着门，哦不对，只有商店和药店开着门；街灯和车灯依旧耀眼，路上的行人和车漫无目的地走，我也骑着车漫无目的地走。街上听不到庆祝声、喧闹声、玩乐声，甚至曾经街边店里的吆喝也销声匿迹，取而代之的则是每个小区门口每个村口那此起彼伏的、喧天的“赶紧下楼做核酸”之声。街上人们的精神状态不像是已经放假，不像即将度假亦或要去和家人团聚，而感觉一个个都在赶路，有的手里提着大小塑料袋来回跑动。万达广场关门、城市各处的公园没人去、市中心一片死寂，熙攘夜市今何在，唯有大铁门吧，甚至一家蜜雪冰城都没给我留。我低头看看手表，是10月1号国庆节没错，抬头看看满街的国旗，确定自己没有穿越，当然两周多的牢也不至于坐得心理障碍，只能说一切都是真实的。我有一点点渴，但不想停下来进商店买水，因为我好奇外面的世界，满脑子的问号。这时脚下的自行车已然加快————还是爬回大牢好吧。因为，一个在监狱里待习惯的犯人，一时间适应不了外面的世界，认为一切都是怪的。4年了，我们最怕的还仅仅是蛋白质包裹着RNA的病毒吗，我看未必。。。。。。