阅读视图

发现新文章,点击刷新页面。
🔲 ⭐

如何创建一个打印友好型的网页

如何创建一个打印友好型的网页

在某些情况下,我们会遇到需要将网页打印出来的需求。但是,直接打印网页的效果往往不尽如人意,因为网页的排版和打印的排版是不同的。本文将介绍如何创建一个在打印时具有出色的质量和可读性的网页。

前置知识:@media print 媒体查询

经常编写 CSS 的读者应该对 @media 媒体查询是比较熟悉的了。这个语句在创建响应式网页时是非常有用的,经常被大家用来调整不同屏幕宽度的设备间的样式。而 @media print 媒体查询则是专门用来调整打印时的样式的。

@media print 媒体查询的语法如下:

@media print {
  /* 在这里定义打印时应用的样式 */
  body {
    font-size: 12pt;
  }

  .header,
  .footer {
    display: none;
  }

  /* 更多样式规则... */
}

这些样式只会在打印时应用,而不会在屏幕上显示。了解了 @media print 媒体查询的基本语法后,我们就可以开始创建打印友好型的网页了。

优化内容和布局

隐藏不必要的页面元素、样式

在打印时,页面上的一些与正文无关的元素需要被隐藏掉。

比如在《二分图学习笔记》页面中(本文在后续部分中将会一直以本页面作为示例),顶部的导航栏以及右侧的侧边栏与正文信息并没有什么关联,因此可以在打印输出中隐去。

确保信息的整齐和清晰可读性

▲ IT 之家某篇文章的打印版截图。

从这张截图中可以看出这个页面似乎并没有对打印机进行适配,并且侧边栏还遮挡到了正文中的文字。不过由于笔者并没有找到更好的遮挡示例,因此只能给出这么一个有点勉强的例子 —— 侧边栏按照上一节中的建议是应该要被隐藏掉的。

对于这种情况,需要在设计、编写页面布局的时候下功夫,以避免遮挡到正文。

除了文字被遮挡的问题,截图下部的超链接在纸质媒介上显然是不能被点击的。

以此处的超链接为例,可以通过特殊处理来在纸上显示出链接的实际指向 URL:

@media print {
  a:not([href^='#'])::after {
    content: ' (' attr(href) ')';
    font-size: 80%;
    color: var(--color-fg-muted);
  }
}

效果如图:

除此之外,如果需要,还要对字体及其大小进行一些调整。

笔者认为在相当一部分情况下,使用衬线字体在打印后的观感要比使用非衬线字体时好很多。(PS:在此对通篇使用微软雅黑出试卷的老师表示强烈谴责)

多媒体内容的处理

有时网页上会包含一些音频、视频等多媒体内容,这些内容在纸质媒介上与超链接类似,无法与读者交互。

此时可以考虑提供一些替代文本来对其内容进行描述,并提供指向相关资源的链接、二维码等辅助工具来帮助读者获取多媒体资源中的信息。

编写适合打印的样式

单位制

在网络世界中,我们的常用单位诸如像素(px)、百分比(%)、相对大小(em、rem) 等。然而在现实世界中,我们常用的单位则为物理单位,如厘米(cm)、点(pt)等。这导致了在打印输出时需要额外注意单位制相关的问题。虽然现代浏览器对这些问题的处理已经比较优秀了,但在部分情况下仍然会导致页面排版布局出现错乱。

CSS 优先级

经常写 CSS 的读者应该对 CSS 中的样式优先级不陌生了。笔者建议在编写 CSS 时将打印相关样式置于靠下的位置以免产生冲突,同时也可以适当地使用 !important 来强制覆盖一些样式。

测试和调整

可以使用 DevTools 来模拟打印环境进行调试(按 Ctrl + Shift + P 组合键唤出菜单;对于中文版浏览器,请搜索「打印」关键字)。

演示

感兴趣的读者可以访问 oi.baoshuo.ren/bi-graph 并尝试打印该页面。

▲ 原网页

▲ 打印效果(预览)

🔲 ⭐

再见,2022 —— 我的 2022 年度总结

再见,2022 —— 我的 2022 年度总结

又一年过去了。由于学业繁忙,这一年中发生的能写出来公之于众的事情并没有多少,但做一些微小的记录总是值得的,所以就有了这篇年度总结。

大事记

竞赛生涯的续命

先向大家报告一个好消息,我在 CSP-S 2022 中取得了省一等奖(省排第 30),这么多年的竞赛算是没有白学。

不过,按照正常的进度,到 NOIP 2022 结束之时也就是我的退役之日了,毕竟河北省只有 15 个省队。

由于疫情影响,河北省取消了 NOIP 2022,并将在 2023 年 3 月举办春季赛,以此作为省选成绩的参考,这也就意味着我可以继续冲刺省选了(虽然进省队的希望不大,但仍然可以一试)。

但是,现在也是时候考虑如何补习文化课的事情了。

网课的日子

在得知 NOIP 2022 取消后,我们便返回家中,开始了上网课的日子。

跟班上网课貌似不太现实,所以只好跟着竞赛一起上网课。竞赛这边没有早读,早上上课时间比较晚,可以多睡会。

在网课期间既要补习文化课,又要兼顾竞赛的进度,实属一个难题。

疫情的终结

此部分内容没有文字描述。关于我感染之后的情况记录,可以查看《我的新冠阳性日记

一些零碎

我的 IPv4 地址段

感谢炮总相助,今年 3 月我终于有了一段属于自己的 IPv4 地址段 —— 174.136.239.0/24。不过因为线路问题,这段地址并没有开展大规模应用。

关于 AS141776 的更多信息,请访问 baoshuo.ren/network

新的个人主页

基于 Vite + React + Primer Design 的新个人主页上线了!

新的个人主页主要分为了个人简介、项目介绍、友情链接、项目单页等几个板块,并可以方便地在后期增删页面及其内容。

请访问 baoshuo.ren 了解更多信息。

OIerDb NG 正式上线

又经过了半年多的开发,OIerDb NG 正式上线了。

详细介绍可以查看文章《OIerDb NG —— 新一代的 OIerDb》,在此不作过多叙述。

附上今年最后一个季度的访问量数据,平均日访客数也能保持在 900 人左右。

欢迎体验:oier.baoshuo.dev

加入 Hexo Core Team

今年上半年折腾了折腾自己的 OI 博客,在折腾的过程中顺手给 Hexo 发了一些 PR,并在 Sukka 大佬的引荐下加入了 Hexo Core Team。

S2OJ v3.0

今年下半年正式接手了学校的 在线测评系统,并进行了 一些大改(当心过大的 diff 导致浏览器卡死)。

▲ 旧版界面

▲ 新版界面

除了界面更新之外,还增加了许多新功能,并修复了一些问题。在开发的过程中,也向上游 UOJ 官网版、UOJ 社区版发送了一些 Pull Request,算是为后人栽树了。

GitHub 上的贡献

又是碌碌无为的一年呢!

后记

2022 年就这样在疫情阴霾退散的过程中结束了。希望在走出这一困难之后,2023 年能够是一个更加美好的年份,让我们一起期待着明天的希望,共同迎接更加美好的未来。

🔲 ⭐

我的新冠阳性日记

一直以为「感染新冠」这件事离我很远,像我这种居家上网课的人也不太可能与外面的病毒产生什么联系。直到 12 月 13 日午休之后,我居然发烧了,拿出抗原一测,发现自己阳了。

至前几日,笔者已经基本康复,于是决定写下这篇文章,记录下这一个多星期的别样的体验。

Day 1(12 月 13 日)

上午正常上课。

午饭后开始感觉略有不适,开始发烧(38.5℃ 左右),遂请假休息。下午烧到 39℃ 后服用一包布洛芬,体温略有下降,但并未退烧。抗原测试呈弱阳性。

半夜继续高烧(39.4℃),服用一包布洛芬,体温略有下降,但仍未退烧。

Day 2(12 月 14 日)

全天请假休息。

上午高烧(39.5℃),服用一包布洛芬后于午饭前退烧。

下午重新开始低烧(37.5℃),一直烧到晚上睡觉。

Day 3(12 月 15 日)

继续全天请假休息。

早上感觉身体已经适应了高烧的状态,没有前几日那么蔫,但体温仍在 39.5℃ 附近徘徊。

下午排便后转为低烧,并未服用退烧药。

Day 4(12 月 16 日)

正常上课。

不再发烧,有轻微咳嗽和流鼻涕的症状。

Day 5(12 月 17 日)

正常上课。

咳嗽和流鼻涕的症状加重,但没有太大影响。

Day 6(12 月 18 日)

正常上课。

咳嗽和流鼻涕导致头昏脑胀。晚上因咳嗽久久无法入眠。

Day 7(12 月 19 日)

正常上课。症状与前一天相似。

Day 8(12 月 20 日)

正常上课。症状开始转轻。

Day 9(12 月 21 日)

正常上课。抗原测试基本转阴。仍有些许咳嗽。

Day 10(12 月 22 日)

症状基本消失,食欲恢复。

后记

人民日报发布的「新冠发病 7 日典型症状过程」还是比较准确的,值得参考。

由于我去年在家中准备了一盒布洛芬颗粒,因此并没有陷入到「一药难求」的境地。从发病到痊愈,只消耗了半盒布洛芬颗粒(即 5 包)和两盒连花清瘟胶囊(48 颗),因此无需囤积药品,够用就好。

最后,希望各位读者在疫情期间保护好自己,也祝大家身体健康,百毒不侵!

🔲 ⭐

OIerDb NG —— 新一代的 OIerDb

OIerDb NG —— 新一代的 OIerDb

笔者有幸能与 OI 巨神虞皓翔等人合作来共同参与 OIerDb NG 的开发,经过数个月的不断改进,目前项目已经初具雏形,特此写下本篇文章对其进行简要介绍。

笔者主要参与了前端用户界面的开发工作,而数据处理部分则主要由虞皓翔完成。

OIerDb NG 的新特性

纯前端处理 —— 效率更高、可离线使用

相比于老版 OIerDb,OIerDb NG 摈弃了传统的「客户端发送查询请求 -> 服务器响应查询请求」的模式,而是采用了纯前端的处理方式 —— 将数据库存储在浏览器的 indexedDB 中,这样在用户查询时无需向服务器发送请求,直接在浏览器端即可处理。这样带来的好处是显而易见的 —— 我们拥有了更快的查询响应速度,同时也减轻了对服务器的压力。至于缺点嘛… 在首次访问网页的时候会下载几 MB 的数据,从统计数据来看,这个过程在大部分情况下最多需要消耗 5 秒左右的时间。

在下载好所有页面的代码、数据加载完成后,即使断开网络连接也能正常使用 OIerDb NG 的基础功能 ,可以在断网打模拟赛 AK 之后找点东西看

查询更灵活 —— 满足用户的不同需求

老版的 OIerDb 只能应对两种类型的查询 —— 以选手或者学校为中心的查询。而新版的 OIerDb 在设计之初就希望具备应对更灵活的查询请求的能力,比如「查询『NOIP 2021』中『河北省』的获奖情况」(如上图所示),更进一步的话还可以「查询『NOIP 2021』中位于『河北省』的『石家庄市第二中学』的获奖情况」(不过这种查询目前还没有在用户界面中实现)。

开发回忆录

在 2021 年 12 月初的一天中午,笔者在 OIerDb 的页面底部发现了 nocriz 的《呼吁广大选手积极参与开发下一代 OIerDb》文章,恰巧笔者在课余时间学习了一些在现在看来非常浅薄的前端技术,于是跃跃欲试地在 12 月 12 号的那天创建了一个新的 GitHub 仓库,并使用模板来提交了 第一个 commit

接下来的几天,笔者利用自己的空余时间来编写代码,终于在 12 月 19 日完成了第一版的 OIerDb NG,并部署到了 Netlify 上。

▲ OIerDb NG 的第一版界面。

当时笔者初学 React,许多知识仍有待学习,再加之笔者忙于完成学业,因此开发进度异常缓慢,网站的功能也有很多欠缺。

第一版完成后没多久,笔者找到了精通 React 的好友 Menci 来帮忙 review 代码。在这个过程中,Menci 提出了许多富有建设性的意见,同时对项目整体进行了一番调整,使其更加现代化、工程化。笔者也从中学到了很多知识。

之后笔者边实践边学习,还从 LibreOJ 的前端中抄来了一些代码,比如手机端的导航栏。

慢慢地,OIerDb NG 上线了 nocriz 的文章中提到的大部分功能(点击图片可以前往对应页面):

▲ 基础 / 高级搜索

▲ 搜索页选手信息卡片

▲ 地区信息学奥林匹克竞赛选手 / 学校排名

▲ 学校 / 比赛详情页面

OIerDb NG 的不足之处

尽管 OIerDb NG 有了一个还算可以的开始,但仍然存在诸多不足之处。

例如,对于网络速度较慢的用户,加载数 MB 的数据可能仍需要十几秒甚至数十秒。并且,即使是一些小更新也需要重新从服务器拉取全量数据,对用户与服务器的流量都是一种浪费。

再比如一些用户可能需要指向性更强的查询条件,目前还没有找到一个比较好的办法来添加到用户界面中。

除了这些之外,还有一些其他的问题存在。这些问题由于团队内的各位开发者都在现实生活中有着自己的工作、学习任务,无法去逐一解决。笔者希望广大对信息学竞赛感兴趣的朋友们能或多或少地参与进 OIerDb NG 的开发,共同为信息学竞赛社区做出贡献。

后记

感谢 nocriz 建立的 OIerDb 网站,为国内的信息学竞赛社区做出了巨大贡献。

也感谢 yhx-12243Menci 参与 OIerDb NG 的开发,完成了许多工作。

最后的最后,给 OIerDb-ng/OIerDb 求一波 Star~

🔲 ⭐

使用 GitHub Actions 自动申请与部署 SSL 证书

使用 GitHub Actions 自动申请与部署 SSL 证书

对于一个有很多服务器的人来说,在不同服务器上同步 SSL 证书是一件麻烦事。笔者尝试过很多种方式,最后在 Menci 的推荐下选定了使用 GitHub Actions 来自动申请、续期 SSL 证书,并自动推送到各个服务器上。

本博客的证书也是使用这种方式进行签发、部署的,可以点击浏览器地址栏上的按钮查看证书。

申请证书

前期准备

首先请在本地(或自己的服务器上)成功使用 acme.shDNS-01 验证方式成功申请一次证书,如果不会操作的话可以参考 烧饼博客的教程 来进行。这个过程包括:

  1. 向 CA 注册 ACME 账户(如果使用 Let’s Encrypt 则会自动进行,详细步骤请参阅 acme.sh 的 Wiki)。
  2. 通过环境变量指定 DNS 提供商的凭据,用于添加/删除 ACME DNS-01 认证所需的 TXT 记录。
  3. 确认证书申请可以成功,为后续调试排除可能的问题。

第一次申请证书后,CA 的 ACME 账户凭据将被存储到 ~/.acme.sh/ca 中,DNS 提供商的凭据将被存储到 ~/.acme.sh/account.conf 中。将它们打包并使用 Base64 编码存储,以备在 GitHub Actions 中使用:

cd ~/.acme.sh
tar cz ca account.conf | base64 -w0

将输出内容添加到 GitHub 仓库的 Secrets 中。注意不要复制输出中的多余信息。

自动化

如果没有特殊需求,可以使用 Menci/acme 来简单地申请证书:

# 全局环境变量
env:
  # Checkout 到的目录
  CERTS_OUTPUT_BASE: certs
  # 证书输出目录
  CERTS_OUTPUT_DIRECTORY: example.com
  # 证书文件名
  FILE_FULLCHAIN: fullchain.pem
  # 私钥文件名
  FILE_KEY: privatekey.key

jobs:
  issue-ssl-certificate:
    name: Issue SSL certificate
    runs-on: ubuntu-latest
    steps:
      - uses: Menci/acme@v2
        with:
          # 指定 acme.sh 的版本
          version: 3.0.2

          # 上方保存的以 Base64 编码存储的凭据
          account-tar: ${{ secrets.ACME_SH_ACCOUNT_TAR }}

          # 域名列表,以空格分隔
          domains: example.com example.net example.org example.edu
          # 是否申请通配符
          append-wildcard: true

          # 传递给 acme.sh 的额外参数
          arguments: --dns dns_cf --challenge-alias example.com

          # 导出的证书路径
          output-fullchain: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
          output-key: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}

如果需要高度自定义 acme.sh 的参数,比如为不同的域名设置不同的 DNS 提供商,可以使用下面的方式手动编写命令来执行:

# 全局环境变量
env:
  # Checkout 到的目录
  CERTS_OUTPUT_BASE: certs
  # 证书输出目录
  CERTS_OUTPUT_DIRECTORY: example.com
  # 证书文件名
  FILE_FULLCHAIN: fullchain.pem
  # 私钥文件名
  FILE_KEY: privatekey.key

jobs:
  issue-ssl-certificate:
    name: Issue SSL certificate
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          ref: master

      - name: Checkout output branch
        uses: actions/checkout@v2
        with:
          ref: certs
          path: ${{ env.CERTS_OUTPUT_BASE }}

      # 安装 acme.sh
      - name: Install acme.sh
        shell: bash
        run: curl -s https://get.acme.sh | sh

      # 解压 acme.sh 配置信息
      - name: Extract account files for acme.sh
        shell: bash
        run: |
          echo "$ACME_SH_ACCOUNT_TAR" | base64 -d | tar -C ~/.acme.sh -xz
        env:
          # Base64 编码的 acme.sh 配置信息
          ACME_SH_ACCOUNT_TAR: ${{ secrets.ACME_SH_ACCOUNT_TAR }}

      # 申请证书
      - name: Issue SSL certificates
        shell: bash
        run: |
          ~/.acme.sh/acme.sh --issue        \
            -d "example.com"   --dns dns_cf \
            -d "*.example.com" --dns dns_cf \
            -d "example.net"   --dns dns_dp \
            -d "*.example.net" --dns dns_dp \
            --server letsencrypt

      # 导出证书
      - name: Copy certificate to output paths
        shell: bash
        run: |
          ACME_SH_TEMP_DIR="$(mktemp -d)"
          ACME_SH_TEMP_FILE_FULLCHAIN="$ACME_SH_TEMP_DIR/fullchain.pem"
          ACME_SH_TEMP_FILE_KEY="$ACME_SH_TEMP_DIR/key.pem"

          ~/.acme.sh/acme.sh --install-cert -d "$ACME_SH_FIRST_DOMAIN" --fullchain-file "$ACME_SH_TEMP_FILE_FULLCHAIN" --key-file "$ACME_SH_TEMP_FILE_KEY"

          [[ -z "$ACME_SH_OUTPUT_FULLCHAIN" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_FULLCHAIN")" && cp "$ACME_SH_TEMP_FILE_FULLCHAIN" "$ACME_SH_OUTPUT_FULLCHAIN")
          [[ -z "$ACME_SH_OUTPUT_KEY" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_KEY")" && cp "$ACME_SH_TEMP_FILE_KEY" "$ACME_SH_OUTPUT_KEY")

          rm -rf "$ACME_SH_TEMP_DIR"
        env:
          # 修改此处的 example.com 为申请时填写的第一个域名
          ACME_SH_FIRST_DOMAIN: example.com
          ACME_SH_OUTPUT_FULLCHAIN: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
          ACME_SH_OUTPUT_KEY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}

上传证书至仓库

# 上传证书
- name: Push to GitHub
  run: |
    git config --global user.name "BaoshuoBot"
    git config --global user.email "79077260+BaoshuoBot@users.noreply.github.com"

    cd "$CERTS_DIRECTORY"

    git add "$FILE_FULLCHAIN" "$FILE_KEY"
    git commit -m "Upload certificates on $(date '+%Y-%m-%d %H:%M:%S')"
    git push
  env:
    TZ: Asia/Shanghai
    CERTS_DIRECTORY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}

部署证书

在申请证书的 Job 执行完成后,可以执行一系列其他的 Job 来将证书部署到各个服务器或云服务。

服务器

可以使用 easingthemes/ssh-deploy 来使用 rsync 将证书同步到服务器上。同步完成后再使用 appleboy/ssh-action 远程执行命令重载 Nginx / Apache。

# 部署到服务器
deploy-to-server:
  name: Deploy Certificate to Server
  runs-on: ubuntu-latest
  needs: issue-ssl-certificate

  strategy:
    matrix:
      host:
        - 174.136.239.1 # Server 1
        - 174.136.239.2 # Server 2
        # ...
        - 174.136.239.254 # Server N

  steps:
    - name: Checkout
      uses: actions/checkout@v2
      with:
        ref: certs

    # 上传证书
    - name: Upload certificate to server
      uses: easingthemes/ssh-deploy@v2.1.5
      env:
        SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        ARGS: '-avz --delete'
        REMOTE_HOST: ${{ matrix.host }}
        REMOTE_USER: ${{ secrets.REMOTE_USER }}
        SOURCE: ${{ env.CERTS_OUTPUT_DIRECTORY }}/
        TARGET: /path/to/ssl/certs/${{ env.CERTS_OUTPUT_DIRECTORY }}/

    # 重载 Nginx
    - name: Force-reload nginx
      uses: appleboy/ssh-action@v0.1.4
      with:
        host: ${{ matrix.host }}
        username: ${{ secrets.REMOTE_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          sudo /opt/hooks/reload-nginx.sh

需要注意的是,重载 Nginx / Apache 的命令需要 root 权限才能执行,可以采用只允许部署用户以 root 权限执行重载脚本的方式来避免出现安全问题。

/opt/hooks 目录下新建一个文件 reload-nginx.sh,内容如下:

#!/bin/bash
sudo systemctl force-reload nginx

然后新建一个名为 actions-cert 的用户,然后在 /etc/sudoers 文件中添加以下内容:

actions-cert ALL=(ALL) NOPASSWD: /opt/hooks/reload-nginx.sh

这个配置可以使 actions-cert 用户免密码以 root 用户的权限执行 /opt/hooks/reload-nginx.sh

最后使用 chmod 755 /opt/hooks/reload-nginx.sh 命令将 reload-nginx.sh 文件设置为可执行,同时禁止非所有者对其进行写入操作。

如果服务器位于 NAT 后,或者禁止了 SSH 连接,还有两个方法可以将证书部署到内网服务器上:

  1. 将证书先部署到有部署条件的服务器上,然后再在内网服务器上使用 rsync 从部署好的服务器上拉取证书。
  2. 将证书上传到 Azure Key Vault 等托管服务中,再在服务器上按照 Menci 的文章 中的教程拉取即可。

阿里云

阿里云的 SSL 证书服务 支持上传自定义证书,该证书可以用于 阿里云 CDN。阿里云暂未提供将证书部署至 OSS 的 API,建议 OSS 用户使用 CDN 回源 OSS 来代替。

使用 Menci/deploy-certificate-to-aliyun 将证书部署到阿里云:

# 部署到阿里云
deploy-to-aliyun:
  name: Deploy Certificate to Aliyun
  runs-on: ubuntu-latest
  needs: issue-ssl-certificate

  steps:
    # 拉取证书存储分支
    - name: Checkout
      uses: actions/checkout@v2
      with:
        ref: certs

    # 上传证书
    - name: Deploy certificate to aliyun
      uses: Menci/deploy-certificate-to-aliyun@beta-v1
      with:
        access-key-id: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
        access-key-secret: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
        fullchain-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
        key-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
        certificate-name: example.com
        cdn-domains: |
          example.com
          example.net

其中 certificate-name 指定上传的证书在证书服务中的名称(将自动替换旧版本),cdn-domains 指定需要将该证书部署到的 CDN 域名列表(用空白字符隔开)。

建议使用子账户 Access Key,为其赋予以下权限(并按需使用资源组隔离):

  • AliyunYundunCertFullAccess
  • AliyunCDNFullAccess
  • AliyunPCDNFullAccess
  • AliyunSCDNFullAccess
  • AliyunDCDNFullAccess

腾讯云

使用 renbaoshuo/deploy-certificate-to-tencentcloud 将证书部署至腾讯云 CDN:

deploy-to-qcloud-cdn:
  name: Deploy certificate to Tencent Cloud CDN
  runs-on: ubuntu-latest
  needs: issue-ssl-certificate

  steps:
    - name: Check out
      uses: actions/checkout@v2
      with:
        # If you just commited and pushed your newly issued certificate to this repo in a previous job,
        # use `ref` to make sure checking out the newest commit in this job
        ref: ${{ github.ref }}

    - uses: renbaoshuo/deploy-certificate-to-tencentcloud@v1
      with:
        # Use Access Key
        secret-id: ${{ secrets.QCLOUD_SECRET_ID }}
        secret-key: ${{ secrets.QCLOUD_SECRET_KEY }}

        # Specify PEM fullchain file
        fullchain-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
        # Specify PEM private key file
        key-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}

        # Deploy to CDN
        cdn-domains: |
          cdn1.example.com
          cdn2.example.com

其中 cdn-domains 指定需要将该证书部署到的 CDN 域名列表(用空白字符隔开)。

建议使用子账户 API 密钥,为其赋予以下权限(并按需使用资源组隔离):

  • QcloudCDNFullAccess

自建 GoEdge CDN

使用 renbaoshuo/deploy-certificate-to-goedge 将证书部署至自建的 GoEdge CDN:

deploy-to-goedge-cdn:
  name: Deploy certificate to GoEdge CDN
  runs-on: ubuntu-latest
  steps:
    - name: Check out
      uses: actions/checkout@v2
      with:
        # If you just commited and pushed your newly issued certificate to this repo in a previous job,
        # use `ref` to make sure checking out the newest commit in this job
        ref: ${{ github.ref }}
    - uses: renbaoshuo/deploy-certificate-to-goedge@beta-v1
      with:
        # GoEdge API endpoint
        api-endpoint: https://cdn.api.baoshuo.dev

        # Use Access Key
        access-key-type: user
        access-key-id: ${{ secrets.GOEDGE_ACCESS_KEY_ID }}
        access-key: ${{ secrets.GOEDGE_ACCESS_KEY }}

        # GoEdge certificate ID
        cert-id: ${{ secrets.GOEDGE_CERT_ID }}

        # Specify PEM fullchain file
        fullchain-file: ${{ env.FILE_FULLCHAIN }}
        # Specify PEM private key file
        key-file: ${{ env.FILE_KEY }}

注:在部署前需要手动上传一次证书以便获取证书 ID。证书 ID 可以在「证书文件下载」处的 URL 参数中找到。

完整例子

这个 Action 完成了以下操作:

  1. 申请证书,并上传到仓库的 certs 分支。
  2. 在申请证书后将 certs 分支中的证书部署到服务器上。
# 名称
name: Issue SSL Certificates

# 触发条件
on:
  # 手动运行
  workflow_dispatch:
  # 定时运行
  schedule:
    # 每两个月运行一次
    - cron: '0 0 1 */2 *'

# 全局环境变量
env:
  # Checkout 到的目录
  CERTS_OUTPUT_BASE: certs
  # 证书输出目录
  CERTS_OUTPUT_DIRECTORY: example.com
  # 证书文件名
  FILE_FULLCHAIN: fullchain.pem
  # 私钥文件名
  FILE_KEY: privatekey.key

jobs:
  issue-ssl-certificate:
    # 申请证书并 push 到 certs 分支
    name: Issue SSL certificate
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          ref: master

      - name: Checkout output branch
        uses: actions/checkout@v2
        with:
          ref: certs
          path: ${{ env.CERTS_OUTPUT_BASE }}

      # 安装 acme.sh
      - name: Install acme.sh
        shell: bash
        run: curl -s https://get.acme.sh | sh

      # 解压 acme.sh 配置信息
      - name: Extract account files for acme.sh
        shell: bash
        run: |
          echo "$ACME_SH_ACCOUNT_TAR" | base64 -d | tar -C ~/.acme.sh -xz
        env:
          # Base64 编码的 acme.sh 配置信息
          ACME_SH_ACCOUNT_TAR: ${{ secrets.ACME_SH_ACCOUNT_TAR }}

      # 申请证书
      - name: Issue SSL certificates
        shell: bash
        run: |
          ~/.acme.sh/acme.sh --issue            \
            -d "example.com" -d "*.example.com" \
            --dns dns_cf --server letsencrypt

      # 导出证书
      - name: Copy certificate to output paths
        shell: bash
        run: |
          ACME_SH_TEMP_DIR="$(mktemp -d)"
          ACME_SH_TEMP_FILE_FULLCHAIN="$ACME_SH_TEMP_DIR/fullchain.pem"
          ACME_SH_TEMP_FILE_KEY="$ACME_SH_TEMP_DIR/key.pem"

          # 不要忘记修改这里的 -d 参数值为上方的第一个域名
          ~/.acme.sh/acme.sh --install-cert -d "example.com" --fullchain-file "$ACME_SH_TEMP_FILE_FULLCHAIN" --key-file "$ACME_SH_TEMP_FILE_KEY"

          [[ -z "$ACME_SH_OUTPUT_FULLCHAIN" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_FULLCHAIN")" && cp "$ACME_SH_TEMP_FILE_FULLCHAIN" "$ACME_SH_OUTPUT_FULLCHAIN")
          [[ -z "$ACME_SH_OUTPUT_KEY" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_KEY")" && cp "$ACME_SH_TEMP_FILE_KEY" "$ACME_SH_OUTPUT_KEY")

          rm -rf "$ACME_SH_TEMP_DIR"
        env:
          ACME_SH_OUTPUT_FULLCHAIN: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
          ACME_SH_OUTPUT_KEY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}

      # 上传证书
      - name: Push to GitHub
        run: |
          git config --global user.name "BaoshuoBot"
          git config --global user.email "79077260+BaoshuoBot@users.noreply.github.com"

          cd "$CERTS_DIRECTORY"

          git add "$FILE_FULLCHAIN" "$FILE_KEY"
          git commit -m "Upload certificates on $(date '+%Y-%m-%d %H:%M:%S')"
          git push
        env:
          TZ: Asia/Shanghai
          CERTS_DIRECTORY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}

  # 部署证书到服务器
  deploy-to-server:
    name: Deploy Certificate to Server
    runs-on: ubuntu-latest
    needs: issue-ssl-certificate

    strategy:
      matrix:
        host:
          - 174.136.239.1 # Server 1
          - 174.136.239.2 # Server 2
          # ...
          - 174.136.239.254 # Server N

    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          ref: certs

      # 上传证书
      - name: Upload certificate to server
        uses: easingthemes/ssh-deploy@v2.1.5
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          ARGS: '-avz --delete'
          REMOTE_HOST: ${{ matrix.host }}
          REMOTE_USER: ${{ secrets.REMOTE_USER }}
          SOURCE: ${{ env.CERTS_OUTPUT_DIRECTORY }}/
          TARGET: /path/to/ssl/certs/${{ env.CERTS_OUTPUT_DIRECTORY }}/

      # 重载 Nginx
      - name: Force-reload nginx
        uses: appleboy/ssh-action@v0.1.4
        with:
          host: ${{ matrix.host }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            sudo /opt/hooks/reload-nginx.sh

杂项

部分情况下,GitHub Actions 中的 GITHUB_TOKEN 只有 Read repository contents permission,而本文中的 Actions 要求这个 Token 具有 Read and write permissions,那么需要在仓库的 Settings > Actions > General 页面的底部赋予其写入权限,如图所示:

设置好后点击 Save 按钮即可。

参考资料

  1. 使用 GitHub Actions 自动申请与部署 ACME SSL 证书,Menci,2022 年 5 月 11 日。对原文章内容的使用已经过作者同意。
  2. 使用 acme.sh 配置自动续签 SSL 证书,烧饼博客,2022 年 2 月 3 日。

文章头图由 Menci 制作,使用已经过授权,在此表示感谢。

☑️ ⭐

USTC Hackergame 2021 Write Up

签到

点击 Next 键,发现页面的 URL 后多了个 ?page=1 ,结合第一个页面中的 1970-01-01 字样,可以判断出来 page 参数应为比赛期间的 Unix 时间戳。

示例:http://202.38.93.111:10000/?page=1635002186

进制十六——参上

可以照着 16 进制数据搞一搞,然后 flag 就出来了:

去吧!追寻自由的电波

下载音频之后使用 Adobe Audition 进行变速即可。

旅行照片

从图片中可以看出拍摄者在 14 层,并且楼下有一个蓝色的肯德基,那么使用 Google 搜索关键词 海洋 蓝色 KFC 可以得到以下结果:

从照片的描述中可以得到这家肯德基位于秦皇岛新澳海底世界。

在百度地图上可以找到这家肯德基的电话、详细位置。同时按照卫星图可以推断出拍摄者所在的方向,进而推断出拍摄的大致时间。

flag 获取成功。

FLAG 助力大红包

查看点击助力按钮后的浏览器请求可以发现请求时有一个名为 IP 的参数,尝试修改这个参数发现会报错提示前后端检测 IP 不一致,那么考虑添加 X-Forwarded-For 头伪造经过代理的来源 IP 地址即可。

比赛平台的速率限制为每秒最多请求一次,所以在每次请求后还需要等待 1 秒。

for ((i=0; $i <= 255; i = ($i + 1))); do
    curl "http://202.38.93.111:10888/invite/$invite_id" -H "X-Forwarded-For: $i.11.45.14" -d "ip=$i.11.45.14"
    sleep 1
done

猫咪问答 Pro Max

  1. 2017 年,中科大信息安全俱乐部(SEC@USTC)并入中科大 Linux 用户协会(USTCLUG)。目前,信息安全俱乐部的域名(sec.ustc.edu.cn)已经无法访问,但你能找到信息安全俱乐部的社团章程在哪一天的会员代表大会上通过的吗?

Wayback Machine 是个好东西啊。(页面存档

  1. 中国科学技术大学 Linux 用户协会在近五年多少次被评为校五星级社团?

LUG 官网上直接搜就出来了。但实际上的答案应该是 5 ,可能是官网没更新最新信息。

  1. 中国科学技术大学 Linux 用户协会位于西区图书馆的活动室门口的牌子上“LUG @ USTC”下方的小字是?

谷歌是你的好朋友。 Hackergame 2020 「猫咪问答++」 flag 。

可以看到正确答案为 Development Team of Library

  1. 在 SIGBOVIK 2021 的一篇关于二进制 Newcomb-Benford 定律的论文中,作者一共展示了多少个数据集对其理论结果进行验证?

可以在 the record of the proceedings of SIGBOVIK 2021页面存档)的 212 页找到这篇论文。

  1. 不严格遵循协议规范的操作着实令人生厌,好在 IETF 于 2021 年成立了 Protocol Police 以监督并惩戒所有违背 RFC 文档的行为个体。假如你发现了某位同学可能违反了协议规范,根据 Protocol Police 相关文档中规定的举报方法,你应该将你的举报信发往何处?

搜索关键词:IETF Protocol Police

可以搜到这个「搞笑 RFC」:Establishing the Protocol Police ,在第 6 节中有相关介绍。

正确答案应为 /dev/null

卖瓜

最开始拿到题我先想的是能不能用负数凑,结果发现不行,于是考虑溢出。

试了试发现使用 6 斤瓜无法触发溢出,而使用 9 斤瓜的就可以触发溢出了。

写了个脚本跑一跑,试出来了几个负数,挨个试了下发现放 2e18 个 9 斤瓜可以凑到 20 斤。

然后在计算器里算了一下,只需要加 6 斤的瓜和 9 斤的瓜各 29782938247303441 个就能让称的显示变成 -1 。

接下来放 2 个 6 斤瓜和 1 个 9 斤瓜就能拿到 flag 了。

透明的文件

本题与 ANSI Escape Code 有关。

首先需要将文件中的 [ 替换成 \033[ ,然后再找一个支持显示 ANSI 控制码的终端输出。

然后发现一片空白,啥也没有。

捣鼓到快怀疑人生才发现终端上的某些字符被遮挡了,进而想到这个脚本可能清除了终端上某些地方的字符来显示 flag 。

先编写一个复读函数用来填满终端:

repeat() {
    for ((i = 1; $i <= $1; i = ($i + 1))); do
        echo -n "▉"
    done
}

再配合上方替换好的文件输出即可,效果如图。

Amnesia

轻度失忆

使用 putchar() 函数即可解决此问题。

#include <stdio.h>

int main() {
    putchar('H');
    putchar('e');
    putchar('l');
    putchar('l');
    putchar('o');
    putchar(',');
    putchar(' ');
    putchar('w');
    putchar('o');
    putchar('r');
    putchar('l');
    putchar('d');
    putchar('!');
    putchar('\n');
    return 0;
}

图之上的信息

可以使用 __schema 字段查询所有存在的类型:

{
  __schema {
    types {
      name
    }
  }
}

发现一个名为 GUser 的类型,再构造一个语句查询类型结构:

{
  __type(name: "GUser") {
    name
    fields {
      name
      type {
        name
        kind
        ofType {
          name
          kind
        }
      }
    }
  }
}

顺便获取了下 GNote 类型的结构:

进行查询即可得到 flag :

后记

今年拿的名次比去年的高,感觉在这一年里自己的 web 水平有很大的提升,但 math 还是一如既往地爆了零,和我的数学中考成绩一样的烂。

以后如果有时间的话逆向、汇编什么的也都要学一学,不然的话每次一看见 binary 就有点不知所措、无从下手属实不太好。

推荐阅读:USTC Hackergame 2020 Write Up

❌