前回の
Perlでの実装
それでは、
ActivityPubはWeb APIベースのプロトコルですので、
AS2オブジェクトモデルの作成
Actubでは、
個々のオブジェクトを表現するクラスを作成する前に、
package WWW::ActivityPub::Base;
use strict;
use warnings;
use Class::Tiny;
sub TO_JSON {
my %ret;
my $self = shift;
for (keys %$self){
my $key = $_;
my $jsonkey = $key;
if($key eq 'context'){
$jsonkey = '@context';
if(!defined $$self{$_}){next;}
}
$ret{$jsonkey} = $$self{$_};
}
return \%ret;
}
1;
ここではClass::Tiny
モジュールを用いています。これは属性のゲッタとセッタを設定するだけのシンプルなモジュールですが、@context
属性があり、@
はメソッド名に使えないため、@context
ではなくcontext
属性として定義し、context
属性を@context
に変換して出力するためのTO_
関数を定義しています。
この下準備により、Follow
クラスの定義は次のようにシンプルなものになっています。
package WWW::ActivityPub::Follow;
use strict;
use warnings;
use parent qw(WWW::ActivityPub::Base);
use Class::Tiny qw(
context id type object
);
1;
MIME型の登録
GETリクエストに対する返却処理は通常のレスポンスなので実装上特筆することはあまりありませんが、application/
を使う必要があるため、startup
フック内で次のようにしてこのMIME型を登録しておきます。
sub startup {
my $self = shift;
...;
$self->types->type(as =>
'application/ld+json; profile=' .
'"https://www.w3.org/ns/activitystreams"');
}
これにより、
# $outは出力するデータ
$self->render(text => $out,
format => 'as');
送信キューの登録
Actubは送信処理を非同期で行う実装となっているので、Follow
オブジェクトが送信されると、Accept
オブジェクトをジョブキューに追加します。ここでのジョブキューシステムはJonk
モジュールを使っています。これはデータベースをバックエンドに使う軽量なジョブキューシステムで、
# $dbhはデータベースハンドル
my $jonk = Jonk->new($dbh);
...;
my $job_id = $jonk->insert('post', $queuestr);
送信処理
Actubでは、
データのキューからの取り出し
スクリプトが起動されると、
my $dbhj = DBI->connect(
"dbi:SQLite:dbname=actub_job.sqlite","","");
my $jonk = Jonk->new($dbhj =>
{functions => [qw/post/]}) or die;
my $job = $jonk->find_job;
if (defined $job) {
# 送信処理
do_post($job->arg);
}
HTTP Signaturesによる署名
ActivityPubのオブジェクトを送信する際には、rsa-sha256
と呼ばれる方法です。rsa-sha256
では次の手順で署名します。
- 署名対象データをSHA256アルゴリズムでハッシュ化する
- ハッシュをRSAアルゴリズムで署名する
- 署名をBase64形式でエンコードする
HTTP Signaturesでは署名対象データも選択できますが、Date
フィールドを対象としています。
通常、post
メソッドに必要な引数を与える形で実装します。しかし今回はHTTPリクエストヘッダのDateフィールドの値に署名をする必要があるため、
my $contenttype = 'application/ld+json; ' .
'profile="https://www.w3.org/ns/activitystreams"';
# リクエストオブジェクトを作成
my $req = POST(
$url,
'Content-Type' => $contenttype,
Content => $content
);
# Dateフィールドに現在時刻を設定
$req->headers->date(time);
# Dateフィールドの文字列表現を取得
my $date = $req->headers->header('date');
# 取得した文字列表現に署名するメソッドを呼び出し
my $sign = Actub::Signature::sign('date: ' . $date);
# 署名した結果を仕様が求める形に整形
my $signature = sprintf
'keyId="%s",algorithm="rsa-sha256",signature="%s"',
$from, $sign;
# 署名した結果をSignatureヘッダに設定
$req->headers->push_header(
Signature => $signature);
# リクエストを実行
my $res = $ua->request($req);
署名処理本体はCrypt::OpenSSL::RSA
モジュールを使って次のように行います。
sub sign {
my ($data) = shift;
# 秘密鍵を取得
my $pk = get_pk();
# 署名
my $key =
Crypt::OpenSSL::RSA->new_private_key($pk);
$key->use_sha256_hash();
my $s = $key->sign($data);
# Base64エンコードした文字列を返却
return encode_base64($s, "");
}
通知を受け取ったアクターは、publicKey
属性の値を使って、
コンテントネゴシエーションによる情報の提供
前述のとおりActivityPubにおいてGETメソッドで情報を取得する際には、Accept
ヘッダにapplication/
を指定することになっています。ただ、
sub entry {
my $self = shift;
...;
# 指定されたIDの短文情報を取得
my $entry = Actub::Model::Entry::read_row($dbh, $self->param('id'));
if(is_ap($self->req->headers->accept)){
...; # $outにAS2形式のデータを設定
$self->render(text => $out, format => 'as');
} else {
$self->render(template => 'entry', e => $entry);
}
}
# AS2を要求しているかのチェック
sub is_ap {
my $arg = shift // '';
my (@params) = split /,/, $arg;
for(@params){
s/^ +//;
s/ +$//;
if($_ eq 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ||
$_ eq 'application/activity+json'){ return 1; }
}
return 0;
}
is_
関数は、application/
が指定された場合もAS2形式の情報を返すべき
<続きの
本誌最新号をチェック!
WEB+DB PRESS Vol.130
2022年8月24日発売
B5判/
定価1,628円
ISBN978-4-297-13000-8
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現! - 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう - 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、 NFT