見出し画像

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追記

ソースコードを一部修正(コメントで当該箇所を表記)

この記事が気に入ったらサポートをしてみませんか?