1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
use crate::decoder::HttpHead;
use crate::error::EncodeError;
use crate::validate::{
is_valid_field_value, is_valid_header_name, is_valid_method, is_valid_protocol_version,
is_valid_request_target,
};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
/// HTTP リクエスト
///
/// `body` フィールドは「ボディなし」と「明示的な空ボディ」を区別する。
/// - `None`: ボディを送る意図がない (`Content-Length` を自動付与しない)
/// - `Some(vec![])`: 明示的に空ボディ (`Content-Length: 0` を自動付与)
/// - `Some(data)`: 通常のボディ (`Content-Length: N` を自動付与)
///
/// 全フィールドは非公開で、構築時バリデーション付きの `new` / `with_version` /
/// `header` / `add_header` / `set_header` 経由でのみ操作できる。`#[non_exhaustive]`
/// により、将来のフィールド追加 (例: `trailers`) は破壊的変更にならない。
///
/// # 構築時バリデーションのスコープ
///
/// - `method`: RFC 9110 Section 9.1 / 5.6.2 の token (1*tchar) に違反する文字を拒否
/// - `uri`: RFC 9112 Section 3.2 の request-target として CRLF / NUL / 制御文字 /
/// RFC 3986 除外文字 / 不正なパーセントエンコーディング (`%00` 含む) を拒否。
/// 加えて送信側ポリシーとして obs-text (0x80-0xFF) も拒否する
/// - `version`: `is_valid_protocol_version` で `token "/" DIGIT+ "." DIGIT+` 形式を要求
/// - ヘッダー: 名前は RFC 9110 Section 5.1 の token、値は RFC 9110 Section 5.5 の
/// field-value (CR/LF/NUL 不可) を要求
///
/// HTTP Request Smuggling (CWE-444) は CRLF / NUL の挿入で TE/CL 競合などを偽装する
/// 攻撃で、本ライブラリの `examples/http11_reverse_proxy` 等の reverse proxy 経路では
/// 致命的な脆弱性となる。構築時バリデーションは不正な Request の生成自体を防ぐ防御線である。
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Request {
method: String,
uri: String,
version: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
}
impl HttpHead for Request {
fn version(&self) -> &str {
&self.version
}
fn headers(&self) -> &[(String, String)] {
&self.headers
}
}
impl Request {
/// 新しいリクエストを作成 (HTTP/1.1)
///
/// バリデーション順序: method → uri (構文) → uri (obs-text 拒否)。
/// 失敗時は最初に検出されたエラーを返す。
///
/// `method` は RFC 9110 Section 9.1 の `method = token` (RFC 9110 Section 5.6.2) を要求する。
/// 検証には既存の `is_valid_method` (validate.rs:70) を流用する。
/// `uri` は request-target として、CRLF (RFC 9112 Section 3.2: whitespace 禁止) および
/// NUL (RFC 9110 Section 5.5: CR/LF/NUL are invalid and dangerous) を含まないことを要求する
/// (構文レベルのバリデーション)。request-target 形式 (origin/absolute/authority/asterisk)
/// の判定は encode 時の `validate_request_target_form` に委ねる。
/// 検証には既存の `is_valid_request_target` (validate.rs:179) を流用する。
/// 加えて送信側のポリシーとして obs-text (0x80-0xFF) の非含有も確認する
/// (既存の encoder.rs 側のチェックと同等の水準)。
/// obs-text 拒否も `InvalidRequestTarget { uri }` を返す (新規エラーバリアント不要)。
///
/// version は `"HTTP/1.1"` 固定のため、`is_valid_protocol_version` は呼び出さない
/// (固定値が常に検証を通過するため)。
pub fn new(method: impl Into<String>, uri: impl Into<String>) -> Result<Self, EncodeError> {
let method = method.into();
let uri = uri.into();
if !is_valid_method(&method) {
return Err(EncodeError::InvalidMethod { method });
}
if !is_valid_request_target(&uri) {
return Err(EncodeError::InvalidRequestTarget { uri });
}
// is_valid_request_target は受信側互換性のため obs-text (0x80-0xFF) を許容する。
// 送信側では新規に obs-text を生成してはならないため、ここで拒否する。
if uri.bytes().any(|b| b >= 0x80) {
return Err(EncodeError::InvalidRequestTarget { uri });
}
Ok(Self {
method,
uri,
version: "HTTP/1.1".to_string(),
headers: Vec::new(),
body: None,
})
}
/// カスタムバージョンでリクエストを作成
///
/// バリデーション順序: method → uri (構文) → uri (obs-text 拒否) → version。
/// 失敗時は最初に検出されたエラーを返す。
///
/// version は `is_valid_protocol_version` (`token "/" DIGIT+ "." DIGIT+`) で検証する。
/// RTSP バージョン (RTSP/1.0 等) も受理する。
///
/// # RFC との乖離
///
/// 本 API は RFC 9112 Section 2.3 の `HTTP-name = %s"HTTP"` (case-sensitive) を強制せず、
/// token として大文字小文字を許容する緩和形式を採用する。これは validate.rs の
/// `is_valid_protocol_version` の既存方針 (RTSP 等の互換のため token を許容) を
/// 継承するものであり、HTTP として送信する場合は呼び出し側が `"HTTP/1.1"` を渡す責務がある。
///
/// 注: DIGIT+ (1 桁以上) は RFC 7826 Section 20.3 の RTSP 対応のための拡張であり、
/// RFC 9112 Section 2.3 の `DIGIT "." DIGIT` (各 1 桁) より広い。
pub fn with_version(
method: impl Into<String>,
uri: impl Into<String>,
version: impl Into<String>,
) -> Result<Self, EncodeError> {
let method = method.into();
let uri = uri.into();
let version = version.into();
if !is_valid_method(&method) {
return Err(EncodeError::InvalidMethod { method });
}
if !is_valid_request_target(&uri) {
return Err(EncodeError::InvalidRequestTarget { uri });
}
if uri.bytes().any(|b| b >= 0x80) {
return Err(EncodeError::InvalidRequestTarget { uri });
}
if !is_valid_protocol_version(&version) {
return Err(EncodeError::InvalidVersion { version });
}
Ok(Self {
method,
uri,
version,
headers: Vec::new(),
body: None,
})
}
/// 検証済みの生フィールドから Request を構築 (デコーダー内部用)
///
/// デコーダー側で既にバリデーション済みのフィールドを直接受け取る。
/// コンストラクタのバリデーションはスキップする。
/// 外部クレートからはアクセス不可 (`pub(crate)`)。
///
/// # 不変条件 (呼び出し側の責務)
///
/// 呼び出し側 (decoder) は以下の不変条件をすべて満たすフィールドのみを渡すこと:
/// - `method`: `is_valid_method` を通過済み (RFC 9110 Section 9.1: method = token)
/// - `uri`: `is_valid_request_target` を通過済み。加えて encoder 側の
/// obs-text 拒否 (0x80-0xFF 非含有) を満たすこと
/// - `version`: `is_valid_protocol_version` を通過済み
/// - `headers`: 各エントリが `is_valid_header_name` / `is_valid_field_value` を通過済み
///
/// 引数は所有値 (`String` / `Vec`) を受け取る。decoder 側 (`RequestHead`) が
/// 所有値を保持しているため、move による zero-copy 構築が可能 (Rust API
/// ガイドライン C-OWNED-PARAMETERS に沿う)。
///
/// 注: 命名は標準ライブラリの unsafe 慣習 (`Vec::from_raw_parts` 等) と表面的に
/// 衝突するが、本関数は unsafe ではない。`pub(crate)` のため外部公開 API には
/// 影響しない。
pub(crate) fn from_raw_parts(
method: String,
uri: String,
version: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
) -> Self {
// debug ビルドのみで契約を検査する。release では検証スキップ (decoder 経路の最適化)。
// 契約違反は decoder のバグであり、release で発覚した場合は encoder 側の
// 二重バリデーション (`validate_request_fields`) が最後の防御線となる。
debug_assert!(
crate::validate::is_valid_method(&method),
"from_raw_parts: invalid method: {method:?}"
);
debug_assert!(
crate::validate::is_valid_request_target(&uri),
"from_raw_parts: invalid request-target: {uri:?}"
);
// 送信側では obs-text を拒否する (validate.rs の is_valid_request_target は
// 受信側互換性のため obs-text を許容しているが、encoder は追加チェックを行う)
debug_assert!(
!uri.bytes().any(|b| b >= 0x80),
"from_raw_parts: request-target contains non-ASCII: {uri:?}"
);
debug_assert!(
crate::validate::is_valid_protocol_version(&version),
"from_raw_parts: invalid version: {version:?}"
);
debug_assert!(
headers.iter().all(|(n, v)| {
crate::validate::is_valid_header_name(n) && crate::validate::is_valid_field_value(v)
}),
"from_raw_parts: invalid header(s)"
);
Self {
method,
uri,
version,
headers,
body,
}
}
/// ヘッダーを追加 (ビルダーパターン)
///
/// 名前は RFC 9110 Section 5.1 の field-name = token (1*tchar、RFC 9110 Section 5.6.2)、
/// 値は RFC 9110 Section 5.5 の field-value を満たす必要がある。
/// CR/LF/NUL は RFC 9110 Section 5.5 で「invalid and dangerous」と明示され、
/// MUST either reject or replace と定義されているため拒否する。
pub fn header(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Result<Self, EncodeError> {
self.add_header(name, value)?;
Ok(self)
}
/// ボディを設定 (ビルダーパターン)
///
/// 空 `Vec` を渡した場合は「明示的な空ボディ」として扱われ、
/// エンコード時に `Content-Length: 0` が自動付与される。
pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = Some(body.into());
self
}
/// ボディなしを明示 (ビルダーパターン)
///
/// `body = None` に設定する。builder チェイン中に `body()` を呼んだ後で
/// ボディを取り消す場合に使用する。
pub fn without_body(mut self) -> Self {
self.body = None;
self
}
/// ボディを設定 (mutator)
///
/// 戻り値は `&mut Self` でチェイン可能。空 `Vec` を渡した場合は
/// 「明示的な空ボディ」として扱われる。
pub fn set_body(&mut self, body: impl Into<Vec<u8>>) -> &mut Self {
self.body = Some(body.into());
self
}
/// ボディをクリア (mutator)
///
/// `body = None` に設定する。`Content-Length` の自動付与はされなくなる。
pub fn clear_body(&mut self) -> &mut Self {
self.body = None;
self
}
/// ヘッダーを追加 (mutator)
///
/// 戻り値は `Result<&mut Self, EncodeError>` でチェイン可能。
/// バリデーション成功後にのみ `headers` に追加される (失敗時は self 不変)。
///
/// 注: `.into()` はバリデーション前に実行されるため、無効な入力でも
/// アロケーションが発生する。これは `impl Into<String>` で所有値のムーブを
/// 受け付けるためのトレードオフである (Response 側と同方針)。
pub fn add_header(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Result<&mut Self, EncodeError> {
let name = name.into();
let value = value.into();
if !is_valid_header_name(&name) {
return Err(EncodeError::InvalidHeaderName { name });
}
if !is_valid_field_value(&value) {
return Err(EncodeError::InvalidHeaderValue { name, value });
}
self.headers.push((name, value));
Ok(self)
}
/// 指定した名前の既存ヘッダーを全削除し、新規に追加する
///
/// 同名 (case-insensitive) のヘッダーをすべて削除した後、
/// 指定した name/value で新規追加する。呼び出し後、対象ヘッダーは末尾に位置する
/// (元の位置は保存しない)。
///
/// バリデーションが失敗した場合は既存ヘッダーは変更されない (アトミック性の保証)。
///
/// 戻り値は `Result<&mut Self, EncodeError>` でチェイン可能。
///
/// 注: Set-Cookie のように同名複数値が意味を持つヘッダーには使ってはならない。
/// その場合は `add_header` を使うこと (RFC 6265 など)。
///
/// 注: `.into()` はバリデーション前に実行されるため、無効な入力でも
/// アロケーションが発生する。アトミック性 (self の状態変更不可) は依然として保たれる
/// (`retain` / `push` はバリデーション成功後にのみ実行される)。
pub fn set_header(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Result<&mut Self, EncodeError> {
let name = name.into();
let value = value.into();
if !is_valid_header_name(&name) {
return Err(EncodeError::InvalidHeaderName { name });
}
if !is_valid_field_value(&value) {
return Err(EncodeError::InvalidHeaderValue { name, value });
}
self.headers.retain(|(n, _)| !n.eq_ignore_ascii_case(&name));
self.headers.push((name, value));
Ok(self)
}
/// HTTP メソッドを取得
pub fn method(&self) -> &str {
&self.method
}
/// リクエスト URI を取得
pub fn uri(&self) -> &str {
&self.uri
}
/// HTTP バージョンを取得
pub fn version(&self) -> &str {
&self.version
}
/// ボディを取得
///
/// 注: builder メソッド `body(data)` と名前を区別するため `body_bytes` と命名している。
/// Rust の inherent impl では `&self` getter と `mut self` builder の同名併存は許されない。
pub fn body_bytes(&self) -> Option<&[u8]> {
self.body.as_deref()
}
/// ヘッダーを取得 (大文字小文字を区別しない)
pub fn get_header(&self, name: &str) -> Option<&str> {
HttpHead::get_header(self, name)
}
/// 指定した名前のヘッダーをすべて取得
pub fn get_headers(&self, name: &str) -> Vec<&str> {
HttpHead::get_headers(self, name)
}
/// ヘッダーが存在するか確認
pub fn has_header(&self, name: &str) -> bool {
HttpHead::has_header(self, name)
}
/// Connection ヘッダーの値を取得 (RFC 9110 Section 7.6.1)
///
/// 最初の `Connection` ヘッダー値をそのままの `&str` で返す。
/// カンマ区切りトークンリストの分割は行わない。
/// `close` / `keep-alive` 等のトークン判定は `is_keep_alive()` が行う。
/// 戻り値から自前でトークン分割する場合は `split(',')` を使用すること。
///
/// `Connection` ヘッダーが存在しない場合は `None` を返す。
pub fn connection(&self) -> Option<&str> {
HttpHead::connection(self)
}
/// キープアライブ接続かどうかを判定
///
/// 判定ロジックは `Connection` ヘッダーのトークンリストを評価した後、
/// プロトコルバージョンにフォールバックする:
///
/// - RFC 9112 Section 9.3: 持続性の判定基準
/// - RFC 9112 Section 9.6: close connection option の定義
/// - RFC 9110 Section 7.6.1: Connection ヘッダーの定義
/// - RFC 9110 Section 5.3: 複数ヘッダー行の結合規則
///
/// 判定順序:
///
/// 1. `Connection` ヘッダーのいずれかに `close` トークンが存在 → `false`
/// (`keep-alive` が同時に存在しても `close` が優先される)
/// 2. `Connection` ヘッダーのいずれかに `keep-alive` トークンが存在 → `true`
/// 3. それ以外 → `version` が `"HTTP/1.1"` 完全一致のときのみ `true` (RTSP/1.1 等は対象外)
///
/// `Connection` ヘッダーはカンマ区切りトークンリストとして扱う
/// (RFC 9110 Section 7.6.1)。
///
/// 注: HTTP/1.1 でも `Connection: close` が指定された場合は keep-alive にならない。
/// HTTP/1.0 で `Connection: keep-alive` がない場合も keep-alive にならない。
/// RFC 9112 Section 9.3 の HTTP/1.0 keep-alive 持続に含まれる proxy 条件
/// (recipient is not a proxy OR message is a response) は本メソッドでは区別しない。
/// これは上位層の責務である。
///
/// 詳細は委譲先 `HttpHead::is_keep_alive` を参照。
pub fn is_keep_alive(&self) -> bool {
HttpHead::is_keep_alive(self)
}
/// `Content-Length` ヘッダーの値を取得
/// (RFC 9110 Section 8.6 / RFC 9112 Section 6.2)
///
/// 最初の `Content-Length` ヘッダー値を `u64` としてパースして返す。
/// 複数ヘッダーが存在しても最初の値のみ参照する
/// (RFC 9110 Section 5.3 により、`Content-Length` の複数フィールド行生成は
/// そもそも禁止されている)。
///
/// 値がパース不能な場合は `None` を返す。
///
/// 注: `Content-Length` の型は `u64` で、RFC 9110 Section 8.6 の
/// 「整数変換オーバーフロー防止 (Section 17.5)」要件に基づく。
/// RFC 9110 Section 17.5 (Attacks via Protocol Element Length) は
/// 算術オーバーフロー・DoS の一般的脅威を論じている。
///
/// Transfer-Encoding と Content-Length の排他関係 (RFC 9112 Section 6.1:
/// MUST NOT send Content-Length in any message that contains Transfer-Encoding)
/// は本メソッドの責務外であり、呼び出し側で判定する。
///
/// 詳細は委譲先 `HttpHead::content_length` を参照。
pub fn content_length(&self) -> Result<Option<u64>, crate::Error> {
HttpHead::content_length(self)
}
/// Transfer-Encoding が chunked かどうかを判定 (RFC 9112 Section 6.3)
///
/// 全 `Transfer-Encoding` ヘッダーを走査し、RFC 9110 Section 5.3 に従い
/// 単一のトークンリストとして扱い、最後のトークンが `chunked` かどうかを確認する。
/// RFC 9112 Section 6.3 #4 の "chunked transfer coding is the final encoding" に基づく。
///
/// 例:
/// - `Transfer-Encoding: chunked` → `true`
/// - `Transfer-Encoding: gzip, chunked` → `true`
/// - `Transfer-Encoding: chunked, gzip` → `false`
///
/// 詳細は委譲先 `HttpHead::is_chunked` を参照。
pub fn is_chunked(&self) -> bool {
HttpHead::is_chunked(self)
}
}