Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.1.280.1 KB2026-09-140.1.167.6 KB2026-09-140.1.064.9 KB2026-09-14
Version
0.1.2
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-14
Size
80.1 KB
Downloads
1
Checksum
65387e928cd11d6abf76cfb0f7a839dd3b2b7bb9cdbe78e7b058d3213d9fbcc2
Dependencies
None

ip.rs

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
//! 相手の IP を決める。
//!
//! nginx のような中継の後ろでは、TCP の相手は中継そのものである。
//! 本当の相手は `X-Forwarded-For` や `X-Real-IP` で渡ってくる。
//! どのヘッダを読むか、右から何個読み飛ばすかは [`Conf`] で決める。
//!
//! ヘッダの左端は相手が自由に書ける。nginx の `$proxy_add_x_forwarded_for` は
//! 自分が見た IP を右へ足すので、信用できるのは右からである。
//! `CLIENT_IP_HEADER` を書かないかぎりヘッダを一切見ないので、
//! 設定を忘れたまま詐称された値を採ることはない。

use axum::extract::{ConnectInfo, FromRequestParts};
use axum::http::request::Parts;
use axum::http::{Extensions, HeaderMap};
use crate::conf::Conf;
use crate::state::AppState;
use std::convert::Infallible;
use std::net::{IpAddr, SocketAddr};


/// 相手の IP。決め方は [`Conf`] の `client_ip_header` と `trusted_proxy` による。
///
/// ヘッダからも `ConnectInfo` からも取れなければ `None` になる。
/// TCP の相手を使うには、使う側が `axum::serve` へ
/// `into_make_service_with_connect_info::<SocketAddr>()` を渡すこと。
pub struct ClientIp(pub Option<IpAddr>);


impl FromRequestParts<AppState> for ClientIp {
    type Rejection = Infallible;

    async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Infallible> {
        Ok(ClientIp(resolve(&parts.headers, &parts.extensions, &state.conf)))
    }
}


/// ヘッダを見て、取れなければ TCP の相手を使う
pub fn resolve(headers: &HeaderMap, extensions: &Extensions, conf: &Conf) -> Option<IpAddr> {
    from_header(headers, conf).or_else(|| peer(extensions))
}


/// 設定されたヘッダを右から読む。左端は相手が書けるので採らない
fn from_header(headers: &HeaderMap, conf: &Conf) -> Option<IpAddr> {
    if conf.client_ip_header.is_empty() {
        return None;
    }

    // 同じヘッダが複数行に分かれて届くことがある。1 本につないでから数える
    let line = headers
        .get_all(conf.client_ip_header.as_str())
        .iter()
        .filter_map(|value| value.to_str().ok())
        .collect::<Vec<&str>>()
        .join(",");

    to_ip(line.rsplit(',').nth(conf.trusted_proxy)?)
}


/// TCP の相手。`into_make_service_with_connect_info` を渡していなければ取れない
fn peer(extensions: &Extensions) -> Option<IpAddr> {
    extensions
        .get::<ConnectInfo<SocketAddr>>()
        .map(|ConnectInfo(address)| address.ip())
}


/// `1.2.3.4` と `1.2.3.4:5678` のどちらでも読む。中継によって書き方が違う
fn to_ip(value: &str) -> Option<IpAddr> {
    let value = value.trim();

    if let Ok(address) = value.parse::<IpAddr>() {
        return Some(address);
    }

    value.parse::<SocketAddr>().ok().map(|address| address.ip())
}


#[cfg(test)]
mod tests {
    use super::*;

    fn conf_with(header: &str, trusted_proxy: usize) -> Conf {
        Conf {
            client_ip_header: header.to_owned(),
            trusted_proxy,
            ..Conf::default()
        }
    }

    fn headers_with(name: &str, value: &str) -> HeaderMap {
        let mut headers = HeaderMap::new();
        headers.insert(
            axum::http::HeaderName::from_bytes(name.as_bytes()).unwrap(),
            value.parse().unwrap(),
        );

        headers
    }

    #[test]
    fn no_header_name_means_no_header_is_read() {
        let headers = headers_with("x-forwarded-for", "1.2.3.4");
        assert_eq!(from_header(&headers, &Conf::default()), None);
    }

    /// nginx が 1 段。右端が nginx の見た相手である
    #[test]
    fn the_rightmost_entry_is_taken() {
        let headers = headers_with("x-forwarded-for", "9.9.9.9, 1.2.3.4");
        let found = from_header(&headers, &conf_with("x-forwarded-for", 0));

        assert_eq!(found, Some("1.2.3.4".parse::<IpAddr>().unwrap()));
    }

    /// 相手が左端に何を書いても採らない
    #[test]
    fn a_spoofed_leftmost_entry_is_not_taken() {
        let headers = headers_with("x-forwarded-for", "203.0.113.9, 1.2.3.4");
        let found = from_header(&headers, &conf_with("x-forwarded-for", 0));

        assert_ne!(found, Some("203.0.113.9".parse::<IpAddr>().unwrap()));
    }

    /// 前に CDN がいる。右端は CDN で、その 1 つ左が相手である
    #[test]
    fn a_trusted_proxy_is_skipped() {
        let headers = headers_with("x-forwarded-for", "1.2.3.4, 198.51.100.7");
        let found = from_header(&headers, &conf_with("x-forwarded-for", 1));

        assert_eq!(found, Some("1.2.3.4".parse::<IpAddr>().unwrap()));
    }

    /// x-real-ip のような単一値のヘッダも同じ経路で通る
    #[test]
    fn a_single_value_header_works() {
        let headers = headers_with("x-real-ip", "1.2.3.4");
        let found = from_header(&headers, &conf_with("x-real-ip", 0));

        assert_eq!(found, Some("1.2.3.4".parse::<IpAddr>().unwrap()));
    }

    #[test]
    fn skipping_past_the_end_gives_nothing() {
        let headers = headers_with("x-forwarded-for", "1.2.3.4");
        assert_eq!(from_header(&headers, &conf_with("x-forwarded-for", 3)), None);
    }

    #[test]
    fn an_address_with_a_port_is_read() {
        assert_eq!(to_ip("1.2.3.4:5678"), Some("1.2.3.4".parse::<IpAddr>().unwrap()));
        assert_eq!(to_ip(" 1.2.3.4 "), Some("1.2.3.4".parse::<IpAddr>().unwrap()));
        assert_eq!(to_ip("なんでもない"), None);
    }
}