Forwarded HTTP フィールドの構文
RFC 7239: Forwarded HTTP Extension という仕様書で Forwarded
HTTP フィールドが定義されています。この HTTP フィールドは、それまで X-Forwarded-For
、X-Forwarded-By
、X-Forwarded-Proto
などの非標準 HTTP フィールドを使って実現していたことを標準化するためのものです。
しかしながら、プログラム内で利用しようとすると、Forwarded
HTTP フィールドは X-Forwarded-*
HTTP フィールド群よりも扱いづらいです。というのは、フィールド値の構文が意外と複雑であり、パースする処理を書くのが面倒だからです。
次の図は Forwarded
HTTP フィールドの構文を表しています。
正規表現処理でパースできそうだと思われるかもしれませんが、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 定義の例
/*
* 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 定義の例
/*
* 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 です。
<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 からの抜粋)
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の紹介』をお読みください。
ではまた!