Add base code (#1)
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			Add base code from https://gitea.communiquons.org/pierre/oidc-test-client with minor improvements Reviewed-on: #1
This commit is contained in:
		
							
								
								
									
										15
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
---
 | 
			
		||||
kind: pipeline
 | 
			
		||||
type: docker
 | 
			
		||||
name: default
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
- name: cargo_check
 | 
			
		||||
  image: rust
 | 
			
		||||
  commands:
 | 
			
		||||
  - rustup component add clippy
 | 
			
		||||
  - cargo clippy -- -D warnings
 | 
			
		||||
  - cargo test
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1264
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1264
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -2,7 +2,15 @@
 | 
			
		||||
name = "actix-remote-ip"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
authors = ["Pierre HUBERT <pierre.git@communiquons.org>"]
 | 
			
		||||
description = "Tiny extractor to get real client IP address, parsing X-Forwarded-For header"
 | 
			
		||||
readme = "README.md"
 | 
			
		||||
license = "GPL-2.0-or-later"
 | 
			
		||||
repository = "https://gitea.communiquons.org/pierre/actix-remote-ip"
 | 
			
		||||
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
log = "0.4.17"
 | 
			
		||||
actix-web = "4"
 | 
			
		||||
futures-util = "0.3.28"
 | 
			
		||||
							
								
								
									
										33
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
# Actix Remote IP extractor
 | 
			
		||||
[](https://drone.communiquons.org/pierre/actix-remote-ip)
 | 
			
		||||
[](https://crates.io/crates/actix-remote-ip)
 | 
			
		||||
 | 
			
		||||
Tiny extractor of remote user IP address, that handles reverse proxy.
 | 
			
		||||
 | 
			
		||||
The `X-Forwarded-For` header is automatically parsed when the request comes from a defined proxy, to determine the real remote client IP Address.
 | 
			
		||||
 | 
			
		||||
Note : regarding IPv6 addresses, the local part of the address is discarded. For example, the  IPv6 client `2001:0db8:85a3:0000:0000:8a2e:0370:7334` will be returned as `2001:0db8:85a3:0000:0000:0000:0000:0000`
 | 
			
		||||
 | 
			
		||||
## Configuration
 | 
			
		||||
Configure it when you configure your Actix server:
 | 
			
		||||
 | 
			
		||||
```rust
 | 
			
		||||
HttpServer::new(move || {
 | 
			
		||||
    App::new()
 | 
			
		||||
        .app_data(web::Data::new(RemoteIPConfig {
 | 
			
		||||
            proxy: Some("IP".to_string())
 | 
			
		||||
        }))
 | 
			
		||||
    // ...
 | 
			
		||||
})
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
In your route, add a `RemoteIP` parameter:
 | 
			
		||||
 | 
			
		||||
```rust
 | 
			
		||||
#[get("/")]
 | 
			
		||||
async fn home(remote_ip: RemoteIP) -> HttpResponse {
 | 
			
		||||
    let ip: IpAddr = remote_ip.0;
 | 
			
		||||
    // ...
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										76
									
								
								src/ip_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/ip_utils.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
use std::net::{IpAddr, Ipv6Addr};
 | 
			
		||||
use std::str::FromStr;
 | 
			
		||||
 | 
			
		||||
/// Parse an IP address
 | 
			
		||||
pub fn parse_ip(ip: &str) -> Option<IpAddr> {
 | 
			
		||||
    let mut ip = match IpAddr::from_str(ip) {
 | 
			
		||||
        Ok(ip) => ip,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::warn!("Failed to parse an IP address: {}", e);
 | 
			
		||||
            return None;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // In case of IPv6 address, we skip the 8 last octets
 | 
			
		||||
    if let IpAddr::V6(ipv6) = &mut ip {
 | 
			
		||||
        let mut octets = ipv6.octets();
 | 
			
		||||
        for o in octets.iter_mut().skip(8) {
 | 
			
		||||
            *o = 0;
 | 
			
		||||
        }
 | 
			
		||||
        ip = IpAddr::V6(Ipv6Addr::from(octets));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Some(ip)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Check if two ips matches
 | 
			
		||||
pub fn match_ip(pattern: &str, ip: &str) -> bool {
 | 
			
		||||
    if pattern.eq(ip) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if pattern.ends_with('*') && ip.starts_with(&pattern.replace('*', "")) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod test {
 | 
			
		||||
    use crate::ip_utils::parse_ip;
 | 
			
		||||
    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn parse_bad_ip() {
 | 
			
		||||
        let ip = parse_ip("badbad");
 | 
			
		||||
        assert_eq!(None, ip);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn parse_ip_v4_address() {
 | 
			
		||||
        let ip = parse_ip("192.168.1.1").unwrap();
 | 
			
		||||
        assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn parse_ip_v6_address() {
 | 
			
		||||
        let ip = parse_ip("2a00:1450:4007:813::200e").unwrap();
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            ip,
 | 
			
		||||
            IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4007, 0x813, 0, 0, 0, 0))
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn parse_ip_v6_address_2() {
 | 
			
		||||
        let ip = parse_ip("::1").unwrap();
 | 
			
		||||
        assert_eq!(ip, IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn parse_ip_v6_address_3() {
 | 
			
		||||
        let ip = parse_ip("a::1").unwrap();
 | 
			
		||||
        assert_eq!(ip, IpAddr::V6(Ipv6Addr::new(0xa, 0, 0, 0, 0, 0, 0, 0)));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										164
									
								
								src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
			
		||||
//! #actix-remote-ip
 | 
			
		||||
//!
 | 
			
		||||
//! A tiny crate to determine the Real IP address of a client, taking account
 | 
			
		||||
//! of an eventual reverse proxy
 | 
			
		||||
 | 
			
		||||
use crate::ip_utils::{match_ip, parse_ip};
 | 
			
		||||
use actix_web::dev::Payload;
 | 
			
		||||
use actix_web::web::Data;
 | 
			
		||||
use actix_web::{Error, FromRequest, HttpRequest};
 | 
			
		||||
use futures_util::future::{ready, Ready};
 | 
			
		||||
use std::fmt::Display;
 | 
			
		||||
use std::net::IpAddr;
 | 
			
		||||
 | 
			
		||||
mod ip_utils;
 | 
			
		||||
 | 
			
		||||
/// Remote IP retrieval configuration
 | 
			
		||||
///
 | 
			
		||||
/// This configuration must be built and set as Actix web data
 | 
			
		||||
#[derive(Debug, Clone, Eq, PartialEq, Default)]
 | 
			
		||||
pub struct RemoteIPConfig {
 | 
			
		||||
    /// The IP address of the proxy. This address can ends with a star '*'
 | 
			
		||||
    pub proxy: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl RemoteIPConfig {
 | 
			
		||||
    /// Initiate a new RemoteIPConfig configuration instance, that
 | 
			
		||||
    /// can be set as [actix_web::Data] structure
 | 
			
		||||
    pub fn with_proxy_ip<D: Display>(proxy: D) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            proxy: Some(proxy.to_string()),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get the remote IP address
 | 
			
		||||
pub fn get_remote_ip(req: &HttpRequest) -> IpAddr {
 | 
			
		||||
    let proxy = req
 | 
			
		||||
        .app_data::<Data<RemoteIPConfig>>()
 | 
			
		||||
        .map(|c| c.proxy.as_ref())
 | 
			
		||||
        .unwrap_or_default();
 | 
			
		||||
    log::trace!("Proxy IP: {:?}", proxy);
 | 
			
		||||
 | 
			
		||||
    let mut ip = req.peer_addr().unwrap().ip();
 | 
			
		||||
 | 
			
		||||
    // We check if the request comes from a trusted reverse proxy
 | 
			
		||||
    if let Some(proxy) = proxy.as_ref() {
 | 
			
		||||
        if match_ip(proxy, &ip.to_string()) {
 | 
			
		||||
            if let Some(header) = req.headers().get("X-Forwarded-For") {
 | 
			
		||||
                let header = header.to_str().unwrap();
 | 
			
		||||
 | 
			
		||||
                let remote_ip = if let Some((upstream_ip, _)) = header.split_once(',') {
 | 
			
		||||
                    upstream_ip
 | 
			
		||||
                } else {
 | 
			
		||||
                    header
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                if let Some(upstream_ip) = parse_ip(remote_ip) {
 | 
			
		||||
                    ip = upstream_ip;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ip
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
 | 
			
		||||
pub struct RemoteIP(pub IpAddr);
 | 
			
		||||
 | 
			
		||||
impl From<RemoteIP> for IpAddr {
 | 
			
		||||
    fn from(i: RemoteIP) -> Self {
 | 
			
		||||
        i.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FromRequest for RemoteIP {
 | 
			
		||||
    type Error = Error;
 | 
			
		||||
    type Future = Ready<Result<Self, Error>>;
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
 | 
			
		||||
        ready(Ok(RemoteIP(get_remote_ip(req))))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod test {
 | 
			
		||||
    use std::net::{IpAddr, SocketAddr};
 | 
			
		||||
    use std::str::FromStr;
 | 
			
		||||
 | 
			
		||||
    use crate::{get_remote_ip, RemoteIPConfig};
 | 
			
		||||
    use actix_web::test::TestRequest;
 | 
			
		||||
    use actix_web::web::Data;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_get_remote_ip() {
 | 
			
		||||
        let req = TestRequest::default()
 | 
			
		||||
            .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
 | 
			
		||||
            .to_http_request();
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            get_remote_ip(&req),
 | 
			
		||||
            "192.168.1.1".parse::<IpAddr>().unwrap()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_get_remote_ip_from_proxy() {
 | 
			
		||||
        let req = TestRequest::default()
 | 
			
		||||
            .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
 | 
			
		||||
            .insert_header(("X-Forwarded-For", "1.1.1.1"))
 | 
			
		||||
            .app_data(Data::new(RemoteIPConfig::with_proxy_ip("192.168.1.1")))
 | 
			
		||||
            .to_http_request();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(get_remote_ip(&req), "1.1.1.1".parse::<IpAddr>().unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_get_remote_ip_from_proxy_2() {
 | 
			
		||||
        let req = TestRequest::default()
 | 
			
		||||
            .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
 | 
			
		||||
            .insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
 | 
			
		||||
            .app_data(Data::new(RemoteIPConfig::with_proxy_ip("192.168.1.1")))
 | 
			
		||||
            .to_http_request();
 | 
			
		||||
        assert_eq!(get_remote_ip(&req), "1.1.1.1".parse::<IpAddr>().unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_get_remote_ip_from_proxy_ipv6() {
 | 
			
		||||
        let req = TestRequest::default()
 | 
			
		||||
            .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
 | 
			
		||||
            .insert_header(("X-Forwarded-For", "10::1, 1.2.2.2"))
 | 
			
		||||
            .app_data(Data::new(RemoteIPConfig::with_proxy_ip("192.168.1.1")))
 | 
			
		||||
            .to_http_request();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(get_remote_ip(&req), "10::".parse::<IpAddr>().unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_get_remote_ip_from_no_proxy() {
 | 
			
		||||
        let req = TestRequest::default()
 | 
			
		||||
            .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
 | 
			
		||||
            .insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
 | 
			
		||||
            .to_http_request();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            get_remote_ip(&req),
 | 
			
		||||
            "192.168.1.1".parse::<IpAddr>().unwrap()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_get_remote_ip_from_other_proxy() {
 | 
			
		||||
        let req = TestRequest::default()
 | 
			
		||||
            .peer_addr(SocketAddr::from_str("192.168.1.1:1000").unwrap())
 | 
			
		||||
            .insert_header(("X-Forwarded-For", "1.1.1.1, 1.2.2.2"))
 | 
			
		||||
            .app_data(Data::new(RemoteIPConfig::with_proxy_ip("192.168.1.2")))
 | 
			
		||||
            .to_http_request();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            get_remote_ip(&req),
 | 
			
		||||
            "192.168.1.1".parse::<IpAddr>().unwrap()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
fn main() {
 | 
			
		||||
    println!("Hello, world!");
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user