Skip to content

Commit 0a17782

Browse files
committed
🎉 Init project
0 parents  commit 0a17782

File tree

5 files changed

+226
-0
lines changed

5 files changed

+226
-0
lines changed

‎.gitignore

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/target
2+
# Created by https://www.toptal.com/developers/gitignore/api/rust
3+
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
4+
5+
### Rust ###
6+
# Generated by Cargo
7+
# will have compiled files and executables
8+
debug/
9+
target/
10+
11+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
12+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
13+
Cargo.lock
14+
15+
# These are backup files generated by rustfmt
16+
**/*.rs.bk
17+
18+
# MSVC Windows builds of rustc generate these, which store debugging information
19+
*.pdb
20+
21+
# End of https://www.toptal.com/developers/gitignore/api/rust
22+
n
23+
.env

‎Cargo.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "chatgpt-proxy-server"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
hyper = { version = "0.14", features = ["full"] }
10+
tokio = { version = "1", features = ["full"] }
11+
rocket = "0.5.0-rc.2"
12+
reqwest = "0.11"
13+
lazy_static = "1.4.0"
14+
dotenv = "0.15.0"
15+
hyper-tls = "0.5.0"

‎env.example

Whitespace-only changes.

‎src/lib.rs

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
2+
use std::{collections::HashMap, time::{SystemTime, UNIX_EPOCH}};
3+
pub struct RateLimiter {
4+
rate_limit: u32,
5+
counts: HashMap<String, u32>,
6+
reset_time: u64,
7+
}
8+
9+
impl RateLimiter {
10+
pub fn new(
11+
rate_limit: u32,
12+
) -> Self {
13+
let reset_time = SystemTime::now()
14+
.duration_since(UNIX_EPOCH)
15+
.unwrap()
16+
.as_secs()
17+
/ 60
18+
* 60;
19+
Self {
20+
rate_limit: rate_limit,
21+
counts: HashMap::new(),
22+
reset_time,
23+
}
24+
}
25+
26+
pub fn check_rate_limit(&mut self, ip: &str) -> bool {
27+
let current_time = SystemTime::now()
28+
.duration_since(UNIX_EPOCH)
29+
.unwrap()
30+
.as_secs()
31+
/ 60
32+
* 60;
33+
if current_time > self.reset_time {
34+
self.counts.clear();
35+
self.reset_time = current_time;
36+
}
37+
println!("RateLimit: {}: {}", ip, self.counts.get(ip).unwrap_or(&0));
38+
let count = self.counts.entry(ip.to_owned()).or_insert(0);
39+
if *count >= self.rate_limit {
40+
false
41+
} else {
42+
*count += 1;
43+
true
44+
}
45+
}
46+
}

‎src/main.rs

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use std::fmt::Formatter;
2+
use std::net::SocketAddr;
3+
use std::str::FromStr;
4+
5+
use core::fmt::Debug;
6+
7+
use hyper::server::conn::AddrStream;
8+
use hyper::service::{make_service_fn, service_fn};
9+
use hyper::{Body, Client, Request, Response, Server, Uri};
10+
use hyper_tls::HttpsConnector;
11+
use lazy_static::lazy_static;
12+
13+
// import lib.rs
14+
mod lib;
15+
16+
// create config struct
17+
18+
use dotenv;
19+
use tokio::sync::Mutex;
20+
21+
#[derive(Debug)]
22+
struct Config {
23+
chatgpt_url: String,
24+
ratelimit: u32,
25+
}
26+
27+
28+
lazy_static! {
29+
static ref CONFIG: Config = {
30+
dotenv::dotenv().ok();
31+
let chatgpt_url = std::env::var("CHATGPT_URL").expect("CHATGPT_URL is not set");
32+
// Rate limit for 1/min
33+
let ratelimit = std::env::var("RATELIMIT")
34+
.unwrap_or("100".to_string())
35+
.parse::<u32>()
36+
.unwrap();
37+
Config {
38+
chatgpt_url,
39+
ratelimit,
40+
}
41+
};
42+
}
43+
// Add Debug log for CONFIG
44+
impl Debug for CONFIG {
45+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
46+
f.debug_struct("Config")
47+
.field("chatgpt_url", &self.chatgpt_url)
48+
.field("ratelimit", &self.ratelimit)
49+
.finish()
50+
}
51+
}
52+
53+
lazy_static! {
54+
static ref RATE_LIMITER: Mutex<lib::RateLimiter> = Mutex::new(lib::RateLimiter::new(
55+
CONFIG.ratelimit
56+
));
57+
}
58+
59+
async fn proxy(req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
60+
let client_ip = &req.extensions().get::<SocketAddr>().unwrap().ip().to_string();
61+
// Read Client IP read form header or client
62+
let real_ip = req
63+
.headers()
64+
.get("X-Forwarded-For")
65+
.map(|x| x.to_str().unwrap())
66+
.unwrap_or_else(|| &client_ip);
67+
// Check rate limit
68+
let mut rate_limiter = RATE_LIMITER.lock().await;
69+
if !rate_limiter.check_rate_limit(real_ip) {
70+
return Ok(Response::builder()
71+
.status(429)
72+
.header("Content-Type", "text/plain")
73+
.body(Body::from("Too Many Requests"))
74+
.unwrap());
75+
}
76+
// try forward request
77+
let https = HttpsConnector::new();
78+
let client = Client::builder().build::<_, hyper::Body>(https);
79+
// If path switch with /backend-api, change it to /api
80+
let path_and_query = req.uri().path_and_query().unwrap().to_string();
81+
let path_and_query = if path_and_query.starts_with("/backend-api") {
82+
path_and_query.replace("/backend-api", "/api")
83+
} else {
84+
path_and_query.to_string()
85+
};
86+
// Create new uri
87+
let uri = Uri::from_str(&format!("{}{}", CONFIG.chatgpt_url, path_and_query,)).unwrap();
88+
println!("uri: {:?}", uri);
89+
let request_builder = Request::builder().method(req.method())
90+
.uri(uri)
91+
.header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50")
92+
.header("Authorization", req.headers().get("Authorization").unwrap().to_str().unwrap_or_else(|_| "null"))
93+
.header("Content-Type", req.headers().get("Content-Type").unwrap().to_str().unwrap_or_else(|_| "application/json"));
94+
95+
let response = client
96+
.request(request_builder.body(req.into_body()).unwrap())
97+
.await
98+
.unwrap();
99+
100+
Ok(Response::builder()
101+
.status(response.status())
102+
.header(
103+
"Content-Type",
104+
response
105+
.headers()
106+
.get("content-type")
107+
.unwrap()
108+
.to_str()
109+
.unwrap(),
110+
)
111+
.body(response.into_body())
112+
.unwrap())
113+
}
114+
115+
#[tokio::main]
116+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
117+
let port = std::env::var("PORT")
118+
.unwrap_or("3000".to_string())
119+
.parse::<u16>()
120+
.unwrap();
121+
let addr = SocketAddr::from(([0, 0, 0, 0], port));
122+
let service = make_service_fn(|conn: &AddrStream| {
123+
let remote_addr = conn.remote_addr();
124+
async move {
125+
Ok::<_, hyper::Error>(service_fn(move |req| {
126+
let mut req = req;
127+
req.extensions_mut().insert(remote_addr);
128+
proxy(req)
129+
}))
130+
}
131+
});
132+
let server = Server::bind(&addr).serve(
133+
service
134+
);
135+
// print config
136+
println!("Config: {:?}", CONFIG);
137+
println!("Listening on http://{}", addr);
138+
139+
server.await?;
140+
141+
Ok(())
142+
}

0 commit comments

Comments
 (0)