wow such web

* temporarily disabled u_run
* clean unused deps a bit
* disable daemonizing in release mode because wtf
* use one async runtime in u_panel
* fix cors issues
* fix passing cmd from frontend
* initial pretty web interface
* remove storing errors in db
* check target and payload platform
* prepare for cross compile to windows binary
pull/1/head
plazmoid 2 years ago
parent a50e6d242f
commit c25fa780bf
  1. 2
      Cargo.toml
  2. 4
      bin/u_agent/Cargo.toml
  3. 12
      bin/u_agent/src/lib.rs
  4. 4
      bin/u_panel/Cargo.toml
  5. 4
      bin/u_panel/src/argparse.rs
  6. 2
      bin/u_panel/src/main.rs
  7. 52
      bin/u_panel/src/server/fe/src/app/app.component.html
  8. 75
      bin/u_panel/src/server/fe/src/app/app.component.ts
  9. 13
      bin/u_panel/src/server/fe/src/app/app.module.ts
  10. 1
      bin/u_panel/src/server/fe/src/app/core/index.ts
  11. 15
      bin/u_panel/src/server/fe/src/app/core/models/agent.model.ts
  12. 8
      bin/u_panel/src/server/fe/src/app/core/models/index.ts
  13. 8
      bin/u_panel/src/server/fe/src/app/core/models/job.model.ts
  14. 13
      bin/u_panel/src/server/fe/src/app/core/models/result.model.ts
  15. 43
      bin/u_panel/src/server/fe/src/app/core/services/api.service.ts
  16. 1
      bin/u_panel/src/server/fe/src/app/core/services/index.ts
  17. 41
      bin/u_panel/src/server/fe/src/app/core/tables/agent.component.ts
  18. 3
      bin/u_panel/src/server/fe/src/app/core/tables/index.ts
  19. 46
      bin/u_panel/src/server/fe/src/app/core/tables/job.component.ts
  20. 46
      bin/u_panel/src/server/fe/src/app/core/tables/result.component.ts
  21. 36
      bin/u_panel/src/server/fe/src/app/core/tables/table.component.html
  22. 24
      bin/u_panel/src/server/fe/src/app/core/tables/table.component.less
  23. 66
      bin/u_panel/src/server/fe/src/app/core/tables/table.component.ts
  24. 3
      bin/u_panel/src/server/fe/src/environments/environment.prod.ts
  25. 3
      bin/u_panel/src/server/fe/src/environments/environment.ts
  26. 26
      bin/u_panel/src/server/mod.rs
  27. 42
      bin/u_server/src/db.rs
  28. 18
      bin/u_server/src/handlers.rs
  29. 0
      bin/u_server/src/models.rs
  30. 11
      bin/u_server/src/u_server.rs
  31. 22
      integration/tests/integration/connection.rs
  32. 1
      integration/tests/integration/mod.rs
  33. 23
      integration/tests/lib.rs
  34. 1
      lib/u_lib/Cargo.toml
  35. 14
      lib/u_lib/src/api.rs
  36. 10
      lib/u_lib/src/builder.rs
  37. 16
      lib/u_lib/src/models/agent.rs
  38. 35
      lib/u_lib/src/models/error.rs
  39. 2
      lib/u_lib/src/models/jobs/assigned.rs
  40. 36
      lib/u_lib/src/models/jobs/meta.rs
  41. 3
      lib/u_lib/src/models/mod.rs
  42. 2
      lib/u_lib/src/utils/mod.rs
  43. 38
      lib/u_lib/src/utils/platform.rs
  44. 8
      lib/u_lib/src/utils/tempfile.rs
  45. 1
      migrations/2020-10-24-111622_create_all/down.sql
  46. 10
      migrations/2020-10-24-111622_create_all/up.sql

@ -2,7 +2,7 @@
members = [ members = [
"bin/u_agent", "bin/u_agent",
"bin/u_panel", "bin/u_panel",
"bin/u_run", #"bin/u_run",
"bin/u_server", "bin/u_server",
"lib/u_lib", "lib/u_lib",
"integration" "integration"

@ -12,9 +12,5 @@ sysinfo = "0.10.5"
log = "^0.4" log = "^0.4"
uuid = "0.6.5" uuid = "0.6.5"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
openssl = "*"
u_lib = { version = "*", path = "../../lib/u_lib" } u_lib = { version = "*", path = "../../lib/u_lib" }
daemonize = "0.4.1"
[build-dependencies]
openssl = "*"

@ -6,7 +6,7 @@
#[macro_use] #[macro_use]
extern crate log; extern crate log;
use daemonize::Daemonize; //use daemonize::Daemonize;
use std::sync::Arc; use std::sync::Arc;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
use u_lib::{ use u_lib::{
@ -78,7 +78,7 @@ async fn do_stuff(client: Arc<ClientHandler>) -> ! {
Ok(resp) => { Ok(resp) => {
process_request(resp, &client).await; process_request(resp, &client).await;
} }
Err(err) => ErrChan::send(err, "personal").await, Err(err) => ErrChan::send(err, "processing").await,
} }
let result: Vec<Reportable> = pop_completed().await.into_iter().collect(); let result: Vec<Reportable> = pop_completed().await.into_iter().collect();
if !result.is_empty() { if !result.is_empty() {
@ -105,10 +105,10 @@ pub async fn run_forever() -> ! {
.next() .next()
.unwrap() .unwrap()
))); )));
} else { // } else {
if let Err(e) = Daemonize::new().start() { // if let Err(e) = Daemonize::new().start() {
ErrChan::send(UError::Runtime(e.to_string()), "deeeemon").await // ErrChan::send(UError::Runtime(e.to_string()), "deeeemon").await
} // }
} }
info!("Startup"); info!("Startup");
do_stuff(client).await do_stuff(client).await

@ -7,7 +7,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
actix-web = "3.3.2" actix-web = "4.1"
backtrace = "0.3.61" backtrace = "0.3.61"
structopt = "0.3.21" structopt = "0.3.21"
uuid = "0.6.5" uuid = "0.6.5"
@ -30,3 +30,5 @@ rust-embed = { version = "6.3.0", features = ["debug-embed", "compression"] }
mime_guess = "2.0.4" mime_guess = "2.0.4"
shlex = "1.1.0" shlex = "1.1.0"
thiserror = "1.0.31" thiserror = "1.0.31"
futures-util = "0.3.21"
actix-cors = "0.6.1"

@ -123,7 +123,9 @@ pub async fn process_cmd(client: ClientHandler, args: Args) -> UResult<String> {
.await .await
.map_err(|e| UError::PanelError(e.to_string()))?,*/ .map_err(|e| UError::PanelError(e.to_string()))?,*/
Cmd::Serve => { 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() String::new()
} }
}) })

@ -20,7 +20,7 @@ struct AccessEnv {
u_server: String, u_server: String,
} }
#[tokio::main] #[actix_web::main]
async fn main() -> AnyResult<()> { async fn main() -> AnyResult<()> {
let env = load_env::<AccessEnv>()?; let env = load_env::<AccessEnv>()?;
let client = ClientHandler::new(&env.u_server, Some(env.admin_auth_token)); let client = ClientHandler::new(&env.u_server, Some(env.admin_auth_token));

@ -1,49 +1,11 @@
<mat-tab-group animationDuration="0ms" mat-align-tabs="center"> <mat-tab-group animationDuration="0ms" mat-align-tabs="center">
<mat-tab label="Agents"> <mat-tab label="Agents">
<div class="example-container mat-elevation-z8"> <agent-table></agent-table>
<div class="example-loading-shade" *ngIf="isLoadingResults"> </mat-tab>
<mat-spinner *ngIf="isLoadingResults"></mat-spinner> <mat-tab label="Jobs">
</div> <job-table></job-table>
</mat-tab>
<div class="example-table-container"> <mat-tab label="Results">
<result-table></result-table>
<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>
<mat-tab label="Jobs"></mat-tab>
<mat-tab label="Results"></mat-tab>
</mat-tab-group> </mat-tab-group>

@ -1,82 +1,9 @@
import { HttpClient } from '@angular/common/http';
import { Component, ViewChild, AfterViewInit } from '@angular/core'; 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 implements AfterViewInit { export class AppComponent {
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,17 +1,23 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatTableModule } from '@angular/material/table'; 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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { AgentComponent, JobComponent, ResultComponent } from './core/tables';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent AppComponent,
AgentComponent,
JobComponent,
ResultComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -19,6 +25,9 @@ import { HttpClientModule } from '@angular/common/http';
AppRoutingModule, AppRoutingModule,
MatTabsModule, MatTabsModule,
MatTableModule, MatTableModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
BrowserAnimationsModule BrowserAnimationsModule
], ],

@ -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,
}

@ -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
}

@ -0,0 +1,8 @@
export interface JobModel {
alias: string,
argv: string,
id: string,
exec_type: string,
platform: string,
payload: Uint8Array | null,
}

@ -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,
}

@ -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<T> {
status: "ok" | "err",
data: T | string
}
export class ApiTableService<T> {
area: string;
constructor(private http: HttpClient, area: string) {
this.area = area;
}
requestUrl = `${environment.server}/cmd/`;
req<R>(cmd: string): Observable<ServerResponse<R>> {
return this.http.post<ServerResponse<R>>(this.requestUrl, cmd);
}
getOne(id: string): Observable<ServerResponse<T>> {
return this.req(`${this.area} read ${id}`)
}
getMany(): Observable<ServerResponse<T[]>> {
return this.req(`${this.area} read`)
}
update(item: T): Observable<ServerResponse<void>> {
return this.req(`${this.area} update '${JSON.stringify(item)}'`)
}
delete(id: string): Observable<ServerResponse<void>> {
return this.req(`${this.area} delete ${id}`)
}
create(item: string): Observable<ServerResponse<void>> {
return this.req(`${this.area} create ${item}`)
}
}

@ -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<AgentModel> {
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);
}

@ -0,0 +1,3 @@
export * from './agent.component';
export * from './job.component';
export * from './result.component';

@ -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<JobModel> {
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);
}

@ -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<ResultModel> {
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);
}

@ -0,0 +1,36 @@
<div class="mat-elevation-z8">
<div class="table-container">
<div class="loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<mat-form-field appearance="standard">
<mat-label>Filter</mat-label>
<input matInput (keyup)="apply_filter($event)" #input>
</mat-form-field>
<button id="refresh_btn" mat-raised-button color="primary" (click)="fetch_many()">Refresh</button>
<table mat-table [dataSource]="table_data" class="data-table" matSort matSortActive="id" matSortDisableClear
matSortDirection="desc">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.def">
<th mat-header-cell *matHeaderCellDef>
{{column.name}}
</th>
<td mat-cell *matCellDef="let row">
{{column.cell(row)}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell">No data</td>
</tr>
</table>
</div>
<!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results">
</mat-paginator> -->
</div>

@ -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;
}

@ -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<T> implements OnInit {
abstract area: "agents" | "jobs" | "map";
data_source!: ApiTableService<T> | null;
table_data!: MatTableDataSource<T>;
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<T>[];
abstract displayedColumns: string[];
}
type ColumnDef<C> = {
def: string,
name: string,
cell: (cell: C) => string
}

@ -1,3 +1,4 @@
export const environment = { export const environment = {
production: true production: true,
server: ""
}; };

@ -3,7 +3,8 @@
// The list of file replacements can be found in `angular.json`. // The list of file replacements can be found in `angular.json`.
export const environment = { export const environment = {
production: false production: false,
server: "http://127.0.0.1:8080"
}; };
/* /*

@ -14,12 +14,14 @@ almost all fields are editable, rows are deletable
mod errors; mod errors;
use crate::{process_cmd, Args}; use crate::{process_cmd, Args};
use actix_cors::Cors;
use actix_web::{get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder}; use actix_web::{get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder};
use errors::Error; use errors::Error;
use futures_util::StreamExt;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use std::borrow::Cow; use std::borrow::Cow;
use structopt::StructOpt; use structopt::StructOpt;
use u_lib::{api::ClientHandler, logging::init_logger, unwrap_enum}; use u_lib::{api::ClientHandler, unwrap_enum};
#[derive(RustEmbed)] #[derive(RustEmbed)]
#[folder = "./src/server/fe/dist/fe/"] #[folder = "./src/server/fe/dist/fe/"]
@ -52,12 +54,21 @@ async fn static_files_adapter(path: web::Path<(String,)>) -> impl Responder {
#[post("/cmd/")] #[post("/cmd/")]
async fn send_cmd( async fn send_cmd(
cmd: web::Json<String>, mut body: web::Payload,
client: web::Data<ClientHandler>, client: web::Data<ClientHandler>,
) -> Result<impl Responder, Error> { ) -> Result<impl Responder, Error> {
let parsed_cmd = Args::from_iter_safe( let mut bytes = web::BytesMut::new();
shlex::split(&cmd.into_inner()).ok_or(Error::JustError("shlex failed".to_string()))?, 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( Ok(
match process_cmd(client.as_ref().clone(), parsed_cmd).await { match process_cmd(client.as_ref().clone(), parsed_cmd).await {
Ok(r) => HttpResponse::Ok().body(r), 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<()> { pub async fn serve(client: ClientHandler) -> std::io::Result<()> {
init_logger(Some("u_panel"));
let addr = "127.0.0.1:8080"; let addr = "127.0.0.1:8080";
info!("Serving at http://{}", addr); info!("Serving at http://{}", addr);
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(Cors::permissive())
.app_data(web::Data::new(client.clone())) .app_data(web::Data::new(client.clone()))
.service(main_page) .service(main_page)
.service(send_cmd) .service(send_cmd)

@ -4,7 +4,7 @@ use once_cell::sync::OnceCell;
use serde::Deserialize; use serde::Deserialize;
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
use u_lib::{ use u_lib::{
models::{schema, Agent, AgentError, AssignedJob, JobMeta, JobState}, models::{schema, Agent, AssignedJob, JobMeta, JobState},
utils::load_env, utils::load_env,
}; };
use uuid::Uuid; use uuid::Uuid;
@ -40,14 +40,6 @@ impl UDB {
.unwrap() .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<()> { pub fn insert_jobs(&self, job_metas: &[JobMeta]) -> SResult<()> {
use schema::jobs; use schema::jobs;
diesel::insert_into(jobs::table) diesel::insert_into(jobs::table)
@ -203,35 +195,3 @@ impl UDB {
Ok(affected) 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()
)
}
}
*/

@ -1,3 +1,5 @@
use std::time::SystemTime;
use crate::db::UDB; use crate::db::UDB;
use crate::errors::Error; use crate::errors::Error;
use diesel::SaveChangesDsl; use diesel::SaveChangesDsl;
@ -89,12 +91,14 @@ impl Endpoints {
let mut failed = vec![]; let mut failed = vec![];
for entry in msg.into_inner().into_vec() { for entry in msg.into_inner().into_vec() {
match entry { match entry {
Reportable::Assigned(res) => { Reportable::Assigned(mut result) => {
if id != res.agent_id { if id != result.agent_id {
continue; continue;
} }
result.state = JobState::Finished;
result.updated = SystemTime::now();
let db = UDB::lock_db(); let db = UDB::lock_db();
if let Err(e) = res if let Err(e) = result
.save_changes::<AssignedJob>(&db.conn) .save_changes::<AssignedJob>(&db.conn)
.map_err(Error::from) .map_err(Error::from)
{ {
@ -106,13 +110,7 @@ impl Endpoints {
Self::add_agent(a).await?; Self::add_agent(a).await?;
} }
Reportable::Error(e) => { Reportable::Error(e) => {
let err = AgentError::from_msg(e, id); warn!("{} reported an error: {}", id, Stripped(&e.as_str()));
warn!(
"{} reported an error: {}",
err.agent_id,
Stripped(&err.msg.as_str())
);
//UDB::lock_db().report_error(&err)?;
} }
Reportable::Dummy => (), Reportable::Dummy => (),
} }

@ -14,6 +14,7 @@ extern crate diesel;
mod db; mod db;
mod errors; mod errors;
mod handlers; mod handlers;
mod models;
use errors::{Error, SResult}; use errors::{Error, SResult};
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
@ -151,12 +152,20 @@ pub fn init_endpoints(
} }
pub fn prefill_jobs() -> SResult<()> { pub fn prefill_jobs() -> SResult<()> {
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() let agent_hello = RawJobMeta::builder()
.with_type(misc::JobType::Manage) .with_type(misc::JobType::Manage)
.with_alias("agent_hello") .with_alias(job_alias)
.build() .build()
.unwrap(); .unwrap();
UDB::lock_db().insert_jobs(&[agent_hello]) UDB::lock_db().insert_jobs(&[agent_hello])
}
Err(e) => Err(e),
}
} }
pub async fn serve() -> SResult<()> { pub async fn serve() -> SResult<()> {

@ -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"),
}
}

@ -1 +1,2 @@
mod behaviour; mod behaviour;
mod connection;

@ -2,28 +2,5 @@ mod fixtures;
mod helpers; mod helpers;
mod integration; mod integration;
use crate::helpers::ENV;
use u_lib::config::MASTER_PORT;
#[macro_use] #[macro_use]
extern crate rstest; 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"),
}
}

@ -28,6 +28,7 @@ tracing-subscriber = { version = "0.3.14", features = ["env-filter"] }
tracing-appender = "0.2.2" tracing-appender = "0.2.2"
log = "*" log = "*"
anyhow = "1.0.58" anyhow = "1.0.58"
platforms = "3.0.1"
[features] [features]
panel = [] panel = []

@ -92,6 +92,12 @@ impl ClientHandler {
pub async fn dl(&self, file: String) -> Result<Vec<u8>> { pub async fn dl(&self, file: String) -> Result<Vec<u8>> {
self._req(format!("dl/{file}"), Empty).await self._req(format!("dl/{file}"), Empty).await
} }
/// get all available jobs
pub async fn get_jobs(&self, job: Option<Uuid>) -> Result<Vec<models::JobMeta>> {
self._req(format!("get_jobs/{}", opt_to_string(job)), Empty)
.await
}
} }
//##########// Admin area //##########// //##########// Admin area //##########//
@ -108,12 +114,6 @@ impl ClientHandler {
self._req("update_item", item).await self._req("update_item", item).await
} }
/// get all available jobs
pub async fn get_jobs(&self, job: Option<Uuid>) -> Result<Vec<models::JobMeta>> {
self._req(format!("get_jobs/{}", opt_to_string(job)), Empty)
.await
}
/// create and upload job /// create and upload job
pub async fn upload_jobs(&self, payload: &[models::JobMeta]) -> Result<Empty> { pub async fn upload_jobs(&self, payload: &[models::JobMeta]) -> Result<Empty> {
self._req("upload_jobs", payload).await self._req("upload_jobs", payload).await
@ -131,7 +131,7 @@ impl ClientHandler {
/// get jobs for any agent /// get jobs for any agent
pub async fn get_agent_jobs(&self, agent: Option<Uuid>) -> Result<Vec<models::AssignedJob>> { pub async fn get_agent_jobs(&self, agent: Option<Uuid>) -> Result<Vec<models::AssignedJob>> {
self._req(format!("get_personal_jobs/{}", opt_to_string(agent)), Empty) self._req(format!("get_agent_jobs/{}", opt_to_string(agent)), Empty)
.await .await
} }
} }

@ -3,10 +3,9 @@ use crate::{
executor::{DynFut, Waiter}, executor::{DynFut, Waiter},
messaging::Reportable, messaging::Reportable,
models::{Agent, AssignedJob, JobMeta, JobType, RawJobMeta}, models::{Agent, AssignedJob, JobMeta, JobType, RawJobMeta},
utils::{CombinedResult, OneOrVec}, utils::{CombinedResult, OneOrVec, Platform},
UError, UResult, UError, UResult,
}; };
use guess_host_triple::guess_host_triple;
use std::collections::HashMap; use std::collections::HashMap;
pub struct JobBuilder { pub struct JobBuilder {
@ -30,12 +29,11 @@ impl JobBuilder {
Ok(match job_meta.exec_type { Ok(match job_meta.exec_type {
JobType::Shell => { JobType::Shell => {
let meta = JobCache::get(req.job_id).ok_or(UError::NoJob(req.job_id))?; 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(); let curr_platform = Platform::current();
//TODO: extend platform checking (partial check) if !curr_platform.matches(&meta.platform) {
if meta.platform != curr_platform {
return Err(UError::InsuitablePlatform( return Err(UError::InsuitablePlatform(
meta.platform.clone(), meta.platform.clone(),
curr_platform, curr_platform.into_string(),
) )
.into()); .into());
} }

@ -7,8 +7,11 @@ use strum::Display;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use crate::builder::NamedJobBuilder; use crate::builder::NamedJobBuilder;
use crate::{ use crate::{
config::get_self_uid, messaging::Reportable, models::schema::*, unwrap_enum, config::get_self_uid,
utils::systime_to_string, messaging::Reportable,
models::schema::*,
unwrap_enum,
utils::{systime_to_string, Platform},
}; };
use uuid::Uuid; use uuid::Uuid;
@ -94,13 +97,16 @@ impl Agent {
hostname: decoder(builder.pop("hostname")), hostname: decoder(builder.pop("hostname")),
is_root: &decoder(builder.pop("is_root")) == "0", is_root: &decoder(builder.pop("is_root")) == "0",
username: decoder(builder.pop("username")), username: decoder(builder.pop("username")),
platform: guess_host_triple::guess_host_triple() platform: Platform::current().into_string(),
.unwrap_or("unknown")
.to_string(),
..Default::default() ..Default::default()
} }
} }
#[cfg(not(unix))]
pub async fn gather() -> Self {
Self::default()
}
pub async fn run() -> Reportable { pub async fn run() -> Reportable {
Reportable::Agent(Agent::gather().await) Reportable::Agent(Agent::gather().await)
} }

@ -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<String>, 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(),
}
}
}

@ -112,8 +112,6 @@ impl AssignedJob {
}; };
self.result = Some(data); self.result = Some(data);
self.retcode = retcode; self.retcode = retcode;
self.updated = SystemTime::now();
self.state = JobState::Finished;
Reportable::Assigned(self) Reportable::Assigned(self)
} }

@ -1,4 +1,5 @@
use super::JobType; use super::JobType;
use crate::utils::Platform;
use crate::{models::schema::*, utils::Stripped, UError, UResult}; use crate::{models::schema::*, utils::Stripped, UError, UResult};
use diesel::{Identifiable, Insertable, Queryable}; use diesel::{Identifiable, Insertable, Queryable};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -23,19 +24,19 @@ pub struct JobMeta {
pub payload: Option<Vec<u8>>, pub payload: Option<Vec<u8>>,
} }
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
pub struct RawJobMeta { pub struct RawJobMeta {
#[serde(default)] #[serde(default)]
pub alias: Option<String>, pub alias: Option<String>,
#[serde(default)] #[serde(default)]
pub argv: String, pub argv: String,
#[serde(default)] #[serde(default)]
pub id: Uuid, pub id: UuidDefaultUnique,
#[serde(default)] #[serde(default)]
pub exec_type: JobType, pub exec_type: JobType,
//pub schedule: JobSchedule, //pub schedule: JobSchedule,
#[serde(default)] #[serde(default)]
pub platform: String, pub platform: Platform,
#[serde(default)] #[serde(default)]
pub payload: Option<Vec<u8>>, pub payload: Option<Vec<u8>>,
#[serde(default)] #[serde(default)]
@ -86,9 +87,7 @@ impl Default for JobMeta {
alias: None, alias: None,
argv: String::new(), argv: String::new(),
exec_type: JobType::Shell, exec_type: JobType::Shell,
platform: guess_host_triple::guess_host_triple() platform: Platform::current().into_string(),
.unwrap_or("unknown")
.to_string(),
payload: None, payload: None,
} }
} }
@ -97,13 +96,11 @@ impl Default for JobMeta {
impl Default for RawJobMeta { impl Default for RawJobMeta {
fn default() -> Self { fn default() -> Self {
Self { Self {
id: Uuid::new_v4(), id: UuidDefaultUnique::default(),
alias: None, alias: None,
argv: String::new(), argv: String::new(),
exec_type: JobType::Shell, exec_type: JobType::Shell,
platform: guess_host_triple::guess_host_triple() platform: Platform::current(),
.unwrap_or("unknown")
.to_string(),
payload: None, payload: None,
payload_path: 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()) Ok(inner.into())
} }
JobType::Manage => Ok(inner.into()), JobType::Manage => Ok(inner.into()),
@ -192,10 +195,19 @@ impl From<RawJobMeta> for JobMeta {
JobMeta { JobMeta {
alias: rjm.alias, alias: rjm.alias,
argv: rjm.argv, argv: rjm.argv,
id: rjm.id, id: rjm.id.0,
exec_type: rjm.exec_type, exec_type: rjm.exec_type,
platform: rjm.platform, platform: rjm.platform.into_string(),
payload: rjm.payload, payload: rjm.payload,
} }
} }
} }
#[derive(Deserialize, Debug)]
pub struct UuidDefaultUnique(Uuid);
impl Default for UuidDefaultUnique {
fn default() -> Self {
Self(Uuid::new_v4())
}
}

@ -1,6 +1,5 @@
mod agent; mod agent;
mod error;
mod jobs; mod jobs;
pub mod schema; pub mod schema;
pub use crate::models::{agent::*, error::*, jobs::*}; pub use crate::models::{agent::*, jobs::*};

@ -3,6 +3,7 @@ pub mod conv;
pub mod env; pub mod env;
pub mod fmt; pub mod fmt;
pub mod misc; pub mod misc;
pub mod platform;
pub mod proc_output; pub mod proc_output;
pub mod storage; pub mod storage;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -16,6 +17,7 @@ pub use conv::*;
pub use env::{load_env, load_env_default}; pub use env::{load_env, load_env_default};
pub use fmt::*; pub use fmt::*;
pub use misc::*; pub use misc::*;
pub use platform::*;
pub use proc_output::*; pub use proc_output::*;
pub use storage::*; pub use storage::*;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]

@ -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<str>) -> 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()
}
}

@ -1,5 +1,7 @@
use crate::{UError, UResult}; 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; use uuid::Uuid;
pub struct TempFile { pub struct TempFile {
@ -28,8 +30,12 @@ impl TempFile {
let path = this.get_path(); let path = this.get_path();
dbg!(&path); dbg!(&path);
this.write_all(data)?; this.write_all(data)?;
#[cfg(unix)]
{
let perms = fs::Permissions::from_mode(0o555); let perms = fs::Permissions::from_mode(0o555);
fs::set_permissions(&path, perms).map_err(|e| UError::FSError(path, e.to_string()))?; fs::set_permissions(&path, perms).map_err(|e| UError::FSError(path, e.to_string()))?;
}
Ok(this) Ok(this)
} }
} }

@ -3,7 +3,6 @@ DROP TABLE results;
DROP TABLE certificates; DROP TABLE certificates;
DROP TABLE jobs; DROP TABLE jobs;
DROP TABLE agents; DROP TABLE agents;
DROP TABLE errors;
DROP TYPE IF EXISTS JobState; DROP TYPE IF EXISTS JobState;
DROP TYPE IF EXISTS JobType; DROP TYPE IF EXISTS JobType;

@ -65,13 +65,3 @@ CREATE TABLE IF NOT EXISTS certificates (
, PRIMARY KEY(id) , PRIMARY KEY(id)
, FOREIGN KEY(agent_id) REFERENCES agents(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)
);
Loading…
Cancel
Save