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 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/// HTTP Server 設定データ。toml 設定に対応する。
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct HttpConfig {
27    /// HTTP Server 機能を有効化する。
28    enabled: bool,
29    /// 管理者専用ページを有効化する。
30    priv_enabled: bool,
31    /// ポート番号。
32    port: u16,
33    /// ルートパス。リバースプロキシ設定に合わせること。
34    path_prefix: String,
35    /// 管理者専用ページのルートパス。リバースプロキシ設定に合わせること。
36    priv_prefix: String,
37    /// アップローダ機能を有効化する。パスは /_rootpath_/upload/。
38    upload_enabled: bool,
39    /// アップロードされたファイルの保存場所。
40    upload_dir: String,
41    /// GitHub Hook 機能を有効化する。パスは /_rootpath_/github/。
42    ghhook_enabled: bool,
43    /// GitHub Hook の SHA256 検証に使うハッシュ。GitHub の設定ページから手に入る。
44    ghhook_secret: String,
45    /// LINE webhook 機能を有効化する。パスは /_rootpath_/line/。
46    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    // クロージャ内に move するデータの準備
88    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    // クロージャはワーカースレッドごとに複数回呼ばれる
93    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    // シャットダウンが来たらハンドルでサーバを停止するタスクを生成
113    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}