From 4a0d7b3d82617099e07bba43496efa33662b8a48 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Thu, 10 Aug 2023 04:15:15 +0000 Subject: [PATCH] Site updated: 2023-08-10 04:15:15 --- 2020/09/github-picgo-jsdelivr/index.html | 1 + 2020/09/hello-hexo/index.html | 66 + 2020/09/hexo-next/index.html | 69 + .../index.html | 29 + .../index.html | 76 + .../index.html | 1 + .../openwrt-expand-overlay-storage/index.html | 4 + .../index.html | 1 + 2020/10/charles/index.html | 11 + .../10/chrome-cookie-do-not-delete/index.html | 1 + 2020/10/how-to-use-rime/index.html | 306 ++ 2020/10/install-pyenv-and-pipenv/index.html | 12 + 2020/10/ruby-in-vscode/index.html | 31 + .../10/ruby-include-implementation/index.html | 287 ++ 2020/10/vim-fast-esc/index.html | 10 + 2020/10/webpack-resovle-@-path/index.html | 27 + 2020/11/hexo-next-valine/index.html | 15 + 2020/11/http-cache/index.html | 1 + 2020/11/receiver-and-ancestors/index.html | 82 + 2020/11/tcp-ip/index.html | 1 + 2020/12/cloudflare-workers/index.html | 153 + 2021/01/ruby-devise/index.html | 324 ++ 2021/02/openwrt-in-pve/index.html | 1 + 2023/01/cloudflare-tunnels/index.html | 32 + 2023/01/run-efb-in-docker/index.html | 12 + 2023/04/build-own-rss-platform/index.html | 9 + 2023/07/zero-rating/index.html | 111 + 404/index.html | 1 + CNAME | 1 + archives/2020/09/index.html | 1 + archives/2020/10/index.html | 1 + archives/2020/11/index.html | 1 + archives/2020/12/index.html | 1 + archives/2020/index.html | 1 + archives/2020/page/2/index.html | 1 + archives/2020/page/3/index.html | 1 + archives/2021/01/index.html | 1 + archives/2021/02/index.html | 1 + archives/2021/index.html | 1 + archives/2023/01/index.html | 1 + archives/2023/04/index.html | 1 + archives/2023/07/index.html | 1 + archives/2023/index.html | 1 + archives/index.html | 1 + archives/page/2/index.html | 1 + archives/page/3/index.html | 1 + baidusitemap.xml | 111 + categories/backend/index.html | 1 + categories/backend/python/index.html | 1 + categories/backend/ruby/index.html | 1 + categories/frotend/index.html | 1 + categories/index.html | 1 + categories/misc/index.html | 1 + categories/misc/page/2/index.html | 1 + categories/network/index.html | 1 + categories/router/index.html | 1 + categories/security/index.html | 1 + categories/test/index.html | 1 + css/main.css | 1 + css/noscript.css | 1 + images/apple-touch-icon-next.png | Bin 0 -> 1354 bytes images/avatar.gif | Bin 0 -> 1785 bytes images/avatar.jpg | Bin 0 -> 14348 bytes images/favicon-16x16-next.png | Bin 0 -> 285 bytes images/favicon-32x32-next.png | Bin 0 -> 488 bytes images/logo.svg | 1 + index.html | 1 + js/comments.js | 1 + js/config.js | 1 + js/motion.js | 1 + js/next-boot.js | 1 + js/third-party/analytics/matomo.js | 1 + js/third-party/pace.js | 1 + js/third-party/search/local-search.js | 3 + js/third-party/tags/mermaid.js | 1 + js/utils.js | 1 + page/2/index.html | 2 + page/3/index.html | 4 + placeholder | 0 robots.txt | 17 + search.xml | 4279 +++++++++++++++++ sitemap.xml | 435 ++ tags/chrome/index.html | 1 + tags/cloudflare/index.html | 1 + tags/csrf/index.html | 1 + tags/development-environment/index.html | 1 + tags/efb/index.html | 1 + tags/ehforwarderbot/index.html | 1 + tags/freebie/index.html | 1 + tags/github/index.html | 1 + tags/hackintosh/index.html | 1 + tags/index.html | 1 + tags/mac/index.html | 1 + tags/openwrt/index.html | 1 + tags/pve/index.html | 1 + tags/rime/index.html | 1 + tags/rss/index.html | 1 + tags/self-hosted/index.html | 1 + tags/study/index.html | 1 + tags/think/index.html | 1 + tags/vim/index.html | 1 + tags/vscode/index.html | 1 + tags/webpack/index.html | 1 + 103 files changed, 6587 insertions(+) create mode 100644 2020/09/github-picgo-jsdelivr/index.html create mode 100644 2020/09/hello-hexo/index.html create mode 100644 2020/09/hexo-next/index.html create mode 100644 2020/09/how-to-install-ehforwarderbot-v1/index.html create mode 100644 2020/09/how-to-install-ehforwarderbot-v2/index.html create mode 100644 2020/09/install-hackintosh-to-nuc8i5beh/index.html create mode 100644 2020/09/openwrt-expand-overlay-storage/index.html create mode 100644 2020/10/anti-csrf-on-separation-of-frontend-and-backend/index.html create mode 100644 2020/10/charles/index.html create mode 100644 2020/10/chrome-cookie-do-not-delete/index.html create mode 100644 2020/10/how-to-use-rime/index.html create mode 100644 2020/10/install-pyenv-and-pipenv/index.html create mode 100644 2020/10/ruby-in-vscode/index.html create mode 100644 2020/10/ruby-include-implementation/index.html create mode 100644 2020/10/vim-fast-esc/index.html create mode 100644 2020/10/webpack-resovle-@-path/index.html create mode 100644 2020/11/hexo-next-valine/index.html create mode 100644 2020/11/http-cache/index.html create mode 100644 2020/11/receiver-and-ancestors/index.html create mode 100644 2020/11/tcp-ip/index.html create mode 100644 2020/12/cloudflare-workers/index.html create mode 100644 2021/01/ruby-devise/index.html create mode 100644 2021/02/openwrt-in-pve/index.html create mode 100644 2023/01/cloudflare-tunnels/index.html create mode 100644 2023/01/run-efb-in-docker/index.html create mode 100644 2023/04/build-own-rss-platform/index.html create mode 100644 2023/07/zero-rating/index.html create mode 100644 404/index.html create mode 100644 CNAME create mode 100644 archives/2020/09/index.html create mode 100644 archives/2020/10/index.html create mode 100644 archives/2020/11/index.html create mode 100644 archives/2020/12/index.html create mode 100644 archives/2020/index.html create mode 100644 archives/2020/page/2/index.html create mode 100644 archives/2020/page/3/index.html create mode 100644 archives/2021/01/index.html create mode 100644 archives/2021/02/index.html create mode 100644 archives/2021/index.html create mode 100644 archives/2023/01/index.html create mode 100644 archives/2023/04/index.html create mode 100644 archives/2023/07/index.html create mode 100644 archives/2023/index.html create mode 100644 archives/index.html create mode 100644 archives/page/2/index.html create mode 100644 archives/page/3/index.html create mode 100644 baidusitemap.xml create mode 100644 categories/backend/index.html create mode 100644 categories/backend/python/index.html create mode 100644 categories/backend/ruby/index.html create mode 100644 categories/frotend/index.html create mode 100644 categories/index.html create mode 100644 categories/misc/index.html create mode 100644 categories/misc/page/2/index.html create mode 100644 categories/network/index.html create mode 100644 categories/router/index.html create mode 100644 categories/security/index.html create mode 100644 categories/test/index.html create mode 100644 css/main.css create mode 100644 css/noscript.css create mode 100644 images/apple-touch-icon-next.png create mode 100644 images/avatar.gif create mode 100644 images/avatar.jpg create mode 100644 images/favicon-16x16-next.png create mode 100644 images/favicon-32x32-next.png create mode 100644 images/logo.svg create mode 100644 index.html create mode 100644 js/comments.js create mode 100644 js/config.js create mode 100644 js/motion.js create mode 100644 js/next-boot.js create mode 100644 js/third-party/analytics/matomo.js create mode 100644 js/third-party/pace.js create mode 100644 js/third-party/search/local-search.js create mode 100644 js/third-party/tags/mermaid.js create mode 100644 js/utils.js create mode 100644 page/2/index.html create mode 100644 page/3/index.html delete mode 100644 placeholder create mode 100644 robots.txt create mode 100644 search.xml create mode 100644 sitemap.xml create mode 100644 tags/chrome/index.html create mode 100644 tags/cloudflare/index.html create mode 100644 tags/csrf/index.html create mode 100644 tags/development-environment/index.html create mode 100644 tags/efb/index.html create mode 100644 tags/ehforwarderbot/index.html create mode 100644 tags/freebie/index.html create mode 100644 tags/github/index.html create mode 100644 tags/hackintosh/index.html create mode 100644 tags/index.html create mode 100644 tags/mac/index.html create mode 100644 tags/openwrt/index.html create mode 100644 tags/pve/index.html create mode 100644 tags/rime/index.html create mode 100644 tags/rss/index.html create mode 100644 tags/self-hosted/index.html create mode 100644 tags/study/index.html create mode 100644 tags/think/index.html create mode 100644 tags/vim/index.html create mode 100644 tags/vscode/index.html create mode 100644 tags/webpack/index.html diff --git a/2020/09/github-picgo-jsdelivr/index.html b/2020/09/github-picgo-jsdelivr/index.html new file mode 100644 index 00000000..1c5082b1 --- /dev/null +++ b/2020/09/github-picgo-jsdelivr/index.html @@ -0,0 +1 @@ +Github+PicGo+Jsdelivr 创建个人图床 | Jiz4oh's Life

Github+PicGo+Jsdelivr 创建个人图床

前言

图床相信经常写东西而且文章图文并茂的朋友多多少少都接触过

公共图床服务要不就是备案麻烦,要不就是免费不稳定

本文就是关于如何利用 Github+Jsdelivr 创建免费、大容量、高速的个人图床,并且利用 vscode+PicGo 实现图片自动上传

创建 GitHub 仓库

20200925204037

仓库名随意

生成 Token

  1. 点击个人设置
    20200926180107
  2. 点击开发者设置
    20200926180238
  3. 点击个人令牌
    20200926180349
  4. 创建令牌
    20200926180428
  5. 保存命令
    20200926180540

配置 PicGo

vocode 安装 PicGo

扩展搜索 picgo

20200926180638

配置

打开 vscode 设置

20200926180821

点开 扩展 => PicGo

20200926181037

具体配置:

20200926181331

  1. 选择 github
  2. 一般选择 master,如果不需要发布到其他分支不要更改
  3. 存入 vscode 的链接地址
    • 使用 jsdelivr:https://cdn.jsdelivr.net/gh/红框5处内容
    • 使用 rawgithubcontent:https://raw.githubusercontent.com/红框5处内容/红框2处内容
  4. 存入 GitHub 仓库的子文件夹,可为空
  5. 仓库名,格式为 用户名/仓库名
  6. 之前生成的 Github Token

使用中的问题

描述:jsdelivr 无法加速,显示 Package size exceeded the configured limit of 50 MB

问题原因:jsdelivr 对每一个 release 限制加速 50MB

解决方案:

  1. 进入仓库
  2. 创建 release
    20200926182953
  3. 创建 master 版本
    20200926183104
  4. 修改 PicGo 设置 Custom Url
    在最后面添加 @master 指定版本号
  5. 修改被拒绝显示的链接,在仓库名后面添加 @master。之前 jsdelivr 加速成功的链接不受影响
\ No newline at end of file diff --git a/2020/09/hello-hexo/index.html b/2020/09/hello-hexo/index.html new file mode 100644 index 00000000..7ee5565d --- /dev/null +++ b/2020/09/hello-hexo/index.html @@ -0,0 +1,66 @@ +Hexo 博客搭建过程实录 | Jiz4oh's Life

Hexo 博客搭建过程实录

创建本地博客 blog

安装 Hexo 脚手架

npm install -g hexo-cli

安装之后可以使用 hexo 对博客命令操作

  1. 创建 blog:hexo init 文件夹
  2. 创建文章:hexo new 文章名
  3. 生成静态文件:hexo generate 简写:hexo g
  4. 清除已生成缓存:hexo clean
  5. 启动本地服务:hexo server 简写:hexo s
    默认监听 4000 端口
  6. 部署:hexo deploy 简写:hexo d
  7. 生成静态文件并部署:hexo g -d

创建博客

hexo init blog
+cd blog
+hexo g
+hexo s

访问 http://localhost:4000 查看本地博客

配置 blog 部署 Github Pages

创建 github.io 仓库

20200925204037

输入仓库名,仓库名以用户名开头,以 github.io 结尾

20200925204246

修改部署文件

  1. 创建成功后,打开刚刚使用 hexo init blog 命令生成的 blog 文件夹

  2. 修改 _config.yml 文件
    20200925203757
    repo 填入刚刚创建的 GitHub 仓库地址,建议填写 ssh 仓库地址,https 地址也可

  3. 部署到 Github Pages

    # 清理缓存
    +hexo clean
    +# 部署
    +hexo g -d

GitHub Actions 自动部署

创建 blog 仓库

创建另外一个 GitHub 私有仓库 blog,用来存放 hexo 项目并触发部署

创建 ssh 密钥

ssh-keygen -f .ssh/github-deploy-key

.ssh 文件夹下会有 github-deploy-key 和 github-deploy-key.pub 两个文件。

配置部署密钥

  1. 打开 blog 仓库设置,Settings -> Secrets -> Add a new secret
    20200925210741
    • 在 Name 输入框填写 HEXO_DEPLOY_KEY。
    • 在 Value 输入框填写 github-deploy-key 文件内容。
  2. 打开 用户名.github.io 仓库设置,
    20200925211312
    • 在 Title 输入框填写 HEXO_DEPLOY_PUB。
    • 在 Key 输入框填写 github-deploy-key.pub 文件内容。
    • 勾选 Allow write access 选项。

编写 GitHub Actions

在 hexo 博客文件夹下创建 .github/workflows/deploy.yml 文件,目录结构如下:

blog
+└── .github
+    └── workflows
+        └── deploy.yml

编辑 deploy.yml 文件:

name: Deploy GitHub Pages
+
+on:
+  push:
+    branches:
+      - master
+
+# 修改 GitHub 用户名及邮箱才能正确部署
+env:
+  GIT_USER: jiz4oh
+  GIT_EMAIL: jiz4oh@gmail.com
+
+jobs:
+  build-and-deploy:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [ 10.x ]
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          persist-credentials: false
+
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v1
+        with:
+          node-version: ${{ matrix.node-version }}
+
+      - name: Configuration environment
+        env:
+          HEXO_DEPLOY_PRI: ${{secrets.HEXO_DEPLOY_KEY}}
+        run: |
+          mkdir -p ~/.ssh/
+          echo "$HEXO_DEPLOY_PRI" > ~/.ssh/id_rsa
+          chmod 600 ~/.ssh/id_rsa
+          ssh-keyscan github.com >> ~/.ssh/known_hosts
+          git config --global user.name $GIT_USER
+          git config --global user.email $GIT_EMAIL
+
+      - name: Install dependencies
+        run: npm i
+
+      - name: Install hexo-cli
+        run: npm i -g hexo-cli
+
+      - name: Clean
+        run: hexo clean
+
+      - name: Build & Deploy
+        run: hexo g -d

测试

在 hexo 博客文件夹下,执行

# 配置 git 账户
+git config --global user.name 用户名
+git config --global user.email 邮箱
+
+git remote add origin BLOG仓库地址
+git push --set-upstream origin master
\ No newline at end of file diff --git a/2020/09/hexo-next/index.html b/2020/09/hexo-next/index.html new file mode 100644 index 00000000..f28eb702 --- /dev/null +++ b/2020/09/hexo-next/index.html @@ -0,0 +1,69 @@ +Hexo 博客使用 Next 主题及美化 | Jiz4oh's Life

Hexo 博客使用 Next 主题及美化

安装 Next

安装

yarn add hexo-theme-next

设置使用 Next

  1. 打开博客文件夹

  2. 打开 _config.yml 文件

    theme: next

删除默认主题(可选)

删除 themes 文件夹

配置 Next

本配置文件基于 hexo-theme-next@^8.0.0

未经特殊注明,均为修改主题配置文件 _config.next.yml

创建配置文件

touch _config.next.yml

设置主题为 Gemini

scheme: Gemini

返回顶部显示在侧边栏

back2top:
+  sidebar: true

显示阅读进度条

reading_progress:
+  enable: true
+  # 显示在底部
+  position: bottom

显示加载进度条

pace:
+  enable: true

侧边栏菜单展开所有

toc:
+  expand_all: true

安装外部依赖:

yarn add hexo-generator-searchdb

编辑 _config.yml

search:
+  path: search.xml
+  field: post
+  format: html
+  limit: 10000

编辑 _config.next.yml

# Local search
+local_search:
+  enable: true

设置代码高亮

codeblock:
+  copy_button:
+    enable: true
+  prism:
+    light: prism-tomorrow
+    dark: prism-tomorrow

添加版权声明

creative_commons:
+  license: by-nc-sa
+  sidebar: true
+  post: true

修改建站时间

footer:
+  since: 2020

开启彩色缎带

canvas_ribbon:
+  enable: true

开启文章字数统计

安装外部依赖:

yarn add hexo-word-counter

编辑 _config.yml

symbols_count_time:
+  symbols: true
+  time: true
+  total_symbols: true
+  total_time: true
+  exclude_codeblock: false
+  # 平均字长,默认为 4,中文为 2
+  awl: 2
+  wpm: 275
+  suffix: "mins."

开启文章阅读量统计

busuanzi_count:
+  enable: true

添加 GitHub Banner

github_banner:
+  enable: enable
+  permalink: https://github.com/jiz4oh
+  title: Follow me on GitHub

删除不必要文件

minify: true

添加标签页面

命令行执行:

hexo new page tags

修改 source/tags/index.md

---
+title: tags
+date: 2020-09-26 12:51:57
+# 添加下面两行
+type: tags
+comments: false
+---

编辑 _config.next.yml

menu:
+  tags: /tags/ || fa fa-tags

添加分类页面

命令行执行:

hexo new page categories

修改 source/tags/categories.md

---
+title: categories
+date: 2020-09-26 13:07:13
+# 添加下面两行
+type: categories
+comments: false
+---

编辑 _config.next.yml

menu:
+  categories: /categories/ || fa fa-categories

设置字体

样式

font:
+  enable: true
+  global:
+    external: true
+    family: Arial
+  # 以下设置只能修改 `` 注释的 md 代码块,不能修改 ```code block``` 字体大小
+  # ```code block``` 字体大小修改见下
+  # codes:
+  #   external: true
+  #   family: Arial
+  #   size: 0.8

大小

编辑 _config.next.yml

custom_file_path:
+  style: source/_data/styles.styl

新建 source/_data/styles.styl

// 修改文章字体大小,强制保持 1em
+div.post-body {
+  font-size: 1em;
+}
+
+// 修改 code block 中的字体,需要写入以下样式
+code[class*=language-] {
+  font-size: 14px;
+}

致谢

博客主题美化参考了诸多前辈的文章,感谢各位大神的无私分享~

参考文章

hexo的next主题个性化教程:打造炫酷网站

Next 原作者 iissnan 大神教程,v6 版本以上部分配置不适用

Next 社区教程

\ No newline at end of file diff --git a/2020/09/how-to-install-ehforwarderbot-v1/index.html b/2020/09/how-to-install-ehforwarderbot-v1/index.html new file mode 100644 index 00000000..48484494 --- /dev/null +++ b/2020/09/how-to-install-ehforwarderbot-v1/index.html @@ -0,0 +1,29 @@ +CentOS 7 安装 ehforwarderbot V1 来收发微信 | Jiz4oh's Life

CentOS 7 安装 ehforwarderbot V1 来收发微信

ehforwarderbot 介绍

ehforwarderbot 是由 blueset 开源在 github 的一款专用于转发微信/QQ/Faceboot Message 到 Telegram 的机器人

ehforwarderbot 支持 多微信,多QQ 汇集到一起集中处理消息,特别适合:

  1. Telegram 重度用户
  2. 需要对多个微信、qq 消息进行处理的用户

最新版的 ehforwarderbot 已更新到 v2.0.0,本教程仅适用于 v1 版本,且 v2 版本与 v1 版本的数据库结构大改,无法从 v1 无缝迁移到 v2 版本。请谨慎安装 v1 版本

优点

  1. 消息云同步,⽂字,语⾳,图⽚,视频,发送的链接,⽂件都可以保存在 tg 云端
  2. 消息⼏乎⽆延迟,对⽐ Gcmformojo,tg 发消息很快,没有卡顿,就像你正常聊 tg ⼀样,也没有消息发送失
    败的情况(除⾮你⽹络没连上)
  3. 耗电,明显优于微信毒瘤。tg ⾃带 gcm,如果你需要,可以不留 tg 后台,由 gcm 拉起通知
  4. ⽆需挂梯,以往 Gcmformojo 有的地区需要挂飞机才能收发,⽽ tg ⾃带⼀个代理功能,可以通过代理收发微
  5. 公众号信息也能推送,⽽且 TG ⾃带应⽤内浏览器,也能⽅便的查看公众号推送的⽂章

缺点

ehforwarderbot 依赖于 web 版微信,所以 web 版微信没有的功能,它也不可能有,例如:

  1. 无法收发红包
  2. 无法查看别人分享的历史记录
  3. 某些表情无法正常显示

安装 docker CE

卸载旧版本docker

sudo yum remove docker \
+                  docker-client \
+                  docker-client-latest \
+                  docker-common \
+                  docker-latest \
+                  docker-latest-logrotate \
+                  docker-logrotate \
+                  docker-engine

添加依赖

sudo yum update
+sudo yum install -y yum-utils \
+  device-mapper-persistent-data \
+  lvm2

添加 Docker 稳定版本的 yum 软件源

sudo yum-config-manager \
+    --add-repo \
+    https://download.docker.com/linux/centos/docker-ce.repo

安装 docker

sudo yum update
+sudo yum install docker-ce

docker开机自启:
sudo systemctl enable docker
启动docker服务:
sudo systemctl start docker

安装efb

创建配置文件

获取 Telegram Bot Token

  1. 在Telegram关注@BotFather
  2. 再到对话框依次输入:/start => /newbot
  3. 然后会要你给机器人命名(如:TestBot)
  4. 命名完成会得到 Token

获取自己的 Userid

  1. 先和你的机器人聊天,随便发一句话。
  2. 在浏览器输入 https://api.telegram.org/botxx:xx/getUpdates(其中xx:xx为Token)
  3. 然后 chat 后面的 id 即为你的 Userid

创建 config

创建 config.py 文件

master_channel = 'plugins.eh_telegram_master', 'TelegramChannel'
+slave_channels = [('plugins.eh_wechat_slave', 'WeChatChannel')]
+
+eh_telegram_master = {
+    "token": "机器人的 TOKEN",
+    "admins": [你自己的 Userid],
+    "bing_speech_api": ["3243f6a8885a308d313198a2e037073", "2b7e151628ae082b7e151628ae08"],
+    "baidu_speech_api": {
+        "app_id": 0,
+        "api_key": "3243f6a8885a308d313198a2e037073",
+        "secret_key": "2b7e151628ae082b7e151628ae08"
+    }
+}

创建 tgdata.db

创建 tgdata.db 文件,该文件可以为空

启动 ehforwarderbot

config.pytgdata.db 放在 /root 目录下

运行:

docker run -d --restart=always --name=efb \
+    -v /root/config.py:/opt/ehForwarderBot/config.py \
+    -v /root/tgdata.db:/opt/ehForwarderBot/plugins/eh_telegram_master/tgdata.db \
+    royx/docker-efb

登录 efb

docker logs efb

此时屏幕上会出现二维码,使用手机扫码登录即可

\ No newline at end of file diff --git a/2020/09/how-to-install-ehforwarderbot-v2/index.html b/2020/09/how-to-install-ehforwarderbot-v2/index.html new file mode 100644 index 00000000..c937babe --- /dev/null +++ b/2020/09/how-to-install-ehforwarderbot-v2/index.html @@ -0,0 +1,76 @@ +CentOS 7 安装 efb v2 来收发微信 | Jiz4oh's Life

CentOS 7 安装 efb v2 来收发微信

上一篇文章已经介绍了如何安装 v1,这篇文章来介绍如何安装 v2 版本的 ehforwarderbot

安装依赖

yum install -y gcc file-devel libwebp-tools git screen
+
+# 安装 python3.6 和 pip3
+wget https://www.moerats.com/usr/shell/Python3/CentOS_Python3.6.sh && sh CentOS_Python3.6.sh
+yum install python3-pip python3-dev python3-setuptools
+
+# 下载 ffmpeg
+wget https://www.moerats.com/usr/down/ffmpeg/ffmpeg-git-$(getconf LONG_BIT)bit-static.tar.xz && tar xvf ffmpeg-git-*-static.tar.xz
+mv ffmpeg-git-*/ffmpeg ffmpeg-git-*/ffprobe /usr/bin/
+rm -rf ffmpeg-git-*

安装 efb

安装 efb(ehforwarderbot)

二选一:

# 安装稳定版
+pip3 install ehforwarderbot
+
+# 安装开发版
+pip3 install git+https://github.com/blueset/ehforwarderbot.git

安装 ETB(efb-telegram-master)和 EWS(efb-wechat-slave)

# 安装TG和微信模块
+pip3 install efb-telegram-master efb-wechat-slave

配置 efb

配置 efb(ehforwarderbot)

mkdir -p ~/.ehforwarderbot/profiles/default
+vi ~/.ehforwarderbot/profiles/default/config.yaml

保存下列代码到 config.yaml

master_channel: "blueset.telegram"
+slave_channels:
+- "blueset.wechat"

这只是登录一个微信号,如果你要同时登录多个微信号,那么配置文件就需要改为:

# 比如我要同时登录并收发3个微信号
+master_channel: blueset.telegram
+slave_channels:
+- blueset.wechat
+- blueset.wechat#moe123
+- blueset.wechat#rats321
+# #号后面指定id,只能是字母、数字、下划线

配置 ETB

# 同样的也建在 default 文件夹,如果你上面更改了 default 文件夹,那这里也要更改
+mkdir ~/.ehforwarderbot/profiles/default/blueset.telegram
+vi ~/.ehforwarderbot/profiles/default/blueset.telegram/config.yaml

保存下列代码到 blueset.telegram/config.yaml

token: "机器人的 TOKEN"
+admins:
+- 你自己的 Userid
+flags:
+    # 关闭自动语言设置,使用 systemd 启动时默认中文
+    auto_locale: false

配置 EWS

# 同样的也建在 default 文件夹,如果你上面更改了 default 文件夹,那这里也要更改
+mkdir ~/.ehforwarderbot/profiles/default/blueset.wechat
+vi ~/.ehforwarderbot/profiles/default/blueset.wechat/config.yaml

保存下列代码到 blueset.wechat/config.yaml

flags:
+    # tg 端编辑消息时以撤回并重新发送的方式发送到微信
+    delete_on_edit: true
+    # 每当请求会话列表时,强制刷新会话列表
+    refresh_friends: true
+    # 使用 iTerm2 图像协议 显示二维码。本功能只适用于 iTerm2 用户
+    imgcat_qr: true
+    # 在收到第三方合作应用分享给微信的链接时,其附带的预览图将缩略图上传到 sm.ms
+    app_shared_link_mode: upload

设置守护进程

创建 efb 服务

vi /etc/systemd/system/efb.service

保存以下配置到 efb.service

[Unit]
+Description=EH Forwarder Bot instance
+After=network.target
+Wants=network.target
+Documentation=https://github.com/blueset/ehForwarderBot
+
+[Service]
+Type=simple
+Environment='LANG=zh_CN.UTF-8' 'PYTHONIOENCODING=utf_8' 'EFB_DATA_PATH=/root/.ehforwarderbot'
+ExecStart=/usr/local/bin/ehforwarderbot --verbose
+Restart=on-abort
+KillSignal=SIGINT
+
+[Install]
+WantedBy=multi-user.target

使用 systemctl 管理 efb

# 开机启动
+sudo systemctl enable efb
+
+# 启动
+sudo systemctl start efb
+
+# 关闭
+sudo systemctl stop efb
+
+# 重启
+sudo systemctl restart efb
+
+# 查看运行详情
+sudo systemctl status efb
+
+# 查看更加详细的运行详情
+sudo systemctl status efb -l

登录 efb

  • 如果不使用 systemctl 管理,则执行 ehforwarderbot --verbose 进行启动
  • 如果使用 systemctl 进行管理
    1. sudo systemctl start efb
    2. sudo systemctl status efb -l 然后复制二维码连接到浏览器,扫码登陆

安装中遇到的错误

缺少 cairo 依赖

描述:

raise OSError(error_message) # pragma: no cover
+OSError: no library called "cairo" was found

解决方案:

yum install -y cairo-devel libtiff* && pip3 install cairosvg cairocffi

描述:

Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-g3q2qdl8/cairocffi/

原因:pip 版本过低

解决方案:

pip3 install --upgrade pip

python 安装失败

描述:

configure: error: in `/usr/local/src/Python-3.6.4':
+configure: error: no acceptable C compiler found in $PATH
+See `config.log' for more details

解决方案:
yum install gcc -y

描述:

zipimport.ZipImportError: can't decompress data; zlib not available
+make: *** [install] Error 1

解决方案:
yum install zlib* -y

pip 安装依赖失败

描述:
install --record /tmp/pip-f5erwx9h-record/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-build-34xxo8de/cairocffi/

解决方案:

pip install -U setuptools
+pip install -U wheel

如何迁移

  1. 保留 /root/.ehforwarderbot/profiles/default/blueset.telegram 下的 config.yamltgdata.db 文件
  2. 保留 /root/.ehforwarderbot/profiles/default/blueset.wechat 下的 wxpy_puid.pkl 文件(这个文件存着对应微信好友的 UID,与 tgdata 的 chatassoc 表相对应)
\ No newline at end of file diff --git a/2020/09/install-hackintosh-to-nuc8i5beh/index.html b/2020/09/install-hackintosh-to-nuc8i5beh/index.html new file mode 100644 index 00000000..28dc5ff4 --- /dev/null +++ b/2020/09/install-hackintosh-to-nuc8i5beh/index.html @@ -0,0 +1 @@ +NUC8i5BEH 黑苹果安装记录 | Jiz4oh's Life

NUC8i5BEH 黑苹果安装记录

安装 mac

拷贝 EFI

  1. 挂载 u 盘的 EFI 分区(在 Finder 中 带有三角符号的即是 u 盘的 EFI 分区)
  2. 拷贝 EFI 分区下的 EFI 文件夹到桌面
  3. 挂载磁盘的 EFI 分区(注:在 10.15.3 和 10.15.4 中 Finder 不会显示两个 EFI 磁盘,可以先卸载 u 盘 的 EFI 分区)
  4. 将桌面的 EFI 文件夹拷入磁盘的 EFI 分区,替换其下的 EFI 文件夹

注入三码

  1. 点击 Clover Configurator 左下角

    image-20200328230934689

  2. 打开 EFI 分区 下的 EFI>CLOVER>config.list

  3. 引导参数:
    image-20200328232028435

  4. (可选)修改 UI 比例,解决开机过程中苹果 logo 从大变小
    image-20200328232246807

  5. 生成序列号和 SMUUID,多点击几下避免随机
    image-20200328232723186

  6. 生成设备的唯一标识号
    image-20200328232851352

  7. 生成 ROM ID
    image-20200328233244117

  8. 保存
    image-20200328233536063

  9. 关闭 Clover Configurator,会提示不支持永久性的版本存储,点击

  10. 重启 mac,此处可以拔出 U 盘了

易用性设置

必做设置

睡眠修复

  1. 打开 Hackintool>电源
  2. 修改睡眠值
    image-20200328234800909

可选设置

打开允许安装其他来源应用

终端执行:sudo spctl --master-disable

关闭读卡器显示

如果硬改了 NUC8,会在右上角出现一个卡片的标识
!!!waring:不要点击关闭卡会造成死机

解决办法是:

  1. 10.15 Catalina 中系统分区默认为只读,故需执行:
    sudo mount -uw / && killall Finder
  2. 关闭读卡器显示:
    fn=".`date +%s`" && sudo mv /System/Library/CoreServices/Menu\ Extras/ExpressCard.menu /System/Library/CoreServices/Menu\ Extras/ExpressCard.menu$fn && sudo touch /System/Library/CoreServices/Menu\ Extras/ExpressCard.menu

黑苹果下装 win10

制作 winpe

下载 winpe 和 纯净 win10 镜像,然后写入到 u 盘中

分区

!!!不要使用 BootCamp,黑苹果使用无效

  1. 打开磁盘工具
  2. 分一个 win10 系统盘的分区(格式随意,后期会重新格式化)出来,如果玩游戏多,可以多分一点
  3. 分一个 500M 左右的分区作为 win10 efi 存放地址

重启到 winpe

  1. 重启到 bios(一般是开机时按 F2 进入 bios)
  2. 选择 winpe 的 uefi 启动
  3. 打开 diskgenius,备份 efi 文件夹 A(此步为保留苹果和 clover 的启动文件),备份到外部磁盘,以防格盘
  4. 选择安装 win10,选择安装文件夹为刚刚格式化的那个磁盘,引导文件夹选择 efi 文件夹 A 所在磁盘(必须先覆盖)
  5. 安装完成后,重启到 win10,进行设置。。。
  6. 重启到 winpe
  7. 将 efi 文件夹 B 中的内容拷贝到 500M 分区中
  8. 将之前备份的 efi 文件夹 A 中内容拷贝到当前 efi 文件夹 B 中,覆盖所有
  9. 重启,会看见 clover。出现 win10 和 mac 双启动选项

问题汇总

  1. 使用外置显卡坞为什么无法点亮?
    1. 进入 BIOS>Security 菜单
    2. 将 Thunderbolt Security Level 设置为 Legacy Mode
    3. 进入 BIOS>Boot 菜单
    4. Boot Configuration – Boot Devices – Thunderbolt Boot 开启
\ No newline at end of file diff --git a/2020/09/openwrt-expand-overlay-storage/index.html b/2020/09/openwrt-expand-overlay-storage/index.html new file mode 100644 index 00000000..0c117c6f --- /dev/null +++ b/2020/09/openwrt-expand-overlay-storage/index.html @@ -0,0 +1,4 @@ +openwrt 扩容 overlay | Jiz4oh's Life

openwrt 扩容 overlay

原理

lsblk:查看当前固件的分区信息

NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
+sda            8:0    1 58.6G  0 disk
+├─sda1         8:1    1  16M  0 part
+├─sda2         8:2    1  300M  0 part /mnt/sda4

image-20200711142556881

sda 2 中的浅蓝色和深蓝色区域为底层 Squash 格式,该格式只读,不支持修改,优势是可以在出错时轻松重置

overlay 就是在 upper layer 层进行读写的形式

image-20200711143340836

overlay 的扩容并不是在 sda2 上进行操作,而是新建一个更大的分区 sda3,并将 Overlay 指向 sda3 ,这样的话,重置 sda2 后并不会损坏 sda3 中的配置

扩容步骤

创建新分区

使用 cfdisk 进行磁盘操作(使用 opkg install cfdisk 安装,如果安装失败请更新 opkg 源 opkg update)

  1. cfdisk

    image-20200711144836714

  2. 新建分区:切换到 free space ,切换到 new 回车,输入分区大小

    image-20200711145123408

    选择主分区或者扩展分区:选择 primary

    image-20200711145248408

  3. 将更改写入分区表:光标移到新分区,选择 wirte,并输入 yes

    image-20200711145522455

  4. 退出 cfdisk:选择 quit

    image-20200711145729840

格式化新分区

mkfs.ext4 /dev/sda3

image-20200711150146466

挂载新分区

将 /dev/sda3 挂载到 /mnt/sda3 下:mount /dev/sda3 /mnt/sda3

ls /mnt/sda3 查看 /mnt/sda3 目录,如果有 lost+found 目录则表示挂载成功

image-20200711150414828

拷贝 /overlay 下所有文件

ls /overlay 查看 /overlay 目录,如果有文件则拷贝到 /mnt/sda3 中

cp -r /overlay/* /mnt/sda3

检测是否拷贝成功

ls /mnt/sda3

在系统中挂载目录

重启路由器

参考

OPENWRT | ESXI 下 OpenWrt扩容Overlay,增加安装插件空间

\ No newline at end of file diff --git a/2020/10/anti-csrf-on-separation-of-frontend-and-backend/index.html b/2020/10/anti-csrf-on-separation-of-frontend-and-backend/index.html new file mode 100644 index 00000000..b892cf72 --- /dev/null +++ b/2020/10/anti-csrf-on-separation-of-frontend-and-backend/index.html @@ -0,0 +1 @@ +前后端分离下如何防止 CSRF 攻击 | Jiz4oh's Life

前后端分离下如何防止 CSRF 攻击

什么是 CSRF 攻击

CSRF 的全名是 Cross Site Request Forgery,翻译成中文就是跨站点请求伪造

CSRF 利用的是网站对用户网页浏览器的信任,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。
由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

例子:

假如一家银行用以运行转账操作的 URL 地址如下:https://bank.example.com/withdraw?account=AccoutName&amount=1000&for=PayeeName

那么,一个恶意攻击者可以在另一个网站上放置如下代码:<img src="https://bank.example.com/withdraw?account=Alice&amount=1000&for=Badman" />

如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金

常见的防御手段

验证码

CSRF 攻击往往是在用户不知情的情况下伪造了请求,而验证码强制用户必须进行交互

  • 措施:给敏感操作添加验证码校验
  • 局限:影响用户体验

检查 Referer 字段

在处理敏感请求时,通常发出请求的网址应该与接受请求的网址属于同一域名

  • 措施:所以我们可以通过校验 HTTP headers 中的 Referer 字段是否是属于本网站,如果不是则可能遭受 CSRF 攻击
  • 局限:这种办法简单易行,工作量低,但是依赖于浏览器的发送正确的 Referer 字段,无法保证浏览器没有安全漏洞影响 Referer 的发送

不使用 GET 请求做敏感操作

在上面的例子中,我们可以知道,Alice 打开恶意站点后,浏览器通过 img 标签向银行转账网址发送了 GET 请求从而实现了 CSRF 攻击。

  • 措施:所以,避免 CSRF 攻击最基础的一条是 永远不使用 GET 请求做敏感操作
  • 局限:这种方法可以避免 imgscriptiframe 等带 src 属性的标签发起 CSRF 攻击。但是无法防止攻击者构建 POST 请求等发起 CSRF 攻击

使用 Anti-CSRF-Token

CSRF 为什么能够攻击成功?其本质原因是重要操作的所有参数都是可以被攻击者猜测到的。攻击者只有预测出 URL 的所有参数与参数值,才能成功地构造一个伪造的请求;反之,攻击者将无法攻击成功。

  • 措施:所以我们可以生成一个足够随机的参数 Token 放入该次请求,保证该次请求无法被攻击者成功伪造。

    核心思路:使用一个由服务器派发的 Token,在前端进行状态修改时,也同时提交这个 Token(往往会放在 html forminput 中,或者 ajax header 中),这时候服务端验证该 Token 是否是之前所生成的,以此来判断这个请求是否被允许

  • 局限

    1. 依赖于 Token 足够随机
    2. 无论是 Token 放在哪里,只要 JS 能读取到,都会面临 XSS 风险

在常见的 Web 后端框架比如 RailsDjango 中都自带实现了 csrf_token 功能,不同的是

  • Rails 使用 meta 标签存放 csrf_token,通过 HTTP headers 发送
  • Django 使用隐藏的 input 标签存放 csrf_token,通过 POST body 发送

前后端分离带来的问题

在现在大前端时代,前端后分离是个再常见不过的情况,我们该如何在前后端分离的情况下防止 CSRF 攻击呢?

首先,我们分析下上面所提到的几种手段在前后端分离下发生了什么变化:

  • 验证码:
    • form 表单完全由前端生成,后端是无法感知的,即使前端使用某种方式在 form 中放入了验证码,但是后端也无法验证
  • 检查 Referer 字段:无变化
  • 不使用 GET 请求做敏感操作:无变化
  • 使用 Anti-CSRF-Token:
    • form 表单完全由前端生成,后端是无法感知的,即使前端使用某种方式在 form 中放入了 Token,但是后端也无法验证

所以根本上是前后端分离之后,后端无法控制 form 生成时机,而前端则必须通过某种方式获取到由后端生成的 验证码 或者 Token 才能保证后端能够校验请求

  • 措施
    1. 后端引入安全模块,可能是写在 WAF 里,也有可能是 Security Sidecar 或者自定义的 API Gateway
    2. 前端请求安全模块,由安全模块生成 csrf_token 并返回,在提交时验证请求
  • 局限:依然无法避免 XSS 攻击

XSRF

CSRFToken 仅仅用于对抗 CSRF 攻击,当网站还同时存在 XSS 漏洞时,这个方案就会变得无效。
因为 XSS 可以模拟客户端浏览器执行任意操作,在 XSS 攻击下,攻击者完全可以请求页面后,读出页面内容里的 Token 值,然后再构造出一个合法的请求。这个过程就被称之为 XSRF

XSS 带来的问题,应该使用 XSS 的防御方案予以解决,否则 CSRFToken 防御就是空中楼阁。安全防御的体系是相辅相成、缺一不可的

迷思

  • Q:以下攻击能成功吗?

    1. 攻击者构建了一个恶意网站
    2. 恶意网站先通过请求上述所说的安全模块获取 csrf_token
    3. 恶意网站构造隐藏的 form 表单并对参数进行伪造
    4. 利用 JS 自动提交(恶意网站是攻击者所有,所以绕过了 XSS 防御策略) form 并携带 csrf_token 给目标网站

    A:不会,因为浏览器的 同源策略 不允许恶意网站获取安全模块所返回的结果(csrf_token),所以攻击在第 2 步时就会失败

参考

白帽子讲Web安全

跨站请求伪造

CSRF - 前后端分离后带来的新问题

\ No newline at end of file diff --git a/2020/10/charles/index.html b/2020/10/charles/index.html new file mode 100644 index 00000000..a092e4dd --- /dev/null +++ b/2020/10/charles/index.html @@ -0,0 +1,11 @@ +使用 charles 调试 | Jiz4oh's Life

使用 charles 调试

安装

点击去官网下载

激活

Registered Name: https://zhile.io
+License Key: 48891cf209c6d32bf4

开启 http 调试

  1. 点击菜单栏 Proxy -> Proxy Settings
  2. 填写 Port
  3. 勾选 Enable transparent HTTP proxying

20201009171421

开启抓取 macOS 请求

  1. 点击菜单栏 Proxy -> Proxy Settings
  2. 点击 macOS
  3. 勾选 Enable macOS proxy
  4. 勾选 Use HTTP proxy

20201009171951

开启抓取 Android 请求

手机连接局域网下 wifi,与 charles 必须为同一网络下。

  1. 自动开启代理

    1. 在已连接的 wifi 上点击更多,进入配置代理页。

    2. 勾选自动,在输入框URL中输入:

      Https://chls.pro/10.10.11.235:6666.pac
  2. 手动设置代理

    1. 在已连接的 wifi 上点击更多,进入配置代理页。
    2. 勾选手动,输入 charles 的 ip 与端口(ip 为 macOS 主机 ip,端口为上面填写的 http 端口)

设置 https

  1. 点击 Proxy -> SSL Proxying Settings
    20201009173147
  2. 勾选 Enable SSL Proxying
  3. 点击 Add,Host 填写 *,Port 填写 443
    20201009173256

macOS 安装证书

  1. 点击菜单栏 Help -> SSL Proxying -> Install Charles Root Certificate
    20201009172253
  2. 找到 Charles Proxy..... 并点击
  3. 点击 Trust -> When using this certificate,并勾选 Always Trust
    20201009172635

Android 安装证书

Android7 以后,系统不再信任用户级的证书,只信任系统级的证书,所以要抓包就需要把我们的 charles 证书安装至 Android 的系统目录中

准备

  1. 一台已 root 的手机
  2. Openssl

证书生成

  1. Filddler 或者其他抓包程序的证书导出,一般为 xxx.cer 或者 xxx.pem

  2. 使用 opensslx509 指令进行 cer 证书转 pem 证书 和 用 md5 方式显示 pem 证书的 hash

    # 1. 证书转换,已经是 pem 格式的证书不需要执行这一步
    +openssl x509 -inform DER -in xxx.cer -out cacert.pem
    +
    +# 2. 进行 MD5 的 hash 显示
    +
    +# openssl 版本在 1.0 以上的版本的执行这一句
    +openssl x509 -inform PEM -subject_hash_old -in cacert.pem
    +
    +# openssl 版本在 1.0 以下的版本的执行这一句
    +openssl x509 -inform PEM -subject_hash -in cacert.pem

    将第二条指令输出的类似 347bacb5 的值进行复制
    tips:查看 openssl 版本的指令 openssl version

  3. 将 pem 证书重命名
    使用上面复制的值(类似于 347bacb5)对 pem 证书进行重命名

    mv cacert.pem 347bacb5.0
  4. 将新证书放入手机系统证书目录(/system/etc/security/cacerts)

    需要拷贝至此目录必须拥有 root 权限

  5. 重启 Android 设备以生效
    拷贝证书至 /system/etc/security/cacerts 之后,重启手机就可以使证书生效了

参考

charles 安装配置 for Mac

给 Android7 及以上的手机安装系统级证书,实现 Fiddler 或者其他程序的 HTTPS 的抓包

\ No newline at end of file diff --git a/2020/10/chrome-cookie-do-not-delete/index.html b/2020/10/chrome-cookie-do-not-delete/index.html new file mode 100644 index 00000000..622a1f12 --- /dev/null +++ b/2020/10/chrome-cookie-do-not-delete/index.html @@ -0,0 +1 @@ +chrome 为什么不会自动过期 expires 为 session 的 cookie | Jiz4oh's Life

chrome 为什么不会自动过期 expires 为 session 的 cookie

前言

今天对自己的项目做前后端联调时,发现在 mac 的 chrome 下(未测试 windows)没有自动过期 expires 为 session 的 cookie,对此感到一些疑惑

尝试

在网上搜索的时候,发现了 这篇文章,遂按照网友的解决方案,尝试了如下行为:

  1. 点击作左上角 x,结果:无效
  2. Command + Q 退出,结果:无效
  3. 右键 Chrome 图标退出,结果:无效

很难受,网友提供的方法都无效

解决

  1. 点击 chrome 设置
  2. 默认浏览器 -> 启动时 -> 取消勾选 继续浏览上次打开的网页,勾选其他两个选项

原因

为了增强用户体验,chrome 会在 继续浏览上次打开的网页 设置下,默认不删除会话 cookie

\ No newline at end of file diff --git a/2020/10/how-to-use-rime/index.html b/2020/10/how-to-use-rime/index.html new file mode 100644 index 00000000..4b5cab9c --- /dev/null +++ b/2020/10/how-to-use-rime/index.html @@ -0,0 +1,306 @@ +Rime 输入法指北 | Jiz4oh's Life

Rime 输入法指北

前言

出于对国产输入法软件的不信任,再加上国产输入法广告太多等因素的影响,促使我想要尽快找到一个开源、快捷、不自动上传云端的输入法。

在网上搜索时,Rime 收到了一致好评,让我萌生了非常大的兴趣,故特意找到了 Rime 的相关资料并整理。

本文基于 Rime v1.5.3 版本进行整理,其他版本可能不适用。

关于 Rime

什么是 Rime

Rime 全名是「中州韵输入法引擎」,Rime 不是一种输入法。是从各种常见键盘输入法中提炼出来的抽象的输入算法框架,由 佛振 大佬开发。
Rime 通过各种输入方案来实现对几乎所有中文的支持,包括但不限于【拼音】、【注音】、【双拼】、【五笔】、【仓颉】,并且可以简单的进行【简繁】的切换

什么是输入方案

Rime 由于本质只是一个框架,本身是不知道如何输入的。
如果我们想要使用 Rime,就得定义一些关于如何输入的设置来告诉 Rime,这些设置就是输入方案。

Rime 的版本

Rime 是一个天生跨平台的框架,在每一个平台都有对应的发行版本:

  1. Linux:
    • 官方:ibus-rime(中州韵)
    • 第三方:fcitx-rime
  2. windowns:
    • 官方:Weasel(小狼毫)
    • 第三方:PRIME
  3. macOS:
    • 官方:Squirrel(鼠须管)
    • 第三方:XIME
  4. Android:
    • 第三方:Trime(同文)
  5. iOS:
    • 第三方:iRime

上面是 Rime 在各平台对应的软件,Rime 默认提供了两个拼音输入方案「朙月拼音」和「地球拼音」,两者都可以输入准确的繁体和简体,而且「地球拼音」还支持声调输入。
Rime 还支持了许多种方言拼音,如吴语、粤语,甚至中古汉语。

Rime 的文件架构

Rime 所有的配置文件、输入方案及词典文件,均是普通的文本文档,均要求 UTF8 编码。
其中配置文件、输入方案要求使用 yaml 格式的文件。词典文件使用普通的 txt 文档即可。

Rime 中将文件分为 共享文件夹用户文件夹

  • 共享文件夹 中存放的是 Rime 发行版的默认文件,一般是不允许进行修改的
    • 【中州韵 ibus-rime/usr/share/rime-data/
    • 【小狼毫 Weasel安装目录\data
    • 【鼠须管 Squirrel/Library/Input Methods/Squirrel.app/Contents/SharedSupport/
  • 用户文件夹 的存放位置
    • 【中州韵 ibus-rime~/.config/ibus/rime/
    • 【小狼毫 Weasel%APPDATA%\Rime,也可以通过“开始菜单\小狼毫输入法\用户文件夹”打开。
    • 【鼠须管 Squirrel~/Library/Rime/,也可以通过“系统输入法菜单/鼠须管/用户设置”打开。

用户文件夹

刚开始安装后,在 用户文件夹 中是没有文件的,当我们运行 Rime 后会出现以下文件/文件夹:

  • build/*:这个文件夹下存放的是我们每次部署之后生成的静态文件
    • <输入方案名>.prism.bin:Rime 棱镜,拼写运算规则
    • <词典名>.table.bin:Rime 固态词典,按音节编码检索词条的索引
    • <词典名>.reverse.bin:Rime 反查词典,按词条检索编码的索引
  • <词典名>.userdb/*:这个文件夹是我们的词典文件夹,当我们进行了输入之后,会将输入的内容存入,以便进行词语调频
  • installation.yaml:这个文件是关于我们本机 Rime 的安装时间、版本信息等
  • user.yaml:这个文件是关于 Rime 的一些设置选项,比如我们上次选择的输入方案

Rime 的配置文件都需要放于 用户文件夹 下,分为三类:

  • default.yaml:全局配置,存放跨输入方案的通用配置
  • <输入方案名>.schema.yaml:存放某一个输入方案的配置
    如:double_pinyin_flypy.schema.yaml
  • <发布版名>.yaml:存放某个发行版的独有配置
    如:symbols.yaml

如果有其他额外文件,也需要放入 用户文件夹 下,才能被配置文件正确使用

自定义 Rime 的全局配置

我的全局配置 default.custom.yaml

可配置项:

  • page_size:每页个数,默认 5,允许 1~9

该项可在每个输入方案中单独设置

示例:

menu:
+  page_size: 8

schema_list

可配置项:

  • schema:每一项 schema 对应一项方案的 schema_id

示例:

schema_list:
+  - schema: luna_pinyin
+  - schema: luna_pinyin_simp
+  - schema: luna_pinyin_fluency
+  - schema: bopomofo
+  - schema: bopomofo_tw
+  - schema: cangjie5
+  - schema: stroke
+  - schema: terra_pinyin

switcher

可配置项:

  • caption:切换器被调用时屏幕显示的名字
  • hotkeys:切换器调用的快捷键
  • abbreviate_options
  • fold_options
  • option_list_separator
  • save_options

示例:

switcher:
+  abbreviate_options: true
+  caption: "〔方案選單〕"
+  fold_options: true
+  hotkeys:
+    - "Alt+Shift+Control+grave"
+    - "Control+grave"
+  option_list_separator: "/"
+  save_options:
+    - full_shape
+    - ascii_punct
+    - simplification
+    - extended_charset
+    - zh_hant
+    - zh_hans
+    - zh_hant_tw

自定义 Rime 的输入法方案

Rime 输入方案主要分为三部分

  • schema:关于整个输入方案的信息阐述
  • switches:可以切换的开关,比如切换全半角,切换简繁体
  • engine:Rime 的核心配置,配置整个 Rime 是如何进行运作

特殊说明:

输入方案的某些细项设置是可以写在全局设置中,比如

  • key_binder
  • punctuator
  • ascii_composer
  • recognizer

在输入方案中设置 menu/page_size 是可以的

schema

可配置项:

  • name:方案的显示名称〔即出现于方案选单中,通常为中文〕
  • schema_id:方案内部名,在代码中引用此方案时的名称,通常由英文,数字,下划线组成
  • author:方案作者。如果您对方案做出了修改,请保留原作者名,放入自己的名字加在后面
  • description:简要描述方案历史,码表来源,该方案规则等
  • dependencies:如果本方案依赖于其他方案〔通常来说会依赖其他方案做为反查,抑或是两种或多种方案混用时]
  • version:版本号,在发布新版前请确保已升版本号

示例:小鹤双拼

schema:
+  name: "小鶴雙拼"
+  schema_id: double_pinyin_flypy
+  author:
+    - "double pinyin layout by 鶴"
+    - "Rime schema by 佛振 <chen.sst@gmail.com>"
+  description: |
+    朙月拼音+小鶴雙拼方案。
+  dependencies:
+    - stroke
+  version: 0.18

switches

  • ascii_mode:中英文切换开关。0 为中文,1 为英文

  • full_shape:全角符号/半角符号开关。默认 0 为半角,1 为全角。注意,开启全角时英文字母亦为全角。

  • extended_charset:字符集开关。0 为 CJK 基本字符集,1 为 CJK 全字符集。仅 table_translator 可用

  • ascii_punct:中西文标点切换开关,0 为中文句读,1 为西文标点。

  • simplification:是转化字开关。一般情況下与上同,0 为不开启转化,1 为转化。
    simplification 选项名称可自定义,亦可添加多套替換用字方案:

    - name: zh_cn
    +  states: ["漢字", "汉字"]
    +  reset: 0

    - options: [ zh_trad, zh_cn, zh_mars ]
    +  states:
    +    - 字型 → 漢字
    +    - 字型 → 汉字
    +    - 字型 → 䕼茡
    +  reset: 0
    • name/options:須与 enginesimplifieroption_name 相同

      switches:
      +  - name: zh_simp
      +    reset: 1
      +    states: [ 漢字, 汉字 ]
      +simplifier:
      +  option_name: zh_simp
    • states:可不写,如不写则此开关存在但不可见,可由快捷鍵操作

    • reset:设定开关默认状态〔若 reset 为空,则使用上次状态〕

  • 字符集过滤。此选项沒有默认名称,須配合 charset_filter 使用。可單用,亦可添加多套字符集

示例:

switches:
+  - name: ascii_mode
+    reset: 0
+    states: ["中文", "西文"]
+  - name: full_shape
+    states: ["半角", "全角"]
+  - name: extended_charset
+    states: ["通用", "增廣"]
+  - name: simplification
+    states: ["漢字", "汉字"]
+  - name: ascii_punct
+    states: ["句讀", "符號"]

engine

Rime 的核心原理是通过 enagine 下的 4 大组件对用户输入进行处理,4 大组件分别是:

  • Processors
  • Segmentors
  • Translators
  • Filters

整个流程为:

  1. Processors 下的各个 processor 对用户的输入(即按下键盘的哪一个键)依次进行处理,将按键按照预设的规则对按键进行响应
    • 不处理:Rime 不对该按键做任何响应,使用系统默认操作
    • 特殊操作:比如 Enter 上屏,切换输入方案、组合键等
    • 输入候选:该按键是需要转换为文字的按键,比如 123abc,将该按键字符存入【输入码】上下文
  2. 当【输入码】上下文改变时,Segmentors 下的 segmentor 会将当前输入码根据格式分段,各自打上标签。比如【朙月拼音】中,输入码 2012nian\,划分为三个编码段:2012(贴 number 标签)、nian(贴 abc 标签)、\(贴 punct 标签)。
  3. 顾名思义,Translators 完成由编码到文字的翻译。但有几个要点:
    • 翻译的对象是划分好的一个代码段。
    • 某个 translator 组件往往只翻译具有特定标签的代码段。
    • 翻译的结果可能有多条,每条结果成为一个展现给用户的候选项。
    • 代码段可由几种 translator 分别翻译、翻译结果按一定规则合并成一列候选。
    • 候选项所对应的编码未必是整个代码段。用拼音敲一个词组时,词组后面继续列出单字候选,即是此例。
  4. 翻译完成后,由 Filters 对所有翻译结果进行处理,比如去重

示例:

engine:
+  processors:
+    - ascii_composer
+    - recognizer
+    - key_binder
+    - speller
+    - punctuator
+    - selector
+    - navigator
+    - express_editor
+  segmentors:
+    - ascii_segmentor
+    - matcher
+    - abc_segmentor
+    - punct_segmentor
+    - fallback_segmentor
+  translators:
+    - punct_translator
+    - script_translator
+    - "table_translator@custom_phrase"
+  filters:
+    - "simplifier@emoji_suggestion"
+    - "simplifier@zh_simp"
+    - uniquifier
+    - single_char_filter

Processors

  • ascii_composer:处理英文模式及中英文切换
  • recognizer:与 matcher 搭配,处理符合特定规则的输入码,如网址,反查等
  • key_binder:在特定条件下将按键绑定到其他按键,如重定义逗号,句号为预设翻页,开关快捷键等
    处理某些自定义的组合键
  • speller:拼写处理器,接受字符按键,编辑输入
    处理自定义的键,通常为 26 个英文字母
  • punctuator:句读处理器,将个别字符按键直接映射为标点符号或文字
    处理句读键、数字键
  • selector:选字处理器
    处理数字键、上下方向键、PageUpPageDown
  • navigator:处理输入栏内的光标移动
    处理左右方向键、HomeEnd
  • express_editor:编辑器
    处理空格、回车、回退键

不常用:

  • fluid_editor:句式编辑器,用于以空格断开词,回车上屏的【注音】,【语句流】等输入方案,与 express_editor 互斥,也可以写作 fluency_editor
  • chord_composer:和弦作曲家或曰并击处理器,用于【宫保拼音】等多键并击的输入方案
  • lua_processor:使用 lua 自定义按键,后接 @lua函数名
    • lua 函数名即用户文件夹内 rime.lua 中函数名,参数为(key, env)
recognizer

可配置项:

  • import_preset:从外部文件导入
  • patterns:配合 segmentor 的 prefix 和 suffix 完成段落划分、tag 标记

示例:

recognizer:
+  import_preset: default
+  patterns:
+    code: "[a-zA-Z]+(*$"
+    email: "^[A-Za-z][-_.0-9A-Za-z]*@.*$"
+    html: "^<[a-z]+>$"
+    punct: "^/([a-z]+|[0-9]0?)$"
+    uppercase: "[A-Z][-_+.'0-9A-Za-z]*$"
+    url: "^(www[.]|https?:|ftp[.:]|mailto:|file:).*$|^[a-z]{1,10}[.:_-].*$"
key_binder

可配置项:

  • import_preset:从外部文件导入
  • bindings:设置组合键的映射
    每一条 binding 中包含字段:
    • accept:键盘输入的按键
    • when:作用条件
    • send:实际调用的按键
    • toggle:切换开关,和 send 不共存

acceptsend 作用的键,需要输入 Ibus 风格的键名:

Mac 只支持 Alt/Option 键,不支持Command键,详见

Shift_L		左Shift
+Shift_R		右Shift
+Control_L	左Ctrl
+Control_R	右Ctrl
+Alt_L		左Alt
+Alt_R		右Alt
+# 在 windows 是 windows 键
+Meta_L		左Meta
+Meta_R		右Meta
+Super_L		左Super
+Super_R		右Super
+Hyper_L		左Hyper
+Hyper_R		右Hyper
+
+BackSpace	退格
+Tab			制表符
+Caps_Lock	大写键
+Linefeed	换行
+Clear		清除
+Return		回车
+Pause		暂停
+Escape		Esc退出
+Delete		刪除
+Home		Home
+Left		左箭头
+Up			上箭头
+Right		右箭头
+Down		下箭头
+Prior		上翻
+Page_Up		上翻
+Next		下翻
+Page_Down	下翻
+space 		空格
+Sys_Req	
+End			末位
+Begin		始位
+Shift_Lock	上檔鎖
+Scroll_Lock	滚动锁
+Num_Lock	小键盘锁
+Select		选择
+Print		打印
+Execute		执行
+Insert		插入
+Undo		还原
+Redo		重做
+Menu		菜单
+Find		寻找
+Cancel		取消
+Help		帮助
+Break		中断
+
+grave			`
+asciitilde		~
+exclam			!
+at				@
+numbersign		#
+dollar			$
+percent			%
+ampersand		&
+asciicircum		^
+asterisk		*
+parenleft		(
+parenright		)
+underscore		_
+minus			-
+plus			+
+equal			=
+bracketleft		[
+bracketright	]
+braceleft		{
+braceright		}
+bar				|
+slash			/
+backslash		\
+semicolon		;
+colon			:
+apostrophe		'
+quotedbl		"
+comma			,
+period			.
+less			<
+greater			>
+question		?
+
+KP_Space		小键盘空格
+KP_Tab			小键盘制表符
+KP_Enter		小键盘回车
+KP_Delete		小键盘刪除
+KP_Home			小键盘原位
+KP_Left			小键盘左箭头
+KP_Up			小键盘上箭头
+KP_Right		小键盘右箭头
+KP_Down			小键盘下箭头
+KP_Prior		小键盘上翻
+KP_Page_Up		小键盘上翻
+KP_Next			小键盘下翻
+KP_Page_Down	小键盘下翻
+KP_End			小键盘末位
+KP_Begin		小键盘始位
+KP_Insert		小键盘插入
+KP_Equal		小键盘等于
+KP_Multiply		小键盘乘号
+KP_Add			小键盘加号
+KP_Subtract		小键盘減号
+KP_Divide		小键盘除号
+KP_Decimal		小键盘小数点
+KP_0			小键盘0
+KP_1			小键盘1
+KP_2			小键盘2
+KP_3			小键盘3
+KP_4			小键盘4
+KP_5			小键盘5
+KP_6			小键盘6
+KP_7			小键盘7
+KP_8			小键盘8
+KP_9			小键盘9

示例:

key_binder:
+  bindings:
+    - {accept: minus, send: Page_Up, when: paging}
+    - {accept: equal, send: Page_Down, when: has_menu}
+    - {accept: bracketleft, send: Page_Up, when: paging}
+    - {accept: bracketright, send: Page_Down, when: has_menu}
+    - {accept: 9, send: Page_Up, when: paging}
+    - {accept: 0, send: Page_Down, when: has_menu}
+    - {accept: semicolon, send: 2, when: has_menu}
+    - {accept: apostrophe, send: 3, when: has_menu}
speller

可配置项:

  • alphabet:设置本 speller 需要监听的键
  • initials:设置哪些键仅在开头的时候才需要监听
  • finals:设置哪些键仅在末尾的时候才需要监听
  • delimiter:分词符
  • algebra:Rime 核心的拼写运算规则,所有 algebra 算出的规则最后写入 prism
  • max_code_length:行码最大码長,超过则自动顶字上屏〔number
  • auto_select:是否开启自动上屏〔truefalse
  • auto_select_pattern:自动上屏规则(正则),当输入码匹配正则时自动顶字上屏。
  • use_space:空格是否可作为输入码〔truefalse
punctuator

可配置项:

  • import_preset:从外部文件导入
  • half_shape:半角模式下的句读映射
    每条选项可以设置上屏模式
    • 默认:选项模式
    • commit:直接上屏
    • pair:交替上屏
  • full_shape:全角模式下的句读映射
    half_shape 可设置上屏模式
  • use_space:是否使用空格顶字〔truefalse

示例:

punctuator:
+  # 标点及特殊表情,引入 symbols 文件
+  import_preset: symbols
+  # 覆盖 symbols 文件对应 key
+  symbols:
+    "/dn": [,,,,,,,,, ↩︎,,,,,,,,,]
+    "/fh": [ ©, ®,,,,,,,,,,,,,, ☑︎,,,,,,,,,,,,]
+    "/xh": [, ×,,,,,,,,,,,,]
+  half_shape:
+    "`": "·"
+    "~": "~"
+    "@": "@"
+    "#": "#"
+    "$": ["¥", "$", "€", "£", "¢", "¤"]
+    "%": "%"
+    "^": "……"
+    "*": ["*", "×", "·", "・", "※", "*", "❂"]
+    "_": "——"
+    "=": "="
+    '\': "、"
+    "'":
+      pair:
+        - "‘"
+        - "’"
+    "|": "|"
+    "(": "("
+    ")": ")"
+    "[": "【"
+    "]": "】"
+    "{": "「"
+    "}": "」"
+    "<": "《"
+    ">": "》"
+    "/": ["/", "÷"]

Segmentors

  • ascii_segmentor:标识英文段落〔例如在英文模式下〕字母直接上屛
  • matcher:配合 recognizer 标识符合特定规则的段落,如网址,反查等,加上特定 tag
  • abc_segmentor:标识常规的文字段落,加上 abc 这个默认tag
  • punct_segmentor:标识句读段落〔键入标点符号用〕加上 punct 这个tag
  • fallback_segmentor:标识其他未标识段落
  • affix_segmentor:用户自定义 tag
    • 可加载多个实例,后接 @tag名

不常用:

  • lua_segmentor 使用lua自定义切分,后接 @lua函数名

Translators

  • table_translator:编码表翻译器,用于仓颉,五笔等基于编码表的输入方案
    • 可加载多个实例,后接 @翻译器名〔如:cangjie,wubi等〕
  • script_translator:脚本翻译器,用于拼音,粤拼等基于音节表的输入方案
    • 可加载多个实例,后接 @翻译器名〔如:pinyin,jyutping等〕
  • punct_translator:配合 punct_segmentor 转换标点符号
  • echo_translator:没有其他候选字时,显示输入码〔输入码可以 Shift+Enter 上屛〕

不常用:

  • reverse_lookup_translator:反查翻译器,用另一种种编码方案查码
  • lua_translator:使用 lua 自定义输入,例如动态输入当前日期,时间,后接 @lua函数名
    • lua 函数名即用户文件夹内 rime.lua 中函数名,参数为(input, seg, env)
    • 可以 env.engine.context:get_option(“option_name”)方式绑定到 switch 开关/key_binder 快捷键
translator

每个输入方案有一个关于 translator 的全局设置,可设置项:

  • dictionary:翻译器使用的字典名

  • prism:设定此翻译器的 speller 生成的棱镜文件名,或此副编译器调用的棱镜名

  • user_dict:设定用户词典名

  • db_class:设定用户词典类型,可设 tabledb〔文本〕或 userdb〔二进制〕

  • preedit_format:上屛码自定义

  • comment_format:提示码自定义

  • initial_quality:设定此翻译器结果优先级

  • disable_user_dict_for_patterns:禁止某些编码录入用户词典〔truefalse

  • enable_sentence:是否开启自动造句〔truefalse

  • enable_user_dict:是否开启用户词典〔用户词典记录动态字词频,用户词〕〔truefalse

table_translator 生效:

  • enable_charset_filter:是否开启字符集过滤〔cjk_minifier 启用后可适用于 script_translator〕〔truefalse
  • enable_encoder:是否开启自动造词〔truefalse
  • encode_commit_history:是否对已上屛词自动成词〔truefalse
  • max_phrase_length:最大自动成词词长〔number
  • enable_completion:提前显示尚未输入完整码的字〔truefalse
  • sentence_over_completion:在无全码对应字而仅有逐键提示时也开启智能组句〔truefalse
  • strict_spelling:配合 speller 中的 fuzz 规则,仅以畧拼码组词〔truefalse

script_translator 生效

  • spelling_hints:设定多少字以内部预定标注完整带调拼音

除了 translator 项设置,其他通过 @ 定义的副翻译器还可单独设置:

  • tag:设定此翻译器针对的 tag。默认 abc
  • prefix:设定此翻译器的前缀标识,默认无
  • suffix:设定此翻译器的后缀标识,默认无
  • tips:设定此翻译器的输入前提示符,默认无
  • closing_tips:设定此翻译器的输入结束提示符,默认无

示例:

# tarnslator
+translator:
+  dictionary: luna_pinyin
+  prism: luna_pinyin_simp
+  preedit_format:
+    - xform/([nl])v/$1ü/
+    - xform/([nl])ue/$1üe/
+    - xform/([jqxy])v/$1u/
+
+# 副翻译器设置
+custom_phrase: # 這是一個 table_translator
+  dictionary: ""
+  user_dict: custom_phrase
+  db_class: tabledb
+  enable_sentence: false
+  enable_completion: false
+  initial_quality: 1
词典与码表

translator 通过词典来翻译对应的片段,通常命名为 <词典名>.dict.yaml

词典

  • name:词典名,内部使用,可以与配套的输入方案名一致,也可不同;
  • version:管理词典的版本;
  • sort:词条初始排序方式,〔by_weight(按词频高低排序)或 original(保持原码表中的顺序)〕;
  • use_preset_vocabulary:选择是否导入默认词汇表【八股文】〔truefalse〕。

示例:

# 这里以 --- ... 分別标记出 YAML 文件的起始与结束位置
+---
+name: luna_pinyin
+version: "0.9"
+sort: by_weight
+use_preset_vocabulary: true
+# 在 ... 标记之后的部分就不会作为 YAML 文件來解析
+...

码表,定义编码与文字的映射关系,通过制表符分割为三列:

  • 文字
  • 编码,如果该编码有多个音节,各音节以空格分开
  • 权重,相同编码时出现在候选列表前面的几率

示例:

你	ni
+我	wo
+的	de	99%
+的	di	1%
+地	de	10%
+地	di	90%
+目	mu
+好	hao
+
+你我
+你的
+我的
+我的天
+天地	tian di
+好天
+好好地
+目的	mu di
+目的地	mu di di

Filters

  • simplifier:简繁转换,表情转换等
    • 可加载多个实例,后接 @d转化器名〔如:zh_simp、emoji_suggestion 等〕
  • uniquifier:过滤重复的候选字,有可能来自 simplifier
  • reverse_lookup_filter:反查滤镜,以更灵活的方式反查,Rime1.0 后替代 reverse_lookup_translator
    • 可加载多个实例,后接 @滤镜名〔如:pinyin_lookup,jyutping_lookup 等〕
  • charset_filter 字符集过滤
    • 后接 @字符集名〔如:utf-8(无过滤),big5,big5hkscs,gbk,gb2312〕
  • lua_filter:使用 lua 自定义过滤,例如过滤字符集,调整排序,后接 @lua函数名
    • lua函数名即用户文件夹内rime.lua中函数名,参数为(input, env)
    • 可以env.engine.context:get_option(“option_name”)方式绑定到switch开关/key_binder快捷键

script_translator

  • cjk_minifier:字符集过滤,使之支持 extended_charset 开关

table_translator

  • single_char_filter:单字过滤器,如加载此组件,则屛敝词典中的词组

使用小鹤双拼

小鹤双拼原始配置文件:double_pinyin_flypy.schema.yaml

我的小鹤双拼配置 double_pinyin_flypy.schema.yaml

Mac 上如何使用 Squirrel

在 mac 上通常使用 homebrew 的方式安装 app

brew cask install squirrel

如果没有 homebrew,点击这里

Squirrel 设置项

Squirrel 配置项基于 0.14.0 整理

我的 suirrel 配置 squirrel.custom.yaml

特定程序操作 options

可配置项:

  • ascii_mode:是否使用英文〔truefalse
  • vim_modeconfig_version 0.34.0 启用,是否支持使用 ESC 键退出编辑模式并切换为英文〔truefalse

示例:

options:
+  com.apple.Xcode:
+    ascii_mode: true

可视化的自定义皮肤

可视化编辑器

Android 上如何使用 Trime

正式版,点击下载

测试版,点击下载

我的 Trime 同文配置 jiz4oh.trime.yaml

基础设置

  1. 启用输入法
    20201004221229

  2. 将用户文件夹指向自定义配置文件所在位置
    20201004221441

  3. 部署
    20201004221538

  4. 使用自定义主题
    20201004222056

其他设置

telegram-cloud-photo-size-5-6327933625852603060-y

Rime 使用心得

重点(必看!)

  1. 很多人修改了方案不生效,或者新安装了 Rime,却发现无法使用,一个很重要但很容易被忽视的解决方案是:

    修改方案后,一定要点击部署!!!
    修改方案后,一定要点击部署!!!
    修改方案后,一定要点击部署!!!

    如何部署?点击这里

    • 【小狼毫】从开始菜单选择「重新部署」;或当开启托盘图标时,在托盘图标上右键选择「重新布署」;
    • 【鼠须管】在系统语言文字选单中选择「重新布署」;
    • 【中州韵】点击输入法状态栏(或IBus菜单)上的⟲ (Deploy) 按钮
  2. 修改方案时,强烈建议使用 <输入方案名>.custom.yaml 的方式进行,除非这个方案是你自己原创!

  3. 使用 <输入方案名>.custom.yaml 修改方案时,一定要写 patch

如何切换 Rime 输入方案

比如如果需要更换输入方案-小鹤双拼,那就需要创建 default.custom.yaml 文件,写入以下内容

# patch 一定要写
+patch:
+  schema_list:
+    # 默认的明月拼音
+    - schema: luna_pinyin_simp
+    # 额外添加的小鹤双拼,前提是需要存在 double_pinyin_flypy.schema.yaml 文件,不然的话会导致无法输入字符
+    - schema: double_pinyin_flypy

如何修改 Rime 输入方案的配置

  1. 如果我们对默认的明月拼音方案的一些设置不满意,我们需要创建一个 luna_pinyin_simp.custom.yaml 文件。

    Q:为什么我要用 luna_pinyin_simp.custom.yaml,而不是 luna_pinyin_simp.schema.yaml,我听说 <输入方案名>.schema.yaml 才是输入方案的正确命名。
    A:<输入方案名>.schema.yaml 确实是输入方案的正确命名方式。
    A:我们使用 <输入方案名>.custom.yaml 是因为原始的 <输入方案名>.schema.yaml 方案(比如 luna_pinyin_simp.schema.yaml)是由他人所编写
    A:我们不使用 <输入方案名>.custom.yaml 而是直接修改 <输入方案名>.schema.yaml 文件进行自定义,如果作者对其进行了一些更新,我们会出现两种情况:

    1. 跟随原作者更新了 <输入方案名>.schema.yaml 文件,我们之前所做的自定义全部白费
    2. 不跟随更新,失去原作者新增或修复的功能
  2. luna_pinyin_simp.custom.yaml 中修改

    # 重点!custom.yaml 必须写 patch,且在第一行并只有一个
    +patch:
    +  # 需要覆写的设置项
    +  xxxx: xxxx

一个高度定制化的 Rime 配置文件结构示例:

20201003122209

  • opencc/*OpenCC字形转换配置及字典文件,简繁转换,emoji 转换等
  • custom_phrase.txt:自定义的词典
  • default.custom.yaml:自定义的全局设置
  • double_pinyin_flypy.custom.yaml:自定义的小鹤双拼输入方案设置
  • double_pinyin_flypy.schema.yaml:从网上下载的小鹤双拼原始输入方案
  • luna_pinyin_simp.custom.yaml:自定义的明月拼音输入方案设置
  • pinyin_simp.dict.yaml:网上下载的默认词库文件
  • squirrel.custom.yaml:自定义的 Squirrel 设置
  • symbols.yaml:额外的关于表情符号的输入配置

如何删除自造词

  1. 打错字后,立刻删掉是不会录入词库的。Rime 是在有新词输入时才把之前的词录入词库
  2. 选中已造词,使用 Shift + Delete 即可删除
  3. mbp 因为移除了 Delete 键,使用 Shift + Fn + Backspaces 键删除

如何同步 Rime 词库

同步原理

  1. 点击同步按钮
  2. installation.yaml 中获取
    • installation_id
    • sync_dir
  3. sync_dir 文件夹下生成 installation_id 文件夹
  4. Rime 会将 用户文件夹 下所有文件写入到步骤 3中文件夹
  5. 根据 <词典名>.userdb/* 下的词典文件,生成一个词典快照文件 <词典名>.userdb.txt
  6. 将快照文件内容与 sync 文件夹下其他文件夹同名快照文件进行对比,更新当前步骤 5中词典文件
  7. 更新后的快照文件放入步骤 3中文件夹

所以重点是设置 installation.yaml 中的 installation_idsync_dir

installation_id 默认为随机生成的 UUID
sync_dir 默认为 用户文件夹 下的 sync 文件夹

示例:

distribution_code_name: Squirrel
+distribution_name: "鼠鬚管"
+distribution_version: 0.14.0
+install_time: "Tue Apr 28 22:33:50 2020"
+rime_version: 1.5.3
+# 上面几项由 Rime 维护
+
+installation_id: "nuc8_mac"
+sync_dir: "/Users/jiz4oh/OneDrive/RimeSync"

同步进阶

知道原理之后,我们可以通过云盘来同步不同设备的词库及设置

比如使用 OneDrive

  1. 在 OneDrive 中设置 RimeSync 文件夹
  2. 将设备 A 的 sync_dir 指向 OneDrive 下 RimeSync
  3. 将设备 B 的 sync_dir 指向 OneDrive 下 RimeSync

这样就是实现了 A 和 B 的的词库同步

参考

注:本文由 Rime 鼠须管【小鹤双拼】输入方案撰写

  1. RIME 官网

  2. Rime 定製指南

  3. Schema.yaml 詳解

\ No newline at end of file diff --git a/2020/10/install-pyenv-and-pipenv/index.html b/2020/10/install-pyenv-and-pipenv/index.html new file mode 100644 index 00000000..497d5af2 --- /dev/null +++ b/2020/10/install-pyenv-and-pipenv/index.html @@ -0,0 +1,12 @@ +使用 pyenv 和 pipenv 管理糟糕的 python 环境 | Jiz4oh's Life

使用 pyenv 和 pipenv 管理糟糕的 python 环境

前言

经常写 python 的开发一定苦恼过一个问题,那就是如何安装 python 环境。经常遇到如下问题:

  1. 许多系统默认只自带 python2,导致现在很多使用 python3 的程序无法正常运行
  2. python3 各版本间也不是完全兼容,某些第三库只支持特定的 python 版本

这些问题导致了我们经常会在各个版本间进行切换,甚至每个项目的 python 版本都不尽相同。所以我们需要一个环境管理器来帮助我们管理各个版本,这就用到了 pyenv

而当我们项目过多之后,每个项目的依赖包就会有多个版本。这些依赖包的管理就需要用到 pipenv

pyenv

安装

mac 使用 homebrew 安装 pyenv

brew install pyenv

设置环境变量

export PYENV_ROOT="$HOME/.pyenv"
+export PATH="$PYENV_ROOT/bin:$PATH"
+if command -v pyenv 1>/dev/null 2>&1;
+then
+  eval "$(pyenv init -)"
+fi
  • 如果使用的是 bash,则将上述代码粘贴到 ~/.bashrc
    重新加载环境变量,source ~/.bashrc
  • 如果使用的是 zsh,则将上述代码粘贴到 ~/.zshrc
    重新加载环境变量,source ~/.zshrc

安装 python 3.6.6

pyenv install 3.6.6

默认使用 3.6.6

pyenv global 3.6.6

pyenv 常用命令

  • 查看有哪些 Python 版本可以安装

    pyenv install --list
  • 安装某个 Python 版本

    pyenv install 3.6.4
  • 查看当前 Python 版本情况(* 表示系统当前的 Python 版本,system表示系统初始版本)

    pyenv versions
      system
    +* 3.6.6 (set by /Users/jiz4oh/.pyenv/version)
  • 切换 Python 默认版本

    # 切换全局默认版本
    +pyenv global 3.6.6
    +# 切换当前项目默认版本
    +pyenv local 3.6.6
    +# 切换 shell 使用的默认版本
    +pyenv shell 3.6.6
  • 卸载指定 Python 版本

    pyenv uninstall 3.6.6

pipenv

安装

mac 使用 homebrew 安装 pipenv

brew install pipenv

【2020.10.01】homebrew 安装版本为 2018.11.26_3,如果需要安装最新版本需要使用

pip3 install pipenv

参考

Pipenv – 超好用的 Python 包管理工具

\ No newline at end of file diff --git a/2020/10/ruby-in-vscode/index.html b/2020/10/ruby-in-vscode/index.html new file mode 100644 index 00000000..829a1e76 --- /dev/null +++ b/2020/10/ruby-in-vscode/index.html @@ -0,0 +1,31 @@ +在 vscode 配置 ruby 开发环境 | Jiz4oh's Life

在 vscode 配置 ruby 开发环境

前言

常在河边走,哪有不湿鞋 –鲁迅

作为多年使用 JetBrains 盗版软件的不法用户,昨日终于被逮到了,经历各种再破解尝试,均告失败。
只好转战最近广受好评的 vscode,准备接下来试试能不能作为主力开发工具。
本篇记录如何在 vscode 配置开发 ruby 环境

安装 vscode

vscode 的安装就不赘述了,在官网直接下载,傻瓜式安装就 ok 了。

配置 vscode

安装配置扩展 ruby

  1. 这个扩展名称就叫 ruby(作者吕鹏,vscode官方开发人员),在 vscode 应用商店中直接搜索安装就好

  2. 在 vscode 配置文件 settings.json 中添加如下

    "ruby.useBundler": true,
    +"ruby.lint": {
    +  "rubocop": {
    +    "useBundler": true
    +  },
    +},
    +"ruby.format": "rubocop",

安装配置扩展 Ruby Solargraph

  1. 在 vscode 应用商店搜索安装

  2. settings.json 中添加

    "solargraph.useBundler": true,
    +"solargraph.references": true,
    +"solargraph.autoformat": true,
    +"solargraph.formatting": true,

配置运行环境

在项目目录下新建 .vscode/launch.js

{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "Rails server",
+      "type": "Ruby",
+      "request": "launch",
+      "program": "${workspaceRoot}/bin/rails",
+      "args": [
+        "server"
+      ],
+      "env": {
+        "PATH": "xxxx",
+        "GEM_HOME": "xxx",
+        "GEM_PATH": "xxx",
+        "RUBY_VERSION": "xxx"
+      }
+    }
+  ]
+}

env 详细内容请在 shell 使用下列命令输出

printf "\n\"env\": {\n  \"PATH\": \"$PATH\",\n  \"GEM_HOME\": \"$GEM_HOME\",\n  \"GEM_PATH\": \"$GEM_PATH\",\n  \"RUBY_VERSION\": \"$RUBY_VERSION\"\n}\n\n"

安装 gem

gem install ruby-debug-ide
+gem install debase
+gem install solargraph

如何打开 settings.json

20201028113907

\ No newline at end of file diff --git a/2020/10/ruby-include-implementation/index.html b/2020/10/ruby-include-implementation/index.html new file mode 100644 index 00000000..880f9fff --- /dev/null +++ b/2020/10/ruby-include-implementation/index.html @@ -0,0 +1,287 @@ +Ruby 是如何实现多重继承的 | Jiz4oh's Life

Ruby 是如何实现多重继承的

前言

Ruby 用了也接近一年了,有很多黑魔法让我感到非常有意思,刚开始学习的时候也经常让我不知所措。
其中很有意思的一点就是 includeextend 这两个方法来变相实现 多重继承,即代码的复用。但是用归用,却没有细细的研究其中的实现,故此,今天好好的看一看。

include

include 方法在 Ruby 中会被 include 模块的方法添加为当前模块下的实例方法

module Person
+  def name
+    p 'My Name Is Person'
+  end
+end
+
+module User
+  include Person
+end
+
+User.new.name
+# My Name Is Person

这样,Person 就可以被随意引用,使用其中的实例方法 name
然而,这是怎么做到的呢,定义在 Person 中的 name 为什么能被 User 的实例调用呢?

那这就需要看看 include 的源码了

class Module
+  # include(module, ...)    -> self
+  # 
+  # Invokes <code>Module.append_features</code> on each parameter in reverse order.
+  def include(module1, *smth)
+    # This is a stub, used for indexing
+  end
+...

append_features

Ruby 源码由 C 写成,所以我们只能看文档,文档中很清楚的指明了调用 include 方法会触发 append_features 方法,那我们就看看 append_features 的源码

# append_features(mod)   -> mod
+# 
+# When this module is included in another, Ruby calls
+# <code>append_features</code> in this module, passing it the
+# receiving module in _mod_. Ruby's default implementation is
+# to add the constants, methods, and module variables of this module
+# to _mod_ if this module has not already been added to
+# _mod_ or one of its ancestors. See also <code>Module#include</code>.
+def append_features(mod)
+  # This is a stub, used for indexing
+end
  1. 当一个 module 作为 mixininclude 时,会默认调用 append_features
  2. Rubyappend_features 的默认实现是将 mixin 中的 constantsmethodsmodule variables 添加到需要 includemodule 中。
  3. 所以当 append_features 执行后,Person module 下的 name 被添加到了 User

验证

重写 append_features 方法:

module Person
+  extend ActiveSupport::Concern
+
+  def name
+    p 'My Name Is Person'
+  end
+
+  def self.append_features(base)
+  end
+end
+
+User.new.name
+# undefined method `name' for #<User:0x00007f84b918c898> (NoMethodError)

很显然,append_features 被覆盖,导致 name 没有被添加到 User

included:include 的回调

append_features 的实现满足了我们对实例方法的代码重用,但是没有实现对类方法的重用,如果我们想在 include 时同时导入类方法,或者其他操作,我们可以选择重写 append_features。但是在 Ruby 中并不建议直接重写 append_features,而是给我们提供了简单便捷的钩子方法 included

# included(othermod)
+# 
+# Callback invoked whenever the receiver is included in another
+# module or class. This should be used in preference to
+# <tt>Module.append_features</tt> if your code wants to perform some
+# action when a module is included in another.
+# 
+#        module A
+#          def A.included(mod)
+#            puts "#{self} included in #{mod}"
+#          end
+#        end
+#        module Enumerable
+#          include A
+#        end
+#         # => prints "A included in Enumerable"
+def included(othermod)
+# This is a stub, used for indexing
+end

如果我们想要导入类方法,重写刚刚的 Person 类:

module Person
+  # 与 included 做对比
+  def self.run
+    p 'I Can Run'
+  end
+
+  def self.included(base)
+    base.class_eval do
+      def self.eat
+        p 'I Can Eat'
+      end
+    end
+  end
+end
+
+User.eat
+# I Can Eat
+User.run
+# undefined method `run' for User:Class (NoMethodError)

Rails 是如何扩展的

Rails 中有一个非常好用的库 ActiveSupport,对 Ruby 语言的很多特性都实现了扩展,今天要看的就是其中 ActiveSupport::Concern 是如何扩展 include

# 所有注释都被我删除了,有兴趣的请查看源码
+module ActiveSupport
+  module Concern
+    class MultipleIncludedBlocks < StandardError
+      def initialize
+        super "Cannot define multiple 'included' blocks for a Concern"
+      end
+    end
+
+    def self.extended(base)
+      base.instance_variable_set(:@_dependencies, [])
+    end
+
+    def append_features(base)
+      if base.instance_variable_defined?(:@_dependencies)
+        base.instance_variable_get(:@_dependencies) << self
+        false
+      else
+        return false if base < self
+        @_dependencies.each { |dep| base.include(dep) }
+        super
+        base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
+        base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
+      end
+    end
+
+    def included(base = nil, &block)
+      if base.nil?
+        if instance_variable_defined?(:@_included_block)
+          if @_included_block.source_location != block.source_location
+            raise MultipleIncludedBlocks
+          end
+        else
+          @_included_block = block
+        end
+      else
+        super
+      end
+    end
+
+    def class_methods(&class_methods_module_definition)
+      mod = const_defined?(:ClassMethods, false) ?
+        const_get(:ClassMethods) :
+        const_set(:ClassMethods, Module.new)
+
+      mod.module_eval(&class_methods_module_definition)
+    end
+  end
+end

Concern 的源码非常简单,总共就定义了 3 个实例方法,1 个类方法,1 个错误,其中 append_featuresincluded 方法刚刚讲过,我们来看看 Rails 做了什么

included:更优雅的 included

def included(base = nil, &block)
+  if base.nil?
+    if instance_variable_defined?(:@_included_block)
+      if @_included_block.source_location != block.source_location
+        raise MultipleIncludedBlocks
+      end
+    else
+      @_included_block = block
+    end
+  else
+    super
+  end
+end

Rails 对 included 进行的扩展其实很简单

  1. included 继续作为 include 的回调使用时,base 不为 nil,则执行 super
  2. included 作为 mixin 中的类方法被调用,并传入一个 block,则将该 block 存入 @_included_block,并且不允许有多个 @_included_block 存在

这么做的目的是,使 mixin 可以通过 included 方法定义 def self.included,让代码更优雅

比如,如果使用 def slef.included 方法

module Student
+  def self.included(base)
+    base.class_eval do
+      def self.study
+        p 'I Can Study'
+      end
+    end
+  end
+end
+
+class User
+  include Student
+end
+
+User.study
+# I Can Study

而使用 included 方法就可以这样写

require 'active_support/concern'
+
+module Student
+  extend ActiveSupport::Concern
+
+  included do
+    def self.study
+      p 'I Can Study'
+    end
+  end
+end
+
+class User
+  include Student
+end
+
+User.study
+# I Can Study

class_methods:更简洁的类方法定义

在上面我们看见了 included 是如何更优雅的实现类方法的定义的,而 Rails 还顺便提供了一个更简单的 magic 方法 class_methods

使用:

require 'active_support/concern'
+
+module Foo
+  extend Concern
+
+  class_methods do
+    def run
+      'I Can Run'
+    end
+  end
+end
+
+class Bar
+  include Foo
+end
+
+p Bar.run
+# I Can Run

源码:

def class_methods(&class_methods_module_definition)
+  mod = const_defined?(:ClassMethods, false) ?
+    const_get(:ClassMethods) :
+    const_set(:ClassMethods, Module.new)
+
+  mod.module_eval(&class_methods_module_definition)
+end

class_methods 的实现比 included 更简单,就是将 block 转换成一个新的 module ClassMethods,而如何注入则依赖于 append_features 来实现

append_features:解决内部依赖

Rails 使用 append_features 解决了一个非常重要的问题,那就是内部依赖的引入

module Foo
+  def self.included(base)
+    base.class_eval do
+      def self.test
+        'test'
+      end
+    end
+  end
+end
+
+module Bar
+  def self.included(base)
+    base.class_eval do
+      p test
+    end
+  end
+end
+
+class User
+  include Foo
+  include Bar
+end

如果 A mixin 依赖于 B mixinincluded 定义的方法,那引入的时候就必须同时引入 AB,这就要求开发者必须对每一个 mixin 的引入都要非常熟悉才能保证不出错。
ActiveSupport::Concern 通过 append_features 解决了这个问题

require 'active_support/concern'
+
+module Foo
+  extend ActiveSupport::Concern
+
+  def self.included(base)
+    base.class_eval do
+      def self.test
+        'test'
+      end
+    end
+  end
+end
+
+module Bar
+  extend ActiveSupport::Concern
+  include Foo
+
+  def self.included(base)
+    base.class_eval do
+      p test
+    end
+  end
+end
+
+class User
+  include Bar
+end
+
+# test

像这样的话,开发者只需要引入 Bar,而不再需要管 Barinclude 了几个模块。

Rails 中是如何这么聪明的实现的呢,源码如下:

# 1. Foo 和 Bar 使用 extend ActiveSupport::Concern 时,定义实例变量 @_dependencies = []
+def self.extended(base) #:nodoc:
+  base.instance_variable_set(:@_dependencies, [])
+end
+
+def append_features(base)
+  if base.instance_variable_defined?(:@_dependencies)
+    # 2. 当 Bar include Foo 时触发 Foo 的 append_features,发现 Bar 已定义实例变量 @_dependencies
+    # 3. 将当前模块 Foo 添加入 Bar 的 @_dependencies
+    base.instance_variable_get(:@_dependencies) << self
+    false
+  else
+    return false if base < self
+    # 4. 当 User include Bar 时 @_dependencies 中的所有模块(Foo)被 User 同时 include
+    @_dependencies.each { |dep| base.include(dep) }
+    super
+    # 配合 class_methods 方法实现类方法的注入
+    base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
+    # 配合 included 方法实现 self.included
+    base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
+  end
+end

append_features 除了解决了内部依赖的引入,还实现了两个 magic 方法 includedclass_methods 的注入

extend

extend 方法在 Ruby 中会被 extend 模块的方法添加为当前模块下的类方法

module Person
+  def name
+    "My name is Person"
+  end
+end
+
+class User
+  extend Person
+end
+
+p User.name
+# My name is Person

extended:extend 的回调

extend 之后如果需要执行某些回调,则只需要定义 self.extended

module Person
+  def self.extended(base)
+    p "#{base} extended #{self}"
+  end
+
+  def name
+    "My name is Person"
+  end
+end
+
+class User
+  extend Person
+end
+# User extended Person

参考

Ruby 中一些重要的钩子方法

\ No newline at end of file diff --git a/2020/10/vim-fast-esc/index.html b/2020/10/vim-fast-esc/index.html new file mode 100644 index 00000000..7014a7af --- /dev/null +++ b/2020/10/vim-fast-esc/index.html @@ -0,0 +1,10 @@ +vim 中使用 esc 切换英文输入 | Jiz4oh's Life

vim 中使用 esc 切换英文输入

前言

在 vim 中使用编辑模式进行了中文输入之后,切换到普通模式时,必须手动切换到英文模式才能进行命令输入,不太方便,故在网上找了找如何自动切换中英文的解决方案

rime 用户

方式一

在方案中填入

key_binder/bindings:
+  - { when: always, accept: Escape, toggle: ascii_mode}

这样就可以使用 Esc 键切换为英文模式,但是这样有一个弊端,必须得先按一次 Esc 切换为英文后,再按一次 Esc 切换为普通模式

方式二(推荐)

在发行版方案中填入

app_options:
+  应用:
+    vim_mode: true

示例:

app_options:
+  com.googlecode.iterm2:
+    vim_mode: true

这样在 iterm 中使用 vim 的时候,就可以在编辑模式按一下 Esc 切换为英文并且 vim 切换为普通模式

!截至此时(2020-11-05)官网 suirrel 稳定版 14.0 尚不支持该功能,可下载测试版常鲜
如何查看当前版本是否支持该 feature?
检查 build/squirrel.yaml 文件 config_version 最低需求 0.34

其他输入法用户

因为我是 mac 用户,所以暂时先介绍关于 mac 的设置方案,其他平台的设置方案请查看解决恼人的 vim 中文输入法切换问题

  1. 安装依赖

    git clone https://github.com/myshov/xkbswitch-macosx.git
    +cp xkbswitch-macosx/bin/xkbswitch /usr/local/bin
    +git clone https://github.com/myshov/libxkbswitch-macosx.git
    +cp libxkbswitch-macosx/bin/libxkbswitch.dylib /usr/local/lib/
  2. 安装 vim 插件 vim-xkbswitch
    Vundle 用户添加到 .vimrc

    Plugin 'lyokha/vim-xkbswitch'
    +" 然后执行 :PluginInstall

参考

[提案] app_options 参数设定追加 esc 自动切换 ascii_mode 选项

解决恼人的 vim 中文输入法切换问题

\ No newline at end of file diff --git a/2020/10/webpack-resovle-@-path/index.html b/2020/10/webpack-resovle-@-path/index.html new file mode 100644 index 00000000..435b269e --- /dev/null +++ b/2020/10/webpack-resovle-@-path/index.html @@ -0,0 +1,27 @@ +配置 Webpack 解析 @ 路径 | Jiz4oh's Life

配置 Webpack 解析 @ 路径

前言

Vue-cli 默认配置了一个使用 @ 表示 src 的功能,这个功能的原理是配置 webpack 解析路径,这篇文章来介绍如何配置 Webpack 使其他项目比如 React 也能使用这个功能

配置

webpack 的配置文件中,写入

resolve: {
+    // 自动补全的扩展名
+    extensions: ['.js', '.vue', '.json'],
+    // 默认路径代理
+    // 例如 import Vue from 'vue',会自动到 'vue/dist/vue.common.js'中寻找
+    alias: {
+        '@': resolve('src'),
+        // '@': paths.appSrc, // react
+        '@config': resolve('config'),
+        'vue$': 'vue/dist/vue.common.js'
+    }
+}
+

IDEA 识别

IDEA 中无法正确解析 @ 代表的路径,导致经常提醒 Module is not installed

20201015121929

解决方案

项目根目录新建 jsconfig.json 文件即可

{
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    }
+  },
+  "exclude": [
+    "node_modules",
+    "dist",
+    "build"
+  ]
+}
\ No newline at end of file diff --git a/2020/11/hexo-next-valine/index.html b/2020/11/hexo-next-valine/index.html new file mode 100644 index 00000000..7fe027b2 --- /dev/null +++ b/2020/11/hexo-next-valine/index.html @@ -0,0 +1,15 @@ +使用 Valine 开启 Hexo 评论系统 | Jiz4oh's Life

使用 Valine 开启 Hexo 评论系统

关于评论系统

博客搭建好之后,由于当时已经晚上12点了,并没有一鼓作气的开启评论系统,就一直拖到了现在。。。

今天终于想起来开启评论系统,在网上搜索了一下 hexo 的评论系统,有以下几个:

  • gitment:基于 GitHub Issue 作为评论存储仓库,但是一直长时间无人维护
  • gitalk:同 gitment 使用 Github Issue,且页面比 gitment 更精美
  • disqus:界面好看,支持 Facebook、twitter、Google+ 等账号登陆,但是 需要科学上网
  • valine:基于 LeanCloud 的无后端评论系统,支持匿名评论
  • 来必力:韩国的评论系统,功能强大,支持多社交帐号登录,链接不太稳定,不支持匿名评论

对于我来讲,我更偏向于使用 gitalk 作为评论系统,省心~但是 gitalk 类的评论系统被反馈有安全风险,虽然是 GitHub 的锅,但是会对访客(还不是我。。。)的账户安全带来很大的不确定性,所以最终我还是选择了支持匿名评论的 Valine

注册 LeanCloud

Valine 是基于 LeanCloud 进行使用的,所以我们需要在 LeanCloud 进行注册

因为现在 LeanCloud 国内版要求备案域名,所以使用国际版

配置 LeanCloud

  • 创建应用:
    20201121111644
  • 切换到存储页面:
    20201121111935
  • 【结构化数据】-【创建 class】
    20201121112126
  • 修改 ACL 的 write 权限,设置成下图效果
    20201121112221
  • 切换到【设置】
  • 【安全中心】-【Web 安全域名】,输入博客会用到的域名,如 http://jiz4oh.comhttps://jiz4oh.com
    20201121112458
  • 【应用 Keys】-【AppID】
    【应用 Keys】-【AppKey】
    这两个数据在下一步中使用(可点击后面的按钮快速复制)
    20201121112803

配置 next

  • 打开 next 的配置文件,如我是 _confg.next.yml,将下列代码复制进去,注意缩进

    valine:
    +  enable: true
    +  appId: # 拷贝上一步的 ID
    +  appKey: # 拷贝上一步的 Key
    +  placeholder: # Comment box placeholder
    +  avatar: mm # Gravatar style
    +  meta: [nick, mail, link] # Custom comment header
    +  pageSize: 10 # Pagination size
    +  lang: # Language, available values: en, zh-cn
    +  visitor: false # Article reading statistic
    +  comment_count: true # If false, comment count will only be displayed in post page, not in home page
    +  recordIP: false # Whether to record the commenter IP
    +  serverURLs: # When the custom domain name is enabled, fill it in here (it will be detected automatically by default, no need to fill in)
    +  enableQQ: false # Whether to enable the Nickname box to automatically get QQ Nickname and QQ Avatar
    +  requiredFields: [] # Set required fields: [nick] | [nick, mail]
\ No newline at end of file diff --git a/2020/11/http-cache/index.html b/2020/11/http-cache/index.html new file mode 100644 index 00000000..94f37e35 --- /dev/null +++ b/2020/11/http-cache/index.html @@ -0,0 +1 @@ +HTTP 中的缓存机制 | Jiz4oh's Life

HTTP 中的缓存机制

前言

最近在思考如何优化 web 应用的响应速度,常见的方式就是使用缓存,而最常接触的技术其实是服务端做共享缓存,例如 redis 缓存热点数据、CDN 缓存静态资源。在客户端做缓存这一块,知识面还比较窄,经过资料查询和思考,总结一些心得

缓存的目的

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术

在不考虑成本的情况下,不使用缓存其实是体验最好方式,可以通过无限堆服务器来达到快速响应的目的,并且不会有一致性等问题。

所以缓存的目的很简单,就是缓解服务器压力,来达到在有限的成本情况下相对最优质的用户体验。

私有缓存和共享缓存

缓存的种类大致可分为:

  • 共享缓存:可被多个用户重复使用,例如 redis 缓存热点数据,CDN 缓存静态资源
  • 私有缓存:只能被单个用户使用,例如浏览器缓存

缓存控制

HTTP header: Cache-control

HTTP/1.1 中引入 Cache-control 头用来定义浏览器应该如何进行缓存决策

Cache-control 的值有:

  • no-store:客户端不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容
  • no-cache:客户端会缓存响应内容,但每次请求都会携带 验证字段 到服务器,验证缓存是否过期,未过期才使用本地已缓存内容
  • public:响应内容可以被中间人如 CDN、网关等缓存
  • private:响应内容只能被客户端缓存
  • max-age:缓存过期的相对时间(秒),超过该时间后会再次发起请求
  • must-revalidate:本地缓存未过期时使用本地缓存,本地缓存过期时必须携带 验证字段 到服务器,验证缓存是否过期

HTTP header: Expries

ExpriesHTTP/1.0 中的内容,作用是指定缓存过期的绝对时间,例如 Expires: Wed, 21 Oct 2015 07:28:00 GMT

Cache-control: max-age 的区别:

  • Expries 是绝对时间,Cache-control 是相对时间
  • HTTP/1.0Expries 优先级大于 Cache-control,在 HTTP/1.1 及以后 Cache-control 优先级大于 Expries

缓存决策

强制缓存

Cache-control: max-ageExpires 指定的缓存,在时间过期之前,浏览器不会再次请求服务器

协商缓存

当缓存失效时,会发出 条件式请求,这时候和服务器的通信就称为协商缓存

协商缓存的优点:

  • 省去了服务器生成 html 的时间
  • 省去了响应体内容,传输更快

Last-Modified

请求响应流程:

  1. 当浏览器第一次请求服务器时,服务器会返回 Last-Modified 头部表明当前资源的最后修改日期
  2. 浏览器会将这个日期值缓存在本地
  3. 当需要发出 条件请求 时,将日期值放在 If-Modified-SinceIf-Unmodified-Since 头部中向服务器发出
  4. 服务器判断日期,决定响应内容
  5. 如果该资源新鲜,则返回 304,客户端使用该缓存渲染内容
  6. 如果该资源被更改过,则返回 200,及全新内容

Etag

Last-Modified 的请求响应流程类似,不同的是服务器响应的是 ETag 头,浏览器发出请求时的头部为 If-MatchIf-None-Match

  • 强验证:HTTP 协议默认使用强验证,强验证要求文档的每一个字节都必须相同
    ETag: "618bbc92e2d35ea1945008b42799b0e7"
  • 弱验证:ETag 头以 W/ 开头时为弱验证,此时只要求文档的内容含义是相同的。
    ETag: W/"618bbc92e2d35ea1945008b42799b0e7"

因为存在当资源时间更改,但是内容未改变等情况,Last-Modified 对资源的判断没有 ETag 准确,所以服务端处理的时候通常优先判断 ETag

决策图

决策流程图

刷新页面对 http 缓存的影响

三种刷新操作:

  • 正常操作:地址栏输入 url,跳转链接,前进后退等
  • 手动刷新:F5(mac 为 Cmd + R),点击刷新按钮,点击菜单刷新
  • 强制刷新:Ctrl + F5(mac 为 Cmd + Shift + R)

不同刷新操作,不同的缓存策略:

  • 正常操作:强制缓存有效,协商缓存有效
  • 手动刷新:强制缓存失效,协商缓存有效
  • 强制刷新:强制缓存失效,协商缓存失效

参考

HTTP 缓存

浅谈 HTTP 缓存

\ No newline at end of file diff --git a/2020/11/receiver-and-ancestors/index.html b/2020/11/receiver-and-ancestors/index.html new file mode 100644 index 00000000..386c10cf --- /dev/null +++ b/2020/11/receiver-and-ancestors/index.html @@ -0,0 +1,82 @@ +Ruby 是如何调用方法的 | Jiz4oh's Life

Ruby 是如何调用方法的

前言

当一个方法被调用时,要做的事情其实只有两件,1. 找到它。2. 调用它

接收者 receiver

接收者就是调用方法所在的对象。比如 'str'.to_sym 语句中,'str' 就是接收者。可以形象的理解为向这个接收者 'str' 发送了一条 to_sym 的消息

上面是显式指定接收者的例子,而在 Ruby 中是可以不指定接收者的,像这样:

class MyClass
+  def my_method
+    test
+  end
+
+  def test
+    p "I'm Test"
+  end
+end
+
+MyClass.new.my_method
+# I'm Test

当一个字符串被调用时,Ruby 首先会在当前作用域查找是否有这个字符串对应的局部变量,如果没有时就会在 self 这个默认的接收者上调用方法

找到它:祖先链 ancestors

在面向对象的语言中,继承是一个很常见的概念,比如 Python 支持多继承,通过继承多个父类来复用代码,当方法被调用时,首先查找该对象是否有该方法,如果没有,则查找「方法解析顺序」(Method Resolution Order,或 MRO),而 MRO 通过 C3算法(python3)来计算,简单来说就是广度优先

而在 Ruby 中是不支持多继承的,转而使用 mixinx 的方式来实现代码的复用,mro 是通过搜索祖先链一直向上查找。当 Ruby 调用一个方法时,会首先查找对象是否有该方法,如果没有的话,则向上搜索整个祖先链,这就是 one step to the right, then up

class M1;end
+class M2 < M1;end
+
+# 查看祖先链
+M2.ancestors  # [M2, M1, Object, Kernel, BasicObject]

include

include 是实现 mixinx 最常见的方式,当模块 A 被包含在类(或者模块)B 中时,这个 A 在 B 的祖先链的位置就在 B 之上,例:

module M1;end
+module M2;end
+module M3
+ include M1
+ include M2
+end
+
+M3.ancestors  # [M3, M2, M1]

prepend

prepend 方法类似于 include,不过这个方法会将模块插入祖先链的下方,例:

module M1;end
+module M2;end
+module M3
+ prepend M1
+ prepend M2
+end
+
+M3.ancestors  # [M2, M1, M3]

多重包含

当模块 C 包含模块 A、B,模块 B 包含模块 A 时,A 被重复导入,Ruby 会忽略已经被加入祖先链的重复模块导入

module A;end
+module B
+  include A
+end
+module C
+  prepend A
+  include B
+end
+
+B.ancestors   # [B, A]
+C.ancestors   # [A, C, B]

更复杂的包含:

module M1;end
+module M2;end
+module M3;end
+module M4
+  include M1
+  include M2
+  prepend M3
+end
+
+module M5
+  prepend M1
+  include M2
+  include M4
+end
+
+M4.ancestors  # [M3, M4, M2, M1]
+M5.ancestors  # [M1, M5, M3, M4, M2]

调用它

刚刚我们已经通过祖先链找到了方法,接下来就要执行这个方法。假如有以下方法:

def my_method
+  temp = 1
+  my_other_method(temp)
+end

当执行 my_method 方法时,方法内部需要调用 my_other_method,而该由哪个对象来调用这个方法?

self 关键字

Ruby 中的每一行代码都会在一个对象中被执行–这个对象就是所谓的当前对象。

当前对象可以用 self 关键字来指代,而所有没有明确指明接收者的方法都会在 self 上调用。

class MyClass
+  def testing_self
+    @var = 10
+    my_method()
+    self
+  end
+
+  def my_method
+    @var += 1
+  end
+end
+
+obj = MyClass.new
+obj.testing_self  # <MyClass:0x00007faea4131b90 @var=11>

private 是怎么实现的

private 方法遵从一个简单的规则:

不能明确指定接收者来调用私有方法

class C
+  def public_method
+    self.private_method
+  end
+
+  private def private_method
+    1
+  end
+end
+
+C.new.public_method  # NoMethodError (private method `private_method' called for #<C:0x00007fd0c40feb90>)

总结:

  1. 如果需要调用其他对象的方法,必须显式指定接收者
  2. 私有方法不能明确指定接收者来调用

所以私有方法不能被其他对象调用,不能被自身显式 self 调用,只能隐式 self 调用

参考

Ruby元编程

\ No newline at end of file diff --git a/2020/11/tcp-ip/index.html b/2020/11/tcp-ip/index.html new file mode 100644 index 00000000..bfbbd652 --- /dev/null +++ b/2020/11/tcp-ip/index.html @@ -0,0 +1 @@ +TCP/IP 五层协议 | Jiz4oh's Life

TCP/IP 五层协议

前言

最近在极客时间学习刘超老师的趣谈网络协议,对 TCP/IP 有了更深的了解与感触,记录下学习的心得与体会

什么是 TCP/IP 五层协议

TCP/IP 五层协议是在一定程度上参考了 OSI 7层模型 的体系结构,将 OSI 7层模型简化为了五层(也有将链路层和物理层合并后简化为四层的)

五层协议:应(应表会)传网数物

  • 应用层:HTTP,FTP 等协议
    封装请求正文
  • 传输层:TCP/UDP 协议
    将端口信息(当前应用监听端口、目标应用监听端口)等封装
  • 网络层:ICMP,IP 等协议
    封装 IP 地址(当前主机 ip 地址、目标主机 ip 地址)
  • 数据链路层:以太网等协议
    封装 MAC 地址(当前主机的 MAC 地址、网关的 MAC 地址),通过 ARP 协议在局域网广播找到网关后,通过网关发送出去
  • 物理层:
    将数据转换为二进制,作为电信号发送出去

五层协议中协议有哪些:

2020-09-08-06-50-40

物理层

物理层就是网线,中间传输的是光信号

两台电脑通过网线直接连接,就可以组成一个最小的 局域网,即 LAN,Local Area Network

如果多台电脑需要组成一个局域网,就需要一个设备来控制数据传输的目的地和先后顺序,比如交换机

数据链路层

数据链路层包格式:

2020-09-12-17-16-00

在数据链路层(MAC 层)需要解决 3 个问题:

  1. 数据的先后顺序
    MAC 层全称 Medium Access Control,即媒体访问控制。控制如何进行 多路访问
  2. 数据的目的地
    这就需要一个物理地址,叫作 链路层地址,也即 MAC 地址
  3. 发送错误的处理方式
    对于以太网,第二层的最后面是 CRC,也就是循环冗余检测。通过 XOR异或 的算法,来计算整个包是否在发送的过程中出现了错误

MAC 地址

MAC 地址更像是身份证,是一个唯一的标识。它的唯一性设计是为了组网的时候,不同的网卡放在一个网络里面的时候,可以不用担心冲突。从硬件角度,保证不同的网卡有不同的标识。

MAC 地址的通信范围比较小,局限在一个子网里面。例如,从 192.168.0.2/24 访问 192.168.0.3/24 是可以用 MAC 地址的。一旦跨子网,即从 192.168.0.2/24 到 192.168.1.2/24,MAC 地址就不行了,需要 IP 地址起作用了

交换机

交换机是二层转发设备,主要作用就是获取到网络包之后,检查包的目标 MAC 头,再根据策略进行转发,避免主机每次都进行广播发送网络包

ARP 协议

当局域网中如果有多台机器,知道目标 IP 地址而不知道 MAC 地址时,就需要使用 ARP 协议来获取。

已知 IP 地址,求 MAC 地址的协议

原理是向局域网发出 ARP 协议进行广播,等待目标主机回应

具体询问和回答的报文:

2020-09-12-17-24-39

ARP 表

主机不可能每次都通过 ARP 协议来广播查找 MAC 地址,这样会极大降低效率,所以当主机通过 ARP 协议获取了 MAC 地址之后,就会在本地缓存一个 ARP 表,存储 IP-MAC 的映射。

只有当 ARP 表没有对应的 MAC 地址时,主机才会通过 ARP 协议去获取 MAC 地址并更新 ARP 表

因为主机的 IP 和 MAC 可能会改变,所以 ARP 表是有过期时间的。

RARP 协议

RARP 协议即 Reverse ARP 协议

已知 MAC 地址,求 IP 地址的协议

转发表

当源主机发出了数据报文之后,数据报文其实只知道 MAC 地址,而不知道具体路径,就像你要到一个地方去,但是你不知道如何去一样

当主机通过网线向连接的设备发送请求时

  1. 设备为 hub 集线器:
    hub 集线器是一层设备,只会简单的完全复制,会向所有非源设备广播转发请求,通过其他主机自己来确认数据包是否应该接受
  2. 设备为交换机:
    交换机刚开始也不会知道所有主机的 MAC 地址,但是当 主机 A 发送一个请求到达交换机时,交换机会记录下该请求的主机的 MAC 地址,当有其他请求目标地址为主机 A 时,会只转发给主机A,不再进行广播。
    然后过一段时间之后就会记录下局域网下所有主机的 MAC 地址,这个就是 转发表

因为主机的 MAC 可能会改变,所以转发表是有过期时间的。

拓扑结构

当局域网中主机过多,比如办公室场景,可能有几十几百个网口,这时候一个交换机是不够用的,就需要多台交换机,这时候交换机连接起来就是 拓扑结构

当交换机过多时,不可避免的会出现 环路问题,例如下图:

2020-09-12-17-44-19

  1. 当机器 1 发送 ARP 广播寻找机器 2 的 MAC 地址时,交换机 A 和交换机 B 都会收到这个广播,此时,都会记得 机器 1 是在左边这个网口
  2. 然后都同时向 LAN2 转发这条广播消息,当交换机 A 收到交换机 B 转发的消息时,会发现是机器 1 在寻找机器 2,这时候就会发现机器 1 位于右边这个网口,于是更新自己的记录,将这个消息转发到 LAN1。
  3. 同理,交换机 B 也会更新自己的记录,然后转发消息到 LAN1
  4. 然后这个广播请求会来回转发,就会堵塞网络

这时候需要破除环路

STP 协议

STP:Spanning Tree Protocol,是一个生成树的算法,用来破解环路

2020-09-12-17-52-00

  • Root Bridge,也就是根交换机。这个比较容易理解,可以比喻为“掌门”交换机,是某棵树的老大,是掌门,最大的大哥。

  • Designated Bridges,有的翻译为指定交换机。这个比较难理解,可以想像成一个“小弟”,对于树来说,就是一棵树的树枝。所谓“指定”的意思是,我拜谁做大哥,其他交换机通过这个交换机到达根交换机,也就相当于拜他做了大哥。这里注意是树枝,不是叶子,因为叶子往往是主机。

  • Bridge Protocol Data Units (BPDU) ,网桥协议数据单元。可以比喻为“相互比较实力”的协议。行走江湖,比的就是武功,拼的就是实力。当两个交换机碰见的时候,也就是相连的时候,就需要互相比一比内力了。BPDU只有掌门能发,已经隶属于某个掌门的交换机只能传达掌门的指示。

  • Priority Vector,优先级向量。可以比喻为实力 (值越小越牛)。实力是啥?就是一组ID数目,[Root Bridge ID, Root Path Cost, Bridge ID, and Port ID]。为什么这样设计呢?这是因为要看怎么来比实力。先看Root Bridge ID。拿出老大的ID看看,发现掌门一样,那就是师兄弟;再比Root Path Cost,也即我距离我的老大的距离,也就是拿和掌门关系比,看同一个门派内谁和老大关系铁;最后比Bridge ID,比我自己的ID,拿自己的本事比。

STP的工作过程是怎样的?

  1. 一开始,江湖纷争,异常混乱。大家都觉得自己是掌门,谁也不服谁。于是,所有的交换机都认为自己是掌门,每个网桥都被分配了一个ID。这个ID里有管理员分配的优先级,当然网络管理员知道哪些交换机贵,哪些交换机好,就会给它们分配高的优先级。这种交换机生下来武功就很高,起步就是乔峰

    (图中圆圈内数据代表优先级,线上数字代表传输距离,优先级越小越优先,距离越小越优先)

    2020-09-12-17-54-49

  2. 既然都是掌门,互相都连着网线,就互相发送 BPDU 来比功夫呗。这一比就发现,有人是岳不群,有人是封不平,赢的接着当掌门,输的就只好做小弟了。当掌门的还会继续发 BPDU,而输的人就没有机会了。它们只有在收到掌门发的 BPDU 的时候,转发一下,表示服从命令。

    2020-09-12-17-55-22

    数字表示优先级。就像这个图,5 和 6 碰见了,6 的优先级低,所以乖乖做小弟。于是一个小门派形成,5 是掌门,6 是小弟。其他诸如 1-7、2-8、3-4 这样的小门派,也诞生了。于是江湖出现了很多小的门派,小的门派,接着合并。

    合并的过程根据传输距离会出现以下四种情形

    1. 情形一:掌门遇到掌门
      当 5 碰到了 1,掌门碰见掌门,1 觉得自己是掌门,5 也刚刚跟别人PK完成为掌门。这俩掌门比较功夫,最终 1 胜出。于是输掉的掌门 5 就会率领所有的小弟归顺。结果就是 1 成为大掌门。

      2020-09-12-17-58-41

    2. 情形二:同门相遇
      同门相遇可以是掌门与自己的小弟相遇,这说明存在“环”了。这个小弟已经通过其他门路拜在你门下,结果你还不认识,就 PK 了一把。结果掌门发现这个小弟功夫不错,不应该级别这么低,就把它招到门下亲自带,那这个小弟就相当于升职了。

      我们再来看,假如 1 和 6 相遇。6 原来就拜在 1 的门下,只不过 6 的上司是 5,5 的上司是 1。1 发现,6 距离我才只有 2,比从 5 这里过来的 5(=4+1)近多了,那 6 就直接汇报给我吧。于是,5 和 6 分别汇报给 1。

      2020-09-12-17-59-00

    3. 情形三:掌门与其他帮派小弟相遇
      小弟拿本帮掌门和这个掌门比较,赢了,这个掌门拜入门来。输了,会拜入新掌门,并且逐渐拉拢和自己连接的兄弟,一起弃暗投明

      2020-09-12-18-01-23

    4. 情形四:不同门小弟相遇
      各自拿掌门比较,输了的拜入赢的门派,并且逐渐将与自己连接的兄弟弃暗投明。

      2020-09-12-18-03-23

      例如,5 和 4 相遇。虽然 4 的武功好于 5,但是 5 的掌门是 1,比 4 牛,于是 4 拜入 5 的门派。后来当 3 和 4 相遇的时候,3 发现 4 已经叛变了,4 说我现在老大是 1,比你牛,要不你也来吧,于是 3 也拜入 1。

VLAN

当交换机过多时,虽然 STP 解决了环路问题,避免了广播风暴,但是在同一个局域网中,总有些数据是想在小黑屋里单独交流的,不希望被人给抓包了。

这时候可以有两个解决方案:

  1. 物理隔离
    交换机隔离,不在同一个拓扑网络下。
  2. 虚拟隔离 VLAN,也就是常说的虚拟局域网
    在同一个交换机上形成多个局域网

交换机如何区分机器属于哪个局域网呢?通过 VLAN ID

2020-09-12-18-10-06

VLAN 下的网络结构:

2020-09-12-18-11-16

网络层

ICMP 协议

ICMP 协议介于网络层和传输层之间,普遍认为属于网络层,ICMP 通常用于 ping 检测网络连通

ICMP:Internet Control Message Protocol,就是互联网控制报文协议

2020-09-12-18-25-18

ICMP 报文有很多的类型,不同的类型有不同的代码。最常用的类型是主动请求为 8,主动请求的应答为 0

报文类型,可以大致分为两类:查询报文 和 差错报文

查询报文

ping 就是使用查询报文的一种

  • ping 的主动请求,被称为 ICMP ECHO REQUEST,类型为 8,
  • 主动请求的响应,被称为 ICMP ECHO REPLY,类型为 0

比起原生的 ICMP,这里面多了两个字段,一个是标识符,另一个是序号,在选项数据中,ping 还会存放发送请求的时间,来计算响应时间

ping 的收发过程:

2020-09-12-18-32-03

  1. 源主机首先会构建一个 ICMP 请求数据包,ICMP 数据包内包含多个字段。
  2. 最重要的是两个,第一个是类型字段,对于请求数据包而言该字段为 8;
  3. 另外一个是顺序号,主要用于区分连续ping的时候发出的多个数据包。每发出一个请求数据包,顺序号会自动加 1。
  4. 为了能够计算往返时间 RTT,它会在报文的数据部分插入发送时间

差错报文

差错报文的类型为有:

  • 终点不可达,类型为 3
    • 网络不可达:代码为 0,对方局域网无响应
    • 主机不可达:代码为 1,对方局域网响应了,但是找不到这个 ip 对应的主机
    • 协议不可达:代码为 2,对方主机响应了,但是对方不接受这个协议
    • 端口不可达:代码为 3,对方主机响应了,但是没有程序监听这个端口
    • 需要进行分片但设置了不分片:代码为 4,中间某个网关需要对长度进行限制,但是源主机不允许
  • 源站抑制:类型为 4,需要源主机放慢发送速度
  • 路由重定向:类型为 5,下次发送时发给另一个路由器
  • 时间超时:类型为 11,超过网络包生命周期还是未到达目标

差错报文的结构相对复杂一些。除了前面还是 IP,ICMP 的前 8 字节不变,后面则跟上出错的那个 IP 包的 IP 头和 IP 正文的前8 个字节

Traceroute 使用差错报文:

  1. 故意设置特殊的 TTL,来追踪去往目的地时沿途经过的路由器
  2. 故意设置不分片,从而确定路径的 MTU

TTL:Time To Live。最大值为 255。该字段指定IP包被路由器丢弃之前允许通过的最大网段数量,每个路由器要将 TTL 减1,TTL 通常表示被丢弃前经过的路由器的个数。当 TTL 变为 0 时,该路由器丢弃该包,并发送一个 ICMP 包给最初的发送者

网关

2020-09-13-10-04-28

网关往往是一个路由器,是一个三层(网络层)转发的设备。
当服务器 A 需要访问服务器 B 时:

  1. 如果目标 ip 地址是本网段主机,所以直接发给目标主机,封装目标主机的 MAC 地址
  2. 如果目标 ip 地址是外网主机,则将请求发给网关,封装网关的 MAC 地址

当网关收到了一个请求时

  1. 取下 MAC 头
    1. 如果 MAC 头不是本主机,则通过转发表将请求转发到本网段其他主机上
    2. 如果 MAC 头是本主机,则取下 IP 头,查看目标 IP 地址
      1. 如果是本主机,则由本主机程序处理
      2. 如果不是本主机,则查看路由表,查询应该由哪个 LAN 口处理,并封装好下一跳(可能是对应主机,也可能是网关)的 MAC 地址,这时包的 目的 MAC 地址 变为了路由规则匹配的下一跳的 MAC 地址源 MAC 地址 变为了网关的 MAC 地址

网关分为两种类型:

  • 转发网关:不修改目标 IP 地址和源 IP 地址的网关
  • NAT(Network Address Translation) 网关:修改目标 IP 地址和源 IP 地址的网关

和路由器的关系

很多情况下,人们把网关就叫作路由器。其实不完全准确,而另一种比喻更加恰当:路由器是一台设备,它有五个 LAN 网口或者网卡,相当于有五只手,分别连着五个局域网。每只手的 IP 地址都和局域网的 IP 地址相同的网段,每只手都是它握住的那个局域网的网关

静态路由

当网关收到一个外网的请求时,就会通过 静态路由动态路由 规则修改 MAC 头(和 IP 头),将包发出去

静态路由,其实就是在路由器上,写死配置一条一条规则。
这些规则包括:想访问 BBS 站(它肯定有个网段),从 2 号口出去,下一跳是 IP2;想访问教学视频站(它也有个自己的网段),从 3 号口出去,下一跳是 IP3,然后保存在路由器里

动态路由

网络环境复杂多变时,静态路由手动修改太麻烦,所以需要动态路由通过一定算法进行自动配置

动态路由算法:

  • 链路状态路由(link state routing),基于 Dijkstra 算法
  • 距离矢量路由(distance vector routing),基于 Bellman-Ford 算法,只适用于小型网络

动态路由协议:

  • 基于链路状态路由算法的 OSPF(Open Shortest Path First,开放式最短路径优先),广泛应用在数据中心中的协议。由于主要用在数据中心内部,用于路由决策,因而称为内部网关协议(Interior Gateway Protocol,简称 IGP)
  • 基于距离矢量路由算法的 BGP(Border Gateway Protocol,边界路由协议)
    • eBGP:在多个 AS(独立的内部网络,如家庭网络) 之间进行路由交换的协议
    • iBGP:在多个 edge touter(AS 面向外界的出口路由器) 之间进行路由交换的协议

传输层

UDP 协议

UDP 协议:数据报协议,不需要接收方确认消息。
特点:面向无连接,无状态,不保证不丢失,不保证到达顺序

  • 优点是速度快
  • 缺点是可能会丢包

2020-09-13-18-26-14

应用场景:

  • 流媒体,比如直播
  • 实时游戏
  • loT物联网
  • 移动通信
  • 需要低时延的 http 访问

TCP 协议

TCP 协议:流式 stream 协议,通过双向管道传输数据,发送请求之后必须收到确认消息,安全但效率稍低
特点:面向连接,有状态,保证无差错,无重复,并按序到达

  • 优点是稳定可靠
  • 缺点是速度较慢,需要维护状态机

2020-09-13-18-39-47

  • 建立连接:
    TCP三次握手:

    1. client 发送管道建立请求(syn)给 server。
    2. server 同意建立 client 到 server 管道(ack),及管道建立请求(syn)给 client。
    3. client 同意建立 server 到 client 管道给 server(ack)。TCP 双向管道连接建立成功

    2020-09-13-19-12-26

  • 断开连接:
    TCP四次挥手:

    1. client 发送数据发送完毕并请求断开管道(FIN)给 server。
    2. server 同意断开管道(ack),client 单向断开管道。
    3. 数据接收完毕后,server 发送数据接收完毕并请求断开管道(FIN)给 client。
    4. client 同意断开管道(ack),server 单向断开管道。TCP 双向管道连接断开

    2020-09-13-19-56-04

握手和挥手整体流程状态图:

20200913222940

  • 加粗的实线是客户端 A 的状态变迁,是主要流程
    其中阿拉伯数字的序号,是握手过程中的顺序
    而大写中文数字的序号,是挥手过程中的顺序
  • 加粗的虚线是服务端 B 的状态变迁
  • 点线是非主要流程

TCP 状态机

为了实现 TCP 协议的可靠性,TCP 协议规定两端必须实现一个状态机,对每一个发出包与接受包进行记录

TCP 发送方状态机的数据结构:

  • 第一部分:发送了并且已经确认的。
  • 第二部分:发送了并且尚未确认的。
  • 第三部分:没有发送,但是已经等待发送的。
  • 第四部分:没有发送,并且暂时还不会发送的。
    第三部分和第四部分的区别是,接收方会返回给发送方一个窗口大小,表示接收方处理数据的能力大小,叫 Advertised window,超过这个窗口大小的包,发送方会暂缓发送

20200914080034

  • LastByteAcked:第一部分和第二部分的分界线
  • LastByteSent:第二部分和第三部分的分界线
  • LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线

TCP 接收方状态机的数据结构:

  • 第一部分:接受并且确认过的。
  • 第二部分:还没接收,但是马上就能接收的。
  • 第三部分:还没接收,也没法接收的。

20200914075917

  • LastByteRead:之后是已经接收了,但是还没被应用层读取的;
  • NextByteExpected:是第一部分和第二部分的分界线。
  • MaxRcvBuffer:最大缓存的量;

累计应答

为了保证顺序性,发送方的包都有一个序号,并且按照序号挨个发送。对于接收方,应答却不是挨个应答,会应答一个序号,表示这个序号之前的包都已收到。

确认与重发机制

因为底层 IP 协议的不可靠性,无法保证网络包的不丢失与按序到达,TCP 协议自己实现了确认与重发机制来保证顺序问题和丢包问题

  1. 超时重试:对于每一个发送了,但是没有 ACK 的包,都设有一个超时时间,超过时间则重新尝试。
    • 超时时间:是 TCP 通过采样 RTT 的时间,然后进行加权平均算出的,这个值是不断变化的。这个算法被称为自适应重传算法
      当一个数据包再次超时时,TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送
    • 快速重传:当接收方收到一个序号大于下一个所期望的序号时,就检测到了数据流中的间隔,于是发送三个冗余的 ACK,发送方收到后就会在超时时间之前提前重传
      比如,接收方收到了 6、8、9,发现 7 没来,于是发送三个 6 的 ACK。发送方收到 3 个 ACK 后,就会立刻重传 7 的报文
  2. Selective Acknowledgment(SACK):这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了

流量控制

  1. 在每一个包的确认中,都会返回一个 AdvertisedWindow(rwnd) 大小。
  2. 当接受端处理信息过慢时,这个窗口会一直缩减直到 0,发送方就会停止发送。
  3. 这时,发送方会定时发送窗口探测数据包,看窗口是否有空余
  4. 接收方为了避免低能窗口综合征,会在窗口达到一定大小,或者缓冲区一半时才会通知发送方窗口有空余了

拥塞控制

TCP 的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽

拥塞也是通过窗口来控制,这个窗口被称为 cwnd (Congestion Window),有一个公式 LastByteSent - LastByteAcked <= min {cwnd, rwnd} 共同来控制带宽

TCP 发送接受过程

如果一个设备如上图所示,只能同时处理 8 个包,如果再调大窗口,就会出现丢包,不想丢包的话就得在中间设备增加缓存,这样就会增加时延

TCP 的拥塞控制就是来避免出现上述问题

BBR 算法

20200916211957

TCP 的拥塞控制有两个问题:

  1. 丢包不代表通道已满,有可能是管子本身就漏水。例如公网本身就有丢包问题
  2. TCP 的拥塞控制会等中间设备的缓存都填充满之后才降速

BBR 拥塞算法:企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡

Socket 接口

因为 TCP/IP 协议过于复杂,如果程序员每开发一个程序都需要自己处理报文的发送与接受等问题,效率太低了,于是由操作系统封装了一套接口,便于快速处理数据包的发送与接受

基于 TCP 协议的 Socket

函数调用过程:

20200916230544

  • 服务端调用 socket 流程:

    1. 调用 bind 监听 ip 和 端口
    2. 调用 listen 进入监听状态(监听 socket)
    3. 调用 accept,当 TCP 连接成功时,accpet 返回另一个 socket(已连接 socket)进行处理
    4. 和客户端进行通信

    重点:监听的 socket 和通信使用的 socket 不相同,一个连接对应一个 socket

  • 客户端调用 socket 流程:

    1. 调用 connect 连接 ip 和 端口
    2. 和服务端进行通信

socket 在内核中读写过程:

20200916231334

基于 UDP 协议的 Socket

函数调用过程:

20200916231723

UDP 无连接,所以只需要一个 socket 就可以和多个客户端通信,每次通信的时候都会调用 sendto 和 recvfrom,都可以传入 IP 地址和端口

应用处理 scoket 连接方式

单体应用可以承载的 socket 连接数是有限的,如何在资源有限的情况下,尽可能多的处理连接

  1. 多进程
  2. 多线程
  3. IO 复用(select):一个线程通过 select 轮询的方式检查 socket
  4. IO 复用(epoll):采用事件通知的方式

应用层

DHCP 协议

DHCP:Dynamic Host Configuration Protocol,动态主机配置协议

因为网络中的每一个主机都需要 ip,而在复杂公共环境下,不可能让管理员来手动给每一个设备分配 ip,所以出现了 DHCP。

DHCP 工作方式:

  1. 新主机使用 0.0.0.0 向 255.255.255.255 发送 DHCP Discover 广播
  2. DHCP Server 监听到这个广播,判断如果是新主机的话,通过 DHCP Offer 方式将 ip 池里的随机 ip 发送过去(也是广播形式)
  3. 新主机发送 DHCP Request 广播数据包,包中包含客户端的 MAC 地址、接受的租约中的 IP 地址、提供此租约的 DHCP 服务器地址等
  4. DHCP Server 收到 DHCP Request 后,会返回 DHCP ACK 消息包,表明新主机已被加入网络
  5. 租约达成后,广播通知其他 DHCP Server

HTTP 协议

目前大部分的 HTTP 协议是 1.1,默认开启了 Keep-Alive 的,实现了连接复用

HTTP 请求的报文格式:

20200917075642
请求报文

  1. 请求行
    HTTP 请求常用的请求方法:

    1. get
    2. post
    3. put
    4. patch
    5. delete
  2. 首部
    常用的 request headers:

    1. Accept-Type
    2. Content-Type
    3. Cache-control
    4. Last-Modified | If-Modified-Since
    5. Etag | If-None-Match
  3. 正文实体

一个完整的 HTTP 请求:

一个完整的 HTTP 请求

HTTP 2.0

HTTP 2.0 为了尝试解决 HTTP 1.1 的实时性、并发性等问题,进行了以下操作:

  1. 对头部进行压缩:将每次都携带的 k-v 结构建立索引,每次只发送索引
  2. 将一个连接切分成多个流
  3. 将所有传输信息分割为更小的帧:header 帧,data 帧

HTTP 2.0 解决了

  1. HTTP 1.1 的队首阻塞问题,同时,也不需要通过 HTTP 1.x 的 pipeline 机制用多条TCP连接来实现并行请求与响应;
  2. 减少了 TCP 连接数对服务器性能的影响,同时将页面的多个数据 css、js、 jpg 等通过一个数据链接进行传输,能够加快页面组件的传输速度

QUIC 协议

因为 HTTP 协议底层是基于 TCP 协议的,严重依赖于包的串行处理,所以 google 出了一个基于 UDP 的 QUIC 协议,尝试解决 HTTP 的一些问题

QUIC 的机制:

  1. 自定义连接机制
    TCP 基于 [源 IP,端口,目的 IP,目的端口] 的四元数组进行连接握手,如果四元数组进行了 wifi 切换等,就会导致重连。
    QUIC 以一个 64 位的随机数作为 ID 标识,只要 ID 不变,不需要重新连接
  2. 自定义重传机制
  3. 无阻塞的多路复用
  4. 自定义流量控制

HTTPS 协议

HTTP 协议在网络中由于是明文信息,容易遭到恶意份子的篡改,所以有了 TLS 协议对其进行加密,保证内容安全。
HTTP + TLS = HTTPS

TLS 加密原理:首先使用非对称加密协商出公钥,然后再使用这个公钥对内容进行对称加密

20200920011230

  1. 对称加密:公钥机制,由公钥加密的内容也能由公钥解开
  2. 非对称加密:公私钥机制,只有对应的私钥才能解开公钥加密的内容。服务端公钥的正确性由 CA 证书来保证

DNS 协议

在正常的用户上网过程中,用户是不可能能记住每一个网站的 ip 地址的,而且 ip 地址经常会因为 NAT、负载均衡等问题发生改变,这时候我们需要一个方便记忆 ip 的方法,DNS 协议就是当用户使用某个域名(比如 baidu.com)时,获取域名对应的 ip 地址的协议

已知域名,求 IP 的协议

DNS 服务器结构:

20200920161444

DNS 递归查询过程:

20200920224427

DNS 负载均衡:

20200920225045

HTTPDNS 协议

传统 DNS 存在以下问题:

  1. 域名缓存:缓存是为了降低递归查询的次数,提高查询效率。
    但是带来的问题是
    1. 服务器地址更改后没有及时刷新缓存,导致访问失败
    2. 客户端地址更改后导致全局负载均衡失败,缓存的并不是最优服务器地址
  2. 域名转发:权威域名服务器的负载均衡会根据运营商的不同返回不同的服务器地址,但是域名查询由 A 运营商转发给 B 运营商查询,就会导致运营商判断错误
  3. 出口 NAT:NAT 会导致权威域名服务器判断运营商错误
  4. 域名更新:IP 更新后重新解析 DNS 会导致一定时间的延迟乃至服务不可用
  5. 解析延迟:DNS 查询需要递归遍历多个 DNS 服务器,延迟较高

HTTPDNS 就是不走传统的 DNS 解析,而是应用自己搭建基于 HTTP 协议的 DNS 服务器集群,分布在多个地点和多个运营商。当客户端需要 DNS 解析的时候,直接通过 HTTP 协议进行请求这个服务器集群,得到就近的地址

20200920230522

参考

趣谈网络协议

网络协议知识图谱

OSI7层协议图谱

socket

\ No newline at end of file diff --git a/2020/12/cloudflare-workers/index.html b/2020/12/cloudflare-workers/index.html new file mode 100644 index 00000000..0c94ce69 --- /dev/null +++ b/2020/12/cloudflare-workers/index.html @@ -0,0 +1,153 @@ +使用 Cloudflare Workers 反代 gist | Jiz4oh's Life

使用 Cloudflare Workers 反代 gist

我经常用 GitHub 的 gist 服务来保存一些比较优秀的代码片段、配置等等,但是苦于 gist 在国内遭受了 DNS 污染,访问太不便利,所以一直在寻求一个类似 jsdelivr 加速 GitHub Repo 的方式,能够避免修改 host 直接访问 gist

可行的方式

  1. raw.githack.com
  2. 反代加速

首先说说第一种,raw.githack.com 确实可以加速 gist,而且可以加速 repo。
使用方式很简单

但是这种方式有一点弊端,就是不方便发表永链,当首次访问时,githack 会将内容缓存在 cloudflare 长达一年。当链接内容变化时,不会及时刷新,只适合发布永久内容,或者分版本发布。

第二种方式就是我经过一段时间摸索并决定采用的方式

Cloudflare Workers

Workers 的工作原理就是最近几年火热的 serverless。网站管理员不再一定需要一个服务器,只需要将对应的 Function 托管在 Workers 上,当用户访问网站时就会执行对应的 Function,值得一提的是 Cloudflare Workers 本质上只支持 JS(其他语言通过编译成 js 来执行)。

前期准备

  1. cloudflare 的帐号
  2. 域名,并且托管到 cloudflare

创建 worker

  1. 登录帐号到下图界面

    20201227143715

  2. 然后进入 Workers

    20201227143837

  3. 点击创建 worker

    20201227144011

  4. 这时候会进入如下界面

    20201227144908

反代加速 gist 代码如下:

// 需要反代的地址
+const upstream = 'gist.github.com'
+// 反代地址的子路径
+const upstreamPath = '/'
+// 反代网站的移动端域名
+const upstreamMobile = 'gist.github.com'
+
+// 是否使用 https
+const useHttps = true
+
+// 禁止使用该 worker 的国家代码
+const blockedRegion = ['KP', 'SY', 'PK', 'CU']
+
+// 禁止使用该 worker 的 ip 地址
+const blockedIp = ['0.0.0.0', '127.0.0.1']
+
+// 是否关闭缓存
+const disableCache = false
+// 替换条件
+const contentTypes = [
+  'text/plain',
+  'text/html'
+]
+// 反代网站中其他需要被替换的地址
+const replaceDict = {
+  '$upstream': '$workerDomain',
+}
+
+addEventListener('fetch', event => {
+  event.respondWith(handleRequest(event.request))
+})
+
+/**
+ * Respond to the request
+ * @param {Request} request
+ */
+async function handleRequest(request) {
+  const region = request.headers.get('cf-ipcountry') || '';
+  const ip = request.headers.get('cf-connecting-ip');
+
+  if (blockedRegion.includes(region.toUpperCase())) {
+    return new Response('Access denied: WorkersProxy is not available in your region yet.', {
+      status: 403
+    });
+  }
+
+  if (blockedIp.includes(ip)) {
+    return new Response('Access denied: Your IP address is blocked by WorkersProxy.', {
+      status: 403
+    });
+  }
+
+  const upstreamDomain = isMobile(request.headers.get('user-agent')) ? upstreamMobile : upstream;
+
+  // 构建上游请求地址
+  let url = new URL(request.url);
+  const workerDomain = url.host;
+  
+  url.protocol = useHttps ? 'https:' : 'http';
+  url.pathname = url.pathname === '/' ? upstreamPath : upstreamPath + url.pathname;
+  url.host = upstreamDomain;
+
+  // 构建上游请求头
+  const newRequestHeaders = new Headers(request.headers);
+  newRequestHeaders.set('Host', upstreamDomain);
+  newRequestHeaders.set('Referer', url.protocol + '//' + workerDomain);
+
+  // 获取上游响应
+  const originalResponse = await fetch(url.href, {
+    method: request.method,
+    headers: newRequestHeaders
+  })
+
+  const connectionUpgrade = newRequestHeaders.get("Upgrade");
+  if (connectionUpgrade && connectionUpgrade.toLowerCase() === "websocket") {
+    return originalResponse;
+  }
+
+  let originalResponseClone = originalResponse.clone();
+
+  // 构建响应头
+  let responseHeaders = originalResponseClone.headers;
+  let newResponseHeaders = buildResponseHeaders(responseHeaders);
+  if (newResponseHeaders.get("x-pjax-url")) {
+    newResponseHeaders.set("x-pjax-url", responseHeaders.get("x-pjax-url").replace("//" + upstreamDomain, "//" + workerDomain));
+  }
+
+  // 构建响应体
+  let originalText;
+  const contentType = newResponseHeaders.get('content-type');
+  if (contentType != null) {
+    const types = contentType.replace(' ','').split(';')
+    if (types.includes('charset=utf-8')){
+      for (let i of contentTypes) {
+        if (types.includes(i)){
+          originalText = await replaceResponseText(originalResponseClone, upstreamDomain, workerDomain);
+          break
+        }
+      }
+    }
+  } else {
+    originalText = originalResponseClone.body
+  }
+
+  return new Response(originalText, {
+    status: originalResponseClone.status,
+    headers: newResponseHeaders
+  })
+}
+
+function isMobile(userAgent) {
+  userAgent = userAgent || ''
+  let agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
+  for (let v = 0; v < agents.length; v++) {
+    if (userAgent.indexOf(agents[v]) > 0) {
+      return true;
+    }
+  }
+}
+
+function buildResponseHeaders(originalHeaders) {
+  const result = new Headers(originalHeaders);
+  if (disableCache) {
+    result.set('Cache-Control', 'no-store');
+  }
+  result.set('access-control-allow-origin', '*');
+  result.set('access-control-allow-credentials', true);
+  result.delete('content-security-policy');
+  result.delete('content-security-policy-report-only');
+  result.delete('clear-site-data');
+
+  return result
+}
+
+async function replaceResponseText(response, upstreamDomain, workerDomain) {
+  let text = await response.text()
+  const placeholders = {
+    "$upstream": upstreamDomain,
+    "$workerDomain": workerDomain
+  }
+
+  for (let origin in replaceDict) {
+    let target = replaceDict[origin]
+
+    origin = placeholders[origin] || origin
+    target = placeholders[target] || target
+
+    const re = new RegExp(origin, 'g')
+    text = text.replace(re, target);
+  }
+
+  return text;
+}

然后点击部署,就可以通过 [project].[subdomain].workers.dev 绕墙访问 gist 了,理论上是可以反代所有网站的,如果有需求的话,各位请自行修改代码~

注:

  1. project 是创建的 worker 的名称
  2. subdomain 是注册 workers 是输入的名字

自定义域名(可选)

经过上面的步骤,我们已经可以使用类似于 test.baidu.workers.dev 这样的域名使用触发 worker 了。但是如果需要使用自定义域名代替上述域名的话,还需要额外设置

ps:刚开始我以为直接在 DNS 中 CNAME 到 workers.dev 就行,但是实际操作之后发现是不可以的。

例如我们需要配置一个 test.jiz4oh.com 域名来使用 worker

  1. 配置 DNS 解析

    20201227180432

  2. 设置 worker 路由

    20201227180558

    20201227180837

  3. 等待数分钟,此时就可以访问 test.jiz4oh.com 来使用 worker 了

\ No newline at end of file diff --git a/2021/01/ruby-devise/index.html b/2021/01/ruby-devise/index.html new file mode 100644 index 00000000..bdc08194 --- /dev/null +++ b/2021/01/ruby-devise/index.html @@ -0,0 +1,324 @@ +Devise 源码浅析 | Jiz4oh's Life

Devise 源码浅析

前言

Devise 是 ruby 世界中最常见的 gem 之一,是用于在 web 请求中做身份验证的,他的设计非常精妙,今天我们来尝试看看 devise 是如何设计的。

如何使用 Devise

devise 的使用重点是为 rails 的 MVC 三层中各引入 devise 相关的 magic 方法,比如我们今天以 User 模型为例

  1. 为 controller 层和 view 层引入 devise,需要在 config/routes.rb 中引入

    devise_for :users
  1. 为 model 层引入 devise,需要在 app/models/user.rb 中引入

    devise :database_authenticatable

    上述我们引入了 database_authenticatable 模块,而 devise 中共有十个模块可由我们按需引入

devise_for

整个 devise 的核心之一就是 devise_for 方法,如果没有调用这个方法,就不会生成可供我们在 controller 层 和 view 层使用的 helper

def devise_for(*resources)
+  ...
+  ...
+
+  resources.each do |resource|
+    mapping = Devise.add_mapping(resource, options)
+
+    begin
+      raise_no_devise_method_error!(mapping.class_name) unless mapping.to.respond_to?(:devise)
+    rescue NameError => e
+      raise unless mapping.class_name == resource.to_s.classify
+      warn "[WARNING] You provided devise_for #{resource.inspect} but there is " \
+        "no model #{mapping.class_name} defined in your application"
+      next
+    rescue NoMethodError => e
+      raise unless e.message.include?("undefined method `devise'")
+      raise_no_devise_method_error!(mapping.class_name)
+    end
+
+    ...
+    ...
+  end
+end

devise_for 方法在调用之后会对每一个传入的值比如 users 调用 Devise.add_mapping,而在这里就引入一个概念:Devise 中的 mapping
Devise 为了支持多种账户的验证,比如 useradmin 账户都可以登录,在内部使用了 mapping 进行区分,每一种账户对应一个 mapping,也对应了后面会讲的 Warden 中的 scope。我们可以对每一个 mapping 来设置不同的策略,比如允许 user 账户进行注册,不允许 admin 账户进行注册等,这样就解决了同一个系统中不同账户验证逻辑需要写两套的问题

Devise.add_mapping

# Small method that adds a mapping to Devise.
+def self.add_mapping(resource, options)
+  mapping = Devise::Mapping.new(resource, options)
+  @@mappings[mapping.name] = mapping
+  @@default_scope ||= mapping.name
+  @@helpers.each { |h| h.define_helpers(mapping) }
+  mapping
+end

当调用 Devise.add_mapping

  1. 会生成一个 Devise::Mapping 对象,这个对象就是上面说的 mapping

  2. 在生成之后会将这个对象放入 Devise.mappings 映射中,方便后续的取用

  3. 然后将默认验证账户 scope 设置为第一个调用 add_mapping 方法的值

  4. mapping 定义帮助方法

    def self.define_helpers(mapping) #:nodoc:
    +  mapping = mapping.name
    +
    +  class_eval <<-METHODS, __FILE__, __LINE__ + 1
    +    def authenticate_#{mapping}!(opts={})
    +      opts[:scope] = :#{mapping}
    +      warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
    +    end
    +
    +    def #{mapping}_signed_in?
    +      !!current_#{mapping}
    +    end
    +
    +    def current_#{mapping}
    +      @current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
    +    end
    +
    +    def #{mapping}_session
    +      current_#{mapping} && warden.session(:#{mapping})
    +    end
    +  METHODS
    +
    +  ActiveSupport.on_load(:action_controller) do
    +    if respond_to?(:helper_method)
    +      helper_method "current_#{mapping}", "#{mapping}_signed_in?", "#{mapping}_session"
    +    end
    +  end
    +end

    比如 user ,会生成 authenticate_user!user_signed_in?current_useruser_session 四个帮助方法,而 authenticate_user! 就是我们实现验证的重要方法

authenticate_user!

在我们的实际应用中,当我们需要对某个 controller 增加验证,不登录就无法访问时,我们需要添加 before_action :authenticate_user!,比如:

class ApplicationController < ActionController::Base
+  before_action :authenticate_user!
+end

然后每个请求到来时,就会调用 authenticate_user! 方法对其进行校验

# 为了方便,我将源码中元编程生成的代码换成了普通的 ruby 代码
+def authenticate_user!(opts={})
+  opts[:scope] = :user
+  warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
+end

上述代码可以看出,authenticate_user! 方法实际上是 warden.authenticate! 方法的代理,所以我们要研究 devise 是如何验证的,就需要查看 warden 是什么

def warden
+  request.env['warden'] or raise MissingWarden
+end
+
+class MissingWarden < StandardError
+  def initialize
+    super "Devise could not find the `Warden::Proxy` instance on your request environment.\n" + \
+      "Make sure that your application is loading Devise and Warden as expected and that " + \
+      "the `Warden::Manager` middleware is present in your middleware stack.\n" + \
+      "If you are seeing this on one of your tests, ensure that your tests are either " + \
+      "executing the Rails middleware stack or that your tests are using the `Devise::Test::ControllerHelpers` " + \
+      "module to inject the `request.env['warden']` object for you."
+  end
+end

warden 方法从 requset 的 env 中取出,那这个 warden 是如何被注入的呢,其实 MissingWarden 错误中已经给了我们不少提示,request.env['warden'] 是一个 Warden::Proxy 对象,而这个对象又是由 Warden::Manager 中间件注入

rack 和 middleware

几乎所有的 Ruby Web 框架都是一个 rack 的应用,而 rails 就是一个 rack 应用加一堆 middleware 的集合,我们可以通过 rails middleware 来查看

$ rails middleware
+use Webpacker::DevServerProxy
+use Raven::Rack
+use Rack::Cors
+use ActionDispatch::HostAuthorization
+use Rack::Sendfile
+use ActionDispatch::Static
+use ActionDispatch::Executor
+use ActiveSupport::Cache::Strategy::LocalCache::Middleware
+use Rack::Runtime
+use Rack::MethodOverride
+use ActionDispatch::RequestId
+use RequestStore::Middleware
+use ActionDispatch::RemoteIp
+use Sprockets::Rails::QuietAssets
+use Rails::Rack::Logger
+use ActionDispatch::ShowExceptions
+use WebConsole::Middleware
+use ActionDispatch::DebugExceptions
+use ActionDispatch::ActionableExceptions
+use ActionDispatch::Reloader
+use ActionDispatch::Callbacks
+use ActiveRecord::Migration::CheckPending
+use ActionDispatch::Cookies
+use ActionDispatch::Session::CookieStore
+use ActionDispatch::Flash
+use ActionDispatch::ContentSecurityPolicy::Middleware
+use Rack::Head
+use Rack::ConditionalGet
+use Rack::ETag
+use Rack::TempfileReaper
+use Warden::Manager
+use ExceptionNotification::Rack
+use Rack::Attack
+run ApplictionName::Application.routes

use 后面跟随的就是 rails 启动的 middlreware 名称,最后包含的就是 rack ApplictionName::Application.routes,也就是说在 Rails 中所有的请求在经过中间件之后都会先有一个路由表来处理,路由会根据一定的规则将请求交给其他控制器处理。

中间件的调用其实类似于柯里化

# config.ru
+use MiddleWare1
+use MiddleWare2
+run RackApp
+
+# equals to
+MiddleWare1.new(MiddleWare2.new(RackApp)))

具体的 middleware 相关就不延展了。而在上面我们可以看到 Warden::Manager 中间件已被启用,这就是 devise 验证的基础

Warden

Warden::Manager

module Warden
+  class Manager
+    extend Warden::Hooks
+
+    attr_accessor :config
+
+    def initialize(app, options={})
+      default_strategies = options.delete(:default_strategies)
+
+      @app, @config = app, Warden::Config.new(options)
+      @config.default_strategies(*default_strategies) if default_strategies
+      yield @config if block_given?
+    end
+
+    def call(env) # :nodoc:
+      return @app.call(env) if env['warden'] && env['warden'].manager != self
+
+      env['warden'] = Proxy.new(env, self)
+      result = catch(:warden) do
+        env['warden'].on_request
+        @app.call(env)
+      end
+
+      result ||= {}
+      case result
+      when Array
+        handle_chain_result(result.first, result, env)
+      when Hash
+        process_unauthenticated(env, result)
+      when Rack::Response
+        handle_chain_result(result.status, result, env)
+      end
+    end
+end

Warden::Manager 中最重要的两个方法就是 initializecall

  • initialize:当一个 rack 应用被启用时,会对每一个中间件调用 new 初始化中间件,并传入一个 app 和一个哈希 options
  • call:当每个请求到来时,会依次调用中间件的 call 方法对请求进行处理,call 方法接受一个参数 env,并在结束时将 env 返回
    • env 是一个三元数组,按照 rack 协议,分别代表 [HTTP 状态码, HTTP Headers, 响应体]

所以当请求到来时,Warden::Manager#call 方法被调用,env['warden'] 被赋值为 Warden::Proxy 的一个对象,这样在 authenticate_user! 方法中就可以使用 warden.authenticate! 进行验证

这里的核心重点是 result = catch(:warden)call 方法会在应用 throw(:warden) 时接管整个后续响应,这时候会根据 result 类型进行处理,通常会进入 process_unauthenticated

def process_unauthenticated(env, options={})
+  options[:action] ||= begin
+    opts = config[:scope_defaults][config.default_scope] || {}
+    opts[:action] || 'unauthenticated'
+  end
+
+  proxy  = env['warden']
+  result = options[:result] || proxy.result
+
+  case result
+  when :redirect
+    body = proxy.message || "You are being redirected to #{proxy.headers['Location']}"
+    [proxy.status, proxy.headers, [body]]
+  when :custom
+    proxy.custom_response
+  else
+    options[:message] ||= proxy.message
+    call_failure_app(env, options)
+  end
+end

根据 options:result 的不同区分处理方式,通常会调用 call_failure_app

def call_failure_app(env, options = {})
+  if config.failure_app
+    options.merge!(:attempted_path => ::Rack::Request.new(env).fullpath)
+    env["PATH_INFO"] = "/#{options[:action]}"
+    env["warden.options"] = options
+
+    _run_callbacks(:before_failure, env, options)
+    config.failure_app.call(env).to_a
+  else
+    raise "No Failure App provided"
+  end
+end

然后调用 config.failure_app.call 进行处理,这个 config.failure_app 通常是 Devise::FailureApp,当然我们也可以自定义失败处理方式

Devise::FailureApp 的处理方式根据配置可以是重定向登录页或者返回401响应码及具体的失败信息,具体的实现和源码可以自行研究

Warden::Proxy

authenticate!

def authenticate!(*args)
+  user, opts = _perform_authentication(*args)
+  throw(:warden, opts) unless user
+  user
+end

执行 authenticate! 时,会调用 _perform_authentication 进行验证

  1. 如果 user 没有被返回的话就会抛出 throw(:warden) 中断请求的执行,将后续处理移交 Warden:Manager
  2. 如果 user 被返回,则通过验证,进行对应的 controller#action
def _perform_authentication(*args)
+  scope, opts = _retrieve_scope_and_opts(args)
+  user = nil
+
+  return user, opts if user = user(opts.merge(:scope => scope))
+  _run_strategies_for(scope, args)
+
+  if winning_strategy && winning_strategy.successful?
+    opts[:store] = opts.fetch(:store, winning_strategy.store?)
+    set_user(winning_strategy.user, opts.merge!(:event => :authentication))
+  end
+
+  [@users[scope], opts]
+end

_perform_authentication 方法中

  1. 首先会调用 user,这个方法会反序列化 session 然后查看 session 中是否有该 scope 的用户信息

    1. 如果有,则通过验证,进行对应的 controller#action
    2. 如果没有,进行第二步
  2. 然后调用 _run_strategies_for,对预定策略 strategies 依次进行校验

    # Run the strategies for a given scope
    +def _run_strategies_for(scope, args) #:nodoc:
    +  self.winning_strategy = @winning_strategies[scope]
    +  return if winning_strategy && winning_strategy.halted?
    +
    +  # Do not run any strategy if locked
    +  return if @locked
    +
    +  if args.empty?
    +    defaults   = @config[:default_strategies]
    +    strategies = defaults[scope] || defaults[:_all]
    +  end
    +
    +  (strategies || args).each do |name|
    +    strategy = _fetch_strategy(name, scope)
    +    next unless strategy && !strategy.performed? && strategy.valid?
    +
    +    strategy._run!
    +    self.winning_strategy = @winning_strategies[scope] = strategy
    +    break if strategy.halted?
    +  end
    +end
  3. 而这需要获取 scope 对应配置了哪些 strategies,默认查找 Warden::Config 中配置的 default_strategies

    if args.empty?
    +  defaults   = @config[:default_strategies]
    +  strategies = defaults[scope] || defaults[:_all]
    +end

devise

前面 讲了在 model 层需要定义 devise 方法,这个方法的目的就是引入模块

def devise(*modules)
+  options = modules.extract_options!.dup
+
+  selected_modules = modules.map(&:to_sym).uniq.sort_by do |s|
+    Devise::ALL.index(s) || -1  # follow Devise::ALL order
+  end
+
+  devise_modules_hook! do
+    include Devise::Models::Authenticatable
+
+    selected_modules.each do |m|
+      mod = Devise::Models.const_get(m.to_s.classify)
+
+      if mod.const_defined?("ClassMethods")
+        class_mod = mod.const_get("ClassMethods")
+        extend class_mod
+
+        if class_mod.respond_to?(:available_configs)
+          available_configs = class_mod.available_configs
+          available_configs.each do |config|
+            next unless options.key?(config)
+            send(:"#{config}=", options.delete(config))
+          end
+        end
+      end
+
+      include mod
+    end
+
+    self.devise_modules |= selected_modules
+    options.each { |key, value| send(:"#{key}=", value) }
+  end
+end

这个方法主要是将各个模块中的定义的实例方法 inclue 到 model 中,将类方法 extend 到 model 中。还有一个重点是为 model 定义了一个 User.devise_modules 方法

strategies

module Devise
+  class Mapping
+    def modules
+      @modules ||= to.respond_to?(:devise_modules) ? to.devise_modules : []
+    end
+
+    def strategies
+      @strategies ||= STRATEGIES.values_at(*self.modules).compact.uniq.reverse
+    end
+  end
+end

每一个 mapping 对象 都会有一个 #strategies 方法,判断 User.devise_modules 是否包含 Devise::STRATEGIES 中的模块

当应用启动路由初始化完成后调用会调用 Devise.configure_warden! 方法

def self.configure_warden! #:nodoc:
+  @@warden_configured ||= begin
+    warden_config.failure_app   = Devise::Delegator.new
+    warden_config.default_scope = Devise.default_scope
+    warden_config.intercept_401 = false
+
+    Devise.mappings.each_value do |mapping|
+      warden_config.scope_defaults mapping.name, strategies: mapping.strategies
+
+      warden_config.serialize_into_session(mapping.name) do |record|
+        mapping.to.serialize_into_session(record)
+      end
+
+      warden_config.serialize_from_session(mapping.name) do |args|
+        mapping.to.serialize_from_session(*args)
+      end
+    end
+
+    @@warden_config_blocks.map { |block| block.call Devise.warden_config }
+    true
+  end
+end

这个方法会将每一个 mapping 对象#strategies 定义到对应的 scope_defaults 中。

def default_strategies(*strategies)
+  opts  = Hash === strategies.last ? strategies.pop : {}
+  hash  = self[:default_strategies]
+  scope = opts[:scope] || :_all
+
+  hash[scope] = strategies.flatten unless strategies.empty?
+  hash[scope] || hash[:_all] || []
+end
+
+def scope_defaults(scope, opts = {})
+  if strategies = opts.delete(:strategies)
+    default_strategies(strategies, :scope => scope)
+  end
+  ...
+end

scope_defaults 方法又会将 strategies 最终存储在 default_strategies,故 验证时最终会查找 default_strategies

Devise::Strategies

在 Devise 中默认有两个策略

  • Devise::Strategies::DatabaseAuthenticatable
  • Devise::Strategies::Rememberable

DatabaseAuthenticatable

lib/devise/strategies/database_authenticatable.rb

def authenticate!
+  resource  = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
+  hashed = false
+
+  if validate(resource){ hashed = true; resource.valid_password?(password) }
+    remember_me(resource)
+    resource.after_database_authentication
+    success!(resource)
+  end
+
+  mapping.to.new.password = password if !hashed && Devise.paranoid
+  unless resource
+    Devise.paranoid ? fail(:invalid) : fail(:not_found_in_database)
+  end
+end

database_authenticatable 策略首先会从数据库中查找是否有这个用户,然后会校验密码是否正确,中间会用 BCrypt 进行密码的哈希运算比对。

Rememberable

lib/devise/strategies/rememberable.rb

def authenticate!
+  resource = mapping.to.serialize_from_cookie(*remember_cookie)
+
+  unless resource
+    cookies.delete(remember_key)
+    return pass
+  end
+
+  if validate(resource)
+    remember_me(resource) if extend_remember_me?(resource)
+    resource.after_remembered
+    success!(resource)
+  end
+end

rememberable 策略会从 cookie 中获取用户的 id 以及 remember_token 和过期时间,判断用户的 token 是否匹配以及是否过期

自定义验证策略

我们也可以自定义 strategy

  1. strategy 必须继承自 Warden::Strategies::Base,每一个 strategy 必须实现 #authenticate! 方法
  2. 调用 Warden::Strategies.add 方法,将 strategy 注册到 Warden 中
  3. 在 controller 中调用 before_action :authenticate_user!, [自定义策略名] 进行验证

总结

devise 通过将自身模块化,实现了功能的解藕,当我们需要使用某些模块时,只需要 devise 导入就行。在验证方面 devise 默认提供两个策略,而这些策略的执行是由 devise 中抽象出来的一个中间件 warden 来处理,又进一步解藕了策略与执行,我们可以随时增添策略,也可以指定是否运行某一个策略,甚至实现自己的 strategy

在验证失败时,devise 使用 throw 和 catch 语句跳出后续 controller#action 的执行,直接处理失败响应,这里又一次将失败处理与 devise 解藕,我们可以选择使用默认的 Devise::FailureApp 也可以自己实现一个 FailureApp

\ No newline at end of file diff --git a/2021/02/openwrt-in-pve/index.html b/2021/02/openwrt-in-pve/index.html new file mode 100644 index 00000000..b54fc285 --- /dev/null +++ b/2021/02/openwrt-in-pve/index.html @@ -0,0 +1 @@ +在pve中安装openwrt | Jiz4oh's Life

在pve中安装openwrt

前言

一直对软路由这个东西念念不忘,想要入手折腾一下。最近因为家里需要一台服务器,我就把我的 n1 拿回了家里,转手就入了一个 j3455 的成品软路由,这边文章记录下在 pve 环境下折腾 openwrt 的心得,顺便学习下 pve(说不定能当个 IDC 呢

安装 PVE

  1. 在官网中下载 ISO 镜像
  2. 烧录到 U 盘中
  3. 使用U盘启动
  4. 安装,具体可参考【纯净安装】Proxmox-VE ISO原版 安装 全过程
  5. 登录PVE后台,地址为 https://IP:8006,重点:https,使用 chrome 登录时因为证书不安全的原因会被拦截,选择信任

下载 OPENWRT 镜像

我使用的esir 的 x86 佛跳墙,下载是 gz 格式的压缩文件,需要解压为 img 格式的镜像文件

分配网卡

路由器最重要的就是将端口的网卡分配成 WAN 口和 LAN 口,这样才能形成一个网络拓扑结构。因为我是在 PVE 中安装虚拟机的方式使用 OPENWRT,所以需要先在 PVE 中将网卡映射到虚拟机中,路由器才能正确分配端口。

  1. 安装 PVE 的过程中,我们已经将 eth0 口(也就是图上的 enp1s0) 虚拟成了 vmbr0

    20210206155524

  2. 因为我的软路由总共有4个网卡,所以我还需要虚拟3个网卡出来,和硬件口一一对应,比如将 en2s0 虚拟成 vmbr1,以此类推
    telegram-cloud-photo-size-5-6123167585387260669-y

    最终效果:

    2021-02-06-16-02-45

  3. 应用配置

    2021-02-06-16-04-34

    如果遇到了这个错误,是因为没有 ifupdown2,需要在 shell 中执行 apt install -y ifupdown2

    2021-02-06-16-05-23

创建OPENWRT虚拟机

  1. 点击右上角 创建虚拟机

  2. 一般:输入名称并设置开机自启,点击下一步
    我使用的 openwrt,注意 VM ID,这是以后在 PVE 中操作虚拟机的关键

    2021-02-06-16-07-58

  3. 操作系统:选择不使用任何介质,点击下一步
    稍后再上传镜像文件,因为需要对磁盘进行一些操作

    2021-02-06-16-09-07

  4. 系统:全部默认,点击下一步

  5. 硬盘:全部默认,点击下一步

  6. CPU:选择分配给虚拟机的CPU,点击下一步
    按个人喜好分配 CPU 个数,我分配的 4 个,CPU 权重是在多个虚拟机中竞争 CPU 时,虚拟机的优先级,默认是 1024,可以增加 OPENWET 的权重保证网络通畅

    2021-02-06-16-16-55

  7. 内存:按照个人喜好分配,如果只是单纯科学上网,1G足矣

  8. 网络:模型选择 VirtIO
    桥接网卡随便选,后面会将全部网卡添加进来

    2021-02-06-16-21-07

  9. 确认

配置虚拟机

  1. 分离创建时选择的硬盘

    2021-02-06-16-24-12

  2. 删除未使用的磁盘0CD/DVD驱动器(ide2)

    2021-02-06-16-25-38

  3. 上传之前下载的 OPENWRT img 文件

    2021-02-06-16-29-01

  4. 拷贝镜像上传地址

    2021-02-06-16-31-56

  5. 将 OPENWET 镜像导入磁盘
    在 shell 中执行 :

    qm importdisk 100 /var/lib/vz/template/iso/openwrt-buddha-v2_2021_-x86-64-generic-squashfs-uefi.img local-lvm

    图中第一个绿框中的 100 为虚拟机的 VM ID,第二个绿框为刚刚上传的镜像地址

    2021-02-06-16-34-18

  6. 设置磁盘

    2021-02-06-16-37-42

  7. 调整引导顺序,将 sata0 磁盘启用并调整到第一位

    2021-02-06-16-42-15

  8. 添加虚拟网卡,将之前虚拟出来的网卡都依次添加进去,还是使用 VirtIO 模型

    2021-02-06-16-40-30

  9. 启动虚拟机

现在就可以使用 OPENWRT 了

参考

【纯净安装】Proxmox-VE ISO原版 安装 全过程

PVE安装Openwrt/LEDE软路由保姆级图文教程

\ No newline at end of file diff --git a/2023/01/cloudflare-tunnels/index.html b/2023/01/cloudflare-tunnels/index.html new file mode 100644 index 00000000..a235355c --- /dev/null +++ b/2023/01/cloudflare-tunnels/index.html @@ -0,0 +1,32 @@ +使用 Cloudflare Tunnels 进行内网穿透 | Jiz4oh's Life

使用 Cloudflare Tunnels 进行内网穿透

前言

Cloudflare Tunnel 是一款隧道软件,可以快速安全地加密应用程序到任何类型基础设施的流量,让您能够隐藏你的web 服务器IP 地址,阻止直接攻击,从而专注于提供出色的应用程序。

相比于 frp 等做内网穿透,Cloudflare Tunnels 的优势是不需要一台额外的公网服务器转发,并且可以享受到 Cloudflare CDN 带来的便利。

使用 frp 做内网穿透的流程如下:

+flowchart LR
+  server[Frp Server]
+  clients[Frp Clients]
+
+User -->|http/https| server -->|http/https| clients
+

使用 Cloudflare Tunnels 做内网穿透的流程如下:

+flowchart LR
+  tunnels[Cloudflare CDN]
+  clients[Original Clients]
+
+User -->|http/https| tunnels -->|Cloudflare Tunnels|clients
+

前置条件

需要有一个由 Cloudflare 管理的域名

配置

  1. 安装 cloudflared

    debian:

    apt install cloudflared

    或者下载最新版

    wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -O cloudflared.deb && dpkg -i cloudflared.deb && rm cloudflared.deb
  2. 登录 cloudflared

    cloudflared tunnel login

    随后会生成一个 ~/.cloudflared/cert.pem 文件用于后续授权

  3. 创建一个隧道 tunnel

    cloudflared tunnel create <名字>

    随后会在 ~/.cloudflared/ 目录下出现一个 json 文件 [Tunnel-UUID].json,里面保存着运行这条隧道所需要的授权信息。

  4. 创建一个配置文件,~/.cloudflared/tunnels/名字.yaml,并添加

    # 反代的源站地址
    +url: http://localhost:8080
    +tunnel: <Tunnel-UUID>
    +credentials-file: /root/.cloudflared/<Tunnel-UUID>.json

    如果在上一步没有保存 UUID 信息,可以通过 cloudflared tunnel info 获取

  5. 配置路由

    cloudflared tunnel route dns [名字或者 Tunnel-UUID] [想要绑定到的域名或其二级域名]
  6. 启动 cloudflared

    cloudflared tunnel --config ~/.cloudflared/tunnels/名字.yaml run

之后就可以通过在第 5 步 设置的域名进行访问

范例

例如,创建一条隧道从公网访问家里的 clash dashboard(地址为 192.168.1.1:9090),设置隧道名字为 clash,并将其绑定到 clash.example.com:

  1. cloudflared tunnel login 授权

  2. cloudflared tunnel create clash,假设输出 Tunnel-UUID 为 c91c37a3-3efd-47fb-af0d-e5676ac122b9

  3. 将下面配置添加到 ~/.cloudflared/tunnels/clash.yaml

    url: http://192.168.1.1:9090
    +tunnel: c91c37a3-3efd-47fb-af0d-e5676ac122b9
    +credentials-file: /root/.cloudflared/c91c37a3-3efd-47fb-af0d-e5676ac122b9.json
  4. cloudflared tunnel route dns clash clash.example.com 创建路由

  5. cloudflared tunnel --config ~/.cloudflared/tunnels/clash.yaml run 启动隧道

此时,访问 clash.example.com 等同于内网访问 192.168.1.1:9090

配置 systemd service 自动启动

  1. /etc/systemd/system 目录下创建 service 配置文件 EXAMPLE.service

    [Unit]
    +Description=Cloudflare Tunnel
    +After=network.target
    +StartLimitIntervalSec=0
    +
    +[Service]
    +Type=simple
    +Restart=always
    +RestartSec=1
    +User=root
    +ExecStart=cloudflared tunnel --config /root/.cloudflared/tunnels/名字.yaml run
    +
    +[Install]
    +WantedBy=multi-user.target
  2. 启动服务

    systemctl enable EXAMPLE
    +systemctl start EXAMPLE
\ No newline at end of file diff --git a/2023/01/run-efb-in-docker/index.html b/2023/01/run-efb-in-docker/index.html new file mode 100644 index 00000000..735d1f66 --- /dev/null +++ b/2023/01/run-efb-in-docker/index.html @@ -0,0 +1,12 @@ +使用 docker 运行 efb | Jiz4oh's Life

使用 docker 运行 efb

前言

之前运行 efb 的服务器突然无法连接,怀疑服务器被攻破,无奈之下重装了系统,导致 efb 的所有配置均丢失,特此记录一下最新的用 docker 安装 efb 过程。之前的安装方式可见 CentOS 7 安装 ehforwarderbot V2 来收发微信

安装 docker

如果已安装 docker,可以跳过该步骤

curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh

构建镜像

  1. 克隆仓库

    git clone https://github.com/jiz4oh/ehforwarderbot.git ehforwarderbot
  2. 构建自己的 ehforwarderbot 镜像

    docker build ehforwarderbot/ -t efb

Tips:

  • 该镜像只安装了 efb-telegram-masterefb-wechat-slave 最新版,如果需要额外的频道可以通过修改 Dockerfile 完成。
  • 截至目前(2023.01.16), efb-wechat-slave 尚未发布 uos 补丁的新版本,故采用直接使用 github 包的形式

更新配置

  1. (required) 将 telegram bot token 更新到 profiles/default/blueset.telegram/config.yamltoken

  2. (required) 获取自己 telegram 账户的 userid,并更新到 profiles/default/blueset.telegram/config.yamladmins

  3. (optional) 根据喜好更新 efb-telegram-master 的配置 profiles/default/blueset.telegram/config.yaml

  4. (optional) 根据喜好更新 efb-wechat-slave 的配置 profiles/default/blueset.wechat/config.yaml

    • 如果在 profiles/default/config.yaml 中有多个 wechat slave,需要设置多个 wechat 配置目录,比如我有两个微信号:

      master_channel: "blueset.telegram"
      +slave_channels:
      +- "blueset.wechat"
      +- "blueset.wechat#jiz4oh"

      需要有两个 wechat 目录

      `-- profiles
      +    `-- default
      +        |-- blueset.telegram
      +        |   |-- config.yaml
      +        |-- blueset.wechat
      +        |   |-- config.yaml
      +        |-- blueset.wechat#jiz4oh
      +        |   |-- config.yaml
      +        `-- config.yaml

启动 efb

  1. 启动镜像

    docker run -d --name=efb --restart=always -v $PWD/:/data/ efb
  2. 扫码登录

    docker logs -f efb

Tips:

  • 我的微信有时候没办法从 tg 端重新登录,必须重启 efb 才能扫码成功,通常我使用

    docker rm -f efb >/dev/null 2>&1 && docker run -d --name=efb --restart=always -v $PWD/:/data/ efb

    来快速重启 efb

\ No newline at end of file diff --git a/2023/04/build-own-rss-platform/index.html b/2023/04/build-own-rss-platform/index.html new file mode 100644 index 00000000..204d7711 --- /dev/null +++ b/2023/04/build-own-rss-platform/index.html @@ -0,0 +1,9 @@ +构建自己的 rss 平台 | Jiz4oh's Life

构建自己的 rss 平台

前言

因为需要在微博关注一些财经博主的更新,但又不想登录微博,遂研究了如何构建自己的 rss 平台,汇总感兴趣的信息。并使用 telegram 作为 rss 阅读器。

Install

  1. 安装 RSSHub 作为 rss 平台

    mkdir rsshub
    +cd rsshub
    +wget https://raw.githubusercontent.com/DIYgod/RSSHub/master/docker-compose.yml
    +docker volume create redis-data
    +docker compose up -d

    更新

    docker compose down && docker compose pull && docker compose up -d
  2. 安装 RSS-to-Telegram-Bot 以便从 telegram 上订阅 rss

    mkdir rsstt
    +cd rsstt
    +wget https://raw.githubusercontent.com/Rongronggg9/RSS-to-Telegram-Bot/dev/docker-compose.yml.sample -O docker-compose.yml
    +vi docker-compose.yml  # fill in env variables
    +docker compose up -d

    更新

    docker compose down && docker compose pull && docker compose up -d

Setup

有三种使用方式

  1. 直接使用 Bot 订阅所有 rss
  2. 将 Bot 添加到频道中,然后在频道中订阅 rss
    1. 创建一个公开频道,并输入 《频道名》,比如 test_rss_hub
      upgit_20230412_1681283739.png
    2. 将 Bot 添加到频道中
    3. 私聊 Bot 发送 /user_info @频道名,比如 /user_info @test_rss_hub
    4. 出现如下选项,点击 将用户状态设置为“用户”
      upgit_20230412_1681283864.png
  3. 将 Bot 添加到群组中,然后在群组中订阅 rss
    1. 步骤与添加至频道类似

Tips: 必须先将频道/群组设置为 public,这样才能设置 Bot 发送到指定频道/群组。如果需要私有频道/群组,只需要在设置完成将频道/群组转为 private 即可。

\ No newline at end of file diff --git a/2023/07/zero-rating/index.html b/2023/07/zero-rating/index.html new file mode 100644 index 00000000..db028d7b --- /dev/null +++ b/2023/07/zero-rating/index.html @@ -0,0 +1,111 @@ +谈谈免流 | Jiz4oh's Life

谈谈免流

免流的由来

不知道各位是否还记得曾经被 5 元 30M 支配的恐惧吗?
在大概 10 多年前,那时候的流量非常昂贵,网页也非常简单,大部分都是一些文字信息,
随着时间的推移,互联网在飞速发展,手机上网从文字转变为了以视频和图片为主,网页内容越来越丰富,10M 流量几个图片就用完了,流量需求大幅提升,
但是运营商套餐价格并没有随着需求的提升而降低,一度都是 5 元 30M 的离谱价格,
人们为了(白嫖)推进运营商的技术发展,研究出了免流

什么是免流

“免流”通常是指在移动网络中使用特定的应用或服务而不消耗用户的数据流量,即上网不要钱。
通常由移动运营商或特定的应用提供商推出,旨在吸引更多用户使用这些应用或服务。

免流的基本原理

上述的免流通常局限于某些应用,我们所聊的免流是不限于特定应用的免流,通过特定手段欺骗运营商的计费系统达到免费的目的。
首先我们要了解运营商的计费系统是如何工作的

访问正常网站:

flowchart LR
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    baidu.com
+    计费 1M"]-->B["代理服务器
+
+    baidu.com"]
+end
+
+用户--baidu.com-->运营商-->百度服务器

访问免流网站:

flowchart LR
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    10086.cn
+    不计费"]-->B["代理服务器
+
+    10086.cn"]
+end
+
+用户--10086.cn-->运营商-->中国移动服务器

免流的核心原理就是让计费系统以为用户在访问免流网站,但实际上却访问了正常网站

flowchart LR
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    10086.cn
+    不计费"]-->B["代理服务器
+
+    baidu.com"]
+end
+
+用户--baidu.com-->运营商-->百度服务器

免流的几种方式

本地免流

本地免流是在用户手机上运行一个代理程序对所有数据包进行修改,从而欺骗计费系统

flowchart LR
+
+subgraph 手机
+    direction LR
+    浏览器--baidu.com-->代理程序
+end
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    10086.cn
+    不计费"]-->B["代理服务器
+
+    baidu.com"]
+end
+
+手机-->运营商-->百度服务器

代理程序的主要实现方式是对 http 数据包请求头进行修改。

请求头中有两个关键字段 Host 和 X-Online-Host
假设计费系统是通过检测 http 数据包中的 Host 字段进行计费,代理服务器通过 http 数据包中的 X-Online-Host 进行实际数据访问,则可以通过修改 http headers 中的 Host 字段即可达到欺骗计费系统的目的。

运营商也不是傻子,对检测系统进行了更新,上述手段就失效了,后来各大网友开始了和运营商的斗志斗勇

  1. 有插入两个 Host 字段的双 h 模式,让计费系统查看第一个 Host 字段,让代理服务器查看第二个 Host 字段
  2. 也有插入两个 X-Online-Host 的双 x 模式
  3. 伪首模式
  4. 伪彩模式

本地免流优点是不需要额外的资源,在用户本地手机即可实现,主要利用的是运营商计费系统和代理服务器系统的实现差异;缺点是各地运营商的系统差异不一致,在上海能免的模式在广东并不一定通用,并且可利用修改的地方有限,所有模式被封禁后就无法使用了

本地免流最大的问题是因为当时运营商账单具有滞后性,当你之前使用的免流模式被修复后,流量已经开始正常计费,你却全然不知,第二天早上起来发现收到了 10086 的巨额账单

定向免流

2015 开始,国家要求三大运营商提速降费,漫游费逐渐被取消,流量都是全国通用,费用也越来越低,但流量费用还没低到可以任意挥霍的程度。
针对有些人喜欢看抖音,有些人喜欢看腾讯视频,三大运营商纷纷推出了定向流量卡,常见的有腾讯王卡,阿里宝卡等。
定向流量卡是针对某些互联网服务在通用流量之外给予大额的定向流量,比如腾讯王卡允许腾讯系应用免费使用最多 40G 流量。

定向免流就是针对这部分流量卡,将其他网站的流量伪装成特定应用的流量使用实现定向流量,并不是完全的无限流量。
比如使用腾讯王卡时将其他应用如抖音的流量伪装成腾讯系应用的流量

flowchart LR
+
+subgraph 手机
+    direction LR
+    浏览器--baidu.com-->代理程序
+end
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    qq.com
+    定向流量计费1M"]-->B["代理服务器
+
+    baidu.com"]
+end
+
+手机-->运营商-->百度服务器

云端免流(云免)

本地免流利用了计费系统和代理服务器之间的差异,后续差异被不断补全(感谢各大网友自费做 QA),甚至后来运营商取消了代理服务器,直接使用计费系统进行互联网访问,本地免流就此绝迹。
后面常用的一般都是云端免流,这是现在的主流免流方式。

云端免流的原理需要一点网络协议的基础知识,了解 tcp/ip 的工作原理

ip

现有互联网是基于 tcp/ip 为架构组成的,计算机之间的通信并不是基于域名而是基于 ip 协议。

ip 协议可以理解为是计算机中的电话号码,比如我们想要联系张三的时候,我们可以通过输入张三的电话号码 123456 拨打电话联系到他

dns

但目前我们访问网站的时候是通过输入域名,为什么计算机之间的通信却基于 ip 协议?他们之间是如何转换的?

这就是 dns 所做的事情。
因为 ip 地址不易于记忆以及输入,所以域名被发明出来简化输入,相当于是电话号码的联系人名称,比如我们通常在电话薄里面保存张三的电话号码为 123456,张三就相当于是域名,123456 是 ip,我们想要联系张三的时候只需要在电话薄里面搜索 ‘张三’,而不是输入 123456

dns 的作用就是查询电话薄,比如我想要访问 google.com 的时候,计算机并不知道应该如何访问到 google.com,所以它需要去向电话薄查询 (dns query) google.com 的电话 (ip)

sequenceDiagram
+    participant User
+    participant Computer
+    participant DNS Server
+    participant Google
+
+User->>Computer: User open browser and enter 'google.com'
+Computer->> DNS Server: Computer send DNS query to DNS Server
+DNS Server-->>Computer: DNS Server response 123456 to Computer
+Computer->>Google: Computer send request to Google
+Google-->>Computer: Google response to Computer
+Computer-->>User: Computer display the response
http

http 协议是普通人最常见的互联网协议,是互联网的基石,我们常见的所有应用几乎都是使用 http 协议进行通信。

http 协议的消息结构如下

GET / HTTP/1.1
+Host: www.baidu.com
+
+xxxxxxx此处即是http请求内容正文xxxxxxx

在本地免流中,我们采用的是修改 Host 字段绕过计费系统。
在云端免流中,采用的方式其实也类似,区别其实是在于绕过计费系统之后如何访问正确的目标网站?

思路其实很简单,既然本地免流由于运营商的代理服务器和计费系统合并而被彻底修复,那我们如果能够自己实现一个代理服务器不就和之前的方式类似了吗?

flowchart LR
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    10086.cn
+    不计费"]
+end
+
+subgraph 云端服务器
+    direction LR
+    B["baidu.com"]
+end
+
+用户--baidu.com-->运营商-->云端服务器-->百度服务器
云免原理

具体是将原始请求封装到 http body 中并发送给代理服务器,代理服务器解析 http body 之后还原原始请求并进行实际请求

upgit_20230808_1691510056.png

sequenceDiagram
+    participant User
+    participant 运营商
+    participant 云端服务器 as 云端服务器(10.10.10.10)
+    participant 百度服务器 as 百度服务器(3.3.3.3)
+
+User-->>运营商: 发送一个数据包到 10.10.10.10 并在 http header 中标注这是发给 10086.cn 的数据包
+运营商-->>云端服务器: 计为免费并将数据包发送给 10.10.10.10
+云端服务器-->>百度服务器: 解开数据包并发送给实际请求地址 3.3.3.3
+百度服务器->>云端服务器: 返回内容
+云端服务器->>运营商: 返回内容
+运营商->>User: 返回内容

直连免流

定向免流还有一种免流方式,不需要云端服务器。
有些定向卡是允许免去某个应用的所有流量而不是某个特定域名的流量,比如腾讯王卡可以免去 qq 浏览器的所有流量,原理是在该应用中内置一个代理服务器,代理服务器产生的流量被运营商计入定向流量。

直连免流就是通过抓包获取应用内置的代理服务器并将其用于所有应用访问。

不过因为现在代理服务器都增加了动态验证,基本都失效了。

停机免流

电信会在用户停机之后给用户开通一个花费充值的绿色通道,这样即使你停机之后仍然能够通过访问网络缴存话费。所以和定向免流类似,停机免流的实现原理也是通过将 Host 改为绿通的网址就能实现免流了。

对于停机用户来说,每个月只需要缴纳停机保号费用(通常5元)即可实现无限流量。不过电信随即采取了相应的策略,大部分地区对停机用户的上网速度施加了限制,毕竟你充个话费要那么快干嘛。

免流的局限性

因为运营商的计费系统如何工作其实是一个黑盒,外界很难得知什么时候工作机制就发生了变化,比如除了检查 host 字段,计费系统也可以检查端口。互联网的流量通常使用的都是 80/443,所以云端服务器的端口通常必须使用 80/443。

另一个缺点是为了实现免流必须使用全局代理,也就是不管国内还是国外流量均需通过云端服务器进行代理,如果云端服务器在国外,那么访问国内的服务会变得很慢,并且有些服务只能是国内 ip 才能使用;如果云端服务器在国内,那还需要一台额外的国外节点进行翻墙代理,以及国内服务器商宽其实非常昂贵,一年花销可能需要好几千,买服务器的钱可能比你免的流量贵的多得多。

\ No newline at end of file diff --git a/404/index.html b/404/index.html new file mode 100644 index 00000000..787c0931 --- /dev/null +++ b/404/index.html @@ -0,0 +1 @@ +404 | Jiz4oh's Life

404

\ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..b7383983 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +jiz4oh.com diff --git a/archives/2020/09/index.html b/archives/2020/09/index.html new file mode 100644 index 00000000..c7a28a35 --- /dev/null +++ b/archives/2020/09/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
\ No newline at end of file diff --git a/archives/2020/10/index.html b/archives/2020/10/index.html new file mode 100644 index 00000000..e2e53548 --- /dev/null +++ b/archives/2020/10/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
\ No newline at end of file diff --git a/archives/2020/11/index.html b/archives/2020/11/index.html new file mode 100644 index 00000000..cd3a9be8 --- /dev/null +++ b/archives/2020/11/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2020
\ No newline at end of file diff --git a/archives/2020/12/index.html b/archives/2020/12/index.html new file mode 100644 index 00000000..0b066992 --- /dev/null +++ b/archives/2020/12/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2020
\ No newline at end of file diff --git a/archives/2020/index.html b/archives/2020/index.html new file mode 100644 index 00000000..ae7c548b --- /dev/null +++ b/archives/2020/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
\ No newline at end of file diff --git a/archives/2020/page/2/index.html b/archives/2020/page/2/index.html new file mode 100644 index 00000000..cf91034a --- /dev/null +++ b/archives/2020/page/2/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
\ No newline at end of file diff --git a/archives/2020/page/3/index.html b/archives/2020/page/3/index.html new file mode 100644 index 00000000..a4a212b0 --- /dev/null +++ b/archives/2020/page/3/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2020
\ No newline at end of file diff --git a/archives/2021/01/index.html b/archives/2021/01/index.html new file mode 100644 index 00000000..bf1ae40c --- /dev/null +++ b/archives/2021/01/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2021
\ No newline at end of file diff --git a/archives/2021/02/index.html b/archives/2021/02/index.html new file mode 100644 index 00000000..5a0a80e1 --- /dev/null +++ b/archives/2021/02/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2021
\ No newline at end of file diff --git a/archives/2021/index.html b/archives/2021/index.html new file mode 100644 index 00000000..cda76d2f --- /dev/null +++ b/archives/2021/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2021
\ No newline at end of file diff --git a/archives/2023/01/index.html b/archives/2023/01/index.html new file mode 100644 index 00000000..257d7bcf --- /dev/null +++ b/archives/2023/01/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2023
\ No newline at end of file diff --git a/archives/2023/04/index.html b/archives/2023/04/index.html new file mode 100644 index 00000000..56ad53bc --- /dev/null +++ b/archives/2023/04/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2023
\ No newline at end of file diff --git a/archives/2023/07/index.html b/archives/2023/07/index.html new file mode 100644 index 00000000..8838e6ad --- /dev/null +++ b/archives/2023/07/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2023
\ No newline at end of file diff --git a/archives/2023/index.html b/archives/2023/index.html new file mode 100644 index 00000000..743f64b2 --- /dev/null +++ b/archives/2023/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
嗯..! 目前共计 27 篇日志。 继续努力。
2023
\ No newline at end of file diff --git a/archives/index.html b/archives/index.html new file mode 100644 index 00000000..25972237 --- /dev/null +++ b/archives/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
\ No newline at end of file diff --git a/archives/page/2/index.html b/archives/page/2/index.html new file mode 100644 index 00000000..a5e2b15d --- /dev/null +++ b/archives/page/2/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
\ No newline at end of file diff --git a/archives/page/3/index.html b/archives/page/3/index.html new file mode 100644 index 00000000..1d065127 --- /dev/null +++ b/archives/page/3/index.html @@ -0,0 +1 @@ +归档 | Jiz4oh's Life
\ No newline at end of file diff --git a/baidusitemap.xml b/baidusitemap.xml new file mode 100644 index 00000000..fa120257 --- /dev/null +++ b/baidusitemap.xml @@ -0,0 +1,111 @@ + + + + http://jiz4oh.com/2023/04/build-own-rss-platform/ + 2023-08-10 + + + http://jiz4oh.com/2023/07/zero-rating/ + 2023-07-27 + + + http://jiz4oh.com/2023/01/cloudflare-tunnels/ + 2023-04-12 + + + http://jiz4oh.com/2023/01/run-efb-in-docker/ + 2023-01-16 + + + http://jiz4oh.com/2021/01/ruby-devise/ + 2021-02-23 + + + http://jiz4oh.com/2021/02/openwrt-in-pve/ + 2021-02-06 + + + http://jiz4oh.com/2020/12/cloudflare-workers/ + 2021-01-20 + + + http://jiz4oh.com/2020/11/receiver-and-ancestors/ + 2021-01-18 + + + http://jiz4oh.com/2020/11/http-cache/ + 2020-11-22 + + + http://jiz4oh.com/2020/11/hexo-next-valine/ + 2020-11-21 + + + http://jiz4oh.com/2020/10/webpack-resovle-@-path/ + 2020-11-20 + + + http://jiz4oh.com/2020/11/tcp-ip/ + 2020-11-08 + + + http://jiz4oh.com/2020/10/vim-fast-esc/ + 2020-11-05 + + + http://jiz4oh.com/2020/10/how-to-use-rime/ + 2020-10-30 + + + http://jiz4oh.com/2020/10/ruby-in-vscode/ + 2020-10-28 + + + http://jiz4oh.com/2020/10/anti-csrf-on-separation-of-frontend-and-backend/ + 2020-10-19 + + + http://jiz4oh.com/2020/10/ruby-include-implementation/ + 2020-10-12 + + + http://jiz4oh.com/2020/10/chrome-cookie-do-not-delete/ + 2020-10-10 + + + http://jiz4oh.com/2020/10/charles/ + 2020-10-09 + + + http://jiz4oh.com/2020/10/install-pyenv-and-pipenv/ + 2020-10-01 + + + http://jiz4oh.com/2020/09/openwrt-expand-overlay-storage/ + 2020-09-27 + + + http://jiz4oh.com/2020/09/install-hackintosh-to-nuc8i5beh/ + 2020-09-27 + + + http://jiz4oh.com/2020/09/github-picgo-jsdelivr/ + 2020-09-26 + + + http://jiz4oh.com/2020/09/how-to-install-ehforwarderbot-v2/ + 2020-09-26 + + + http://jiz4oh.com/2020/09/how-to-install-ehforwarderbot-v1/ + 2020-09-26 + + + http://jiz4oh.com/2020/09/hexo-next/ + 2020-09-25 + + + http://jiz4oh.com/2020/09/hello-hexo/ + 2020-09-23 + + \ No newline at end of file diff --git a/categories/backend/index.html b/categories/backend/index.html new file mode 100644 index 00000000..6e58b619 --- /dev/null +++ b/categories/backend/index.html @@ -0,0 +1 @@ +分类: 后端 | Jiz4oh's Life
\ No newline at end of file diff --git a/categories/backend/python/index.html b/categories/backend/python/index.html new file mode 100644 index 00000000..1b85b778 --- /dev/null +++ b/categories/backend/python/index.html @@ -0,0 +1 @@ +分类: python | Jiz4oh's Life
\ No newline at end of file diff --git a/categories/backend/ruby/index.html b/categories/backend/ruby/index.html new file mode 100644 index 00000000..8f41fd23 --- /dev/null +++ b/categories/backend/ruby/index.html @@ -0,0 +1 @@ +分类: ruby | Jiz4oh's Life
\ No newline at end of file diff --git a/categories/frotend/index.html b/categories/frotend/index.html new file mode 100644 index 00000000..81738a86 --- /dev/null +++ b/categories/frotend/index.html @@ -0,0 +1 @@ +分类: 前端 | Jiz4oh's Life

前端 分类

2020
\ No newline at end of file diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 00000000..e15f7d8d --- /dev/null +++ b/categories/index.html @@ -0,0 +1 @@ +categories | Jiz4oh's Life

categories

\ No newline at end of file diff --git a/categories/misc/index.html b/categories/misc/index.html new file mode 100644 index 00000000..28cfd058 --- /dev/null +++ b/categories/misc/index.html @@ -0,0 +1 @@ +分类: 其他 | Jiz4oh's Life
\ No newline at end of file diff --git a/categories/misc/page/2/index.html b/categories/misc/page/2/index.html new file mode 100644 index 00000000..bac21c51 --- /dev/null +++ b/categories/misc/page/2/index.html @@ -0,0 +1 @@ +分类: 其他 | Jiz4oh's Life
\ No newline at end of file diff --git a/categories/network/index.html b/categories/network/index.html new file mode 100644 index 00000000..3e723758 --- /dev/null +++ b/categories/network/index.html @@ -0,0 +1 @@ +分类: 网络 | Jiz4oh's Life
\ No newline at end of file diff --git a/categories/router/index.html b/categories/router/index.html new file mode 100644 index 00000000..df135101 --- /dev/null +++ b/categories/router/index.html @@ -0,0 +1 @@ +分类: 路由器 | Jiz4oh's Life

路由器 分类

2021
2020
\ No newline at end of file diff --git a/categories/security/index.html b/categories/security/index.html new file mode 100644 index 00000000..ee7e0d5d --- /dev/null +++ b/categories/security/index.html @@ -0,0 +1 @@ +分类: 安全 | Jiz4oh's Life
\ No newline at end of file diff --git a/categories/test/index.html b/categories/test/index.html new file mode 100644 index 00000000..f045516a --- /dev/null +++ b/categories/test/index.html @@ -0,0 +1 @@ +分类: 测试 | Jiz4oh's Life

测试 分类

2020
\ No newline at end of file diff --git a/css/main.css b/css/main.css new file mode 100644 index 00000000..56d112bd --- /dev/null +++ b/css/main.css @@ -0,0 +1 @@ +:root{--body-bg-color:#eee;--content-bg-color:#fff;--card-bg-color:#f5f5f5;--text-color:#555;--blockquote-color:#666;--link-color:#555;--link-hover-color:#222;--brand-color:#fff;--brand-hover-color:#fff;--table-row-odd-bg-color:#f9f9f9;--table-row-hover-bg-color:#f5f5f5;--menu-item-bg-color:#f5f5f5;--theme-color:#222;--btn-default-bg:#fff;--btn-default-color:#555;--btn-default-border-color:#555;--btn-default-hover-bg:#222;--btn-default-hover-color:#fff;--btn-default-hover-border-color:#222;--highlight-background:#f0f0f0;--highlight-foreground:#444;--highlight-gutter-background:#dedede;--highlight-gutter-foreground:#555;color-scheme:light}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background:0 0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}::selection{background:#262a30;color:#eee}body,html{height:100%}body{background:var(--body-bg-color);box-sizing:border-box;color:var(--text-color);font-family:Arial,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1em;line-height:2;min-height:100%;position:relative;transition:padding .2s ease-in-out}h1,h2,h3,h4,h5,h6{font-family:Arial,'PingFang SC','Microsoft YaHei',sans-serif;font-weight:700;line-height:1.5;margin:30px 0 15px}h1{font-size:1.5em}h2{font-size:1.375em}h3{font-size:1.25em}h4{font-size:1.125em}h5{font-size:1em}h6{font-size:.875em}p{margin:0 0 20px}a{border-bottom:1px solid #999;color:var(--link-color);cursor:pointer;outline:0;text-decoration:none;overflow-wrap:break-word}a:hover{border-bottom-color:var(--link-hover-color);color:var(--link-hover-color)}embed,iframe,img,video{display:block;margin-left:auto;margin-right:auto;max-width:100%}hr{background-image:repeating-linear-gradient(-45deg,#ddd,#ddd 4px,transparent 4px,transparent 8px);border:0;height:3px;margin:40px 0}blockquote{border-left:4px solid #ddd;color:var(--blockquote-color);margin:0;padding:0 15px}blockquote cite::before{content:'-';padding:0 5px}dt{font-weight:700}dd{margin:0;padding:0}.table-container{overflow:auto}table{border-collapse:collapse;border-spacing:0;font-size:.875em;margin:0 0 20px;width:100%}tbody tr:nth-of-type(odd){background:var(--table-row-odd-bg-color)}tbody tr:hover{background:var(--table-row-hover-bg-color)}caption,td,th{padding:8px}td,th{border:1px solid #ddd;border-bottom:3px solid #ddd}th{font-weight:700;padding-bottom:10px}td{border-bottom-width:1px}.btn{background:var(--btn-default-bg);border:2px solid var(--btn-default-border-color);border-radius:2px;color:var(--btn-default-color);display:inline-block;font-size:.875em;line-height:2;padding:0 20px;transition:background-color .2s ease-in-out}.btn:hover{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.btn+.btn{margin:0 0 8px 8px}.btn .fa-fw{text-align:left;width:1.285714285714286em}.toggle{line-height:0}.toggle .toggle-line{background:#fff;display:block;height:2px;left:0;position:relative;top:0;transition:all .4s;width:100%}.toggle .toggle-line:first-child{margin-top:1px}.toggle .toggle-line:not(:first-child){margin-top:4px}.toggle.toggle-arrow :first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}.toggle.toggle-arrow :last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}.toggle.toggle-close :nth-child(2){opacity:0}.toggle.toggle-close :first-child{top:6px;transform:rotate(45deg)}.toggle.toggle-close :last-child{top:-6px;transform:rotate(-45deg)}code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}.highlight:hover .copy-btn,pre:hover .copy-btn{opacity:1}figure.highlight .table-container{position:relative}.copy-btn{color:#333;cursor:pointer;line-height:1.6;opacity:0;padding:2px 6px;position:absolute;transition:opacity .2s ease-in-out;background-color:#eee;background-image:linear-gradient(#fcfcfc,#eee);border:1px solid #d5d5d5;border-radius:3px;font-size:.8125em;right:4px;top:8px}code,figure.highlight,kbd,pre{background:var(--highlight-background);color:var(--highlight-foreground)}figure.highlight,pre{line-height:1.6;margin:0 auto 20px}figure.highlight figcaption,pre .caption,pre figcaption{background:var(--highlight-gutter-background);color:var(--highlight-foreground);display:flow-root;font-size:.875em;line-height:1.2;padding:.5em}figure.highlight figcaption a,pre .caption a,pre figcaption a{color:var(--highlight-foreground);float:right}figure.highlight figcaption a:hover,pre .caption a:hover,pre figcaption a:hover{border-bottom-color:var(--highlight-foreground)}code,pre{font-family:consolas,Menlo,monospace,'PingFang SC','Microsoft YaHei'}code{border-radius:3px;font-size:.875em;padding:2px 4px;overflow-wrap:break-word}kbd{border:2px solid #ccc;border-radius:.2em;box-shadow:.1em .1em .2em rgba(0,0,0,.1);font-family:inherit;padding:.1em .3em;white-space:nowrap}figure.highlight{overflow:auto;position:relative}figure.highlight pre{border:0;margin:0;padding:10px 0}figure.highlight table{border:0;margin:0;width:auto}figure.highlight td{border:0;padding:0}figure.highlight .gutter{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}figure.highlight .gutter pre{background:var(--highlight-gutter-background);color:var(--highlight-gutter-foreground);padding-left:10px;padding-right:10px;text-align:right}figure.highlight .code pre{padding-left:10px;width:100%}figure.highlight .marked{background:rgba(0,0,0,.3)}pre .caption,pre figcaption{margin-bottom:10px}.gist table{width:auto}.gist table td{border:0}pre{overflow:auto;padding:10px;position:relative}pre code{background:0 0;padding:0;text-shadow:none}.blockquote-center{border-left:0;margin:40px 0;padding:0;position:relative;text-align:center}.blockquote-center::after,.blockquote-center::before{left:0;line-height:1;opacity:.6;position:absolute;width:100%}.blockquote-center::before{border-top:1px solid #ccc;text-align:left;top:-20px;content:'\f10d';font-family:'Font Awesome 6 Free';font-weight:900}.blockquote-center::after{border-bottom:1px solid #ccc;bottom:-20px;text-align:right;content:'\f10e';font-family:'Font Awesome 6 Free';font-weight:900}.blockquote-center div,.blockquote-center p{text-align:center}.group-picture{margin-bottom:20px}.group-picture .group-picture-row{display:flex;gap:3px;margin-bottom:3px}.group-picture .group-picture-column{flex:1}.group-picture .group-picture-column img{height:100%;margin:0;object-fit:cover;width:100%}.post-body .label{color:#555;padding:0 2px}.post-body .label.default{background:#f0f0f0}.post-body .label.primary{background:#efe6f7}.post-body .label.info{background:#e5f2f8}.post-body .label.success{background:#e7f4e9}.post-body .label.warning{background:#fcf6e1}.post-body .label.danger{background:#fae8eb}.post-body .link-grid{display:grid;grid-gap:1.5rem;gap:1.5rem;grid-template-columns:1fr 1fr;margin-bottom:20px;padding:1rem}@media (max-width:767px){.post-body .link-grid{grid-template-columns:1fr}}.post-body .link-grid .link-grid-container{border:solid #ddd;box-shadow:1rem 1rem .5rem rgba(0,0,0,.5);min-height:5rem;min-width:0;padding:.5rem;position:relative;transition:background .3s}.post-body .link-grid .link-grid-container:hover{animation:next-shake .5s;background:var(--card-bg-color)}.post-body .link-grid .link-grid-container:active{box-shadow:.5rem .5rem .25rem rgba(0,0,0,.5);transform:translate(.2rem,.2rem)}.post-body .link-grid .link-grid-container .link-grid-image{border:1px solid #ddd;border-radius:50%;box-sizing:border-box;height:5rem;padding:3px;position:absolute;width:5rem}.post-body .link-grid .link-grid-container p{margin:0 1rem 0 6rem}.post-body .link-grid .link-grid-container p:first-of-type{font-size:1.2em}.post-body .link-grid .link-grid-container p:last-of-type{font-size:.8em;line-height:1.3rem;opacity:.7}.post-body .link-grid .link-grid-container a{border:0;height:100%;left:0;position:absolute;top:0;width:100%}@keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}.mermaid{margin-bottom:20px;text-align:center}.post-body .note{border-radius:3px;margin-bottom:20px;padding:1em;position:relative;border:1px solid #eee;border-left-width:5px}.post-body .note summary{cursor:pointer;outline:0}.post-body .note summary p{display:inline}.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6{border-bottom:initial;margin:0;padding-top:0}.post-body .note :first-child{margin-top:0}.post-body .note :last-child{margin-bottom:0}.post-body .note.default{border-left-color:#777}.post-body .note.default h2,.post-body .note.default h3,.post-body .note.default h4,.post-body .note.default h5,.post-body .note.default h6{color:#777}.post-body .note.primary{border-left-color:#6f42c1}.post-body .note.primary h2,.post-body .note.primary h3,.post-body .note.primary h4,.post-body .note.primary h5,.post-body .note.primary h6{color:#6f42c1}.post-body .note.info{border-left-color:#428bca}.post-body .note.info h2,.post-body .note.info h3,.post-body .note.info h4,.post-body .note.info h5,.post-body .note.info h6{color:#428bca}.post-body .note.success{border-left-color:#5cb85c}.post-body .note.success h2,.post-body .note.success h3,.post-body .note.success h4,.post-body .note.success h5,.post-body .note.success h6{color:#5cb85c}.post-body .note.warning{border-left-color:#f0ad4e}.post-body .note.warning h2,.post-body .note.warning h3,.post-body .note.warning h4,.post-body .note.warning h5,.post-body .note.warning h6{color:#f0ad4e}.post-body .note.danger{border-left-color:#d9534f}.post-body .note.danger h2,.post-body .note.danger h3,.post-body .note.danger h4,.post-body .note.danger h5,.post-body .note.danger h6{color:#d9534f}.post-body .tabs{margin-bottom:20px}.post-body .tabs,.tabs-comment{padding-top:10px}.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{background:var(--content-bg-color);display:flex;display:flex;flex-wrap:wrap;justify-content:center;margin:0;padding:0;position:-webkit-sticky;position:sticky;top:0;z-index:5}@media (max-width:413px){.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{display:block;margin-bottom:5px}}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid #ddd;border-left:1px solid transparent;border-right:1px solid transparent;border-radius:0 0 0 0;border-top:3px solid transparent;flex-grow:1;list-style-type:none}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid transparent;border-left:3px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent}}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-radius:0}}.post-body .tabs ul.nav-tabs li.tab a,.tabs-comment ul.nav-tabs li.tab a{border-bottom:initial;display:block;line-height:1.8;padding:.25em .75em;text-align:center;transition:all .2s ease-out}.post-body .tabs ul.nav-tabs li.tab a i,.tabs-comment ul.nav-tabs li.tab a i{width:1.285714285714286em}.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-bottom-color:transparent;border-left-color:#ddd;border-right-color:#ddd;border-top-color:#fc6423}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-bottom-color:#ddd;border-left-color:#fc6423;border-right-color:#ddd;border-top-color:#ddd}}.post-body .tabs ul.nav-tabs li.tab.active a,.tabs-comment ul.nav-tabs li.tab.active a{cursor:default}.post-body .tabs .tab-content,.tabs-comment .tab-content{border:1px solid #ddd;border-radius:0 0 0 0;border-top-color:transparent}@media (max-width:413px){.post-body .tabs .tab-content,.tabs-comment .tab-content{border-radius:0;border-top-color:#ddd}}.post-body .tabs .tab-content .tab-pane,.tabs-comment .tab-content .tab-pane{padding:20px 20px 0}.post-body .tabs .tab-content .tab-pane:not(.active),.tabs-comment .tab-content .tab-pane:not(.active){display:none}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{display:inline-block;margin:-1px 10px 0;padding:0 10px}@media (max-width:767px){.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{margin:0 5px}}.pagination .page-number.current{background:#ccc;border-color:#ccc;color:var(--content-bg-color)}.pagination{border-top:1px solid #eee;margin:120px 0 0;text-align:center}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:0;border-top:1px solid #eee;transition:border-color .2s ease-in-out}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-top-color:var(--link-hover-color)}@media (max-width:767px){.pagination{border-top:0}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:1px solid #eee;border-top:0}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-bottom-color:var(--link-hover-color)}}.pagination .space{margin:0;padding:0}.comments{margin-top:60px;overflow:hidden}.comment-button-group{display:flex;display:flex;flex-wrap:wrap;justify-content:center;justify-content:center;margin:1em 0}.comment-button-group .comment-button{margin:.1em .2em}.comment-button-group .comment-button.active{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.comment-position{display:none}.comment-position.active{display:block}.tabs-comment{margin-top:4em;padding-top:0}.tabs-comment .comments{margin-top:0;padding-top:0}.headband{background:var(--theme-color);height:3px}@media (max-width:991px){.headband{display:none}}.site-brand-container{display:flex;flex-shrink:0;padding:0 10px}.use-motion .column,.use-motion .site-brand-container .toggle{opacity:0}.site-meta{flex-grow:1;text-align:center}@media (max-width:767px){.site-meta{text-align:center}}.custom-logo-image{margin-top:20px}@media (max-width:991px){.custom-logo-image{display:none}}.brand{border-bottom:0;color:var(--brand-color);display:inline-block;padding:0}.brand:hover{color:var(--brand-hover-color)}.site-title{font-family:Arial,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.375em;font-weight:400;line-height:1.5;margin:0}.site-subtitle{color:#ddd;font-size:.8125em;margin:10px 10px 0}.use-motion .custom-logo-image,.use-motion .site-subtitle,.use-motion .site-title{opacity:0;position:relative;top:-10px}.site-nav-right,.site-nav-toggle{display:none}@media (max-width:767px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}}.site-nav-right .toggle,.site-nav-toggle .toggle{color:var(--text-color);padding:10px;width:22px}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:var(--text-color);border-radius:1px}@media (max-width:767px){.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height)}}.menu{margin:0;padding:1em 0;text-align:center}.menu-item{display:inline-block;list-style:none;margin:0 10px}@media (max-width:767px){.menu-item{display:block;margin-top:10px}.menu-item.menu-item-search{display:none}}.menu-item a{border-bottom:0;display:block;font-size:.8125em;transition:border-color .2s ease-in-out}.menu-item a.menu-item-active,.menu-item a:hover{background:var(--menu-item-bg-color)}.menu-item .fa,.menu-item .fab,.menu-item .far,.menu-item .fas{margin-right:8px}.menu-item .badge{display:inline-block;font-weight:700;line-height:1;margin-left:.35em;margin-top:.35em;text-align:center;white-space:nowrap}@media (max-width:767px){.menu-item .badge{float:right;margin-left:0}}.use-motion .menu-item{visibility:hidden}.github-corner :hover .octo-arm{animation:octocat-wave 560ms ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color);position:absolute;right:0;top:0;z-index:5}@media (max-width:991px){.github-corner{display:none}.github-corner svg{color:var(--theme-color);fill:#fff}.github-corner .github-corner:hover .octo-arm{animation:none}.github-corner .github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}.sidebar-inner{color:#999;padding:18px 10px;text-align:center;display:flex;flex-direction:column;justify-content:center}.cc-license .cc-opacity{border-bottom:0;opacity:.7}.cc-license .cc-opacity:hover{opacity:.9}.cc-license img{display:inline-block}.site-author-image{border:1px solid #eee;max-width:120px;padding:2px;border-radius:50%}.site-author-name{color:var(--text-color);font-weight:600;margin:0}.site-description{color:#999;font-size:.8125em;margin-top:0}.links-of-author a{font-size:.8125em}.links-of-author .fa,.links-of-author .fab,.links-of-author .far,.links-of-author .fas{margin-right:2px}.sidebar .sidebar-button:not(:first-child){margin-top:15px}.sidebar .sidebar-button button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:1px solid #fc6423;border-radius:4px}.sidebar .sidebar-button button:hover{background:#fc6423;color:#fff}.sidebar .sidebar-button button .fa,.sidebar .sidebar-button button .fab,.sidebar .sidebar-button button .far,.sidebar .sidebar-button button .fas{margin-right:5px}.links-of-blogroll{font-size:.8125em}.links-of-blogroll-title{font-size:.875em;font-weight:600}.links-of-blogroll-list{list-style:none;margin:0;padding:0}.sidebar-nav{display:none;margin:0;padding-bottom:20px;padding-left:0}.sidebar-nav-active .sidebar-nav{display:block}.sidebar-nav li{border-bottom:1px solid transparent;color:var(--text-color);cursor:pointer;display:inline-block;font-size:.875em}.sidebar-nav li.sidebar-nav-overview{margin-left:10px}.sidebar-nav li:hover{color:#fc6423}.sidebar-overview-active .sidebar-nav-overview,.sidebar-toc-active .sidebar-nav-toc{border-bottom-color:#fc6423;color:#fc6423}.sidebar-overview-active .sidebar-nav-overview:hover,.sidebar-toc-active .sidebar-nav-toc:hover{color:#fc6423}.sidebar-panel-container{flex:1;overflow-x:hidden;overflow-y:auto}.sidebar-panel{display:none}.sidebar-overview-active .site-overview-wrap{display:flex;flex-direction:column;justify-content:center;gap:10px}.sidebar-toc-active .post-toc-wrap{display:block}.sidebar-toggle{bottom:61px;height:16px;padding:5px;width:16px;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;right:30px}@media (max-width:991px){.sidebar-toggle{right:20px}}.sidebar-toggle:hover{opacity:.8}@media (max-width:991px){.sidebar-toggle{opacity:.8}}.sidebar-toggle:hover .toggle-line{background:#fc6423}@media (any-hover:hover){body:not(.sidebar-active) .sidebar-toggle:hover :first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}body:not(.sidebar-active) .sidebar-toggle:hover :last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}}.sidebar-active .sidebar-toggle :nth-child(2){opacity:0}.sidebar-active .sidebar-toggle :first-child{top:6px;transform:rotate(45deg)}.sidebar-active .sidebar-toggle :last-child{top:-6px;transform:rotate(-45deg)}.post-toc{font-size:.875em}.post-toc ol{list-style:none;margin:0;padding:0 2px 5px 10px;text-align:left}.post-toc ol>ol{padding-left:0}.post-toc ol a{transition:all .2s ease-in-out}.post-toc .nav-item{line-height:1.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.post-toc .nav .nav-child{display:block}.post-toc .nav .active>.nav-child{display:block}.post-toc .nav .active-current>.nav-child{display:block}.post-toc .nav .active-current>.nav-child>.nav-item{display:block}.post-toc .nav .active>a{border-bottom-color:#fc6423;color:#fc6423}.post-toc .nav .active-current>a{color:#fc6423}.post-toc .nav .active-current>a:hover{color:#fc6423}.site-state{display:flex;flex-wrap:wrap;justify-content:center;line-height:1.4}.site-state-item{padding:0 15px}.site-state-item a{border-bottom:0;display:block}.site-state-item-count{display:block;font-size:1em;font-weight:600}.site-state-item-name{color:#999;font-size:.8125em}.footer{color:#999;font-size:.875em;padding:20px 0}.footer.footer-fixed{bottom:0;left:0;position:absolute;right:0}.footer-inner{box-sizing:border-box;text-align:center;display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.footer-inner{width:auto}}@media (min-width:1200px){.footer-inner{width:1160px}}@media (min-width:1600px){.footer-inner{width:73%}}.use-motion .footer{opacity:0}.languages{display:inline-block;font-size:1.125em;position:relative}.languages .lang-select-label span{margin:0 .5em}.languages .lang-select{height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.with-love{color:red;display:inline-block;margin:0 5px}@keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}.back-to-top{font-size:12px;margin:8px -10px -20px;opacity:0;transition:opacity .2s ease-in-out}.back-to-top span{margin-right:8px;display:none}.back-to-top .fa{text-align:center;width:26px}.back-to-top.back-to-top-on{cursor:pointer;opacity:.6}.back-to-top.back-to-top-on:hover{opacity:.8}.reading-progress-bar{--progress:0;background:#37c6c0;height:3px;position:fixed;z-index:50;width:var(--progress);left:0;bottom:0}.rtl.post-body a,.rtl.post-body h1,.rtl.post-body h2,.rtl.post-body h3,.rtl.post-body h4,.rtl.post-body h5,.rtl.post-body h6,.rtl.post-body li,.rtl.post-body ol,.rtl.post-body p,.rtl.post-body ul{direction:rtl;font-family:UKIJ Ekran}.rtl.post-title{font-family:UKIJ Ekran}.post-button{margin-top:40px;text-align:center}.use-motion .comments,.use-motion .pagination,.use-motion .post-block{visibility:hidden}.use-motion .post-header{visibility:hidden}.use-motion .post-body{visibility:hidden}.use-motion .collection-header{visibility:hidden}.posts-collapse .post-content{margin-bottom:35px;margin-left:35px;position:relative}@media (max-width:767px){.posts-collapse .post-content{margin-left:0;margin-right:0}}.posts-collapse .post-content .collection-title{font-size:1.125em;position:relative}.posts-collapse .post-content .collection-title::before{background:#999;border:1px solid #fff;margin-left:-6px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:10px;width:10px}.posts-collapse .post-content .collection-year{font-size:1.5em;font-weight:700;margin:60px 0;position:relative}.posts-collapse .post-content .collection-year::before{background:#bbb;margin-left:-4px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:8px;width:8px}.posts-collapse .post-content .collection-header{display:block;margin-left:20px}.posts-collapse .post-content .collection-header small{color:#bbb;margin-left:5px}.posts-collapse .post-content .post-header{border-bottom:1px dashed #ccc;margin:30px 2px 0;padding-left:15px;position:relative;transition:border .2s ease-in-out}.posts-collapse .post-content .post-header::before{background:#bbb;border:1px solid #fff;left:-6px;position:absolute;top:.75em;transition:background .2s ease-in-out;border-radius:50%;content:' ';height:6px;width:6px}.posts-collapse .post-content .post-header:hover{border-bottom-color:#666}.posts-collapse .post-content .post-header:hover::before{background:#222}.posts-collapse .post-content .post-meta-container{display:inline;font-size:.75em;margin-right:10px}.posts-collapse .post-content .post-title{display:inline}.posts-collapse .post-content .post-title a{border-bottom:0;color:var(--link-color)}.posts-collapse .post-content .post-title .fa-external-link-alt{font-size:.875em;margin-left:5px}.posts-collapse .post-content::before{background:#f5f5f5;content:' ';height:100%;margin-left:-2px;position:absolute;top:1.25em;width:4px}.post-body{font-family:Arial,'PingFang SC','Microsoft YaHei',sans-serif;overflow-wrap:break-word}@media (min-width:1200px){.post-body{font-size:1.125em}}@media (min-width:992px){.post-body{text-align:justify}}@media (max-width:991px){.post-body{text-align:justify}}.post-body h1 .header-anchor,.post-body h1 .headerlink,.post-body h2 .header-anchor,.post-body h2 .headerlink,.post-body h3 .header-anchor,.post-body h3 .headerlink,.post-body h4 .header-anchor,.post-body h4 .headerlink,.post-body h5 .header-anchor,.post-body h5 .headerlink,.post-body h6 .header-anchor,.post-body h6 .headerlink{border-bottom-style:none;color:inherit;float:right;font-size:.875em;margin-left:10px;opacity:0}.post-body h1 .header-anchor::before,.post-body h1 .headerlink::before,.post-body h2 .header-anchor::before,.post-body h2 .headerlink::before,.post-body h3 .header-anchor::before,.post-body h3 .headerlink::before,.post-body h4 .header-anchor::before,.post-body h4 .headerlink::before,.post-body h5 .header-anchor::before,.post-body h5 .headerlink::before,.post-body h6 .header-anchor::before,.post-body h6 .headerlink::before{content:'\f0c1';font-family:'Font Awesome 6 Free';font-weight:900}.post-body h1:hover .header-anchor,.post-body h1:hover .headerlink,.post-body h2:hover .header-anchor,.post-body h2:hover .headerlink,.post-body h3:hover .header-anchor,.post-body h3:hover .headerlink,.post-body h4:hover .header-anchor,.post-body h4:hover .headerlink,.post-body h5:hover .header-anchor,.post-body h5:hover .headerlink,.post-body h6:hover .header-anchor,.post-body h6:hover .headerlink{opacity:.5}.post-body h1:hover .header-anchor:hover,.post-body h1:hover .headerlink:hover,.post-body h2:hover .header-anchor:hover,.post-body h2:hover .headerlink:hover,.post-body h3:hover .header-anchor:hover,.post-body h3:hover .headerlink:hover,.post-body h4:hover .header-anchor:hover,.post-body h4:hover .headerlink:hover,.post-body h5:hover .header-anchor:hover,.post-body h5:hover .headerlink:hover,.post-body h6:hover .header-anchor:hover,.post-body h6:hover .headerlink:hover{opacity:1}.post-body .exturl .fa{font-size:.875em;margin-left:4px}.post-body .fancybox+figcaption,.post-body .image-caption,.post-body img+figcaption{color:#999;font-size:.875em;font-weight:700;line-height:1;margin:-15px auto 15px;text-align:center}.post-body embed,.post-body iframe,.post-body img,.post-body video{margin-bottom:20px}.post-body .video-container{height:0;margin-bottom:20px;overflow:hidden;padding-top:75%;position:relative;width:100%}.post-body .video-container embed,.post-body .video-container iframe,.post-body .video-container object{height:100%;left:0;margin:0;position:absolute;top:0;width:100%}.post-gallery{display:flex;min-height:200px}.post-gallery .post-gallery-image{flex:1}.post-gallery .post-gallery-image:not(:first-child){clip-path:polygon(40px 0,100% 0,100% 100%,0 100%);margin-left:-20px}.post-gallery .post-gallery-image:not(:last-child){margin-right:-20px}.post-gallery .post-gallery-image img{height:100%;object-fit:cover;opacity:1;width:100%}.posts-expand .post-gallery{margin-bottom:60px}.posts-collapse .post-gallery{margin:15px 0}.posts-expand .post-header{font-size:1.125em;margin-bottom:60px;text-align:center}.posts-expand .post-title{font-size:1.5em;font-weight:400;margin:initial;overflow-wrap:break-word}.posts-expand .post-title-link{border-bottom:0;color:var(--link-color);display:inline-block;position:relative}.posts-expand .post-title-link::before{background:var(--link-color);bottom:0;content:'';height:2px;left:0;position:absolute;transform:scaleX(0);transition:transform .2s ease-in-out;width:100%}.posts-expand .post-title-link:hover::before{transform:scaleX(1)}.posts-expand .post-title-link .fa-external-link-alt{font-size:.875em;margin-left:5px}.post-sticky-flag{display:inline-block;margin-right:8px;transform:rotate(30deg)}.posts-expand .post-meta-container{color:#999;font-family:Arial,'PingFang SC','Microsoft YaHei',sans-serif;font-size:.75em;margin-top:3px}.posts-expand .post-meta-container .post-description{font-size:.875em;margin-top:2px}.posts-expand .post-meta-container time{border-bottom:1px dashed #999}.post-meta{display:flex;flex-wrap:wrap;justify-content:center}:not(.post-meta-break)+.post-meta-item::before{content:'|';margin:0 .5em}.post-meta-item-icon{margin-right:3px}@media (max-width:991px){.post-meta-item-text{display:none}}.post-meta-break{flex-basis:100%;height:0}.post-nav{border-top:1px solid #eee;display:flex;gap:30px;justify-content:space-between;margin-top:1em;padding:10px 5px 0}.post-nav-item{flex:1}.post-nav-item a{border-bottom:0;display:block;font-size:.875em;line-height:1.6}.post-nav-item a:active{top:2px}.post-nav-item .fa{font-size:.75em}.post-nav-item:first-child .fa{margin-right:5px}.post-nav-item:last-child{text-align:right}.post-nav-item:last-child .fa{margin-left:5px}.post-footer{display:flex;flex-direction:column;justify-content:center}.post-eof{background:#ccc;height:1px;margin:80px auto 60px;width:8%}.post-block:last-of-type .post-eof{display:none}.post-copyright ul{list-style:none;overflow:hidden;padding:.5em 1em;position:relative;background:var(--card-bg-color);border-left:3px solid #ff2a2a;margin:1em 0 0}.post-copyright ul::after{content:'\f25e';font-family:'Font Awesome 6 Brands';font-size:200px;opacity:.1;position:absolute;right:-50px;top:-150px}.post-tags{margin-top:40px;text-align:center}.post-tags a{display:inline-block;font-size:.8125em}.post-tags a:not(:last-child){margin-right:10px}.social-like{border-top:1px solid #eee;font-size:.875em;margin-top:1em;padding-top:1em;text-align:center}.reward-container{margin:1em 0 0;padding:1em 0;text-align:center}.reward-container button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:2px solid #fc6423;border-radius:2px;outline:0;transition:all .2s ease-in-out;vertical-align:text-top}.reward-container button:hover{background:#fc6423;color:#fff}.post-reward{display:none;padding-top:20px}.post-reward.active{display:block}.post-reward div{display:inline-block}.post-reward div span{display:block}.post-reward img{display:inline-block;margin:.8em 2em 0;max-width:100%;width:180px}@keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}.category-all-page .category-all-title{text-align:center}.category-all-page .category-all{margin-top:20px}.category-all-page .category-list{list-style:none;margin:0;padding:0}.category-all-page .category-list-item{margin:5px 10px}.category-all-page .category-list-count{color:#bbb}.category-all-page .category-list-count::before{content:' ('}.category-all-page .category-list-count::after{content:') '}.category-all-page .category-list-child{padding-left:10px}.event-list hr{background:#222;margin:20px 0 45px}.event-list hr::after{background:#222;color:#fff;content:'NOW';display:inline-block;font-weight:700;padding:0 5px}.event-list .event{--event-background:#222;--event-foreground:#bbb;--event-title:#fff;background:var(--event-background);padding:15px}.event-list .event .event-summary{border-bottom:0;color:var(--event-title);margin:0;padding:0 0 0 35px;position:relative}.event-list .event .event-summary::before{animation:dot-flash 1s alternate infinite ease-in-out;background:var(--event-title);left:0;margin-top:-6px;position:absolute;top:50%;border-radius:50%;content:' ';height:12px;width:12px}.event-list .event:nth-of-type(odd) .event-summary::before{animation-delay:.5s}.event-list .event:not(:last-child){margin-bottom:20px}.event-list .event .event-relative-time{color:var(--event-foreground);display:inline-block;font-size:12px;font-weight:400;padding-left:12px}.event-list .event .event-details{color:var(--event-foreground);display:block;line-height:18px;padding:6px 0 6px 35px}.event-list .event .event-details::before{color:var(--event-foreground);display:inline-block;margin-right:9px;width:14px;font-family:'Font Awesome 6 Free';font-weight:900}.event-list .event .event-details.event-location::before{content:'\f041'}.event-list .event .event-details.event-duration::before{content:'\f017'}.event-list .event .event-details.event-description::before{content:'\f024'}.event-list .event-past{--event-background:#f5f5f5;--event-foreground:#999;--event-title:#222}@keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}ul.breadcrumb{font-size:.75em;list-style:none;margin:1em 0;padding:0 2em;text-align:center}ul.breadcrumb li{display:inline}ul.breadcrumb li:not(:first-child)::before{content:'/\00a0';font-weight:400;padding:.5em}ul.breadcrumb li:last-child{font-weight:700}.tag-cloud{text-align:center}.tag-cloud a{display:inline-block;margin:10px}.tag-cloud-0{border-bottom-color:#aaa;color:#aaa}.tag-cloud-1{border-bottom-color:#9a9a9a;color:#9a9a9a}.tag-cloud-2{border-bottom-color:#8b8b8b;color:#8b8b8b}.tag-cloud-3{border-bottom-color:#7c7c7c;color:#7c7c7c}.tag-cloud-4{border-bottom-color:#6c6c6c;color:#6c6c6c}.tag-cloud-5{border-bottom-color:#5d5d5d;color:#5d5d5d}.tag-cloud-6{border-bottom-color:#4e4e4e;color:#4e4e4e}.tag-cloud-7{border-bottom-color:#3e3e3e;color:#3e3e3e}.tag-cloud-8{border-bottom-color:#2f2f2f;color:#2f2f2f}.tag-cloud-9{border-bottom-color:#202020;color:#202020}.tag-cloud-10{border-bottom-color:#111;color:#111}.search-active{overflow:hidden}.search-pop-overlay{background:rgba(0,0,0,0);display:flex;height:100%;left:0;position:fixed;top:0;transition:visibility .4s,background .4s;visibility:hidden;width:100%;z-index:40}.search-active .search-pop-overlay{background:rgba(0,0,0,.3);visibility:visible}.search-popup{background:var(--card-bg-color);border-radius:5px;height:80%;margin:auto;transform:scale(0);transition:transform .4s;width:700px}.search-active .search-popup{transform:scale(1)}@media (max-width:767px){.search-popup{border-radius:0;height:100%;width:100%}}.search-popup .popup-btn-close,.search-popup .search-icon{color:#999;font-size:18px;padding:0 10px}.search-popup .popup-btn-close{cursor:pointer}.search-popup .popup-btn-close:hover .fa{color:#222}.search-popup .search-header{background:#eee;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;padding:5px}.search-popup input.search-input{background:0 0;border:0;outline:0;width:100%}.search-popup input.search-input::-webkit-search-cancel-button{display:none}.search-popup .search-result-container{height:calc(100% - 55px);overflow:auto;padding:5px 25px}.search-popup .search-result-container hr{margin:5px 0 10px}.search-popup .search-result-container hr:first-child{display:none}.search-popup .search-result-list{margin:0 5px;padding:0}.search-popup a.search-result-title{font-weight:700}.search-popup p.search-result{border-bottom:1px dashed #ccc;padding:5px 0}.search-popup .search-input-container{flex-grow:1;padding:2px}.search-popup .no-result{display:flex}.search-popup .search-result-list{width:100%}.search-popup .search-result-icon{color:#ccc;margin:auto}mark.search-keyword{background:0 0;border-bottom:1px dashed #ff2a2a;color:#ff2a2a;font-weight:700}.use-motion .animated{animation-fill-mode:none;visibility:inherit}.use-motion .sidebar .animated{animation-fill-mode:both}.header{background:var(--content-bg-color);border-radius:initial;box-shadow:0 2px 2px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}@media (max-width:991px){.header{border-radius:initial}}.main{align-items:stretch;display:flex;justify-content:space-between;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.main{width:auto}}@media (min-width:1200px){.main{width:1160px}}@media (min-width:1600px){.main{width:73%}}@media (max-width:991px){.main{display:block;width:auto}}.main-inner{border-radius:initial;box-sizing:border-box;width:calc(100% - 252px)}@media (max-width:991px){.main-inner{border-radius:initial;width:100%}}.footer-inner{padding-left:252px}@media (max-width:991px){.footer-inner{padding-left:0;padding-right:0;width:auto}}.column{width:240px}@media (max-width:991px){.column{width:auto}}.site-brand-container{background:var(--theme-color)}@media (max-width:991px){.site-nav-on .site-brand-container{box-shadow:0 0 16px rgba(0,0,0,.5)}}.site-meta{padding:20px 0}@media (min-width:768px) and (max-width:991px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}}.site-nav-right .toggle,.site-nav-toggle .toggle{color:#fff}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:#fff}@media (min-width:768px) and (max-width:991px){.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height)}}.menu .menu-item{display:block;margin:0}.menu .menu-item a{padding:5px 20px;position:relative;text-align:left;transition-property:background-color}@media (max-width:991px){.menu .menu-item.menu-item-search{display:none}}.menu .menu-item .badge{background:#ccc;border-radius:10px;color:var(--content-bg-color);float:right;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.main-menu .menu-item-active::after{background:#bbb;border-radius:50%;content:' ';height:6px;margin-top:-3px;position:absolute;right:15px;top:50%;width:6px}.sub-menu{margin:0;padding:6px 0}.sub-menu .menu-item{display:inline-block}.sub-menu .menu-item a{background:0 0;margin:5px 10px;padding:initial}.sub-menu .menu-item a:hover{background:0 0;color:#fc6423}.sub-menu .menu-item-active{border-bottom-color:#fc6423;color:#fc6423}.sub-menu .menu-item-active:hover{border-bottom-color:#fc6423}.sidebar{position:-webkit-sticky;position:sticky;top:12px}@media (max-width:991px){.sidebar{display:none}}.sidebar-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:0 2px 2px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12),0 -1px .5px 0 rgba(0,0,0,.09);box-sizing:border-box;color:var(--text-color);margin-top:12px;max-height:calc(100vh - 24px);visibility:hidden}.site-state-item{padding:0 10px}.sidebar .sidebar-button{border-bottom:1px dotted #ccc;border-top:1px dotted #ccc}.sidebar .sidebar-button button{border:0;color:#fc6423;display:block;width:100%}.sidebar .sidebar-button button:hover{background:0 0;border:0;color:#e34603}.links-of-author{display:flex;flex-wrap:wrap;justify-content:center}.links-of-author-item{margin:5px 0 0;width:50%}.links-of-author-item a{box-sizing:border-box;display:inline-block;max-width:100%;overflow:hidden;padding:0 5px;text-overflow:ellipsis;white-space:nowrap}.links-of-author-item a{border-bottom:0;border-radius:4px;display:block}.links-of-author-item a:hover{background:var(--body-bg-color)}.back-to-top{background:var(--body-bg-color);margin:-4px -10px -18px}.back-to-top.back-to-top-on{margin-top:16px}.main-inner .pagination,.main-inner .post-block,.main-inner .sub-menu,.main-inner .tabs-comment,.main-inner :not(.tab-pane)>.comments{background:var(--content-bg-color);border-radius:initial;box-shadow:0 2px 2px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12)}.main-inner .post-block:not(:first-child):not(:first-child){border-radius:initial;box-shadow:0 2px 2px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12),0 -1px .5px 0 rgba(0,0,0,.09);margin-top:12px}@media (min-width:768px) and (max-width:991px){.main-inner .post-block:not(:first-child):not(:first-child){margin-top:10px}}@media (max-width:767px){.main-inner .post-block:not(:first-child):not(:first-child){margin-top:8px}}.main-inner .pagination,.main-inner .tabs-comment,.main-inner :not(.tab-pane)>.comments{border-radius:initial;box-shadow:0 2px 2px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.12),0 -1px .5px 0 rgba(0,0,0,.09);margin-top:12px}@media (min-width:768px) and (max-width:991px){.main-inner .pagination,.main-inner .tabs-comment,.main-inner :not(.tab-pane)>.comments{margin-top:10px}}@media (max-width:767px){.main-inner .pagination,.main-inner .tabs-comment,.main-inner :not(.tab-pane)>.comments{margin-top:8px}}.comments,.post-block{padding:40px}.post-eof{display:none}.pagination{border-top:initial;padding:10px 0}.post-body h1,.post-body h2{border-bottom:1px solid #eee}.post-body h3{border-bottom:1px dotted #eee}@media (min-width:768px) and (max-width:991px){.main-inner{padding:10px}.posts-expand .post-button{margin-top:20px}.post-block{padding:20px}.comments{padding:10px 20px}}@media (max-width:767px){.main-inner{padding:8px}.posts-expand .post-button{margin:12px 0}.post-block{padding:12px}.comments{padding:10px 12px}}.giscus{max-width:unset;box-sizing:border-box} \ No newline at end of file diff --git a/css/noscript.css b/css/noscript.css new file mode 100644 index 00000000..5bdf61d7 --- /dev/null +++ b/css/noscript.css @@ -0,0 +1 @@ +body{margin-top:2rem}.use-motion .collection-header,.use-motion .comments,.use-motion .menu-item,.use-motion .pagination,.use-motion .post-block,.use-motion .post-body,.use-motion .post-header,.use-motion .sidebar,.use-motion .sidebar-inner{visibility:visible}.use-motion .column,.use-motion .footer,.use-motion .site-brand-container .toggle{opacity:initial}.use-motion .custom-logo-image,.use-motion .site-subtitle,.use-motion .site-title{opacity:initial;top:initial}.use-motion .logo-line{transform:scaleX(1)}.search-pop-overlay,.sidebar-nav{display:none}.sidebar-panel{display:block}.noscript-warning{background-color:#f55;color:#fff;font-family:sans-serif;font-size:1rem;font-weight:700;left:0;position:fixed;text-align:center;top:0;width:100%;z-index:50} \ No newline at end of file diff --git a/images/apple-touch-icon-next.png b/images/apple-touch-icon-next.png new file mode 100644 index 0000000000000000000000000000000000000000..3969d69af9398942f1480033a65377fafba2860c GIT binary patch literal 1354 zcmV-Q1-1H#P)&kg!ng0u3L z6||`VRb|Hpdbnu7*6Gay`g>;MN;Uod(4R9KSNU~kXk*RnZCX1DdT2F-Ra|SwKtBoy zE*;a_;n2%7o4mC{q5DQbY?aS#plKG^8#6 z^O!I6Er;agZ8i8oze*q}&(`*VHnjq(^0FH6&^`i`xV6#H_X3CuZ#5vH{RL=HYa^`& z6m)-9uub8t1`KqY!*+$S8r;xb37ZzeYRER7*+|jqYV9n-G}^7Tvke4j^7B}>g#Zm| z?UnOb7Hb+&wzelU^~{d58nT^cc2sN6L7NKLltIAI$blCxV@@fNaF+8CiBut^l zvqb1s>BJR!b;huT9y=uvdPUtchF+dAyr2zk#_)nZP5+;DKtnZs7;6#Ga9*1+T%m0XI2Qq}U&}LwEi_nyuMq)l3%*JOv?cgVAp+VGe7_bj zXK2vxc?*~|G?e}9jNuJ!3BJA*0c{DsFAJCu`kn&X5`1?N(6-=fL_pi8uPO|>+ZNgq zd?ptG4Oe@{NPxBkUn>II7JMfW(9=Kd*f%1ep_!h(WI)66{T0U0`YjOJei&sy+ovxX z(9lYRR-Hg-Tky$%wgsOIXiM-3frhI@X#FIHz7aq}VZz?#xhJ$G_=G@Pf=>vvE%;#9P$1gQ(E#;PY7IUWv$f@*t}r zdV@!F5WIhD$L~sskKFgP8d7YfNwM6(qqWoQ|4Fk(0^8b&HZ7%E*nzPcf-}^qpxstO z{33?*&RzspSE=16?RztFdIb_iPoW~NbCL)BMS`8$t z`$!hx{hY^0Hp!8#8Z@;c+u29B)bUkOZAU!TXCBZ8^Z|W9AJ7E#2kQrSMlxE(ivR!s M07*qoM6N<$g22^>tpET3 literal 0 HcmV?d00001 diff --git a/images/avatar.gif b/images/avatar.gif new file mode 100644 index 0000000000000000000000000000000000000000..3b5d744b17ae541e331e9296640f0418d4e7c821 GIT binary patch literal 1785 zcmc(eiCfYK1Au>kC}AoSube4(tj%1XGs-IqwL{J4`&#nMW_^ZNl2cuo5V((w@KF|BSr-Dy?Ci?Qibx<>SXfwIUgogbVv$IpPzd>aHjAZHDkmh84Xt*5 zetu+ln8)SL&&~0%H{C|LeZF5GCnaWl}%00$Q845^9#jbGjmg0YYZq% z1HxF_nwpz|7kNflF9N9Q5iJvD0w&0S9`em0*f}BgKh?c=3?YfFSYWEn(AG8D&f9}T zF|^Ozg(s2g21~=--BqP2;U+HbKfJ-6t^Mpd+Vd(rH9aPG>VjZaYzrEFkzM1W4W*?PEbt7_?0ObX=R3 zfm*vF0T2e?IaX0s7omGCFNR2kL@?w2C0yDOh`J7v?|JOT+#U{?U0_{uqpk}`=r+VP zC12fo8O3{HxoZwDCgwbOZNi&VzKT}`9?t!=AVtC=y%<;l+psDMv*su+IEO5Q?TgN0 z=&H7$Gf?L=qqi5`@bimQ^6tFqKOeh&ck{=aiDvo7nlqFv&4wkwGeO}maQB{~k%?ck zE7Cdok^c8R9AM5IP1JJ)~Cz5Sv{>KNvX5%lB* z9%4%%UT_Wehfmg@gc%I&MaBNNZg03yUt|q!t%QXZJ9njhaoqbXT&%>qob*i$7vJ9{ z<5xiO5wWzi1105!#^PQCsto8sM4BepmIPkI5zoZ?*-1r_m8zst+M_T+_C9c`BdGyZ z()dMh4bJ_{K1iXtN>KzH?;xb$$ACsu8o5!>Fp}gw-jJ7^zUL{9PKe8II{SSj5atkhyp)&z8{9L0;5~dN8IV%e^8!Cm#@R*x{T5QR+y!yez%{*GTHhd}S&e?yi8`c_py&b`IA#{jM67?2A- z96X6KMj50BNJHBI?9Zj78OXyk^6Q>ufCnUfBpmb@X3fgX_M_S)8KAM>*1*t+*UyZc z^4w;`mO~qzDlqH`0`+#Ffh1i?m ztGf;Harv-B6uCo0TB=_qclsj^84AC_7^!(v;Oi5;Kl$z+X{K*fpE(N zWyVw!0PLHXpb*M(Bl@dIQsB%LhJP-?;tE={h{Y(QpYjdD9`hDbfEiJN) znh_Ry7j#5m>=)N|?3XDh_r1_zMIHc&wJ0=9`=ky7CuDT$ia(uo`t1DDrGgKeCI{e@ z{!P0)EBNAc7g6Tp|0X`9!<~%Y?miKrA{MDPqrRMh1+NgBt?oT@PRlxMyn2}v=_`HO z`{&U{yA!^h75UlWQ#)`T_pCemHpYGaJuhRu^ZY~){h#)L$)<4Zd(gT$0zX0)xk sj2>F=QOaOm@==$w^6D`S!9xiV>IXt*{ozi=Q1Vaehf=%aC;;&N3lE276aWAK literal 0 HcmV?d00001 diff --git a/images/avatar.jpg b/images/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..629be7c86c630c4669665dc77e4358c663b9e1c0 GIT binary patch literal 14348 zcmb7rcRXBO)b1IEsEINJK^R>SU9?0ShCz&86WwUhMTjT~(Ps1>b%yAS79Hk_CEXUz0UKjwa%KgoxclzKLJbzy85~R1OfmM@CW$2 z3}^#1)YKQKsc0@-xIjxwL&pGPWT2;K;ACOF1moel%FDyW&CMq$CBg@n5a8w(RT7hs zmXVj2zbc}venVDGN={z(d=dyPEiD5*0|z4`hb%ugzwG~a`P&09(@;cHL{mcG00lFI zk{R;154ZvVl#ufR{NDnhpoCIUUjR?aFaZ=45K1WcO97$!Zw!QjnUaNH2C9j6Vuhub zQNd+R17frUoO@UQmCFDx;rMT||7SK32XH>{3@`Klocgc+pT(FVEEKGiP#8b_?-Ia3 z2?52TWCm^le_Ss#ZEh!WYyz4HfJM42jeimzElLeDVFV@_9A#q|kOUJ_8Y6?Y*fj_q z=`gIRjRZ=|vS`X+X&NeA+IUTQn1g@cBwcJ3A@&8tL^8RS4T#P6q+&9<_!MCgN}-b; zvKitR&1deZg`q7iuhI~w6BVVce?krxKSUrP!7yrHrnxUzS^`@U<(=emTiI$%PPdJZ z5hPQm6+@QfG?zzx>SE=KPb^TJWYhhwB1@h&XSX)+!f)!d;v2r9qU(1Q5 zA3+1RVvcl!~z@V4{fErjO6x(A}$$aijwiI7U8tco!+8VLU^2uUyuv{Yg+3sYCrMjmq*JLrv`YK4dGPEyR;<4fras>;S z4NCUm;)}I$c4eAODwD2al}g0K>X>O!=$J8aC6wZ8sWi3eAP{qPA(phwdI2VhUh zGF=l^RpSI3!O)Z`xX0GGlDYIMQUVg$RBU-J+od9zwZ$Zp>XTS5TtQecmf_X;30*6& z`+QlC_4OJ1Iuqiq@Rm@SA=nv~EWS1wENd&|%E4N93$`Jc*RJst zyjduR%s}hxCc~sr+~1lAuSlpA)?{S0v`(noG-97D;2SElV4tiSZejMN-PL4daiEKi z7d8WF7d5w_O+zsoYTCw_a~Y2^P4T7^n5u$pH8ntW(=LzGL|g*JEN_iZhZ0nH7-(ul zMVT0aX<<42T1XZb1e+FuEXJO0GHWuK;pPA{y`HJitbp6l)UJGzjHE;%yA@*)T!P1L zbk~9{B*k-nS{A&Ax7LWuWUO3sK1|gvt(r1TDnGQIiMn5|NvFmr>|HkHRV6=j|C45M zalh`>)+w2`hx{D@5e1Kx*tFKkeK5B7GzkPQqnq*zI{|dVk5TedbgKS*V>(7$Ll&%e zo-h*-7V#P4qDHo$HZv5IW60%#E*2LPBu^F+;o^eY5;+MX9r3?dAtrf4<>J=)zAS_M z3GFC&MPin)CPPlLb2&|=aNUc+f?$L@S>98p6>7?gAWJf7vm#oJATaonP`U$GM5?~$ zCH=)QF0JG#`6o>2=BC<6O-72u(#@(kmMq`56;U2KSx~;ykq@0jo2wkMiCs)G z7%t_9XIw*-^`kY?rg$!OVpJ(O1Vu5ehGT!}XFHSadr3mPk0!T1;W*OQ}7GJZ9r{R~ANe$DZD#a-&^ zd%NL&_b6mitPl6*pLh`oZ4L`rSK>ctj9q&em9b)VDPxAWqD^75g1uol1;g!oCG-va z{u)u&*4KgpD_qaz8Cqz_)jc=LP`sVs9dw_%+KOi=Ao$xNoESD!^RzObfH(by40&?` zZ_3c}gbmfYa<;n93y+UJw<1p1x-K$uwd$m#6bO>j1>ZhzW3dxjlsBRY(KspIVpfXnppaRc&P1 zY`Lja3?GX>ZZ$Jtuz zESRCY&0Ecyp{iu>qdDp%7=Ji+KK4iA-zu=odCbLp-3QbnWunkFa$#OzvWUCC5;`Oy$&1F%} zj{1@?K6M9Z)t;ZZaP{K*p@M{|=FNa;7QCC9r@w9dMpgT6Q^S2o)wELBF@cUjr;b~H z+QW*Zo2s*7KQEVnei=vGH%u%e*qY1NC5@*YzM(EKm}JYqt)b=7lA3cp?&VxYttwn3 zYFm`aL4J8WsVTX#Q^UEe+s{2c7PVOOSW;^}^Alc(@r7>km9v)x4=s_KyK(_S=l}-#)QR`Mp$qq+rzki`Dp)9jBDkzJ8nc z1MVNJtXrb579MAPWTZ=4wf6~Dv#j>H$jd|s@?10x?r{8c_?}0FN4#SFF3(LWf#Msx zytlfP-XMjx?kwlvsWaP9Q92~rJ#6)dx2VWz!)r=dM$NhH{Pu!m&*x4RsQBHiz>TN3jYQV91q^0PO|K7!TNK#{=)WGxEU;e6Zs4d_cCDY!6sukJ z4>p$6f4)G{Rj3@q#RJ_oEk5D9i zUc{5)@%K`70u4yEq$;u|yt=0AW9%0poq&H316+%8_MSIG-G^bh?B1L~4@2v%5B+*) zEqi-O?|n==Emb1$%6iyLm9K);@Xc5VcDKi$S+W_dRP z0fUdYD5D|()XBrj*5~z_XSlxfj38uZY?}L?g{{nSRIArrbYMJ@f8yM(n4{3Nid9cz ztuR~6X7C*$zY36h&VFgJI$Ozl?VV?h0#0}uvu*v1hYeOfS=Tg>99$dJ$jinSu^l11 znyBQyPdX%H-hHmMuoy!3drmEETZi5*!m_iVhZtDi{{{Nma8u^Ee#TyBFLTii>&%?Z zMyx|oxcsDq#Rw6(mZQM48O76v*$MMIQdnSDxJV^{Lr)bAcrWDlD>Uo5?l<35ucp%z zkCV)R2W&|BY=>pDd=ld(CspqFqwU(qRxrkjBgkHDkBaNAp6&NOXQGl9o+h>@+oX8C zGnlpdsbu|;5ng;5n%qEQHR|+h3k?0Ec=RxTg7xLU zV%)6BrokyfSS9bwY0x6p-&$w1x5FMB^cB~v9=J&8zgAS|d`HeCeWWkxy)yP7BYxS2 z>q@dZ=O+|4J>JmHrybrQe14>ON9d~@+$Z{&onLiBq`Joi z$RwpK^hG`w9dfOmJSssqSmio`uZEHnF6b@fGp@rik|F3V6`3zBlt#w$$Dy=DGiY6# z7bRjjl3isl;nPj5Bx4PhEAL2l8ZB-$peqEwVLQ6M^xuhW&RBfzp^AOaqih%0NHdInlya}jh00+Nv-#`i znzIl#-MZD@De^I@r z)i$zmsJWl#S};pTSzG|2EwR(oj^{3YRKKe3!bwEv@7PygV>gVilV3~anAh_(zcZL> z-e2n=EYwU~H8`}p?a|T_GEFFMRO2y;(>nV6Lh44NhffyHA~{Yw)p+I~Z5; zX$rg!%dqE|xVZF&BE4tzw@*G|cl>$MA}eXKntM;|;7R$?TBa|6uf&?lS}z=OMYwsw zQoJ4Yt?+Ia-~BR$;X+c)>`YLM{=uSK@2Mx!O_klSb_;LAe2ex}nfAlk>{*?(hFf0(znVyZZlokh3z4_ z-Jo$nONp@peI_!L#ZG6ERSQ%3WRhvftr-)`C7kR`xeZf1FeQDO?I(olQKpJ5@7fLc zTQ=avEru%gLv)tS{vEG~uP{(OTDV#GdKW#`y
|NI5?HeIqpFZ%`(Jx%}N^X9X} z+H7^~{+r!d>#tN9+eN>wRP*Y5^^0}6(q*CNZd?+a5gNy>pkk0IJ0}zNA%2VVp`X>U z`qe~*erVH5sNuCS>!VP6{@cN!59y6RrVl`1#NLsqhdg=2rIptulo>O#^E7+?)2-R0 zLN<=Ie)G(Q#^<59%~b;n?Oz{ig#IAIJ6AupKyGG zw9oV>wyFUJ#meBid$as?Mgbn~YWHdK34v;NlKT_C>)Db#xjjQ+DFWEnA7kfKBITH*c6Sr2(2U&7 zQE|3jUWyYadj6?HOLxC_v*RP_oH*{~4al+uQ(M3)!MWn^BTreDc&VRwGM0ObtT8yMSD z5f~YYQ{Q>WQdJCqT&!Ns!Yr> zJDu75cw5p=W^l>j7i^%gAhRXMlP4(I^uv*K7Vmn^hF3q~{aShO6lGDLvGA0RJhc=Y zGh3ekeuLr1TgF*Z8`106YE3bp(7pY&*fw%~>u0yxL9mwUO(ou(Y~jV4^)K;MV!j+i z*R7TN3)=(O+LiacnD%G2gMVot*z1as0NS4^x0f_*zE$1W=`)<;jB!g^Co|NR{n|7LMsh?FIctsRr3d{t8NqW;GJ{yAr!vr_=2f+gzJUA9kNg}q%lXP9h?Ed^1j8v{ zn;)q!E`9R2q4Hw|beV~svN@MMTpmxoXWoDL3H|lUR@7E|wH&Bk+xX3jR8#!ZTL$xf z#Gom?tkNa zMt(EJU+)upE16RRZCc{jJ0K@ob~UvM=!)AJ{%`HOkXuj8A~Hh~tXI7sTjACm##p$L zJ$DU=@6-Z6y$vf^Y?%#9aeHdUBjK-ob#FPdoI}W+*lQytVw=Rn{Ep$`_29Y>H_Q^} zx$h54-I(1?w2~QYYJI#GV#FK!4i!^((`3uuzwf;uc6EC0H@=pg=a))Z%ze<;oGM;u zQH7~raUtP*8=kn=1#Ys(hy2n0)o;D#PnED6R#%Zor>7Rsr5Jzt(w z37;{jo52KQ%1skEJ-K3P5)NZm@tL-ad|)_Pu*IHnO@#SIp(lGGEV>eGJ_C{wne(BF3MB)Erm!ET5pF^{&OJpl4P$b15u>I?_G||8WlM z=U(#U92T*QVIhgGVa$VXb6#Y~N@-op`W(Ckw_b+fbh%XV_*Fk*K>wzxa*puuuXdH3 zTUuEd(;FY;wc~h9v1zN$>7)DUqE;=HCXs2c6_1VR(g)WfSn<6J`{5#=5Brq^)j~Zo z*}HLz&R~EXfc)+;J)h&0`TWJ%gsJtY2eYY~XYk(m!y+a8Viv1Chg)0&XWilWD;vio z_lxWkC@CJQo+y=HytH*K z{OZ)(tGhyBB?fMpmG!A1hx;|sV~L)N?9Xr0u6h$6W05~a63rsV#n#%n8p$$i3tRE( zWB6DHa-B+0&WV>SPH+q#hi;WYd-0}B$yk#)WfW(H^n7Hp_LZ8oD{wT2KfSr>#s@!R zrf%eIxn$g6WQXCh6UZ)y;sYvud1tWD0oKT5YlQXsKoYhL+kUU9;2o2I=$eA{=B!2= zMeU^&Z%%S)IPle)RO&~Nk4e^2^k0UyjIpt8%RQ>XV!=$>_IIb-^ zN=Z20gK3IdUR-gCOJe}(sW>GGPYI6Wnl!=lzytoLm8@TzPyqALDqmtExPLaz^aAsx z$qCPl>~%g5>Pm?gQJ+9R(R|I3AN?Ru-;)b8+)m+@tLP~Lf?m09so2v@X~|%o!h$`8 z`j|)elQpMHZ6VwfT#OftV=zJjMQi-MJjQ|s6ty^$7G>?;h0i)=P54-oJvRxDhx@x8 z5?X`@L8OM!uUPcf$R)g()5qXzS5o0&*?A++yCucDFc2u9kl#tUP$YZ>7g;{ra*?%Qo9wgi<=z+Pz9`qnSC_OKlSkggC3XbYZrLsw@ve8%sucmI1#`II4^Q z^<+(~pn_ym>Sn>HgXQzf!J+>VlaJ_cE` zrK1~8obuJeFxcw{D1l)D1D7y%MSqg5xPVT6T9d%=3_(~dw@1oOfmakzs>7!|wMu2d zaf{`)Y&JL2KHd8)DJW2~<{UPTg-_AEL$N=;&+G|x{3OEao``wk_*pw~k}Sm+bYoA1 z)l&znsQDP1_W2%}kp*8w8r@*6DCc71ZlJz*rKKJ(relut)EM?(ERyx^y;v1oxG^Nk zv7R_9?PR{E@{0B29Ph?G7d{mBM#UQ}_^Eoos3d+<49ZuEK5T>XUhr-s4Y(OeGX2Z z(qD>uUyq#$6%)oesLO=S3i`oZV1B&xWUjte9h7d%6aqeoZ1q;)C)g&GW3XS+rX=}c zSa$-Lkwvzc3nn3{!DO~fmLxx+Y?4*G3>SS4S{a2l2Td@Iqc|N0P$lXJCTKO#@>Bq; zrZxq*p%SNI1oV~)B1~c{LF`U-j{pDPWn4FZoI>*l*qD72MWbBkOcO=W_ z;Ew#^>p`kp0!4UbolSq&8*-|fIDMUQ>uC>{#LO5Kfh225=6{P$Lg6MNIss}CcCtCD z5d!Td>*FRo&$!@m3c!m1IiR{XT`r zN<;|QY{AoA;pA6S9DVU#1XD4VBt#W8JlQ0kQj&rr9sp#c0XhH@P2)%bR@8i83&Nmd z3Q2$h0KZ93k~uul38_sfo?*bmGFc$1V=lu^77j6mKN%%U6o@5cJ6nTwR)RD59cqZo z*^1J|vg+QSA{nueYjxy3A|+hOU{V;Rt3>9Mcb~*8P3sXccZGX8c5;s1J0W z2J-IY;)LG5r<9?Im&n0vlw;e;nF z$uJh{K}g~fB={-7(}uJa3X-&iNuCVOoFcK(XK7?dOk~DC*3G*Jteer zLz1)A4D(dbBeHF{+?PGDw@V=f>+(>a;tRCH_$WuIU$)TK5Cxa@?cBA zoRpv8CLBEBv!_6;)N*NQ2zQqnaJ3%$La=q=3kA`MH;T(XuX4)4DoGSd5l;cphEV0i zQxI)LLn-LBqBWg~<~^3z_;^3DU!#LECJkvbSp!c7FqQ~@N{r5XYv@ItMJ3BCLh)tF z0$1wYSp~Th18;=?oKe(fO?yTjiEPWOM7gRZ4_$mjo``HKwp>pLgxg%s(jKhMyz;HV zR+(wt{h;yMp7!u7eDUu)T*|bALmSw@*XwG;gV$fxf6j$@t)nuOH}a)6$8F|+>fWEW zIqtG2U-XdMzWh_ybQ_*|Iyg4h@;wB_|ExJeS!fn<|85ZNkK|!Nu=Dx3OLu)i|VdX&A$uv>WwTbj%42C zw7lkB3Bn`4B=3b0ci5L3J8O2}jYo~0H|CVSN1%t3ZCvzlU4wrCw<84y+p~}9<1KFr z=WSdNb+tnqQq@s@L*6xamk;baNN0XFu6Iy2>cytpxj+9D=+#*K!R7DRd>q@VOCFYV zy@UQTP?wLHp&7$?lqiN?g^~W2Yo&l;s)N4ZB#KfObIn6UofH1P*b31W5b3u-X% zW*Pl9v(n-0aaXa{9qsHNG*4#)uKWWsy?8)7x)y0@6?oP1T)PhE#}*D)4!C7^_+B=4 zW_4};^xkv1KiBZ+-cj1qyzkHF`=f>;M}z+ z5IrYNgm-y$7ga5nYnsXxRV~u@7Z8>^MMMSc|ETxKZIh~gAd_qPs+jvPfKL7S=BLy3 z8JDjIcfar0d_zZRCd+Xq6V_s^?-E?O*FpTvS z5!86#a%;xv6r8)|SRwr1ql)>zz)H}_5BA~%HQKWZ*k9m$=JMZ-ov+TccUBK&PmT7g z^H4t&iw}H`x6TN!&J5j7;$F@kt9b0Uydkgt<9cd#_QmD;%(p1TFxefmdmq<_9Xwhd z{K+Hz1*~#AZ_N}%32;7oF7!@KZ}#_}{E?cYW0kK5YgbOger%j%`eiDeJZtzNzqmG1 z_4-TTxZkZ3?xT~BT3t&I@zy^Yen(Ewx<9f($sLUS@t@dm>hfG+TT0Xa`+1f z9Fsqt+I^Fqw>*4NH}}Z%FTne8{ONam-$`GQw^&HAtM`|QKM^PuhxwJ8K?5fvpLI?x ze>eY|%lmOgXl;+aN-z0G>fl-Puk;4`R5#FAsv55bks|F@M-HxEFE>frQm=jf%ad#*cch%#iRHw!BW4J>| z)W|{DT{Gb~nV`9S{4+3csbwX1oA&;neAEw~qxVsn?{cE#_`i5Y347n-zWI+=RMqdh zL7Zo0%YOlpl<$5czw=RYD9_7liANDEr@Alx0z)ZpOpc;<_cvy4e#;xF%KfRZPr84i zuru+4e)Zd#q0sy-`3I)sAOGZMwcHZ=wRUUs@bIJca@5NMovv+r{78<+`nXMXP-oCS zX!d%~@i*J&Mc+Ey_r04LYh$<3B4!j6Q7|#Wd4BHUN&ns7(vdC)0mF|LpEchSejPS` zc2*R)-T5doA64~maYPK1M_4C%W_hp5Y);rf&ceYwvI7g&N#&-q- zS6;PzXw6Cp;&0|o=KRM>Z1JM&qGUpm*jUCFl&hk7&5Q`#j@7ZDUq#IDtEm_g(GaI- zJ2>tJpGjozV%M6}#ks+=p4=%y50kwGlwnnEGMDQ^{?xT)==F4R#MqbHFN9wo;Pn0bu;%ZC%+o<^$Q_+hG> zTyuY7{rgRe^|tH>+zKV_-3l|qSSyR`w=#zvv``syZl{U8Kj&+CLGAgjm};fI>S^4>RQ@2z14ZOcJTJ44n9%M+&pXZ$KQTl$UAt{!1ii3o45IrFY+0v z`EogUyl>*Kft)+b$pUcW;m8KQJb4{`sK&wQI51 zYui-whrZ>ee@Lq44@Sx{<4li|AGEc`gAa~Z{O!Z9t9%~J@$$GZp0Sh=i8$geQvDD* zm=h84?3M}nx@`T2-B*<-_tg$Pr0|P<-|2=HYMoY84wQAtTCJEs8i#S1>+`+qm{%!_POxBW-Amd=Zgo+sXL~cPtmQGB&E! zmbE)JIyRVyBo<8BaAp>xZTser{$B=0|siWu1(ZKK1K&9M@8_E zvB21D2ezpf86P`0#hm|532IM@g6=WmW9yJbSGs|RO4=^lB*8R&oG?(G)sQWlHEtnn zns%*PKdW-wP0_N+$}~HRFzzNhr`6w(osne2rTkj6u9_x$rdf7QUoP>BU-WUsy4K6A zMK2GEvFp0Ti47X7z;rJ?SkSx0)8+f7rq z>Sg7bZ0d5h8Nzm*f*h5q*ljO*JCg7Hh};8P{ciF&fzF)0OX05n?8-Z3NoEH{SG{87sQqq7-1!fz54k(S~JnJW0Mav zP+ro3jCOXe*F4?Ra;)4XpZB!+u)naeV;wRcRj3g$iSs!~T(`>uLsz(!-f%*Ybk-t> zzul{s>0)~>je#LFmJP1Tzh#4jhLh=LM-vT)26H8!7`%|>9=tc?OCve@^qL4-5=^zG ztqX%-ucOimg_^FU%Q}OcToLYVwgY@}dYd^k7V4lqoRV>g(KeEgo%t+p$b@8UX*r1eN|z+3(+@NCDdZAuUDSbRQY7h=M-xo>TB*v(1#Ti5UWIss zuycNZlbu-GN?Z1`t*ktrtJk;&cfT68@l)?y6LZHGAqjo9@7-MYnS8bw9 z(SFSJkan!yb)gMfAF^xw!cI;}M8`{Xal%=lMMRu`sJXBHgBGtgzallxCs##orf-?% zx$GS+XwQqRrx`;&g^(Ra9YvHQW*zj0B5>-3xtPmMRc43IKk5OVXB=NCp6Frj*(}PkzTB?xhkHHG! z%I4KrqnO#c3W03H@C6>*n9(oCqU>51>+ei@E4N|YD!S-4XB`QS&DJxyX(|eQGbsv} zOt`k$@#5*4*_dYE>N5?jQ6a5!(~`Cgou=-y)C1lD5wmcJyC`}>Z-<<^jh7PdKkqZd4{ta-2*i&^1S zx3dXR=%hCVch_UH8{jbIYV8KCP3KIsUDut#GO}diu$Wh%KGhQRa%a4KeKkVls%+#~Va4mdPSUNR z@=Iy}gCaE_T`fdd8j=7w!lS*6REQy*?lHYB*`>&IAiS|V^gFR!8VcJW3CF-~Fm zo>`g2f=n()-)gEQ*`^ulp)7(+&C@Ep>)X+`NwJs~r`jM%b9J4^cJy+yE~ZqTE-i|i z#vw0TW9MoNy?oYkEi(q32C;QBUk z%_8)aJ3qX7y(au}xj^GBI}TmqjbT0Ru2%=4U#&iohFtM?t44o5@Hmp~{`P(S21RVI zQU8)Ji!gPnqP}=g=3l_IQ`X1I6$VYIa&~eA2y5>!jU_@BX~LApvQZBrd%qN#FDk;t zXO>nAB<5T6ZdmJCmfK02dB=+g|BmTSd?`CO>D?qrl=U%+US7J=7x3~g;4|Jp8(?42JI#wfOAhxWWhiIN8f>^HU6eLPWxJZ+ zmiXFQLA(?&wC#J$MB_{$?+5@gz(o=tBt;u&VgjaDbDD>=4KkOz)AD+jFQL`UzN~%P z9hh^;TJkgg&5OCLP<&^}lQ8{~=NZp+lerWF$%cb}Ll?WoKkvoo7iHz6D{!OVa6PR{ zF3pm-VM&>4X2(1(VfoP?7kj3*E}@HXJ!A!!F4@-`UZOAQ7qG~u*^-m^$h*#N#mq+U zTZ(PmgzT9Tb^oc!e5jN7CF`TY8E2YF`H$PbNA~Cf>o&kZ1ttgBUsk z900(eK?T$S9Yb&R2i%k6AEpmih;?<3>Y9fWw`GRJAD+m6K2m#x8Yl?!?LWAa`LNrS zl(Hrys*CCp<8mvHpehdm5bslxd9rMsT zu>kTUBt&CTksWOrJ4A$pLanXYRpD>niu#t{>vuF`s?)G?*3h817p((W(})7}0MRRpXgJCM7!n^J#+gF{d;@~%El4m&G&KbW z0{~F-gAK0Lv3>onmKq*I5zE$=XwSJbdQ)}j7B$lcGS$A{;OTX4j)GJdd$b{y6jaTS z=t3q~QCxx}I2DBtD@`erH3L(f1YHvm%=JMa&HzLj0ytdY6Nb@%xnU^KCj&%70X_i2 zm=G=Kn;0!4%?tp193Yio|B*QW9rIU&v^WLQPc6Oh+n&;9A5dVhqMu<$#Kly^u(MMsvLk_Ih;Bs^dX@VVNyFjn7A>DJZ7 znUwXaKuqKcBx9+I_%P|6xos2uzi$_O?c^mrV-f)Bl>fyV#N1I(7Q7!vKJfM!K@)>Y zGxY!%Xf!oD0NA^J9t2dHHgfbk$er(KZy1p0IZ0mAQr@b`L^_gJ?0_>90~B)cZ66@b z1iXulNCAz&G1|!);EApSzyoL)z%o;852!@DM$!2&sss%%I^hGD&KY0=Or$gtJEdq? zQv+SAC)<=lxk}WiM1DVDTX2Y`G{5mri!(qklmeuk0}lXHAPMKWekky^9c+RuP(ncI z0Y{Jv@Bq382)Zzi%tuX}P-yQTP?!9S3m$OdNwUQ|y2Vr$D2QV?qWMACDF84Eas(*A zOPGK@3P_A1I~7eKy(Eku0*Q#Oqbt=|h<1k9P%_Z~P(BVC!warcFX^eF9Us<(eCGNV zECW$ZXkS{0ya>^*+?s}C`T`%{|6*w(b1u&XXAV#|H7K<}iI^yvfF5e_nQ)FQ&;w{t zGVuWj8b{B#nXzeQSap$fz1WeHf+awu4FMQ{Uh6euVV zJ50?FQVuF>A1L*C5BNF}1WG|I{5F0w8h|ip@Bws=U@rjX{?7q>+XI|~LkUn&M?<4s zKvKa%k#}A*G9aC30Kmi0+dvuXq+p>j*G|D{LnY+8K;-~NC;;`jK>&be2N2r>;tfzV zfsk>;gW3nsgo3669sq3+AFvmI$_nmA`=o&au^~Yv1!q6kAFzN+gEtRK!+Fk=vj)f> z*vo%zr~n`jW6+@7qu|gmS6)e@B)FrC#2;P`H8-K_0YFDz2mHqpMdk$;CK@p80gDEx zQW`Q8dk}DYhlxRhL7O!g+G!#Nw`=FSZc0IS+Yz8|cI^=yj8y0UmLV%3J#!8_I!zc$N!`Po1naJLHwfm04}CUh<`{6ABAEo$(>*kqjAm2QJVgI zBLyrV;B+9Q=ZgD;lG&UpF=OGfbF&XnDq`XVX?E+ebBzYo22|5lDP1^-4iHcEpQu1I zK;sBr4>9~-cMiG`nFI_73)o4?)I$vbs)CNv039`-2I%0U!QLL{tBwF-krMRE8q6BV zU8VuoxESzxa_%j3qOC)r(eOIjCWIVc z2B-{!=zks_L|Y~SaA9Hsmj^>64L(p~DZ#57{u{^$+NBIg*f}#2|EYkCb&n+gu+u(f pVgNLx9nbw=-?dL6L1O+c{vQ@GGY|j( literal 0 HcmV?d00001 diff --git a/images/favicon-16x16-next.png b/images/favicon-16x16-next.png new file mode 100644 index 0000000000000000000000000000000000000000..fe7da22fb1bab953ec9dcc7a1af50c63667763f3 GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!cYsfbt50U3A{ZzsDaFUfXJllk zsHpt?`xi(8Mc@F)m6w+XD%8}})Y8&={P^*|fB)96U%z_w>M2vEw6?YaRsLvm^?&8G@yMp~m%c(T7px0?u(BJu0ENCN?;%-C#RyK0$~Hm z@87?F`SQih&28(}t?SpXS5s313Mztuf`UR>S=pUCcQ$X{4Ak=O-Mjt!_ro+9>!n>P=r7bv25ef9>Rt8_|& z{DK)Oo^UJtsIR$s>CS;Y%%|HT^^YFd`|-@)gGt^iZAEVuZJ%}c+4JM~J{~*vZa<@R zK@U*hLr)jS5R21GFP{`@G7w-1Xmk-ty)Sad@lMsh|KDvA7Ir$X()lg2>b_z~n%ZP} z<(7?QF`CZld-`)2He9!hKd!w;C}&1h^%3{1zgqw7+J)X~UFD14xcK{e*#l}F@dsY* z_g{Hkeoyu#`DI!&0xvT$Of!*Z4Dk@#!Mq@daUGx4q9YvVBx7xq5A>#4ZVC)|bT2TA zf&0Mj3x_!hl(c#nR44K+~wV!eF zxq \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..91704fca --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +Jiz4oh's Life

前言

因为需要在微博关注一些财经博主的更新,但又不想登录微博,遂研究了如何构建自己的 rss 平台,汇总感兴趣的信息。并使用 telegram 作为 rss 阅读器。

阅读全文 »

免流的由来

不知道各位是否还记得曾经被 5 元 30M 支配的恐惧吗?
在大概 10 多年前,那时候的流量非常昂贵,网页也非常简单,大部分都是一些文字信息,
随着时间的推移,互联网在飞速发展,手机上网从文字转变为了以视频和图片为主,网页内容越来越丰富,10M 流量几个图片就用完了,流量需求大幅提升,
但是运营商套餐价格并没有随着需求的提升而降低,一度都是 5 元 30M 的离谱价格,
人们为了(白嫖)推进运营商的技术发展,研究出了免流

什么是免流

“免流”通常是指在移动网络中使用特定的应用或服务而不消耗用户的数据流量,即上网不要钱。
通常由移动运营商或特定的应用提供商推出,旨在吸引更多用户使用这些应用或服务。

阅读全文 »

前言

Cloudflare Tunnel 是一款隧道软件,可以快速安全地加密应用程序到任何类型基础设施的流量,让您能够隐藏你的web 服务器IP 地址,阻止直接攻击,从而专注于提供出色的应用程序。

相比于 frp 等做内网穿透,Cloudflare Tunnels 的优势是不需要一台额外的公网服务器转发,并且可以享受到 Cloudflare CDN 带来的便利。

阅读全文 »

前言

Devise 是 ruby 世界中最常见的 gem 之一,是用于在 web 请求中做身份验证的,他的设计非常精妙,今天我们来尝试看看 devise 是如何设计的。

阅读全文 »

前言

一直对软路由这个东西念念不忘,想要入手折腾一下。最近因为家里需要一台服务器,我就把我的 n1 拿回了家里,转手就入了一个 j3455 的成品软路由,这边文章记录下在 pve 环境下折腾 openwrt 的心得,顺便学习下 pve(说不定能当个 IDC 呢

阅读全文 »

我经常用 GitHub 的 gist 服务来保存一些比较优秀的代码片段、配置等等,但是苦于 gist 在国内遭受了 DNS 污染,访问太不便利,所以一直在寻求一个类似 jsdelivr 加速 GitHub Repo 的方式,能够避免修改 host 直接访问 gist

阅读全文 »

前言

最近在思考如何优化 web 应用的响应速度,常见的方式就是使用缓存,而最常接触的技术其实是服务端做共享缓存,例如 redis 缓存热点数据、CDN 缓存静态资源。在客户端做缓存这一块,知识面还比较窄,经过资料查询和思考,总结一些心得

阅读全文 »

关于评论系统

博客搭建好之后,由于当时已经晚上12点了,并没有一鼓作气的开启评论系统,就一直拖到了现在。。。

今天终于想起来开启评论系统,在网上搜索了一下 hexo 的评论系统,有以下几个:

  • gitment:基于 GitHub Issue 作为评论存储仓库,但是一直长时间无人维护
  • gitalk:同 gitment 使用 Github Issue,且页面比 gitment 更精美
  • disqus:界面好看,支持 Facebook、twitter、Google+ 等账号登陆,但是 需要科学上网
  • valine:基于 LeanCloud 的无后端评论系统,支持匿名评论
  • 来必力:韩国的评论系统,功能强大,支持多社交帐号登录,链接不太稳定,不支持匿名评论

对于我来讲,我更偏向于使用 gitalk 作为评论系统,省心~但是 gitalk 类的评论系统被反馈有安全风险,虽然是 GitHub 的锅,但是会对访客(还不是我。。。)的账户安全带来很大的不确定性,所以最终我还是选择了支持匿名评论的 Valine

阅读全文 »
\ No newline at end of file diff --git a/js/comments.js b/js/comments.js new file mode 100644 index 00000000..9342b8a4 --- /dev/null +++ b/js/comments.js @@ -0,0 +1 @@ +window.addEventListener("tabs:register",()=>{let t=CONFIG.comments["activeClass"];if(CONFIG.comments.storage&&(t=localStorage.getItem("comments_active")||t),t){const e=document.querySelector(`a[href="#comment-${t}"]`);e&&e.click()}}),CONFIG.comments.storage&&window.addEventListener("tabs:click",t=>{t.target.matches(".tabs-comment .tab-content .tab-pane")&&(t=t.target.classList[1],localStorage.setItem("comments_active",t))}); \ No newline at end of file diff --git a/js/config.js b/js/config.js new file mode 100644 index 00000000..9de6a90a --- /dev/null +++ b/js/config.js @@ -0,0 +1 @@ +window.NexT||(window.NexT={}),function(){const c={};let i={};const r=e=>{var n=document.querySelector(`.next-config[data-name="${e}"]`);n&&(n=n.text,n=JSON.parse(n||"{}"),"main"===e?Object.assign(c,n):i[e]=n)};r("main"),window.CONFIG=new Proxy({},{get(e,n){let t;if(t=n in c?c[n]:(n in i||r(n),i[n]),n in e||"object"!=typeof t||(e[n]={}),n in e){const o=e[n];return"object"==typeof o&&"object"==typeof t?new Proxy({...t,...o},{set(e,n,t){return e[n]=t,o[n]=t,!0}}):o}return t}}),document.addEventListener("pjax:success",()=>{i={}})}(); \ No newline at end of file diff --git a/js/motion.js b/js/motion.js new file mode 100644 index 00000000..98d2276c --- /dev/null +++ b/js/motion.js @@ -0,0 +1 @@ +NexT.motion={},NexT.motion.integrator={queue:[],init:function(){return this.queue=[],this},add:function(e){e=e();return CONFIG.motion.async?this.queue.push(e):this.queue=this.queue.concat(e),this},bootstrap:function(){CONFIG.motion.async||(this.queue=[this.queue]),this.queue.forEach(e=>{const t=window.anime.timeline({duration:200,easing:"linear"});e.forEach(e=>{e.deltaT?t.add(e,e.deltaT):t.add(e)})})}},NexT.motion.middleWares={header:function(){const o=[];function e(e,t=!1){o.push({targets:e,opacity:1,top:0,deltaT:t?"-=200":"-=0"})}e(".column"),"Mist"===CONFIG.scheme&&o.push({targets:".logo-line",scaleX:[0,1],duration:500,deltaT:"-=200"}),"Muse"===CONFIG.scheme&&e(".custom-logo-image"),e(".site-title"),e(".site-brand-container .toggle",!0),e(".site-subtitle"),"Pisces"!==CONFIG.scheme&&"Gemini"!==CONFIG.scheme||e(".custom-logo-image");const t=CONFIG.motion.transition.menu_item;return t&&document.querySelectorAll(".menu-item").forEach(e=>{o.push({targets:e,complete:()=>e.classList.add("animated",t),deltaT:"-=200"})}),o},subMenu:function(){const e=document.querySelectorAll(".sub-menu .menu-item");return 0{e.classList.add("animated")}),[]},postList:function(){const o=[],{post_block:t,post_header:n,post_body:s,coll_header:i}=CONFIG.motion.transition;function a(t,e){t&&e.forEach(e=>{o.push({targets:e,complete:()=>e.classList.add("animated",t),deltaT:"-=100"})})}return document.querySelectorAll(".post-block").forEach(e=>{o.push({targets:e,complete:()=>e.classList.add("animated",t),deltaT:"-=100"}),a(i,e.querySelectorAll(".collection-header")),a(n,e.querySelectorAll(".post-header")),a(s,e.querySelectorAll(".post-body"))}),a(t,document.querySelectorAll(".pagination, .comments")),o},sidebar:function(){const t=[],e=document.querySelectorAll(".sidebar-inner"),o=CONFIG.motion.transition.sidebar;return!o||"Pisces"!==CONFIG.scheme&&"Gemini"!==CONFIG.scheme||e.forEach(e=>{t.push({targets:e,complete:()=>e.classList.add("animated",o),deltaT:"-=100"})}),t},footer:function(){return[{targets:document.querySelector(".footer"),opacity:1}]}}; \ No newline at end of file diff --git a/js/next-boot.js b/js/next-boot.js new file mode 100644 index 00000000..cc670b2a --- /dev/null +++ b/js/next-boot.js @@ -0,0 +1 @@ +NexT.boot={},NexT.boot.registerEvents=function(){NexT.utils.registerScrollPercent(),NexT.utils.registerCanIUseTag(),document.querySelector(".site-nav-toggle .toggle").addEventListener("click",e=>{e.currentTarget.classList.toggle("toggle-close");const t=document.querySelector(".site-nav");t&&(t.style.setProperty("--scroll-height",t.scrollHeight+"px"),document.body.classList.toggle("site-nav-on"))}),document.querySelectorAll(".sidebar-nav li").forEach((e,t)=>{e.addEventListener("click",()=>{NexT.utils.activateSidebarPanel(t)})}),window.addEventListener("hashchange",()=>{const e=location.hash;if(""!==e&&!e.match(/%\S{2}/)){const t=document.querySelector(`.tabs ul.nav-tabs li a[href="${e}"]`);t&&t.click()}})},NexT.boot.refresh=function(){CONFIG.prism&&window.Prism.highlightAll(),CONFIG.mediumzoom&&window.mediumZoom(".post-body :not(a) > img, .post-body > img",{background:"var(--content-bg-color)"}),CONFIG.lazyload&&window.lozad(".post-body img").observe(),CONFIG.pangu&&window.pangu.spacingPage(),CONFIG.exturl&&NexT.utils.registerExtURL(),NexT.utils.wrapTableWithBox(),NexT.utils.registerCopyCode(),NexT.utils.registerTabsTag(),NexT.utils.registerActiveMenuItem(),NexT.utils.registerLangSelect(),NexT.utils.registerSidebarTOC(),NexT.utils.registerPostReward(),NexT.utils.registerVideoIframe()},NexT.boot.motion=function(){CONFIG.motion.enable&&NexT.motion.integrator.add(NexT.motion.middleWares.header).add(NexT.motion.middleWares.postList).add(NexT.motion.middleWares.sidebar).add(NexT.motion.middleWares.footer).bootstrap(),NexT.utils.updateSidebarPosition()},document.addEventListener("DOMContentLoaded",()=>{NexT.boot.registerEvents(),NexT.boot.refresh(),NexT.boot.motion()}); \ No newline at end of file diff --git a/js/third-party/analytics/matomo.js b/js/third-party/analytics/matomo.js new file mode 100644 index 00000000..33a95fee --- /dev/null +++ b/js/third-party/analytics/matomo.js @@ -0,0 +1 @@ +if(CONFIG.matomo.enable){window._paq=window._paq||[];const a=window._paq;a.push(["trackPageView"]),a.push(["enableLinkTracking"]);const b=CONFIG.matomo.server_url;a.push(["setTrackerUrl",b+"matomo.php"]),a.push(["setSiteId",CONFIG.matomo.site_id]);const c=document,d=c.createElement("script"),e=c.getElementsByTagName("script")[0];d.async=!0,d.src=b+"matomo.js",e.parentNode.insertBefore(d,e)} \ No newline at end of file diff --git a/js/third-party/pace.js b/js/third-party/pace.js new file mode 100644 index 00000000..bd8d72c8 --- /dev/null +++ b/js/third-party/pace.js @@ -0,0 +1 @@ +Pace.options.restartOnPushState=!1,document.addEventListener("pjax:send",()=>{Pace.restart()}); \ No newline at end of file diff --git a/js/third-party/search/local-search.js b/js/third-party/search/local-search.js new file mode 100644 index 00000000..a1fcc7a0 --- /dev/null +++ b/js/third-party/search/local-search.js @@ -0,0 +1,3 @@ +document.addEventListener("DOMContentLoaded",()=>{if(CONFIG.path){const r=new LocalSearch({path:CONFIG.path,top_n_per_article:CONFIG.localsearch.top_n_per_article,unescape:CONFIG.localsearch.unescape}),a=document.querySelector(".search-input"),t=()=>{if(r.isfetched){const s=a.value.trim().toLowerCase();var t=s.split(/[-\s]+/);const c=document.querySelector(".search-result-container");let e=[];0'):0===e.length?(c.classList.add("no-result"),c.innerHTML='
'):(e.sort((e,t)=>e.includedCount!==t.includedCount?t.includedCount-e.includedCount:e.hitCount!==t.hitCount?t.hitCount-e.hitCount:t.id-e.id),t=CONFIG.i18n.hits.replace("${hits}",e.length),c.classList.remove("no-result"),c.innerHTML=`
${t}
+
+
    ${e.map(e=>e.item).join("")}
`,"object"==typeof pjax&&pjax.refresh(c))}};r.highlightSearchWords(document.querySelector(".post-body")),CONFIG.localsearch.preload&&r.fetchData(),"auto"===CONFIG.localsearch.trigger?a.addEventListener("input",t):(document.querySelector(".search-icon").addEventListener("click",t),a.addEventListener("keypress",e=>{"Enter"===e.key&&t()})),window.addEventListener("search:loaded",t),document.querySelectorAll(".popup-trigger").forEach(e=>{e.addEventListener("click",()=>{document.body.classList.add("search-active"),setTimeout(()=>a.focus(),500),r.isfetched||r.fetchData()})});const s=()=>{document.body.classList.remove("search-active")};document.querySelector(".search-pop-overlay").addEventListener("click",e=>{e.target===document.querySelector(".search-pop-overlay")&&s()}),document.querySelector(".popup-btn-close").addEventListener("click",s),document.addEventListener("pjax:success",()=>{r.highlightSearchWords(document.querySelector(".post-body")),s()}),window.addEventListener("keyup",e=>{"Escape"===e.key&&s()})}else console.warn("`hexo-generator-searchdb` plugin is not installed!")}); \ No newline at end of file diff --git a/js/third-party/tags/mermaid.js b/js/third-party/tags/mermaid.js new file mode 100644 index 00000000..9c971c02 --- /dev/null +++ b/js/third-party/tags/mermaid.js @@ -0,0 +1 @@ +document.addEventListener("page:loaded",()=>{const e=document.querySelectorAll(".mermaid");e.length&&NexT.utils.getScript(CONFIG.mermaid.js,{condition:window.mermaid}).then(()=>{e.forEach(e=>{const a=document.createElement("div");a.innerHTML=e.innerHTML,a.className=e.className;const t=e.parentNode;t.matches("pre")?t.parentNode.replaceChild(a,t):t.replaceChild(a,e)}),mermaid.initialize({theme:CONFIG.darkmode&&window.matchMedia("(prefers-color-scheme: dark)").matches?CONFIG.mermaid.theme.dark:CONFIG.mermaid.theme.light,logLevel:4,flowchart:{curve:"linear"},gantt:{axisFormat:"%m/%d/%Y"},sequence:{actorMargin:50}}),mermaid.init()})}); \ No newline at end of file diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 00000000..dda949d6 --- /dev/null +++ b/js/utils.js @@ -0,0 +1 @@ +HTMLElement.prototype.wrap=function(e){this.parentNode.insertBefore(e,this),this.parentNode.removeChild(this),e.appendChild(this)},function(){var e=()=>document.dispatchEvent(new Event("page:loaded",{bubbles:!0}));"loading"===document.readyState?document.addEventListener("readystatechange",e,{once:!0}):e(),document.addEventListener("pjax:success",e)}(),NexT.utils={registerExtURL:function(){document.querySelectorAll("span.exturl").forEach(e=>{const t=document.createElement("a");t.href=decodeURIComponent(atob(e.dataset.url).split("").map(e=>"%"+("00"+e.charCodeAt(0).toString(16)).slice(-2)).join("")),t.rel="noopener external nofollow noreferrer",t.target="_blank",t.className=e.className,t.title=e.title,t.innerHTML=e.innerHTML,e.parentNode.replaceChild(t,e)})},registerCopyCode:function(){let e=document.querySelectorAll("figure.highlight");0===e.length&&(e=document.querySelectorAll("pre:not(.mermaid)")),e.forEach(n=>{if(n.querySelectorAll(".code .line span").forEach(t=>{t.classList.forEach(e=>{t.classList.replace(e,"hljs-"+e)})}),CONFIG.copycode.enable){let e=n;"mac"!==CONFIG.copycode.style&&(e=n.querySelector(".table-container")||n),e.insertAdjacentHTML("beforeend",'
');const o=n.querySelector(".copy-btn");o.addEventListener("click",()=>{var e=(n.querySelector(".code")||n.querySelector("code")).innerText;if(navigator.clipboard)navigator.clipboard.writeText(e).then(()=>{o.querySelector("i").className="fa fa-check-circle fa-fw"},()=>{o.querySelector("i").className="fa fa-times-circle fa-fw"});else{const t=document.createElement("textarea");t.style.top=window.scrollY+"px",t.style.position="absolute",t.style.opacity="0",t.readOnly=!0,t.value=e,document.body.append(t),t.select(),t.setSelectionRange(0,e.length),t.readOnly=!1;e=document.execCommand("copy");o.querySelector("i").className=e?"fa fa-check-circle fa-fw":"fa fa-times-circle fa-fw",t.blur(),o.blur(),document.body.removeChild(t)}}),n.addEventListener("mouseleave",()=>{setTimeout(()=>{o.querySelector("i").className="fa fa-copy fa-fw"},300)})}})},wrapTableWithBox:function(){document.querySelectorAll("table").forEach(e=>{const t=document.createElement("div");t.className="table-container",e.wrap(t)})},registerVideoIframe:function(){document.querySelectorAll("iframe").forEach(t=>{if(["www.youtube.com","player.vimeo.com","player.youku.com","player.bilibili.com","www.tudou.com"].some(e=>t.src.includes(e))&&!t.parentNode.matches(".video-container")){const o=document.createElement("div");o.className="video-container",t.wrap(o);var e=Number(t.width),n=Number(t.height);e&&n&&(o.style.paddingTop=n/e*100+"%")}})},registerScrollPercent:function(){const n=document.querySelector(".back-to-top"),o=document.querySelector(".reading-progress-bar");window.addEventListener("scroll",()=>{if(n||o){var e=document.body.scrollHeight-window.innerHeight;const t=0e&&10{window.anime({targets:document.scrollingElement,duration:500,easing:"linear",scrollTop:0})})},registerTabsTag:function(){document.querySelectorAll(".tabs ul.nav-tabs .tab").forEach(s=>{s.addEventListener("click",e=>{if(e.preventDefault(),!s.classList.contains("active")){const n=s.parentNode,o=n.nextElementSibling;o.style.overflow="hidden",o.style.transition="height 1s";var t=o.querySelector(".active")||o.firstElementChild,e=parseInt(window.getComputedStyle(t).height.replace("px",""),10)||0;const i=parseInt(window.getComputedStyle(t).paddingTop.replace("px",""),10),a=parseInt(window.getComputedStyle(t.firstElementChild).marginBottom.replace("px",""),10);o.style.height=e+i+a+"px",[...n.children].forEach(e=>{e.classList.toggle("active",e===s)});const r=document.getElementById(s.querySelector("a").getAttribute("href").replace("#",""));[...r.parentNode.children].forEach(e=>{e.classList.toggle("active",e===r)}),r.dispatchEvent(new Event("tabs:click",{bubbles:!0}));const c=document.body.scrollHeight>(window.innerHeight||document.documentElement.clientHeight);e=parseInt(window.getComputedStyle(o.querySelector(".active")).height.replace("px",""),10);o.style.height=e+i+a+"px",setTimeout(()=>{var e;document.body.scrollHeight>(window.innerHeight||document.documentElement.clientHeight)!=c&&(o.style.transition="height 0.3s linear",e=parseInt(window.getComputedStyle(o.querySelector(".active")).height.replace("px",""),10),o.style.height=e+i+a+"px"),setTimeout(()=>{o.style.transition="",o.style.height=""},250)},1e3),CONFIG.stickytabs&&(e=n.parentNode.getBoundingClientRect().top+window.scrollY+10,window.anime({targets:document.scrollingElement,duration:500,easing:"linear",scrollTop:e}))}})}),window.dispatchEvent(new Event("tabs:register"))},registerCanIUseTag:function(){window.addEventListener("message",({data:e})=>{var t;"string"==typeof e&&e.includes("ciu_embed")&&(t=e.split(":")[1],e=e.split(":")[2],document.querySelector(`iframe[data-feature=${t}]`).style.height=parseInt(e,10)+5+"px")},!1)},registerActiveMenuItem:function(){document.querySelectorAll(".menu-item a[href]").forEach(e=>{var t=e.pathname===location.pathname||e.pathname===location.pathname.replace("index.html",""),n=!CONFIG.root.startsWith(e.pathname)&&location.pathname.startsWith(e.pathname);e.classList.toggle("menu-item-active",e.hostname===location.hostname&&(t||n))})},registerLangSelect:function(){const e=document.querySelectorAll(".lang-select");e.forEach(e=>{e.value=CONFIG.page.lang,e.addEventListener("change",()=>{const t=e.options[e.selectedIndex];document.querySelectorAll(".lang-select-label span").forEach(e=>{e.innerText=t.text}),window.location.href=t.dataset.href})})},registerSidebarTOC:function(){this.sections=[...document.querySelectorAll(".post-toc li a.nav-link")].map(t=>{const n=document.getElementById(decodeURI(t.getAttribute("href")).replace("#",""));return t.addEventListener("click",e=>{e.preventDefault();e=n.getBoundingClientRect().top+window.scrollY;window.anime({targets:document.scrollingElement,duration:500,easing:"linear",scrollTop:e,complete:()=>{history.pushState(null,document.title,t.href)}})}),n})},registerPostReward:function(){const e=document.querySelector(".reward-container button");e&&e.addEventListener("click",()=>{document.querySelector(".post-reward").classList.toggle("active")})},activateNavByIndex:function(e){const t=document.querySelectorAll(".post-toc li a.nav-link")[e];if(t&&!t.classList.contains("active-current")){document.querySelectorAll(".post-toc .active").forEach(e=>{e.classList.remove("active","active-current")}),t.classList.add("active","active-current");let e=t.parentNode;for(;!e.matches(".post-toc");)e.matches("li")&&e.classList.add("active"),e=e.parentNode;const n=document.querySelector("Pisces"===CONFIG.scheme||"Gemini"===CONFIG.scheme?".sidebar-panel-container":".sidebar");document.querySelector(".sidebar-toc-active")&&window.anime({targets:n,duration:200,easing:"linear",scrollTop:n.scrollTop-n.offsetHeight/2+t.getBoundingClientRect().top-n.getBoundingClientRect().top})}},updateSidebarPosition:function(){if(!(window.innerWidth<1200||"Pisces"===CONFIG.scheme||"Gemini"===CONFIG.scheme)){var t=document.querySelector(".post-toc");let e=CONFIG.page.sidebar;"boolean"!=typeof e&&(e="always"===CONFIG.sidebar.display||"post"===CONFIG.sidebar.display&&t),e&&window.dispatchEvent(new Event("sidebar:show"))}},activateSidebarPanel:function(e){const t=document.querySelector(".sidebar-inner"),n=document.querySelector(".sidebar-panel-container"),o=["sidebar-toc-active","sidebar-overview-active"];t.classList.contains(o[e])||window.anime({duration:200,targets:n,easing:"linear",opacity:0,translateY:[0,-20],complete:()=>{t.classList.replace(o[1-e],o[e]),window.anime({duration:200,targets:n,easing:"linear",opacity:[0,1],translateY:[-20,0]})}})},getScript:function(o,e={},t){if("function"==typeof e)return this.getScript(o,{condition:t}).then(e);const{condition:i=!1,attributes:{id:a="",async:r=!1,defer:c=!1,crossOrigin:s="",dataset:l={},...d}={},parentNode:u=null}=e;return new Promise((e,t)=>{if(i)e();else{const n=document.createElement("script");a&&(n.id=a),s&&(n.crossOrigin=s),n.async=r,n.defer=c,Object.assign(n.dataset,l),Object.entries(d).forEach(([e,t])=>{n.setAttribute(e,String(t))}),n.onload=e,n.onerror=t,"object"==typeof o?({url:e,integrity:t}=o,n.src=e,t&&(n.integrity=t,n.crossOrigin="anonymous")):n.src=o,(u||document.head).appendChild(n)}})},loadComments:function(o,e){return e?this.loadComments(o).then(e):new Promise(n=>{var e=document.querySelector(o);if(CONFIG.comments.lazyload&&e){const t=new IntersectionObserver((e,t)=>{e[0].isIntersecting&&(n(),t.disconnect())});t.observe(e)}else n()})}}; \ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html new file mode 100644 index 00000000..97983b53 --- /dev/null +++ b/page/2/index.html @@ -0,0 +1,2 @@ +Jiz4oh's Life

前言

Vue-cli 默认配置了一个使用 @ 表示 src 的功能,这个功能的原理是配置 webpack 解析路径,这篇文章来介绍如何配置 Webpack 使其他项目比如 React 也能使用这个功能

阅读全文 »

前言

在 vim 中使用编辑模式进行了中文输入之后,切换到普通模式时,必须手动切换到英文模式才能进行命令输入,不太方便,故在网上找了找如何自动切换中英文的解决方案

阅读全文 »

前言

出于对国产输入法软件的不信任,再加上国产输入法广告太多等因素的影响,促使我想要尽快找到一个开源、快捷、不自动上传云端的输入法。

在网上搜索时,Rime 收到了一致好评,让我萌生了非常大的兴趣,故特意找到了 Rime 的相关资料并整理。

本文基于 Rime v1.5.3 版本进行整理,其他版本可能不适用。

阅读全文 »

前言

常在河边走,哪有不湿鞋 –鲁迅

作为多年使用 JetBrains 盗版软件的不法用户,昨日终于被逮到了,经历各种再破解尝试,均告失败。
只好转战最近广受好评的 vscode,准备接下来试试能不能作为主力开发工具。
本篇记录如何在 vscode 配置开发 ruby 环境

阅读全文 »

什么是 CSRF 攻击

CSRF 的全名是 Cross Site Request Forgery,翻译成中文就是跨站点请求伪造

CSRF 利用的是网站对用户网页浏览器的信任,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。
由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

阅读全文 »

前言

Ruby 用了也接近一年了,有很多黑魔法让我感到非常有意思,刚开始学习的时候也经常让我不知所措。
其中很有意思的一点就是 includeextend 这两个方法来变相实现 多重继承,即代码的复用。但是用归用,却没有细细的研究其中的实现,故此,今天好好的看一看。

阅读全文 »

前言

经常写 python 的开发一定苦恼过一个问题,那就是如何安装 python 环境。经常遇到如下问题:

  1. 许多系统默认只自带 python2,导致现在很多使用 python3 的程序无法正常运行
  2. python3 各版本间也不是完全兼容,某些第三库只支持特定的 python 版本

这些问题导致了我们经常会在各个版本间进行切换,甚至每个项目的 python 版本都不尽相同。所以我们需要一个环境管理器来帮助我们管理各个版本,这就用到了 pyenv

而当我们项目过多之后,每个项目的依赖包就会有多个版本。这些依赖包的管理就需要用到 pipenv

阅读全文 »
\ No newline at end of file diff --git a/page/3/index.html b/page/3/index.html new file mode 100644 index 00000000..b0212ff3 --- /dev/null +++ b/page/3/index.html @@ -0,0 +1,4 @@ +Jiz4oh's Life

原理

lsblk:查看当前固件的分区信息

NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
+sda            8:0    1 58.6G  0 disk
+├─sda1         8:1    1  16M  0 part
+├─sda2         8:2    1  300M  0 part /mnt/sda4

image-20200711142556881

sda 2 中的浅蓝色和深蓝色区域为底层 Squash 格式,该格式只读,不支持修改,优势是可以在出错时轻松重置

overlay 就是在 upper layer 层进行读写的形式

image-20200711143340836

overlay 的扩容并不是在 sda2 上进行操作,而是新建一个更大的分区 sda3,并将 Overlay 指向 sda3 ,这样的话,重置 sda2 后并不会损坏 sda3 中的配置

阅读全文 »

前言

图床相信经常写东西而且文章图文并茂的朋友多多少少都接触过

公共图床服务要不就是备案麻烦,要不就是免费不稳定

本文就是关于如何利用 Github+Jsdelivr 创建免费、大容量、高速的个人图床,并且利用 vscode+PicGo 实现图片自动上传

阅读全文 »

ehforwarderbot 介绍

ehforwarderbot 是由 blueset 开源在 github 的一款专用于转发微信/QQ/Faceboot Message 到 Telegram 的机器人

ehforwarderbot 支持 多微信,多QQ 汇集到一起集中处理消息,特别适合:

  1. Telegram 重度用户
  2. 需要对多个微信、qq 消息进行处理的用户
阅读全文 »

创建本地博客 blog

安装 Hexo 脚手架

npm install -g hexo-cli

安装之后可以使用 hexo 对博客命令操作

  1. 创建 blog:hexo init 文件夹
  2. 创建文章:hexo new 文章名
  3. 生成静态文件:hexo generate 简写:hexo g
  4. 清除已生成缓存:hexo clean
  5. 启动本地服务:hexo server 简写:hexo s
    默认监听 4000 端口
  6. 部署:hexo deploy 简写:hexo d
  7. 生成静态文件并部署:hexo g -d
阅读全文 »
\ No newline at end of file diff --git a/placeholder b/placeholder deleted file mode 100644 index e69de29b..00000000 diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..4c2671ca --- /dev/null +++ b/robots.txt @@ -0,0 +1,17 @@ +#hexo robots.txt +User-agent: * + +Allow: / +Allow: /archives/ +Allow: /categories/ +Allow: /tags/ + +Disallow: /vendors/ +Disallow: /js/ +Disallow: /css/ +Disallow: /fonts/ +Disallow: /vendors/ + +Sitemap: https://jiz4oh.com/search.xml +Sitemap: https://jiz4oh.com/sitemap.xml +Sitemap: https://jiz4oh.com/baidusitemap.xml diff --git a/search.xml b/search.xml new file mode 100644 index 00000000..ff4d2ac6 --- /dev/null +++ b/search.xml @@ -0,0 +1,4279 @@ + + + + 前后端分离下如何防止 CSRF 攻击 + /2020/10/anti-csrf-on-separation-of-frontend-and-backend/ + 什么是 CSRF 攻击

CSRF 的全名是 Cross Site Request Forgery,翻译成中文就是跨站点请求伪造

+

CSRF 利用的是网站对用户网页浏览器的信任,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。
由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

+ + +

例子:

+

假如一家银行用以运行转账操作的 URL 地址如下:https://bank.example.com/withdraw?account=AccoutName&amount=1000&for=PayeeName

+

那么,一个恶意攻击者可以在另一个网站上放置如下代码:<img src="https://bank.example.com/withdraw?account=Alice&amount=1000&for=Badman" />

+

如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金

+

常见的防御手段

验证码

CSRF 攻击往往是在用户不知情的情况下伪造了请求,而验证码强制用户必须进行交互

+
    +
  • 措施:给敏感操作添加验证码校验
  • +
  • 局限:影响用户体验
  • +
+

检查 Referer 字段

在处理敏感请求时,通常发出请求的网址应该与接受请求的网址属于同一域名

+
    +
  • 措施:所以我们可以通过校验 HTTP headers 中的 Referer 字段是否是属于本网站,如果不是则可能遭受 CSRF 攻击
  • +
  • 局限:这种办法简单易行,工作量低,但是依赖于浏览器的发送正确的 Referer 字段,无法保证浏览器没有安全漏洞影响 Referer 的发送
  • +
+

不使用 GET 请求做敏感操作

在上面的例子中,我们可以知道,Alice 打开恶意站点后,浏览器通过 img 标签向银行转账网址发送了 GET 请求从而实现了 CSRF 攻击。

+
    +
  • 措施:所以,避免 CSRF 攻击最基础的一条是 永远不使用 GET 请求做敏感操作
  • +
  • 局限:这种方法可以避免 imgscriptiframe 等带 src 属性的标签发起 CSRF 攻击。但是无法防止攻击者构建 POST 请求等发起 CSRF 攻击
  • +
+

使用 Anti-CSRF-Token

CSRF 为什么能够攻击成功?其本质原因是重要操作的所有参数都是可以被攻击者猜测到的。攻击者只有预测出 URL 的所有参数与参数值,才能成功地构造一个伪造的请求;反之,攻击者将无法攻击成功。

+
    +
  • 措施:所以我们可以生成一个足够随机的参数 Token 放入该次请求,保证该次请求无法被攻击者成功伪造。

    +

    核心思路:使用一个由服务器派发的 Token,在前端进行状态修改时,也同时提交这个 Token(往往会放在 html forminput 中,或者 ajax header 中),这时候服务端验证该 Token 是否是之前所生成的,以此来判断这个请求是否被允许

    +
  • +
  • 局限

    +
      +
    1. 依赖于 Token 足够随机
    2. +
    3. 无论是 Token 放在哪里,只要 JS 能读取到,都会面临 XSS 风险
    4. +
    +
  • +
+

在常见的 Web 后端框架比如 RailsDjango 中都自带实现了 csrf_token 功能,不同的是

+
    +
  • Rails 使用 meta 标签存放 csrf_token,通过 HTTP headers 发送
  • +
  • Django 使用隐藏的 input 标签存放 csrf_token,通过 POST body 发送
  • +
+

前后端分离带来的问题

在现在大前端时代,前端后分离是个再常见不过的情况,我们该如何在前后端分离的情况下防止 CSRF 攻击呢?

+

首先,我们分析下上面所提到的几种手段在前后端分离下发生了什么变化:

+
    +
  • 验证码:
      +
    • form 表单完全由前端生成,后端是无法感知的,即使前端使用某种方式在 form 中放入了验证码,但是后端也无法验证
    • +
    +
  • +
  • 检查 Referer 字段:无变化
  • +
  • 不使用 GET 请求做敏感操作:无变化
  • +
  • 使用 Anti-CSRF-Token:
      +
    • form 表单完全由前端生成,后端是无法感知的,即使前端使用某种方式在 form 中放入了 Token,但是后端也无法验证
    • +
    +
  • +
+

所以根本上是前后端分离之后,后端无法控制 form 生成时机,而前端则必须通过某种方式获取到由后端生成的 验证码 或者 Token 才能保证后端能够校验请求

+
    +
  • 措施
      +
    1. 后端引入安全模块,可能是写在 WAF 里,也有可能是 Security Sidecar 或者自定义的 API Gateway
    2. +
    3. 前端请求安全模块,由安全模块生成 csrf_token 并返回,在提交时验证请求
    4. +
    +
  • +
  • 局限:依然无法避免 XSS 攻击
  • +
+

XSRF

CSRFToken 仅仅用于对抗 CSRF 攻击,当网站还同时存在 XSS 漏洞时,这个方案就会变得无效。
因为 XSS 可以模拟客户端浏览器执行任意操作,在 XSS 攻击下,攻击者完全可以请求页面后,读出页面内容里的 Token 值,然后再构造出一个合法的请求。这个过程就被称之为 XSRF

+

XSS 带来的问题,应该使用 XSS 的防御方案予以解决,否则 CSRFToken 防御就是空中楼阁。安全防御的体系是相辅相成、缺一不可的

+

迷思

    +
  • Q:以下攻击能成功吗?

    +
      +
    1. 攻击者构建了一个恶意网站
    2. +
    3. 恶意网站先通过请求上述所说的安全模块获取 csrf_token
    4. +
    5. 恶意网站构造隐藏的 form 表单并对参数进行伪造
    6. +
    7. 利用 JS 自动提交(恶意网站是攻击者所有,所以绕过了 XSS 防御策略) form 并携带 csrf_token 给目标网站
    8. +
    +

    A:不会,因为浏览器的 同源策略 不允许恶意网站获取安全模块所返回的结果(csrf_token),所以攻击在第 2 步时就会失败

    +
  • +
+

参考

白帽子讲Web安全

+

跨站请求伪造

+

CSRF - 前后端分离后带来的新问题

+]]>
+ + 安全 + + + think + csrf + +
+ + 构建自己的 rss 平台 + /2023/04/build-own-rss-platform/ + 前言

因为需要在微博关注一些财经博主的更新,但又不想登录微博,遂研究了如何构建自己的 rss 平台,汇总感兴趣的信息。并使用 telegram 作为 rss 阅读器。

+ + +

Install

    +
  1. 安装 RSSHub 作为 rss 平台

    +
    mkdir rsshub
    +cd rsshub
    +wget https://raw.githubusercontent.com/DIYgod/RSSHub/master/docker-compose.yml
    +docker volume create redis-data
    +docker compose up -d
    + +

    更新

    +
    docker compose down && docker compose pull && docker compose up -d
    +
  2. +
  3. 安装 RSS-to-Telegram-Bot 以便从 telegram 上订阅 rss

    +
    mkdir rsstt
    +cd rsstt
    +wget https://raw.githubusercontent.com/Rongronggg9/RSS-to-Telegram-Bot/dev/docker-compose.yml.sample -O docker-compose.yml
    +vi docker-compose.yml  # fill in env variables
    +docker compose up -d
    + +

    更新

    +
    docker compose down && docker compose pull && docker compose up -d
    + +
  4. +
+

Setup

有三种使用方式

+
    +
  1. 直接使用 Bot 订阅所有 rss
  2. +
  3. 将 Bot 添加到频道中,然后在频道中订阅 rss
      +
    1. 创建一个公开频道,并输入 《频道名》,比如 test_rss_hub
      upgit_20230412_1681283739.png
    2. +
    3. 将 Bot 添加到频道中
    4. +
    5. 私聊 Bot 发送 /user_info @频道名,比如 /user_info @test_rss_hub
    6. +
    7. 出现如下选项,点击 将用户状态设置为“用户”
      upgit_20230412_1681283864.png
    8. +
    +
  4. +
  5. 将 Bot 添加到群组中,然后在群组中订阅 rss
      +
    1. 步骤与添加至频道类似
    2. +
    +
  6. +
+

Tips: 必须先将频道/群组设置为 public,这样才能设置 Bot 发送到指定频道/群组。如果需要私有频道/群组,只需要在设置完成将频道/群组转为 private 即可。

+]]>
+ + 其他 + + + rss + 自建 + +
+ + 使用 charles 调试 + /2020/10/charles/ + 安装

点击去官网下载

+

激活

Registered Name: https://zhile.io
+License Key: 48891cf209c6d32bf4
+ + + +

开启 http 调试

    +
  1. 点击菜单栏 Proxy -> Proxy Settings
  2. +
  3. 填写 Port
  4. +
  5. 勾选 Enable transparent HTTP proxying
  6. +
+

20201009171421

+

开启抓取 macOS 请求

    +
  1. 点击菜单栏 Proxy -> Proxy Settings
  2. +
  3. 点击 macOS
  4. +
  5. 勾选 Enable macOS proxy
  6. +
  7. 勾选 Use HTTP proxy
  8. +
+

20201009171951

+

开启抓取 Android 请求

手机连接局域网下 wifi,与 charles 必须为同一网络下。

+
    +
  1. 自动开启代理

    +
      +
    1. 在已连接的 wifi 上点击更多,进入配置代理页。

      +
    2. +
    3. 勾选自动,在输入框URL中输入:

      +
      Https://chls.pro/10.10.11.235:6666.pac
      +
    4. +
    +
  2. +
  3. 手动设置代理

    +
      +
    1. 在已连接的 wifi 上点击更多,进入配置代理页。
    2. +
    3. 勾选手动,输入 charles 的 ip 与端口(ip 为 macOS 主机 ip,端口为上面填写的 http 端口)
    4. +
    +
  4. +
+

设置 https

    +
  1. 点击 Proxy -> SSL Proxying Settings
    20201009173147
  2. +
  3. 勾选 Enable SSL Proxying
  4. +
  5. 点击 Add,Host 填写 *,Port 填写 443
    20201009173256
  6. +
+

macOS 安装证书

    +
  1. 点击菜单栏 Help -> SSL Proxying -> Install Charles Root Certificate
    20201009172253
  2. +
  3. 找到 Charles Proxy..... 并点击
  4. +
  5. 点击 Trust -> When using this certificate,并勾选 Always Trust
    20201009172635
  6. +
+

Android 安装证书

Android7 以后,系统不再信任用户级的证书,只信任系统级的证书,所以要抓包就需要把我们的 charles 证书安装至 Android 的系统目录中

+

准备

    +
  1. 一台已 root 的手机
  2. +
  3. Openssl
  4. +
+

证书生成

    +
  1. Filddler 或者其他抓包程序的证书导出,一般为 xxx.cer 或者 xxx.pem

    +
  2. +
  3. 使用 opensslx509 指令进行 cer 证书转 pem 证书 和 用 md5 方式显示 pem 证书的 hash

    +
    # 1. 证书转换,已经是 pem 格式的证书不需要执行这一步
    +openssl x509 -inform DER -in xxx.cer -out cacert.pem
    +
    +# 2. 进行 MD5 的 hash 显示
    +
    +# openssl 版本在 1.0 以上的版本的执行这一句
    +openssl x509 -inform PEM -subject_hash_old -in cacert.pem
    +
    +# openssl 版本在 1.0 以下的版本的执行这一句
    +openssl x509 -inform PEM -subject_hash -in cacert.pem
    + +

    将第二条指令输出的类似 347bacb5 的值进行复制
    tips:查看 openssl 版本的指令 openssl version

    +
  4. +
  5. 将 pem 证书重命名
    使用上面复制的值(类似于 347bacb5)对 pem 证书进行重命名

    +
    mv cacert.pem 347bacb5.0
    +
  6. +
  7. 将新证书放入手机系统证书目录(/system/etc/security/cacerts)

    +

    需要拷贝至此目录必须拥有 root 权限

    +
  8. +
  9. 重启 Android 设备以生效
    拷贝证书至 /system/etc/security/cacerts 之后,重启手机就可以使证书生效了

    +
  10. +
+

参考

charles 安装配置 for Mac

+

给 Android7 及以上的手机安装系统级证书,实现 Fiddler 或者其他程序的 HTTPS 的抓包

+]]>
+ + 测试 + +
+ + chrome 为什么不会自动过期 expires 为 session 的 cookie + /2020/10/chrome-cookie-do-not-delete/ + 前言

今天对自己的项目做前后端联调时,发现在 mac 的 chrome 下(未测试 windows)没有自动过期 expires 为 session 的 cookie,对此感到一些疑惑

+ + +

尝试

在网上搜索的时候,发现了 这篇文章,遂按照网友的解决方案,尝试了如下行为:

+
    +
  1. 点击作左上角 x,结果:无效
  2. +
  3. Command + Q 退出,结果:无效
  4. +
  5. 右键 Chrome 图标退出,结果:无效
  6. +
+

很难受,网友提供的方法都无效

+

解决

    +
  1. 点击 chrome 设置
  2. +
  3. 默认浏览器 -> 启动时 -> 取消勾选 继续浏览上次打开的网页,勾选其他两个选项
  4. +
+

原因

为了增强用户体验,chrome 会在 继续浏览上次打开的网页 设置下,默认不删除会话 cookie

+]]>
+ + 其他 + + + chrome + +
+ + 使用 Cloudflare Tunnels 进行内网穿透 + /2023/01/cloudflare-tunnels/ + 前言

Cloudflare Tunnel 是一款隧道软件,可以快速安全地加密应用程序到任何类型基础设施的流量,让您能够隐藏你的web 服务器IP 地址,阻止直接攻击,从而专注于提供出色的应用程序。

+

相比于 frp 等做内网穿透,Cloudflare Tunnels 的优势是不需要一台额外的公网服务器转发,并且可以享受到 Cloudflare CDN 带来的便利。

+ + +

使用 frp 做内网穿透的流程如下:

+
+flowchart LR
+  server[Frp Server]
+  clients[Frp Clients]
+
+User -->|http/https| server -->|http/https| clients
+
+ +

使用 Cloudflare Tunnels 做内网穿透的流程如下:

+
+flowchart LR
+  tunnels[Cloudflare CDN]
+  clients[Original Clients]
+
+User -->|http/https| tunnels -->|Cloudflare Tunnels|clients
+
+ +

前置条件

需要有一个由 Cloudflare 管理的域名

+

配置

    +
  1. 安装 cloudflared

    +

    debian:

    +
    apt install cloudflared
    + +

    或者下载最新版

    +
    wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -O cloudflared.deb && dpkg -i cloudflared.deb && rm cloudflared.deb
    +
  2. +
  3. 登录 cloudflared

    +
    cloudflared tunnel login
    + +

    随后会生成一个 ~/.cloudflared/cert.pem 文件用于后续授权

    +
  4. +
  5. 创建一个隧道 tunnel

    +
    cloudflared tunnel create <名字>
    + +

    随后会在 ~/.cloudflared/ 目录下出现一个 json 文件 [Tunnel-UUID].json,里面保存着运行这条隧道所需要的授权信息。

    +
  6. +
  7. 创建一个配置文件,~/.cloudflared/tunnels/名字.yaml,并添加

    +
    # 反代的源站地址
    +url: http://localhost:8080
    +tunnel: <Tunnel-UUID>
    +credentials-file: /root/.cloudflared/<Tunnel-UUID>.json
    + +

    如果在上一步没有保存 UUID 信息,可以通过 cloudflared tunnel info 获取

    +
  8. +
  9. 配置路由

    +
    cloudflared tunnel route dns [名字或者 Tunnel-UUID] [想要绑定到的域名或其二级域名]
    +
  10. +
  11. 启动 cloudflared

    +
    cloudflared tunnel --config ~/.cloudflared/tunnels/名字.yaml run
    + +
  12. +
+

之后就可以通过在第 5 步 设置的域名进行访问

+

范例

例如,创建一条隧道从公网访问家里的 clash dashboard(地址为 192.168.1.1:9090),设置隧道名字为 clash,并将其绑定到 clash.example.com:

+
    +
  1. cloudflared tunnel login 授权

    +
  2. +
  3. cloudflared tunnel create clash,假设输出 Tunnel-UUID 为 c91c37a3-3efd-47fb-af0d-e5676ac122b9

    +
  4. +
  5. 将下面配置添加到 ~/.cloudflared/tunnels/clash.yaml

    +
    url: http://192.168.1.1:9090
    +tunnel: c91c37a3-3efd-47fb-af0d-e5676ac122b9
    +credentials-file: /root/.cloudflared/c91c37a3-3efd-47fb-af0d-e5676ac122b9.json
    +
  6. +
  7. cloudflared tunnel route dns clash clash.example.com 创建路由

    +
  8. +
  9. cloudflared tunnel --config ~/.cloudflared/tunnels/clash.yaml run 启动隧道

    +
  10. +
+

此时,访问 clash.example.com 等同于内网访问 192.168.1.1:9090

+

配置 systemd service 自动启动

    +
  1. /etc/systemd/system 目录下创建 service 配置文件 EXAMPLE.service

    +
    [Unit]
    +Description=Cloudflare Tunnel
    +After=network.target
    +StartLimitIntervalSec=0
    +
    +[Service]
    +Type=simple
    +Restart=always
    +RestartSec=1
    +User=root
    +ExecStart=cloudflared tunnel --config /root/.cloudflared/tunnels/名字.yaml run
    +
    +[Install]
    +WantedBy=multi-user.target
    +
  2. +
  3. 启动服务

    +
    systemctl enable EXAMPLE
    +systemctl start EXAMPLE
    + +
  4. +
+]]>
+ + 网络 + + + cloudflare + +
+ + 使用 Cloudflare Workers 反代 gist + /2020/12/cloudflare-workers/ + 我经常用 GitHub 的 gist 服务来保存一些比较优秀的代码片段、配置等等,但是苦于 gist 在国内遭受了 DNS 污染,访问太不便利,所以一直在寻求一个类似 jsdelivr 加速 GitHub Repo 的方式,能够避免修改 host 直接访问 gist

+ + +

可行的方式

    +
  1. raw.githack.com
  2. +
  3. 反代加速
  4. +
+

首先说说第一种,raw.githack.com 确实可以加速 gist,而且可以加速 repo。
使用方式很简单

+ +

但是这种方式有一点弊端,就是不方便发表永链,当首次访问时,githack 会将内容缓存在 cloudflare 长达一年。当链接内容变化时,不会及时刷新,只适合发布永久内容,或者分版本发布。

+

第二种方式就是我经过一段时间摸索并决定采用的方式

+

Cloudflare Workers

Workers 的工作原理就是最近几年火热的 serverless。网站管理员不再一定需要一个服务器,只需要将对应的 Function 托管在 Workers 上,当用户访问网站时就会执行对应的 Function,值得一提的是 Cloudflare Workers 本质上只支持 JS(其他语言通过编译成 js 来执行)。

+

前期准备

    +
  1. cloudflare 的帐号
  2. +
  3. 域名,并且托管到 cloudflare
  4. +
+

创建 worker

    +
  1. 登录帐号到下图界面

    +

    20201227143715

    +
  2. +
  3. 然后进入 Workers

    +

    20201227143837

    +
  4. +
  5. 点击创建 worker

    +

    20201227144011

    +
  6. +
  7. 这时候会进入如下界面

    +

    20201227144908

    +
  8. +
+

反代加速 gist 代码如下:

+
// 需要反代的地址
+const upstream = 'gist.github.com'
+// 反代地址的子路径
+const upstreamPath = '/'
+// 反代网站的移动端域名
+const upstreamMobile = 'gist.github.com'
+
+// 是否使用 https
+const useHttps = true
+
+// 禁止使用该 worker 的国家代码
+const blockedRegion = ['KP', 'SY', 'PK', 'CU']
+
+// 禁止使用该 worker 的 ip 地址
+const blockedIp = ['0.0.0.0', '127.0.0.1']
+
+// 是否关闭缓存
+const disableCache = false
+// 替换条件
+const contentTypes = [
+  'text/plain',
+  'text/html'
+]
+// 反代网站中其他需要被替换的地址
+const replaceDict = {
+  '$upstream': '$workerDomain',
+}
+
+addEventListener('fetch', event => {
+  event.respondWith(handleRequest(event.request))
+})
+
+/**
+ * Respond to the request
+ * @param {Request} request
+ */
+async function handleRequest(request) {
+  const region = request.headers.get('cf-ipcountry') || '';
+  const ip = request.headers.get('cf-connecting-ip');
+
+  if (blockedRegion.includes(region.toUpperCase())) {
+    return new Response('Access denied: WorkersProxy is not available in your region yet.', {
+      status: 403
+    });
+  }
+
+  if (blockedIp.includes(ip)) {
+    return new Response('Access denied: Your IP address is blocked by WorkersProxy.', {
+      status: 403
+    });
+  }
+
+  const upstreamDomain = isMobile(request.headers.get('user-agent')) ? upstreamMobile : upstream;
+
+  // 构建上游请求地址
+  let url = new URL(request.url);
+  const workerDomain = url.host;
+  
+  url.protocol = useHttps ? 'https:' : 'http';
+  url.pathname = url.pathname === '/' ? upstreamPath : upstreamPath + url.pathname;
+  url.host = upstreamDomain;
+
+  // 构建上游请求头
+  const newRequestHeaders = new Headers(request.headers);
+  newRequestHeaders.set('Host', upstreamDomain);
+  newRequestHeaders.set('Referer', url.protocol + '//' + workerDomain);
+
+  // 获取上游响应
+  const originalResponse = await fetch(url.href, {
+    method: request.method,
+    headers: newRequestHeaders
+  })
+
+  const connectionUpgrade = newRequestHeaders.get("Upgrade");
+  if (connectionUpgrade && connectionUpgrade.toLowerCase() === "websocket") {
+    return originalResponse;
+  }
+
+  let originalResponseClone = originalResponse.clone();
+
+  // 构建响应头
+  let responseHeaders = originalResponseClone.headers;
+  let newResponseHeaders = buildResponseHeaders(responseHeaders);
+  if (newResponseHeaders.get("x-pjax-url")) {
+    newResponseHeaders.set("x-pjax-url", responseHeaders.get("x-pjax-url").replace("//" + upstreamDomain, "//" + workerDomain));
+  }
+
+  // 构建响应体
+  let originalText;
+  const contentType = newResponseHeaders.get('content-type');
+  if (contentType != null) {
+    const types = contentType.replace(' ','').split(';')
+    if (types.includes('charset=utf-8')){
+      for (let i of contentTypes) {
+        if (types.includes(i)){
+          originalText = await replaceResponseText(originalResponseClone, upstreamDomain, workerDomain);
+          break
+        }
+      }
+    }
+  } else {
+    originalText = originalResponseClone.body
+  }
+
+  return new Response(originalText, {
+    status: originalResponseClone.status,
+    headers: newResponseHeaders
+  })
+}
+
+function isMobile(userAgent) {
+  userAgent = userAgent || ''
+  let agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
+  for (let v = 0; v < agents.length; v++) {
+    if (userAgent.indexOf(agents[v]) > 0) {
+      return true;
+    }
+  }
+}
+
+function buildResponseHeaders(originalHeaders) {
+  const result = new Headers(originalHeaders);
+  if (disableCache) {
+    result.set('Cache-Control', 'no-store');
+  }
+  result.set('access-control-allow-origin', '*');
+  result.set('access-control-allow-credentials', true);
+  result.delete('content-security-policy');
+  result.delete('content-security-policy-report-only');
+  result.delete('clear-site-data');
+
+  return result
+}
+
+async function replaceResponseText(response, upstreamDomain, workerDomain) {
+  let text = await response.text()
+  const placeholders = {
+    "$upstream": upstreamDomain,
+    "$workerDomain": workerDomain
+  }
+
+  for (let origin in replaceDict) {
+    let target = replaceDict[origin]
+
+    origin = placeholders[origin] || origin
+    target = placeholders[target] || target
+
+    const re = new RegExp(origin, 'g')
+    text = text.replace(re, target);
+  }
+
+  return text;
+}
+ +

然后点击部署,就可以通过 [project].[subdomain].workers.dev 绕墙访问 gist 了,理论上是可以反代所有网站的,如果有需求的话,各位请自行修改代码~

+

注:

+
    +
  1. project 是创建的 worker 的名称
  2. +
  3. subdomain 是注册 workers 是输入的名字
  4. +
+

自定义域名(可选)

经过上面的步骤,我们已经可以使用类似于 test.baidu.workers.dev 这样的域名使用触发 worker 了。但是如果需要使用自定义域名代替上述域名的话,还需要额外设置

+

ps:刚开始我以为直接在 DNS 中 CNAME 到 workers.dev 就行,但是实际操作之后发现是不可以的。

+

例如我们需要配置一个 test.jiz4oh.com 域名来使用 worker

+
    +
  1. 配置 DNS 解析

    +

    20201227180432

    +
  2. +
  3. 设置 worker 路由

    +

    20201227180558

    +

    20201227180837

    +
  4. +
  5. 等待数分钟,此时就可以访问 test.jiz4oh.com 来使用 worker 了

    +
  6. +
+]]>
+ + 其他 + + + freebie + +
+ + Github+PicGo+Jsdelivr 创建个人图床 + /2020/09/github-picgo-jsdelivr/ + 前言

图床相信经常写东西而且文章图文并茂的朋友多多少少都接触过

+

公共图床服务要不就是备案麻烦,要不就是免费不稳定

+

本文就是关于如何利用 Github+Jsdelivr 创建免费、大容量、高速的个人图床,并且利用 vscode+PicGo 实现图片自动上传

+ + +

创建 GitHub 仓库

20200925204037

+

仓库名随意

+

生成 Token

    +
  1. 点击个人设置
    20200926180107
  2. +
  3. 点击开发者设置
    20200926180238
  4. +
  5. 点击个人令牌
    20200926180349
  6. +
  7. 创建令牌
    20200926180428
  8. +
  9. 保存命令
    20200926180540
  10. +
+

配置 PicGo

vocode 安装 PicGo

扩展搜索 picgo

+

20200926180638

+

配置

打开 vscode 设置

+

20200926180821

+

点开 扩展 => PicGo

+

20200926181037

+

具体配置:

+

20200926181331

+
    +
  1. 选择 github
  2. +
  3. 一般选择 master,如果不需要发布到其他分支不要更改
  4. +
  5. 存入 vscode 的链接地址
      +
    • 使用 jsdelivr:https://cdn.jsdelivr.net/gh/红框5处内容
    • +
    • 使用 rawgithubcontent:https://raw.githubusercontent.com/红框5处内容/红框2处内容
    • +
    +
  6. +
  7. 存入 GitHub 仓库的子文件夹,可为空
  8. +
  9. 仓库名,格式为 用户名/仓库名
  10. +
  11. 之前生成的 Github Token
  12. +
+

使用中的问题

描述:jsdelivr 无法加速,显示 Package size exceeded the configured limit of 50 MB

+

问题原因:jsdelivr 对每一个 release 限制加速 50MB

+

解决方案:

+
    +
  1. 进入仓库
  2. +
  3. 创建 release
    20200926182953
  4. +
  5. 创建 master 版本
    20200926183104
  6. +
  7. 修改 PicGo 设置 Custom Url
    在最后面添加 @master 指定版本号
  8. +
  9. 修改被拒绝显示的链接,在仓库名后面添加 @master。之前 jsdelivr 加速成功的链接不受影响
  10. +
+]]>
+ + 其他 + + + vscode + github + 白嫖 + +
+ + Hexo 博客搭建过程实录 + /2020/09/hello-hexo/ + 创建本地博客 blog

安装 Hexo 脚手架

npm install -g hexo-cli
+ +

安装之后可以使用 hexo 对博客命令操作

+
    +
  1. 创建 blog:hexo init 文件夹
  2. +
  3. 创建文章:hexo new 文章名
  4. +
  5. 生成静态文件:hexo generate 简写:hexo g
  6. +
  7. 清除已生成缓存:hexo clean
  8. +
  9. 启动本地服务:hexo server 简写:hexo s
    默认监听 4000 端口
  10. +
  11. 部署:hexo deploy 简写:hexo d
  12. +
  13. 生成静态文件并部署:hexo g -d
  14. +
+ + +

创建博客

hexo init blog
+cd blog
+hexo g
+hexo s
+ +

访问 http://localhost:4000 查看本地博客

+

配置 blog 部署 Github Pages

创建 github.io 仓库

20200925204037

+

输入仓库名,仓库名以用户名开头,以 github.io 结尾

+

20200925204246

+

修改部署文件

    +
  1. 创建成功后,打开刚刚使用 hexo init blog 命令生成的 blog 文件夹

    +
  2. +
  3. 修改 _config.yml 文件
    20200925203757
    repo 填入刚刚创建的 GitHub 仓库地址,建议填写 ssh 仓库地址,https 地址也可

    +
  4. +
  5. 部署到 Github Pages

    +
    # 清理缓存
    +hexo clean
    +# 部署
    +hexo g -d
    + +
  6. +
+

GitHub Actions 自动部署

创建 blog 仓库

创建另外一个 GitHub 私有仓库 blog,用来存放 hexo 项目并触发部署

+

创建 ssh 密钥

ssh-keygen -f .ssh/github-deploy-key
+ +

.ssh 文件夹下会有 github-deploy-key 和 github-deploy-key.pub 两个文件。

+

配置部署密钥

    +
  1. 打开 blog 仓库设置,Settings -> Secrets -> Add a new secret
    20200925210741
      +
    • 在 Name 输入框填写 HEXO_DEPLOY_KEY。
    • +
    • 在 Value 输入框填写 github-deploy-key 文件内容。
    • +
    +
  2. +
  3. 打开 用户名.github.io 仓库设置,
    20200925211312
      +
    • 在 Title 输入框填写 HEXO_DEPLOY_PUB。
    • +
    • 在 Key 输入框填写 github-deploy-key.pub 文件内容。
    • +
    • 勾选 Allow write access 选项。
    • +
    +
  4. +
+

编写 GitHub Actions

在 hexo 博客文件夹下创建 .github/workflows/deploy.yml 文件,目录结构如下:

+
blog
+└── .github
+    └── workflows
+        └── deploy.yml
+ +

编辑 deploy.yml 文件:

+
name: Deploy GitHub Pages
+
+on:
+  push:
+    branches:
+      - master
+
+# 修改 GitHub 用户名及邮箱才能正确部署
+env:
+  GIT_USER: jiz4oh
+  GIT_EMAIL: jiz4oh@gmail.com
+
+jobs:
+  build-and-deploy:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [ 10.x ]
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          persist-credentials: false
+
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v1
+        with:
+          node-version: ${{ matrix.node-version }}
+
+      - name: Configuration environment
+        env:
+          HEXO_DEPLOY_PRI: ${{secrets.HEXO_DEPLOY_KEY}}
+        run: |
+          mkdir -p ~/.ssh/
+          echo "$HEXO_DEPLOY_PRI" > ~/.ssh/id_rsa
+          chmod 600 ~/.ssh/id_rsa
+          ssh-keyscan github.com >> ~/.ssh/known_hosts
+          git config --global user.name $GIT_USER
+          git config --global user.email $GIT_EMAIL
+
+      - name: Install dependencies
+        run: npm i
+
+      - name: Install hexo-cli
+        run: npm i -g hexo-cli
+
+      - name: Clean
+        run: hexo clean
+
+      - name: Build & Deploy
+        run: hexo g -d
+ +

测试

在 hexo 博客文件夹下,执行

+
# 配置 git 账户
+git config --global user.name 用户名
+git config --global user.email 邮箱
+
+git remote add origin BLOG仓库地址
+git push --set-upstream origin master
+]]>
+ + 其他 + +
+ + 使用 Valine 开启 Hexo 评论系统 + /2020/11/hexo-next-valine/ + 关于评论系统

博客搭建好之后,由于当时已经晚上12点了,并没有一鼓作气的开启评论系统,就一直拖到了现在。。。

+

今天终于想起来开启评论系统,在网上搜索了一下 hexo 的评论系统,有以下几个:

+
    +
  • gitment:基于 GitHub Issue 作为评论存储仓库,但是一直长时间无人维护
  • +
  • gitalk:同 gitment 使用 Github Issue,且页面比 gitment 更精美
  • +
  • disqus:界面好看,支持 Facebook、twitter、Google+ 等账号登陆,但是 需要科学上网
  • +
  • valine:基于 LeanCloud 的无后端评论系统,支持匿名评论
  • +
  • 来必力:韩国的评论系统,功能强大,支持多社交帐号登录,链接不太稳定,不支持匿名评论
  • +
+

对于我来讲,我更偏向于使用 gitalk 作为评论系统,省心~但是 gitalk 类的评论系统被反馈有安全风险,虽然是 GitHub 的锅,但是会对访客(还不是我。。。)的账户安全带来很大的不确定性,所以最终我还是选择了支持匿名评论的 Valine

+ + +

注册 LeanCloud

Valine 是基于 LeanCloud 进行使用的,所以我们需要在 LeanCloud 进行注册

+

因为现在 LeanCloud 国内版要求备案域名,所以使用国际版

+

配置 LeanCloud

    +
  • 创建应用:
    20201121111644
  • +
  • 切换到存储页面:
    20201121111935
  • +
  • 【结构化数据】-【创建 class】
    20201121112126
  • +
  • 修改 ACL 的 write 权限,设置成下图效果
    20201121112221
  • +
  • 切换到【设置】
  • +
  • 【安全中心】-【Web 安全域名】,输入博客会用到的域名,如 http://jiz4oh.comhttps://jiz4oh.com
    20201121112458
  • +
  • 【应用 Keys】-【AppID】
    【应用 Keys】-【AppKey】
    这两个数据在下一步中使用(可点击后面的按钮快速复制)
    20201121112803
  • +
+

配置 next

    +
  • 打开 next 的配置文件,如我是 _confg.next.yml,将下列代码复制进去,注意缩进

    +
    valine:
    +  enable: true
    +  appId: # 拷贝上一步的 ID
    +  appKey: # 拷贝上一步的 Key
    +  placeholder: # Comment box placeholder
    +  avatar: mm # Gravatar style
    +  meta: [nick, mail, link] # Custom comment header
    +  pageSize: 10 # Pagination size
    +  lang: # Language, available values: en, zh-cn
    +  visitor: false # Article reading statistic
    +  comment_count: true # If false, comment count will only be displayed in post page, not in home page
    +  recordIP: false # Whether to record the commenter IP
    +  serverURLs: # When the custom domain name is enabled, fill it in here (it will be detected automatically by default, no need to fill in)
    +  enableQQ: false # Whether to enable the Nickname box to automatically get QQ Nickname and QQ Avatar
    +  requiredFields: [] # Set required fields: [nick] | [nick, mail]
    +
  • +
+]]>
+ + 其他 + +
+ + Hexo 博客使用 Next 主题及美化 + /2020/09/hexo-next/ + 安装 Next

安装

yarn add hexo-theme-next
+ +

设置使用 Next

    +
  1. 打开博客文件夹

    +
  2. +
  3. 打开 _config.yml 文件

    +
    theme: next
    + +
  4. +
+ + +

删除默认主题(可选)

删除 themes 文件夹

+

配置 Next

本配置文件基于 hexo-theme-next@^8.0.0

+

未经特殊注明,均为修改主题配置文件 _config.next.yml

+

创建配置文件

touch _config.next.yml
+ +

设置主题为 Gemini

scheme: Gemini
+ +

返回顶部显示在侧边栏

back2top:
+  sidebar: true
+ +

显示阅读进度条

reading_progress:
+  enable: true
+  # 显示在底部
+  position: bottom
+ +

显示加载进度条

pace:
+  enable: true
+ +

侧边栏菜单展开所有

toc:
+  expand_all: true
+ +

安装外部依赖:

+
yarn add hexo-generator-searchdb
+ +

编辑 _config.yml

+
search:
+  path: search.xml
+  field: post
+  format: html
+  limit: 10000
+ +

编辑 _config.next.yml

+
# Local search
+local_search:
+  enable: true
+ +

设置代码高亮

codeblock:
+  copy_button:
+    enable: true
+  prism:
+    light: prism-tomorrow
+    dark: prism-tomorrow
+ +

添加版权声明

creative_commons:
+  license: by-nc-sa
+  sidebar: true
+  post: true
+ +

修改建站时间

footer:
+  since: 2020
+ +

开启彩色缎带

canvas_ribbon:
+  enable: true
+ +

开启文章字数统计

安装外部依赖:

+
yarn add hexo-word-counter
+ +

编辑 _config.yml

+
symbols_count_time:
+  symbols: true
+  time: true
+  total_symbols: true
+  total_time: true
+  exclude_codeblock: false
+  # 平均字长,默认为 4,中文为 2
+  awl: 2
+  wpm: 275
+  suffix: "mins."
+ +

开启文章阅读量统计

busuanzi_count:
+  enable: true
+ +

添加 GitHub Banner

github_banner:
+  enable: enable
+  permalink: https://github.com/jiz4oh
+  title: Follow me on GitHub
+ +

删除不必要文件

minify: true
+ +

添加标签页面

命令行执行:

+
hexo new page tags
+ +

修改 source/tags/index.md

+
---
+title: tags
+date: 2020-09-26 12:51:57
+# 添加下面两行
+type: tags
+comments: false
+---
+ +

编辑 _config.next.yml

+
menu:
+  tags: /tags/ || fa fa-tags
+ +

添加分类页面

命令行执行:

+
hexo new page categories
+ +

修改 source/tags/categories.md

+
---
+title: categories
+date: 2020-09-26 13:07:13
+# 添加下面两行
+type: categories
+comments: false
+---
+ +

编辑 _config.next.yml

+
menu:
+  categories: /categories/ || fa fa-categories
+ +

设置字体

样式

font:
+  enable: true
+  global:
+    external: true
+    family: Arial
+  # 以下设置只能修改 `` 注释的 md 代码块,不能修改 ```code block``` 字体大小
+  # ```code block``` 字体大小修改见下
+  # codes:
+  #   external: true
+  #   family: Arial
+  #   size: 0.8
+ +

大小

编辑 _config.next.yml

+
custom_file_path:
+  style: source/_data/styles.styl
+ +

新建 source/_data/styles.styl

+
// 修改文章字体大小,强制保持 1em
+div.post-body {
+  font-size: 1em;
+}
+
+// 修改 code block 中的字体,需要写入以下样式
+code[class*=language-] {
+  font-size: 14px;
+}
+ +

致谢

博客主题美化参考了诸多前辈的文章,感谢各位大神的无私分享~

+

参考文章

hexo的next主题个性化教程:打造炫酷网站

+

Next 原作者 iissnan 大神教程,v6 版本以上部分配置不适用

+

Next 社区教程

+]]>
+ + 其他 + +
+ + CentOS 7 安装 ehforwarderbot V1 来收发微信 + /2020/09/how-to-install-ehforwarderbot-v1/ + ehforwarderbot 介绍

ehforwarderbot 是由 blueset 开源在 github 的一款专用于转发微信/QQ/Faceboot Message 到 Telegram 的机器人

+

ehforwarderbot 支持 多微信,多QQ 汇集到一起集中处理消息,特别适合:

+
    +
  1. Telegram 重度用户
  2. +
  3. 需要对多个微信、qq 消息进行处理的用户
  4. +
+ + +

最新版的 ehforwarderbot 已更新到 v2.0.0,本教程仅适用于 v1 版本,且 v2 版本与 v1 版本的数据库结构大改,无法从 v1 无缝迁移到 v2 版本。请谨慎安装 v1 版本

+

优点

    +
  1. 消息云同步,⽂字,语⾳,图⽚,视频,发送的链接,⽂件都可以保存在 tg 云端
  2. +
  3. 消息⼏乎⽆延迟,对⽐ Gcmformojo,tg 发消息很快,没有卡顿,就像你正常聊 tg ⼀样,也没有消息发送失
    败的情况(除⾮你⽹络没连上)
  4. +
  5. 耗电,明显优于微信毒瘤。tg ⾃带 gcm,如果你需要,可以不留 tg 后台,由 gcm 拉起通知
  6. +
  7. ⽆需挂梯,以往 Gcmformojo 有的地区需要挂飞机才能收发,⽽ tg ⾃带⼀个代理功能,可以通过代理收发微
  8. +
  9. 公众号信息也能推送,⽽且 TG ⾃带应⽤内浏览器,也能⽅便的查看公众号推送的⽂章
  10. +
+

缺点

ehforwarderbot 依赖于 web 版微信,所以 web 版微信没有的功能,它也不可能有,例如:

+
    +
  1. 无法收发红包
  2. +
  3. 无法查看别人分享的历史记录
  4. +
  5. 某些表情无法正常显示
  6. +
+

安装 docker CE

卸载旧版本docker

sudo yum remove docker \
+                  docker-client \
+                  docker-client-latest \
+                  docker-common \
+                  docker-latest \
+                  docker-latest-logrotate \
+                  docker-logrotate \
+                  docker-engine
+ +

添加依赖

sudo yum update
+sudo yum install -y yum-utils \
+  device-mapper-persistent-data \
+  lvm2
+ +

添加 Docker 稳定版本的 yum 软件源

sudo yum-config-manager \
+    --add-repo \
+    https://download.docker.com/linux/centos/docker-ce.repo
+ +

安装 docker

sudo yum update
+sudo yum install docker-ce
+ +

docker开机自启:
sudo systemctl enable docker
启动docker服务:
sudo systemctl start docker

+

安装efb

创建配置文件

获取 Telegram Bot Token

    +
  1. 在Telegram关注@BotFather
  2. +
  3. 再到对话框依次输入:/start => /newbot
  4. +
  5. 然后会要你给机器人命名(如:TestBot)
  6. +
  7. 命名完成会得到 Token
  8. +
+

获取自己的 Userid

    +
  1. 先和你的机器人聊天,随便发一句话。
  2. +
  3. 在浏览器输入 https://api.telegram.org/botxx:xx/getUpdates(其中xx:xx为Token)
  4. +
  5. 然后 chat 后面的 id 即为你的 Userid
  6. +
+

创建 config

创建 config.py 文件

+
master_channel = 'plugins.eh_telegram_master', 'TelegramChannel'
+slave_channels = [('plugins.eh_wechat_slave', 'WeChatChannel')]
+
+eh_telegram_master = {
+    "token": "机器人的 TOKEN",
+    "admins": [你自己的 Userid],
+    "bing_speech_api": ["3243f6a8885a308d313198a2e037073", "2b7e151628ae082b7e151628ae08"],
+    "baidu_speech_api": {
+        "app_id": 0,
+        "api_key": "3243f6a8885a308d313198a2e037073",
+        "secret_key": "2b7e151628ae082b7e151628ae08"
+    }
+}
+ +

创建 tgdata.db

创建 tgdata.db 文件,该文件可以为空

+

启动 ehforwarderbot

config.pytgdata.db 放在 /root 目录下

+

运行:

+
docker run -d --restart=always --name=efb \
+    -v /root/config.py:/opt/ehForwarderBot/config.py \
+    -v /root/tgdata.db:/opt/ehForwarderBot/plugins/eh_telegram_master/tgdata.db \
+    royx/docker-efb
+ +

登录 efb

docker logs efb

+

此时屏幕上会出现二维码,使用手机扫码登录即可

+]]>
+ + 其他 + + + efb + ehforwarderbot + +
+ + CentOS 7 安装 efb v2 来收发微信 + /2020/09/how-to-install-ehforwarderbot-v2/ + 上一篇文章已经介绍了如何安装 v1,这篇文章来介绍如何安装 v2 版本的 ehforwarderbot

+ + +

安装依赖

yum install -y gcc file-devel libwebp-tools git screen
+
+# 安装 python3.6 和 pip3
+wget https://www.moerats.com/usr/shell/Python3/CentOS_Python3.6.sh && sh CentOS_Python3.6.sh
+yum install python3-pip python3-dev python3-setuptools
+
+# 下载 ffmpeg
+wget https://www.moerats.com/usr/down/ffmpeg/ffmpeg-git-$(getconf LONG_BIT)bit-static.tar.xz && tar xvf ffmpeg-git-*-static.tar.xz
+mv ffmpeg-git-*/ffmpeg ffmpeg-git-*/ffprobe /usr/bin/
+rm -rf ffmpeg-git-*
+ +

安装 efb

安装 efb(ehforwarderbot)

二选一:

+
# 安装稳定版
+pip3 install ehforwarderbot
+
+# 安装开发版
+pip3 install git+https://github.com/blueset/ehforwarderbot.git
+ +

安装 ETB(efb-telegram-master)和 EWS(efb-wechat-slave)

# 安装TG和微信模块
+pip3 install efb-telegram-master efb-wechat-slave
+ +

配置 efb

配置 efb(ehforwarderbot)

mkdir -p ~/.ehforwarderbot/profiles/default
+vi ~/.ehforwarderbot/profiles/default/config.yaml
+ +

保存下列代码到 config.yaml

+
master_channel: "blueset.telegram"
+slave_channels:
+- "blueset.wechat"
+ +

这只是登录一个微信号,如果你要同时登录多个微信号,那么配置文件就需要改为:

+
# 比如我要同时登录并收发3个微信号
+master_channel: blueset.telegram
+slave_channels:
+- blueset.wechat
+- blueset.wechat#moe123
+- blueset.wechat#rats321
+# #号后面指定id,只能是字母、数字、下划线
+ +

配置 ETB

# 同样的也建在 default 文件夹,如果你上面更改了 default 文件夹,那这里也要更改
+mkdir ~/.ehforwarderbot/profiles/default/blueset.telegram
+vi ~/.ehforwarderbot/profiles/default/blueset.telegram/config.yaml
+ +

保存下列代码到 blueset.telegram/config.yaml

+
token: "机器人的 TOKEN"
+admins:
+- 你自己的 Userid
+flags:
+    # 关闭自动语言设置,使用 systemd 启动时默认中文
+    auto_locale: false
+ +

配置 EWS

# 同样的也建在 default 文件夹,如果你上面更改了 default 文件夹,那这里也要更改
+mkdir ~/.ehforwarderbot/profiles/default/blueset.wechat
+vi ~/.ehforwarderbot/profiles/default/blueset.wechat/config.yaml
+ +

保存下列代码到 blueset.wechat/config.yaml

+
flags:
+    # tg 端编辑消息时以撤回并重新发送的方式发送到微信
+    delete_on_edit: true
+    # 每当请求会话列表时,强制刷新会话列表
+    refresh_friends: true
+    # 使用 iTerm2 图像协议 显示二维码。本功能只适用于 iTerm2 用户
+    imgcat_qr: true
+    # 在收到第三方合作应用分享给微信的链接时,其附带的预览图将缩略图上传到 sm.ms
+    app_shared_link_mode: upload
+ +

设置守护进程

创建 efb 服务

vi /etc/systemd/system/efb.service

+

保存以下配置到 efb.service

+
[Unit]
+Description=EH Forwarder Bot instance
+After=network.target
+Wants=network.target
+Documentation=https://github.com/blueset/ehForwarderBot
+
+[Service]
+Type=simple
+Environment='LANG=zh_CN.UTF-8' 'PYTHONIOENCODING=utf_8' 'EFB_DATA_PATH=/root/.ehforwarderbot'
+ExecStart=/usr/local/bin/ehforwarderbot --verbose
+Restart=on-abort
+KillSignal=SIGINT
+
+[Install]
+WantedBy=multi-user.target
+ +

使用 systemctl 管理 efb

# 开机启动
+sudo systemctl enable efb
+
+# 启动
+sudo systemctl start efb
+
+# 关闭
+sudo systemctl stop efb
+
+# 重启
+sudo systemctl restart efb
+
+# 查看运行详情
+sudo systemctl status efb
+
+# 查看更加详细的运行详情
+sudo systemctl status efb -l
+ +

登录 efb

    +
  • 如果不使用 systemctl 管理,则执行 ehforwarderbot --verbose 进行启动
  • +
  • 如果使用 systemctl 进行管理
      +
    1. sudo systemctl start efb
    2. +
    3. sudo systemctl status efb -l 然后复制二维码连接到浏览器,扫码登陆
    4. +
    +
  • +
+

安装中遇到的错误

缺少 cairo 依赖

描述:

+
raise OSError(error_message) # pragma: no cover
+OSError: no library called "cairo" was found
+ +

解决方案:

+
yum install -y cairo-devel libtiff* && pip3 install cairosvg cairocffi
+ +

描述:

+
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-g3q2qdl8/cairocffi/
+ +

原因:pip 版本过低

+

解决方案:

+

pip3 install --upgrade pip

+

python 安装失败

描述:

+
configure: error: in `/usr/local/src/Python-3.6.4':
+configure: error: no acceptable C compiler found in $PATH
+See `config.log' for more details
+ +

解决方案:
yum install gcc -y

+

描述:

+
zipimport.ZipImportError: can't decompress data; zlib not available
+make: *** [install] Error 1
+ +

解决方案:
yum install zlib* -y

+

pip 安装依赖失败

描述:
install --record /tmp/pip-f5erwx9h-record/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-build-34xxo8de/cairocffi/

+

解决方案:

+
pip install -U setuptools
+pip install -U wheel
+ +

如何迁移

    +
  1. 保留 /root/.ehforwarderbot/profiles/default/blueset.telegram 下的 config.yamltgdata.db 文件
  2. +
  3. 保留 /root/.ehforwarderbot/profiles/default/blueset.wechat 下的 wxpy_puid.pkl 文件(这个文件存着对应微信好友的 UID,与 tgdata 的 chatassoc 表相对应)
  4. +
+]]>
+ + 其他 + + + efb + ehforwarderbot + +
+ + HTTP 中的缓存机制 + /2020/11/http-cache/ + 前言

最近在思考如何优化 web 应用的响应速度,常见的方式就是使用缓存,而最常接触的技术其实是服务端做共享缓存,例如 redis 缓存热点数据、CDN 缓存静态资源。在客户端做缓存这一块,知识面还比较窄,经过资料查询和思考,总结一些心得

+ + +

缓存的目的

+

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术

+
+

在不考虑成本的情况下,不使用缓存其实是体验最好方式,可以通过无限堆服务器来达到快速响应的目的,并且不会有一致性等问题。

+

所以缓存的目的很简单,就是缓解服务器压力,来达到在有限的成本情况下相对最优质的用户体验。

+

私有缓存和共享缓存

缓存的种类大致可分为:

+
    +
  • 共享缓存:可被多个用户重复使用,例如 redis 缓存热点数据,CDN 缓存静态资源
  • +
  • 私有缓存:只能被单个用户使用,例如浏览器缓存
  • +
+

缓存控制

HTTP header: Cache-control

HTTP/1.1 中引入 Cache-control 头用来定义浏览器应该如何进行缓存决策

+

Cache-control 的值有:

+
    +
  • no-store:客户端不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容
  • +
  • no-cache:客户端会缓存响应内容,但每次请求都会携带 验证字段 到服务器,验证缓存是否过期,未过期才使用本地已缓存内容
  • +
  • public:响应内容可以被中间人如 CDN、网关等缓存
  • +
  • private:响应内容只能被客户端缓存
  • +
  • max-age:缓存过期的相对时间(秒),超过该时间后会再次发起请求
  • +
  • must-revalidate:本地缓存未过期时使用本地缓存,本地缓存过期时必须携带 验证字段 到服务器,验证缓存是否过期
  • +
+

HTTP header: Expries

ExpriesHTTP/1.0 中的内容,作用是指定缓存过期的绝对时间,例如 Expires: Wed, 21 Oct 2015 07:28:00 GMT

+

Cache-control: max-age 的区别:

+
    +
  • Expries 是绝对时间,Cache-control 是相对时间
  • +
  • HTTP/1.0Expries 优先级大于 Cache-control,在 HTTP/1.1 及以后 Cache-control 优先级大于 Expries
  • +
+

缓存决策

强制缓存

Cache-control: max-ageExpires 指定的缓存,在时间过期之前,浏览器不会再次请求服务器

+

协商缓存

当缓存失效时,会发出 条件式请求,这时候和服务器的通信就称为协商缓存

+

协商缓存的优点:

+
    +
  • 省去了服务器生成 html 的时间
  • +
  • 省去了响应体内容,传输更快
  • +
+

Last-Modified

请求响应流程:

+
    +
  1. 当浏览器第一次请求服务器时,服务器会返回 Last-Modified 头部表明当前资源的最后修改日期
  2. +
  3. 浏览器会将这个日期值缓存在本地
  4. +
  5. 当需要发出 条件请求 时,将日期值放在 If-Modified-SinceIf-Unmodified-Since 头部中向服务器发出
  6. +
  7. 服务器判断日期,决定响应内容
  8. +
  9. 如果该资源新鲜,则返回 304,客户端使用该缓存渲染内容
  10. +
  11. 如果该资源被更改过,则返回 200,及全新内容
  12. +
+

Etag

Last-Modified 的请求响应流程类似,不同的是服务器响应的是 ETag 头,浏览器发出请求时的头部为 If-MatchIf-None-Match

+
    +
  • 强验证:HTTP 协议默认使用强验证,强验证要求文档的每一个字节都必须相同
    ETag: "618bbc92e2d35ea1945008b42799b0e7"
  • +
  • 弱验证:ETag 头以 W/ 开头时为弱验证,此时只要求文档的内容含义是相同的。
    ETag: W/"618bbc92e2d35ea1945008b42799b0e7"
  • +
+

因为存在当资源时间更改,但是内容未改变等情况,Last-Modified 对资源的判断没有 ETag 准确,所以服务端处理的时候通常优先判断 ETag

+

决策图

决策流程图

+

刷新页面对 http 缓存的影响

三种刷新操作:

+
    +
  • 正常操作:地址栏输入 url,跳转链接,前进后退等
  • +
  • 手动刷新:F5(mac 为 Cmd + R),点击刷新按钮,点击菜单刷新
  • +
  • 强制刷新:Ctrl + F5(mac 为 Cmd + Shift + R)
  • +
+

不同刷新操作,不同的缓存策略:

+
    +
  • 正常操作:强制缓存有效,协商缓存有效
  • +
  • 手动刷新:强制缓存失效,协商缓存有效
  • +
  • 强制刷新:强制缓存失效,协商缓存失效
  • +
+

参考

HTTP 缓存

+

浅谈 HTTP 缓存

+]]>
+ + 网络 + + + study + +
+ + NUC8i5BEH 黑苹果安装记录 + /2020/09/install-hackintosh-to-nuc8i5beh/ + 安装 mac + +

拷贝 EFI

    +
  1. 挂载 u 盘的 EFI 分区(在 Finder 中 带有三角符号的即是 u 盘的 EFI 分区)
  2. +
  3. 拷贝 EFI 分区下的 EFI 文件夹到桌面
  4. +
  5. 挂载磁盘的 EFI 分区(注:在 10.15.3 和 10.15.4 中 Finder 不会显示两个 EFI 磁盘,可以先卸载 u 盘 的 EFI 分区)
  6. +
  7. 将桌面的 EFI 文件夹拷入磁盘的 EFI 分区,替换其下的 EFI 文件夹
  8. +
+

注入三码

    +
  1. 点击 Clover Configurator 左下角

    +

    image-20200328230934689

    +
  2. +
  3. 打开 EFI 分区 下的 EFI>CLOVER>config.list

    +
  4. +
  5. 引导参数:
    image-20200328232028435

    +
  6. +
  7. (可选)修改 UI 比例,解决开机过程中苹果 logo 从大变小
    image-20200328232246807

    +
  8. +
  9. 生成序列号和 SMUUID,多点击几下避免随机
    image-20200328232723186

    +
  10. +
  11. 生成设备的唯一标识号
    image-20200328232851352

    +
  12. +
  13. 生成 ROM ID
    image-20200328233244117

    +
  14. +
  15. 保存
    image-20200328233536063

    +
  16. +
  17. 关闭 Clover Configurator,会提示不支持永久性的版本存储,点击

    +
  18. +
  19. 重启 mac,此处可以拔出 U 盘了

    +
  20. +
+

易用性设置

必做设置

睡眠修复

    +
  1. 打开 Hackintool>电源
  2. +
  3. 修改睡眠值
    image-20200328234800909
  4. +
+

可选设置

打开允许安装其他来源应用

终端执行:sudo spctl --master-disable

+

关闭读卡器显示

如果硬改了 NUC8,会在右上角出现一个卡片的标识
!!!waring:不要点击关闭卡会造成死机

+

解决办法是:

+
    +
  1. 10.15 Catalina 中系统分区默认为只读,故需执行:
    sudo mount -uw / && killall Finder
  2. +
  3. 关闭读卡器显示:
    fn=".`date +%s`" && sudo mv /System/Library/CoreServices/Menu\ Extras/ExpressCard.menu /System/Library/CoreServices/Menu\ Extras/ExpressCard.menu$fn && sudo touch /System/Library/CoreServices/Menu\ Extras/ExpressCard.menu
  4. +
+

黑苹果下装 win10

制作 winpe

下载 winpe 和 纯净 win10 镜像,然后写入到 u 盘中

+

分区

!!!不要使用 BootCamp,黑苹果使用无效

+
    +
  1. 打开磁盘工具
  2. +
  3. 分一个 win10 系统盘的分区(格式随意,后期会重新格式化)出来,如果玩游戏多,可以多分一点
  4. +
  5. 分一个 500M 左右的分区作为 win10 efi 存放地址
  6. +
+

重启到 winpe

    +
  1. 重启到 bios(一般是开机时按 F2 进入 bios)
  2. +
  3. 选择 winpe 的 uefi 启动
  4. +
  5. 打开 diskgenius,备份 efi 文件夹 A(此步为保留苹果和 clover 的启动文件),备份到外部磁盘,以防格盘
  6. +
  7. 选择安装 win10,选择安装文件夹为刚刚格式化的那个磁盘,引导文件夹选择 efi 文件夹 A 所在磁盘(必须先覆盖)
  8. +
  9. 安装完成后,重启到 win10,进行设置。。。
  10. +
  11. 重启到 winpe
  12. +
  13. 将 efi 文件夹 B 中的内容拷贝到 500M 分区中
  14. +
  15. 将之前备份的 efi 文件夹 A 中内容拷贝到当前 efi 文件夹 B 中,覆盖所有
  16. +
  17. 重启,会看见 clover。出现 win10 和 mac 双启动选项
  18. +
+

问题汇总

    +
  1. 使用外置显卡坞为什么无法点亮?
      +
    1. 进入 BIOS>Security 菜单
    2. +
    3. 将 Thunderbolt Security Level 设置为 Legacy Mode
    4. +
    5. 进入 BIOS>Boot 菜单
    6. +
    7. Boot Configuration – Boot Devices – Thunderbolt Boot 开启
    8. +
    +
  2. +
+]]>
+ + 其他 + + + mac + 黑苹果 + +
+ + 使用 pyenv 和 pipenv 管理糟糕的 python 环境 + /2020/10/install-pyenv-and-pipenv/ + 前言

经常写 python 的开发一定苦恼过一个问题,那就是如何安装 python 环境。经常遇到如下问题:

+
    +
  1. 许多系统默认只自带 python2,导致现在很多使用 python3 的程序无法正常运行
  2. +
  3. python3 各版本间也不是完全兼容,某些第三库只支持特定的 python 版本
  4. +
+

这些问题导致了我们经常会在各个版本间进行切换,甚至每个项目的 python 版本都不尽相同。所以我们需要一个环境管理器来帮助我们管理各个版本,这就用到了 pyenv

+

而当我们项目过多之后,每个项目的依赖包就会有多个版本。这些依赖包的管理就需要用到 pipenv

+ + +

pyenv

安装

mac 使用 homebrew 安装 pyenv

+
brew install pyenv
+ +

设置环境变量

export PYENV_ROOT="$HOME/.pyenv"
+export PATH="$PYENV_ROOT/bin:$PATH"
+if command -v pyenv 1>/dev/null 2>&1;
+then
+  eval "$(pyenv init -)"
+fi
+ +
    +
  • 如果使用的是 bash,则将上述代码粘贴到 ~/.bashrc
    重新加载环境变量,source ~/.bashrc
  • +
  • 如果使用的是 zsh,则将上述代码粘贴到 ~/.zshrc
    重新加载环境变量,source ~/.zshrc
  • +
+

安装 python 3.6.6

pyenv install 3.6.6
+ +

默认使用 3.6.6

+
pyenv global 3.6.6
+ +

pyenv 常用命令

    +
  • 查看有哪些 Python 版本可以安装

    +
    pyenv install --list
    +
  • +
  • 安装某个 Python 版本

    +
    pyenv install 3.6.4
    +
  • +
  • 查看当前 Python 版本情况(* 表示系统当前的 Python 版本,system表示系统初始版本)

    +
    pyenv versions
    + +
      system
    +* 3.6.6 (set by /Users/jiz4oh/.pyenv/version)
    +
  • +
  • 切换 Python 默认版本

    +
    # 切换全局默认版本
    +pyenv global 3.6.6
    +# 切换当前项目默认版本
    +pyenv local 3.6.6
    +# 切换 shell 使用的默认版本
    +pyenv shell 3.6.6
    +
  • +
  • 卸载指定 Python 版本

    +
    pyenv uninstall 3.6.6
    + +
  • +
+

pipenv

安装

mac 使用 homebrew 安装 pipenv

+
brew install pipenv
+ +

【2020.10.01】homebrew 安装版本为 2018.11.26_3,如果需要安装最新版本需要使用

+
pip3 install pipenv
+ +

参考

Pipenv – 超好用的 Python 包管理工具

+]]>
+ + 后端 + python + + + 开发环境 + +
+ + openwrt 扩容 overlay + /2020/09/openwrt-expand-overlay-storage/ + 原理

lsblk:查看当前固件的分区信息

+
NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
+sda            8:0    1 58.6G  0 disk
+├─sda1         8:1    1  16M  0 part
+├─sda2         8:2    1  300M  0 part /mnt/sda4
+ +

image-20200711142556881

+

sda 2 中的浅蓝色和深蓝色区域为底层 Squash 格式,该格式只读,不支持修改,优势是可以在出错时轻松重置

+

overlay 就是在 upper layer 层进行读写的形式

+

image-20200711143340836

+

overlay 的扩容并不是在 sda2 上进行操作,而是新建一个更大的分区 sda3,并将 Overlay 指向 sda3 ,这样的话,重置 sda2 后并不会损坏 sda3 中的配置

+ + +

扩容步骤

创建新分区

使用 cfdisk 进行磁盘操作(使用 opkg install cfdisk 安装,如果安装失败请更新 opkg 源 opkg update)

+
    +
  1. cfdisk

    +

    image-20200711144836714

    +
  2. +
  3. 新建分区:切换到 free space ,切换到 new 回车,输入分区大小

    +

    image-20200711145123408

    +

    选择主分区或者扩展分区:选择 primary

    +

    image-20200711145248408

    +
  4. +
  5. 将更改写入分区表:光标移到新分区,选择 wirte,并输入 yes

    +

    image-20200711145522455

    +
  6. +
  7. 退出 cfdisk:选择 quit

    +

    image-20200711145729840

    +
  8. +
+

格式化新分区

mkfs.ext4 /dev/sda3

+

image-20200711150146466

+

挂载新分区

将 /dev/sda3 挂载到 /mnt/sda3 下:mount /dev/sda3 /mnt/sda3

+

ls /mnt/sda3 查看 /mnt/sda3 目录,如果有 lost+found 目录则表示挂载成功

+

image-20200711150414828

+

拷贝 /overlay 下所有文件

ls /overlay 查看 /overlay 目录,如果有文件则拷贝到 /mnt/sda3 中

+

cp -r /overlay/* /mnt/sda3

+

检测是否拷贝成功

+

ls /mnt/sda3

+

在系统中挂载目录

+

重启路由器

+

参考

OPENWRT | ESXI 下 OpenWrt扩容Overlay,增加安装插件空间

+]]>
+ + 路由器 + + + openwrt + +
+ + 在pve中安装openwrt + /2021/02/openwrt-in-pve/ + 前言

一直对软路由这个东西念念不忘,想要入手折腾一下。最近因为家里需要一台服务器,我就把我的 n1 拿回了家里,转手就入了一个 j3455 的成品软路由,这边文章记录下在 pve 环境下折腾 openwrt 的心得,顺便学习下 pve(说不定能当个 IDC 呢

+ + +

安装 PVE

    +
  1. 在官网中下载 ISO 镜像
  2. +
  3. 烧录到 U 盘中
  4. +
  5. 使用U盘启动
  6. +
  7. 安装,具体可参考【纯净安装】Proxmox-VE ISO原版 安装 全过程
  8. +
  9. 登录PVE后台,地址为 https://IP:8006,重点:https,使用 chrome 登录时因为证书不安全的原因会被拦截,选择信任
  10. +
+

下载 OPENWRT 镜像

我使用的esir 的 x86 佛跳墙,下载是 gz 格式的压缩文件,需要解压为 img 格式的镜像文件

+

分配网卡

路由器最重要的就是将端口的网卡分配成 WAN 口和 LAN 口,这样才能形成一个网络拓扑结构。因为我是在 PVE 中安装虚拟机的方式使用 OPENWRT,所以需要先在 PVE 中将网卡映射到虚拟机中,路由器才能正确分配端口。

+
    +
  1. 安装 PVE 的过程中,我们已经将 eth0 口(也就是图上的 enp1s0) 虚拟成了 vmbr0

    +

    20210206155524

    +
  2. +
  3. 因为我的软路由总共有4个网卡,所以我还需要虚拟3个网卡出来,和硬件口一一对应,比如将 en2s0 虚拟成 vmbr1,以此类推
    telegram-cloud-photo-size-5-6123167585387260669-y

    +

    最终效果:

    +

    2021-02-06-16-02-45

    +
  4. +
  5. 应用配置

    +

    2021-02-06-16-04-34

    +

    如果遇到了这个错误,是因为没有 ifupdown2,需要在 shell 中执行 apt install -y ifupdown2

    +

    2021-02-06-16-05-23

    +
  6. +
+

创建OPENWRT虚拟机

    +
  1. 点击右上角 创建虚拟机

    +
  2. +
  3. 一般:输入名称并设置开机自启,点击下一步
    我使用的 openwrt,注意 VM ID,这是以后在 PVE 中操作虚拟机的关键

    +

    2021-02-06-16-07-58

    +
  4. +
  5. 操作系统:选择不使用任何介质,点击下一步
    稍后再上传镜像文件,因为需要对磁盘进行一些操作

    +

    2021-02-06-16-09-07

    +
  6. +
  7. 系统:全部默认,点击下一步

    +
  8. +
  9. 硬盘:全部默认,点击下一步

    +
  10. +
  11. CPU:选择分配给虚拟机的CPU,点击下一步
    按个人喜好分配 CPU 个数,我分配的 4 个,CPU 权重是在多个虚拟机中竞争 CPU 时,虚拟机的优先级,默认是 1024,可以增加 OPENWET 的权重保证网络通畅

    +

    2021-02-06-16-16-55

    +
  12. +
  13. 内存:按照个人喜好分配,如果只是单纯科学上网,1G足矣

    +
  14. +
  15. 网络:模型选择 VirtIO
    桥接网卡随便选,后面会将全部网卡添加进来

    +

    2021-02-06-16-21-07

    +
  16. +
  17. 确认

    +
  18. +
+

配置虚拟机

    +
  1. 分离创建时选择的硬盘

    +

    2021-02-06-16-24-12

    +
  2. +
  3. 删除未使用的磁盘0CD/DVD驱动器(ide2)

    +

    2021-02-06-16-25-38

    +
  4. +
  5. 上传之前下载的 OPENWRT img 文件

    +

    2021-02-06-16-29-01

    +
  6. +
  7. 拷贝镜像上传地址

    +

    2021-02-06-16-31-56

    +
  8. +
  9. 将 OPENWET 镜像导入磁盘
    在 shell 中执行 :

    +
    qm importdisk 100 /var/lib/vz/template/iso/openwrt-buddha-v2_2021_-x86-64-generic-squashfs-uefi.img local-lvm
    + +

    图中第一个绿框中的 100 为虚拟机的 VM ID,第二个绿框为刚刚上传的镜像地址

    +

    2021-02-06-16-34-18

    +
  10. +
  11. 设置磁盘

    +

    2021-02-06-16-37-42

    +
  12. +
  13. 调整引导顺序,将 sata0 磁盘启用并调整到第一位

    +

    2021-02-06-16-42-15

    +
  14. +
  15. 添加虚拟网卡,将之前虚拟出来的网卡都依次添加进去,还是使用 VirtIO 模型

    +

    2021-02-06-16-40-30

    +
  16. +
  17. 启动虚拟机

    +
  18. +
+

现在就可以使用 OPENWRT 了

+

参考

【纯净安装】Proxmox-VE ISO原版 安装 全过程

+

PVE安装Openwrt/LEDE软路由保姆级图文教程

+]]>
+ + 路由器 + + + openwrt + pve + +
+ + Ruby 是如何调用方法的 + /2020/11/receiver-and-ancestors/ + 前言

当一个方法被调用时,要做的事情其实只有两件,1. 找到它。2. 调用它

+ + +

接收者 receiver

接收者就是调用方法所在的对象。比如 'str'.to_sym 语句中,'str' 就是接收者。可以形象的理解为向这个接收者 'str' 发送了一条 to_sym 的消息

+

上面是显式指定接收者的例子,而在 Ruby 中是可以不指定接收者的,像这样:

+
class MyClass
+  def my_method
+    test
+  end
+
+  def test
+    p "I'm Test"
+  end
+end
+
+MyClass.new.my_method
+# I'm Test
+ +

当一个字符串被调用时,Ruby 首先会在当前作用域查找是否有这个字符串对应的局部变量,如果没有时就会在 self 这个默认的接收者上调用方法

+

找到它:祖先链 ancestors

在面向对象的语言中,继承是一个很常见的概念,比如 Python 支持多继承,通过继承多个父类来复用代码,当方法被调用时,首先查找该对象是否有该方法,如果没有,则查找「方法解析顺序」(Method Resolution Order,或 MRO),而 MRO 通过 C3算法(python3)来计算,简单来说就是广度优先

+

而在 Ruby 中是不支持多继承的,转而使用 mixinx 的方式来实现代码的复用,mro 是通过搜索祖先链一直向上查找。当 Ruby 调用一个方法时,会首先查找对象是否有该方法,如果没有的话,则向上搜索整个祖先链,这就是 one step to the right, then up

+
class M1;end
+class M2 < M1;end
+
+# 查看祖先链
+M2.ancestors  # [M2, M1, Object, Kernel, BasicObject]
+ +

include

include 是实现 mixinx 最常见的方式,当模块 A 被包含在类(或者模块)B 中时,这个 A 在 B 的祖先链的位置就在 B 之上,例:

+
module M1;end
+module M2;end
+module M3
+ include M1
+ include M2
+end
+
+M3.ancestors  # [M3, M2, M1]
+ +

prepend

prepend 方法类似于 include,不过这个方法会将模块插入祖先链的下方,例:

+
module M1;end
+module M2;end
+module M3
+ prepend M1
+ prepend M2
+end
+
+M3.ancestors  # [M2, M1, M3]
+ +

多重包含

当模块 C 包含模块 A、B,模块 B 包含模块 A 时,A 被重复导入,Ruby 会忽略已经被加入祖先链的重复模块导入

+
module A;end
+module B
+  include A
+end
+module C
+  prepend A
+  include B
+end
+
+B.ancestors   # [B, A]
+C.ancestors   # [A, C, B]
+ +

更复杂的包含:

+
module M1;end
+module M2;end
+module M3;end
+module M4
+  include M1
+  include M2
+  prepend M3
+end
+
+module M5
+  prepend M1
+  include M2
+  include M4
+end
+
+M4.ancestors  # [M3, M4, M2, M1]
+M5.ancestors  # [M1, M5, M3, M4, M2]
+ +

调用它

刚刚我们已经通过祖先链找到了方法,接下来就要执行这个方法。假如有以下方法:

+
def my_method
+  temp = 1
+  my_other_method(temp)
+end
+ +

当执行 my_method 方法时,方法内部需要调用 my_other_method,而该由哪个对象来调用这个方法?

+

self 关键字

+

Ruby 中的每一行代码都会在一个对象中被执行–这个对象就是所谓的当前对象。

+
+

当前对象可以用 self 关键字来指代,而所有没有明确指明接收者的方法都会在 self 上调用。

+
class MyClass
+  def testing_self
+    @var = 10
+    my_method()
+    self
+  end
+
+  def my_method
+    @var += 1
+  end
+end
+
+obj = MyClass.new
+obj.testing_self  # <MyClass:0x00007faea4131b90 @var=11>
+ +

private 是怎么实现的

private 方法遵从一个简单的规则:

+
+

不能明确指定接收者来调用私有方法

+
+
class C
+  def public_method
+    self.private_method
+  end
+
+  private def private_method
+    1
+  end
+end
+
+C.new.public_method  # NoMethodError (private method `private_method' called for #<C:0x00007fd0c40feb90>)
+ +

总结:

+
    +
  1. 如果需要调用其他对象的方法,必须显式指定接收者
  2. +
  3. 私有方法不能明确指定接收者来调用
  4. +
+

所以私有方法不能被其他对象调用,不能被自身显式 self 调用,只能隐式 self 调用

+

参考

Ruby元编程

+]]>
+ + 后端 + ruby + + + study + +
+ + 在 vscode 配置 ruby 开发环境 + /2020/10/ruby-in-vscode/ + 前言
+

常在河边走,哪有不湿鞋 –鲁迅

+
+

作为多年使用 JetBrains 盗版软件的不法用户,昨日终于被逮到了,经历各种再破解尝试,均告失败。
只好转战最近广受好评的 vscode,准备接下来试试能不能作为主力开发工具。
本篇记录如何在 vscode 配置开发 ruby 环境

+ + +

安装 vscode

vscode 的安装就不赘述了,在官网直接下载,傻瓜式安装就 ok 了。

+

配置 vscode

安装配置扩展 ruby

    +
  1. 这个扩展名称就叫 ruby(作者吕鹏,vscode官方开发人员),在 vscode 应用商店中直接搜索安装就好

    +
  2. +
  3. 在 vscode 配置文件 settings.json 中添加如下

    +
    "ruby.useBundler": true,
    +"ruby.lint": {
    +  "rubocop": {
    +    "useBundler": true
    +  },
    +},
    +"ruby.format": "rubocop",
    + +
  4. +
+

安装配置扩展 Ruby Solargraph

    +
  1. 在 vscode 应用商店搜索安装

    +
  2. +
  3. settings.json 中添加

    +
    "solargraph.useBundler": true,
    +"solargraph.references": true,
    +"solargraph.autoformat": true,
    +"solargraph.formatting": true,
    + +
  4. +
+

配置运行环境

在项目目录下新建 .vscode/launch.js

+
{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "Rails server",
+      "type": "Ruby",
+      "request": "launch",
+      "program": "${workspaceRoot}/bin/rails",
+      "args": [
+        "server"
+      ],
+      "env": {
+        "PATH": "xxxx",
+        "GEM_HOME": "xxx",
+        "GEM_PATH": "xxx",
+        "RUBY_VERSION": "xxx"
+      }
+    }
+  ]
+}
+ +

env 详细内容请在 shell 使用下列命令输出

+
printf "\n\"env\": {\n  \"PATH\": \"$PATH\",\n  \"GEM_HOME\": \"$GEM_HOME\",\n  \"GEM_PATH\": \"$GEM_PATH\",\n  \"RUBY_VERSION\": \"$RUBY_VERSION\"\n}\n\n"
+ +

安装 gem

gem install ruby-debug-ide
+gem install debase
+gem install solargraph
+ +

如何打开 settings.json

20201028113907

+]]>
+ + 后端 + ruby + + + vscode + 开发环境 + +
+ + Ruby 是如何实现多重继承的 + /2020/10/ruby-include-implementation/ + 前言

Ruby 用了也接近一年了,有很多黑魔法让我感到非常有意思,刚开始学习的时候也经常让我不知所措。
其中很有意思的一点就是 includeextend 这两个方法来变相实现 多重继承,即代码的复用。但是用归用,却没有细细的研究其中的实现,故此,今天好好的看一看。

+ + +

include

include 方法在 Ruby 中会被 include 模块的方法添加为当前模块下的实例方法

+
module Person
+  def name
+    p 'My Name Is Person'
+  end
+end
+
+module User
+  include Person
+end
+
+User.new.name
+# My Name Is Person
+ +

这样,Person 就可以被随意引用,使用其中的实例方法 name
然而,这是怎么做到的呢,定义在 Person 中的 name 为什么能被 User 的实例调用呢?

+

那这就需要看看 include 的源码了

+
class Module
+  # include(module, ...)    -> self
+  # 
+  # Invokes <code>Module.append_features</code> on each parameter in reverse order.
+  def include(module1, *smth)
+    # This is a stub, used for indexing
+  end
+...
+ +

append_features

Ruby 源码由 C 写成,所以我们只能看文档,文档中很清楚的指明了调用 include 方法会触发 append_features 方法,那我们就看看 append_features 的源码

+
# append_features(mod)   -> mod
+# 
+# When this module is included in another, Ruby calls
+# <code>append_features</code> in this module, passing it the
+# receiving module in _mod_. Ruby's default implementation is
+# to add the constants, methods, and module variables of this module
+# to _mod_ if this module has not already been added to
+# _mod_ or one of its ancestors. See also <code>Module#include</code>.
+def append_features(mod)
+  # This is a stub, used for indexing
+end
+ +
    +
  1. 当一个 module 作为 mixininclude 时,会默认调用 append_features
  2. +
  3. Rubyappend_features 的默认实现是将 mixin 中的 constantsmethodsmodule variables 添加到需要 includemodule 中。
  4. +
  5. 所以当 append_features 执行后,Person module 下的 name 被添加到了 User
  6. +
+

验证

重写 append_features 方法:

+
module Person
+  extend ActiveSupport::Concern
+
+  def name
+    p 'My Name Is Person'
+  end
+
+  def self.append_features(base)
+  end
+end
+
+User.new.name
+# undefined method `name' for #<User:0x00007f84b918c898> (NoMethodError)
+ +

很显然,append_features 被覆盖,导致 name 没有被添加到 User

+

included:include 的回调

append_features 的实现满足了我们对实例方法的代码重用,但是没有实现对类方法的重用,如果我们想在 include 时同时导入类方法,或者其他操作,我们可以选择重写 append_features。但是在 Ruby 中并不建议直接重写 append_features,而是给我们提供了简单便捷的钩子方法 included

+
# included(othermod)
+# 
+# Callback invoked whenever the receiver is included in another
+# module or class. This should be used in preference to
+# <tt>Module.append_features</tt> if your code wants to perform some
+# action when a module is included in another.
+# 
+#        module A
+#          def A.included(mod)
+#            puts "#{self} included in #{mod}"
+#          end
+#        end
+#        module Enumerable
+#          include A
+#        end
+#         # => prints "A included in Enumerable"
+def included(othermod)
+# This is a stub, used for indexing
+end
+ +

如果我们想要导入类方法,重写刚刚的 Person 类:

+
module Person
+  # 与 included 做对比
+  def self.run
+    p 'I Can Run'
+  end
+
+  def self.included(base)
+    base.class_eval do
+      def self.eat
+        p 'I Can Eat'
+      end
+    end
+  end
+end
+
+User.eat
+# I Can Eat
+User.run
+# undefined method `run' for User:Class (NoMethodError)
+ +

Rails 是如何扩展的

Rails 中有一个非常好用的库 ActiveSupport,对 Ruby 语言的很多特性都实现了扩展,今天要看的就是其中 ActiveSupport::Concern 是如何扩展 include

+
# 所有注释都被我删除了,有兴趣的请查看源码
+module ActiveSupport
+  module Concern
+    class MultipleIncludedBlocks < StandardError
+      def initialize
+        super "Cannot define multiple 'included' blocks for a Concern"
+      end
+    end
+
+    def self.extended(base)
+      base.instance_variable_set(:@_dependencies, [])
+    end
+
+    def append_features(base)
+      if base.instance_variable_defined?(:@_dependencies)
+        base.instance_variable_get(:@_dependencies) << self
+        false
+      else
+        return false if base < self
+        @_dependencies.each { |dep| base.include(dep) }
+        super
+        base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
+        base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
+      end
+    end
+
+    def included(base = nil, &block)
+      if base.nil?
+        if instance_variable_defined?(:@_included_block)
+          if @_included_block.source_location != block.source_location
+            raise MultipleIncludedBlocks
+          end
+        else
+          @_included_block = block
+        end
+      else
+        super
+      end
+    end
+
+    def class_methods(&class_methods_module_definition)
+      mod = const_defined?(:ClassMethods, false) ?
+        const_get(:ClassMethods) :
+        const_set(:ClassMethods, Module.new)
+
+      mod.module_eval(&class_methods_module_definition)
+    end
+  end
+end
+ +

Concern 的源码非常简单,总共就定义了 3 个实例方法,1 个类方法,1 个错误,其中 append_featuresincluded 方法刚刚讲过,我们来看看 Rails 做了什么

+

included:更优雅的 included

def included(base = nil, &block)
+  if base.nil?
+    if instance_variable_defined?(:@_included_block)
+      if @_included_block.source_location != block.source_location
+        raise MultipleIncludedBlocks
+      end
+    else
+      @_included_block = block
+    end
+  else
+    super
+  end
+end
+ +

Rails 对 included 进行的扩展其实很简单

+
    +
  1. included 继续作为 include 的回调使用时,base 不为 nil,则执行 super
  2. +
  3. included 作为 mixin 中的类方法被调用,并传入一个 block,则将该 block 存入 @_included_block,并且不允许有多个 @_included_block 存在
  4. +
+

这么做的目的是,使 mixin 可以通过 included 方法定义 def self.included,让代码更优雅

+

比如,如果使用 def slef.included 方法

+
module Student
+  def self.included(base)
+    base.class_eval do
+      def self.study
+        p 'I Can Study'
+      end
+    end
+  end
+end
+
+class User
+  include Student
+end
+
+User.study
+# I Can Study
+ +

而使用 included 方法就可以这样写

+
require 'active_support/concern'
+
+module Student
+  extend ActiveSupport::Concern
+
+  included do
+    def self.study
+      p 'I Can Study'
+    end
+  end
+end
+
+class User
+  include Student
+end
+
+User.study
+# I Can Study
+ +

class_methods:更简洁的类方法定义

在上面我们看见了 included 是如何更优雅的实现类方法的定义的,而 Rails 还顺便提供了一个更简单的 magic 方法 class_methods

+

使用:

+
require 'active_support/concern'
+
+module Foo
+  extend Concern
+
+  class_methods do
+    def run
+      'I Can Run'
+    end
+  end
+end
+
+class Bar
+  include Foo
+end
+
+p Bar.run
+# I Can Run
+ +

源码:

+
def class_methods(&class_methods_module_definition)
+  mod = const_defined?(:ClassMethods, false) ?
+    const_get(:ClassMethods) :
+    const_set(:ClassMethods, Module.new)
+
+  mod.module_eval(&class_methods_module_definition)
+end
+ +

class_methods 的实现比 included 更简单,就是将 block 转换成一个新的 module ClassMethods,而如何注入则依赖于 append_features 来实现

+

append_features:解决内部依赖

Rails 使用 append_features 解决了一个非常重要的问题,那就是内部依赖的引入

+
module Foo
+  def self.included(base)
+    base.class_eval do
+      def self.test
+        'test'
+      end
+    end
+  end
+end
+
+module Bar
+  def self.included(base)
+    base.class_eval do
+      p test
+    end
+  end
+end
+
+class User
+  include Foo
+  include Bar
+end
+ +

如果 A mixin 依赖于 B mixinincluded 定义的方法,那引入的时候就必须同时引入 AB,这就要求开发者必须对每一个 mixin 的引入都要非常熟悉才能保证不出错。
ActiveSupport::Concern 通过 append_features 解决了这个问题

+
require 'active_support/concern'
+
+module Foo
+  extend ActiveSupport::Concern
+
+  def self.included(base)
+    base.class_eval do
+      def self.test
+        'test'
+      end
+    end
+  end
+end
+
+module Bar
+  extend ActiveSupport::Concern
+  include Foo
+
+  def self.included(base)
+    base.class_eval do
+      p test
+    end
+  end
+end
+
+class User
+  include Bar
+end
+
+# test
+ +

像这样的话,开发者只需要引入 Bar,而不再需要管 Barinclude 了几个模块。

+

Rails 中是如何这么聪明的实现的呢,源码如下:

+
# 1. Foo 和 Bar 使用 extend ActiveSupport::Concern 时,定义实例变量 @_dependencies = []
+def self.extended(base) #:nodoc:
+  base.instance_variable_set(:@_dependencies, [])
+end
+
+def append_features(base)
+  if base.instance_variable_defined?(:@_dependencies)
+    # 2. 当 Bar include Foo 时触发 Foo 的 append_features,发现 Bar 已定义实例变量 @_dependencies
+    # 3. 将当前模块 Foo 添加入 Bar 的 @_dependencies
+    base.instance_variable_get(:@_dependencies) << self
+    false
+  else
+    return false if base < self
+    # 4. 当 User include Bar 时 @_dependencies 中的所有模块(Foo)被 User 同时 include
+    @_dependencies.each { |dep| base.include(dep) }
+    super
+    # 配合 class_methods 方法实现类方法的注入
+    base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
+    # 配合 included 方法实现 self.included
+    base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
+  end
+end
+ +

append_features 除了解决了内部依赖的引入,还实现了两个 magic 方法 includedclass_methods 的注入

+

extend

extend 方法在 Ruby 中会被 extend 模块的方法添加为当前模块下的类方法

+
module Person
+  def name
+    "My name is Person"
+  end
+end
+
+class User
+  extend Person
+end
+
+p User.name
+# My name is Person
+ +

extended:extend 的回调

extend 之后如果需要执行某些回调,则只需要定义 self.extended

+
module Person
+  def self.extended(base)
+    p "#{base} extended #{self}"
+  end
+
+  def name
+    "My name is Person"
+  end
+end
+
+class User
+  extend Person
+end
+# User extended Person
+ +

参考

Ruby 中一些重要的钩子方法

+]]>
+ + 后端 + ruby + + + think + +
+ + 使用 docker 运行 efb + /2023/01/run-efb-in-docker/ + 前言

之前运行 efb 的服务器突然无法连接,怀疑服务器被攻破,无奈之下重装了系统,导致 efb 的所有配置均丢失,特此记录一下最新的用 docker 安装 efb 过程。之前的安装方式可见 CentOS 7 安装 ehforwarderbot V2 来收发微信

+ + +

安装 docker

如果已安装 docker,可以跳过该步骤

+
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh
+ +

构建镜像

    +
  1. 克隆仓库

    +
    git clone https://github.com/jiz4oh/ehforwarderbot.git ehforwarderbot
    +
  2. +
  3. 构建自己的 ehforwarderbot 镜像

    +
    docker build ehforwarderbot/ -t efb
    + +
  4. +
+

Tips:

+
    +
  • 该镜像只安装了 efb-telegram-masterefb-wechat-slave 最新版,如果需要额外的频道可以通过修改 Dockerfile 完成。
  • +
  • 截至目前(2023.01.16), efb-wechat-slave 尚未发布 uos 补丁的新版本,故采用直接使用 github 包的形式
  • +
+

更新配置

    +
  1. (required) 将 telegram bot token 更新到 profiles/default/blueset.telegram/config.yamltoken

    +
  2. +
  3. (required) 获取自己 telegram 账户的 userid,并更新到 profiles/default/blueset.telegram/config.yamladmins

    +
  4. +
  5. (optional) 根据喜好更新 efb-telegram-master 的配置 profiles/default/blueset.telegram/config.yaml

    +
  6. +
  7. (optional) 根据喜好更新 efb-wechat-slave 的配置 profiles/default/blueset.wechat/config.yaml

    +
      +
    • 如果在 profiles/default/config.yaml 中有多个 wechat slave,需要设置多个 wechat 配置目录,比如我有两个微信号:

      +
      master_channel: "blueset.telegram"
      +slave_channels:
      +- "blueset.wechat"
      +- "blueset.wechat#jiz4oh"
      + +

      需要有两个 wechat 目录

      +
      `-- profiles
      +    `-- default
      +        |-- blueset.telegram
      +        |   |-- config.yaml
      +        |-- blueset.wechat
      +        |   |-- config.yaml
      +        |-- blueset.wechat#jiz4oh
      +        |   |-- config.yaml
      +        `-- config.yaml
      + +
    • +
    +
  8. +
+

启动 efb

    +
  1. 启动镜像

    +
    docker run -d --name=efb --restart=always -v $PWD/:/data/ efb
    +
  2. +
  3. 扫码登录

    +
    docker logs -f efb
    + +
  4. +
+

Tips:

+
    +
  • 我的微信有时候没办法从 tg 端重新登录,必须重启 efb 才能扫码成功,通常我使用

    +
    docker rm -f efb >/dev/null 2>&1 && docker run -d --name=efb --restart=always -v $PWD/:/data/ efb
    + +

    来快速重启 efb

    +
  • +
+]]>
+ + 其他 + + + efb + ehforwarderbot + +
+ + vim 中使用 esc 切换英文输入 + /2020/10/vim-fast-esc/ + 前言

在 vim 中使用编辑模式进行了中文输入之后,切换到普通模式时,必须手动切换到英文模式才能进行命令输入,不太方便,故在网上找了找如何自动切换中英文的解决方案

+ + +

rime 用户

方式一

在方案中填入

+
key_binder/bindings:
+  - { when: always, accept: Escape, toggle: ascii_mode}
+ +

这样就可以使用 Esc 键切换为英文模式,但是这样有一个弊端,必须得先按一次 Esc 切换为英文后,再按一次 Esc 切换为普通模式

+

方式二(推荐)

在发行版方案中填入

+
app_options:
+  应用:
+    vim_mode: true
+ +

示例:

+
app_options:
+  com.googlecode.iterm2:
+    vim_mode: true
+ +

这样在 iterm 中使用 vim 的时候,就可以在编辑模式按一下 Esc 切换为英文并且 vim 切换为普通模式

+

!截至此时(2020-11-05)官网 suirrel 稳定版 14.0 尚不支持该功能,可下载测试版常鲜
如何查看当前版本是否支持该 feature?
检查 build/squirrel.yaml 文件 config_version 最低需求 0.34

+

其他输入法用户

因为我是 mac 用户,所以暂时先介绍关于 mac 的设置方案,其他平台的设置方案请查看解决恼人的 vim 中文输入法切换问题

+
    +
  1. 安装依赖

    +
    git clone https://github.com/myshov/xkbswitch-macosx.git
    +cp xkbswitch-macosx/bin/xkbswitch /usr/local/bin
    +git clone https://github.com/myshov/libxkbswitch-macosx.git
    +cp libxkbswitch-macosx/bin/libxkbswitch.dylib /usr/local/lib/
    +
  2. +
  3. 安装 vim 插件 vim-xkbswitch
    Vundle 用户添加到 .vimrc

    +
    Plugin 'lyokha/vim-xkbswitch'
    +" 然后执行 :PluginInstall
    + +
  4. +
+

参考

[提案] app_options 参数设定追加 esc 自动切换 ascii_mode 选项

+

解决恼人的 vim 中文输入法切换问题

+]]>
+ + 其他 + + + vim + rime + +
+ + 配置 Webpack 解析 @ 路径 + /2020/10/webpack-resovle-@-path/ + 前言

Vue-cli 默认配置了一个使用 @ 表示 src 的功能,这个功能的原理是配置 webpack 解析路径,这篇文章来介绍如何配置 Webpack 使其他项目比如 React 也能使用这个功能

+ + +

配置

webpack 的配置文件中,写入

+
resolve: {
+    // 自动补全的扩展名
+    extensions: ['.js', '.vue', '.json'],
+    // 默认路径代理
+    // 例如 import Vue from 'vue',会自动到 'vue/dist/vue.common.js'中寻找
+    alias: {
+        '@': resolve('src'),
+        // '@': paths.appSrc, // react
+        '@config': resolve('config'),
+        'vue$': 'vue/dist/vue.common.js'
+    }
+}
+
+ +

IDEA 识别

IDEA 中无法正确解析 @ 代表的路径,导致经常提醒 Module is not installed

+

20201015121929

+ + +

解决方案

项目根目录新建 jsconfig.json 文件即可

+
{
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    }
+  },
+  "exclude": [
+    "node_modules",
+    "dist",
+    "build"
+  ]
+}
+]]>
+ + 前端 + + + webpack + +
+ + 谈谈免流 + /2023/07/zero-rating/ + 免流的由来

不知道各位是否还记得曾经被 5 元 30M 支配的恐惧吗?
在大概 10 多年前,那时候的流量非常昂贵,网页也非常简单,大部分都是一些文字信息,
随着时间的推移,互联网在飞速发展,手机上网从文字转变为了以视频和图片为主,网页内容越来越丰富,10M 流量几个图片就用完了,流量需求大幅提升,
但是运营商套餐价格并没有随着需求的提升而降低,一度都是 5 元 30M 的离谱价格,
人们为了(白嫖)推进运营商的技术发展,研究出了免流

+

什么是免流

“免流”通常是指在移动网络中使用特定的应用或服务而不消耗用户的数据流量,即上网不要钱。
通常由移动运营商或特定的应用提供商推出,旨在吸引更多用户使用这些应用或服务。

+ + +

免流的基本原理

上述的免流通常局限于某些应用,我们所聊的免流是不限于特定应用的免流,通过特定手段欺骗运营商的计费系统达到免费的目的。
首先我们要了解运营商的计费系统是如何工作的

+

访问正常网站:

+
flowchart LR
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    baidu.com
+    计费 1M"]-->B["代理服务器
+
+    baidu.com"]
+end
+
+用户--baidu.com-->运营商-->百度服务器
+ +

访问免流网站:

+
flowchart LR
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    10086.cn
+    不计费"]-->B["代理服务器
+
+    10086.cn"]
+end
+
+用户--10086.cn-->运营商-->中国移动服务器
+ +

免流的核心原理就是让计费系统以为用户在访问免流网站,但实际上却访问了正常网站

+
flowchart LR
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    10086.cn
+    不计费"]-->B["代理服务器
+
+    baidu.com"]
+end
+
+用户--baidu.com-->运营商-->百度服务器
+ +

免流的几种方式

本地免流

本地免流是在用户手机上运行一个代理程序对所有数据包进行修改,从而欺骗计费系统

+
flowchart LR
+
+subgraph 手机
+    direction LR
+    浏览器--baidu.com-->代理程序
+end
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    10086.cn
+    不计费"]-->B["代理服务器
+
+    baidu.com"]
+end
+
+手机-->运营商-->百度服务器
+ +

代理程序的主要实现方式是对 http 数据包请求头进行修改。

+

请求头中有两个关键字段 Host 和 X-Online-Host
假设计费系统是通过检测 http 数据包中的 Host 字段进行计费,代理服务器通过 http 数据包中的 X-Online-Host 进行实际数据访问,则可以通过修改 http headers 中的 Host 字段即可达到欺骗计费系统的目的。

+

运营商也不是傻子,对检测系统进行了更新,上述手段就失效了,后来各大网友开始了和运营商的斗志斗勇

+
    +
  1. 有插入两个 Host 字段的双 h 模式,让计费系统查看第一个 Host 字段,让代理服务器查看第二个 Host 字段
  2. +
  3. 也有插入两个 X-Online-Host 的双 x 模式
  4. +
  5. 伪首模式
  6. +
  7. 伪彩模式
  8. +
  9. +
+

本地免流优点是不需要额外的资源,在用户本地手机即可实现,主要利用的是运营商计费系统和代理服务器系统的实现差异;缺点是各地运营商的系统差异不一致,在上海能免的模式在广东并不一定通用,并且可利用修改的地方有限,所有模式被封禁后就无法使用了

+

本地免流最大的问题是因为当时运营商账单具有滞后性,当你之前使用的免流模式被修复后,流量已经开始正常计费,你却全然不知,第二天早上起来发现收到了 10086 的巨额账单

+

定向免流

2015 开始,国家要求三大运营商提速降费,漫游费逐渐被取消,流量都是全国通用,费用也越来越低,但流量费用还没低到可以任意挥霍的程度。
针对有些人喜欢看抖音,有些人喜欢看腾讯视频,三大运营商纷纷推出了定向流量卡,常见的有腾讯王卡,阿里宝卡等。
定向流量卡是针对某些互联网服务在通用流量之外给予大额的定向流量,比如腾讯王卡允许腾讯系应用免费使用最多 40G 流量。

+

定向免流就是针对这部分流量卡,将其他网站的流量伪装成特定应用的流量使用实现定向流量,并不是完全的无限流量。
比如使用腾讯王卡时将其他应用如抖音的流量伪装成腾讯系应用的流量

+
flowchart LR
+
+subgraph 手机
+    direction LR
+    浏览器--baidu.com-->代理程序
+end
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    qq.com
+    定向流量计费1M"]-->B["代理服务器
+
+    baidu.com"]
+end
+
+手机-->运营商-->百度服务器
+ +

云端免流(云免)

本地免流利用了计费系统和代理服务器之间的差异,后续差异被不断补全(感谢各大网友自费做 QA),甚至后来运营商取消了代理服务器,直接使用计费系统进行互联网访问,本地免流就此绝迹。
后面常用的一般都是云端免流,这是现在的主流免流方式。

+

云端免流的原理需要一点网络协议的基础知识,了解 tcp/ip 的工作原理

+
ip

现有互联网是基于 tcp/ip 为架构组成的,计算机之间的通信并不是基于域名而是基于 ip 协议。

+

ip 协议可以理解为是计算机中的电话号码,比如我们想要联系张三的时候,我们可以通过输入张三的电话号码 123456 拨打电话联系到他

+
dns

但目前我们访问网站的时候是通过输入域名,为什么计算机之间的通信却基于 ip 协议?他们之间是如何转换的?

+

这就是 dns 所做的事情。
因为 ip 地址不易于记忆以及输入,所以域名被发明出来简化输入,相当于是电话号码的联系人名称,比如我们通常在电话薄里面保存张三的电话号码为 123456,张三就相当于是域名,123456 是 ip,我们想要联系张三的时候只需要在电话薄里面搜索 ‘张三’,而不是输入 123456

+

dns 的作用就是查询电话薄,比如我想要访问 google.com 的时候,计算机并不知道应该如何访问到 google.com,所以它需要去向电话薄查询 (dns query) google.com 的电话 (ip)

+
sequenceDiagram
+    participant User
+    participant Computer
+    participant DNS Server
+    participant Google
+
+User->>Computer: User open browser and enter 'google.com'
+Computer->> DNS Server: Computer send DNS query to DNS Server
+DNS Server-->>Computer: DNS Server response 123456 to Computer
+Computer->>Google: Computer send request to Google
+Google-->>Computer: Google response to Computer
+Computer-->>User: Computer display the response
+ +
http

http 协议是普通人最常见的互联网协议,是互联网的基石,我们常见的所有应用几乎都是使用 http 协议进行通信。

+

http 协议的消息结构如下

+
GET / HTTP/1.1
+Host: www.baidu.com
+
+xxxxxxx此处即是http请求内容正文xxxxxxx
+ +

在本地免流中,我们采用的是修改 Host 字段绕过计费系统。
在云端免流中,采用的方式其实也类似,区别其实是在于绕过计费系统之后如何访问正确的目标网站?

+

思路其实很简单,既然本地免流由于运营商的代理服务器和计费系统合并而被彻底修复,那我们如果能够自己实现一个代理服务器不就和之前的方式类似了吗?

+
flowchart LR
+
+subgraph 运营商
+    direction LR
+    A["计费系统
+
+    10086.cn
+    不计费"]
+end
+
+subgraph 云端服务器
+    direction LR
+    B["baidu.com"]
+end
+
+用户--baidu.com-->运营商-->云端服务器-->百度服务器
+ +
云免原理

具体是将原始请求封装到 http body 中并发送给代理服务器,代理服务器解析 http body 之后还原原始请求并进行实际请求

+

upgit_20230808_1691510056.png

+

+
sequenceDiagram
+    participant User
+    participant 运营商
+    participant 云端服务器 as 云端服务器(10.10.10.10)
+    participant 百度服务器 as 百度服务器(3.3.3.3)
+
+User-->>运营商: 发送一个数据包到 10.10.10.10 并在 http header 中标注这是发给 10086.cn 的数据包
+运营商-->>云端服务器: 计为免费并将数据包发送给 10.10.10.10
+云端服务器-->>百度服务器: 解开数据包并发送给实际请求地址 3.3.3.3
+百度服务器->>云端服务器: 返回内容
+云端服务器->>运营商: 返回内容
+运营商->>User: 返回内容
+ +

直连免流

定向免流还有一种免流方式,不需要云端服务器。
有些定向卡是允许免去某个应用的所有流量而不是某个特定域名的流量,比如腾讯王卡可以免去 qq 浏览器的所有流量,原理是在该应用中内置一个代理服务器,代理服务器产生的流量被运营商计入定向流量。

+

直连免流就是通过抓包获取应用内置的代理服务器并将其用于所有应用访问。

+

不过因为现在代理服务器都增加了动态验证,基本都失效了。

+

停机免流

电信会在用户停机之后给用户开通一个花费充值的绿色通道,这样即使你停机之后仍然能够通过访问网络缴存话费。所以和定向免流类似,停机免流的实现原理也是通过将 Host 改为绿通的网址就能实现免流了。

+

对于停机用户来说,每个月只需要缴纳停机保号费用(通常5元)即可实现无限流量。不过电信随即采取了相应的策略,大部分地区对停机用户的上网速度施加了限制,毕竟你充个话费要那么快干嘛。

+

免流的局限性

因为运营商的计费系统如何工作其实是一个黑盒,外界很难得知什么时候工作机制就发生了变化,比如除了检查 host 字段,计费系统也可以检查端口。互联网的流量通常使用的都是 80/443,所以云端服务器的端口通常必须使用 80/443。

+

另一个缺点是为了实现免流必须使用全局代理,也就是不管国内还是国外流量均需通过云端服务器进行代理,如果云端服务器在国外,那么访问国内的服务会变得很慢,并且有些服务只能是国内 ip 才能使用;如果云端服务器在国内,那还需要一台额外的国外节点进行翻墙代理,以及国内服务器商宽其实非常昂贵,一年花销可能需要好几千,买服务器的钱可能比你免的流量贵的多得多。

+]]>
+ + 网络 + + + 白嫖 + +
+ + Rime 输入法指北 + /2020/10/how-to-use-rime/ + 前言

出于对国产输入法软件的不信任,再加上国产输入法广告太多等因素的影响,促使我想要尽快找到一个开源、快捷、不自动上传云端的输入法。

+

在网上搜索时,Rime 收到了一致好评,让我萌生了非常大的兴趣,故特意找到了 Rime 的相关资料并整理。

+

本文基于 Rime v1.5.3 版本进行整理,其他版本可能不适用。

+ + +

关于 Rime

什么是 Rime

Rime 全名是「中州韵输入法引擎」,Rime 不是一种输入法。是从各种常见键盘输入法中提炼出来的抽象的输入算法框架,由 佛振 大佬开发。
Rime 通过各种输入方案来实现对几乎所有中文的支持,包括但不限于【拼音】、【注音】、【双拼】、【五笔】、【仓颉】,并且可以简单的进行【简繁】的切换

+

什么是输入方案

Rime 由于本质只是一个框架,本身是不知道如何输入的。
如果我们想要使用 Rime,就得定义一些关于如何输入的设置来告诉 Rime,这些设置就是输入方案。

+

Rime 的版本

Rime 是一个天生跨平台的框架,在每一个平台都有对应的发行版本:

+
    +
  1. Linux:
      +
    • 官方:ibus-rime(中州韵)
    • +
    • 第三方:fcitx-rime
    • +
    +
  2. +
  3. windowns:
      +
    • 官方:Weasel(小狼毫)
    • +
    • 第三方:PRIME
    • +
    +
  4. +
  5. macOS:
      +
    • 官方:Squirrel(鼠须管)
    • +
    • 第三方:XIME
    • +
    +
  6. +
  7. Android:
      +
    • 第三方:Trime(同文)
    • +
    +
  8. +
  9. iOS:
      +
    • 第三方:iRime
    • +
    +
  10. +
+

上面是 Rime 在各平台对应的软件,Rime 默认提供了两个拼音输入方案「朙月拼音」和「地球拼音」,两者都可以输入准确的繁体和简体,而且「地球拼音」还支持声调输入。
Rime 还支持了许多种方言拼音,如吴语、粤语,甚至中古汉语。

+

Rime 的文件架构

Rime 所有的配置文件、输入方案及词典文件,均是普通的文本文档,均要求 UTF8 编码。
其中配置文件、输入方案要求使用 yaml 格式的文件。词典文件使用普通的 txt 文档即可。

+

Rime 中将文件分为 共享文件夹用户文件夹

+
    +
  • 共享文件夹 中存放的是 Rime 发行版的默认文件,一般是不允许进行修改的
      +
    • 【中州韵 ibus-rime/usr/share/rime-data/
    • +
    • 【小狼毫 Weasel安装目录\data
    • +
    • 【鼠须管 Squirrel/Library/Input Methods/Squirrel.app/Contents/SharedSupport/
    • +
    +
  • +
  • 用户文件夹 的存放位置
      +
    • 【中州韵 ibus-rime~/.config/ibus/rime/
    • +
    • 【小狼毫 Weasel%APPDATA%\Rime,也可以通过“开始菜单\小狼毫输入法\用户文件夹”打开。
    • +
    • 【鼠须管 Squirrel~/Library/Rime/,也可以通过“系统输入法菜单/鼠须管/用户设置”打开。
    • +
    +
  • +
+

用户文件夹

刚开始安装后,在 用户文件夹 中是没有文件的,当我们运行 Rime 后会出现以下文件/文件夹:

+
    +
  • build/*:这个文件夹下存放的是我们每次部署之后生成的静态文件
      +
    • <输入方案名>.prism.bin:Rime 棱镜,拼写运算规则
    • +
    • <词典名>.table.bin:Rime 固态词典,按音节编码检索词条的索引
    • +
    • <词典名>.reverse.bin:Rime 反查词典,按词条检索编码的索引
    • +
    +
  • +
  • <词典名>.userdb/*:这个文件夹是我们的词典文件夹,当我们进行了输入之后,会将输入的内容存入,以便进行词语调频
  • +
  • installation.yaml:这个文件是关于我们本机 Rime 的安装时间、版本信息等
  • +
  • user.yaml:这个文件是关于 Rime 的一些设置选项,比如我们上次选择的输入方案
  • +
+

Rime 的配置文件都需要放于 用户文件夹 下,分为三类:

+
    +
  • default.yaml:全局配置,存放跨输入方案的通用配置
  • +
  • <输入方案名>.schema.yaml:存放某一个输入方案的配置
    如:double_pinyin_flypy.schema.yaml
  • +
  • <发布版名>.yaml:存放某个发行版的独有配置
    如:symbols.yaml
  • +
+

如果有其他额外文件,也需要放入 用户文件夹 下,才能被配置文件正确使用

+

自定义 Rime 的全局配置

我的全局配置 default.custom.yaml

+

可配置项:

+
    +
  • page_size:每页个数,默认 5,允许 1~9
  • +
+

该项可在每个输入方案中单独设置

+

示例:

+
menu:
+  page_size: 8
+ +

schema_list

可配置项:

+
    +
  • schema:每一项 schema 对应一项方案的 schema_id
  • +
+

示例:

+
schema_list:
+  - schema: luna_pinyin
+  - schema: luna_pinyin_simp
+  - schema: luna_pinyin_fluency
+  - schema: bopomofo
+  - schema: bopomofo_tw
+  - schema: cangjie5
+  - schema: stroke
+  - schema: terra_pinyin
+ +

switcher

可配置项:

+
    +
  • caption:切换器被调用时屏幕显示的名字
  • +
  • hotkeys:切换器调用的快捷键
  • +
  • abbreviate_options
  • +
  • fold_options
  • +
  • option_list_separator
  • +
  • save_options
  • +
+

示例:

+
switcher:
+  abbreviate_options: true
+  caption: "〔方案選單〕"
+  fold_options: true
+  hotkeys:
+    - "Alt+Shift+Control+grave"
+    - "Control+grave"
+  option_list_separator: "/"
+  save_options:
+    - full_shape
+    - ascii_punct
+    - simplification
+    - extended_charset
+    - zh_hant
+    - zh_hans
+    - zh_hant_tw
+ +

自定义 Rime 的输入法方案

Rime 输入方案主要分为三部分

+
    +
  • schema:关于整个输入方案的信息阐述
  • +
  • switches:可以切换的开关,比如切换全半角,切换简繁体
  • +
  • engine:Rime 的核心配置,配置整个 Rime 是如何进行运作
  • +
+ +

特殊说明:

+

输入方案的某些细项设置是可以写在全局设置中,比如

+
    +
  • key_binder
  • +
  • punctuator
  • +
  • ascii_composer
  • +
  • recognizer
  • +
+

在输入方案中设置 menu/page_size 是可以的

+

schema

可配置项:

+
    +
  • name:方案的显示名称〔即出现于方案选单中,通常为中文〕
  • +
  • schema_id:方案内部名,在代码中引用此方案时的名称,通常由英文,数字,下划线组成
  • +
  • author:方案作者。如果您对方案做出了修改,请保留原作者名,放入自己的名字加在后面
  • +
  • description:简要描述方案历史,码表来源,该方案规则等
  • +
  • dependencies:如果本方案依赖于其他方案〔通常来说会依赖其他方案做为反查,抑或是两种或多种方案混用时]
  • +
  • version:版本号,在发布新版前请确保已升版本号
  • +
+

示例:小鹤双拼

+
schema:
+  name: "小鶴雙拼"
+  schema_id: double_pinyin_flypy
+  author:
+    - "double pinyin layout by 鶴"
+    - "Rime schema by 佛振 <chen.sst@gmail.com>"
+  description: |
+    朙月拼音+小鶴雙拼方案。
+  dependencies:
+    - stroke
+  version: 0.18
+ +

switches

    +
  • ascii_mode:中英文切换开关。0 为中文,1 为英文

    +
  • +
  • full_shape:全角符号/半角符号开关。默认 0 为半角,1 为全角。注意,开启全角时英文字母亦为全角。

    +
  • +
  • extended_charset:字符集开关。0 为 CJK 基本字符集,1 为 CJK 全字符集。仅 table_translator 可用

    +
  • +
  • ascii_punct:中西文标点切换开关,0 为中文句读,1 为西文标点。

    +
  • +
  • simplification:是转化字开关。一般情況下与上同,0 为不开启转化,1 为转化。
    simplification 选项名称可自定义,亦可添加多套替換用字方案:

    +
    - name: zh_cn
    +  states: ["漢字", "汉字"]
    +  reset: 0
    + +

    +
    - options: [ zh_trad, zh_cn, zh_mars ]
    +  states:
    +    - 字型 → 漢字
    +    - 字型 → 汉字
    +    - 字型 → 䕼茡
    +  reset: 0
    + +
      +
    • name/options:須与 enginesimplifieroption_name 相同

      +
      switches:
      +  - name: zh_simp
      +    reset: 1
      +    states: [ 漢字, 汉字 ]
      +simplifier:
      +  option_name: zh_simp
      +
    • +
    • states:可不写,如不写则此开关存在但不可见,可由快捷鍵操作

      +
    • +
    • reset:设定开关默认状态〔若 reset 为空,则使用上次状态〕

      +
    • +
    +
  • +
  • 字符集过滤。此选项沒有默认名称,須配合 charset_filter 使用。可單用,亦可添加多套字符集

    +
  • +
+

示例:

+
switches:
+  - name: ascii_mode
+    reset: 0
+    states: ["中文", "西文"]
+  - name: full_shape
+    states: ["半角", "全角"]
+  - name: extended_charset
+    states: ["通用", "增廣"]
+  - name: simplification
+    states: ["漢字", "汉字"]
+  - name: ascii_punct
+    states: ["句讀", "符號"]
+ +

engine

Rime 的核心原理是通过 enagine 下的 4 大组件对用户输入进行处理,4 大组件分别是:

+
    +
  • Processors
  • +
  • Segmentors
  • +
  • Translators
  • +
  • Filters
  • +
+

整个流程为:

+
    +
  1. Processors 下的各个 processor 对用户的输入(即按下键盘的哪一个键)依次进行处理,将按键按照预设的规则对按键进行响应
      +
    • 不处理:Rime 不对该按键做任何响应,使用系统默认操作
    • +
    • 特殊操作:比如 Enter 上屏,切换输入方案、组合键等
    • +
    • 输入候选:该按键是需要转换为文字的按键,比如 123abc,将该按键字符存入【输入码】上下文
    • +
    +
  2. +
  3. 当【输入码】上下文改变时,Segmentors 下的 segmentor 会将当前输入码根据格式分段,各自打上标签。比如【朙月拼音】中,输入码 2012nian\,划分为三个编码段:2012(贴 number 标签)、nian(贴 abc 标签)、\(贴 punct 标签)。
  4. +
  5. 顾名思义,Translators 完成由编码到文字的翻译。但有几个要点:
      +
    • 翻译的对象是划分好的一个代码段。
    • +
    • 某个 translator 组件往往只翻译具有特定标签的代码段。
    • +
    • 翻译的结果可能有多条,每条结果成为一个展现给用户的候选项。
    • +
    • 代码段可由几种 translator 分别翻译、翻译结果按一定规则合并成一列候选。
    • +
    • 候选项所对应的编码未必是整个代码段。用拼音敲一个词组时,词组后面继续列出单字候选,即是此例。
    • +
    +
  6. +
  7. 翻译完成后,由 Filters 对所有翻译结果进行处理,比如去重
  8. +
+

示例:

+
engine:
+  processors:
+    - ascii_composer
+    - recognizer
+    - key_binder
+    - speller
+    - punctuator
+    - selector
+    - navigator
+    - express_editor
+  segmentors:
+    - ascii_segmentor
+    - matcher
+    - abc_segmentor
+    - punct_segmentor
+    - fallback_segmentor
+  translators:
+    - punct_translator
+    - script_translator
+    - "table_translator@custom_phrase"
+  filters:
+    - "simplifier@emoji_suggestion"
+    - "simplifier@zh_simp"
+    - uniquifier
+    - single_char_filter
+ +

Processors

    +
  • ascii_composer:处理英文模式及中英文切换
  • +
  • recognizer:与 matcher 搭配,处理符合特定规则的输入码,如网址,反查等
  • +
  • key_binder:在特定条件下将按键绑定到其他按键,如重定义逗号,句号为预设翻页,开关快捷键等
    处理某些自定义的组合键
  • +
  • speller:拼写处理器,接受字符按键,编辑输入
    处理自定义的键,通常为 26 个英文字母
  • +
  • punctuator:句读处理器,将个别字符按键直接映射为标点符号或文字
    处理句读键、数字键
  • +
  • selector:选字处理器
    处理数字键、上下方向键、PageUpPageDown
  • +
  • navigator:处理输入栏内的光标移动
    处理左右方向键、HomeEnd
  • +
  • express_editor:编辑器
    处理空格、回车、回退键
  • +
+

不常用:

+
    +
  • fluid_editor:句式编辑器,用于以空格断开词,回车上屏的【注音】,【语句流】等输入方案,与 express_editor 互斥,也可以写作 fluency_editor
  • +
  • chord_composer:和弦作曲家或曰并击处理器,用于【宫保拼音】等多键并击的输入方案
  • +
  • lua_processor:使用 lua 自定义按键,后接 @lua函数名
      +
    • lua 函数名即用户文件夹内 rime.lua 中函数名,参数为(key, env)
    • +
    +
  • +
+
recognizer

可配置项:

+
    +
  • import_preset:从外部文件导入
  • +
  • patterns:配合 segmentor 的 prefix 和 suffix 完成段落划分、tag 标记
  • +
+

示例:

+
recognizer:
+  import_preset: default
+  patterns:
+    code: "[a-zA-Z]+(*$"
+    email: "^[A-Za-z][-_.0-9A-Za-z]*@.*$"
+    html: "^<[a-z]+>$"
+    punct: "^/([a-z]+|[0-9]0?)$"
+    uppercase: "[A-Z][-_+.'0-9A-Za-z]*$"
+    url: "^(www[.]|https?:|ftp[.:]|mailto:|file:).*$|^[a-z]{1,10}[.:_-].*$"
+ +
key_binder

可配置项:

+
    +
  • import_preset:从外部文件导入
  • +
  • bindings:设置组合键的映射
    每一条 binding 中包含字段:
      +
    • accept:键盘输入的按键
    • +
    • when:作用条件
    • +
    • send:实际调用的按键
    • +
    • toggle:切换开关,和 send 不共存
    • +
    +
  • +
+

acceptsend 作用的键,需要输入 Ibus 风格的键名:

+

Mac 只支持 Alt/Option 键,不支持Command键,详见

+
Shift_L		左Shift
+Shift_R		右Shift
+Control_L	左Ctrl
+Control_R	右Ctrl
+Alt_L		左Alt
+Alt_R		右Alt
+# 在 windows 是 windows 键
+Meta_L		左Meta
+Meta_R		右Meta
+Super_L		左Super
+Super_R		右Super
+Hyper_L		左Hyper
+Hyper_R		右Hyper
+
+BackSpace	退格
+Tab			制表符
+Caps_Lock	大写键
+Linefeed	换行
+Clear		清除
+Return		回车
+Pause		暂停
+Escape		Esc退出
+Delete		刪除
+Home		Home
+Left		左箭头
+Up			上箭头
+Right		右箭头
+Down		下箭头
+Prior		上翻
+Page_Up		上翻
+Next		下翻
+Page_Down	下翻
+space 		空格
+Sys_Req	
+End			末位
+Begin		始位
+Shift_Lock	上檔鎖
+Scroll_Lock	滚动锁
+Num_Lock	小键盘锁
+Select		选择
+Print		打印
+Execute		执行
+Insert		插入
+Undo		还原
+Redo		重做
+Menu		菜单
+Find		寻找
+Cancel		取消
+Help		帮助
+Break		中断
+
+grave			`
+asciitilde		~
+exclam			!
+at				@
+numbersign		#
+dollar			$
+percent			%
+ampersand		&
+asciicircum		^
+asterisk		*
+parenleft		(
+parenright		)
+underscore		_
+minus			-
+plus			+
+equal			=
+bracketleft		[
+bracketright	]
+braceleft		{
+braceright		}
+bar				|
+slash			/
+backslash		\
+semicolon		;
+colon			:
+apostrophe		'
+quotedbl		"
+comma			,
+period			.
+less			<
+greater			>
+question		?
+
+KP_Space		小键盘空格
+KP_Tab			小键盘制表符
+KP_Enter		小键盘回车
+KP_Delete		小键盘刪除
+KP_Home			小键盘原位
+KP_Left			小键盘左箭头
+KP_Up			小键盘上箭头
+KP_Right		小键盘右箭头
+KP_Down			小键盘下箭头
+KP_Prior		小键盘上翻
+KP_Page_Up		小键盘上翻
+KP_Next			小键盘下翻
+KP_Page_Down	小键盘下翻
+KP_End			小键盘末位
+KP_Begin		小键盘始位
+KP_Insert		小键盘插入
+KP_Equal		小键盘等于
+KP_Multiply		小键盘乘号
+KP_Add			小键盘加号
+KP_Subtract		小键盘減号
+KP_Divide		小键盘除号
+KP_Decimal		小键盘小数点
+KP_0			小键盘0
+KP_1			小键盘1
+KP_2			小键盘2
+KP_3			小键盘3
+KP_4			小键盘4
+KP_5			小键盘5
+KP_6			小键盘6
+KP_7			小键盘7
+KP_8			小键盘8
+KP_9			小键盘9
+ +

示例:

+
key_binder:
+  bindings:
+    - {accept: minus, send: Page_Up, when: paging}
+    - {accept: equal, send: Page_Down, when: has_menu}
+    - {accept: bracketleft, send: Page_Up, when: paging}
+    - {accept: bracketright, send: Page_Down, when: has_menu}
+    - {accept: 9, send: Page_Up, when: paging}
+    - {accept: 0, send: Page_Down, when: has_menu}
+    - {accept: semicolon, send: 2, when: has_menu}
+    - {accept: apostrophe, send: 3, when: has_menu}
+ +
speller

可配置项:

+
    +
  • alphabet:设置本 speller 需要监听的键
  • +
  • initials:设置哪些键仅在开头的时候才需要监听
  • +
  • finals:设置哪些键仅在末尾的时候才需要监听
  • +
  • delimiter:分词符
  • +
  • algebra:Rime 核心的拼写运算规则,所有 algebra 算出的规则最后写入 prism
  • +
  • max_code_length:行码最大码長,超过则自动顶字上屏〔number
  • +
  • auto_select:是否开启自动上屏〔truefalse
  • +
  • auto_select_pattern:自动上屏规则(正则),当输入码匹配正则时自动顶字上屏。
  • +
  • use_space:空格是否可作为输入码〔truefalse
  • +
+
punctuator

可配置项:

+
    +
  • import_preset:从外部文件导入
  • +
  • half_shape:半角模式下的句读映射
    每条选项可以设置上屏模式
      +
    • 默认:选项模式
    • +
    • commit:直接上屏
    • +
    • pair:交替上屏
    • +
    +
  • +
  • full_shape:全角模式下的句读映射
    half_shape 可设置上屏模式
  • +
  • use_space:是否使用空格顶字〔truefalse
  • +
+

示例:

+
punctuator:
+  # 标点及特殊表情,引入 symbols 文件
+  import_preset: symbols
+  # 覆盖 symbols 文件对应 key
+  symbols:
+    "/dn": [,,,,,,,,, ↩︎,,,,,,,,,]
+    "/fh": [ ©, ®,,,,,,,,,,,,,, ☑︎,,,,,,,,,,,,]
+    "/xh": [, ×,,,,,,,,,,,,]
+  half_shape:
+    "`": "·"
+    "~": "~"
+    "@": "@"
+    "#": "#"
+    "$": ["¥", "$", "€", "£", "¢", "¤"]
+    "%": "%"
+    "^": "……"
+    "*": ["*", "×", "·", "・", "※", "*", "❂"]
+    "_": "——"
+    "=": "="
+    '\': "、"
+    "'":
+      pair:
+        - "‘"
+        - "’"
+    "|": "|"
+    "(": "("
+    ")": ")"
+    "[": "【"
+    "]": "】"
+    "{": "「"
+    "}": "」"
+    "<": "《"
+    ">": "》"
+    "/": ["/", "÷"]
+ +

Segmentors

    +
  • ascii_segmentor:标识英文段落〔例如在英文模式下〕字母直接上屛
  • +
  • matcher:配合 recognizer 标识符合特定规则的段落,如网址,反查等,加上特定 tag
  • +
  • abc_segmentor:标识常规的文字段落,加上 abc 这个默认tag
  • +
  • punct_segmentor:标识句读段落〔键入标点符号用〕加上 punct 这个tag
  • +
  • fallback_segmentor:标识其他未标识段落
  • +
  • affix_segmentor:用户自定义 tag
      +
    • 可加载多个实例,后接 @tag名
    • +
    +
  • +
+

不常用:

+
    +
  • lua_segmentor 使用lua自定义切分,后接 @lua函数名
  • +
+

Translators

    +
  • table_translator:编码表翻译器,用于仓颉,五笔等基于编码表的输入方案
      +
    • 可加载多个实例,后接 @翻译器名〔如:cangjie,wubi等〕
    • +
    +
  • +
  • script_translator:脚本翻译器,用于拼音,粤拼等基于音节表的输入方案
      +
    • 可加载多个实例,后接 @翻译器名〔如:pinyin,jyutping等〕
    • +
    +
  • +
  • punct_translator:配合 punct_segmentor 转换标点符号
  • +
  • echo_translator:没有其他候选字时,显示输入码〔输入码可以 Shift+Enter 上屛〕
  • +
+

不常用:

+
    +
  • reverse_lookup_translator:反查翻译器,用另一种种编码方案查码
  • +
  • lua_translator:使用 lua 自定义输入,例如动态输入当前日期,时间,后接 @lua函数名
      +
    • lua 函数名即用户文件夹内 rime.lua 中函数名,参数为(input, seg, env)
    • +
    • 可以 env.engine.context:get_option(“option_name”)方式绑定到 switch 开关/key_binder 快捷键
    • +
    +
  • +
+
translator

每个输入方案有一个关于 translator 的全局设置,可设置项:

+
    +
  • dictionary:翻译器使用的字典名

    +
  • +
  • prism:设定此翻译器的 speller 生成的棱镜文件名,或此副编译器调用的棱镜名

    +
  • +
  • user_dict:设定用户词典名

    +
  • +
  • db_class:设定用户词典类型,可设 tabledb〔文本〕或 userdb〔二进制〕

    +
  • +
  • preedit_format:上屛码自定义

    +
  • +
  • comment_format:提示码自定义

    +
  • +
  • initial_quality:设定此翻译器结果优先级

    +
  • +
  • disable_user_dict_for_patterns:禁止某些编码录入用户词典〔truefalse

    +
  • +
  • enable_sentence:是否开启自动造句〔truefalse

    +
  • +
  • enable_user_dict:是否开启用户词典〔用户词典记录动态字词频,用户词〕〔truefalse

    +
  • +
+

table_translator 生效:

+
    +
  • enable_charset_filter:是否开启字符集过滤〔cjk_minifier 启用后可适用于 script_translator〕〔truefalse
  • +
  • enable_encoder:是否开启自动造词〔truefalse
  • +
  • encode_commit_history:是否对已上屛词自动成词〔truefalse
  • +
  • max_phrase_length:最大自动成词词长〔number
  • +
  • enable_completion:提前显示尚未输入完整码的字〔truefalse
  • +
  • sentence_over_completion:在无全码对应字而仅有逐键提示时也开启智能组句〔truefalse
  • +
  • strict_spelling:配合 speller 中的 fuzz 规则,仅以畧拼码组词〔truefalse
  • +
+

script_translator 生效

+
    +
  • spelling_hints:设定多少字以内部预定标注完整带调拼音
  • +
+

除了 translator 项设置,其他通过 @ 定义的副翻译器还可单独设置:

+
    +
  • tag:设定此翻译器针对的 tag。默认 abc
  • +
  • prefix:设定此翻译器的前缀标识,默认无
  • +
  • suffix:设定此翻译器的后缀标识,默认无
  • +
  • tips:设定此翻译器的输入前提示符,默认无
  • +
  • closing_tips:设定此翻译器的输入结束提示符,默认无
  • +
+

示例:

+
# tarnslator
+translator:
+  dictionary: luna_pinyin
+  prism: luna_pinyin_simp
+  preedit_format:
+    - xform/([nl])v/$1ü/
+    - xform/([nl])ue/$1üe/
+    - xform/([jqxy])v/$1u/
+
+# 副翻译器设置
+custom_phrase: # 這是一個 table_translator
+  dictionary: ""
+  user_dict: custom_phrase
+  db_class: tabledb
+  enable_sentence: false
+  enable_completion: false
+  initial_quality: 1
+ +
词典与码表

translator 通过词典来翻译对应的片段,通常命名为 <词典名>.dict.yaml

+

词典

+
    +
  • name:词典名,内部使用,可以与配套的输入方案名一致,也可不同;
  • +
  • version:管理词典的版本;
  • +
  • sort:词条初始排序方式,〔by_weight(按词频高低排序)或 original(保持原码表中的顺序)〕;
  • +
  • use_preset_vocabulary:选择是否导入默认词汇表【八股文】〔truefalse〕。
  • +
+

示例:

+
# 这里以 --- ... 分別标记出 YAML 文件的起始与结束位置
+---
+name: luna_pinyin
+version: "0.9"
+sort: by_weight
+use_preset_vocabulary: true
+# 在 ... 标记之后的部分就不会作为 YAML 文件來解析
+...
+ +

码表,定义编码与文字的映射关系,通过制表符分割为三列:

+
    +
  • 文字
  • +
  • 编码,如果该编码有多个音节,各音节以空格分开
  • +
  • 权重,相同编码时出现在候选列表前面的几率
  • +
+

示例:

+
你	ni
+我	wo
+的	de	99%
+的	di	1%
+地	de	10%
+地	di	90%
+目	mu
+好	hao
+
+你我
+你的
+我的
+我的天
+天地	tian di
+好天
+好好地
+目的	mu di
+目的地	mu di di
+ +

Filters

    +
  • simplifier:简繁转换,表情转换等
      +
    • 可加载多个实例,后接 @d转化器名〔如:zh_simp、emoji_suggestion 等〕
    • +
    +
  • +
  • uniquifier:过滤重复的候选字,有可能来自 simplifier
  • +
  • reverse_lookup_filter:反查滤镜,以更灵活的方式反查,Rime1.0 后替代 reverse_lookup_translator
      +
    • 可加载多个实例,后接 @滤镜名〔如:pinyin_lookup,jyutping_lookup 等〕
    • +
    +
  • +
  • charset_filter 字符集过滤
      +
    • 后接 @字符集名〔如:utf-8(无过滤),big5,big5hkscs,gbk,gb2312〕
    • +
    +
  • +
  • lua_filter:使用 lua 自定义过滤,例如过滤字符集,调整排序,后接 @lua函数名
      +
    • lua函数名即用户文件夹内rime.lua中函数名,参数为(input, env)
    • +
    • 可以env.engine.context:get_option(“option_name”)方式绑定到switch开关/key_binder快捷键
    • +
    +
  • +
+

script_translator

+
    +
  • cjk_minifier:字符集过滤,使之支持 extended_charset 开关
  • +
+

table_translator

+
    +
  • single_char_filter:单字过滤器,如加载此组件,则屛敝词典中的词组
  • +
+

使用小鹤双拼

小鹤双拼原始配置文件:double_pinyin_flypy.schema.yaml

+

我的小鹤双拼配置 double_pinyin_flypy.schema.yaml

+

Mac 上如何使用 Squirrel

在 mac 上通常使用 homebrew 的方式安装 app

+
brew cask install squirrel
+ +

如果没有 homebrew,点击这里

+

Squirrel 设置项

Squirrel 配置项基于 0.14.0 整理

+

我的 suirrel 配置 squirrel.custom.yaml

+

特定程序操作 options

可配置项:

+
    +
  • ascii_mode:是否使用英文〔truefalse
  • +
  • vim_modeconfig_version 0.34.0 启用,是否支持使用 ESC 键退出编辑模式并切换为英文〔truefalse
  • +
+

示例:

+
options:
+  com.apple.Xcode:
+    ascii_mode: true
+ +

可视化的自定义皮肤

可视化编辑器

+

Android 上如何使用 Trime

正式版,点击下载

+

测试版,点击下载

+

我的 Trime 同文配置 jiz4oh.trime.yaml

+

基础设置

    +
  1. 启用输入法
    20201004221229

    +
  2. +
  3. 将用户文件夹指向自定义配置文件所在位置
    20201004221441

    +
  4. +
  5. 部署
    20201004221538

    +
  6. +
  7. 使用自定义主题
    20201004222056

    +
  8. +
+

其他设置

telegram-cloud-photo-size-5-6327933625852603060-y

+

Rime 使用心得

重点(必看!)

    +
  1. 很多人修改了方案不生效,或者新安装了 Rime,却发现无法使用,一个很重要但很容易被忽视的解决方案是:

    +

    修改方案后,一定要点击部署!!!
    修改方案后,一定要点击部署!!!
    修改方案后,一定要点击部署!!!

    +

    如何部署?点击这里

    +
      +
    • 【小狼毫】从开始菜单选择「重新部署」;或当开启托盘图标时,在托盘图标上右键选择「重新布署」;
    • +
    • 【鼠须管】在系统语言文字选单中选择「重新布署」;
    • +
    • 【中州韵】点击输入法状态栏(或IBus菜单)上的⟲ (Deploy) 按钮
    • +
    +
  2. +
  3. 修改方案时,强烈建议使用 <输入方案名>.custom.yaml 的方式进行,除非这个方案是你自己原创!

    +
  4. +
  5. 使用 <输入方案名>.custom.yaml 修改方案时,一定要写 patch

    +
  6. +
+

如何切换 Rime 输入方案

比如如果需要更换输入方案-小鹤双拼,那就需要创建 default.custom.yaml 文件,写入以下内容

+
# patch 一定要写
+patch:
+  schema_list:
+    # 默认的明月拼音
+    - schema: luna_pinyin_simp
+    # 额外添加的小鹤双拼,前提是需要存在 double_pinyin_flypy.schema.yaml 文件,不然的话会导致无法输入字符
+    - schema: double_pinyin_flypy
+ +

如何修改 Rime 输入方案的配置

    +
  1. 如果我们对默认的明月拼音方案的一些设置不满意,我们需要创建一个 luna_pinyin_simp.custom.yaml 文件。

    +

    Q:为什么我要用 luna_pinyin_simp.custom.yaml,而不是 luna_pinyin_simp.schema.yaml,我听说 <输入方案名>.schema.yaml 才是输入方案的正确命名。
    A:<输入方案名>.schema.yaml 确实是输入方案的正确命名方式。
    A:我们使用 <输入方案名>.custom.yaml 是因为原始的 <输入方案名>.schema.yaml 方案(比如 luna_pinyin_simp.schema.yaml)是由他人所编写
    A:我们不使用 <输入方案名>.custom.yaml 而是直接修改 <输入方案名>.schema.yaml 文件进行自定义,如果作者对其进行了一些更新,我们会出现两种情况:

    +
      +
    1. 跟随原作者更新了 <输入方案名>.schema.yaml 文件,我们之前所做的自定义全部白费
    2. +
    3. 不跟随更新,失去原作者新增或修复的功能
    4. +
    +
  2. +
  3. luna_pinyin_simp.custom.yaml 中修改

    +
    # 重点!custom.yaml 必须写 patch,且在第一行并只有一个
    +patch:
    +  # 需要覆写的设置项
    +  xxxx: xxxx
    + +
  4. +
+

一个高度定制化的 Rime 配置文件结构示例:

+

20201003122209

+
    +
  • opencc/*OpenCC字形转换配置及字典文件,简繁转换,emoji 转换等
  • +
  • custom_phrase.txt:自定义的词典
  • +
  • default.custom.yaml:自定义的全局设置
  • +
  • double_pinyin_flypy.custom.yaml:自定义的小鹤双拼输入方案设置
  • +
  • double_pinyin_flypy.schema.yaml:从网上下载的小鹤双拼原始输入方案
  • +
  • luna_pinyin_simp.custom.yaml:自定义的明月拼音输入方案设置
  • +
  • pinyin_simp.dict.yaml:网上下载的默认词库文件
  • +
  • squirrel.custom.yaml:自定义的 Squirrel 设置
  • +
  • symbols.yaml:额外的关于表情符号的输入配置
  • +
+

如何删除自造词

    +
  1. 打错字后,立刻删掉是不会录入词库的。Rime 是在有新词输入时才把之前的词录入词库
  2. +
  3. 选中已造词,使用 Shift + Delete 即可删除
  4. +
  5. mbp 因为移除了 Delete 键,使用 Shift + Fn + Backspaces 键删除
  6. +
+

如何同步 Rime 词库

同步原理

    +
  1. 点击同步按钮
  2. +
  3. installation.yaml 中获取
      +
    • installation_id
    • +
    • sync_dir
    • +
    +
  4. +
  5. sync_dir 文件夹下生成 installation_id 文件夹
  6. +
  7. Rime 会将 用户文件夹 下所有文件写入到步骤 3中文件夹
  8. +
  9. 根据 <词典名>.userdb/* 下的词典文件,生成一个词典快照文件 <词典名>.userdb.txt
  10. +
  11. 将快照文件内容与 sync 文件夹下其他文件夹同名快照文件进行对比,更新当前步骤 5中词典文件
  12. +
  13. 更新后的快照文件放入步骤 3中文件夹
  14. +
+

所以重点是设置 installation.yaml 中的 installation_idsync_dir

+

installation_id 默认为随机生成的 UUID
sync_dir 默认为 用户文件夹 下的 sync 文件夹

+

示例:

+
distribution_code_name: Squirrel
+distribution_name: "鼠鬚管"
+distribution_version: 0.14.0
+install_time: "Tue Apr 28 22:33:50 2020"
+rime_version: 1.5.3
+# 上面几项由 Rime 维护
+
+installation_id: "nuc8_mac"
+sync_dir: "/Users/jiz4oh/OneDrive/RimeSync"
+ +

同步进阶

知道原理之后,我们可以通过云盘来同步不同设备的词库及设置

+

比如使用 OneDrive

+
    +
  1. 在 OneDrive 中设置 RimeSync 文件夹
  2. +
  3. 将设备 A 的 sync_dir 指向 OneDrive 下 RimeSync
  4. +
  5. 将设备 B 的 sync_dir 指向 OneDrive 下 RimeSync
  6. +
+

这样就是实现了 A 和 B 的的词库同步

+

参考

注:本文由 Rime 鼠须管【小鹤双拼】输入方案撰写

+
    +
  1. RIME 官网

    +
  2. +
  3. Rime 定製指南

    +
  4. +
  5. Schema.yaml 詳解

    +
  6. +
+]]>
+ + 其他 + + + rime + +
+ + Devise 源码浅析 + /2021/01/ruby-devise/ + 前言

Devise 是 ruby 世界中最常见的 gem 之一,是用于在 web 请求中做身份验证的,他的设计非常精妙,今天我们来尝试看看 devise 是如何设计的。

+ + +

如何使用 Devise

devise 的使用重点是为 rails 的 MVC 三层中各引入 devise 相关的 magic 方法,比如我们今天以 User 模型为例

+
    +
  1. 为 controller 层和 view 层引入 devise,需要在 config/routes.rb 中引入

    +
    devise_for :users
    + +
  2. +
+ + +
    +
  1. 为 model 层引入 devise,需要在 app/models/user.rb 中引入

    +
    devise :database_authenticatable
    + +

    上述我们引入了 database_authenticatable 模块,而 devise 中共有十个模块可由我们按需引入

    +
  2. +
+

devise_for

整个 devise 的核心之一就是 devise_for 方法,如果没有调用这个方法,就不会生成可供我们在 controller 层 和 view 层使用的 helper

+
def devise_for(*resources)
+  ...
+  ...
+
+  resources.each do |resource|
+    mapping = Devise.add_mapping(resource, options)
+
+    begin
+      raise_no_devise_method_error!(mapping.class_name) unless mapping.to.respond_to?(:devise)
+    rescue NameError => e
+      raise unless mapping.class_name == resource.to_s.classify
+      warn "[WARNING] You provided devise_for #{resource.inspect} but there is " \
+        "no model #{mapping.class_name} defined in your application"
+      next
+    rescue NoMethodError => e
+      raise unless e.message.include?("undefined method `devise'")
+      raise_no_devise_method_error!(mapping.class_name)
+    end
+
+    ...
+    ...
+  end
+end
+ +

devise_for 方法在调用之后会对每一个传入的值比如 users 调用 Devise.add_mapping,而在这里就引入一个概念:Devise 中的 mapping
Devise 为了支持多种账户的验证,比如 useradmin 账户都可以登录,在内部使用了 mapping 进行区分,每一种账户对应一个 mapping,也对应了后面会讲的 Warden 中的 scope。我们可以对每一个 mapping 来设置不同的策略,比如允许 user 账户进行注册,不允许 admin 账户进行注册等,这样就解决了同一个系统中不同账户验证逻辑需要写两套的问题

+

Devise.add_mapping

# Small method that adds a mapping to Devise.
+def self.add_mapping(resource, options)
+  mapping = Devise::Mapping.new(resource, options)
+  @@mappings[mapping.name] = mapping
+  @@default_scope ||= mapping.name
+  @@helpers.each { |h| h.define_helpers(mapping) }
+  mapping
+end
+ +

当调用 Devise.add_mapping

+
    +
  1. 会生成一个 Devise::Mapping 对象,这个对象就是上面说的 mapping

    +
  2. +
  3. 在生成之后会将这个对象放入 Devise.mappings 映射中,方便后续的取用

    +
  4. +
  5. 然后将默认验证账户 scope 设置为第一个调用 add_mapping 方法的值

    +
  6. +
  7. mapping 定义帮助方法

    +
    def self.define_helpers(mapping) #:nodoc:
    +  mapping = mapping.name
    +
    +  class_eval <<-METHODS, __FILE__, __LINE__ + 1
    +    def authenticate_#{mapping}!(opts={})
    +      opts[:scope] = :#{mapping}
    +      warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
    +    end
    +
    +    def #{mapping}_signed_in?
    +      !!current_#{mapping}
    +    end
    +
    +    def current_#{mapping}
    +      @current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
    +    end
    +
    +    def #{mapping}_session
    +      current_#{mapping} && warden.session(:#{mapping})
    +    end
    +  METHODS
    +
    +  ActiveSupport.on_load(:action_controller) do
    +    if respond_to?(:helper_method)
    +      helper_method "current_#{mapping}", "#{mapping}_signed_in?", "#{mapping}_session"
    +    end
    +  end
    +end
    + +

    比如 user ,会生成 authenticate_user!user_signed_in?current_useruser_session 四个帮助方法,而 authenticate_user! 就是我们实现验证的重要方法

    +
  8. +
+

authenticate_user!

在我们的实际应用中,当我们需要对某个 controller 增加验证,不登录就无法访问时,我们需要添加 before_action :authenticate_user!,比如:

+
class ApplicationController < ActionController::Base
+  before_action :authenticate_user!
+end
+ +

然后每个请求到来时,就会调用 authenticate_user! 方法对其进行校验

+
# 为了方便,我将源码中元编程生成的代码换成了普通的 ruby 代码
+def authenticate_user!(opts={})
+  opts[:scope] = :user
+  warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
+end
+ +

上述代码可以看出,authenticate_user! 方法实际上是 warden.authenticate! 方法的代理,所以我们要研究 devise 是如何验证的,就需要查看 warden 是什么

+
def warden
+  request.env['warden'] or raise MissingWarden
+end
+
+class MissingWarden < StandardError
+  def initialize
+    super "Devise could not find the `Warden::Proxy` instance on your request environment.\n" + \
+      "Make sure that your application is loading Devise and Warden as expected and that " + \
+      "the `Warden::Manager` middleware is present in your middleware stack.\n" + \
+      "If you are seeing this on one of your tests, ensure that your tests are either " + \
+      "executing the Rails middleware stack or that your tests are using the `Devise::Test::ControllerHelpers` " + \
+      "module to inject the `request.env['warden']` object for you."
+  end
+end
+ +

warden 方法从 requset 的 env 中取出,那这个 warden 是如何被注入的呢,其实 MissingWarden 错误中已经给了我们不少提示,request.env['warden'] 是一个 Warden::Proxy 对象,而这个对象又是由 Warden::Manager 中间件注入

+

rack 和 middleware

几乎所有的 Ruby Web 框架都是一个 rack 的应用,而 rails 就是一个 rack 应用加一堆 middleware 的集合,我们可以通过 rails middleware 来查看

+
$ rails middleware
+use Webpacker::DevServerProxy
+use Raven::Rack
+use Rack::Cors
+use ActionDispatch::HostAuthorization
+use Rack::Sendfile
+use ActionDispatch::Static
+use ActionDispatch::Executor
+use ActiveSupport::Cache::Strategy::LocalCache::Middleware
+use Rack::Runtime
+use Rack::MethodOverride
+use ActionDispatch::RequestId
+use RequestStore::Middleware
+use ActionDispatch::RemoteIp
+use Sprockets::Rails::QuietAssets
+use Rails::Rack::Logger
+use ActionDispatch::ShowExceptions
+use WebConsole::Middleware
+use ActionDispatch::DebugExceptions
+use ActionDispatch::ActionableExceptions
+use ActionDispatch::Reloader
+use ActionDispatch::Callbacks
+use ActiveRecord::Migration::CheckPending
+use ActionDispatch::Cookies
+use ActionDispatch::Session::CookieStore
+use ActionDispatch::Flash
+use ActionDispatch::ContentSecurityPolicy::Middleware
+use Rack::Head
+use Rack::ConditionalGet
+use Rack::ETag
+use Rack::TempfileReaper
+use Warden::Manager
+use ExceptionNotification::Rack
+use Rack::Attack
+run ApplictionName::Application.routes
+ +

use 后面跟随的就是 rails 启动的 middlreware 名称,最后包含的就是 rack ApplictionName::Application.routes,也就是说在 Rails 中所有的请求在经过中间件之后都会先有一个路由表来处理,路由会根据一定的规则将请求交给其他控制器处理。

+

中间件的调用其实类似于柯里化

+
# config.ru
+use MiddleWare1
+use MiddleWare2
+run RackApp
+
+# equals to
+MiddleWare1.new(MiddleWare2.new(RackApp)))
+ +

具体的 middleware 相关就不延展了。而在上面我们可以看到 Warden::Manager 中间件已被启用,这就是 devise 验证的基础

+

Warden

Warden::Manager

module Warden
+  class Manager
+    extend Warden::Hooks
+
+    attr_accessor :config
+
+    def initialize(app, options={})
+      default_strategies = options.delete(:default_strategies)
+
+      @app, @config = app, Warden::Config.new(options)
+      @config.default_strategies(*default_strategies) if default_strategies
+      yield @config if block_given?
+    end
+
+    def call(env) # :nodoc:
+      return @app.call(env) if env['warden'] && env['warden'].manager != self
+
+      env['warden'] = Proxy.new(env, self)
+      result = catch(:warden) do
+        env['warden'].on_request
+        @app.call(env)
+      end
+
+      result ||= {}
+      case result
+      when Array
+        handle_chain_result(result.first, result, env)
+      when Hash
+        process_unauthenticated(env, result)
+      when Rack::Response
+        handle_chain_result(result.status, result, env)
+      end
+    end
+end
+ +

Warden::Manager 中最重要的两个方法就是 initializecall

+
    +
  • initialize:当一个 rack 应用被启用时,会对每一个中间件调用 new 初始化中间件,并传入一个 app 和一个哈希 options
  • +
  • call:当每个请求到来时,会依次调用中间件的 call 方法对请求进行处理,call 方法接受一个参数 env,并在结束时将 env 返回
      +
    • env 是一个三元数组,按照 rack 协议,分别代表 [HTTP 状态码, HTTP Headers, 响应体]
    • +
    +
  • +
+

所以当请求到来时,Warden::Manager#call 方法被调用,env['warden'] 被赋值为 Warden::Proxy 的一个对象,这样在 authenticate_user! 方法中就可以使用 warden.authenticate! 进行验证

+ + +

这里的核心重点是 result = catch(:warden)call 方法会在应用 throw(:warden) 时接管整个后续响应,这时候会根据 result 类型进行处理,通常会进入 process_unauthenticated

+
def process_unauthenticated(env, options={})
+  options[:action] ||= begin
+    opts = config[:scope_defaults][config.default_scope] || {}
+    opts[:action] || 'unauthenticated'
+  end
+
+  proxy  = env['warden']
+  result = options[:result] || proxy.result
+
+  case result
+  when :redirect
+    body = proxy.message || "You are being redirected to #{proxy.headers['Location']}"
+    [proxy.status, proxy.headers, [body]]
+  when :custom
+    proxy.custom_response
+  else
+    options[:message] ||= proxy.message
+    call_failure_app(env, options)
+  end
+end
+ +

根据 options:result 的不同区分处理方式,通常会调用 call_failure_app

+
def call_failure_app(env, options = {})
+  if config.failure_app
+    options.merge!(:attempted_path => ::Rack::Request.new(env).fullpath)
+    env["PATH_INFO"] = "/#{options[:action]}"
+    env["warden.options"] = options
+
+    _run_callbacks(:before_failure, env, options)
+    config.failure_app.call(env).to_a
+  else
+    raise "No Failure App provided"
+  end
+end
+ +

然后调用 config.failure_app.call 进行处理,这个 config.failure_app 通常是 Devise::FailureApp,当然我们也可以自定义失败处理方式

+

Devise::FailureApp 的处理方式根据配置可以是重定向登录页或者返回401响应码及具体的失败信息,具体的实现和源码可以自行研究

+

Warden::Proxy

authenticate!

def authenticate!(*args)
+  user, opts = _perform_authentication(*args)
+  throw(:warden, opts) unless user
+  user
+end
+ +

执行 authenticate! 时,会调用 _perform_authentication 进行验证

+
    +
  1. 如果 user 没有被返回的话就会抛出 throw(:warden) 中断请求的执行,将后续处理移交 Warden:Manager
  2. +
  3. 如果 user 被返回,则通过验证,进行对应的 controller#action
  4. +
+
def _perform_authentication(*args)
+  scope, opts = _retrieve_scope_and_opts(args)
+  user = nil
+
+  return user, opts if user = user(opts.merge(:scope => scope))
+  _run_strategies_for(scope, args)
+
+  if winning_strategy && winning_strategy.successful?
+    opts[:store] = opts.fetch(:store, winning_strategy.store?)
+    set_user(winning_strategy.user, opts.merge!(:event => :authentication))
+  end
+
+  [@users[scope], opts]
+end
+ +

_perform_authentication 方法中

+
    +
  1. 首先会调用 user,这个方法会反序列化 session 然后查看 session 中是否有该 scope 的用户信息

    +
      +
    1. 如果有,则通过验证,进行对应的 controller#action
    2. +
    3. 如果没有,进行第二步
    4. +
    +
  2. +
  3. 然后调用 _run_strategies_for,对预定策略 strategies 依次进行校验

    +
    # Run the strategies for a given scope
    +def _run_strategies_for(scope, args) #:nodoc:
    +  self.winning_strategy = @winning_strategies[scope]
    +  return if winning_strategy && winning_strategy.halted?
    +
    +  # Do not run any strategy if locked
    +  return if @locked
    +
    +  if args.empty?
    +    defaults   = @config[:default_strategies]
    +    strategies = defaults[scope] || defaults[:_all]
    +  end
    +
    +  (strategies || args).each do |name|
    +    strategy = _fetch_strategy(name, scope)
    +    next unless strategy && !strategy.performed? && strategy.valid?
    +
    +    strategy._run!
    +    self.winning_strategy = @winning_strategies[scope] = strategy
    +    break if strategy.halted?
    +  end
    +end
    + +
  4. +
  5. 而这需要获取 scope 对应配置了哪些 strategies,默认查找 Warden::Config 中配置的 default_strategies

    +
    if args.empty?
    +  defaults   = @config[:default_strategies]
    +  strategies = defaults[scope] || defaults[:_all]
    +end
    + +
  6. +
+

devise

前面 讲了在 model 层需要定义 devise 方法,这个方法的目的就是引入模块

+
def devise(*modules)
+  options = modules.extract_options!.dup
+
+  selected_modules = modules.map(&:to_sym).uniq.sort_by do |s|
+    Devise::ALL.index(s) || -1  # follow Devise::ALL order
+  end
+
+  devise_modules_hook! do
+    include Devise::Models::Authenticatable
+
+    selected_modules.each do |m|
+      mod = Devise::Models.const_get(m.to_s.classify)
+
+      if mod.const_defined?("ClassMethods")
+        class_mod = mod.const_get("ClassMethods")
+        extend class_mod
+
+        if class_mod.respond_to?(:available_configs)
+          available_configs = class_mod.available_configs
+          available_configs.each do |config|
+            next unless options.key?(config)
+            send(:"#{config}=", options.delete(config))
+          end
+        end
+      end
+
+      include mod
+    end
+
+    self.devise_modules |= selected_modules
+    options.each { |key, value| send(:"#{key}=", value) }
+  end
+end
+ +

这个方法主要是将各个模块中的定义的实例方法 inclue 到 model 中,将类方法 extend 到 model 中。还有一个重点是为 model 定义了一个 User.devise_modules 方法

+

strategies

module Devise
+  class Mapping
+    def modules
+      @modules ||= to.respond_to?(:devise_modules) ? to.devise_modules : []
+    end
+
+    def strategies
+      @strategies ||= STRATEGIES.values_at(*self.modules).compact.uniq.reverse
+    end
+  end
+end
+ +

每一个 mapping 对象 都会有一个 #strategies 方法,判断 User.devise_modules 是否包含 Devise::STRATEGIES 中的模块

+

当应用启动路由初始化完成后调用会调用 Devise.configure_warden! 方法

+
def self.configure_warden! #:nodoc:
+  @@warden_configured ||= begin
+    warden_config.failure_app   = Devise::Delegator.new
+    warden_config.default_scope = Devise.default_scope
+    warden_config.intercept_401 = false
+
+    Devise.mappings.each_value do |mapping|
+      warden_config.scope_defaults mapping.name, strategies: mapping.strategies
+
+      warden_config.serialize_into_session(mapping.name) do |record|
+        mapping.to.serialize_into_session(record)
+      end
+
+      warden_config.serialize_from_session(mapping.name) do |args|
+        mapping.to.serialize_from_session(*args)
+      end
+    end
+
+    @@warden_config_blocks.map { |block| block.call Devise.warden_config }
+    true
+  end
+end
+ +

这个方法会将每一个 mapping 对象#strategies 定义到对应的 scope_defaults 中。

+
def default_strategies(*strategies)
+  opts  = Hash === strategies.last ? strategies.pop : {}
+  hash  = self[:default_strategies]
+  scope = opts[:scope] || :_all
+
+  hash[scope] = strategies.flatten unless strategies.empty?
+  hash[scope] || hash[:_all] || []
+end
+
+def scope_defaults(scope, opts = {})
+  if strategies = opts.delete(:strategies)
+    default_strategies(strategies, :scope => scope)
+  end
+  ...
+end
+ +

scope_defaults 方法又会将 strategies 最终存储在 default_strategies,故 验证时最终会查找 default_strategies

+

Devise::Strategies

在 Devise 中默认有两个策略

+
    +
  • Devise::Strategies::DatabaseAuthenticatable
  • +
  • Devise::Strategies::Rememberable
  • +
+

DatabaseAuthenticatable

lib/devise/strategies/database_authenticatable.rb

+
def authenticate!
+  resource  = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
+  hashed = false
+
+  if validate(resource){ hashed = true; resource.valid_password?(password) }
+    remember_me(resource)
+    resource.after_database_authentication
+    success!(resource)
+  end
+
+  mapping.to.new.password = password if !hashed && Devise.paranoid
+  unless resource
+    Devise.paranoid ? fail(:invalid) : fail(:not_found_in_database)
+  end
+end
+ +

database_authenticatable 策略首先会从数据库中查找是否有这个用户,然后会校验密码是否正确,中间会用 BCrypt 进行密码的哈希运算比对。

+

Rememberable

lib/devise/strategies/rememberable.rb

+
def authenticate!
+  resource = mapping.to.serialize_from_cookie(*remember_cookie)
+
+  unless resource
+    cookies.delete(remember_key)
+    return pass
+  end
+
+  if validate(resource)
+    remember_me(resource) if extend_remember_me?(resource)
+    resource.after_remembered
+    success!(resource)
+  end
+end
+ +

rememberable 策略会从 cookie 中获取用户的 id 以及 remember_token 和过期时间,判断用户的 token 是否匹配以及是否过期

+

自定义验证策略

我们也可以自定义 strategy

+
    +
  1. strategy 必须继承自 Warden::Strategies::Base,每一个 strategy 必须实现 #authenticate! 方法
  2. +
  3. 调用 Warden::Strategies.add 方法,将 strategy 注册到 Warden 中
  4. +
  5. 在 controller 中调用 before_action :authenticate_user!, [自定义策略名] 进行验证
  6. +
+

总结

devise 通过将自身模块化,实现了功能的解藕,当我们需要使用某些模块时,只需要 devise 导入就行。在验证方面 devise 默认提供两个策略,而这些策略的执行是由 devise 中抽象出来的一个中间件 warden 来处理,又进一步解藕了策略与执行,我们可以随时增添策略,也可以指定是否运行某一个策略,甚至实现自己的 strategy

+

在验证失败时,devise 使用 throw 和 catch 语句跳出后续 controller#action 的执行,直接处理失败响应,这里又一次将失败处理与 devise 解藕,我们可以选择使用默认的 Devise::FailureApp 也可以自己实现一个 FailureApp

+]]>
+ + 后端 + ruby + + + study + +
+ + TCP/IP 五层协议 + /2020/11/tcp-ip/ + 前言

最近在极客时间学习刘超老师的趣谈网络协议,对 TCP/IP 有了更深的了解与感触,记录下学习的心得与体会

+ + +

什么是 TCP/IP 五层协议

TCP/IP 五层协议是在一定程度上参考了 OSI 7层模型 的体系结构,将 OSI 7层模型简化为了五层(也有将链路层和物理层合并后简化为四层的)

+

五层协议:应(应表会)传网数物

+
    +
  • 应用层:HTTP,FTP 等协议
    封装请求正文
  • +
  • 传输层:TCP/UDP 协议
    将端口信息(当前应用监听端口、目标应用监听端口)等封装
  • +
  • 网络层:ICMP,IP 等协议
    封装 IP 地址(当前主机 ip 地址、目标主机 ip 地址)
  • +
  • 数据链路层:以太网等协议
    封装 MAC 地址(当前主机的 MAC 地址、网关的 MAC 地址),通过 ARP 协议在局域网广播找到网关后,通过网关发送出去
  • +
  • 物理层:
    将数据转换为二进制,作为电信号发送出去
  • +
+

五层协议中协议有哪些:

+

2020-09-08-06-50-40

+

物理层

物理层就是网线,中间传输的是光信号

+

两台电脑通过网线直接连接,就可以组成一个最小的 局域网,即 LAN,Local Area Network

+

如果多台电脑需要组成一个局域网,就需要一个设备来控制数据传输的目的地和先后顺序,比如交换机

+

数据链路层

数据链路层包格式:

+

2020-09-12-17-16-00

+

在数据链路层(MAC 层)需要解决 3 个问题:

+
    +
  1. 数据的先后顺序
    MAC 层全称 Medium Access Control,即媒体访问控制。控制如何进行 多路访问
  2. +
  3. 数据的目的地
    这就需要一个物理地址,叫作 链路层地址,也即 MAC 地址
  4. +
  5. 发送错误的处理方式
    对于以太网,第二层的最后面是 CRC,也就是循环冗余检测。通过 XOR异或 的算法,来计算整个包是否在发送的过程中出现了错误
  6. +
+

MAC 地址

MAC 地址更像是身份证,是一个唯一的标识。它的唯一性设计是为了组网的时候,不同的网卡放在一个网络里面的时候,可以不用担心冲突。从硬件角度,保证不同的网卡有不同的标识。

+

MAC 地址的通信范围比较小,局限在一个子网里面。例如,从 192.168.0.2/24 访问 192.168.0.3/24 是可以用 MAC 地址的。一旦跨子网,即从 192.168.0.2/24 到 192.168.1.2/24,MAC 地址就不行了,需要 IP 地址起作用了

+

交换机

交换机是二层转发设备,主要作用就是获取到网络包之后,检查包的目标 MAC 头,再根据策略进行转发,避免主机每次都进行广播发送网络包

+

ARP 协议

当局域网中如果有多台机器,知道目标 IP 地址而不知道 MAC 地址时,就需要使用 ARP 协议来获取。

+
+

已知 IP 地址,求 MAC 地址的协议

+
+

原理是向局域网发出 ARP 协议进行广播,等待目标主机回应

+

具体询问和回答的报文:

+

2020-09-12-17-24-39

+

ARP 表

主机不可能每次都通过 ARP 协议来广播查找 MAC 地址,这样会极大降低效率,所以当主机通过 ARP 协议获取了 MAC 地址之后,就会在本地缓存一个 ARP 表,存储 IP-MAC 的映射。

+

只有当 ARP 表没有对应的 MAC 地址时,主机才会通过 ARP 协议去获取 MAC 地址并更新 ARP 表

+

因为主机的 IP 和 MAC 可能会改变,所以 ARP 表是有过期时间的。

+

RARP 协议

RARP 协议即 Reverse ARP 协议

+
+

已知 MAC 地址,求 IP 地址的协议

+
+

转发表

当源主机发出了数据报文之后,数据报文其实只知道 MAC 地址,而不知道具体路径,就像你要到一个地方去,但是你不知道如何去一样

+

当主机通过网线向连接的设备发送请求时

+
    +
  1. 设备为 hub 集线器:
    hub 集线器是一层设备,只会简单的完全复制,会向所有非源设备广播转发请求,通过其他主机自己来确认数据包是否应该接受
  2. +
  3. 设备为交换机:
    交换机刚开始也不会知道所有主机的 MAC 地址,但是当 主机 A 发送一个请求到达交换机时,交换机会记录下该请求的主机的 MAC 地址,当有其他请求目标地址为主机 A 时,会只转发给主机A,不再进行广播。
    然后过一段时间之后就会记录下局域网下所有主机的 MAC 地址,这个就是 转发表
  4. +
+

因为主机的 MAC 可能会改变,所以转发表是有过期时间的。

+

拓扑结构

当局域网中主机过多,比如办公室场景,可能有几十几百个网口,这时候一个交换机是不够用的,就需要多台交换机,这时候交换机连接起来就是 拓扑结构

+

当交换机过多时,不可避免的会出现 环路问题,例如下图:

+

2020-09-12-17-44-19

+
    +
  1. 当机器 1 发送 ARP 广播寻找机器 2 的 MAC 地址时,交换机 A 和交换机 B 都会收到这个广播,此时,都会记得 机器 1 是在左边这个网口
  2. +
  3. 然后都同时向 LAN2 转发这条广播消息,当交换机 A 收到交换机 B 转发的消息时,会发现是机器 1 在寻找机器 2,这时候就会发现机器 1 位于右边这个网口,于是更新自己的记录,将这个消息转发到 LAN1。
  4. +
  5. 同理,交换机 B 也会更新自己的记录,然后转发消息到 LAN1
  6. +
  7. 然后这个广播请求会来回转发,就会堵塞网络
  8. +
+

这时候需要破除环路

+

STP 协议

STP:Spanning Tree Protocol,是一个生成树的算法,用来破解环路

+

2020-09-12-17-52-00

+
    +
  • Root Bridge,也就是根交换机。这个比较容易理解,可以比喻为“掌门”交换机,是某棵树的老大,是掌门,最大的大哥。

    +
  • +
  • Designated Bridges,有的翻译为指定交换机。这个比较难理解,可以想像成一个“小弟”,对于树来说,就是一棵树的树枝。所谓“指定”的意思是,我拜谁做大哥,其他交换机通过这个交换机到达根交换机,也就相当于拜他做了大哥。这里注意是树枝,不是叶子,因为叶子往往是主机。

    +
  • +
  • Bridge Protocol Data Units (BPDU) ,网桥协议数据单元。可以比喻为“相互比较实力”的协议。行走江湖,比的就是武功,拼的就是实力。当两个交换机碰见的时候,也就是相连的时候,就需要互相比一比内力了。BPDU只有掌门能发,已经隶属于某个掌门的交换机只能传达掌门的指示。

    +
  • +
  • Priority Vector,优先级向量。可以比喻为实力 (值越小越牛)。实力是啥?就是一组ID数目,[Root Bridge ID, Root Path Cost, Bridge ID, and Port ID]。为什么这样设计呢?这是因为要看怎么来比实力。先看Root Bridge ID。拿出老大的ID看看,发现掌门一样,那就是师兄弟;再比Root Path Cost,也即我距离我的老大的距离,也就是拿和掌门关系比,看同一个门派内谁和老大关系铁;最后比Bridge ID,比我自己的ID,拿自己的本事比。

    +
  • +
+

STP的工作过程是怎样的?

+
    +
  1. 一开始,江湖纷争,异常混乱。大家都觉得自己是掌门,谁也不服谁。于是,所有的交换机都认为自己是掌门,每个网桥都被分配了一个ID。这个ID里有管理员分配的优先级,当然网络管理员知道哪些交换机贵,哪些交换机好,就会给它们分配高的优先级。这种交换机生下来武功就很高,起步就是乔峰

    +

    (图中圆圈内数据代表优先级,线上数字代表传输距离,优先级越小越优先,距离越小越优先)

    +

    2020-09-12-17-54-49

    +
  2. +
  3. 既然都是掌门,互相都连着网线,就互相发送 BPDU 来比功夫呗。这一比就发现,有人是岳不群,有人是封不平,赢的接着当掌门,输的就只好做小弟了。当掌门的还会继续发 BPDU,而输的人就没有机会了。它们只有在收到掌门发的 BPDU 的时候,转发一下,表示服从命令。

    +

    2020-09-12-17-55-22

    +

    数字表示优先级。就像这个图,5 和 6 碰见了,6 的优先级低,所以乖乖做小弟。于是一个小门派形成,5 是掌门,6 是小弟。其他诸如 1-7、2-8、3-4 这样的小门派,也诞生了。于是江湖出现了很多小的门派,小的门派,接着合并。

    +

    合并的过程根据传输距离会出现以下四种情形

    +
      +
    1. 情形一:掌门遇到掌门
      当 5 碰到了 1,掌门碰见掌门,1 觉得自己是掌门,5 也刚刚跟别人PK完成为掌门。这俩掌门比较功夫,最终 1 胜出。于是输掉的掌门 5 就会率领所有的小弟归顺。结果就是 1 成为大掌门。

      +

      2020-09-12-17-58-41

      +
    2. +
    3. 情形二:同门相遇
      同门相遇可以是掌门与自己的小弟相遇,这说明存在“环”了。这个小弟已经通过其他门路拜在你门下,结果你还不认识,就 PK 了一把。结果掌门发现这个小弟功夫不错,不应该级别这么低,就把它招到门下亲自带,那这个小弟就相当于升职了。

      +

      我们再来看,假如 1 和 6 相遇。6 原来就拜在 1 的门下,只不过 6 的上司是 5,5 的上司是 1。1 发现,6 距离我才只有 2,比从 5 这里过来的 5(=4+1)近多了,那 6 就直接汇报给我吧。于是,5 和 6 分别汇报给 1。

      +

      2020-09-12-17-59-00

      +
    4. +
    5. 情形三:掌门与其他帮派小弟相遇
      小弟拿本帮掌门和这个掌门比较,赢了,这个掌门拜入门来。输了,会拜入新掌门,并且逐渐拉拢和自己连接的兄弟,一起弃暗投明

      +

      2020-09-12-18-01-23

      +
    6. +
    7. 情形四:不同门小弟相遇
      各自拿掌门比较,输了的拜入赢的门派,并且逐渐将与自己连接的兄弟弃暗投明。

      +

      2020-09-12-18-03-23

      +

      例如,5 和 4 相遇。虽然 4 的武功好于 5,但是 5 的掌门是 1,比 4 牛,于是 4 拜入 5 的门派。后来当 3 和 4 相遇的时候,3 发现 4 已经叛变了,4 说我现在老大是 1,比你牛,要不你也来吧,于是 3 也拜入 1。

      +
    8. +
    +
  4. +
+

VLAN

当交换机过多时,虽然 STP 解决了环路问题,避免了广播风暴,但是在同一个局域网中,总有些数据是想在小黑屋里单独交流的,不希望被人给抓包了。

+

这时候可以有两个解决方案:

+
    +
  1. 物理隔离
    交换机隔离,不在同一个拓扑网络下。
  2. +
  3. 虚拟隔离 VLAN,也就是常说的虚拟局域网
    在同一个交换机上形成多个局域网
  4. +
+

交换机如何区分机器属于哪个局域网呢?通过 VLAN ID

+

2020-09-12-18-10-06

+

VLAN 下的网络结构:

+

2020-09-12-18-11-16

+

网络层

ICMP 协议

ICMP 协议介于网络层和传输层之间,普遍认为属于网络层,ICMP 通常用于 ping 检测网络连通

+

ICMP:Internet Control Message Protocol,就是互联网控制报文协议

+

2020-09-12-18-25-18

+

ICMP 报文有很多的类型,不同的类型有不同的代码。最常用的类型是主动请求为 8,主动请求的应答为 0

+

报文类型,可以大致分为两类:查询报文 和 差错报文

+

查询报文

ping 就是使用查询报文的一种

+
    +
  • ping 的主动请求,被称为 ICMP ECHO REQUEST,类型为 8,
  • +
  • 主动请求的响应,被称为 ICMP ECHO REPLY,类型为 0
  • +
+

比起原生的 ICMP,这里面多了两个字段,一个是标识符,另一个是序号,在选项数据中,ping 还会存放发送请求的时间,来计算响应时间

+

ping 的收发过程:

+

2020-09-12-18-32-03

+
    +
  1. 源主机首先会构建一个 ICMP 请求数据包,ICMP 数据包内包含多个字段。
  2. +
  3. 最重要的是两个,第一个是类型字段,对于请求数据包而言该字段为 8;
  4. +
  5. 另外一个是顺序号,主要用于区分连续ping的时候发出的多个数据包。每发出一个请求数据包,顺序号会自动加 1。
  6. +
  7. 为了能够计算往返时间 RTT,它会在报文的数据部分插入发送时间
  8. +
+

差错报文

差错报文的类型为有:

+
    +
  • 终点不可达,类型为 3
      +
    • 网络不可达:代码为 0,对方局域网无响应
    • +
    • 主机不可达:代码为 1,对方局域网响应了,但是找不到这个 ip 对应的主机
    • +
    • 协议不可达:代码为 2,对方主机响应了,但是对方不接受这个协议
    • +
    • 端口不可达:代码为 3,对方主机响应了,但是没有程序监听这个端口
    • +
    • 需要进行分片但设置了不分片:代码为 4,中间某个网关需要对长度进行限制,但是源主机不允许
    • +
    +
  • +
  • 源站抑制:类型为 4,需要源主机放慢发送速度
  • +
  • 路由重定向:类型为 5,下次发送时发给另一个路由器
  • +
  • 时间超时:类型为 11,超过网络包生命周期还是未到达目标
  • +
+

差错报文的结构相对复杂一些。除了前面还是 IP,ICMP 的前 8 字节不变,后面则跟上出错的那个 IP 包的 IP 头和 IP 正文的前8 个字节

+

Traceroute 使用差错报文:

+
    +
  1. 故意设置特殊的 TTL,来追踪去往目的地时沿途经过的路由器
  2. +
  3. 故意设置不分片,从而确定路径的 MTU
  4. +
+

TTL:Time To Live。最大值为 255。该字段指定IP包被路由器丢弃之前允许通过的最大网段数量,每个路由器要将 TTL 减1,TTL 通常表示被丢弃前经过的路由器的个数。当 TTL 变为 0 时,该路由器丢弃该包,并发送一个 ICMP 包给最初的发送者

+

网关

2020-09-13-10-04-28

+

网关往往是一个路由器,是一个三层(网络层)转发的设备。
当服务器 A 需要访问服务器 B 时:

+
    +
  1. 如果目标 ip 地址是本网段主机,所以直接发给目标主机,封装目标主机的 MAC 地址
  2. +
  3. 如果目标 ip 地址是外网主机,则将请求发给网关,封装网关的 MAC 地址
  4. +
+

当网关收到了一个请求时

+
    +
  1. 取下 MAC 头
      +
    1. 如果 MAC 头不是本主机,则通过转发表将请求转发到本网段其他主机上
    2. +
    3. 如果 MAC 头是本主机,则取下 IP 头,查看目标 IP 地址
        +
      1. 如果是本主机,则由本主机程序处理
      2. +
      3. 如果不是本主机,则查看路由表,查询应该由哪个 LAN 口处理,并封装好下一跳(可能是对应主机,也可能是网关)的 MAC 地址,这时包的 目的 MAC 地址 变为了路由规则匹配的下一跳的 MAC 地址源 MAC 地址 变为了网关的 MAC 地址
      4. +
      +
    4. +
    +
  2. +
+

网关分为两种类型:

+
    +
  • 转发网关:不修改目标 IP 地址和源 IP 地址的网关
  • +
  • NAT(Network Address Translation) 网关:修改目标 IP 地址和源 IP 地址的网关
  • +
+

和路由器的关系

很多情况下,人们把网关就叫作路由器。其实不完全准确,而另一种比喻更加恰当:路由器是一台设备,它有五个 LAN 网口或者网卡,相当于有五只手,分别连着五个局域网。每只手的 IP 地址都和局域网的 IP 地址相同的网段,每只手都是它握住的那个局域网的网关

+

静态路由

当网关收到一个外网的请求时,就会通过 静态路由动态路由 规则修改 MAC 头(和 IP 头),将包发出去

+

静态路由,其实就是在路由器上,写死配置一条一条规则。
这些规则包括:想访问 BBS 站(它肯定有个网段),从 2 号口出去,下一跳是 IP2;想访问教学视频站(它也有个自己的网段),从 3 号口出去,下一跳是 IP3,然后保存在路由器里

+

动态路由

网络环境复杂多变时,静态路由手动修改太麻烦,所以需要动态路由通过一定算法进行自动配置

+

动态路由算法:

+
    +
  • 链路状态路由(link state routing),基于 Dijkstra 算法
  • +
  • 距离矢量路由(distance vector routing),基于 Bellman-Ford 算法,只适用于小型网络
  • +
+

动态路由协议:

+
    +
  • 基于链路状态路由算法的 OSPF(Open Shortest Path First,开放式最短路径优先),广泛应用在数据中心中的协议。由于主要用在数据中心内部,用于路由决策,因而称为内部网关协议(Interior Gateway Protocol,简称 IGP)
  • +
  • 基于距离矢量路由算法的 BGP(Border Gateway Protocol,边界路由协议)
      +
    • eBGP:在多个 AS(独立的内部网络,如家庭网络) 之间进行路由交换的协议
    • +
    • iBGP:在多个 edge touter(AS 面向外界的出口路由器) 之间进行路由交换的协议
    • +
    +
  • +
+

传输层

UDP 协议

UDP 协议:数据报协议,不需要接收方确认消息。
特点:面向无连接,无状态,不保证不丢失,不保证到达顺序

+
    +
  • 优点是速度快
  • +
  • 缺点是可能会丢包
  • +
+

2020-09-13-18-26-14

+

应用场景:

+
    +
  • 流媒体,比如直播
  • +
  • 实时游戏
  • +
  • loT物联网
  • +
  • 移动通信
  • +
  • 需要低时延的 http 访问
  • +
+

TCP 协议

TCP 协议:流式 stream 协议,通过双向管道传输数据,发送请求之后必须收到确认消息,安全但效率稍低
特点:面向连接,有状态,保证无差错,无重复,并按序到达

+
    +
  • 优点是稳定可靠
  • +
  • 缺点是速度较慢,需要维护状态机
  • +
+

2020-09-13-18-39-47

+
    +
  • 建立连接:
    TCP三次握手:

    +
      +
    1. client 发送管道建立请求(syn)给 server。
    2. +
    3. server 同意建立 client 到 server 管道(ack),及管道建立请求(syn)给 client。
    4. +
    5. client 同意建立 server 到 client 管道给 server(ack)。TCP 双向管道连接建立成功
    6. +
    +

    2020-09-13-19-12-26

    +
  • +
  • 断开连接:
    TCP四次挥手:

    +
      +
    1. client 发送数据发送完毕并请求断开管道(FIN)给 server。
    2. +
    3. server 同意断开管道(ack),client 单向断开管道。
    4. +
    5. 数据接收完毕后,server 发送数据接收完毕并请求断开管道(FIN)给 client。
    6. +
    7. client 同意断开管道(ack),server 单向断开管道。TCP 双向管道连接断开
    8. +
    +

    2020-09-13-19-56-04

    +
  • +
+

握手和挥手整体流程状态图:

+

20200913222940

+
    +
  • 加粗的实线是客户端 A 的状态变迁,是主要流程
    其中阿拉伯数字的序号,是握手过程中的顺序
    而大写中文数字的序号,是挥手过程中的顺序
  • +
  • 加粗的虚线是服务端 B 的状态变迁
  • +
  • 点线是非主要流程
  • +
+

TCP 状态机

为了实现 TCP 协议的可靠性,TCP 协议规定两端必须实现一个状态机,对每一个发出包与接受包进行记录

+

TCP 发送方状态机的数据结构:

+
    +
  • 第一部分:发送了并且已经确认的。
  • +
  • 第二部分:发送了并且尚未确认的。
  • +
  • 第三部分:没有发送,但是已经等待发送的。
  • +
  • 第四部分:没有发送,并且暂时还不会发送的。
    第三部分和第四部分的区别是,接收方会返回给发送方一个窗口大小,表示接收方处理数据的能力大小,叫 Advertised window,超过这个窗口大小的包,发送方会暂缓发送
  • +
+

20200914080034

+
    +
  • LastByteAcked:第一部分和第二部分的分界线
  • +
  • LastByteSent:第二部分和第三部分的分界线
  • +
  • LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线
  • +
+

TCP 接收方状态机的数据结构:

+
    +
  • 第一部分:接受并且确认过的。
  • +
  • 第二部分:还没接收,但是马上就能接收的。
  • +
  • 第三部分:还没接收,也没法接收的。
  • +
+

20200914075917

+
    +
  • LastByteRead:之后是已经接收了,但是还没被应用层读取的;
  • +
  • NextByteExpected:是第一部分和第二部分的分界线。
  • +
  • MaxRcvBuffer:最大缓存的量;
  • +
+

累计应答

为了保证顺序性,发送方的包都有一个序号,并且按照序号挨个发送。对于接收方,应答却不是挨个应答,会应答一个序号,表示这个序号之前的包都已收到。

+

确认与重发机制

因为底层 IP 协议的不可靠性,无法保证网络包的不丢失与按序到达,TCP 协议自己实现了确认与重发机制来保证顺序问题和丢包问题

+
    +
  1. 超时重试:对于每一个发送了,但是没有 ACK 的包,都设有一个超时时间,超过时间则重新尝试。
      +
    • 超时时间:是 TCP 通过采样 RTT 的时间,然后进行加权平均算出的,这个值是不断变化的。这个算法被称为自适应重传算法
      当一个数据包再次超时时,TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送
    • +
    • 快速重传:当接收方收到一个序号大于下一个所期望的序号时,就检测到了数据流中的间隔,于是发送三个冗余的 ACK,发送方收到后就会在超时时间之前提前重传
      比如,接收方收到了 6、8、9,发现 7 没来,于是发送三个 6 的 ACK。发送方收到 3 个 ACK 后,就会立刻重传 7 的报文
    • +
    +
  2. +
  3. Selective Acknowledgment(SACK):这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了
  4. +
+

流量控制

    +
  1. 在每一个包的确认中,都会返回一个 AdvertisedWindow(rwnd) 大小。
  2. +
  3. 当接受端处理信息过慢时,这个窗口会一直缩减直到 0,发送方就会停止发送。
  4. +
  5. 这时,发送方会定时发送窗口探测数据包,看窗口是否有空余
  6. +
  7. 接收方为了避免低能窗口综合征,会在窗口达到一定大小,或者缓冲区一半时才会通知发送方窗口有空余了
  8. +
+

拥塞控制

TCP 的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽

+

拥塞也是通过窗口来控制,这个窗口被称为 cwnd (Congestion Window),有一个公式 LastByteSent - LastByteAcked <= min {cwnd, rwnd} 共同来控制带宽

+

TCP 发送接受过程

+

如果一个设备如上图所示,只能同时处理 8 个包,如果再调大窗口,就会出现丢包,不想丢包的话就得在中间设备增加缓存,这样就会增加时延

+

TCP 的拥塞控制就是来避免出现上述问题

+

BBR 算法

20200916211957

+

TCP 的拥塞控制有两个问题:

+
    +
  1. 丢包不代表通道已满,有可能是管子本身就漏水。例如公网本身就有丢包问题
  2. +
  3. TCP 的拥塞控制会等中间设备的缓存都填充满之后才降速
  4. +
+

BBR 拥塞算法:企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡

+

Socket 接口

因为 TCP/IP 协议过于复杂,如果程序员每开发一个程序都需要自己处理报文的发送与接受等问题,效率太低了,于是由操作系统封装了一套接口,便于快速处理数据包的发送与接受

+

基于 TCP 协议的 Socket

函数调用过程:

+

20200916230544

+
    +
  • 服务端调用 socket 流程:

    +
      +
    1. 调用 bind 监听 ip 和 端口
    2. +
    3. 调用 listen 进入监听状态(监听 socket)
    4. +
    5. 调用 accept,当 TCP 连接成功时,accpet 返回另一个 socket(已连接 socket)进行处理
    6. +
    7. 和客户端进行通信
    8. +
    +

    重点:监听的 socket 和通信使用的 socket 不相同,一个连接对应一个 socket

    +
  • +
  • 客户端调用 socket 流程:

    +
      +
    1. 调用 connect 连接 ip 和 端口
    2. +
    3. 和服务端进行通信
    4. +
    +
  • +
+

socket 在内核中读写过程:

+

20200916231334

+

基于 UDP 协议的 Socket

函数调用过程:

+

20200916231723

+

UDP 无连接,所以只需要一个 socket 就可以和多个客户端通信,每次通信的时候都会调用 sendto 和 recvfrom,都可以传入 IP 地址和端口

+

应用处理 scoket 连接方式

单体应用可以承载的 socket 连接数是有限的,如何在资源有限的情况下,尽可能多的处理连接

+
    +
  1. 多进程
  2. +
  3. 多线程
  4. +
  5. IO 复用(select):一个线程通过 select 轮询的方式检查 socket
  6. +
  7. IO 复用(epoll):采用事件通知的方式
  8. +
+

应用层

DHCP 协议

DHCP:Dynamic Host Configuration Protocol,动态主机配置协议

+

因为网络中的每一个主机都需要 ip,而在复杂公共环境下,不可能让管理员来手动给每一个设备分配 ip,所以出现了 DHCP。

+

DHCP 工作方式:

+
    +
  1. 新主机使用 0.0.0.0 向 255.255.255.255 发送 DHCP Discover 广播
  2. +
  3. DHCP Server 监听到这个广播,判断如果是新主机的话,通过 DHCP Offer 方式将 ip 池里的随机 ip 发送过去(也是广播形式)
  4. +
  5. 新主机发送 DHCP Request 广播数据包,包中包含客户端的 MAC 地址、接受的租约中的 IP 地址、提供此租约的 DHCP 服务器地址等
  6. +
  7. DHCP Server 收到 DHCP Request 后,会返回 DHCP ACK 消息包,表明新主机已被加入网络
  8. +
  9. 租约达成后,广播通知其他 DHCP Server
  10. +
+

HTTP 协议

目前大部分的 HTTP 协议是 1.1,默认开启了 Keep-Alive 的,实现了连接复用

+

HTTP 请求的报文格式:

+

20200917075642
请求报文

+
    +
  1. 请求行
    HTTP 请求常用的请求方法:

    +
      +
    1. get
    2. +
    3. post
    4. +
    5. put
    6. +
    7. patch
    8. +
    9. delete
    10. +
    +
  2. +
  3. 首部
    常用的 request headers:

    +
      +
    1. Accept-Type
    2. +
    3. Content-Type
    4. +
    5. Cache-control
    6. +
    7. Last-Modified | If-Modified-Since
    8. +
    9. Etag | If-None-Match
    10. +
    +
  4. +
  5. 正文实体

    +
  6. +
+

一个完整的 HTTP 请求:

+

一个完整的 HTTP 请求

+

HTTP 2.0

HTTP 2.0 为了尝试解决 HTTP 1.1 的实时性、并发性等问题,进行了以下操作:

+
    +
  1. 对头部进行压缩:将每次都携带的 k-v 结构建立索引,每次只发送索引
  2. +
  3. 将一个连接切分成多个流
  4. +
  5. 将所有传输信息分割为更小的帧:header 帧,data 帧
  6. +
+

HTTP 2.0 解决了

+
    +
  1. HTTP 1.1 的队首阻塞问题,同时,也不需要通过 HTTP 1.x 的 pipeline 机制用多条TCP连接来实现并行请求与响应;
  2. +
  3. 减少了 TCP 连接数对服务器性能的影响,同时将页面的多个数据 css、js、 jpg 等通过一个数据链接进行传输,能够加快页面组件的传输速度
  4. +
+

QUIC 协议

因为 HTTP 协议底层是基于 TCP 协议的,严重依赖于包的串行处理,所以 google 出了一个基于 UDP 的 QUIC 协议,尝试解决 HTTP 的一些问题

+

QUIC 的机制:

+
    +
  1. 自定义连接机制
    TCP 基于 [源 IP,端口,目的 IP,目的端口] 的四元数组进行连接握手,如果四元数组进行了 wifi 切换等,就会导致重连。
    QUIC 以一个 64 位的随机数作为 ID 标识,只要 ID 不变,不需要重新连接
  2. +
  3. 自定义重传机制
  4. +
  5. 无阻塞的多路复用
  6. +
  7. 自定义流量控制
  8. +
+

HTTPS 协议

HTTP 协议在网络中由于是明文信息,容易遭到恶意份子的篡改,所以有了 TLS 协议对其进行加密,保证内容安全。
HTTP + TLS = HTTPS

+

TLS 加密原理:首先使用非对称加密协商出公钥,然后再使用这个公钥对内容进行对称加密

+

20200920011230

+
    +
  1. 对称加密:公钥机制,由公钥加密的内容也能由公钥解开
  2. +
  3. 非对称加密:公私钥机制,只有对应的私钥才能解开公钥加密的内容。服务端公钥的正确性由 CA 证书来保证
  4. +
+

DNS 协议

在正常的用户上网过程中,用户是不可能能记住每一个网站的 ip 地址的,而且 ip 地址经常会因为 NAT、负载均衡等问题发生改变,这时候我们需要一个方便记忆 ip 的方法,DNS 协议就是当用户使用某个域名(比如 baidu.com)时,获取域名对应的 ip 地址的协议

+
+

已知域名,求 IP 的协议

+
+

DNS 服务器结构:

+

20200920161444

+

DNS 递归查询过程:

+

20200920224427

+

DNS 负载均衡:

+

20200920225045

+

HTTPDNS 协议

传统 DNS 存在以下问题:

+
    +
  1. 域名缓存:缓存是为了降低递归查询的次数,提高查询效率。
    但是带来的问题是
      +
    1. 服务器地址更改后没有及时刷新缓存,导致访问失败
    2. +
    3. 客户端地址更改后导致全局负载均衡失败,缓存的并不是最优服务器地址
    4. +
    +
  2. +
  3. 域名转发:权威域名服务器的负载均衡会根据运营商的不同返回不同的服务器地址,但是域名查询由 A 运营商转发给 B 运营商查询,就会导致运营商判断错误
  4. +
  5. 出口 NAT:NAT 会导致权威域名服务器判断运营商错误
  6. +
  7. 域名更新:IP 更新后重新解析 DNS 会导致一定时间的延迟乃至服务不可用
  8. +
  9. 解析延迟:DNS 查询需要递归遍历多个 DNS 服务器,延迟较高
  10. +
+

HTTPDNS 就是不走传统的 DNS 解析,而是应用自己搭建基于 HTTP 协议的 DNS 服务器集群,分布在多个地点和多个运营商。当客户端需要 DNS 解析的时候,直接通过 HTTP 协议进行请求这个服务器集群,得到就近的地址

+

20200920230522

+

参考

趣谈网络协议

+

网络协议知识图谱

+

OSI7层协议图谱

+

socket

+]]>
+ + 网络 + + + think + study + +
+
diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..c944c277 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,435 @@ + + + + + http://jiz4oh.com/categories/index.html + + 2023-08-10 + + + + + http://jiz4oh.com/tags/index.html + + 2023-08-10 + + + + + http://jiz4oh.com/2023/04/build-own-rss-platform/ + + 2023-08-10 + + + + + http://jiz4oh.com/404/index.html + + 2023-08-10 + + + + + http://jiz4oh.com/2023/07/zero-rating/ + + 2023-07-27 + + + + + http://jiz4oh.com/2023/01/cloudflare-tunnels/ + + 2023-04-12 + + + + + http://jiz4oh.com/2023/01/run-efb-in-docker/ + + 2023-01-16 + + + + + http://jiz4oh.com/2021/01/ruby-devise/ + + 2021-02-23 + + + + + http://jiz4oh.com/2021/02/openwrt-in-pve/ + + 2021-02-06 + + + + + http://jiz4oh.com/2020/12/cloudflare-workers/ + + 2021-01-20 + + + + + http://jiz4oh.com/2020/11/receiver-and-ancestors/ + + 2021-01-18 + + + + + http://jiz4oh.com/2020/11/http-cache/ + + 2020-11-22 + + + + + http://jiz4oh.com/2020/11/hexo-next-valine/ + + 2020-11-21 + + + + + http://jiz4oh.com/2020/10/webpack-resovle-@-path/ + + 2020-11-20 + + + + + http://jiz4oh.com/2020/11/tcp-ip/ + + 2020-11-08 + + + + + http://jiz4oh.com/2020/10/vim-fast-esc/ + + 2020-11-05 + + + + + http://jiz4oh.com/2020/10/how-to-use-rime/ + + 2020-10-30 + + + + + http://jiz4oh.com/2020/10/ruby-in-vscode/ + + 2020-10-28 + + + + + http://jiz4oh.com/2020/10/anti-csrf-on-separation-of-frontend-and-backend/ + + 2020-10-19 + + + + + http://jiz4oh.com/2020/10/ruby-include-implementation/ + + 2020-10-12 + + + + + http://jiz4oh.com/2020/10/chrome-cookie-do-not-delete/ + + 2020-10-10 + + + + + http://jiz4oh.com/2020/10/charles/ + + 2020-10-09 + + + + + http://jiz4oh.com/2020/10/install-pyenv-and-pipenv/ + + 2020-10-01 + + + + + http://jiz4oh.com/2020/09/openwrt-expand-overlay-storage/ + + 2020-09-27 + + + + + http://jiz4oh.com/2020/09/install-hackintosh-to-nuc8i5beh/ + + 2020-09-27 + + + + + http://jiz4oh.com/2020/09/github-picgo-jsdelivr/ + + 2020-09-26 + + + + + http://jiz4oh.com/2020/09/how-to-install-ehforwarderbot-v2/ + + 2020-09-26 + + + + + http://jiz4oh.com/2020/09/how-to-install-ehforwarderbot-v1/ + + 2020-09-26 + + + + + http://jiz4oh.com/2020/09/hexo-next/ + + 2020-09-25 + + + + + http://jiz4oh.com/2020/09/hello-hexo/ + + 2020-09-23 + + + + + + http://jiz4oh.com/ + 2023-08-10 + daily + 1.0 + + + + + http://jiz4oh.com/tags/vscode/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/think/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/csrf/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/rss/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/self-hosted/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/chrome/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/cloudflare/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/freebie/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/github/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/freebie/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/efb/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/ehforwarderbot/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/study/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/mac/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/hackintosh/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/development-environment/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/openwrt/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/pve/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/vim/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/rime/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/tags/webpack/ + 2023-08-10 + daily + 0.6 + + + + + + http://jiz4oh.com/categories/backend/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/categories/security/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/categories/misc/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/categories/test/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/categories/network/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/categories/backend/python/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/categories/router/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/categories/backend/ruby/ + 2023-08-10 + daily + 0.6 + + + + http://jiz4oh.com/categories/frotend/ + 2023-08-10 + daily + 0.6 + + + diff --git a/tags/chrome/index.html b/tags/chrome/index.html new file mode 100644 index 00000000..604ee749 --- /dev/null +++ b/tags/chrome/index.html @@ -0,0 +1 @@ +标签: chrome | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/cloudflare/index.html b/tags/cloudflare/index.html new file mode 100644 index 00000000..e3d1a6ae --- /dev/null +++ b/tags/cloudflare/index.html @@ -0,0 +1 @@ +标签: cloudflare | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/csrf/index.html b/tags/csrf/index.html new file mode 100644 index 00000000..81eed9eb --- /dev/null +++ b/tags/csrf/index.html @@ -0,0 +1 @@ +标签: csrf | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/development-environment/index.html b/tags/development-environment/index.html new file mode 100644 index 00000000..1d60f9c2 --- /dev/null +++ b/tags/development-environment/index.html @@ -0,0 +1 @@ +标签: 开发环境 | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/efb/index.html b/tags/efb/index.html new file mode 100644 index 00000000..5c96a903 --- /dev/null +++ b/tags/efb/index.html @@ -0,0 +1 @@ +标签: efb | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/ehforwarderbot/index.html b/tags/ehforwarderbot/index.html new file mode 100644 index 00000000..df8ca67c --- /dev/null +++ b/tags/ehforwarderbot/index.html @@ -0,0 +1 @@ +标签: ehforwarderbot | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/freebie/index.html b/tags/freebie/index.html new file mode 100644 index 00000000..f06931bf --- /dev/null +++ b/tags/freebie/index.html @@ -0,0 +1 @@ +标签: 白嫖 | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/github/index.html b/tags/github/index.html new file mode 100644 index 00000000..25f031bf --- /dev/null +++ b/tags/github/index.html @@ -0,0 +1 @@ +标签: github | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/hackintosh/index.html b/tags/hackintosh/index.html new file mode 100644 index 00000000..0d9b12c6 --- /dev/null +++ b/tags/hackintosh/index.html @@ -0,0 +1 @@ +标签: 黑苹果 | Jiz4oh's Life

黑苹果 标签

2020
\ No newline at end of file diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 00000000..06eade9c --- /dev/null +++ b/tags/index.html @@ -0,0 +1 @@ +tags | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/mac/index.html b/tags/mac/index.html new file mode 100644 index 00000000..c4e5dbf8 --- /dev/null +++ b/tags/mac/index.html @@ -0,0 +1 @@ +标签: mac | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/openwrt/index.html b/tags/openwrt/index.html new file mode 100644 index 00000000..07085ed9 --- /dev/null +++ b/tags/openwrt/index.html @@ -0,0 +1 @@ +标签: openwrt | Jiz4oh's Life

openwrt 标签

2021
2020
\ No newline at end of file diff --git a/tags/pve/index.html b/tags/pve/index.html new file mode 100644 index 00000000..ea925340 --- /dev/null +++ b/tags/pve/index.html @@ -0,0 +1 @@ +标签: pve | Jiz4oh's Life

pve 标签

2021
\ No newline at end of file diff --git a/tags/rime/index.html b/tags/rime/index.html new file mode 100644 index 00000000..ee530711 --- /dev/null +++ b/tags/rime/index.html @@ -0,0 +1 @@ +标签: rime | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/rss/index.html b/tags/rss/index.html new file mode 100644 index 00000000..87052b06 --- /dev/null +++ b/tags/rss/index.html @@ -0,0 +1 @@ +标签: rss | Jiz4oh's Life

rss 标签

2023
\ No newline at end of file diff --git a/tags/self-hosted/index.html b/tags/self-hosted/index.html new file mode 100644 index 00000000..7614742a --- /dev/null +++ b/tags/self-hosted/index.html @@ -0,0 +1 @@ +标签: 自建 | Jiz4oh's Life

自建 标签

2023
\ No newline at end of file diff --git a/tags/study/index.html b/tags/study/index.html new file mode 100644 index 00000000..4e769445 --- /dev/null +++ b/tags/study/index.html @@ -0,0 +1 @@ +标签: study | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/think/index.html b/tags/think/index.html new file mode 100644 index 00000000..18f351b4 --- /dev/null +++ b/tags/think/index.html @@ -0,0 +1 @@ +标签: think | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/vim/index.html b/tags/vim/index.html new file mode 100644 index 00000000..4ac8a99e --- /dev/null +++ b/tags/vim/index.html @@ -0,0 +1 @@ +标签: vim | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/vscode/index.html b/tags/vscode/index.html new file mode 100644 index 00000000..56ede206 --- /dev/null +++ b/tags/vscode/index.html @@ -0,0 +1 @@ +标签: vscode | Jiz4oh's Life
\ No newline at end of file diff --git a/tags/webpack/index.html b/tags/webpack/index.html new file mode 100644 index 00000000..2fa79efb --- /dev/null +++ b/tags/webpack/index.html @@ -0,0 +1 @@ +标签: webpack | Jiz4oh's Life

webpack 标签

2020
\ No newline at end of file