DOAP2023開発日記 #9
最初のリクエスト&レスポンス
#8でインストールに成功した。
次の目標はリクエストとレスポンスだ。
DOAPにだけ理解できるリクエストを送り、DOAPでレスポンスを返して、ブラウザに表示する。
// main.cpp
#include "doap_global.h"
#include <dsapi.h>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QVariantMap>
#include <QJsonDocument>
#include <QTextStream>
/**
* @brief フィルターアドインインストール時に呼ばれる関数
* @param pInitData 初期化データ構造体へのポインタ
* @return インストール結果を渡す
*/
DOAP_EXPORT uint FilterInit(FilterInitData *pInitData) {
pInitData->appFilterVersion = kInterfaceVersion;
// ※1
pInitData->eventFlags = kFilterParsedRequest;
qstrcpy(pInitData->filterDesc, "DOAP2023");
return kFilterHandledEvent;
}
/**
* @brief イベントごとにフックされるエキスポート関数
* @param pContext DSAPIコンテキストへのポインタ
* @param eventType イベントタイプ
* @param ptr イベントタイプごとのデータへのポインタ
* @return 処理済みならkFilterHandledEvent、未処理ならkFilterNotHandledを渡す
*/
DOAP_EXPORT uint HttpFilterProc(
FilterContext *pContext,
uint eventType,
void *ptr
) {
if (eventType == kFilterParsedRequest) { // ※2
uint errId = 0;
FilterRequest request;
if (pContext->GetRequest(pContext, &request, &errId) // ※3
&& errId == 0) {
// 2022.8.11 修正ここから
auto path = QString(request.URL).replace('+', ' ');
auto dummyHost = QString("http://dummy%1").arg(path);
QUrl url(dummyHost); // ※4
path = url.path();
// 2022.8.11 修正ここまで
if (path == "/doap") { // ※5
// ※6
QUrlQuery queries(url);
QVariantMap map {{"Hello!", queries.queryItemValue("name")}};
auto json = QJsonDocument::fromVariant(map);
// ※7
QString buffer;
QTextStream s(&buffer, QIODevice::WriteOnly);
s << QString("%1 200 OK").arg(request.version) << endl
<< "Content-Type: application/json; charset=utf-8" << endl
<< endl;
s.flush();
auto utf8 = buffer.toUtf8() + json.toJson();
if (pContext->WriteClient( // ※8
pContext,
utf8.data(),
utf8.size(),
0,
&errId
) && errId == 0) {
return kFilterHandledEvent; // ※9
}
}
}
}
return kFilterNotHandled; // ※10
}
まず、FilterInit関数でイベント「kFilterParsedRequest」のみフィルタリングするようにする(※1)。
「kFilterParsedRequest」イベントは、リクエストヘッダーが確定した状態である。ちなみに、この前の段階に「kFilterRawRequest」イベントがある。このイベントでフィルタリングすると、リクエストヘッダーが確定する前段階の状態であるため、リクエストヘッダーをプリプロセスする(言わば改ざんする)ことができる。
次に、HttpFilterProc関数でも、eventTypeを「kFilterParsedRequest」のみ感知するIf-Then構造を作る(※2)。
リクエストの情報を取得するには、FilterContextのGetRequest関数を使って、FilterRequest構造体データとして受け取る(※3)。
受け取ったリクエストデータを使って、QUrlオブジェクトを作成する(※4)。
QUrlのpath部分が「/doap」となっていたら、DOAPに対するリクエストであると解釈する(※5)。
DOAPリクエストと判断したら、クエリからnameの値を取り出し、JSONデータを構築する(※6)。
続けて、レスポンス用のテキストデータを構築する(※7)。
レスポンスデータができたら、FilterContextのWriteClient関数を使って書き込み(※8)、イベントを正しく処理したという値を返して処理を終える(※9)。それ以外の結果だったら、処理していないという値を返す(※10)。
FilterRequest構造体
前述の※3で受け取ったFilterRequestは、次のような構造を持つ。
typedef struct {
unsigned int method;
char* URL;
char* version;
char* userName;
char* password;
unsigned char* clientCert;
unsigned int clientCertLen;
char* contentRead;
unsigned int contentReadLen;
} FilterRequest;
これらは、Httpリクエストの最初の行(例: GET /sales.nsf?OpenDatabase HTTP/1.1)と付随情報が含まれる。
methodには、GETやPOSTなどのHttpリクエストに紐付く定数(kRequestGETやkRequestPOSTなど)が入る。
URLには、Httpリクエストの1行目にあるURL部の文字列ポインタが入る。
versionには、リクエストのHttpプロトコルバージョン(たいてい「HTTP/1.0」や「HTTP/1.1」)という文字列ポインタが入る。
userNameとpasswordには、リクエストに含まれるユーザ名とパスワードが入る。
clientCert/clientCertLenには、SSLクライアント証明書のバイト列とその長さが含まれる。
contentRead/contentReadLenには、リクエストボディ部の本体へのポインタとその長さが入っている。たいていの場合、POSTなどのリクエストデータが含まれる。
WriteClient関数
前述の※8で使用したWriteClient関数は、アドインが直接レスポンス情報を返したい際に使用する。
typedef struct _FilterContext {
// 中略
int ( *WriteClient )( struct _FilterContext *pContext,
char *pBuffer,
unsigned int bufferLen,
unsigned int reserved,
unsigned int *pErrID );
// 中略
} FilterContext;
pContextには、現在試用しているFilterContextのポインタをそのまま入れる。
pBuffer/bufferLenには、出力したいデータとその長さを入れる。
reservedには0を入れる。
pErrIDには、エラーコードを格納する符号なし整数へのポインタを入れる。
この関数を使う時のポイントは、非常に原始的な関数なので、Httpレスポンスのすべてを自身で面倒を見ないといけないところと、出力データを追記できないところだろう。
Httpレスポンスは、ステータスライン(Httpプロトコル、レスポンスコードとフレーズをスペースで区切った1行)、レスポンスヘッダー、空行、レスポンスボディで構成されるが、アドインは自身でレスポンスを返す場合、これらを正しく構成して出力する責任を負う。
また、レスポンスしたいデータがあっても、ストリームのように後から追記することはできない。一度にステータスラインからレスポンスボディまで一括出力する必要がある。
実行例
ここまでのコードでDOAPをビルドし、Dominoにインストールする。/doap?name=Taro%20Yamadaとリクエストすると、次のようになる。
まとめ
リクエスト情報(FilterRequest)とレスポンス出力(WriteClient)は、どちらも原始的な扱い方しかできない。できればクラスなどを定義して、扱いやすくしたいところだろう。
2022.8.11追記
ソースコードを一部修正(コメントで当該箇所を表記)