master
veypi 10 months ago
parent c180cd4241
commit dfd1549f11

@ -81,7 +81,7 @@ pub async fn get(
.one(stat.db())
.await?
.unwrap();
let str = u.token(result).to_string(&appobj.key)?;
let str = u.token(appobj.id.clone(), result).to_string(&appobj.key)?;
Ok(str)
} else {
Err(Error::NotAuthed)
@ -92,7 +92,9 @@ pub async fn get(
.await?
.unwrap();
let str = u
.token(vec![
.token(
stat.uuid.clone(),
vec![
AccessCore {
name: "app".to_string(),
rid: None,
@ -103,7 +105,8 @@ pub async fn get(
rid: None,
level: models::AccessLevel::Read,
},
])
],
)
.to_string(&stat.key)?;
Ok(str)
}

@ -143,7 +143,10 @@ pub async fn login(
.fetch_all(stat.sqlx())
.await?;
Ok(HttpResponse::build(http::StatusCode::OK)
.insert_header(("auth_token", u.token(result).to_string(&stat.key)?))
.insert_header((
"auth_token",
u.token(stat.uuid.clone(), result).to_string(&stat.key)?,
))
.body("".to_string()))
} else {
Ok(HttpResponse::build(http::StatusCode::FORBIDDEN)

@ -0,0 +1,126 @@
//
// app.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-11-07 00:58
// Distributed under terms of the MIT license.
//
use std::{fs, path::Path};
use actix_web::web;
use dav_server::{
actix::{DavRequest, DavResponse},
body::Body,
fakels::FakeLs,
localfs::LocalFs,
DavConfig, DavHandler,
};
use http::Response;
use http_auth_basic::Credentials;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use tracing::{info, warn};
use crate::{
models::{self, UserPlugin},
AppState, Error, Result,
};
pub fn client() -> DavHandler {
DavHandler::builder()
.locksystem(FakeLs::new())
.strip_prefix("/fs/a/")
.build_handler()
}
pub async fn dav_handler(
id: web::Path<(String, String)>,
req: DavRequest,
davhandler: web::Data<DavHandler>,
stat: web::Data<AppState>,
) -> DavResponse {
let root = stat.fs_root.clone();
let id = id.into_inner().0;
info!("start app: {}", id);
match handle_file(&req, stat).await {
Ok(()) => {
let p = Path::new(&root).join(format!("app/{}/", id));
if !p.exists() {
match fs::create_dir_all(p.clone()) {
Ok(_) => {}
Err(e) => {
warn!("{}", e);
}
}
}
info!("mount {}", p.to_str().unwrap());
let config = DavConfig::new()
.filesystem(LocalFs::new(p, false, false, true))
.strip_prefix(format!("/fs/a/{}/", id));
davhandler.handle_with(config, req.request).await.into()
}
Err(e) => {
warn!("handle file failed: {}", e);
Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"file\"")
.body(Body::from("please auth".to_string()))
.unwrap()
.into()
}
}
}
async fn handle_file(
req: &dav_server::actix::DavRequest,
stat: web::Data<AppState>,
) -> Result<()> {
let p = req.request.uri();
let headers = req.request.headers();
let m = req.request.method();
info!("access {} to {}", m, p);
let authorization = headers.get("authorization");
match authorization {
Some(au) => {
if let Some((auth_type, encoded_credentials)) =
au.to_str().unwrap_or("").split_once(' ')
{
if encoded_credentials.contains(' ') {
// Invalid authorization token received
return Err(Error::InvalidToken);
}
match auth_type.to_lowercase().as_str() {
"basic" => {
let credentials = Credentials::decode(encoded_credentials.to_string())?;
info!("{}|{}", credentials.user_id, credentials.password);
if credentials.user_id == "cli" && credentials.password == "cli" {
return Ok(());
}
match models::user::Entity::find()
.filter(models::user::Column::Username.eq(credentials.user_id))
.one(stat.db())
.await?
{
Some(u) => {
u.check_pass(&credentials.password)?;
return Ok(());
}
None => {}
}
}
"bearer" => {
let t = models::Token::from(encoded_credentials, &stat.key)?;
if t.is_valid() {
return Ok(());
}
}
_ => {
return Err(Error::InvalidScheme(auth_type.to_string()));
}
};
}
}
None => {}
}
Err(Error::NotAuthed)
}

@ -0,0 +1,131 @@
//
// mod.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-11-07 00:07
// Distributed under terms of the MIT license.
//
mod app;
mod usr;
use std::future::{ready, Ready};
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
web, Error,
};
use futures_util::future::LocalBoxFuture;
pub fn routes(cfg: &mut web::ServiceConfig) {
cfg.service(
actix_web::web::scope("u")
.app_data(web::Data::new(usr::client()))
.service(web::resource("/{tail:.*}").to(usr::dav_handler)),
);
cfg.service(
actix_web::web::scope("a")
.app_data(web::Data::new(app::client()))
.service(web::resource("/{id}/{tail:.*}").to(app::dav_handler)),
);
}
pub struct FsWrap;
impl<S, B> Transform<S, ServiceRequest> for FsWrap
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = FsMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(FsMiddleware { service }))
}
}
pub struct FsMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for FsMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
println!("start fs: {}", req.path());
let reqheaders = req.headers().clone();
let is_preflight = is_request_preflight(&req);
let fut = self.service.call(req);
Box::pin(async move {
let mut res = fut.await?;
if is_preflight {
let mut rt = actix_web::HttpResponse::Ok();
if let Some(o) = reqheaders.get("Access-Control-Request-Headers") {
rt.insert_header(("Access-Control-Allow-Headers", o.to_str().unwrap_or("")));
};
if let Some(o) = reqheaders.get("Access-Control-Request-Method") {
rt.insert_header(("Access-Control-Allow-Methods", o.to_str().unwrap_or("")));
};
if let Some(o) = reqheaders.get("Origin") {
rt.insert_header(("Access-Control-Allow-Origin", o.to_str().unwrap_or("")));
};
rt.insert_header((
"Access-Control-Expose-Headers",
"access-control-allow-origin, content-type",
));
rt.insert_header(("Access-Control-Allow-Credentials", "true"));
rt.insert_header(("WWW-Authenticate", "Basic realm=\"file\""));
res = ServiceResponse::new(
res.request().to_owned(),
rt.message_body(res.into_body())?,
);
return Ok(res);
} else if let Some(o) = reqheaders.get("Origin") {
res.headers_mut().insert(
http::header::HeaderName::try_from("Access-Control-Allow-Origin").unwrap(),
o.to_owned(),
);
};
Ok(res)
})
}
}
/// Try to parse header value as HTTP method.
fn header_value_try_into_method(hdr: &http::header::HeaderValue) -> Option<http::Method> {
hdr.to_str()
.ok()
.and_then(|meth| http::Method::try_from(meth).ok())
}
fn is_request_preflight(req: &ServiceRequest) -> bool {
// check request method is OPTIONS
if req.method() != http::Method::OPTIONS {
return false;
}
// check follow-up request method is present and valid
if req
.headers()
.get(http::header::ACCESS_CONTROL_REQUEST_METHOD)
.and_then(header_value_try_into_method)
.is_none()
{
return false;
}
true
}

@ -0,0 +1,116 @@
//
// fs.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-02 22:51
// Distributed under terms of the MIT license.
//
//
use std::{fs, path::Path};
use actix_web::web;
use dav_server::{
actix::{DavRequest, DavResponse},
body::Body,
fakels::FakeLs,
localfs::LocalFs,
DavConfig, DavHandler,
};
use http::Response;
use http_auth_basic::Credentials;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use tracing::{info, warn};
use crate::{
models::{self, UserPlugin},
AppState, Error, Result,
};
pub fn client() -> DavHandler {
DavHandler::builder()
.locksystem(FakeLs::new())
.strip_prefix("/fs/u/")
.build_handler()
}
pub async fn dav_handler(
req: DavRequest,
davhandler: web::Data<DavHandler>,
stat: web::Data<AppState>,
) -> DavResponse {
let root = stat.fs_root.clone();
match handle_file(&req, stat).await {
Ok(p) => {
let p = Path::new(&root).join(p);
if !p.exists() {
match fs::create_dir_all(p.clone()) {
Ok(_) => {}
Err(e) => {
warn!("{}", e);
}
}
}
info!("mount {}", p.to_str().unwrap());
let config = DavConfig::new().filesystem(LocalFs::new(p, false, false, true));
davhandler.handle_with(config, req.request).await.into()
}
Err(e) => {
warn!("handle file failed: {}", e);
Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"file\"")
.body(Body::from("please auth".to_string()))
.unwrap()
.into()
}
}
}
async fn handle_file(req: &DavRequest, stat: web::Data<AppState>) -> Result<String> {
let p = req.request.uri();
let headers = req.request.headers();
let m = req.request.method();
info!("access {} to {}", m, p);
let authorization = headers.get("authorization");
match authorization {
Some(au) => {
if let Some((auth_type, encoded_credentials)) =
au.to_str().unwrap_or("").split_once(' ')
{
if encoded_credentials.contains(' ') {
// Invalid authorization token received
return Err(Error::InvalidToken);
}
match auth_type.to_lowercase().as_str() {
"basic" => {
let credentials = Credentials::decode(encoded_credentials.to_string())?;
info!("{}|{}", credentials.user_id, credentials.password);
match models::user::Entity::find()
.filter(models::user::Column::Username.eq(credentials.user_id))
.one(stat.db())
.await?
{
Some(u) => {
u.check_pass(&credentials.password)?;
return Ok(format!("user/{}/", u.id));
}
None => {}
}
}
"bearer" => {
let t = models::Token::from(encoded_credentials, &stat.key)?;
if t.is_valid() {
return Ok(format!("user/{}/", t.id));
}
}
_ => {
return Err(Error::InvalidScheme(auth_type.to_string()));
}
};
}
}
None => {}
}
Err(Error::NotAuthed)
}

@ -7,6 +7,7 @@
pub mod api;
mod cfg;
pub mod fs;
pub mod libs;
pub mod models;
mod result;

@ -1,178 +0,0 @@
//
// fs.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-02 22:51
// Distributed under terms of the MIT license.
//
//
use std::{fs, path::Path};
use actix_web::web;
use dav_server::{
actix::{DavRequest, DavResponse},
body::Body,
fakels::FakeLs,
localfs::LocalFs,
DavConfig, DavHandler,
};
use http::{header, Method, Response};
use http_auth_basic::Credentials;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use tracing::{info, warn};
use crate::{
models::{self, UserPlugin},
AppState, Error, Result,
};
pub fn core() -> DavHandler {
DavHandler::builder()
.locksystem(FakeLs::new())
.strip_prefix("/file/")
.build_handler()
}
/// Try to parse header value as HTTP method.
fn header_value_try_into_method(hdr: &header::HeaderValue) -> Option<Method> {
hdr.to_str()
.ok()
.and_then(|meth| Method::try_from(meth).ok())
}
fn is_request_preflight(req: &DavRequest) -> bool {
// check request method is OPTIONS
if req.request.method() != Method::OPTIONS {
return false;
}
// check follow-up request method is present and valid
if req
.request
.headers()
.get(header::ACCESS_CONTROL_REQUEST_METHOD)
.and_then(header_value_try_into_method)
.is_none()
{
return false;
}
true
}
pub async fn dav_handler(
req: DavRequest,
davhandler: web::Data<DavHandler>,
stat: web::Data<AppState>,
) -> DavResponse {
let root = stat.fs_root.clone();
match handle_file(&req, stat).await {
Ok(p) => {
let p = Path::new(&root).join(p);
if !p.exists() {
match fs::create_dir_all(p.clone()) {
Ok(_) => {}
Err(e) => {
warn!("{}", e);
}
}
}
info!("mount {}", p.to_str().unwrap());
let config = DavConfig::new().filesystem(LocalFs::new(p, false, false, true));
davhandler.handle_with(config, req.request).await.into()
}
Err(e) => {
warn!("handle file failed: {}", e);
if is_request_preflight(&req) {
let origin = match req.request.headers().get("Origin") {
Some(o) => o.to_str().unwrap(),
None => "",
};
let allowed_headers =
match req.request.headers().get("Access-Control-Request-Headers") {
Some(o) => o.to_str().unwrap(),
None => "",
};
let allowed_method =
match req.request.headers().get("Access-Control-Request-Method") {
Some(o) => o.to_str().unwrap(),
None => "",
};
Response::builder()
.status(200)
.header("WWW-Authenticate", "Basic realm=\"file\"")
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Credentials", "true")
.header("Access-Control-Allow-Headers", allowed_headers)
.header("Access-Control-Allow-Methods", allowed_method)
.header(
"Access-Control-Expose-Headers",
"access-control-allow-origin, content-type",
)
.body(Body::from("please auth".to_string()))
.unwrap()
.into()
} else {
Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"file\"")
.body(Body::from("please auth".to_string()))
.unwrap()
.into()
}
}
}
}
async fn handle_file(req: &DavRequest, stat: web::Data<AppState>) -> Result<String> {
let p = req.request.uri();
let headers = req.request.headers();
let m = req.request.method();
// handle_authorization(req.request.headers());
info!("access {} to {}", m, p);
let auth_token = headers.get("auth_token");
let authorization = headers.get("authorization");
let app_id = match headers.get("app_id") {
Some(i) => i.to_str().unwrap_or(""),
None => "",
};
match auth_token {
Some(t) => match models::Token::from(t.to_str().unwrap_or(""), &stat.key) {
Ok(t) => {
if t.is_valid() {
if app_id != "" {
// 只有秘钥才能访问app数据
if t.can_read("app", app_id) {
return Ok(format!("app/{}/", app_id));
}
} else {
return Ok(format!("user/{}/", t.id));
}
}
}
Err(_) => {}
},
None => {}
}
match authorization {
Some(au) => {
let credentials =
Credentials::from_header(au.to_str().unwrap_or("").to_string()).unwrap();
info!("{}|{}", credentials.user_id, credentials.password);
match models::user::Entity::find()
.filter(models::user::Column::Username.eq(credentials.user_id))
.one(stat.db())
.await?
{
Some(u) => {
u.check_pass(&credentials.password)?;
return Ok(format!("user/{}/", u.id));
}
None => {}
}
}
None => {}
}
Err(Error::NotAuthed)
}

@ -6,9 +6,9 @@
//
pub mod app;
pub mod appfs;
pub mod auth;
pub mod cors;
pub mod fs;
pub mod proxy;
pub mod task;
pub mod user;

@ -43,7 +43,7 @@ pub fn start_stats_info(url: String) {
s.refresh_process_specifics(pid, props);
if let Some(process) = s.process(pid) {
let stat_str = format!(
"oa_stats_cpu {}\noa_stats_mem {}\noa_stats_start {}",
"srv_cpu{{i=\"oa\"}} {}\nsrv_mem{{i=\"oa\"}} {}\nsrv_start{{i=\"oa\"}} {}",
process.cpu_usage(),
process.memory(),
start.elapsed().as_secs(),

@ -5,19 +5,17 @@
// Distributed under terms of the Apache license.
//
use actix_files as fs;
use actix_files;
use actix_web::{
dev::{self, Service},
dev::{self},
http::StatusCode,
middleware::{self, ErrorHandlerResponse, ErrorHandlers},
web::{self},
App, HttpResponse, HttpServer, Responder,
};
use futures_util::future::FutureExt;
use mime_guess::from_path;
use rust_embed::RustEmbed;
use http::{HeaderName, HeaderValue};
use oab::{api, init_log, libs, models, AppCli, AppState, Clis, Result};
use tracing::{error, info, warn};
@ -54,7 +52,6 @@ async fn web(data: AppState) -> Result<()> {
// libs::task::start_nats_online(client.clone());
libs::task::start_stats_info(data.ts_url.clone());
let url = data.server_url.clone();
let dav = libs::fs::core();
let serv = HttpServer::new(move || {
let logger = middleware::Logger::default();
let json_config = web::JsonConfig::default()
@ -80,7 +77,9 @@ async fn web(data: AppState) -> Result<()> {
app.wrap(logger)
.wrap(middleware::Compress::default())
.app_data(web::Data::new(data.clone()))
.service(fs::Files::new("/media", data.media_path.clone()).show_files_listing())
.service(
actix_files::Files::new("/media", data.media_path.clone()).show_files_listing(),
)
.service(
web::scope("api")
.wrap(cors)
@ -95,31 +94,9 @@ async fn web(data: AppState) -> Result<()> {
.configure(api::routes),
)
.service(
web::scope("file")
.wrap_fn(|req, srv| {
let headers = &req.headers().clone();
let origin = match headers.get("Origin") {
Some(o) => o.to_str().unwrap().to_string(),
None => "".to_string(),
};
srv.call(req).map(move |res| {
let res = match res {
Ok(mut expr) => {
let headers = expr.headers_mut();
headers.insert(
HeaderName::try_from("Access-Control-Allow-Origin")
.unwrap(),
HeaderValue::from_str(&origin).unwrap(),
);
Ok(expr)
}
Err(e) => Err(e),
};
res
})
})
.app_data(web::Data::new(dav.clone()))
.service(web::resource("/{tail:.*}").to(libs::fs::dav_handler)),
web::scope("fs")
.wrap(oab::fs::FsWrap {})
.configure(oab::fs::routes),
)
.service(index)
});

@ -51,14 +51,14 @@ pub struct AccessCore {
}
pub trait UserPlugin {
fn token(&self, ac: Vec<AccessCore>) -> Token;
fn token(&self, aid: String, ac: Vec<AccessCore>) -> Token;
fn check_pass(&self, p: &str) -> Result<()>;
fn update_pass(&mut self, p: &str) -> Result<()>;
}
// impl User {
impl UserPlugin for super::entity::user::Model {
fn token(&self, ac: Vec<AccessCore>) -> Token {
fn token(&self, aid: String, ac: Vec<AccessCore>) -> Token {
let default_ico = "/media/".to_string();
let t = Token {
iss: "oa".to_string(),
@ -66,6 +66,7 @@ impl UserPlugin for super::entity::user::Model {
exp: (Utc::now() + Duration::days(4)).timestamp(),
iat: Utc::now().timestamp(),
id: self.id.clone(),
aid: aid,
icon: self.icon.as_ref().unwrap_or(&default_ico).to_string(),
access: Some(ac),
nickname: self
@ -174,6 +175,7 @@ pub struct Token {
pub id: String, // 用户id
pub nickname: String,
pub icon: String,
pub aid: String,
pub access: Option<Vec<AccessCore>>,
}

@ -86,9 +86,12 @@ pub enum Error {
InvalidVerifyCode,
#[error("invalid token")]
InvalidToken,
#[error("invalid token scheme: {0}")]
InvalidScheme(String),
#[error("expired token")]
ExpiredToken,
// #[error("basic auth error")]
// InvalidBasicAuthError(#[from] http_auth_basic::AuthBasicError),
#[error("no access")]
NotAuthed,
#[error("login failed")]
@ -172,12 +175,22 @@ impl From<actix_multipart::MultipartError> for Error {
Error::BusinessException(format!("{:?}", e))
}
}
impl From<http_auth_basic::AuthBasicError> for Error {
fn from(value: http_auth_basic::AuthBasicError) -> Self {
Error::BusinessException(format!("{:?}", value))
}
}
impl From<Box<dyn std::fmt::Display>> for Error {
fn from(e: Box<dyn std::fmt::Display>) -> Self {
Error::BusinessException(format!("{}", e))
}
}
impl From<&str> for Error {
fn from(value: &str) -> Self {
Error::BusinessException(format!("{}", value))
}
}
impl actix_web::Responder for Error {
type Body = actix_web::body::BoxBody;

@ -122,7 +122,7 @@ module.exports = configure(function(/* ctx */) {
changeOrigin: true,
ws: true,
},
'/file': {
'/fs': {
target: 'http://127.0.0.1:4001/',
changeOrigin: true,
ws: true,

File diff suppressed because one or more lines are too long

@ -39,20 +39,20 @@ const querys = ref<{
}[]>([
{
name: 'cpu',
query: `oa_stats_cpu`,
query: `srv_cpu{i='oa'}`,
label: 'cpu',
valueFormatter: (value: number) => value.toFixed(2) + "%",
},
{
name: '内存',
query: `oa_stats_mem / 1048576`,
query: `srv_mem{i='oa'} / 1048576`,
label: '内存',
valueFormatter: (value: number) => value.toFixed(2) + "MB",
},
])
onMounted(() => {
api.tsdb.query('oa_stats_start').then(e => {
api.tsdb.query('srv_start{i="oa"}').then(e => {
if (e.data.result.length) {
let s = Number(e.data.result[0].value[1])
if (s < 60) {

Loading…
Cancel
Save