* 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 binarypull/1/head
parent
a50e6d242f
commit
c25fa780bf
46 changed files with 550 additions and 308 deletions
@ -1,49 +1,11 @@ |
||||
<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> |
||||
|
||||
<agent-table></agent-table> |
||||
</mat-tab> |
||||
<mat-tab label="Jobs"> |
||||
<job-table></job-table> |
||||
</mat-tab> |
||||
<mat-tab label="Results"> |
||||
<result-table></result-table> |
||||
</mat-tab> |
||||
<mat-tab label="Jobs"></mat-tab> |
||||
<mat-tab label="Results"></mat-tab> |
||||
</mat-tab-group> |
@ -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<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); |
||||
} |
||||
export class AppComponent { |
||||
} |
||||
|
@ -0,0 +1 @@ |
||||
export * from './services';
|
@ -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 @@ |
||||
export * from './api.service' |
@ -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 = { |
||||
production: true |
||||
production: true, |
||||
server: "" |
||||
}; |
||||
|
@ -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 connection; |
||||
|
@ -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(), |
||||
} |
||||
} |
||||
} |
@ -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::*}; |
||||
|
@ -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() |
||||
} |
||||
} |
Loading…
Reference in new issue