diff --git a/Cargo.toml b/Cargo.toml index 02c9d71..228808c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = [ "bin/u_agent", "bin/u_panel", - "bin/u_run", + #"bin/u_run", "bin/u_server", "lib/u_lib", "integration" diff --git a/bin/u_agent/Cargo.toml b/bin/u_agent/Cargo.toml index e06bc51..a7cfb91 100644 --- a/bin/u_agent/Cargo.toml +++ b/bin/u_agent/Cargo.toml @@ -12,9 +12,5 @@ sysinfo = "0.10.5" log = "^0.4" uuid = "0.6.5" reqwest = { version = "0.11", features = ["json"] } -openssl = "*" u_lib = { version = "*", path = "../../lib/u_lib" } -daemonize = "0.4.1" -[build-dependencies] -openssl = "*" diff --git a/bin/u_agent/src/lib.rs b/bin/u_agent/src/lib.rs index a7405ab..09e016f 100644 --- a/bin/u_agent/src/lib.rs +++ b/bin/u_agent/src/lib.rs @@ -6,7 +6,7 @@ #[macro_use] extern crate log; -use daemonize::Daemonize; +//use daemonize::Daemonize; use std::sync::Arc; use tokio::time::{sleep, Duration}; use u_lib::{ @@ -78,7 +78,7 @@ async fn do_stuff(client: Arc) -> ! { Ok(resp) => { process_request(resp, &client).await; } - Err(err) => ErrChan::send(err, "personal").await, + Err(err) => ErrChan::send(err, "processing").await, } let result: Vec = pop_completed().await.into_iter().collect(); if !result.is_empty() { @@ -105,10 +105,10 @@ pub async fn run_forever() -> ! { .next() .unwrap() ))); - } else { - if let Err(e) = Daemonize::new().start() { - ErrChan::send(UError::Runtime(e.to_string()), "deeeemon").await - } + // } else { + // if let Err(e) = Daemonize::new().start() { + // ErrChan::send(UError::Runtime(e.to_string()), "deeeemon").await + // } } info!("Startup"); do_stuff(client).await diff --git a/bin/u_panel/Cargo.toml b/bin/u_panel/Cargo.toml index 4ba454a..1ea2be9 100644 --- a/bin/u_panel/Cargo.toml +++ b/bin/u_panel/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -actix-web = "3.3.2" +actix-web = "4.1" backtrace = "0.3.61" structopt = "0.3.21" uuid = "0.6.5" @@ -30,3 +30,5 @@ rust-embed = { version = "6.3.0", features = ["debug-embed", "compression"] } mime_guess = "2.0.4" shlex = "1.1.0" thiserror = "1.0.31" +futures-util = "0.3.21" +actix-cors = "0.6.1" diff --git a/bin/u_panel/src/argparse.rs b/bin/u_panel/src/argparse.rs index e89710b..b414fe7 100644 --- a/bin/u_panel/src/argparse.rs +++ b/bin/u_panel/src/argparse.rs @@ -123,7 +123,9 @@ pub async fn process_cmd(client: ClientHandler, args: Args) -> UResult { .await .map_err(|e| UError::PanelError(e.to_string()))?,*/ Cmd::Serve => { - crate::server::serve(client).map_err(|e| UError::PanelError(e.to_string()))?; + crate::server::serve(client) + .await + .map_err(|e| UError::PanelError(e.to_string()))?; String::new() } }) diff --git a/bin/u_panel/src/main.rs b/bin/u_panel/src/main.rs index 0726a88..ea263b8 100644 --- a/bin/u_panel/src/main.rs +++ b/bin/u_panel/src/main.rs @@ -20,7 +20,7 @@ struct AccessEnv { u_server: String, } -#[tokio::main] +#[actix_web::main] async fn main() -> AnyResult<()> { let env = load_env::()?; let client = ClientHandler::new(&env.u_server, Some(env.admin_auth_token)); diff --git a/bin/u_panel/src/server/fe/src/app/app.component.html b/bin/u_panel/src/server/fe/src/app/app.component.html index a323767..6aedf2e 100644 --- a/bin/u_panel/src/server/fe/src/app/app.component.html +++ b/bin/u_panel/src/server/fe/src/app/app.component.html @@ -1,49 +1,11 @@ -
-
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
id{{row.id}}Alias{{row.alias}}user@hostname{{row.username}}@{{row.hostname}} - Last active - {{row.last_active}}
- -
- - -
- + +
+ + + + + - -
\ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/app.component.ts b/bin/u_panel/src/server/fe/src/app/app.component.ts index 76deda5..a76961e 100644 --- a/bin/u_panel/src/server/fe/src/app/app.component.ts +++ b/bin/u_panel/src/server/fe/src/app/app.component.ts @@ -1,82 +1,9 @@ -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({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.less'] }) -export class AppComponent implements AfterViewInit { - 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 { - status: "ok" | "err", - data: T | string -} - -class ExampleHttpDatabase { - constructor(private _httpClient: HttpClient) { } - - getAgents(): Observable> { - const requestUrl = "/cmd/"; - const cmd = "agents list"; - - return this._httpClient.post>(requestUrl, cmd); - } +export class AppComponent { } diff --git a/bin/u_panel/src/server/fe/src/app/app.module.ts b/bin/u_panel/src/server/fe/src/app/app.module.ts index 56e6d5c..b251fa0 100644 --- a/bin/u_panel/src/server/fe/src/app/app.module.ts +++ b/bin/u_panel/src/server/fe/src/app/app.module.ts @@ -1,17 +1,23 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; - import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTableModule } from '@angular/material/table'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonModule } from '@angular/material/button' +import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { HttpClientModule } from '@angular/common/http'; +import { AgentComponent, JobComponent, ResultComponent } from './core/tables'; @NgModule({ declarations: [ - AppComponent + AppComponent, + AgentComponent, + JobComponent, + ResultComponent ], imports: [ BrowserModule, @@ -19,6 +25,9 @@ import { HttpClientModule } from '@angular/common/http'; AppRoutingModule, MatTabsModule, MatTableModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, MatProgressSpinnerModule, BrowserAnimationsModule ], diff --git a/bin/u_panel/src/server/fe/src/app/core/index.ts b/bin/u_panel/src/server/fe/src/app/core/index.ts new file mode 100644 index 0000000..b248abc --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/index.ts @@ -0,0 +1 @@ +export * from './services'; \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/models/agent.model.ts b/bin/u_panel/src/server/fe/src/app/core/models/agent.model.ts new file mode 100644 index 0000000..8015ee2 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/models/agent.model.ts @@ -0,0 +1,15 @@ +import { UTCDate } from "."; + +export interface AgentModel { + alias: string | null, + hostname: string, + id: string, + is_root: boolean, + is_root_allowed: boolean, + last_active: UTCDate, + platform: string, + regtime: UTCDate, + state: "new" | "active" | "banned", + token: string | null, + username: string, +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/models/index.ts b/bin/u_panel/src/server/fe/src/app/core/models/index.ts new file mode 100644 index 0000000..38623ef --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/models/index.ts @@ -0,0 +1,8 @@ +export * from './agent.model'; +export * from './result.model'; +export * from './job.model'; + +export interface UTCDate { + secs_since_epoch: number, + nanos_since_epoch: number +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/models/job.model.ts b/bin/u_panel/src/server/fe/src/app/core/models/job.model.ts new file mode 100644 index 0000000..66f1012 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/models/job.model.ts @@ -0,0 +1,8 @@ +export interface JobModel { + alias: string, + argv: string, + id: string, + exec_type: string, + platform: string, + payload: Uint8Array | null, +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/models/result.model.ts b/bin/u_panel/src/server/fe/src/app/core/models/result.model.ts new file mode 100644 index 0000000..825323b --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/models/result.model.ts @@ -0,0 +1,13 @@ +import { UTCDate } from "."; + +export interface ResultModel { + agent_id: string, + alias: string, + created: UTCDate, + id: string, + job_id: string, + result: Uint8Array, + state: "Queued" | "Running" | "Finished", + retcode: number | null, + updated: UTCDate, +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/services/api.service.ts b/bin/u_panel/src/server/fe/src/app/core/services/api.service.ts new file mode 100644 index 0000000..cc04493 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/services/api.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +interface ServerResponse { + status: "ok" | "err", + data: T | string +} + +export class ApiTableService { + area: string; + + constructor(private http: HttpClient, area: string) { + this.area = area; + } + + requestUrl = `${environment.server}/cmd/`; + + req(cmd: string): Observable> { + return this.http.post>(this.requestUrl, cmd); + } + + getOne(id: string): Observable> { + return this.req(`${this.area} read ${id}`) + } + + getMany(): Observable> { + return this.req(`${this.area} read`) + } + + update(item: T): Observable> { + return this.req(`${this.area} update '${JSON.stringify(item)}'`) + } + + delete(id: string): Observable> { + return this.req(`${this.area} delete ${id}`) + } + + create(item: string): Observable> { + return this.req(`${this.area} create ${item}`) + } +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/services/index.ts b/bin/u_panel/src/server/fe/src/app/core/services/index.ts new file mode 100644 index 0000000..aeef56f --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/services/index.ts @@ -0,0 +1 @@ +export * from './api.service' \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.ts new file mode 100644 index 0000000..8b1b5cb --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/agent.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit } from '@angular/core'; +import { TablesComponent } from './table.component'; +import { AgentModel } from '../models'; + +@Component({ + selector: 'agent-table', + templateUrl: './table.component.html', + styleUrls: ['./table.component.less'] +}) +export class AgentComponent extends TablesComponent { + area = 'agents' as const; + + columns = [ + { + def: "id", + name: "ID", + cell: (cell: AgentModel) => `${cell.id}` + }, + { + def: "alias", + name: "Alias", + cell: (cell: AgentModel) => `${cell.alias}` + }, + { + def: "username", + name: "User", + cell: (cell: AgentModel) => `${cell.username}` + }, + { + def: "hostname", + name: "Host", + cell: (cell: AgentModel) => `${cell.hostname}` + }, + { + def: "last_active", + name: "Last active", + cell: (cell: AgentModel) => `${cell.last_active.secs_since_epoch}` + }, + ] + displayedColumns = this.columns.map((c) => c.def); +} diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/index.ts b/bin/u_panel/src/server/fe/src/app/core/tables/index.ts new file mode 100644 index 0000000..11dfaf2 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/index.ts @@ -0,0 +1,3 @@ +export * from './agent.component'; +export * from './job.component'; +export * from './result.component'; \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/job.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/job.component.ts new file mode 100644 index 0000000..27d8d42 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/job.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from '@angular/core'; +import { TablesComponent } from './table.component'; +import { JobModel } from '../models'; + +@Component({ + selector: 'job-table', + templateUrl: './table.component.html', + styleUrls: ['./table.component.less'] +}) +export class JobComponent extends TablesComponent { + area = 'jobs' as const; + + columns = [ + { + def: "id", + name: "ID", + cell: (cell: JobModel) => `${cell.id}` + }, + { + def: "alias", + name: "Alias", + cell: (cell: JobModel) => `${cell.alias}` + }, + { + def: "argv", + name: "Cmd-line args", + cell: (cell: JobModel) => `${cell.argv}` + }, + { + def: "platform", + name: "Platform", + cell: (cell: JobModel) => `${cell.platform}` + }, + { + def: "payload", + name: "Payload", + cell: (cell: JobModel) => `${cell.payload}` + }, + { + def: "etype", + name: "Type", + cell: (cell: JobModel) => `${cell.exec_type}` + }, + ] + displayedColumns = this.columns.map((c) => c.def); +} diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/result.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/result.component.ts new file mode 100644 index 0000000..964c8cd --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/result.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from '@angular/core'; +import { TablesComponent } from './table.component'; +import { ResultModel } from '../models'; + +@Component({ + selector: 'result-table', + templateUrl: './table.component.html', + styleUrls: ['./table.component.less'] +}) +export class ResultComponent extends TablesComponent { + area = 'map' as const; + + columns = [ + { + def: "id", + name: "ID", + cell: (cell: ResultModel) => `${cell.id}` + }, + { + def: "alias", + name: "Alias", + cell: (cell: ResultModel) => `${cell.alias}` + }, + { + def: "agent_id", + name: "Agent ID", + cell: (cell: ResultModel) => `${cell.agent_id}` + }, + { + def: "job_id", + name: "Job ID", + cell: (cell: ResultModel) => `${cell.job_id}` + }, + { + def: "state", + name: "State", + cell: (cell: ResultModel) => `${cell.state} `.concat((cell.state === "Finished") ? `(${cell.retcode})` : '') + }, + { + def: "last_updated", + name: "Last updated", + cell: (cell: ResultModel) => `${cell.updated.secs_since_epoch}` + }, + ] + displayedColumns = this.columns.map((c) => c.def); +} diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/table.component.html b/bin/u_panel/src/server/fe/src/app/core/tables/table.component.html new file mode 100644 index 0000000..777d508 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/table.component.html @@ -0,0 +1,36 @@ +
+ +
+
+ +
+ + Filter + + + + + + + + + + + + + + + + + +
+ {{column.name}} + + {{column.cell(row)}} +
No data
+
+ + +
\ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/table.component.less b/bin/u_panel/src/server/fe/src/app/core/tables/table.component.less new file mode 100644 index 0000000..6a4475a --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/table.component.less @@ -0,0 +1,24 @@ +.data-table { + width: 100%; +} + +.table-container { + margin: 50px; +} + +.loading-shade { + position: absolute; + top: 0; + left: 0; + bottom: 56px; + right: 0; + background: rgba(0, 0, 0, 0.15); + z-index: 1; + display: flex; + align-items: center; + justify-content: center; +} + +#refresh_btn { + margin-left: 10px; +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/app/core/tables/table.component.ts b/bin/u_panel/src/server/fe/src/app/core/tables/table.component.ts new file mode 100644 index 0000000..5581de6 --- /dev/null +++ b/bin/u_panel/src/server/fe/src/app/core/tables/table.component.ts @@ -0,0 +1,66 @@ +import { Component, OnInit, Directive } from '@angular/core'; +import { timer, of as observableOf } from 'rxjs'; +import { catchError, map, startWith, switchMap } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { ApiTableService } from '../'; +import { MatTableDataSource } from '@angular/material/table'; + +@Directive() +export abstract class TablesComponent implements OnInit { + abstract area: "agents" | "jobs" | "map"; + data_source!: ApiTableService | null; + table_data!: MatTableDataSource; + + isLoadingResults = true; + + constructor(private _httpClient: HttpClient) { + this.table_data = new MatTableDataSource; + } + + ngOnInit() { + this.data_source = new ApiTableService(this._httpClient, this.area); + this.fetch_many(); + // If the user changes the sort order, reset back to the first page. + //this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0)); + + } + + fetch_many() { + timer(0) + .pipe( + startWith({}), + switchMap(() => { + this.isLoadingResults = true; + return this.data_source!.getMany().pipe(catchError(() => observableOf(null))); + }), + map(data => { + 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 = data } else { alert(`Error: ${data}`) } }); + } + + apply_filter(event: Event) { + const filterValue = (event.target as HTMLInputElement).value; + this.table_data.filter = filterValue.trim().toLowerCase(); + } + + + abstract columns: ColumnDef[]; + abstract displayedColumns: string[]; +} + +type ColumnDef = { + def: string, + name: string, + cell: (cell: C) => string +} \ No newline at end of file diff --git a/bin/u_panel/src/server/fe/src/environments/environment.prod.ts b/bin/u_panel/src/server/fe/src/environments/environment.prod.ts index 3612073..95d0187 100644 --- a/bin/u_panel/src/server/fe/src/environments/environment.prod.ts +++ b/bin/u_panel/src/server/fe/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { - production: true + production: true, + server: "" }; diff --git a/bin/u_panel/src/server/fe/src/environments/environment.ts b/bin/u_panel/src/server/fe/src/environments/environment.ts index f56ff47..c016fed 100644 --- a/bin/u_panel/src/server/fe/src/environments/environment.ts +++ b/bin/u_panel/src/server/fe/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + server: "http://127.0.0.1:8080" }; /* diff --git a/bin/u_panel/src/server/mod.rs b/bin/u_panel/src/server/mod.rs index a427567..49b08f7 100644 --- a/bin/u_panel/src/server/mod.rs +++ b/bin/u_panel/src/server/mod.rs @@ -14,12 +14,14 @@ almost all fields are editable, rows are deletable mod errors; use crate::{process_cmd, Args}; +use actix_cors::Cors; use actix_web::{get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder}; use errors::Error; +use futures_util::StreamExt; use rust_embed::RustEmbed; use std::borrow::Cow; use structopt::StructOpt; -use u_lib::{api::ClientHandler, logging::init_logger, unwrap_enum}; +use u_lib::{api::ClientHandler, unwrap_enum}; #[derive(RustEmbed)] #[folder = "./src/server/fe/dist/fe/"] @@ -52,12 +54,21 @@ async fn static_files_adapter(path: web::Path<(String,)>) -> impl Responder { #[post("/cmd/")] async fn send_cmd( - cmd: web::Json, + mut body: web::Payload, client: web::Data, ) -> Result { - let parsed_cmd = Args::from_iter_safe( - shlex::split(&cmd.into_inner()).ok_or(Error::JustError("shlex failed".to_string()))?, - )?; + let mut bytes = web::BytesMut::new(); + while let Some(item) = body.next().await { + bytes.extend_from_slice( + &item.map_err(|e| Error::JustError(format!("payload loading failure: {e}")))?, + ); + } + let cmd = String::from_utf8(bytes.to_vec()) + .map_err(|_| Error::JustError("cmd contains non-utf8 data".to_string()))?; + let mut cmd = shlex::split(&cmd).ok_or(Error::JustError("argparse failed".to_string()))?; + info!("cmd: {:?}", cmd); + cmd.insert(0, String::from("u_panel")); + let parsed_cmd = Args::from_iter_safe(cmd)?; Ok( match process_cmd(client.as_ref().clone(), parsed_cmd).await { Ok(r) => HttpResponse::Ok().body(r), @@ -66,15 +77,14 @@ async fn send_cmd( ) } -#[actix_web::main] pub async fn serve(client: ClientHandler) -> std::io::Result<()> { - init_logger(Some("u_panel")); - let addr = "127.0.0.1:8080"; info!("Serving at http://{}", addr); + HttpServer::new(move || { App::new() .wrap(Logger::default()) + .wrap(Cors::permissive()) .app_data(web::Data::new(client.clone())) .service(main_page) .service(send_cmd) diff --git a/bin/u_server/src/db.rs b/bin/u_server/src/db.rs index 2c77133..d0929ca 100644 --- a/bin/u_server/src/db.rs +++ b/bin/u_server/src/db.rs @@ -4,7 +4,7 @@ use once_cell::sync::OnceCell; use serde::Deserialize; use std::sync::{Arc, Mutex, MutexGuard}; use u_lib::{ - models::{schema, Agent, AgentError, AssignedJob, JobMeta, JobState}, + models::{schema, Agent, AssignedJob, JobMeta, JobState}, utils::load_env, }; use uuid::Uuid; @@ -40,14 +40,6 @@ impl UDB { .unwrap() } - pub fn report_error(&self, error: &AgentError) -> SResult<()> { - use schema::errors; - diesel::insert_into(errors::table) - .values(error) - .execute(&self.conn)?; - Ok(()) - } - pub fn insert_jobs(&self, job_metas: &[JobMeta]) -> SResult<()> { use schema::jobs; diesel::insert_into(jobs::table) @@ -203,35 +195,3 @@ impl UDB { Ok(affected) } } -/* -#[cfg(test)] -mod tests { - use super::*; - - fn setup_db() -> Storage { - return UDB::new().unwrap(); - } - - #[tokio::test] - async fn test_add_agent() { - let db = setup_db(); - let agent = IAgent { - alias: None, - id: "000-000".to_string(), - hostname: "test".to_string(), - is_root: false, - is_root_allowed: false, - platform: "linux".to_string(), - status: None, - token: None, - username: "test".to_string() - }; - db.lock().unwrap().new_agent(agent).unwrap(); - let result = db.lock().unwrap().get_agents().unwrap(); - assert_eq!( - result[0].username, - "test".to_string() - ) - } -} -*/ diff --git a/bin/u_server/src/handlers.rs b/bin/u_server/src/handlers.rs index 41c2411..4e1eebe 100644 --- a/bin/u_server/src/handlers.rs +++ b/bin/u_server/src/handlers.rs @@ -1,3 +1,5 @@ +use std::time::SystemTime; + use crate::db::UDB; use crate::errors::Error; use diesel::SaveChangesDsl; @@ -89,12 +91,14 @@ impl Endpoints { let mut failed = vec![]; for entry in msg.into_inner().into_vec() { match entry { - Reportable::Assigned(res) => { - if id != res.agent_id { + Reportable::Assigned(mut result) => { + if id != result.agent_id { continue; } + result.state = JobState::Finished; + result.updated = SystemTime::now(); let db = UDB::lock_db(); - if let Err(e) = res + if let Err(e) = result .save_changes::(&db.conn) .map_err(Error::from) { @@ -106,13 +110,7 @@ impl Endpoints { Self::add_agent(a).await?; } Reportable::Error(e) => { - let err = AgentError::from_msg(e, id); - warn!( - "{} reported an error: {}", - err.agent_id, - Stripped(&err.msg.as_str()) - ); - //UDB::lock_db().report_error(&err)?; + warn!("{} reported an error: {}", id, Stripped(&e.as_str())); } Reportable::Dummy => (), } diff --git a/bin/u_server/src/models.rs b/bin/u_server/src/models.rs new file mode 100644 index 0000000..e69de29 diff --git a/bin/u_server/src/u_server.rs b/bin/u_server/src/u_server.rs index 406159c..9c6a2d7 100644 --- a/bin/u_server/src/u_server.rs +++ b/bin/u_server/src/u_server.rs @@ -14,6 +14,7 @@ extern crate diesel; mod db; mod errors; mod handlers; +mod models; use errors::{Error, SResult}; use serde::{de::DeserializeOwned, Deserialize}; @@ -151,12 +152,20 @@ pub fn init_endpoints( } pub fn prefill_jobs() -> SResult<()> { - let agent_hello = RawJobMeta::builder() - .with_type(misc::JobType::Manage) - .with_alias("agent_hello") - .build() - .unwrap(); - UDB::lock_db().insert_jobs(&[agent_hello]) + let job_alias = "agent_hello"; + let if_job_exists = UDB::lock_db().find_job_by_alias(job_alias); + match if_job_exists { + Ok(_) => Ok(()), + Err(Error::DBError(diesel::result::Error::NotFound)) => { + let agent_hello = RawJobMeta::builder() + .with_type(misc::JobType::Manage) + .with_alias(job_alias) + .build() + .unwrap(); + UDB::lock_db().insert_jobs(&[agent_hello]) + } + Err(e) => Err(e), + } } pub async fn serve() -> SResult<()> { diff --git a/integration/tests/integration/connection.rs b/integration/tests/integration/connection.rs new file mode 100644 index 0000000..78a4c2c --- /dev/null +++ b/integration/tests/integration/connection.rs @@ -0,0 +1,22 @@ +use crate::helpers::ENV; +use u_lib::config::MASTER_PORT; + +#[tokio::test] +async fn test_non_auth_connection_dropped() { + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + match client + .get(format!("https://{}:{}", &ENV.u_server, MASTER_PORT)) + .send() + .await + { + Err(e) => { + let err = e.to_string(); + println!("captured err: {err}"); + assert!(err.contains("certificate required")); + } + _ => panic!("no error occured on foreign client connection"), + } +} diff --git a/integration/tests/integration/mod.rs b/integration/tests/integration/mod.rs index b3a413f..0b76512 100644 --- a/integration/tests/integration/mod.rs +++ b/integration/tests/integration/mod.rs @@ -1 +1,2 @@ mod behaviour; +mod connection; diff --git a/integration/tests/lib.rs b/integration/tests/lib.rs index 8009650..826b82d 100644 --- a/integration/tests/lib.rs +++ b/integration/tests/lib.rs @@ -2,28 +2,5 @@ mod fixtures; mod helpers; mod integration; -use crate::helpers::ENV; -use u_lib::config::MASTER_PORT; - #[macro_use] extern crate rstest; - -#[tokio::test] -async fn test_non_auth_connection_dropped() { - let client = reqwest::ClientBuilder::new() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - match client - .get(format!("https://{}:{}", &ENV.u_server, MASTER_PORT)) - .send() - .await - { - Err(e) => { - let err = e.to_string(); - println!("{err}"); - assert!(err.contains("channel closed")) - } - _ => panic!("no error occured on foreign client connection"), - } -} diff --git a/lib/u_lib/Cargo.toml b/lib/u_lib/Cargo.toml index 54f1c42..70644bb 100644 --- a/lib/u_lib/Cargo.toml +++ b/lib/u_lib/Cargo.toml @@ -28,6 +28,7 @@ tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } tracing-appender = "0.2.2" log = "*" anyhow = "1.0.58" +platforms = "3.0.1" [features] panel = [] diff --git a/lib/u_lib/src/api.rs b/lib/u_lib/src/api.rs index f5c86fd..7669da2 100644 --- a/lib/u_lib/src/api.rs +++ b/lib/u_lib/src/api.rs @@ -92,6 +92,12 @@ impl ClientHandler { pub async fn dl(&self, file: String) -> Result> { self._req(format!("dl/{file}"), Empty).await } + + /// get all available jobs + pub async fn get_jobs(&self, job: Option) -> Result> { + self._req(format!("get_jobs/{}", opt_to_string(job)), Empty) + .await + } } //##########// Admin area //##########// @@ -108,12 +114,6 @@ impl ClientHandler { self._req("update_item", item).await } - /// get all available jobs - pub async fn get_jobs(&self, job: Option) -> Result> { - self._req(format!("get_jobs/{}", opt_to_string(job)), Empty) - .await - } - /// create and upload job pub async fn upload_jobs(&self, payload: &[models::JobMeta]) -> Result { self._req("upload_jobs", payload).await @@ -131,7 +131,7 @@ impl ClientHandler { /// get jobs for any agent pub async fn get_agent_jobs(&self, agent: Option) -> Result> { - self._req(format!("get_personal_jobs/{}", opt_to_string(agent)), Empty) + self._req(format!("get_agent_jobs/{}", opt_to_string(agent)), Empty) .await } } diff --git a/lib/u_lib/src/builder.rs b/lib/u_lib/src/builder.rs index 01b949d..84e46c5 100644 --- a/lib/u_lib/src/builder.rs +++ b/lib/u_lib/src/builder.rs @@ -3,10 +3,9 @@ use crate::{ executor::{DynFut, Waiter}, messaging::Reportable, models::{Agent, AssignedJob, JobMeta, JobType, RawJobMeta}, - utils::{CombinedResult, OneOrVec}, + utils::{CombinedResult, OneOrVec, Platform}, UError, UResult, }; -use guess_host_triple::guess_host_triple; use std::collections::HashMap; pub struct JobBuilder { @@ -30,12 +29,11 @@ impl JobBuilder { Ok(match job_meta.exec_type { JobType::Shell => { let meta = JobCache::get(req.job_id).ok_or(UError::NoJob(req.job_id))?; - let curr_platform = guess_host_triple().unwrap_or("unknown").to_string(); - //TODO: extend platform checking (partial check) - if meta.platform != curr_platform { + let curr_platform = Platform::current(); + if !curr_platform.matches(&meta.platform) { return Err(UError::InsuitablePlatform( meta.platform.clone(), - curr_platform, + curr_platform.into_string(), ) .into()); } diff --git a/lib/u_lib/src/models/agent.rs b/lib/u_lib/src/models/agent.rs index 6843e0e..869be5e 100644 --- a/lib/u_lib/src/models/agent.rs +++ b/lib/u_lib/src/models/agent.rs @@ -7,8 +7,11 @@ use strum::Display; #[cfg(not(target_arch = "wasm32"))] use crate::builder::NamedJobBuilder; use crate::{ - config::get_self_uid, messaging::Reportable, models::schema::*, unwrap_enum, - utils::systime_to_string, + config::get_self_uid, + messaging::Reportable, + models::schema::*, + unwrap_enum, + utils::{systime_to_string, Platform}, }; use uuid::Uuid; @@ -94,13 +97,16 @@ impl Agent { hostname: decoder(builder.pop("hostname")), is_root: &decoder(builder.pop("is_root")) == "0", username: decoder(builder.pop("username")), - platform: guess_host_triple::guess_host_triple() - .unwrap_or("unknown") - .to_string(), + platform: Platform::current().into_string(), ..Default::default() } } + #[cfg(not(unix))] + pub async fn gather() -> Self { + Self::default() + } + pub async fn run() -> Reportable { Reportable::Agent(Agent::gather().await) } diff --git a/lib/u_lib/src/models/error.rs b/lib/u_lib/src/models/error.rs deleted file mode 100644 index a916dd0..0000000 --- a/lib/u_lib/src/models/error.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::models::schema::*; -use diesel::{Insertable, Queryable}; -use serde::{Deserialize, Serialize}; -use std::time::SystemTime; -use uuid::Uuid; - -#[derive(Serialize, Deserialize, Clone, Debug, Queryable, Insertable, PartialEq)] -#[table_name = "errors"] -pub struct AgentError { - pub agent_id: Uuid, - pub created: SystemTime, - pub id: Uuid, - pub msg: String, -} - -impl AgentError { - pub fn from_msg(msg: impl Into, agent_id: Uuid) -> Self { - AgentError { - agent_id, - msg: msg.into(), - ..Default::default() - } - } -} - -impl Default for AgentError { - fn default() -> Self { - Self { - agent_id: Uuid::new_v4(), - created: SystemTime::now(), - id: Uuid::new_v4(), - msg: String::new(), - } - } -} diff --git a/lib/u_lib/src/models/jobs/assigned.rs b/lib/u_lib/src/models/jobs/assigned.rs index d2465b6..2854e0f 100644 --- a/lib/u_lib/src/models/jobs/assigned.rs +++ b/lib/u_lib/src/models/jobs/assigned.rs @@ -112,8 +112,6 @@ impl AssignedJob { }; self.result = Some(data); self.retcode = retcode; - self.updated = SystemTime::now(); - self.state = JobState::Finished; Reportable::Assigned(self) } diff --git a/lib/u_lib/src/models/jobs/meta.rs b/lib/u_lib/src/models/jobs/meta.rs index 657be72..ccb88d5 100644 --- a/lib/u_lib/src/models/jobs/meta.rs +++ b/lib/u_lib/src/models/jobs/meta.rs @@ -1,4 +1,5 @@ use super::JobType; +use crate::utils::Platform; use crate::{models::schema::*, utils::Stripped, UError, UResult}; use diesel::{Identifiable, Insertable, Queryable}; use serde::{Deserialize, Serialize}; @@ -23,19 +24,19 @@ pub struct JobMeta { pub payload: Option>, } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct RawJobMeta { #[serde(default)] pub alias: Option, #[serde(default)] pub argv: String, #[serde(default)] - pub id: Uuid, + pub id: UuidDefaultUnique, #[serde(default)] pub exec_type: JobType, //pub schedule: JobSchedule, #[serde(default)] - pub platform: String, + pub platform: Platform, #[serde(default)] pub payload: Option>, #[serde(default)] @@ -86,9 +87,7 @@ impl Default for JobMeta { alias: None, argv: String::new(), exec_type: JobType::Shell, - platform: guess_host_triple::guess_host_triple() - .unwrap_or("unknown") - .to_string(), + platform: Platform::current().into_string(), payload: None, } } @@ -97,13 +96,11 @@ impl Default for JobMeta { impl Default for RawJobMeta { fn default() -> Self { Self { - id: Uuid::new_v4(), + id: UuidDefaultUnique::default(), alias: None, argv: String::new(), exec_type: JobType::Shell, - platform: guess_host_triple::guess_host_triple() - .unwrap_or("unknown") - .to_string(), + platform: Platform::current(), payload: None, payload_path: None, } @@ -179,6 +176,12 @@ impl JobMetaBuilder { } } }; + if !inner.platform.check() { + return Err(UError::JobArgsError(format!( + "Unknown platform {}", + inner.platform.into_string() + ))); + } Ok(inner.into()) } JobType::Manage => Ok(inner.into()), @@ -192,10 +195,19 @@ impl From for JobMeta { JobMeta { alias: rjm.alias, argv: rjm.argv, - id: rjm.id, + id: rjm.id.0, exec_type: rjm.exec_type, - platform: rjm.platform, + platform: rjm.platform.into_string(), payload: rjm.payload, } } } + +#[derive(Deserialize, Debug)] +pub struct UuidDefaultUnique(Uuid); + +impl Default for UuidDefaultUnique { + fn default() -> Self { + Self(Uuid::new_v4()) + } +} diff --git a/lib/u_lib/src/models/mod.rs b/lib/u_lib/src/models/mod.rs index 3178567..b0b30a7 100644 --- a/lib/u_lib/src/models/mod.rs +++ b/lib/u_lib/src/models/mod.rs @@ -1,6 +1,5 @@ mod agent; -mod error; mod jobs; pub mod schema; -pub use crate::models::{agent::*, error::*, jobs::*}; +pub use crate::models::{agent::*, jobs::*}; diff --git a/lib/u_lib/src/utils/mod.rs b/lib/u_lib/src/utils/mod.rs index df527db..4d6e004 100644 --- a/lib/u_lib/src/utils/mod.rs +++ b/lib/u_lib/src/utils/mod.rs @@ -3,6 +3,7 @@ pub mod conv; pub mod env; pub mod fmt; pub mod misc; +pub mod platform; pub mod proc_output; pub mod storage; #[cfg(not(target_arch = "wasm32"))] @@ -16,6 +17,7 @@ pub use conv::*; pub use env::{load_env, load_env_default}; pub use fmt::*; pub use misc::*; +pub use platform::*; pub use proc_output::*; pub use storage::*; #[cfg(not(target_arch = "wasm32"))] diff --git a/lib/u_lib/src/utils/platform.rs b/lib/u_lib/src/utils/platform.rs new file mode 100644 index 0000000..7e13301 --- /dev/null +++ b/lib/u_lib/src/utils/platform.rs @@ -0,0 +1,38 @@ +use guess_host_triple::guess_host_triple; +use platforms::{Platform as _Platform, PlatformReq}; +use serde::Deserialize; +use std::str::FromStr; + +#[derive(Debug, Deserialize)] +pub struct Platform(String); + +impl Platform { + pub fn current() -> Platform { + Self(guess_host_triple().unwrap_or("unknown").to_string()) + } + + pub fn matches(&self, pf: impl AsRef) -> bool { + match PlatformReq::from_str(pf.as_ref()) { + Ok(p) => p.matches(&_Platform::find(&self.0).unwrap()), + Err(_) => false, + } + } + + pub fn check(&self) -> bool { + PlatformReq::from_str(&self.0).is_ok() + } + + pub fn into_string(self) -> String { + self.0 + } + + pub fn any() -> Platform { + Self(String::from("*")) + } +} + +impl Default for Platform { + fn default() -> Self { + Self::any() + } +} diff --git a/lib/u_lib/src/utils/tempfile.rs b/lib/u_lib/src/utils/tempfile.rs index a5a510e..d2fb58f 100644 --- a/lib/u_lib/src/utils/tempfile.rs +++ b/lib/u_lib/src/utils/tempfile.rs @@ -1,5 +1,7 @@ use crate::{UError, UResult}; -use std::{env::temp_dir, fs, ops::Drop, os::unix::fs::PermissionsExt, path::PathBuf}; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::{env::temp_dir, fs, ops::Drop, path::PathBuf}; use uuid::Uuid; pub struct TempFile { @@ -28,8 +30,12 @@ impl TempFile { let path = this.get_path(); dbg!(&path); this.write_all(data)?; - let perms = fs::Permissions::from_mode(0o555); - fs::set_permissions(&path, perms).map_err(|e| UError::FSError(path, e.to_string()))?; + + #[cfg(unix)] + { + let perms = fs::Permissions::from_mode(0o555); + fs::set_permissions(&path, perms).map_err(|e| UError::FSError(path, e.to_string()))?; + } Ok(this) } } diff --git a/migrations/2020-10-24-111622_create_all/down.sql b/migrations/2020-10-24-111622_create_all/down.sql index c3bfd8c..3ded775 100644 --- a/migrations/2020-10-24-111622_create_all/down.sql +++ b/migrations/2020-10-24-111622_create_all/down.sql @@ -3,7 +3,6 @@ DROP TABLE results; DROP TABLE certificates; DROP TABLE jobs; DROP TABLE agents; -DROP TABLE errors; DROP TYPE IF EXISTS JobState; DROP TYPE IF EXISTS JobType; diff --git a/migrations/2020-10-24-111622_create_all/up.sql b/migrations/2020-10-24-111622_create_all/up.sql index d319d65..a26d094 100644 --- a/migrations/2020-10-24-111622_create_all/up.sql +++ b/migrations/2020-10-24-111622_create_all/up.sql @@ -64,14 +64,4 @@ CREATE TABLE IF NOT EXISTS certificates ( , is_revoked BOOLEAN NOT NULL DEFAULT FALSE , PRIMARY KEY(id) , FOREIGN KEY(agent_id) REFERENCES agents(id) -); - - -CREATE TABLE IF NOT EXISTS errors ( - agent_id UUID NOT NULL - , created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP - , id UUID NOT NULL DEFAULT uuid_generate_v4() - , msg TEXT - , FOREIGN KEY(agent_id) REFERENCES agents(id) ON DELETE CASCADE - , PRIMARY KEY(id) ); \ No newline at end of file