日々のあれこれφ(..)

もっぱら壁打ち

【一万円選書リレー】1冊目:エンド・オブ・ライフ

普段読んだ本はブクログに記録をつけていて感想は書いたり書かなかったりその都度ですが、一万円選書で選んでもらった本の感想はせっかくなので書いていこうと思いました。

1冊目は佐々 涼子 著『エンド・オブ・ライフ』です。

本の紹介

「死ぬ前に家族と潮干狩りに行きたい…」患者の最期の望みを献身的に叶えていく医師と看護師たち。最期を迎える人と、そこに寄り添う人たちの姿を通して、終末期のあり方を考えるノンフィクション。

内容紹介にそう書いてあったことから、医療関係者から見た人の最期の様子が収録されたドキュメンタリーの短編集を想像していたのですが、実際にはそれだけではありませんでした。

  • 渡辺西賀茂診療所の取り組み
  • そこに勤める訪問看護師:森山
  • 在宅医療で最期を迎えた筆者の母

まずこの本は京都で訪問医療を行う渡辺西賀茂診療所とそこで働く人々に焦点が当てられています。診療所では患者の最後の希望を叶えるというボランティアが行われていました。例えば患者に同行して県を跨いだ浜に潮干狩りに行ったり、ディズニーランドに同行したり、家で演奏会を催して人を集めて過ごしたり、中には生きたままの土壌が食べたいと無理難題をぶつけられて街中へ探しに行くといった具合です。もちろんただ一緒にくっ付いて遊んでいるはずがなく、患者の症状を見て酸素ボンベや車椅子や薬などあらゆるケースに想定した準備を行い、患者とその家族が楽しい時間を過ごす影でライブ本番中の裏方スタッフよろしく徹底的にサポートしています。そしてそんな診療所の取り組みをノンフィクションライターとして取材していたのが筆者でした。

そんな本のプロローグでは、そこで訪問看護師をする森山という男性が自身の癌に気付くところから始まります。上記のような話の合間に死に向かう森山が何を考えどのように過ごして来たのか、友人として共著を持ちかけられた仕事仲間として一緒に過ごしてきた筆者が見てきた彼の生き様が全編を通して少しずつ挿入されています。

そしてもう一つ、筆者の母親についても全編の中で少しずつ描かれていました。筆者の母親は難病を患い自分では体を全く動かすことができずに寝たきりの生活を送っていましたが、入院ではなく夫に世話をしてもらいながら在宅医療を続け最期を迎えた一人でした。この話は唯一、渡辺西賀茂診療所とは関係のないところで起きていたものでした。

遠いところ、近いところ、幸せな面、大変な面、様々な距離・角度から人の最期と在宅医療のリアルを描いている本で、当初は道徳の教科書に載っている短編を読むくらいのラフな気持ちで本を開いていた私は想像以上の深さと濃さに衝撃を受け、読み進めるのにそれなりの覚悟を必要としたのでした。

感想

私は身近な人の死に立ち会ったことがほとんどありませんでした。そのため私にとって死は遠いところにあり、できればそのまま遠いところにあり続けてくれと無意識に目を背けて来たものでした。

在宅医療についても全く知識がなく、中でも渡辺西賀茂診療所の取り組みには「ボランティアといえども仕事でそこまでする人たちがいるだなんて」と驚愕しました。

そんな私にこの本は、これから生きていく上で知っておいたほうがいいであろう医療の一面と、自分や周りの人達の終末についての考え方・選択肢を与えてくれました。

とりわけ印象に残っていることの一つが医療の意外な一面です。

痛みを取り除くことは患者の不安を和らげる重要な要素の一つになるが、治療することばかりに意識が向き、緩和のための技術はそれほど重要視されていなかったそうです。

ただしこれは2013年の取材の証言で、その頃緩和ケアの知識を高めるための運動も起きていたらしいので、現在はもう少し関心が向けられているのかもしれません。

またその一方で痛みを取らない方がいいという考え方もあるようで、一概に良し悪し言える分野でもないことも意外でした。

いい医者に出会うか、出会わないかが、患者の幸福を左右しますね p70

試験を受けて知識と技術を保証されている医者ならどの人に見せても一定の水準が担保されているだろうと安易に考えていました。が、やはり医師といえども人は人。本の中ではセカンドオピニオンによって誤診であることが発覚したり、入院患者のケアを怠るピリピリしたナースとのやりとりなども描かれていました。また別の章では病気を完治することに関心の強く倫理観に欠如したことを悪気なく行えてしまう医者と、そんな医療現場にショックを受け、治すこと以上に患者を最後まで人間として扱い、人間らしい仕事をしたい医者が描かれてもいました。

とはいえ良い医者と悪い医者、素人目ではなかなか見分けはつかないと思うのでこればかりは運頼みになってしまうのでしょうが。

もう一つは本のあらすじにも載っているような、あえて治療に専念せず余生を大事に過ごす選択肢を選んだ人たちの様子が数多く納められていたことです。

自分が病気で余命宣告されたら当然生に未練ができて長く生きたいと願うと思いますし、自分ではなく大切な人がそうなったら自分の時以上に長く生きて欲しいと強く願って、あれこれ手を尽くしたくなると思います。医者も熱心な人は海外に学びに行って腕を磨いていて、医療技術も少しずつ進歩しています。でも延命治療の過程で薬による副作用や痛みが続いたり、入院生活が続いて家に帰れず、家族との時間も減って辛い闘病生活になることも考えられます。それでも可能性があるからにはやらない選択肢はない、と思いがちですが、この本では筆者や筆者がインタビューをした医療関係者が見てきたそれ以外の選択肢もたくさん提示されていました。延命治療だけが正解でない、選ばないことが悪でないと思わせてくれます。

二十数年間死から目を背けて生きてこれた私にもいつかきっと、身近な大切な人(もしくは自分)の命の選択に関わる時が来るかもしれません。そういった時に色々な命の閉じ方があることを知っているだけでも、きっと本を読む前よりも良い選択をできるような気がします。

おわりに

診療所の取り組のスタンスに関して、渡辺西賀茂診療所の委員長の言葉が印象的でした。

「僕らは、患者さんが主人公の劇の観劇ではなく、一緒に舞台に上がりたいんですわ。みんなでにぎやかで楽しいお芝居をするんです」 p25

日銭を稼ぐとか、役割をこなすだけじゃなくて、そこまでのことを想い、実施していけること。一社会人としてこんな風に考えて今の仕事ができたらなと思ってしまいました。

【Python】並行処理再入門2

reiichii.hateblo.jp

1から大分日が空いてしまったのですが、あれからエキスパートPythonプログラミング改訂2版で標準ライブラリで実装する並行処理のサンプルコードを動かして理解するなどしておりました。

でやはり自分でも一つ何か作ってみようと思い立ち、書いたコードの忘備録が今回の記事です。

より実践的なコード書きたいなぁと思いつつ、実践的なコードってなんだって情報収集して考えた結果、loggerの実装に行き着きました。

書いたコード

github.com

概要は書いてある通りです。テスト用データを作る機会があったので、そんな感じの仕様になりました。

具体的には、次の情報に基づいて以下のような処理をしています。

  • 情報
    • 全ユーザー数
    • 全アイテム数
    • ヘビーユーザーが持つアイテムの数
    • 一般ユーザーが持つアイテム数
  • 処理
    • メインプロセスが立ち上がる
    • 作成するファイルの情報を生成し、キューに積む
      • キューに積まれる情報:ユーザーid、アイテムid、後ほどtemplateに埋め込まれるデータの参照先
    • 環境変数で指定した数の子プロセスを立ち上げる
      • 子プロセスがキューの情報をgetし、templateからファイルを作成していく

終了するときはctrl + cです。

ログ周りに関しては、QueueHandlerとQueueListenerを使って次のようになっています。

  • mainプロセス、子プロセス
    • QueueHandlerを設定する。インスタンス生成時に渡したログ用のqueueに発生したログをputしていく
  • listenerプロセス
    • Listenerによって生成されたプロセス。queueからログを取得しファイルに書き出していく

ログの実装に関しては、公式のクックブックを参考にしました。

Logging クックブック — Python 3.9.4 ドキュメント

一万円選書のカルテを書いた

なんと、今年度初応募で当選しました!

ある朝眠気まなこでメールチェックをしたら「一万円選書(2021.06)xxx 様」という件名のメールが来ていて一気に目が覚めました😳

一万円選書とは、北海道砂川市にあるいわた書店という個人経営の本屋さんが行なっているサービスです。

内容はカルテを記入し、社長の岩田さんがカルテの内容を元にお薦めの本を約一万円分選んで届けてくれます。一万円選書のことはどこで知ったのか覚えていませんが、おそらくメディアで取り上げられたのをツイッターで見かけたのがきっかけだった気がします。

iwatasyoten.my.coocan.jp

ただ最初はどうやって応募していいか分からず、ツイッターをフォローして情報を待ってみました。そしたらどうやら年に1度だけ応募を受け付けていて、毎月当選者が発表されるシステムでした。いつ応募なのかよく分からずツイートに通知設定をして待ち続けたところ、2020年10月に来年度分を応募開始しますとのことで専用フォームにて晴れて応募することができました。

ただいろんなメディアで取り上げられて全国から応募が来ているため倍率は高く、毎年応募していて数年越しに当選したという感想ツイートも何度か見かけました。私はくじ運がない方なのでまあ気長に待つかと思いつつも、誰かの当選ツイートをきっかけにメールボックスを開いては溜息を付くことを繰り返していたのですが...当たりました😨

私の場合は5.21に当選メールとカルテが届いており、締め切りが2週間後の6.7でした。なのでこの土日はカルテを記入する作業をしていました。

カルテで聞かれることは主に2種類、ひとつは自分の過去の読書歴、もう一つは自分の過去の経験でした。

読書歴は過去に印象に残った本を20冊上げるものでした。読書メーターブクログと家の本棚を漁って書きました。(ただラノベと漫画は岩田さんの守備範囲外だったそうなので一部替えよう)

自分の過去の経験に関しては、「上手に歳をとることが出来ると思いますか」や「これだけはしないと心に決めていることはありますか」など複数の質問がありましたが、基本書きたいことを自由に書いていいようものでした。なのでお言葉に甘えてカルテを書いている間に描きたくなったことを書いてみたりもしました。土曜日に書き散らして(約7000文字くらいになった)、一晩寝かせて日曜日に読みにくいところなど修正しつつ清書しました。

就活や転職活動で昔のことを振り返る作業は私にとってはなかなかしんどいものでしたが、今回は会った事もない見知らぬ書店員さんという事で恥をさらけ出すつもりで書くつもりでした。が、意外というか時間が経ったからなのか、思ったよりしんどくはならなかったです笑。面白くないから滅多にしないのですが自分語りもたまには楽しいものですね😎言語化したものを人に見てもらうことでより整理されるので、本当はもっとすべきなのかも

そんな人の暴露話みたいなものに対して言葉で返信や感想を書くのはなかなか大変なことだと思いますが、今回返ってくるのは言葉ではなくお薦めの本ということで、岩田さんがこれを読んで何を思って本を選んでくれたのか、想像するのが今からとても楽しみです。

ちなみに当選メールで知ったのですが、いきなり本が届くのではなく、選書が終わったら一度タイトル一覧が送られてくるそうです。それでもし読んだことある本があったらそれらは差し替てくれるそうです。なんと手厚い...!

SECCON Beginner 2021 参加した

CTFは去年末くらいに常設問題をぽちぽち解いていたりしたのですが、この度コンテストに初参加しました😃

以下感想と解いたweb系のwriteupです。writeupはコンテストが終了した直後にQiitaの方に作問者による公式のものが出ていますが、自分の整理用も兼ねてまとようと思います。

SECCON Beginner 2021について

www.seccon.jp

内容は用意されたセキュリティ知識や情報処理に関する問題をひたすら解いていくものになっています。例年は上位チームによる決勝戦が開催されていたらしいのですが、コロナ禍で完全オンラインで開催されていた今年はそれがなかったのが残念でした。観戦してみたかった。

問題数は28問あり、それぞれジャンル(welcome, crypto, reversing, pwnable, web ,misc)と難易度(easy, medium, hard)に分かれています。

開催期間は5/22(土)14:00~23(日)14:00までの24h、当日の進行は全てDiscord上で行われ、約900のチームが参加していたようでした👀

戦績

私はCTFのCの字も知らない旦那(スマホアプリエンジニア)を誘って二人チームで参加しました。

方針としては、web系の問題を中心に解いていき、余力があればmisc、さらに他の問題を解いていく形にしていました。

触れた問題は以下です。

  • web

    • osoba(easy)
    • Werewolf(easy)
    • check_url(easy)
    • json(medium)
    • cant_use_db(medium)
    • magic(hard)→ 解けなかった
  • misc

    • git-leak(easy)
    • Mail_Address_Validator(easy)
    • depixelization(depixelization)→ 解けなかった
  • crypto

    • simple_RSA(easy)
    • Logical_SEESAW(easy)→ 解けなかった

最終的な結果は693点で227位(943位中)でした。まあまあ。

ちなみにこれだけのチームが参加していながらも、全問解いたチームはたった2チームしかいませんでした。

writeup

問題では対象のwebシステムとアプリケーションのソースコードが配布されるのですが、web系では多くの問題がdocker-composeで提供されていました。ソースコードを読めば解ける問題がほとんどだったのでdocker環境は必須ではないと思うのですが、ローカル環境にdockerが入っているとやりやすいかもです。

[web] osoba (easy)

美味しいお蕎麦を食べたいですね。フラグはサーバの /flag にあります!

という文言とともに静的コンテンツが掲載されているwebサイトのurlと、ソースコードが渡されています。

webサイトでページ遷移すると分かるのですが、urlパラメータが以下のようになっていました。

?page=public/wip.html

アプリケーションのソースコードを見ると、次のようになっています。

── app
│   ├── Dockerfile
│   ├── flag
│   └── src
│       ├── app.py
│       ├── public
│       │   ├── index.html
│       │   ├── kikin.html
│       │   ├── neck.html
│       │   └── wip.html
│       ├── requirements.txt
│       └── uwsgi.ini
├── docker-compose.yml
└── nginx
    └── nginx.conf

なのでこのurlパラメータを ?page=../flag のようにしてあげることで、フラグを入手できます。

典型的なディレクトリトラバーサルの問題でした。

[web] Werewolf (easy)

I wish I could play as a werewolf...

という文言とともにwebサイトurlとPythonファイルが1枚渡されます。

webサイトは入力フォームになっていて、名前とcolorを入力してsubmitすると、 {NAME}, you are FORTUNE_TELLER などと表示されます。

Pythonファイルは以下のようなものでした。

import os
import random
from flask import Flask, render_template, request, session

# ====================

app = Flask(__name__)
app.FLAG = os.getenv("CTF4B_FLAG")

# ====================

class Player:
    def __init__(self):
        self.name = None
        self.color = None
        self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN'])
        # :-)
        # self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN', 'WEREWOLF'])

    @property
    def role(self):
        return self.__role

    # :-)
    # @role.setter
    # def role(self, role):
    #     self.__role = role


# ====================

@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == 'GET':
        return render_template('index.html')

    if request.method == 'POST':
        player = Player()

        for k, v in request.form.items():
            player.__dict__[k] = v

        return render_template('result.html',
            name=player.name,
            color=player.color,
            role=player.role,
            flag=app.FLAG if player.role == 'WEREWOLF' else ''
        )

# ====================

if __name__ == '__main__':
    app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT"))

def index()の最後に書いてある通り、player.role が 'WEREWOLF' だった場合に環境変数に設定されたフラグが入手できるようになっています。 しかしフォームにはnameとcolorしかないためどうにかしてroleを指定できるようにする必要があります。いろいろやり方はあると思いますが、私は単純にChromeデベロッパーツールで <input class="input" type="text" name="name">のnameの値を <input class="input" type="text" name="role"> のように書き換えてWEREWOLFになろうとしました。

ですがこれではまだ通りません。というのも、player.__dict__[k] = vの部分でPlayerクラスの属性をリクエストデータの値に置き換えているのですが、role属性はアンスコが二つ付いたものになっています。

調べて初めて知ったのですが、これは「ネームマングリング」というものでした。 mangleとは難号化という意味で、__が二つついた変数・関数はクラス外からそのままではアクセスできなくなります。 アクセスするにはマングリング後の名前(_Classname__spamのような形)を指定する必要があります。 というわけで、「name="role"」でもなく「name="__role"」でもなく 「name="_Player__role"」 にしてあげることで人狼になることができました😊

参考

[web] check_url (easy)

Have you ever used curl ?

という文言とともにurlとindex.phpが渡されます。

urlにアクセスすると、「c(heck)_url」というタイトルのサイトが開きますが、見ただけでは何のサイトなのかよくわかりません。 「like this」をクリックすると「URL: https://www.example.com」と共にサイトの中央の四角の中に Example Domainという別のサイトが表示されています。urlパラメータに ?url=https://www.example.com のようにurlを入力すると、その入力されたサイトが表示されるものみたいです。

index.phpの中身は以下のようになっていました。

<!-- HTML Template -->
          <?php
            error_reporting(0);
            if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){
              echo "Hi, Admin or SSSSRFer<br>";
              echo "********************FLAG********************";
            }else{
              echo "Here, take this<br>";
              $url = $_GET["url"];
              if ($url !== "https://www.example.com"){
                $url = preg_replace("/[^a-zA-Z0-9\/:]+/u", "👻", $url); //Super sanitizing
              }
              if(stripos($url,"localhost") !== false || stripos($url,"apache") !== false){
                die("do not hack me!");
              }
              echo "URL: ".$url."<br>";
              $ch = curl_init();
              curl_setopt($ch, CURLOPT_URL, $url);
              curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 2000);
              curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
              echo "<iframe srcdoc='";
              curl_exec($ch);
              echo "' width='750' height='500'></iframe>";
              curl_close($ch);
            }
          ?>
<!-- HTML Template -->

if ($url !== "https://www.example.com"){ にある通り、 ?url=https://www.google.comのように入力しても、urlが「URL: https://www👻google👻com」のように置換されてしまいます。

一方どうすればフラグが入手できるのかというと、一番上のif文 if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){にある通り、 127.0.0.1(自身)からアクセスした時だけフラグが表示されるみたいです。 ただし三つ目のif文 if(stripos($url,"localhost") !== false で書いてある通りlocalhostは使えません。

このif文を突破する解はIPアドレスを16進数に変換してあげることでした。

IPアドレスはは10進数、16進数に変換した形でもアクセスできるらしいです👀

アドレスが 127.0.0.1であればいいので、それを16進数に直した ?url=https://0x7f000001 のような形でアクセスすると、フラグをゲットできました。

ちなみに10進数だとBad Requestになってしまいます(Apacheの仕様みたいな話を聞いたけどソースを見つけられなかった)

easyだけど早くも苦戦した問題でした。言われてみれば知識としては持っていたような気はするのですが、こういう使い方をしてみようという発想がありませんでした。相方がいなければ解くのにもっと時間がかかっていたと思います😨

参考

[web] json (meduim)

外部公開されている社内システムを見つけました。このシステムからFlagを取り出してください。

という文言とともにurlとアプリケーションコードが入ったzipファイルが渡されます。

urlを開くと「Internal Website / 内部ページ このページはローカルネットワーク(192.168.111.0/24)内の端末からのみ閲覧できます。」などと表示されます。

アプリケーションコードを見ると、bffとapiの二層構成になっています。いずれもgolangで書かれていました。

bff/main.goはipアドレスをチェックした後ページを開くGETリクエストと、POSTリクエストを受け付けるエンドポイントがあります。POSTでは0から2までのidをJSON形式で受け付けており、バリデーションした後apiにリクエストを投げています。

(コードは一部抜粋)

// bff/main.go
type Info struct {
    ID int `json:"id" binding:"required"`
}

// check if the accessed user is in the local network (192.168.111.0/24)
func checkLocal() gin.HandlerFunc {
    return func(c *gin.Context) {
        clientIP := c.ClientIP()
        ip := net.ParseIP(clientIP).To4()
        if ip[0] != byte(192) || ip[1] != byte(168) || ip[2] != byte(111) {
            c.HTML(200, "error.tmpl", gin.H{
                "ip": clientIP,
            })
            c.Abort()
            return
        }
    }
}

func main() {
    r := gin.Default()
    r.Use(checkLocal())
    r.LoadHTMLGlob("templates/*")

    r.GET("/", func(c *gin.Context) {
        c.HTML(200, "index.html", nil)
    })

    r.POST("/", func(c *gin.Context) {
        // get request body
        body, err := ioutil.ReadAll(c.Request.Body)
        if err != nil {
            c.JSON(400, gin.H{"error": "Failed to read body."})
            return
        }

        // parse json
        var info Info
        if err := json.Unmarshal(body, &info); err != nil {
            c.JSON(400, gin.H{"error": "Invalid parameter."})
            return
        }

        // validation
        if info.ID < 0 || info.ID > 2 {
            c.JSON(400, gin.H{"error": "ID must be an integer between 0 and 2."})
            return
        }

        if info.ID == 2 {
            c.JSON(400, gin.H{"error": "It is forbidden to retrieve Flag from this BFF server."})
            return
        }

        // get data from api server
        req, err := http.NewRequest("POST", "http://api:8000", bytes.NewReader(body))
        if err != nil {
            c.JSON(400, gin.H{"error": "Failed to request API."})
            return
        }
        req.Header.Set("Content-Type", "application/json")
        client := new(http.Client)
        resp, err := client.Do(req)
        if err != nil {
            c.JSON(400, gin.H{"error": "Failed to request API."})
            return
        }
        defer resp.Body.Close()
        result, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            c.JSON(400, gin.H{"error": "Failed to request API."})
            return
        }

        c.JSON(200, gin.H{"result": string(result)})
    })

api/main.goではリクエストをJSON parseし、idの値に応じた処理を行っています。

(コードは一部抜粋)

// api/main.go
func main() {
    r := gin.Default()

    r.POST("/", func(c *gin.Context) {
        body, err := ioutil.ReadAll(c.Request.Body)
        if err != nil {
            c.String(400, "Failed to read body")
            return
        }

        id, err := jsonparser.GetInt(body, "id")
        if err != nil {
            c.String(400, "Failed to parse json")
            return
        }

        if id == 0 {
            c.String(200, "The quick brown fox jumps over the lazy dog.")
            return
        }
        if id == 1 {
            c.String(200, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
            return
        }
        if id == 2 {
            // Flag!!!
            flag := os.Getenv("FLAG")
            c.String(200, flag)
            return
        }

        c.String(400, "No data")
    })

id=2 だとフラグが出るようになっています。curlでPOSTリクエストしてデータをいい感じに渡してあげれば解けそうです。

ですがbff/main.goの方ではid=2にすると c.JSON(400, gin.H{"error": "It is forbidden to retrieve Flag from this BFF server."}) のエラーが出るようになっています。

そこを考える前に、まずIPアドレスのフィルタリングを突破する必要があります。

今回の場合だとcurl実行時に --header "X-Forwarded-For: 192.168.111.0" を渡してあげればできました。

X-Forwarded-For (XFF) ヘッダーとは、HTTP プロキシ又はロードバランサーを通過してウェブサーバーへ接続したクライアントの、送信元 IP アドレスを特定するためのヘッダーです。

validationの突破方法ですが、ポイントはbffとapi二ヶ所で別々のやり方でJSON parseしている点にありました。

bffでは json.Unmarshal(body, &info)type Infoで定義した形でparseされていて、info.IDがバリデーションされています。ところがapiリクエスト時に渡されているデータはinfoではなく、Unmarshal前のbodyです(!)。

apiではシンプルにid, err := jsonparser.GetInt(body, "id") で idに値が渡されています。

このvalidationを突破する方法は、json.Unmarshalとjsonparser.GetIntの処理方法の違いにありました。

json.Unmarshalでは重複するキーを渡した場合、後者の値が返るようになっています。

func main() {
    b := []byte(`{"id":1,"id":2}`)
    type Info struct {
        ID int `json:"id" binding:"required"`
    }

    var info Info
    json.Unmarshal(b, &info)
    fmt.Printf("%+v", info.ID)
}

// 出力: 2

一方jsonparser.GetIntでは最初の方の値が返されるようです。

func main() {
    b := []byte(`{"id":1,"id":2}`)
    id, err := jsonparser.GetInt(b, "id")
    if err != nil {
        fmt.Printf("Failed to parse json")
        return
    }
    fmt.Printf("%+v", id)
}

// 出力: 1

というわけで今回の問題は、 curlのPOSTデータに{"id": 2, "id":1} を指定することでbffのバリデーションを交わしてフラグをゲットできました🚩

プログラムや利用しているライブラリでJSONの重複の扱いがどうなっているのか、存在していた場合どう処理するようにするのかは気をつけないといけないなぁと身に沁みました。

参考

[web] cant_use_db (meduim)

Can't use DB. I have so little money that I can't even buy the ingredients for ramen. 🍜

という文言とともにurlとアプリケーションのファイルが一式渡されます。

urlを開くと購買画面になっていて、左側の方はユーザーのステータス(所持金 $20000、Noodlesの購入個数が0/2、Soupの購入個数が0/1)が表示されています。右側の方は購入画面になっており、 Noodles $10000, Soup $20000それぞれの購入ボタンと「Flag (Noodles>=2, Soup>=1)」と書かれたボタンがあります。少ない所持金でどうやって必要な材料を揃えるかという問題みたいでした。

アプリケーションコードを見ると、リクエストごとに各ボタンを押した時の処理が分かります。

pp = Flask(__name__)
app.secret_key = secrets.token_bytes(256)


def init_userdata(user_id):
    try:
        os.makedirs(f"./users/{user_id}", exist_ok=True)
        open(f"./users/{user_id}/balance.txt", "w").write("20000")
        open(f"./users/{user_id}/noodles.txt", "w").write("0")
        open(f"./users/{user_id}/soup.txt", "w").write("0")
        return True
    except:
        return False


def get_userdata(user_id):
    try:
        balance = open(f"./users/{user_id}/balance.txt").read()
        noodles = open(f"./users/{user_id}/noodles.txt").read()
        soup = open(f"./users/{user_id}/soup.txt").read()
        return [int(i) for i in [balance, noodles, soup]]
    except:
        return [0] * 3


@app.route("/")
def top_page():
    user_id = session.get("user")
    if not user_id:
        dirnames = datetime.datetime.now()
        user_id = f"{dirnames.hour}{dirnames.minute}/" + secrets.token_urlsafe(30)
        if not init_userdata(user_id):
            return redirect("/")
        session["user"] = user_id
    userdata = get_userdata(user_id)
    info = {
        "user_id": re.sub("^[0-9]*?/", "", user_id),
        "balance": userdata[0],
        "noodles": userdata[1],
        "soup": userdata[2]
    }
    return render_template("index.html", info = info)


@app.route("/buy_noodles", methods=["POST"])
def buy_noodles():
    user_id = session.get("user")
    if not user_id:
        return redirect("/")
    balance, noodles, soup = get_userdata(user_id)
    if balance >= 10000:
        noodles += 1
        open(f"./users/{user_id}/noodles.txt", "w").write(str(noodles))
        time.sleep(random.uniform(-0.2, 0.2) + 1.0)
        balance -= 10000
        open(f"./users/{user_id}/balance.txt", "w").write(str(balance))
        return "💸$10000"
    return "ERROR: INSUFFICIENT FUNDS"


@app.route("/buy_soup", methods=["POST"])
def buy_soup():
    user_id = session.get("user")
    if not user_id:
        return redirect("/")
    balance, noodles, soup = get_userdata(user_id)
    if balance >= 20000:
        soup += 1
        open(f"./users/{user_id}/soup.txt", "w").write(str(soup))
        time.sleep(random.uniform(-0.2, 0.2) + 1.0)
        balance -= 20000
        open(f"./users/{user_id}/balance.txt", "w").write(str(balance))
        return "💸💸$20000"
    return "ERROR: INSUFFICIENT FUNDS"


@app.route("/eat")
def eat():
    user_id = session.get("user")
    if not user_id:
        return redirect("/")
    balance, noodles, soup = get_userdata(user_id)
    shutil.rmtree(f"./users/{user_id}/")
    session["user"] = None
    if (noodles >= 2) and (soup >= 1):
        return os.getenv("CTF4B_FLAG")
    if (noodles >= 2):
        return "The noodles seem to get stuck in my throat."
    if (soup >= 1):
        return "This is soup, not ramen."
    return "Please make ramen."


if __name__ == "__main__":
    app.run()

購入個数や残高の情報がDBではなくファイルに保存されているのが特徴的です。

このファイルの扱い方がこの問題の解につながるのですが、私がこの問題を解いたのはほとんど偶然の気づきでした。

というのも最初はディレクトリトラバーサル系の問題なのかなと、どうにかしてファイルにアクセスして改ざんできないものかと考えていたのですができず、別のやり方を探そうと思った時にふと一番最初にwebアプリの購入ボタンを押した時にほんの少しラグがあってあれ反応がないなと連打したらNoodleが4/2購入できてしまった時のことを思い出しました。

あ、これかと...。

ちなみにボタン押下時にラグがあったのはソースコードにも書いてある通りです。この事象に遭遇した時はまだソースコードさえ開いていなかったので、このやり方でこのままフラグをゲットしてしまうことに抵抗があったのだと思います。にしても早く気づけよ自分...って頭を抱えました。😅

というわけで間髪入れずにNoodleとSoupを必要な数連打する形でフラグを入手しました。

所感

疲れたけど楽しかったに尽きました!

まず何がいいかって土曜日14:00から日曜日14:00までという開催時間がちょうど良かったです。土曜日の午前中はゆっくり寝れるし、終わった後もCTFの疲労をそのまま月曜日に持ち込むことなく、休んだり公式writeup読んだり時間があります。

土日の大半をCTFの問題に向き合うのに使っていましたが、日曜日の夕方は自分でも謎なくらいすごい充実感に満たされていました笑

あとで数えてみたら11hくらいは解いていたみたいでした。他の参加者と比べて多いのか少ないのかはさておき、体感時間はあっという間でした。

  • 土曜日
    • 14:00~16:30 問題解く
    • コンビニまで散歩。おやつ休憩
    • 17:30~19:00 問題解く
    • 夕飯休憩&だらだらする
    • 2100~23:00 問題解く
  • 日曜日
    • 気の済むまで寝る
    • 10:00~14:00 問題解く

問題はやっぱりweb系が一番楽しいです。実装するやレビューするときの参考にもなりそうでした。

自分のwriteupをまとめたので、解けなかった問題のを読みに行こう...

【Python】並行処理再入門1

読めはするけど設計できるかちょっと怪しい、知ってはいるけどいまいち分かった気になれない並行処理について勉強したことのメモです。

本当はコードも含めて載せる予定だったのですが、ちょっと疲れてきたので一旦用語の整理までで。

再入門の目的・ゴールとしては、

  • 並行処理を用いた設計イメージが沸くようになる
  • 実装するときに気をつけることを知る

を設定しています。

用語の整理

並行処理(Concurrent)

  • 特定のマシンの限られたリソースの中でマルチスレッドやマルチプロセスによって同時に処理を行うこと
  • 同時といっても実態はさまざま
    • 1つの重たいタスクを複数の処理が分担して行なっている(並列処理という言葉が指すもの)
    • 複数のタスクがあり、短時間で切り替えながら処理をして、あたかも同時に裁いているように見せかけているもの

並列処理(Parallel)

  • リソースが共有されているか否かに限らず、"完全に"同時処理を行なうこと
  • 並行処理の中の具体的な処理方式の一つに位置付けられている言葉
    • 並行という言葉だけだと、完全に同時実行なのかそう見せかけたような処理を指すのか判別できないが、並列は完全に同時実行されている

プロセス

  • 実行されているプログラムの処理のこと。プログラムがOSによって主記憶装置に読み込まれたもの
  • 役割
    • システムプロセス: OSの機能の一部を実行する
    • ユーザープロセス: 利用者の指示で実行される
  • ユーザーごとに起動できるプロセス数のソフトリミットとハードリミットが/etc/security/limits.confなどで定義されている
    • 確認例: ulimit -a
    • ちなみにこのulimitコマンドでユーザーやプロセスが使えるリソースの上限を変更できる
  • 作成できるプロセスの数自体に制限はない(CPUやメモリなどのリソースが枯渇しない限り)が、プロセスIDの上限が設定されている
    • 確認例: cat /proc/sys/kernel/pid_max

スレッド

  • CPU利用の単位のこと
  • 一つのプロセスは、一つ以上のスレッドで構成される
  • スレッド自身もプロセスの一種なので、ulimit -uの数を超えて作成することはできない
  • CPUのコアに命令を実行させるのはスレッド
  • サーバアプリケーションでは1リクエストや1コネクションにつき、1スレッド用意する必要がある
  • プロセスごとに作成できるスレッド数は/proc/[pid]/limitsで定義されているMax processesの値が適用される
  • システム全体でのスレッド数の上限が設定されている
    • 確認例: cat /proc/sys/kernel/threads-max

並行処理が必要なタスクのタイプ

CPUバウンドなタスク

  • タスクを実行している時間の大半が、CPUの処理に使われているもの
    • 例: 計算処理

I/Oバウンドなタスク

  • タスクを実行している時間の大半が、CPUの処理以外(ディクスへの入出力やリクエスト、その応答など)に使われているもの
    • 例: DBに接続して結果を返すようなAPIの処理

協調的マルチタスク(ノンプリエンプティブマルチタスク

  • 1つのCPUに対して、複数のアプリケーションが同時に稼働している際、OSではなくアプリケーション自身がCPUを制御し、開放するようなマルチタスクの実行方法
    • 昔はOSでも採用されていたが、システムのリソースをアプリケーション側が管理するこのやり方はなかなかリソースを手放さないアプリが出ると他の作業が滞ったりなど問題が多々あったらしく、現在のプリエンプティブマルチタスク(OSがプロセスやスレッドを直接管理して、コンテキストスイッチさせるやり方)方式になったとか

並行処理の実現手段

マルチスレッド

  • 複数のスレッドはCPUやメモリなどのリソースを共有する
  • シングルプロセッサ上でマルチスレッドが動いている場合、スレッドを順次切り替えることで見かけ上の並列処理を実現している
  • マルチプロセッサ上でマルチスレッドが動いている場合、各スレッドが別々のプロセッサ上で同時に動いていれば並列処理になる

マルチプロセス

  • 同じOS上に動く複数のプロセスは、それぞれ割り当てられた資源の中で独立して処理を行なっている
    • プロセスAが使っているメモリ領域にプロセスBがアクセスするようなことはできない

非同期プログラミング

並行処理の文脈でよく出てきて、位置付けを整理しておきたかったので軽く触れます。

  • 複数のプロセスやスレッドを使わず、複数のタスクを全て一つのプロセス、スレッド管理する
  • 故に並列処理にはならないコンテキストスイッチが発生するタイプの並行処理になるが、シングルプロセッサ上のマルチスレッドと違う点は、スイッチを行うのはOSではなくプログラム内部(特定の関数など)になる点
    • この制御する関数はイベントループスケジューラーと呼ばれている
  • Pythonでは非同期処理で扱う並行タスクをコルーチンと呼ぶ

Pythonの並行処理の実装手段

Pythonのマルチスレッド(threadingモジュール)

  • I/Oバウンドなタスク向き
  • 複数のスレッドはメモリを共有している
    • データを並行処理から保護することを考えないといけない
    • 保護しなかった場合、複数のスレッドが一つのデータを同時に更新しようとして予期しない結果になる(race hazard)
    • データの保護を間違えた場合はデッドロックとなりうるが、再入可能ロックを使うと一部のデッドロックを防げたりする
  • GIL(Global Interpreter Lock)の制約で、Pythonを実行するスレッドはグローバルロックにより常に一つに限定されている
  • 故にマルチコアのマルチスレッド下でもCPUバウンドな処理は1スレッドでしか実行されず、他のスレッドは待機状態になる
  • 標準ライブラリだけだと信頼性の高いシステムにするにはプールやキューを用意し、ワーカースレッドの例外を適切に処理するなど実装するものが多い

Pythonで並列処理をするなら知っておくべきGILをできる限り詳しく調べてみた - Qiita

マルチプロセスにすることで、プロセスごとにGILを持つため並列処理を実現できるようになります。

Pythonのマルチプロセス(multiprocessingモジュール)

  • CPUバウンドなタスク向き
  • メモリを共有しないのでデッドロックなどの心配がない
    • その代わりにプロセス間でデータ共有をするには実装が必要になるが、プロセス間通信に関しては簡単で信頼性のある機能が複数提供されている
  • 新しいインタプリンタを立ち上げるのでCPUバウンドなPythonコードもGILに邪魔されずに並列処理できる
  • プロセス間通信を行うため関数や返り値がpicklableでないといけない

これだけ読むと必ずしもマルチプロセスにして並列数を設定可能な最大値にすれば一番処理性能が良くなりそうに思えてくるかもしれませんが、一概にそうともいえなく、 マルチスレッド・マルチプロセスどれをどのくらいの設定にするのがいいかは、負荷試験などの検証を踏まえて決定する必要がある、そうです。

Pythonの非同期処理(asyncioモジュール)

  • I/Oバウンド向き
  • コルーチンを定義する

非同期と同期処理を合わせて使う(concurrent.futureモジュール)

  • I/Oバウンドなタスクなので非同期で行いたい処理の中に一部CPUバウンドなものが含まれていた場合、そのタスクを捌いている間は他のタスクが待機中になってしまうので非同期処理は有効でなくなってしまう
  • そういったケースにおいて一部のCPUバウンドな処理をプロセスに委譲し、委譲した処理をコルーチンのように扱えるようにして制御をイベントループに開放するといった、同期処理と非同期処理の良いとこどり(?)を実現する

おわりに

Pythonで並行処理を実装するにあたって、ドキュメントを見にいくと上記で取り上げたようないくつかのモジュールが紹介されている中で「処理をさせたいタスクの種類などに応じて判断してね」と書いてあるのですが、どうやってそれらを選べばいいのか最初は分かっていませんでした。そもそも並行処理とはを整理した上でPythonの実行時の特性(GILの話)を知ることでその判断基準を少しは持てたような気がします。

参考

他いろいろな記事をつまみ食いする形で穴を埋めました。

【自作キーボード】Sparrow62を組み立てた

デスク環境改善の一環として、新しいキーボードをお迎えしました👏

f:id:reiichii:20210504204214j:plain

キーボード変遷

1 会社から支給されたキーボード

特にこだわりはなかった時代です。

電気屋で買った薄くて押しやすそうなキーボード

指が痛くなったため、会社の昼休みに近くの電気屋で探して書いました。確か2000円程度のものだった気がします。

FILCO Majestouch MINILA Air JP68キー

www.diatec.co.jp

配線が嫌になり、Bluetoothのキーボードを考えるようになりました。またファンクションキーとテンキーが邪魔だなと思うようになり、当時隣の席の人が使っていたこのキーボードみたいなものいいなと探した結果、結局同じものを書いました。新宿のヨドバシカメラで赤や青などの軸を比較して赤軸のものを選びました。

4 Corne Cherryを作る

より小さいキーボード、分割タイプのキーボードに興味を持ち自作キーボードに挑戦しました。作るところまでは良かったのですが、40%キーボードに結局慣れることができず継続して使うことはありませんでした。というのもデスクを離れてノートPCで作業することも多かったため、そのスイッチングコストが自分にとっては負担でした。

5 Mistel BAROCCO MD770

archisite.co.jp

ペアプロがきっかけでUS配列の方がプログラミングがしやすいことに気づきました。気に入っていたFILCOのUS配列版を買うでも良かったのですが、分割タイプへの憧れを捨てきれず探した結果このキーボードを見つけました。逆にこれ以外で条件に合うキーボードは自分が探した限りではありませんでした。

MD770も決して悪くはなかったのですが、ある日左commandキーが反応しなくなったこと、Macがサポート対象外で不具合修正したファームウェアのアップデートを自力でできなかったことを踏まえて乗り換えするに至りました。

新しいキーボードに求めているもの

  • 分割タイプであること

先代に慣れ、ちょっと開いていた方が落ち着くようになってしまっていたため。 それから真ん中にノートを置きたい時などにもちょうど良かったりします。

  • 60%であること

テンキーとファンクションキーは要らんでしょ、と思っています。 でもカーソルキーはあると嬉しい...🙂

  • できるだけ薄い

分割タイプにするとパームレストが2つ必要になりますが、掃除の時に退かすものが多くなるので置くものは最小限にしたいと思っています。薄いキーボードにすればパームレストも置かずに済むのではと考えていました。(ちなみにパームレストは先代のキーボードのタイミングで購入していました。)

既製品では上記を満たすようなキーボードはありませんでした。

Sparrow62との出会い

自キーならどうだろう、とはいえ私が知っている多くの自キーは板3枚(PCB基盤、トッププレート、ボトムプレート)から成り立っているものが多いイメージを持っていたので果たして薄いものはあるのだろうかと思っていたと探していたところ、ドンピシャリな記事を見つけました。

zenn.dev

この記事の筆者が作ったSparrow62は私が期待していた要件に加えて、LEDや液晶などがなくミニマルな点が良く最有力候補になりました。

74th.booth.pm

ちなみにもう一つ候補にあったのが、7sKBというキーボードでした。

booth.pm

こちらは会社の同僚から教えてもらったものでした。Sparrow62と違ってキーのレイアウトがより一般的なものに近いところが良く、とても迷いました。最終的な決め手はキースイッチにKail choc v2を使ってみたいと思っていたので、v2にも対応していたSparrow62に決めました。

Kailh Choc v2との出会い - 自作キーボードでキーボードの低さを目指した話

Sparrow62組み立て時のメモ

公式のビルドガイドがとても丁寧なので改めて書くようなことはほとんどないのですが、作業ログがてらスムーズにいかなかった点だけ残します。

スイッチソケットを間違えて買っていた

ただのうっかりです😓 スイッチソケットを購入するときにタイプを選択しないといけないのですが、その購入オプションを見落としていました。はんだ付けをする直前に気づき(キースイッチがどのようにハマるのかイメージが持てず、製作者様に相談メールを送ったところ指摘をもらって気づいた)、パーツを買い直しました。1500円程度の損失で済んでまだ良かったです...。

スイッチソケットは実は向きがあった

はんだ付け終わった後にたまたま別の記事を見つけて知りました。

www.eisbahn.jp

v1とありますが、v2も同じものを使っているので当てはまります。 ただビルドガイドには明記されておらず、この記事曰く仮に向きが逆でも動作はするらしいです。ただ二つある銅線の厚みと穴の大きさが異なることから接触不良が起きる可能性が若干高くなるかも...?程度のものだったのでもしかしたら直さなくても良かったかもですが、私は念のため付け直すことにしました。スイッチソケットは付け直すのが大変なパーツで、それを約半分の30個外してつけ直す作業をしたので軽く泣きました。

ファームウェアの書き込みに失敗する

ビルドガイドに載っていたQMK Toolboxを使ったやり方で行なっていたのですが、書いてある通りに行ってもデバイスが検知されていないといったエラーが出てしまいます。

しかしこれも既知のエラーで、同じ症状の以下の方の記事を参考になんとか解決(?)しました。

ハマったところ | Helixキーボードを組み立てた - 面白コンテンツ探求日記

私の環境の場合はAutoloadにチェックを入れた状態で、書き込みが行われなければ再度リセットボタンを押す作業を成功するまで繰り返しました。多い時で5回くらいやり直せばだいたいうまくいきました。成功した時はログに出るより先に赤いランプが点滅するのでわかります。この赤いランプも最初は何かのエラーかと思って冷や冷やしたのですが、ただの仕様みたいでした。

完成品

そうしてできたものがトップの写真のものでした。

キーキャップ に関してはDSAプロファイルをバラで買い揃えました。基盤の黒に馴染み、且つあまり重くなり過ぎないようにしたくてこの配色になっています。欲を言うと直したい部分をいくつかあるのですが、今のままでもまあまあ満足しています😃

動作も今の所問題なく動いてくれていてホッとしています。

薄さも、前に触らせてもらって良さげだった純正Macのキーボードに引けを取らない薄さで素晴らしいです。

f:id:reiichii:20210504204317j:plain

キーマップのPDCA

無事動作確認までできたら、あとは自分の理想のキーマップを追い求める苦しくも楽しい時間が始まります。

私はあまり凝ったことはしていないので、QMK ConfiguratorからGUIでキーマップを作り、出力したhexファイルを先ほどのQMK Toolboxに読み込ませてキーボードへの書き込みを行い、確認してまた直して...を落ち着くまで繰り返しています。

この記事を書いている時点では、以下のような形になっています。

f:id:reiichii:20210504122142p:plain

ベースはUS配列で、収まりきらないところや押しづらい部分だけずらす思想で作っています。具体的には以下の部分です。

  • [{ ]} キーを内側に移動
  • shift + 数字キーを別レイヤーの届きやすいところにも配置

左の一番内側が空いていて勿体無いので、押しにくい右上のキー達をここにも持ってきてもいいかもなんて今書いていて思いました。

不足に思う点

継続して使っていける気はするのですが、2点解決できていない問題があります。

  • 時々動いてしまう

薄型で軽いのだからそりゃそうだろって気もしますが...。裏面には付属のゴム足がついているのですが、私の机の上だと少し滑ってしまいます。稀になので一旦様子見て気になるようだったら別で滑り止めを探すことにします。

  • 斜めにできない

上のキーが押しにくいので上の写真のMacの純正キーボードみたいに上の部分をもう少し高くしたいなと試行錯誤したのですが、この形だとバランスを取るのが難しくぐらついてしまいます。こちらはどうしたものかなー

おわりに

この記事も最初は新しいキーボードで最初から書くつもりだったのですが、慣らし作業と内容を考えるというマルチタスクを行うのは無理でした笑。なので一度Macのキーボードでメモ帳に書いた記事をコピペではなく新しいキーボードで書き写すというやり方をしていたりします。

ただ3500文字くらい書くと思っていたより手に馴染んできて、タイポも減ったような気がします。 command + shift + layer + ←みたいなのは一瞬手が止まったりしますが、文字を打つ分にはもうスムーズです😊

デスクの配線を整理した

2020年の緊急事態宣言以降ずっとリモートワークをしています。

当時は仕事用のデスクは持っていなく、家でPCをいじるときはノマドワーカーよろしくダイニングテーブルや膝の上でカタカタやっていたのですが、旦那に唆されてFLEXISPOTの昇降デスクを買わされ、オフィスを縮小するタイミングで不要になったディスプレイと椅子(okamuraの良いやつ)を貰い、あれよあれよとデスク環境が整っていきました。

そんな中ネックになってきたのが配線です。

f:id:reiichii:20210424161001j:plain

いい加減にけりを付けねばと重い腰に鞭を打ち、配線処理を改善させました。

とりわけこだわった点は、色を黒に統一させることでした。

After

f:id:reiichii:20210424161813j:plain

だいぶスッキリしたんじゃないでしょうか😌

手前にぶら下がっているBeatsのイヤホンが配線みたいになっていますが、他にいい置き場所がないので許容しています。

f:id:reiichii:20210424161918j:plain
低くした時

環境

図のようになっています。

f:id:reiichii:20210424173739j:plain

書き忘れたけど昇降デスクのコンセントもあるので、全部で8本の配線をなんとかしないといけない状態でした。

電源タップとイヤホンは普段は机の裏にくっついています。

やったこと

1. Macの充電器を変えた

純正のものを使っていましたが、重くてよく落ちるのが悩みでした。

あと黒で統一したいなぁと思っていたので、思い切って小さいやつを買いました。

これを、100均一で買った黒のマステで巻く。

f:id:reiichii:20210424175329j:plain:w400

f:id:reiichii:20210424175341j:plain:w400

基本見えないところにあるものなので、値段を上げて黒色を買うまでもありません。

ちなみに他にもコードに付いていた白いタグとか、目に付いた部分はすべてこのマステで黒に塗りつぶしました。

2. 電源タップの磁石を強化

これの黒を使っています。マグネットは元々付いていました。

しかし、これも時々落ちることがあったのでマグネットを強化しました。

もしかしたらMacの充電器を変えたので大丈夫だったかもしれませんが、念のため。

3. 垂らす方のケーブルをカバーで巻く

配線をどうすればいい感じにできるか考えた結果、以下の形に収まりました。

  • 左に下げる:地面に置く系のもの(PS4とスピーカー用)
  • 右に下げる:電源タップに電気を送る配線用。1本しかないので何もしないことにした
  • 天板の裏に隠す:机に置く系のもの(ディスプレイ、PCの充電器、スイッチ、昇降デスク用)

本当は下げるものに関しては片方に統一できれば良かったのですが、無理そうだったので諦めました。

左に下げる二本のコンセントを配線カバーで隠してマシにします。

安っぽいプラスチックのチューブですが、ないよりはだいぶマシな印象。

4. ケーブルトレーを導入する

配線整理を億劫に感じていた一番の悩みどころでした。

しかしやると決めた以上、天板の裏に穴を開けるのも厭わない気持ちでケーブルトレーを探していたらなんと、穴を開けなくても設置できるグッツを見つけました。(机の脇に挟むタイプのものは嫌だった)

貼るだけで設置できるケーブルトレーです。しかも黒。

本当にくっつくのかと疑わしい気持ちもありましたが、口コミもなかなか良さそうでちょっと試してみよう、最悪ダメだったら机に穴開けてトレーだけ使おうと購入してみました。

設置して1週間以上経過し、ニンテンドースイッチまで乗せていますが落ちてくる気配もなく安心しています😃

f:id:reiichii:20210424184605j:plain:w400

おわりに

以上のようなことをして仕上がりました。

配線は片付いたのですが机の上はまた別の問題があり...次はここをなんとかする予定。