sys/sysmod/
http.rs

1//! HTTP Server 機能。
2//!
3//! actix_web ライブラリ / フレームワークによる。
4
5mod 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/// HTTP Server 設定データ。toml 設定に対応する。
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct HttpConfig {
29    /// HTTP Server 機能を有効化する。
30    enabled: bool,
31    /// 管理者専用ページを有効化する。
32    priv_enabled: bool,
33    /// ポート番号。
34    port: u16,
35    /// ベース URL。
36    /// e.g. "http://example.com"
37    server_url: String,
38    /// ルートパス。
39    /// リバースプロキシ条件の URL プレフィクスに合わせること。
40    ///
41    /// 例: "/rhouse" で始まるものを転送するという条件のリバースプロキシ設定の場合、
42    /// "/rhouse/some/path" はそのままバックエンドサーバに送られる。
43    /// ここで [path_prefix] を "/rhouse" に設定すると、
44    /// バックエンドサーバ側では "{path_prefix}/some/path" が有効なパスとなり、
45    /// 正常に稼働する。
46    path_prefix: String,
47    /// 管理者専用ページのルートパス。[path_prefix] の後に連結される。
48    /// リバースプロキシ条件の URL プレフィクスに合わせること。
49    priv_prefix: String,
50    /// アップローダ機能を有効化する。パスは /_rootpath_/upload/。
51    upload_enabled: bool,
52    /// アップロードされたファイルの保存場所。
53    upload_dir: String,
54    /// GitHub Hook 機能を有効化する。パスは /_rootpath_/github/。
55    ghhook_enabled: bool,
56    /// GitHub Hook の SHA256 検証に使うハッシュ。GitHub の設定ページから手に入る。
57    ghhook_secret: String,
58    /// LINE webhook 機能を有効化する。パスは /_rootpath_/line/。
59    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    // クロージャ内に move するデータの準備
147    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    // クロージャはワーカースレッドごとに複数回呼ばれる
152    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    // シャットダウンが来たらハンドルでサーバを停止するタスクを生成
172    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}