0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HTTPAdvent Calendar 2024

Day 9

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

Last updated at Posted at 2024-12-08

Forwarded HTTP フィールドの構文

RFC 7239: Forwarded HTTP Extension という仕様書で Forwarded HTTP フィールドが定義されています。この HTTP フィールドは、それまで X-Forwarded-ForX-Forwarded-ByX-Forwarded-Proto などの非標準 HTTP フィールドを使って実現していたことを標準化するためのものです。

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

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

forwarded_http_field_syntax.png

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

また、Forwarded HTTP フィールドの構文は RFC 8941: Structured Field Values for HTTP で定義されている汎用構文とも異なるため、汎用ライブラリも利用できません。

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

Forwarded HTTP フィールドの解析

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

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

Lexer 定義の例
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

Parser 定義の例
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

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

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

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

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 に挙げられている Forwarded HTTP フィールド値を authlete/http-field-parser ライブラリを用いてパースする例です。(テストコード ForwardedFieldValueTest.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 で定義されている @target-uri derived component (Section 2.2.2. Target URI) の値をリバースプロキシの後ろで求める方法の一つとして Forwarded HTTP フィールドを利用したかったからです。(RequestUriResolver.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 の Certification Team に提供するためです。詳細は『FAPI 2.0 HTTP Signingの紹介』をお読みください。

ではまた!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?