1mod github;
6mod index;
7mod line_hook;
8mod priv_camera;
9mod priv_index;
10mod tmp;
11mod upload;
12
13use super::SystemModule;
14use crate::taskserver;
15use crate::{config, taskserver::Control};
16use actix_web::http::StatusCode;
17use actix_web::{HttpResponse, Responder, http::header::ContentType};
18use actix_web::{HttpResponseBuilder, web};
19use anyhow::{Result, anyhow};
20use log::{error, info};
21use serde::{Deserialize, Serialize};
22use std::collections::VecDeque;
23use std::fmt::Display;
24use std::sync::Arc;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct HttpConfig {
29 enabled: bool,
31 priv_enabled: bool,
33 port: u16,
35 server_url: String,
38 path_prefix: String,
47 priv_prefix: String,
50 upload_enabled: bool,
52 upload_dir: String,
54 ghhook_enabled: bool,
56 ghhook_secret: String,
58 line_hook_enabled: bool,
60}
61
62impl Default for HttpConfig {
63 fn default() -> Self {
64 Self {
65 enabled: false,
66 priv_enabled: false,
67 port: 8899,
68 server_url: "".to_string(),
69 path_prefix: "/rhouse".to_string(),
70 priv_prefix: "/priv".to_string(),
71 upload_enabled: false,
72 upload_dir: "./upload".to_string(),
73 ghhook_enabled: false,
74 ghhook_secret: "".to_string(),
75 line_hook_enabled: false,
76 }
77 }
78}
79
80pub struct TmpElement {
81 pub id: String,
82 pub ctype: ContentType,
83 pub data: Vec<u8>,
84}
85
86pub struct HttpServer {
87 config: HttpConfig,
88 tmp_data: VecDeque<TmpElement>,
89}
90
91impl HttpServer {
92 const TMP_COUNT_MAX: usize = 32;
93
94 pub fn new() -> Result<Self> {
95 info!("[http] initialize");
96
97 let config = config::get(|cfg| cfg.http.clone());
98
99 Ok(Self {
100 config,
101 tmp_data: Default::default(),
102 })
103 }
104
105 pub fn export_tmp_data(&mut self, ctype: ContentType, data: Vec<u8>) -> Result<String> {
106 anyhow::ensure!(
107 !self.config.server_url.is_empty(),
108 "config server_url is empty"
109 );
110
111 let id = loop {
112 let id = format!(
113 "{:016x}{:016x}",
114 rand::random::<u64>(),
115 rand::random::<u64>()
116 );
117 if !self.tmp_data.iter().any(|elem| elem.id == id) {
118 break id;
119 }
120 };
121
122 self.tmp_data.push_back(TmpElement {
123 id: id.clone(),
124 ctype,
125 data,
126 });
127 while self.tmp_data.len() > Self::TMP_COUNT_MAX {
128 self.tmp_data.pop_front();
129 }
130
131 let url = format!(
132 "{}{}/tmp/{id}",
133 self.config.server_url, self.config.path_prefix
134 );
135 Ok(url)
136 }
137}
138
139async fn http_main_task(ctrl: Control) -> Result<()> {
140 let http_config = {
141 let http = ctrl.sysmods().http.lock().await;
142 http.config.clone()
143 };
144
145 let port = http_config.port;
146 let data_config = web::Data::new(http_config.clone());
148 let data_ctrl = web::Data::new(ctrl.clone());
149 let config_regular = index::server_config();
150 let config_privileged = priv_index::server_config();
151 let server = actix_web::HttpServer::new(move || {
153 actix_web::App::new()
154 .app_data(data_config.clone())
155 .app_data(data_ctrl.clone())
156 .service(root_index_get)
157 .service(
158 web::scope(&data_config.path_prefix)
159 .configure(|cfg| {
160 config_regular(cfg, &http_config);
161 })
162 .service(web::scope(&data_config.priv_prefix).configure(|cfg| {
163 config_privileged(cfg, &http_config);
164 })),
165 )
166 })
167 .disable_signals()
168 .bind(("127.0.0.1", port))?
169 .run();
170
171 let ctrl_for_stop = Arc::clone(&ctrl);
173 let handle = server.handle();
174 taskserver::spawn_oneshot_fn(&ctrl, "http-exit", async move {
175 ctrl_for_stop.wait_cancel_rx().await;
176 info!("[http-exit] recv cancel");
177 handle.stop(true).await;
178 info!("[http-exit] server stop ok");
179
180 Ok(())
181 });
182
183 server.await?;
184 info!("[http] server exit");
185
186 Ok(())
187}
188
189impl SystemModule for HttpServer {
190 fn on_start(&mut self, ctrl: &Control) {
191 info!("[http] on_start");
192 if self.config.enabled {
193 taskserver::spawn_oneshot_task(ctrl, "http", http_main_task);
194 }
195 }
196}
197
198pub type WebResult = Result<HttpResponse, ActixError>;
199
200#[derive(Debug)]
201pub struct ActixError {
202 err: anyhow::Error,
203 status: StatusCode,
204}
205
206impl ActixError {
207 pub fn new(msg: &str, status: u16) -> Self {
208 if !(400..600).contains(&status) {
209 panic!("status must be 400 <= status < 600");
210 }
211 ActixError {
212 err: anyhow!(msg.to_string()),
213 status: StatusCode::from_u16(status).unwrap(),
214 }
215 }
216}
217
218impl actix_web::error::ResponseError for ActixError {
219 fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
220 error!("HTTP error by Error: {}", self.status);
221 error!("{:#}", self.err);
222
223 HttpResponse::build(self.status)
224 .insert_header(ContentType::plaintext())
225 .body(self.status.to_string())
226 }
227}
228
229impl Display for ActixError {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 write!(f, "{}, status={}", self.err, self.status.as_str())
232 }
233}
234
235impl From<anyhow::Error> for ActixError {
236 fn from(err: anyhow::Error) -> ActixError {
237 ActixError {
238 err,
239 status: StatusCode::INTERNAL_SERVER_ERROR,
240 }
241 }
242}
243
244pub fn error_resp(status: StatusCode) -> HttpResponse {
245 error_resp_msg(status, status.canonical_reason().unwrap_or_default())
246}
247
248pub fn error_resp_msg(status: StatusCode, msg: &str) -> HttpResponse {
249 let body = format!("{} {}", status.as_str(), msg);
250
251 HttpResponseBuilder::new(status)
252 .content_type(ContentType::plaintext())
253 .body(body)
254}
255
256#[actix_web::get("/")]
257async fn root_index_get(cfg: web::Data<HttpConfig>) -> impl Responder {
258 let body = format!(
259 r#"<!DOCTYPE html>
260<html lang="en">
261 <head>
262 <title>House Management System Web Interface</title>
263 </head>
264 <body>
265 <h1>House Management System Web Interface</h1>
266 <p>This is the root page. Web module is working fine.</p>
267 <p>
268 This system is intended to be connected from a front web server (reverse proxy).
269 Therefore, this page will not be visible from the network.
270 </p>
271 <p>Application endpoint (reverse proxy root) is <strong>{}</strong>.<p>
272 </body>
273</html>
274"#,
275 cfg.path_prefix
276 );
277 HttpResponse::Ok()
278 .content_type(ContentType::html())
279 .body(body)
280}