Forwarded HTTPフィールドの構文と解析

# Forwarded HTTP フィールドの構文

[RFC 7239: Forwarded HTTP Extension](https://www.rfc-editor.org/rfc/rfc7239.html) という仕様書で `Forwarded` HTTP フィールドが定義されています。この HTTP フィールドは、それまで `X-Forwarded-For`、`X-Forwarded-By`、`X-Forwarded-Proto` などの非標準 HTTP フィールドを使って実現していたことを標準化するためのものです。

しかしながら、プログラム内で利用しようとすると、`Forwarded` HTTP フィールドは `X-Forwarded-*` HTTP フィールド群よりも扱いづらいです。というのは、フィールド値の構文が意外と複雑であり、パースする処理を書くのが面倒だからです。

次の図は `Forwarded` HTTP フィールドの構文を表しています。

![forwarded_http_field_syntax.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/981c2e7a-676b-9785-4529-0085ed303420.png)

正規表現処理でパースできそうだと思われるかもしれませんが、quoted-string の中にカンマやセミコロンなどの区切り文字、加えてエスケープシーケンス (図中の quoted-pair) が含まれる可能性があるので、不可能ではないにしても正規表現処理でパースするのは難しいです。

また、`Forwarded` HTTP フィールドの構文は [RFC 8941: Structured Field Values for HTTP](https://www.rfc-editor.org/rfc/rfc8941.html) で定義されている汎用構文とも異なるため、汎用ライブラリも利用できません。

これらの理由により、`Forwarded` HTTP フィールドの値を利用しようとすると、それ専用のパース処理を書かなければなりません。

# Forwarded HTTP フィールドの解析

フィールド値を一文字ずつ読みながらパースする処理を手作業で書くこともできますが、「フィールド値を小さなコンピュータ言語とみなし、パーサジェネレータに `Forwarded` HTTP フィールド用のパーサを生成させる」という方法もあります。

私が `Forwarded` HTTP フィールドの値をパースする処理を書いたときはパーサジェネレータを使う方法を取りました。以下に、パーサジェネレータ [ANTLR](https://www.antlr.org/) に渡す Lexer 定義と Parser 定義の例を紹介します。

<details>
<summary><b><font color="green">Lexer 定義の例</font></b></summary>

```g4:AntlrHttpFieldLexer.g4
/*
 * Copyright (C) 2024 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
lexer grammar AntlrHttpFieldLexer;


COMMA     : ',';
SEMICOLON : ';';
EQUALS    : '=';


//-------------------------------------------------------------------
// Core Rules
//
//   RFC 5234: Augmented BNF for Syntax Specifications: ABNF
//   B.1. Core Rules
//
//       ALPHA  = %x41-5A / %x61-7A  ; A-Z / a-z
//       DIGIT  = %x30-39            ; 0-9
//       DQUOTE = %x22               ; double quote
//       HTAB   = %x09               ; horizontal tab
//       SP     = %x20               ; space
//       VCHAR  = %x21-7E            ; visible (printing) characters
//
//-------------------------------------------------------------------


//-------------------------------------------------------------------
// Field Values
//
//   RFC 9110: HTTP Semantics
//   5.5. Field Values
//
//       obs-text = %x80-FF
//
//-------------------------------------------------------------------


//-------------------------------------------------------------------
// Token
//
//   RFC 9110: HTTP Semantics
//   5.6.2. Tokens
//
//       token = 1*tchar
//
//       tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
//             / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
//             / DIGIT / ALPHA
//
//-------------------------------------------------------------------
TokenCharacterSequence
    : TokenCharacter+
    ;

fragment
TokenCharacter
    : [!#$%&'*+.^_`|~0-9A-Za-z-]
    ;


//-------------------------------------------------------------------
// Whitespace
//
//   RFC 9110: HTTP Semantics
//   5.6.3. Whitespace
//
//       OWS = *( SP / HTAB )
//           ; optional whitespace
//       RWS = 1*( SP / HTAB )
//           ; required whitespace
//       BWS = OWS
//           ; "bad" whitespace
//
//-------------------------------------------------------------------
WhiteSpaceCharacterSequence
    : WhiteSpaceCharacter+
    ;

fragment
WhiteSpaceCharacter
    : [ \t]
    ;


//-------------------------------------------------------------------
// Quoted Strings
//
//   RFC 9110: HTTP Semantics
//   5.6.4. Quoted Strings
//
//       quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
//       qdtext        = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
//       quoted-pair   = "\" ( HTAB / SP / VCHAR / obs-text )
//
//-------------------------------------------------------------------
fragment
QuotedStringCharacterSequence
    : QuotedStringCharacter+
    ;

fragment
QuotedStringCharacter
    : [\t \u0021\u0023-\u005B\u005D-\u007E\u0080-\u00FF]
    | '\\' [\t \u0021-\u007E\u0080-\u00FF]
    ;

// Mode switcher for Quoted String
QuotedStringOpen
    : '"' -> pushMode(INSIDE_QUOTED_STRING)
    ;

// Mode for Quoted String
mode INSIDE_QUOTED_STRING;

QuotedStringClose
    : '"' -> popMode
    ;

QuotedStringContent
    : QuotedStringCharacterSequence
    ;

```

最新のソースコードはこちら → [AntlrHttpFieldLexer.g4](https://github.com/authlete/http-field-parser/blob/main/src/main/antlr4/com/authlete/http/antlr/AntlrHttpFieldLexer.g4)
</details>


<details>
<summary><b><font color="green">Parser 定義の例</font></b></summary>

```g4:AntlrHttpFieldParser.g4
/*
 * Copyright (C) 2024 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
parser grammar AntlrHttpFieldParser;


options {
    tokenVocab=AntlrHttpFieldLexer;
}


//-------------------------------------------------------------------
// Token
//
//   RFC 9110: HTTP Semantics
//   5.6.2. Tokens
//
//       token = 1*tchar
//
//       tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
//             / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
//             / DIGIT / ALPHA
//
//-------------------------------------------------------------------
token
    : TokenCharacterSequence
    ;

tokenWithEOF
    : token EOF
    ;


//-------------------------------------------------------------------
// Whitespace
//
//   RFC 9110: HTTP Semantics
//   5.6.3. Whitespace
//
//       OWS = *( SP / HTAB )
//           ; optional whitespace
//       RWS = 1*( SP / HTAB )
//           ; required whitespace
//       BWS = OWS
//           ; "bad" whitespace
//
//-------------------------------------------------------------------
whiteSpace
    : WhiteSpaceCharacterSequence
    ;


//-------------------------------------------------------------------
// Quoted String
//
//   RFC 9110: HTTP Semantics
//   5.6.4. Quoted Strings
//
//     quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
//     qdtext        = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
//     quoted-pair   = "\" ( HTAB / SP / VCHAR / obs-text )
//
//-------------------------------------------------------------------
quotedString
    : QuotedStringOpen quotedStringContent* QuotedStringClose
    ;

quotedStringWithEOF
    : quotedString EOF
    ;

quotedStringContent
    : QuotedStringContent
    ;


//-------------------------------------------------------------------
// RFC 7239: Forwarded HTTP Extension
//
//   RFC 7239: Forwarded HTTP Extension
//   4. Forwarded HTTP Header Field
//
//     Forwarded   = 1#forwarded-element
//
//     forwarded-element =
//         [ forwarded-pair ] *( ";" [ forwarded-pair ] )
//
//     forwarded-pair = token "=" value
//     value          = token / quoted-string
//
//     token = <Defined in [RFC7230], Section 3.2.6>
//     quoted-string = <Defined in [RFC7230], Section 3.2.6>
//
//-------------------------------------------------------------------
forwardedFieldValue
    : forwardedElement (whiteSpace* COMMA whiteSpace* forwardedElement)*
    ;

forwardedFieldValueWithEOF
    : forwardedFieldValue EOF
    ;

forwardedElement
    : forwardedPair (whiteSpace* SEMICOLON whiteSpace* forwardedPair)*
    ;

forwardedElementWithEOF
    : forwardedElement EOF
    ;

forwardedPair
    : forwardedPairName whiteSpace* EQUALS whiteSpace* forwardedPairValue
    ;

forwardedPairWithEOF
    : forwardedPair EOF
    ;

forwardedPairName
    : token
    ;

forwardedPairNameWithEOF
    : forwardedPairName EOF
    ;

forwardedPairValue
    : token
    | quotedString
    ;

forwardedPairValueWithEOF
    : forwardedPairValue EOF
    ;
```

最新のソースコードはこちら → [AntlrHttpFieldParser.g4](https://github.com/authlete/http-field-parser/blob/main/src/main/antlr4/com/authlete/http/antlr/AntlrHttpFieldParser.g4)
</details>

# Forwarded HTTPフィールド用パーサを含むライブラリ

前節で紹介した方法で生成した `Forwarded` HTTP フィールド用パーサは **[authlete/http-field-parser](https://github.com/authlete/http-field-parser)** ライブラリ (Java) に含まれています。

`pom.xml` ファイルに次の dependency を追加することでライブラリを利用できます。本記事公開時点 (2024 年 12 月上旬) のライブラリの最新バージョンは 1.0 です。

```xml:pom.xmlに追加するdependency
<dependency>
    <groupId>com.authlete.http</groupId>
    <artifactId>http-field-parser</artifactId>
    <version>${http-field-parser.version}</version>
</dependency>
```

次のコードは、RFC 7239 の [Section 4. Forwarded HTTP Header Field](https://www.rfc-editor.org/rfc/rfc7239.html#section-4) に挙げられている `Forwarded` HTTP フィールド値を authlete/http-field-parser ライブラリを用いてパースする例です。(テストコード [ForwardedFieldValueTest.java](https://github.com/authlete/http-field-parser/blob/main/src/test/java/com/authlete/http/ForwardedFieldValueTest.java) からの抜粋)

```java:Forwarded HTTPフィールドの値をパースする例
public void test_parse_multiple_elements()
{
    ForwardedFieldValue ffv = ForwardedFieldValue.parse(String.join(", ",
            "for=\"_gazonk\"",
            "For=\"[2001:db8:cafe::17]:4711\"",
            "for=192.0.2.60;proto=http;by=203.0.113.43",
            "for=192.0.2.43;host=example.com"
    ));

    assertNotNull(ffv);
    assertEquals(4, ffv.size());

    // 0
    assertEquals("_gazonk", ffv.get(0).getFor());

    // 1
    assertEquals("[2001:db8:cafe::17]:4711", ffv.get(1).getFor());

    // 2
    assertEquals("192.0.2.60",   ffv.get(2).getFor());
    assertEquals("http",         ffv.get(2).getProto());
    assertEquals("203.0.113.43", ffv.get(2).getBy());

    // 3
    assertEquals("192.0.2.43",  ffv.get(3).getFor());
    assertEquals("example.com", ffv.get(3).getHost());
}
```

# おわりに

`Forwarded` HTTP フィールドのパース処理を書いた理由は、[RFC 9421: HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421.html) で定義されている `@target-uri` derived component ([Section 2.2.2. Target URI](https://www.rfc-editor.org/rfc/rfc9421.html#section-2.2.2)) の値をリバースプロキシの後ろで求める方法の一つとして `Forwarded` HTTP フィールドを利用したかったからです。([RequestUriResolver.java](https://github.com/authlete/authlete-java-jaxrs/blob/master/src/main/java/com/authlete/jaxrs/util/RequestUrlResolver.java) 参照)

なぜ `@target-uri` derived component の値を求める必要があるかというと、**FAPI 2.0 HTTP Signing** 仕様が `@target-uri` を HTTP 署名処理の入力として用いることを要求しているからです。

なぜ FAPI 2.0 HTTP Signing 仕様の実装を急いだかというと、FAPI 2.0 HTTP Signing の実装を [OpenID Foundation](https://openid.net/) の Certification Team に提供するためです。詳細は『[FAPI 2.0 HTTP Signingの紹介](https://qiita.com/TakahikoKawasaki/items/f57aebbffb3859b19fb4)』をお読みください。

ではまた！