1mod github;
6mod index;
7mod line_hook;
8mod priv_camera;
9mod priv_index;
10mod upload;
11
12use super::SystemModule;
13use crate::taskserver;
14use crate::{config, taskserver::Control};
15use actix_web::{HttpResponse, Responder, http::header::ContentType};
16use actix_web::{HttpResponseBuilder, web};
17use anyhow::{Result, anyhow};
18use log::{error, info};
19use serde::{Deserialize, Serialize};
20use serenity::http::StatusCode;
21use std::fmt::Display;
22use std::sync::Arc;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct HttpConfig {
27 enabled: bool,
29 priv_enabled: bool,
31 port: u16,
33 path_prefix: String,
35 priv_prefix: String,
37 upload_enabled: bool,
39 upload_dir: String,
41 ghhook_enabled: bool,
43 ghhook_secret: String,
45 line_hook_enabled: bool,
47}
48
49impl Default for HttpConfig {
50 fn default() -> Self {
51 Self {
52 enabled: false,
53 priv_enabled: false,
54 port: 8899,
55 path_prefix: "/rhouse".to_string(),
56 priv_prefix: "/rhouse/priv".to_string(),
57 upload_enabled: false,
58 upload_dir: "./upload".to_string(),
59 ghhook_enabled: false,
60 ghhook_secret: "".to_string(),
61 line_hook_enabled: false,
62 }
63 }
64}
65
66pub struct HttpServer {
67 config: HttpConfig,
68}
69
70impl HttpServer {
71 pub fn new() -> Result<Self> {
72 info!("[http] initialize");
73
74 let config = config::get(|cfg| cfg.http.clone());
75
76 Ok(Self { config })
77 }
78}
79
80async fn http_main_task(ctrl: Control) -> Result<()> {
81 let http_config = {
82 let http = ctrl.sysmods().http.lock().await;
83 http.config.clone()
84 };
85
86 let port = http_config.port;
87 let data_config = web::Data::new(http_config.clone());
89 let data_ctrl = web::Data::new(ctrl.clone());
90 let config_regular = index::server_config();
91 let config_privileged = priv_index::server_config();
92 let server = actix_web::HttpServer::new(move || {
94 actix_web::App::new()
95 .app_data(data_config.clone())
96 .app_data(data_ctrl.clone())
97 .service(root_index_get)
98 .service(
99 web::scope(&data_config.path_prefix)
100 .configure(|cfg| {
101 config_regular(cfg, &http_config);
102 })
103 .service(web::scope(&data_config.priv_prefix).configure(|cfg| {
104 config_privileged(cfg, &http_config);
105 })),
106 )
107 })
108 .disable_signals()
109 .bind(("127.0.0.1", port))?
110 .run();
111
112 let ctrl_for_stop = Arc::clone(&ctrl);
114 let handle = server.handle();
115 taskserver::spawn_oneshot_fn(&ctrl, "http-exit", async move {
116 ctrl_for_stop.wait_cancel_rx().await;
117 info!("[http-exit] recv cancel");
118 handle.stop(true).await;
119 info!("[http-exit] server stop ok");
120
121 Ok(())
122 });
123
124 server.await?;
125 info!("[http] server exit");
126
127 Ok(())
128}
129
130impl SystemModule for HttpServer {
131 fn on_start(&mut self, ctrl: &Control) {
132 info!("[http] on_start");
133 if self.config.enabled {
134 taskserver::spawn_oneshot_task(ctrl, "http", http_main_task);
135 }
136 }
137}
138
139pub type WebResult = Result<HttpResponse, ActixError>;
140
141#[derive(Debug)]
142pub struct ActixError {
143 err: anyhow::Error,
144 status: StatusCode,
145}
146
147impl ActixError {
148 pub fn new(msg: &str, status: u16) -> Self {
149 if !(400..600).contains(&status) {
150 panic!("status must be 400 <= status < 600");
151 }
152 ActixError {
153 err: anyhow!(msg.to_string()),
154 status: StatusCode::from_u16(status).unwrap(),
155 }
156 }
157}
158
159impl actix_web::error::ResponseError for ActixError {
160 fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
161 error!("HTTP error by Error: {}", self.status);
162 error!("{:#}", self.err);
163
164 HttpResponse::build(self.status)
165 .insert_header(ContentType::plaintext())
166 .body(self.status.to_string())
167 }
168}
169
170impl Display for ActixError {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 write!(f, "{}, status={}", self.err, self.status.as_str())
173 }
174}
175
176impl From<anyhow::Error> for ActixError {
177 fn from(err: anyhow::Error) -> ActixError {
178 ActixError {
179 err,
180 status: StatusCode::INTERNAL_SERVER_ERROR,
181 }
182 }
183}
184
185pub fn error_resp(status: StatusCode) -> HttpResponse {
186 error_resp_msg(status, status.canonical_reason().unwrap_or_default())
187}
188
189pub fn error_resp_msg(status: StatusCode, msg: &str) -> HttpResponse {
190 let body = format!("{} {}", status.as_str(), msg);
191
192 HttpResponseBuilder::new(status)
193 .content_type(ContentType::plaintext())
194 .body(body)
195}
196
197#[actix_web::get("/")]
198async fn root_index_get(cfg: web::Data<HttpConfig>) -> impl Responder {
199 let body = format!(
200 r#"<!DOCTYPE html>
201<html lang="en">
202 <head>
203 <title>House Management System Web Interface</title>
204 </head>
205 <body>
206 <h1>House Management System Web Interface</h1>
207 <p>This is the root page. Web module is working fine.</p>
208 <p>
209 This system is intended to be connected from a front web server (reverse proxy).
210 Therefore, this page will not be visible from the network.
211 </p>
212 <p>Application endpoint (reverse proxy root) is <strong>{}</strong>.<p>
213 </body>
214</html>
215"#,
216 cfg.path_prefix
217 );
218 HttpResponse::Ok()
219 .content_type(ContentType::html())
220 .body(body)
221}