1#![allow(non_snake_case)]
2
3use crate::{AccountNumber, Hex, Pack, ToKey, ToSchema, Unpack};
4use anyhow::anyhow;
5use percent_encoding::percent_decode;
6use serde::{de::DeserializeOwned, Deserialize, Serialize};
7use std::borrow::Cow;
8use std::collections::HashMap;
9
10#[derive(
15 Debug, Default, PartialEq, Eq, Clone, Pack, Unpack, ToKey, ToSchema, Serialize, Deserialize,
16)]
17#[fracpack(definition_will_not_change, fracpack_mod = "fracpack")]
18#[to_key(psibase_mod = "crate")]
19pub struct HttpHeader {
20 pub name: String,
22
23 pub value: String,
25}
26
27impl HttpHeader {
28 pub fn new(name: &str, value: &str) -> Self {
29 HttpHeader {
30 name: name.to_string(),
31 value: value.to_string(),
32 }
33 }
34 pub fn matches(&self, name: &str) -> bool {
35 self.name.eq_ignore_ascii_case(name)
36 }
37}
38
39#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
41#[repr(u16)]
42pub enum HttpStatus {
43 Ok = 200,
44 MovedPermanently = 301,
45 Found = 302,
46 NotModified = 304,
47 Unauthorized = 401,
48 Forbidden = 403,
49 NotFound = 404,
50 MethodNotAllowed = 405,
51 NotAcceptable = 406,
52 UnsupportedMediaType = 415,
53 InternalServerError = 500,
54 ServiceUnavailable = 503,
55}
56
57#[derive(
63 Debug, Default, PartialEq, Eq, Clone, Pack, Unpack, ToKey, ToSchema, Serialize, Deserialize,
64)]
65#[fracpack(fracpack_mod = "fracpack")]
66#[to_key(psibase_mod = "crate")]
67pub struct HttpRequest {
68 pub host: String,
70
71 pub method: String,
73
74 pub target: String,
76
77 pub contentType: String,
79
80 pub headers: Vec<HttpHeader>,
82
83 pub body: Hex<Vec<u8>>,
85}
86
87impl HttpRequest {
88 pub fn path<'a>(&'a self) -> Cow<'a, str> {
89 let encoded = self
90 .target
91 .split_once('?')
92 .map_or(self.target.as_str(), |s| s.0);
93 return percent_decode(encoded.as_bytes()).decode_utf8_lossy();
94 }
95 pub fn query(&self) -> HashMap<String, String> {
96 let encoded = self.target.split_once('?').map_or("", |s| s.1);
97 return form_urlencoded::parse(encoded.as_bytes())
98 .into_owned()
99 .collect();
100 }
101 pub fn get_header(&self, name: &str) -> Option<&str> {
102 self.headers
103 .iter()
104 .find(|h| h.matches(name))
105 .map(|h| h.value.as_str())
106 }
107}
108
109pub struct HttpBody {
110 pub contentType: String,
111 pub body: Hex<Vec<u8>>,
112}
113
114impl HttpBody {
115 pub fn json(data: &str) -> Self {
116 HttpBody {
117 contentType: "application/json".into(),
118 body: data.to_string().into_bytes().into(),
119 }
120 }
121 pub fn graphql(query: &str) -> Self {
122 HttpBody {
123 contentType: "application/graphql".into(),
124 body: query.to_string().into_bytes().into(),
125 }
126 }
127}
128
129#[derive(
133 Debug, Default, PartialEq, Eq, Clone, Pack, Unpack, ToSchema, ToKey, Serialize, Deserialize,
134)]
135#[fracpack(fracpack_mod = "fracpack")]
136#[to_key(psibase_mod = "crate")]
137pub struct HttpReply {
138 pub status: u16,
139
140 pub contentType: String,
142
143 pub body: Hex<Vec<u8>>,
145
146 pub headers: Vec<HttpHeader>,
148}
149
150impl HttpReply {
151 pub fn text(self) -> Result<String, anyhow::Error> {
152 Ok(String::from_utf8(self.body.0)?)
153 }
154 pub fn json<T: DeserializeOwned>(self) -> Result<T, anyhow::Error> {
155 if self.status != 200 {
156 let status = self.status;
157 if self.contentType == "text/html" {
158 if let Ok(msg) = self.text() {
159 Err(anyhow!("Request returned {} {}", status, msg))?
160 }
161 }
162 return Err(anyhow!("Request returned {}", status));
163 }
164 Ok(serde_json::de::from_str(&self.text()?)?)
165 }
166}
167
168struct Origin {
169 scheme: String,
170 host: String,
171}
172
173impl Origin {
174 fn new(url: &str) -> Self {
175 let mut scheme = String::new();
176 let mut host = String::new();
177 if let Some(pos) = url.find("://") {
178 scheme = url[..pos].to_string();
179 let after_scheme = &url[pos + 3..];
180 if let Some(colon_pos) = after_scheme.rfind(':') {
181 if !after_scheme[..colon_pos].contains(']') {
182 host = after_scheme[..colon_pos].to_string();
183 } else {
184 host = after_scheme.to_string();
185 }
186 } else {
187 host = after_scheme.to_string();
188 }
189 }
190 Origin { scheme, host }
191 }
192
193 fn is_secure(&self) -> bool {
194 self.scheme == "https" || self.host == "localhost" || self.host.ends_with(".localhost")
195 }
196
197 fn is_service(&self, root_host: &str, account: AccountNumber) -> bool {
198 self.is_secure() && self.host == format!("{}.{}", account, root_host)
199 }
200
201 fn is_subdomain(&self, root_host: &str) -> bool {
202 self.is_secure()
203 && (self.host == root_host || self.host.ends_with(&format!(".{}", root_host)))
204 }
205}
206
207pub fn root_host(req: &HttpRequest, host_is_subdomain: bool) -> &str {
208 if host_is_subdomain {
209 let pos = req.host.find('.').expect("Subdomain expected");
210 &req.host[pos + 1..]
211 } else {
212 &req.host
213 }
214}
215
216pub fn allow_cors_for_account(
217 req: &HttpRequest,
218 account: AccountNumber,
219 host_is_subdomain: bool,
220) -> Vec<HttpHeader> {
221 if let Some(o) = req.get_header("origin") {
222 let origin = Origin::new(o);
223 if origin.is_service(root_host(req, host_is_subdomain), account) {
224 return allow_cors_with_origin(o);
225 }
226 }
227 Vec::new()
228}
229
230pub fn allow_cors_for_subdomains(req: &HttpRequest, host_is_subdomain: bool) -> Vec<HttpHeader> {
231 if let Some(origin) = req.get_header("origin") {
232 let origin_obj = Origin::new(origin);
233 if origin_obj.is_subdomain(root_host(req, host_is_subdomain)) {
234 return allow_cors_with_origin(origin);
235 }
236 }
237 Vec::new()
238}
239
240pub fn allow_cors_with_origin(origin: &str) -> Vec<HttpHeader> {
241 vec![
242 HttpHeader::new("Access-Control-Allow-Origin", origin),
243 HttpHeader::new("Access-Control-Allow-Methods", "POST, GET, OPTIONS, HEAD"),
244 HttpHeader::new("Access-Control-Allow-Headers", "*"),
245 ]
246}