* removed api client macro * fixed passing --release mode in cargo make * optimized integration tests * added logging module (tracing) * allow u_panel to alter entries * reworked u_panel args (CRUD) * improved db hooks * started implementing web-interface ** incapsulated all frontend in binary ** setup workflow ** make u_panel accept commands from interfacepull/1/head
parent
83252b6f95
commit
c70cdbd262
47 changed files with 808 additions and 651 deletions
@ -0,0 +1,17 @@ |
|||||||
|
use actix_web::http::StatusCode; |
||||||
|
use actix_web::ResponseError; |
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)] |
||||||
|
pub enum Error { |
||||||
|
#[error("Arg parse error: {0}")] |
||||||
|
ArgparseError(#[from] structopt::clap::Error), |
||||||
|
|
||||||
|
#[error("Just an error: {0}")] |
||||||
|
JustError(String), |
||||||
|
} |
||||||
|
|
||||||
|
impl ResponseError for Error { |
||||||
|
fn status_code(&self) -> actix_web::http::StatusCode { |
||||||
|
StatusCode::BAD_REQUEST |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files. |
||||||
|
|
||||||
|
# compiled output |
||||||
|
/dist |
||||||
|
/tmp |
||||||
|
/out-tsc |
||||||
|
# Only exists if Bazel was run |
||||||
|
/bazel-out |
||||||
|
|
||||||
|
# dependencies |
||||||
|
/node_modules |
||||||
|
|
||||||
|
# profiling files |
||||||
|
chrome-profiler-events*.json |
||||||
|
|
||||||
|
# IDEs and editors |
||||||
|
/.idea |
||||||
|
.project |
||||||
|
.classpath |
||||||
|
.c9/ |
||||||
|
*.launch |
||||||
|
.settings/ |
||||||
|
*.sublime-workspace |
||||||
|
|
||||||
|
# IDE - VSCode |
||||||
|
.vscode/* |
||||||
|
!.vscode/settings.json |
||||||
|
!.vscode/tasks.json |
||||||
|
!.vscode/launch.json |
||||||
|
!.vscode/extensions.json |
||||||
|
.history/* |
||||||
|
|
||||||
|
# misc |
||||||
|
/.angular/cache |
||||||
|
/.sass-cache |
||||||
|
/connect.lock |
||||||
|
/coverage |
||||||
|
/libpeerconnection.log |
||||||
|
npm-debug.log |
||||||
|
yarn-error.log |
||||||
|
testem.log |
||||||
|
/typings |
||||||
|
|
||||||
|
# System Files |
||||||
|
.DS_Store |
||||||
|
Thumbs.db |
||||||
|
|
||||||
|
package-lock.json |
@ -1 +1,49 @@ |
|||||||
<span>{{ title }}</span> |
<mat-tab-group animationDuration="0ms" mat-align-tabs="center"> |
||||||
|
<mat-tab label="Agents"> |
||||||
|
<div class="example-container mat-elevation-z8"> |
||||||
|
<div class="example-loading-shade" *ngIf="isLoadingResults"> |
||||||
|
<mat-spinner *ngIf="isLoadingResults"></mat-spinner> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="example-table-container"> |
||||||
|
|
||||||
|
<table mat-table [dataSource]="table_data" class="example-table" matSort matSortActive="id" matSortDisableClear |
||||||
|
matSortDirection="desc"> |
||||||
|
|
||||||
|
<ng-container matColumnDef="id"> |
||||||
|
<th mat-header-cell *matHeaderCellDef>id</th> |
||||||
|
<td mat-cell *matCellDef="let row">{{row.id}}</td> |
||||||
|
</ng-container> |
||||||
|
|
||||||
|
|
||||||
|
<ng-container matColumnDef="alias"> |
||||||
|
<th mat-header-cell *matHeaderCellDef>Alias</th> |
||||||
|
<td mat-cell *matCellDef="let row">{{row.alias}}</td> |
||||||
|
</ng-container> |
||||||
|
|
||||||
|
<ng-container matColumnDef="username"> |
||||||
|
<th mat-header-cell *matHeaderCellDef>user@hostname</th> |
||||||
|
<td mat-cell *matCellDef="let row">{{row.username}}@{{row.hostname}}</td> |
||||||
|
</ng-container> |
||||||
|
|
||||||
|
<ng-container matColumnDef="last_active"> |
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> |
||||||
|
Last active |
||||||
|
</th> |
||||||
|
<td mat-cell *matCellDef="let row">{{row.last_active}}</td> |
||||||
|
</ng-container> |
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> |
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> |
||||||
|
</table> |
||||||
|
<button mat-raised-button (click)="fetch_agents()">Refresh</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results"> |
||||||
|
</mat-paginator> --> |
||||||
|
</div> |
||||||
|
|
||||||
|
</mat-tab> |
||||||
|
<mat-tab label="Jobs"></mat-tab> |
||||||
|
<mat-tab label="Results"></mat-tab> |
||||||
|
</mat-tab-group> |
@ -1,10 +1,82 @@ |
|||||||
import { Component } from '@angular/core'; |
import { HttpClient } from '@angular/common/http'; |
||||||
|
import { Component, ViewChild, AfterViewInit } from '@angular/core'; |
||||||
|
import { timer, Observable, of as observableOf } from 'rxjs'; |
||||||
|
import { catchError, map, startWith, switchMap } from 'rxjs/operators'; |
||||||
|
|
||||||
|
interface Agent { |
||||||
|
alias: string | null, |
||||||
|
hostname: string, |
||||||
|
id: string, |
||||||
|
is_root: boolean, |
||||||
|
is_root_allowed: boolean, |
||||||
|
last_active: Date, |
||||||
|
platform: string, |
||||||
|
regtime: Date, |
||||||
|
state: "new" | "active" | "banned", |
||||||
|
token: string | null, |
||||||
|
username: string, |
||||||
|
} |
||||||
|
|
||||||
@Component({ |
@Component({ |
||||||
selector: 'app-root', |
selector: 'app-root', |
||||||
templateUrl: './app.component.html', |
templateUrl: './app.component.html', |
||||||
styleUrls: ['./app.component.less'] |
styleUrls: ['./app.component.less'] |
||||||
}) |
}) |
||||||
export class AppComponent { |
export class AppComponent implements AfterViewInit { |
||||||
title = 'ты лох'; |
displayedColumns: string[] = ['id', 'alias', 'username', 'last_active']; |
||||||
|
exampleDatabase!: ExampleHttpDatabase | null; |
||||||
|
|
||||||
|
table_data: Agent[] = []; |
||||||
|
isLoadingResults = true; |
||||||
|
|
||||||
|
constructor(private _httpClient: HttpClient) { } |
||||||
|
|
||||||
|
ngAfterViewInit() { |
||||||
|
this.exampleDatabase = new ExampleHttpDatabase(this._httpClient); |
||||||
|
this.fetch_agents(); |
||||||
|
// If the user changes the sort order, reset back to the first page.
|
||||||
|
//this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0));
|
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
fetch_agents() { |
||||||
|
timer(0) |
||||||
|
.pipe( |
||||||
|
startWith({}), |
||||||
|
switchMap(() => { |
||||||
|
this.isLoadingResults = true; |
||||||
|
return this.exampleDatabase!.getAgents().pipe(catchError(() => observableOf(null))); |
||||||
|
}), |
||||||
|
map(data => { |
||||||
|
// Flip flag to show that loading has finished.
|
||||||
|
this.isLoadingResults = false; |
||||||
|
|
||||||
|
if (data === null) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
// Only refresh the result length if there is new data. In case of rate
|
||||||
|
// limit errors, we do not want to reset the paginator to zero, as that
|
||||||
|
// would prevent users from re-triggering requests.
|
||||||
|
return data.data; |
||||||
|
}), |
||||||
|
) |
||||||
|
.subscribe(data => { if (typeof data !== 'string') { this.table_data = data } else { alert(`Error: ${data}`) } }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
interface ServerResponse<T> { |
||||||
|
status: "ok" | "err", |
||||||
|
data: T | string |
||||||
|
} |
||||||
|
|
||||||
|
class ExampleHttpDatabase { |
||||||
|
constructor(private _httpClient: HttpClient) { } |
||||||
|
|
||||||
|
getAgents(): Observable<ServerResponse<Agent[]>> { |
||||||
|
const requestUrl = "/cmd/"; |
||||||
|
const cmd = "agents list"; |
||||||
|
|
||||||
|
return this._httpClient.post<ServerResponse<Agent[]>>(requestUrl, cmd); |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -1 +1,4 @@ |
|||||||
/* You can add global styles to this file, and also import other style files */ |
/* You can add global styles to this file, and also import other style files */ |
||||||
|
|
||||||
|
html, body { height: 100%; } |
||||||
|
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } |
||||||
|
@ -1,136 +0,0 @@ |
|||||||
use crate::handlers::Endpoints; |
|
||||||
use crate::{db::UDB, errors::SResult}; |
|
||||||
use serde::de::DeserializeOwned; |
|
||||||
use std::path::PathBuf; |
|
||||||
use u_lib::{ |
|
||||||
messaging::{AsMsg, BaseMessage, Reportable}, |
|
||||||
models::*, |
|
||||||
}; |
|
||||||
use uuid::Uuid; |
|
||||||
use warp::{ |
|
||||||
body, |
|
||||||
reply::{json, reply, Json}, |
|
||||||
Filter, Rejection, Reply, |
|
||||||
}; |
|
||||||
|
|
||||||
fn get_content<M>() -> impl Filter<Extract = (BaseMessage<'static, M>,), Error = Rejection> + Clone |
|
||||||
where |
|
||||||
M: AsMsg + Sync + Send + DeserializeOwned + 'static, |
|
||||||
{ |
|
||||||
body::content_length_limit(1024 * 64).and(body::json::<BaseMessage<M>>()) |
|
||||||
} |
|
||||||
|
|
||||||
fn into_message<M: AsMsg>(msg: M) -> Json { |
|
||||||
json(&msg.as_message()) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn init_filters( |
|
||||||
auth_token: &str, |
|
||||||
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone { |
|
||||||
let infallible_none = |_| async { Ok::<(Option<Uuid>,), std::convert::Infallible>((None,)) }; |
|
||||||
|
|
||||||
let get_agents = warp::get() |
|
||||||
.and(warp::path("get_agents")) |
|
||||||
.and( |
|
||||||
warp::path::param::<Uuid>() |
|
||||||
.map(Some) |
|
||||||
.or_else(infallible_none), |
|
||||||
) |
|
||||||
.and_then(Endpoints::get_agents) |
|
||||||
.map(into_message); |
|
||||||
|
|
||||||
let upload_jobs = warp::post() |
|
||||||
.and(warp::path("upload_jobs")) |
|
||||||
.and(get_content::<Vec<JobMeta>>()) |
|
||||||
.and_then(Endpoints::upload_jobs) |
|
||||||
.map(|_| reply()); |
|
||||||
|
|
||||||
let get_jobs = warp::get() |
|
||||||
.and(warp::path("get_jobs")) |
|
||||||
.and( |
|
||||||
warp::path::param::<Uuid>() |
|
||||||
.map(Some) |
|
||||||
.or_else(infallible_none), |
|
||||||
) |
|
||||||
.and_then(Endpoints::get_jobs) |
|
||||||
.map(into_message); |
|
||||||
|
|
||||||
let get_agent_jobs = warp::get() |
|
||||||
.and(warp::path("get_agent_jobs")) |
|
||||||
.and( |
|
||||||
warp::path::param::<Uuid>() |
|
||||||
.map(Some) |
|
||||||
.or_else(infallible_none), |
|
||||||
) |
|
||||||
.and_then(Endpoints::get_agent_jobs) |
|
||||||
.map(into_message); |
|
||||||
|
|
||||||
let get_personal_jobs = warp::get() |
|
||||||
.and(warp::path("get_personal_jobs")) |
|
||||||
.and(warp::path::param::<Uuid>().map(Some)) |
|
||||||
.and_then(Endpoints::get_personal_jobs) |
|
||||||
.map(into_message); |
|
||||||
|
|
||||||
let del = warp::get() |
|
||||||
.and(warp::path("del")) |
|
||||||
.and(warp::path::param::<Uuid>()) |
|
||||||
.and_then(Endpoints::del) |
|
||||||
.map(|_| reply()); |
|
||||||
|
|
||||||
let set_jobs = warp::post() |
|
||||||
.and(warp::path("set_jobs")) |
|
||||||
.and(warp::path::param::<Uuid>()) |
|
||||||
.and(get_content::<Vec<String>>()) |
|
||||||
.and_then(Endpoints::set_jobs) |
|
||||||
.map(into_message); |
|
||||||
|
|
||||||
let report = warp::post() |
|
||||||
.and(warp::path("report")) |
|
||||||
.and(get_content::<Vec<Reportable>>()) |
|
||||||
.and_then(Endpoints::report) |
|
||||||
.map(|_| reply()); |
|
||||||
|
|
||||||
let auth_token = format!("Bearer {auth_token}",).into_boxed_str(); |
|
||||||
let auth_header = warp::header::exact("authorization", Box::leak(auth_token)); |
|
||||||
|
|
||||||
let auth_zone = (get_agents |
|
||||||
.or(get_jobs) |
|
||||||
.or(upload_jobs) |
|
||||||
.or(del) |
|
||||||
.or(set_jobs) |
|
||||||
.or(get_agent_jobs)) |
|
||||||
.and(auth_header); |
|
||||||
|
|
||||||
let agent_zone = get_jobs.clone().or(get_personal_jobs).or(report); |
|
||||||
|
|
||||||
auth_zone.or(agent_zone) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn prefill_jobs() -> SResult<()> { |
|
||||||
let agent_hello = JobMeta::builder() |
|
||||||
.with_type(misc::JobType::Manage) |
|
||||||
.with_alias("agent_hello") |
|
||||||
.build() |
|
||||||
.unwrap(); |
|
||||||
UDB::lock_db().insert_jobs(&[agent_hello]) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn init_logger() { |
|
||||||
use simplelog::*; |
|
||||||
use std::fs::OpenOptions; |
|
||||||
let log_cfg = ConfigBuilder::new() |
|
||||||
.set_time_format_str("%x %X") |
|
||||||
.set_time_to_local(true) |
|
||||||
.build(); |
|
||||||
let logfile = OpenOptions::new() |
|
||||||
.append(true) |
|
||||||
.create(true) |
|
||||||
.open(PathBuf::from("logs").join("u_server.log")) |
|
||||||
.unwrap(); |
|
||||||
let level = LevelFilter::Info; |
|
||||||
let loggers = vec![ |
|
||||||
WriteLogger::new(level, log_cfg.clone(), logfile) as Box<dyn SharedLogger>, |
|
||||||
TermLogger::new(level, log_cfg, TerminalMode::Stderr, ColorChoice::Auto), |
|
||||||
]; |
|
||||||
CombinedLogger::init(loggers).unwrap(); |
|
||||||
} |
|
@ -1,4 +1,4 @@ |
|||||||
FROM rust:1.60 |
FROM rust:1.62 |
||||||
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl |
RUN rustup target add x86_64-unknown-linux-musl |
||||||
CMD ["sleep", "3600"] |
CMD ["sleep", "3600"] |
@ -1,16 +0,0 @@ |
|||||||
[package] |
|
||||||
name = "u_api_proc_macro" |
|
||||||
version = "0.1.0" |
|
||||||
authors = ["plazmoid <kronos44@mail.ru>"] |
|
||||||
edition = "2018" |
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
|
||||||
|
|
||||||
[lib] |
|
||||||
proc-macro = true |
|
||||||
|
|
||||||
[dependencies] |
|
||||||
syn = { version = "1.0", features = ["full", "extra-traits"] } |
|
||||||
quote = "1.0" |
|
||||||
strum = { version = "0.20", features = ["derive"] } |
|
||||||
proc-macro2 = "1.0" |
|
@ -1,181 +0,0 @@ |
|||||||
use proc_macro::TokenStream; |
|
||||||
use proc_macro2::{Ident, TokenStream as TokenStream2}; |
|
||||||
use quote::quote; |
|
||||||
use std::{collections::HashMap, str::FromStr}; |
|
||||||
use strum::EnumString; |
|
||||||
use syn::{ |
|
||||||
parse_macro_input, punctuated::Punctuated, AttributeArgs, FnArg, ItemFn, Lit, NestedMeta, |
|
||||||
ReturnType, Signature, Token, Type, |
|
||||||
}; |
|
||||||
|
|
||||||
#[derive(EnumString, Debug)] |
|
||||||
enum ReqMethod { |
|
||||||
GET, |
|
||||||
POST, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug)] |
|
||||||
struct Endpoint { |
|
||||||
method: ReqMethod, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug)] |
|
||||||
struct FnArgs { |
|
||||||
url_param: Option<Type>, |
|
||||||
payload: Option<Type>, |
|
||||||
} |
|
||||||
|
|
||||||
#[proc_macro_attribute] |
|
||||||
pub fn api_route(args: TokenStream, item: TokenStream) -> TokenStream { |
|
||||||
let args: AttributeArgs = parse_macro_input!(args); |
|
||||||
let input: ItemFn = parse_macro_input!(item); |
|
||||||
let Signature { |
|
||||||
ident, |
|
||||||
inputs, |
|
||||||
generics, |
|
||||||
output, |
|
||||||
.. |
|
||||||
} = input.sig; |
|
||||||
let (impl_generics, _, _) = generics.split_for_impl(); |
|
||||||
let FnArgs { url_param, payload } = parse_fn_args(inputs); |
|
||||||
let Endpoint { method } = parse_attr_args(args); |
|
||||||
let url_path = build_url_path(&ident, &url_param); |
|
||||||
let return_ty = match output { |
|
||||||
ReturnType::Type(_, ty) => quote!(#ty), |
|
||||||
ReturnType::Default => quote!(()), |
|
||||||
}; |
|
||||||
let request = match method { |
|
||||||
ReqMethod::GET => build_get(url_path), |
|
||||||
ReqMethod::POST => build_post(url_path, &payload), |
|
||||||
}; |
|
||||||
let url_param = match url_param { |
|
||||||
Some(p) => quote!(, param: #p), |
|
||||||
None => TokenStream2::new(), |
|
||||||
}; |
|
||||||
let payload = match payload { |
|
||||||
Some(p) => quote!(, payload: #p), |
|
||||||
None => TokenStream2::new(), |
|
||||||
}; |
|
||||||
let q = quote! { |
|
||||||
pub async fn #ident #impl_generics( |
|
||||||
&self #url_param #payload |
|
||||||
) -> UResult<#return_ty> { |
|
||||||
let request = { |
|
||||||
#request |
|
||||||
}; |
|
||||||
let response = request.send().await?; |
|
||||||
let content_len = response.content_length(); |
|
||||||
let is_success = match response.error_for_status_ref() { |
|
||||||
Ok(_) => Ok(()), |
|
||||||
Err(e) => Err(UError::from(e)) |
|
||||||
}; |
|
||||||
let resp = response.text().await?; |
|
||||||
let result = match is_success { |
|
||||||
Ok(_) => { |
|
||||||
serde_json::from_str::<BaseMessage<#return_ty>>(&resp) |
|
||||||
.map(|msg| msg.into_inner()) |
|
||||||
.or_else(|e| { |
|
||||||
match content_len { |
|
||||||
Some(0) => Ok(Default::default()), |
|
||||||
_ => Err(UError::NetError(e.to_string(), resp.clone())) |
|
||||||
} |
|
||||||
}) |
|
||||||
}, |
|
||||||
Err(UError::NetError(err_src, _)) => Err( |
|
||||||
UError::NetError( |
|
||||||
err_src, |
|
||||||
resp |
|
||||||
) |
|
||||||
), |
|
||||||
_ => unreachable!() |
|
||||||
}; |
|
||||||
Ok(result?) |
|
||||||
} |
|
||||||
}; |
|
||||||
q.into() |
|
||||||
} |
|
||||||
|
|
||||||
fn parse_fn_args(raw: Punctuated<FnArg, Token![,]>) -> FnArgs { |
|
||||||
let mut arg: HashMap<String, Type> = raw |
|
||||||
.into_iter() |
|
||||||
.filter_map(|arg| { |
|
||||||
if let FnArg::Typed(argt) = arg { |
|
||||||
let mut arg_name = String::new(); |
|
||||||
// did you think I won't overplay you? won't destroy?
|
|
||||||
|arg_ident| -> TokenStream { |
|
||||||
let q: TokenStream = quote!(#arg_ident).into(); |
|
||||||
arg_name = parse_macro_input!(q as Ident).to_string(); |
|
||||||
TokenStream::new() |
|
||||||
}(argt.pat); |
|
||||||
if &arg_name != "url_param" && &arg_name != "payload" { |
|
||||||
panic!("Wrong arg name: {}", &arg_name) |
|
||||||
} |
|
||||||
let arg_type = *argt.ty; |
|
||||||
Some((arg_name, arg_type)) |
|
||||||
} else { |
|
||||||
None |
|
||||||
} |
|
||||||
}) |
|
||||||
.collect(); |
|
||||||
FnArgs { |
|
||||||
url_param: arg.remove("url_param"), |
|
||||||
payload: arg.remove("payload"), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn build_get(url: TokenStream2) -> TokenStream2 { |
|
||||||
quote! { |
|
||||||
let request = self.build_get(#url); |
|
||||||
request |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn build_post(url: TokenStream2, payload: &Option<Type>) -> TokenStream2 { |
|
||||||
let pld = match payload { |
|
||||||
Some(_) => quote! { |
|
||||||
.json(&payload.as_message()) |
|
||||||
}, |
|
||||||
None => TokenStream2::new(), |
|
||||||
}; |
|
||||||
quote! { |
|
||||||
let request = self.build_post(#url); |
|
||||||
request #pld |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn build_url_path(path: &Ident, url_param: &Option<Type>) -> TokenStream2 { |
|
||||||
let url_param = match url_param { |
|
||||||
Some(_) => quote! { |
|
||||||
+ &opt_to_string(param) |
|
||||||
}, |
|
||||||
None => TokenStream2::new(), |
|
||||||
}; |
|
||||||
quote! { |
|
||||||
&format!( |
|
||||||
"{}/{}", |
|
||||||
stringify!(#path), |
|
||||||
String::new() #url_param |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn parse_attr_args(args: AttributeArgs) -> Endpoint { |
|
||||||
let mut args = args.into_iter(); |
|
||||||
let method = match args.next() { |
|
||||||
Some(method) => match method { |
|
||||||
NestedMeta::Lit(l) => { |
|
||||||
if let Lit::Str(s) = l { |
|
||||||
match ReqMethod::from_str(&s.value()) { |
|
||||||
Ok(v) => v, |
|
||||||
Err(_) => panic!("Unknown method"), |
|
||||||
} |
|
||||||
} else { |
|
||||||
panic!("Method must be a str") |
|
||||||
} |
|
||||||
} |
|
||||||
_ => panic!("Method must be on the first place"), |
|
||||||
}, |
|
||||||
None => panic!("Method required"), |
|
||||||
}; |
|
||||||
Endpoint { method } |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
/* |
|
||||||
use std::fmt::Display; |
|
||||||
use u_api_proc_macro::api_route; |
|
||||||
|
|
||||||
type UResult<T, E> = Result<T, E>; |
|
||||||
|
|
||||||
struct ClientHandler; |
|
||||||
struct Paths; |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test1() { |
|
||||||
#[api_route("GET", Uuid)] |
|
||||||
fn list<T: Display>(url_param: T) {} |
|
||||||
} |
|
||||||
*/ |
|
@ -1,98 +1,134 @@ |
|||||||
|
use std::collections::HashMap; |
||||||
|
|
||||||
use crate::{ |
use crate::{ |
||||||
config::MASTER_PORT, |
config::MASTER_PORT, |
||||||
messaging::{self, AsMsg, BaseMessage}, |
messaging::{self, AsMsg, BaseMessage, Empty}, |
||||||
models, |
models::{self, Agent}, |
||||||
utils::{opt_to_string, VecDisplay}, |
utils::opt_to_string, |
||||||
UError, UResult, |
UError, UResult, |
||||||
}; |
}; |
||||||
use reqwest::{Certificate, Client, Identity, RequestBuilder, Url}; |
use reqwest::{header::HeaderMap, Certificate, Client, Identity, Url}; |
||||||
use u_api_proc_macro::api_route; |
use serde::de::DeserializeOwned; |
||||||
use uuid::Uuid; |
use uuid::Uuid; |
||||||
|
|
||||||
const AGENT_IDENTITY: &[u8] = include_bytes!("../../../certs/alice.p12"); |
const AGENT_IDENTITY: &[u8] = include_bytes!("../../../certs/alice.p12"); |
||||||
const ROOT_CA_CERT: &[u8] = include_bytes!("../../../certs/ca.crt"); |
const ROOT_CA_CERT: &[u8] = include_bytes!("../../../certs/ca.crt"); |
||||||
|
|
||||||
|
#[derive(Clone)] |
||||||
pub struct ClientHandler { |
pub struct ClientHandler { |
||||||
base_url: Url, |
base_url: Url, |
||||||
client: Client, |
client: Client, |
||||||
password: Option<String>, |
|
||||||
} |
} |
||||||
|
|
||||||
impl ClientHandler { |
impl ClientHandler { |
||||||
pub fn new(server: &str) -> Self { |
pub fn new(server: &str, password: Option<String>) -> Self { |
||||||
let identity = Identity::from_pkcs12_der(AGENT_IDENTITY, "").unwrap(); |
let identity = Identity::from_pkcs12_der(AGENT_IDENTITY, "").unwrap(); |
||||||
let client = Client::builder() |
let mut client = Client::builder().identity(identity); |
||||||
.identity(identity) |
if let Some(pwd) = password { |
||||||
|
client = client.default_headers( |
||||||
|
HeaderMap::try_from(&HashMap::from([( |
||||||
|
"Authorization".to_string(), |
||||||
|
format!("Bearer {pwd}"), |
||||||
|
)])) |
||||||
|
.unwrap(), |
||||||
|
) |
||||||
|
} |
||||||
|
let client = client |
||||||
.add_root_certificate(Certificate::from_pem(ROOT_CA_CERT).unwrap()) |
.add_root_certificate(Certificate::from_pem(ROOT_CA_CERT).unwrap()) |
||||||
.build() |
.build() |
||||||
.unwrap(); |
.unwrap(); |
||||||
Self { |
Self { |
||||||
client, |
client, |
||||||
base_url: Url::parse(&format!("https://{}:{}", server, MASTER_PORT)).unwrap(), |
base_url: Url::parse(&format!("https://{}:{}", server, MASTER_PORT)).unwrap(), |
||||||
password: None, |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
pub fn password(mut self, password: String) -> ClientHandler { |
async fn _req<P: AsMsg, M: AsMsg + DeserializeOwned>( |
||||||
self.password = Some(password); |
&self, |
||||||
self |
url: impl AsRef<str>, |
||||||
} |
payload: P, |
||||||
|
) -> UResult<M> { |
||||||
|
let request = self |
||||||
|
.client |
||||||
|
.post(self.base_url.join(url.as_ref()).unwrap()) |
||||||
|
.json(&payload.as_message()); |
||||||
|
|
||||||
fn set_pwd(&self, rb: RequestBuilder) -> RequestBuilder { |
let response = request.send().await?; |
||||||
match &self.password { |
let is_success = match response.error_for_status_ref() { |
||||||
Some(p) => rb.bearer_auth(p), |
Ok(_) => Ok(()), |
||||||
None => rb, |
Err(e) => Err(UError::from(e)), |
||||||
|
}; |
||||||
|
let resp = response.text().await?; |
||||||
|
match is_success { |
||||||
|
Ok(_) => serde_json::from_str::<BaseMessage<M>>(&resp) |
||||||
|
.map(|msg| msg.into_inner()) |
||||||
|
.or_else(|e| Err(UError::NetError(e.to_string(), resp.clone()))), |
||||||
|
Err(UError::NetError(err, _)) => Err(UError::NetError(err, resp)), |
||||||
|
_ => unreachable!(), |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
fn build_get(&self, url: &str) -> RequestBuilder { |
// get jobs for client
|
||||||
let rb = self.client.get(self.base_url.join(url).unwrap()); |
pub async fn get_personal_jobs( |
||||||
self.set_pwd(rb) |
&self, |
||||||
|
url_param: Option<Uuid>, |
||||||
|
) -> UResult<Vec<models::AssignedJob>> { |
||||||
|
self._req( |
||||||
|
format!("get_personal_jobs/{}", opt_to_string(url_param)), |
||||||
|
Empty, |
||||||
|
) |
||||||
|
.await |
||||||
} |
} |
||||||
|
|
||||||
fn build_post(&self, url: &str) -> RequestBuilder { |
|
||||||
let rb = self.client.post(self.base_url.join(url).unwrap()); |
|
||||||
self.set_pwd(rb) |
|
||||||
} |
|
||||||
//
|
|
||||||
// get jobs for client
|
|
||||||
#[api_route("GET")] |
|
||||||
async fn get_personal_jobs(&self, url_param: Option<Uuid>) -> VecDisplay<models::AssignedJob> {} |
|
||||||
//
|
|
||||||
// send something to server
|
// send something to server
|
||||||
#[api_route("POST")] |
pub async fn report(&self, payload: &[messaging::Reportable]) -> UResult<Empty> { |
||||||
async fn report(&self, payload: &[messaging::Reportable]) -> messaging::Empty {} |
self._req("report", payload).await |
||||||
//
|
} |
||||||
|
|
||||||
// download file
|
// download file
|
||||||
#[api_route("GET")] |
pub async fn dl(&self, file: String) -> UResult<Vec<u8>> { |
||||||
async fn dl(&self, url_param: Option<Uuid>) -> Vec<u8> {} |
self._req(format!("dl/{file}"), Empty).await |
||||||
//
|
} |
||||||
// request download
|
} |
||||||
#[api_route("POST")] |
|
||||||
async fn dlr(&self, url_param: Option<String>) -> messaging::DownloadInfo {} |
//##########// Admin area //##########//
|
||||||
|
#[cfg(feature = "panel")] |
||||||
//##########// Admin area //##########//
|
impl ClientHandler { |
||||||
/// client listing
|
/// agent listing
|
||||||
#[api_route("GET")] |
pub async fn get_agents(&self, agent: Option<Uuid>) -> UResult<Vec<models::Agent>> { |
||||||
async fn get_agents(&self, url_param: Option<Uuid>) -> VecDisplay<models::Agent> {} |
self._req(format!("get_agents/{}", opt_to_string(agent)), Empty) |
||||||
//
|
.await |
||||||
// get all available jobs
|
} |
||||||
#[api_route("GET")] |
|
||||||
async fn get_jobs(&self, url_param: Option<Uuid>) -> VecDisplay<models::JobMeta> {} |
/// update something
|
||||||
//
|
pub async fn update_item(&self, item: impl AsMsg) -> UResult<Empty> { |
||||||
// create and upload job
|
self._req("update_item", item).await |
||||||
#[api_route("POST")] |
} |
||||||
async fn upload_jobs(&self, payload: &[models::JobMeta]) -> messaging::Empty {} |
|
||||||
//
|
/// get all available jobs
|
||||||
// delete something
|
pub async fn get_jobs(&self, job: Option<Uuid>) -> UResult<Vec<models::JobMeta>> { |
||||||
#[api_route("GET")] |
self._req(format!("get_jobs/{}", opt_to_string(job)), Empty) |
||||||
async fn del(&self, url_param: Option<Uuid>) -> i32 {} |
.await |
||||||
//
|
} |
||||||
// set jobs for any client
|
|
||||||
#[api_route("POST")] |
/// create and upload job
|
||||||
async fn set_jobs(&self, url_param: Option<Uuid>, payload: &[String]) -> VecDisplay<Uuid> {} |
pub async fn upload_jobs(&self, payload: &[models::JobMeta]) -> UResult<Empty> { |
||||||
//
|
self._req("upload_jobs", payload).await |
||||||
// get jobs for any client
|
} |
||||||
#[api_route("GET")] |
|
||||||
async fn get_agent_jobs(&self, url_param: Option<Uuid>) -> VecDisplay<models::AssignedJob> {} |
/// delete something
|
||||||
|
pub async fn del(&self, item: Uuid) -> UResult<i32> { |
||||||
|
self._req(format!("del/{item}"), Empty).await |
||||||
|
} |
||||||
|
|
||||||
|
/// set jobs for any agent
|
||||||
|
pub async fn set_jobs(&self, agent: Uuid, job_idents: &[String]) -> UResult<Vec<Uuid>> { |
||||||
|
self._req(format!("set_jobs/{agent}"), job_idents).await |
||||||
|
} |
||||||
|
|
||||||
|
/// get jobs for any agent
|
||||||
|
pub async fn get_agent_jobs(&self, agent: Option<Uuid>) -> UResult<Vec<models::AssignedJob>> { |
||||||
|
self._req(format!("set_jobs/{}", opt_to_string(agent)), Empty) |
||||||
|
.await |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,16 @@ |
|||||||
|
use std::env; |
||||||
|
use std::path::Path; |
||||||
|
|
||||||
|
use tracing_appender::rolling; |
||||||
|
use tracing_subscriber::EnvFilter; |
||||||
|
|
||||||
|
pub fn init_logger(logfile: impl AsRef<Path> + Send + Sync + 'static) { |
||||||
|
if env::var("RUST_LOG").is_err() { |
||||||
|
env::set_var("RUST_LOG", "info") |
||||||
|
} |
||||||
|
|
||||||
|
tracing_subscriber::fmt::fmt() |
||||||
|
.with_env_filter(EnvFilter::from_default_env()) |
||||||
|
.with_writer(move || rolling::never(".", logfile.as_ref().with_extension("log"))) |
||||||
|
.init(); |
||||||
|
} |
Loading…
Reference in new issue