1use std::sync::Arc;
10use std::time::Duration;
11
12use serde_json::{json, Value};
13use tokio::io::{AsyncWriteExt, BufReader};
14use tokio::net::TcpListener;
15
16use crate::backend::{tls::default_client_config, BackendConfig, TlsMode};
17use crate::config::GraphqlGatewayConfig;
18use crate::graphql::introspector::{ColumnDefinition, TableDefinition};
19use crate::graphql::{GraphQLConfig, GraphQLEngine, GraphQLRequest, SchemaIntrospector};
20use crate::{ProxyError, Result};
21
22pub struct GraphqlGateway {
23 config: Arc<GraphqlGatewayConfig>,
24 engine: Arc<GraphQLEngine>,
25}
26
27impl GraphqlGateway {
28 pub fn new(config: GraphqlGatewayConfig) -> Self {
29 let tabledefs: Vec<TableDefinition> = config
31 .tables
32 .iter()
33 .map(|t| TableDefinition {
34 name: t.name.clone(),
35 schema: "public".to_string(),
36 columns: t
37 .columns
38 .iter()
39 .map(|c| ColumnDefinition {
40 name: c.clone(),
41 data_type: "text".to_string(),
42 nullable: true,
43 is_primary_key: c == "id",
44 has_default: false,
45 })
46 .collect(),
47 foreign_keys: Vec::new(),
48 })
49 .collect();
50 let schema = SchemaIntrospector::new().build_schema(&tabledefs);
51
52 let bcfg = BackendConfig {
53 host: config.backend_host.clone(),
54 port: config.backend_port,
55 user: config.backend_user.clone(),
56 password: config.backend_password.clone(),
57 database: config.backend_database.clone(),
58 application_name: Some("heliosproxy-graphql".to_string()),
59 tls_mode: TlsMode::Disable,
60 connect_timeout: Duration::from_secs(5),
61 query_timeout: Duration::from_secs(30),
62 tls_config: default_client_config(),
63 };
64 let engine = GraphQLEngine::new(GraphQLConfig::default(), schema).with_backend(bcfg);
65
66 Self {
67 config: Arc::new(config),
68 engine: Arc::new(engine),
69 }
70 }
71
72 pub async fn run(self) -> Result<()> {
73 let listener = TcpListener::bind(&self.config.listen_address)
74 .await
75 .map_err(|e| {
76 ProxyError::Network(format!(
77 "GraphQL gateway bind {}: {}",
78 self.config.listen_address, e
79 ))
80 })?;
81 tracing::info!(addr = %self.config.listen_address, "GraphQL gateway listening");
82 let config = self.config.clone();
83 let engine = self.engine.clone();
84 loop {
85 let (stream, peer) = match listener.accept().await {
86 Ok(x) => x,
87 Err(e) => {
88 tracing::warn!("GraphQL gateway accept error: {}", e);
89 continue;
90 }
91 };
92 let config = config.clone();
93 let engine = engine.clone();
94 tokio::spawn(async move {
95 if let Err(e) = Self::handle(stream, config, engine).await {
96 tracing::debug!(%peer, "GraphQL gateway error: {}", e);
97 }
98 });
99 }
100 }
101
102 async fn handle(
103 mut stream: tokio::net::TcpStream,
104 cfg: Arc<GraphqlGatewayConfig>,
105 engine: Arc<GraphQLEngine>,
106 ) -> Result<()> {
107 use crate::http_util;
108 let (reader, mut writer) = stream.split();
109 let mut reader = BufReader::new(reader);
110
111 let deadline = tokio::time::Instant::now() + http_util::HTTP_READ_TIMEOUT;
113 let head = match http_util::read_head(&mut reader, deadline).await {
114 Ok(h) => h,
115 Err(_) => return Ok(()), };
117 let method = head.method.as_str();
118 let path = head.path.as_str();
119
120 if method == "GET" && (path == "/health" || path == "/") {
121 return Self::respond(&mut writer, 200, &json!({"status":"ok"})).await;
122 }
123 let authorized = match cfg.auth_token.as_ref() {
125 None => true,
126 Some(tok) => head
127 .header("authorization")
128 .and_then(|v| v.strip_prefix("Bearer "))
129 .map(|got| http_util::constant_time_eq_str(got, tok))
130 .unwrap_or(false),
131 };
132 if !authorized {
133 return Self::respond(&mut writer, 401, &json!({"error":"unauthorized"})).await;
134 }
135 if method != "POST" {
136 return Self::respond(
137 &mut writer,
138 405,
139 &json!({"error":"use POST with a GraphQL query"}),
140 )
141 .await;
142 }
143 if head.content_length > http_util::MAX_HTTP_BODY_BYTES {
145 return Self::respond(&mut writer, 413, &json!({"error":"request body too large"}))
146 .await;
147 }
148
149 let body_buf = match http_util::read_body(&mut reader, head.content_length, deadline).await
150 {
151 Ok(b) => b,
152 Err(_) => return Ok(()),
153 };
154 let req: Value = match serde_json::from_slice(&body_buf) {
155 Ok(v) => v,
156 Err(e) => {
157 return Self::respond(
158 &mut writer,
159 400,
160 &json!({"errors":[{"message": format!("invalid JSON: {}", e)}]}),
161 )
162 .await
163 }
164 };
165 let query = req
166 .get("query")
167 .and_then(|q| q.as_str())
168 .unwrap_or("")
169 .trim();
170 if query.is_empty() {
171 return Self::respond(
172 &mut writer,
173 400,
174 &json!({"errors":[{"message":"missing 'query'"}]}),
175 )
176 .await;
177 }
178
179 let response = engine.execute(GraphQLRequest::new(query)).await;
180 let errors = response.errors.map(|errs| {
181 errs.iter()
182 .map(|e| json!({ "message": e.to_string() }))
183 .collect::<Vec<_>>()
184 });
185 let body = json!({ "data": response.data, "errors": errors });
186 Self::respond(&mut writer, 200, &body).await
187 }
188
189 async fn respond<W: AsyncWriteExt + Unpin>(
190 writer: &mut W,
191 status: u16,
192 body: &Value,
193 ) -> Result<()> {
194 let payload = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
195 let reason = match status {
196 200 => "OK",
197 400 => "Bad Request",
198 401 => "Unauthorized",
199 405 => "Method Not Allowed",
200 _ => "OK",
201 };
202 let head = format!(
203 "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
204 status,
205 reason,
206 payload.len()
207 );
208 writer
209 .write_all(head.as_bytes())
210 .await
211 .map_err(|e| ProxyError::Network(format!("GraphQL gw write: {}", e)))?;
212 writer
213 .write_all(&payload)
214 .await
215 .map_err(|e| ProxyError::Network(format!("GraphQL gw write: {}", e)))?;
216 let _ = writer.flush().await;
217 Ok(())
218 }
219}