ぱいぱいにっき

Pythonが好きすぎるけれど、今からPerlを好きになりますにっき

GitHub AppsでPithubを使うためのモジュールGitHub::Apps::Authと使った黒魔術の紹介

こんにちは、おげんきですか。最近体がバキバキなので良い整体を探しております。川崎近辺でお願いします。

この記事は、Perl Advent Calendar 2019の14日目の記事です。13日目はomokawa_yasuさんのTie::Fileで大容量ファイルを処理する - Qiitaでした。今回もtieの話を少しします。

GitHub Appsって何? なんで使いたいの?

同日の会社のAdvent Calendarに記事を書いたのでこれを読んでほしい。

techblog.kayac.com

つまり、要約すると、

  • GitHub APIを叩くときに、管理とか諸々の理由で個人のGitHub Token使うやつから、GitHub AppsのTokenを使いたい
  • 個人のGitHub Tokenと違ってGitHub AppsのTokenは1時間で有効期限が切れる
    • よりセキュアであると言えるし、たぶん用途としてはWebhook来たときにワンショットで処理する感じだから、Botみたいにこちらから能動的にやるみたいなのはメインの用途ではないのでは?
    • そもそも永続的な認証トークン自体は寛大すぎた
  • 何かしらのテクで既存のGitHub APIを使うライブラリを使ってBotが動くときもコードの変更をそこまでせずにGitHub Appsに移行したい

という内容です。

最近流行りのGitHub Actionsを使えば、対象のリポジトリを操作するためのGitHub Tokenが環境変数で降ってくるので、こういう苦労はないかもしれないですね。ただ、さらにそこから別のリポジトリを触るとかすると、必要な話に。

PithubでのGitHub Tokenの扱い

Perl製のBotGitHub APIを叩くときに、Pithubを使っております。

metacpan.org

他にもPerl界ではNet::GitHubってのもあります。しかしここでは、Pithubに焦点を絞ってお話します。

public repositoryのread以外の操作を行う場合はGitHub Tokenが必要です。というわけで、Pithubのコンストラクタでは以下のように、GitHub Tokenを渡して皆さん操作します。

my $pit = Pithub->new(
  user  => 'plu',
  repo  => 'pithub',
  token => 'my_oauth_token',
);

(SYNOPSISより抜粋)

ここでtokenは文字列として渡しています。しかし、Perlにおいて文字列は変なことをしない限り不変なもの。GitHub Appsで要求されるような「1時間毎にAPI叩いて更新する」みたいなのはできないわけです。

では頑張って「変なこと」をしていきましょう。

変なこと案その1 tie

みんな大好きtie変数です。Perl Mongerの半分ぐらいがtie期の麻疹にかかると言われています。僕も今回1日ほどかかりました。

perldoc.jp

tie変数とは、Perlのプリミティブ型の変数として振る舞いながら、アクセス・代入などの機構をPerlコード上で自作する仕組みです。

今回は、変数がアクセスされるたびに、

  • GitHub Tokenを変数内部に持っていない
    • APIを叩いてGitHub Tokenを取得しキャッシュしGitHub Tokenを返す
  • キャッシュしているGitHub Tokenを内部に持っている
    • 有効期限が切れてなければそのまま返す
    • 切れていれば再びAPIを叩いてGitHub Tokenを取得してキャッシュし返す

という機構を作ろうと考えました。

今回はスカラ変数でtie変数を作りたいと考えたので、Tie::Scalarを継承してpackageを作ります。このpackageは上記の機構をObjectで管理するクラスを受け取ってtie変数として利用できるようにします。

package GitHub::Apps::Auth {
    sub new { ... }

    # issued_tokenは有効期限を考慮しつつGitHub Tokenを返すメソッドです
    sub issued_token { ... }
}

package GitHub::Apps::Auth::Tie {
    require Tie::Scalar;
    our @ISA = qw/Tie::StdScalar/;

    sub FETCH {
         my $self = shift;
         return $self->issued_token;
    }
}

そして、これをtieでtie変数にします。

my $auth = GitHub::Apps::Auth->new(...);

tie $auth, "GitHub::Apps::Auth::Tie";

すると、このあと$authは他の変数に代入するたびに、有効期限を考慮したvalidなtokenを常に返してくれるようになります!

my $token1 = $auth;
sleep 3600;
my $token2 = $auth; # $token1のトークンは有効期限が切れているので違うトークンが返ってくる。

いや〜〜ヘンtieですね〜〜〜〜。

あ、すみません今のナシ。

つまり、これで我々は勝ったか!?に思えました。が、これでは要件満たさないのです。

触った瞬間に文字列になるtie変数

ところで今まで言ってたアクセスってなんでしょうか。他の変数への代入だとか、関数に対して引数に渡すときにも変数のアクセスが行われます。では上記のtie変数をPithubで使ってみましょう。

my $pit = Pithub->new(
  user  => 'plu',
  repo  => 'pithub',
  token => $auth,
);

これで良さそう・・・? ところでこの名前付き引数でコンストラクタにtie変数を渡したときもアクセスが行われます。アクセスが行われるということは......つまりPithubに渡されるのはただ文字列トークンであるということです。

いや〜〜これは欲しいものではなかった! ほしいのは外面は文字列として振る舞いながら、中はいい感じにメソッドが叩かれていいかんじにその時々最適な文字列を返すやつ!

もうちょっと詳しく言うと、tie変数は値を収める変数に対して魔法をかけるものであって、今回ほしいのは変数の中身である値のほうに魔法をかける物が欲しいのでした。

変なこと案その2 overload

演算子オーバーロードPerlに限らず様々なオブジェクト指向言語に存在する機構です。しかしそのトリッキーな挙動から、多くの言語では黒魔術扱いされているでしょう。Perlも例外では有りません。ただ、他の黒魔術に比べると、見た目のえげつなさは抑えられているかも。

perldoc.jp

普通、演算子といえば四則演算や、比較演算子を思い浮かべるでしょう。しかし、Perlは変数を文字列として評価する際の""ダブルクオートも演算子であり、オーバーロードが可能です。その他、文字列比較演算子eqや文字列結合演算子.なども含めてオーバーロードすれば、文字列として自然に扱えるオブジェクトを作成することが出来るでしょう。

ではやってみましょう。

package GitHub::Apps::Auth {
use overload
    "\"\"" => sub { shift->issued_token },
    "." => sub {
        my $self = shift;
        my $other = shift;
        my $reverse = shift;

        return $reverse ? $other . $self->issued_token : $self->issued_token . $other;
    },
    "eq" => sub { shift->issued_token eq shift };

    sub new { ... }

    sub issued_token { ... }
}

そんでって、Pithubに突っ込む!

my $auth = GitHub::Apps::Auth->new(...);
my $pit = Pithub->new(
  user  => 'plu',
  repo  => 'pithub',
  token => $auth,
);

これで、GitHub::Apps::Authインスタンスは、文字列として振る舞うので、Pithubの内部でもObjectとして保持されるものの、いざ使われるときはそのときに使われるトークンを払い出します。結論を言えば、CPANに上げているGitHub::Apps::Authはこの方式を採用しています。

勝った・・・! Pithubでもちゃんと使えるのと、1時間以上経ったら新しいtokenが使われているのを確認しております。

その他の工夫

上記のoverloadの定義だと、文字列結合をした際には「常に有効なtokenを返す不思議な文字列」としての性質を失ってしまいます。なので、文字列結合のメソッドはもう少し工夫をしています。

use overload
    "." => sub {
        my $self = shift;
        my $other = shift;
        my $reverse = shift;
 
        $other = "" unless defined $other;
 
        my $new_self = bless {}, ref $self;
        %$new_self = %$self;
 
        $reverse ?
            $new_self->_prefix($other . $new_self->_prefix) :
            $new_self->_suffix($new_self->_suffix . $other);
        return $new_self;
    };

クラスのattributeとしてprefixとsuffixの入れ物を用意し、文字列結合をしようとした際は、objectをcloneした上で、新しいobjectに結合を試みた文字列を入れています。

そして、tokenを返すときに、prefixとsuffixをその場で結合して返しています。

sub issued_token {
    my $self = shift;
 
    if ($self->_is_expired_token) {
        return $self->_prefix . $self->_fetch_access_token . $self->_suffix;
    }
 
    return $self->_prefix . $self->token . $self->_suffix;
}

この工夫で例えば my $header = "Bearer x-access-token:" . $auth;のように文字列結合をした場合でも、GitHub Tokenの部分は1時間ごとに入れ替わります。

この措置は、GitHub APIクライアント側で認証のために使うHTTPヘッダをあらかじめ組み立てて、それを使い回すようなケースを想定して実装しています。

まとめ

  • Pithubとかに渡すGitHub Tokenと入れ替えるだけでGitHub AppsのTokenが使えるモジュールを書いたよ
    • CPANにすでに上げているよ
  • 中身はoverloadっていう黒魔術を使っているよ
    • あんまりやりすぎると制御が効かなくなるよ、たぶん
  • patches welcomeだよ https://github.com/mackee/GitHub-Apps-Auth
  • PithubにPull Request送るのも試してみますね

明日はbayashi_netさんです。