この一週間のこと。 先週までにDiscord のOAuth 2.0の仕組みを使ってDiscordの外のアプリケーションのサインアップ、ログインをサーバーを用意せずに無料で使えるリソースで作ってみる、っていうのをやってた。 先週、動くものができた。できたけど、動くものを作ってみたかっただけで自分では使い道がない。作る途中で出会った小さなtipsがいくつかあって、それを誰かに話したいと思っていた。 このブログや、zennやqiitaなどに書いてみようとも思っていた。んで、いざ動くものができてみたら、もう興味を失ってしまって、何もできなくなってしまった。いつもの突然のブロッキング現象だ。 メモは書きながら作業していたけど、ここでは、今思いつくだけのことを確認をとらずに書き出してみる。

  • discordはopenid connectに対応していないので、oauthのアクセストークンで@me apiにアクセスできることをもって認証とする
    • なので、アプリケーションにはサーバーサイドの処理が必要
      • サーバーサイドでの Authorization Code Flow でアクセストークンを受け取る
  • discordの OAuth2.0 エンドポイントは undocumented だけど PKCE に対応している
    • PKCE を利用すれば、client secret は不要
  • IDaaSとしてのFirebase Authentication
    • SMS確認機能を使わないなら無料
    • 無料プランでもユーザー数無制限
    • REST API は identity tool kit という名前が付いていて
    • SDKも identity tool kit という名前だったのが最近 Firebase Authに変更されていた
    • API の認証はサービスアカウントの秘密鍵の署名でできる
      • これは認証APIでアクセストークンを取得しなくてもいい、ということ
  • Firebase Functionsは従量制課金のプランでないと使えない
  • ...

Jupyter NotebookでGMail APIにアクセスするためのOAuth 2.0 アクセストークンをAuthorization Code Flowで取得する

Get OAuth 2.0 Refresh Tokens · GitHub

Jupyter Notebookで、一回きりのTCP接続の待ち受けをしてOAuth 2.0 のアクセストークンを取得する。

GMail APIに使用できるアクセストークンはデバイスフローでは取得できない。gmail関連のscopeを受け付けない。 Authorization Code Flow (認可コードフロー)のローカルリダイレクトを使ってNotebookで受け取る。

google api の場合、redirect_urilocalhostは事前に登録しなくても使用できる。ポート番号もauthorization リクエストのときにパラーメターで渡して決められるので、ランダムポートでよい。

Jupyter Notebookは既定でasyncioのループが実行されているので、asyncio.start_serverしてasyncio.wait_forで待てばいい。

async def get_authorization_code(scope, client_id, auth_uri, port=0, timeout=60):
    from asyncio import start_server, wait_for, Event, TimeoutError
    from urllib.parse import urlencode, urlparse, parse_qs

    event = Event()
    
    code: str
    async def handler(reader, writer):
        nonlocal code

        data = await reader.readline()
        data = data.decode()
        code = parse_qs(urlparse(data.split()[1]).query)["code"][0]
    
        writer.write(b"HTTP/1.1 200 OK\nContent-Length: 0\n\n")
        await writer.drain()
        writer.close()
        await writer.wait_closed()
        event.set()
    
    redirect_uri: str
    async with await start_server(handler, '0.0.0.0', port) as srv:
        _, port = srv.sockets[0].getsockname()
        redirect_uri = f"http://localhost:{port}"
        params = urlencode({
            "client_id": client_id,
            "scope": scope,
            "response_type": "code",
            "redirect_uri": redirect_uri
        })

        print(f"{auth_uri}?{params}")

        try:
            await wait_for(event.wait(), timeout=timeout)
        except TimeoutError:
            print("timeout")
            code = None

    print("server closed")
    return code, redirect_uri

認可コードを受信したときのレスポンスに204 No Contentではなくて200を返す。204ではブラウザで認証画面の表示が残ったままになってどうにも気持ち悪いから。

Cloudflare WorkersでHMAC付きCookieを使ったステートレスセッションを実装した

github.com

Cloudflare WorkersのランタイムはNodejsではない。Web Standards API (Crypto, atob, btoa, ... )で実装する必要がある。

有効期限を持ち、MACで改ざんされたら分かるようにしたトークンを発行する。このトークンをクッキーに設定して、ブラウザとサーバーでの連続したラウンドトリップ間にセッションを作る。

この実装ではHMACのハッシュアルゴリズムにはSHA256を選択した。

キーの長さは32bitで十分らしい timing attackへの対策、定数時間でのハッシュ値比較、web cryptoのhmac verifyはどうなってる?

Pythonのurllib.requestでリダイレクトをさせない

urllib.request.urlopenメソッドはレスポンスが300番台だった場合、リダイレクトを実行してそのレスポンスを結果とする。 HTTPのステータスコード302のレスポンスを返すAPIを作ってるときに、リダイレクトを実行せずに、最初に返ってきた302レスポンスを取得したかった。

既定のオープナーではレスポンスが200以外のステータスコードのときHTTPErrorProcessorによって各ハンドラーに振り分けられる。 docs.python.org urllib.request.HTTPErrorProcessor

今回は何か処理をしたいのではなくてレスポンスを結果として取得したいだけ。HTTPErrorProcessorでの処理をステータスコード200のときと同じようにすればいい。

build_openerにHTTPErrorProcessorのサブクラスを渡せば既定のHTTPErrorProcessorの動作を置き換えることができる。

from urllib.request import build_opener, HTTPErrorProcessor

class Handler(HTTPErrorProcessor):
    def http_response(self, request, response):
        return response
    
    https_response = http_response

opener = build_opener(Handler())

with opener.open(req) as res:
    print(res.code)
    print(res.headers)

参考にした

壊れたブルーレイレコーダーを何とかしようとしてできなかった

家族が使っていたSharpAQUOS ブルーレイディスクレコーダー BD-T1300 が壊れた、と伝えられた。HDDが壊れたっぽいメッセージがずっと表示されて、本体のHDDランプがとても強く光り続けていた。

これ、テレビチューナーが3つもついているのでこのまま捨ててしまうのはもったいない。古いパソコンから取り出したHDDが余っているので載せ換えて使えないだろうか。

面倒なので故障した機体を調べずにとりあえずインターネットに聞いてみた。HDD換装の記録がいくつも見付けられた。やってみた記事らによると、

  • HDDはAVコマンドに対応していること
  • 元のHDDをクローンしてたり、したかどうか明記してなかったり
  • HDDを交換したらサービスモードで登録する必要がある
  • サービスモードを呼び出すリモコン信号は AA 5A 8F 30 F5 01 他2つ

らしい。

AQUOSの「設定」>「初期化・更新」> 「システム動作テスト」を表示させた状態で特定のリモコン信号を受信させるとサービスモード画面が表示される、らしい。

サービスモードはサービスマンモードと書かれている場合もあった。サービスモードを呼び出すための専用リモコンがメルカリなどで販売されているようだ。

Raspberry PiArduino で赤外線リモコンの送受信

Arduinoで赤外線リモコン信号を出すのを作ったことがあった。そのときは既存のリモコン信号のon/offの時間間隔を記録して、それを再生することで信号を送信した。今度は、送る信号がバイト列として与えられている。バイト列を赤外線LEDのon/offの時間間隔に変換する必要がある。

Raspberry Pi で赤外線リモコン信号を受信する

まずは、純正リモコンの信号を受信してバイト列で表現(デコード)する。そのバイト列をリモコン信号にエンコードしてArduinoから送信する。

AQUOSリモコン GB079PA の選局 ^ ボタンを受信してVCDファイルに記録した。

vcdファイルからリモコン信号を取り出す

家電協フォーマットと呼ばれるものらしいとインターネットで既に聞いていた。 on時間とoff時間を組みにすると、1信号は0信号の2倍の長さになる。01の列を4つずつ16進で表記すると選局 ^ ボタンは 55 5A F1 0C B1 8F になった。

Arduino Uno で赤外線リモコン信号を送信する

シリアル通信でHEX文字列を送るとそれをリモコン信号として送信するプログラムを書いた。 55 5A F1 0C B1 8F を送信してみた、成功した。

サービスモードに入れなかった

AA 5A 8F 30 F5 01 を「システム動作テスト」画面で送信してみたけど反応なしだった。 他にネットで見付けた AA 5A 8F 30 F5 11、AA 5A 8F 30 F5 11、AA 5A 8F 30 F5 21、AA 5A 8F 31 F5 11、AA 5A 8F 32 F5 21 も送信してみたけどダメだった。

簡単にできるならと思ってやってみたけど、雑にやってもできなかったのであきらめた。HDDのパーティションとかの問題かもしれないし、リモコンのコマンドが違ってたのかもしれない。

そもそも、リモコンコードが書かれたやってみた記事はどれも、特定のリモコンkit向けプログラムへの入力として挙げられていた。コードの解釈を間違えたのかもしれない。

とりあえず、ここまでやってあきらめた。

WSL2でrustup docしたらWindowsのChromeが開くようにした

WSL2でディストリビューションDebian。 rustup docしてドキュメント(documentation)を読みたいがエラーメッセージが表示されるだけ。

ドキュメントをブラウザで開く

  • rustup docでは openerによって環境変数BROWSERに設定されたコマンドがHTMLファイルパスを引数にして呼ばれる
  • explorer.exeにHTMLファイルのパスを渡せばChromeで表示される
  • rustup docから渡されるファイルパスはWSL内部でのパスなので、wslpathコマンドでWindowsパスに変換する
rustup doc

rustup docはローカルのHTMLファイルをWebブラウザーで開こうとする。 rustup docはopenerを使ってHTMLファイルを開く。

openerは渡されたファイルパスまたはURLを環境に合せた方法で開く。 openerはシステムのxdg-open、なければopenerが内部で持つxdg-openスクリプトを呼ぶ。 xdg-openは環境変数BROWSERでWebブラウザーを起動するコマンドを指定できる。

HTMLファイルを開く

WindowsエクスプローラーでHTMLファイルをダブルクリックするとChromeで開くようになってた。 たぶん既定のウェブブラウザが開くように設定されてるから。 WSLからHTMLファイルパスを引数にexplorer.exeを呼べばWindowsのデスクトップ側でHTMLファイルが読み込またChromeが表示される。 ただし、explorer.exeにはWindowsのパスを渡す必要がある。

やったこと

#!/bin/sh

/mnt/c/Windows/explorer.exe $(wslpath -w "$@")

exit 0
  • export BROWSER=open-rustup-doc
その他
  • wslpath -w にURLを渡すとエラーになるので、URLを「開く」に対応するには引数が「"https://"や"http://"で始まったら」などの分岐が必要になる
  • rustup docを呼んだ場合のHTMLファイルのパスは ~/.rustup/toolchains/<toolchain name>/share/doc/rust/html/index.html
  • rustup doc --path はHTMLファイルのパスを表示できる
  • Google Cloud Shellでは、BROWSER=echoだった
  • xdg-openではスペースが含まれた環境変数BROWSERの値に対応していない
  • インストールしたままの~/.profileには、~/binディレクトリがあればPATHに含めるように書かれている