add payload-overview component & use try_from rawjob instead of builder

pull/9/head
plazmoid 1 year ago
parent 45bba0dd9b
commit 69b1d3d901
  1. 8
      Cargo.lock
  2. 14
      bin/u_panel/src/argparse.rs
  3. 4
      bin/u_panel/src/gui/fe/src/app/app.module.ts
  4. 22
      bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.html
  5. 29
      bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.ts
  6. 13
      bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.html
  7. 5
      bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.ts
  8. 9
      bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.html
  9. 0
      bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.less
  10. 22
      bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.ts
  11. 4
      bin/u_panel/src/gui/fe/src/app/components/tables/agent-table/agent-table.component.ts
  12. 5
      bin/u_panel/src/gui/fe/src/app/components/tables/base-table/base-table.component.ts
  13. 62
      bin/u_panel/src/gui/fe/src/app/components/tables/job-table/job-table.component.ts
  14. 6
      bin/u_panel/src/gui/fe/src/app/components/tables/payload-table/payload-table.component.ts
  15. 4
      bin/u_panel/src/gui/fe/src/app/components/tables/result-table/result-table.component.ts
  16. 4
      bin/u_panel/src/gui/fe/src/app/models/index.ts
  17. 6
      bin/u_panel/src/gui/fe/src/app/models/job.model.ts
  18. 23
      bin/u_panel/src/gui/fe/src/app/services/api.service.ts
  19. 20
      bin/u_server/src/db.rs
  20. 14
      bin/u_server/src/handlers.rs
  21. 8
      bin/u_server/src/u_server.rs
  22. 4
      integration-tests/tests/fixtures/agent.rs
  23. 8
      integration-tests/tests/integration_tests/behaviour.rs
  24. 16
      integration-tests/tests/integration_tests/endpoints.rs
  25. 31
      lib/u_lib/src/api.rs
  26. 2
      lib/u_lib/src/cache.rs
  27. 40
      lib/u_lib/src/jobs.rs
  28. 2
      lib/u_lib/src/messaging.rs
  29. 6
      lib/u_lib/src/models/jobs/assigned.rs
  30. 268
      lib/u_lib/src/models/jobs/meta.rs
  31. 3
      lib/u_lib/src/models/jobs/misc.rs
  32. 10
      lib/u_lib/src/models/payload.rs

8
Cargo.lock generated

@ -1700,9 +1700,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "platforms"
@ -2024,9 +2024,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.37.17"
version = "0.37.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc809f704c03a812ac71f22456c857be34185cac691a4316f27ab0f633bb9009"
checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433"
dependencies = [
"bitflags",
"errno 0.3.1",

@ -99,7 +99,7 @@ pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult<Value> {
CRUD::Create { item: job } => {
let raw_job = from_str::<RawJob>(&job)
.map_err(|e| UError::DeserializeError(e.to_string(), job))?;
let mut job = raw_job.validated()?;
let mut job = raw_job.try_into_job()?;
if let Some(payload) = &mut job.payload {
payload.join_payload()?;
@ -112,13 +112,13 @@ pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult<Value> {
None => into_value(client.get_jobs().await?),
},
CRUD::RUD(RUD::Update { item }) => {
let raw_job = from_str::<RawJob>(&item)
let raw_job = from_str::<JobMeta>(&item)
.map_err(|e| UError::DeserializeError(e.to_string(), item))?;
let mut job = raw_job.validated()?;
let job = raw_job.validate()?;
if let Some(payload) = &mut job.payload {
payload.join_payload()?;
}
// if let Some(payload) = &mut job.payload {
// payload.join_payload()?;
// }
into_value(client.update_job(&job).await?)
}
@ -145,7 +145,7 @@ pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult<Value> {
}
PayloadCRUD::Read { id } => match id {
None => into_value(client.get_payloads().await?),
Some(id) => into_value(client.get_payload(id, args.brief).await?),
Some(id) => into_value(vec![client.get_payload(id, args.brief).await?]),
},
PayloadCRUD::Update { item } => {
let payload = from_str::<Payload>(&item)

@ -27,6 +27,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatListModule } from '@angular/material/list';
import { GlobalErrorComponent } from './components/global-error/global-error.component';
import { PayloadOverviewComponent } from './components/payload-overview/payload-overview.component';
@NgModule({
declarations: [
@ -40,7 +41,8 @@ import { GlobalErrorComponent } from './components/global-error/global-error.com
AssignJobDialogComponent,
PayloadComponent,
PayloadInfoDialogComponent,
GlobalErrorComponent
GlobalErrorComponent,
PayloadOverviewComponent
],
imports: [
BrowserModule,

@ -4,46 +4,40 @@
<div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field">
<mat-label>ID</mat-label>
<input matInput disabled value="{{data.job.id}}">
<input matInput disabled value="{{data.meta.id}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Alias</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.alias">
<input matInput [readonly]="isPreview" [(ngModel)]="data.meta.alias">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Args</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.argv">
<input matInput [readonly]="isPreview" [(ngModel)]="data.meta.argv">
</mat-form-field>
</div>
<div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field">
<mat-label>Type</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.exec_type">
<input matInput [readonly]="isPreview" [(ngModel)]="data.meta.exec_type">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Platform</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.target_platforms">
<input matInput [readonly]="isPreview" [(ngModel)]="data.meta.target_platforms">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Schedule</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.schedule">
<input matInput [readonly]="isPreview" [(ngModel)]="data.meta.schedule">
</mat-form-field>
</div>
<div class="info-dialog-forms-box">
<mat-form-field class="info-dlg-field">
<mat-label>Payload</mat-label>
<mat-select [disabled]="isPreview" [(value)]="data.job.payload">
<mat-select [disabled]="isPreview" [(value)]="data.meta.payload_id">
<mat-option *ngFor="let pld of allPayloads" [value]="pld[0]">{{ pld[1] }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="info-dlg-field" floatLabel="always">
<mat-label>Payload data</mat-label>
<textarea matInput cdkTextareaAutosize *ngIf="!isTooBigPayload" [readonly]="isPreview"
[(ngModel)]="decodedPayload">
</textarea>
<input matInput *ngIf="isTooBigPayload" disabled placeholder="Payload is too big to display">
</mat-form-field>
</div>
<payload-overview *ngIf="data.payload" [preview]="true" [payload]="data.payload"></payload-overview>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button *ngIf="isPreview" (click)="isPreview = false">Edit</button>

@ -1,7 +1,7 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { EventEmitter } from '@angular/core';
import { Job } from '../../../models/job.model';
import { Job, JobModel } from '../../../models/job.model';
import { ApiTableService } from 'src/app/services';
import { PayloadModel } from 'src/app/models';
@ -11,36 +11,19 @@ import { PayloadModel } from 'src/app/models';
styleUrls: ['../base-info-dialog.component.less']
})
export class JobInfoDialogComponent {
isPreview = true;
isTooBigPayload = false;
decodedPayload = "";
//[id, name]
allPayloads: [string, string][] = [];
isPreview = true;
allPayloads: [string | null, string][] = [[null, "none"]];
onSave = new EventEmitter();
onSave = new EventEmitter<JobModel>();
constructor(@Inject(MAT_DIALOG_DATA) public data: Job, dataSource: ApiTableService) {
if (data.payload !== null) {
this.showPayload(data.payload)
}
dataSource.getPayloads().subscribe(resp => {
this.allPayloads = resp.map(r => [r.id, r.name])
this.allPayloads = this.allPayloads.concat(resp.map(r => [r.id, r.name]))
})
}
showPayload(payload: PayloadModel) {
if (payload.data !== null) {
this.decodedPayload = new TextDecoder().decode(new Uint8Array(payload.data))
} else {
this.isTooBigPayload = true
}
}
updateJob() {
// if (this.decodedPayload.length > 0) {
// this.data.payload = Array.from(new TextEncoder().encode(this.decodedPayload))
// }
// this.onSave.emit(this.data);
this.onSave.emit(this.data.meta);
}
}

@ -1,23 +1,26 @@
<h2 mat-dialog-title>Result</h2>
<h2 mat-dialog-title>Payload</h2>
<mat-dialog-content>
<div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field" cdkFocusInitial>
<mat-label>ID</mat-label>
<input matInput readonly value="{{data.id}}">
<input matInput readonly value="{{payload.id}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Name</mat-label>
<input matInput value="{{data.name}}">
<input matInput value="{{payload.name}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>MIME-type</mat-label>
<input matInput value="{{data.mime_type}}">
<input matInput value="{{payload.mime_type}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Size</mat-label>
<input matInput value="{{data.size}}">
<input matInput value="{{payload.size}}">
</mat-form-field>
</div>
<div class="info-dialog-forms-box">
<payload-overview [preview]="true" [payload]="payload"></payload-overview>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Close</button>

@ -5,10 +5,9 @@ import { PayloadModel } from 'src/app/models/payload.model';
@Component({
selector: 'payload-info-dialog',
templateUrl: 'payload-info-dialog.component.html',
styleUrls: []
styleUrls: ['../base-info-dialog.component.less']
})
export class PayloadInfoDialogComponent {
constructor(@Inject(MAT_DIALOG_DATA) public data: PayloadModel) { }
constructor(@Inject(MAT_DIALOG_DATA) public payload: PayloadModel) { }
}

@ -0,0 +1,9 @@
<div class="info-dialog-forms-box">
<mat-form-field class="info-dlg-field" floatLabel="always">
<mat-label>Payload data</mat-label>
<textarea matInput cdkTextareaAutosize *ngIf="!isTooBigPayload" [readonly]="isPreview"
[(ngModel)]="decodedPayload">
</textarea>
<input matInput *ngIf="isTooBigPayload" disabled placeholder="Payload is too big to display">
</mat-form-field>
</div>

@ -0,0 +1,22 @@
import { Component, Input, OnInit } from '@angular/core';
import { PayloadModel } from 'src/app/models';
@Component({
selector: 'payload-overview',
templateUrl: './payload-overview.component.html',
styleUrls: ['./payload-overview.component.less']
})
export class PayloadOverviewComponent implements OnInit {
@Input() payload!: PayloadModel;
@Input("preview") isPreview = true;
isTooBigPayload = false;
decodedPayload = "";
ngOnInit() {
if (this.payload.data !== null) {
this.decodedPayload = new TextDecoder().decode(new Uint8Array(this.payload.data))
} else {
this.isTooBigPayload = true
}
}
}

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { TablesComponent } from '../base-table/base-table.component';
import { TableComponent } from '../base-table/base-table.component';
import { AgentModel, Area } from '../../../models';
import { AssignJobDialogComponent, AgentInfoDialogComponent } from '../../dialogs';
@ -8,7 +8,7 @@ import { AssignJobDialogComponent, AgentInfoDialogComponent } from '../../dialog
templateUrl: './agent-table.component.html',
styleUrls: ['../base-table/base-table.component.less'],
})
export class AgentComponent extends TablesComponent<AgentModel> implements OnInit {
export class AgentComponent extends TableComponent<AgentModel> implements OnInit {
area = 'agents' as Area
displayedColumns = ['id', 'alias', 'username', 'hostname', 'last_active', 'actions']

@ -6,7 +6,7 @@ import { ApiModel, Area } from '../../../models';
import { ActivatedRoute, Router } from '@angular/router';
@Directive()
export abstract class TablesComponent<T extends ApiModel> implements OnInit {
export abstract class TableComponent<T extends ApiModel> implements OnInit {
abstract area: Area;
table_data: MatTableDataSource<T> = new MatTableDataSource;
isLoadingResults = true;
@ -49,7 +49,8 @@ export abstract class TablesComponent<T extends ApiModel> implements OnInit {
deleteItem(id: string) {
if (confirm(`Delete ${id}?`)) {
this.dataSource.delete(id, this.area)
this.dataSource.delete(id, this.area).subscribe(_ => { })
this.loadTableData()
}
}

@ -1,7 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { TablesComponent } from '../base-table/base-table.component';
import { Area, JobModel } from '../../../models';
import { TableComponent } from '../base-table/base-table.component';
import { Area, JobModel, Job } from '../../../models';
import { JobInfoDialogComponent } from '../../dialogs';
import { Observable } from 'rxjs';
@Component({
selector: 'job-table',
@ -9,27 +10,57 @@ import { JobInfoDialogComponent } from '../../dialogs';
styleUrls: ['../base-table/base-table.component.less'],
providers: [{ provide: 'area', useValue: 'jobs' }]
})
export class JobComponent extends TablesComponent<JobModel> {
export class JobComponent extends TableComponent<JobModel> {
area = 'jobs' as Area;
displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions']
showItemDialog(id: string | null) {
const show_dlg = (id: string, edit: boolean) => {
this.dataSource.getJob(id).subscribe(resp => {
var dialog = this.infoDialog.open(JobInfoDialogComponent, {
data: resp,
const is_new_job = id === null;
var dialogData$: Observable<Job>;
if (is_new_job) {
dialogData$ = new Observable(subscriber => {
var defaultJob: Job = {
meta: {
alias: null,
argv: '',
exec_type: 'shell',
target_platforms: '*',
payload_id: null,
schedule: null
},
payload: null
};
subscriber.next(defaultJob)
})
} else {
dialogData$ = this.dataSource.getJob(id)
}
dialogData$.subscribe(dialogData => {
const dialog = this.infoDialog.open(JobInfoDialogComponent, {
data: dialogData,
width: '1000px',
});
if (edit) {
dialog.componentInstance.isPreview = false
}
dialog.componentInstance.isPreview = !is_new_job;
const saveSub = dialog.componentInstance.onSave.subscribe(result => {
if (is_new_job) {
this.dataSource.create(dialogData.meta, this.area)
.subscribe(_ => {
alert("Created")
this.loadTableData()
})
} else {
this.dataSource.updateJob(result)
.subscribe(_ => {
alert("Saved")
alert("Updated")
this.loadTableData()
})
}
dialog.close()
})
dialog.afterClosed().subscribe(result => {
@ -38,13 +69,4 @@ export class JobComponent extends TablesComponent<JobModel> {
})
})
}
if (id) {
show_dlg(id, false)
} else {
this.dataSource.create(null, this.area).subscribe(resp => {
show_dlg(resp[0], true)
})
}
}
}

@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { Area } from 'src/app/models';
import { PayloadModel } from 'src/app/models/payload.model';
import { PayloadInfoDialogComponent } from '../../dialogs';
import { TablesComponent } from '../base-table/base-table.component';
import { TableComponent } from '../base-table/base-table.component';
@Component({
selector: 'payload-table',
@ -10,9 +10,9 @@ import { TablesComponent } from '../base-table/base-table.component';
styleUrls: ['../base-table/base-table.component.less'],
providers: [{ provide: 'area', useValue: 'payloads' }]
})
export class PayloadComponent extends TablesComponent<PayloadModel> {
export class PayloadComponent extends TableComponent<PayloadModel> {
area = 'payloads' as Area
displayedColumns = ["name", "mime_type", "size"];
displayedColumns = ["name", "mime_type", "size", 'actions'];
showItemDialog(id: string) {
this.dataSource.getPayload(id).subscribe(resp => {

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { TablesComponent } from '../base-table/base-table.component';
import { TableComponent } from '../base-table/base-table.component';
import { Area, ResultModel } from '../../../models';
import { ResultInfoDialogComponent } from '../../dialogs';
@ -9,7 +9,7 @@ import { ResultInfoDialogComponent } from '../../dialogs';
styleUrls: ['../base-table/base-table.component.less'],
providers: [{ provide: 'area', useValue: 'map' }]
})
export class ResultComponent extends TablesComponent<ResultModel> {
export class ResultComponent extends TableComponent<ResultModel> {
area = 'map' as Area
displayedColumns = [
'id',

@ -18,7 +18,3 @@ export type Area = "agents" | "jobs" | "map" | "payloads";
export type ApiModel = AgentModel | JobModel | ResultModel | PayloadModel | Empty;
export interface Empty { }
export function getAreaByModel(_: AgentModel): Area {
return "agents"
}

@ -3,14 +3,14 @@ import { PayloadModel } from './'
export interface JobModel {
alias: string | null,
argv: string,
id: string,
id?: string,
exec_type: string,
target_platforms: string,
payload: string | null,
payload_id: string | null,
schedule: string | null,
}
export interface Job {
job: JobModel,
meta: JobModel,
payload: PayloadModel | null,
}

@ -1,7 +1,7 @@
import { environment } from 'src/environments/environment';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, map, catchError, throwError } from 'rxjs';
import { ApiModel, getAreaByModel, PayloadModel, Empty, Area, AgentModel, JobModel, ResultModel, Job } from '../models';
import { ApiModel, PayloadModel, Empty, Area, AgentModel, JobModel, ResultModel, Job } from '../models';
import { Injectable, Inject } from '@angular/core';
import { ErrorService } from './error.service';
@ -26,9 +26,7 @@ export class ApiTableService {
requestUrl = `${environment.server}/cmd/`;
req<R extends ApiModel>(cmd: string): Observable<ServerResponse<R>> {
return this.http.post<ServerResponse<R>>(this.requestUrl, cmd).pipe(
catchError(this.errorHandler)
)
return this.http.post<ServerResponse<R>>(this.requestUrl, cmd)
}
getOne<T extends ApiModel>(id: string, area: Area, brief: 'yes' | 'no' | 'auto' | null = null): Observable<T> {
@ -73,11 +71,11 @@ export class ApiTableService {
return this.getMany('agents')
}
getJobs(): Observable<AgentModel[]> {
getJobs(): Observable<JobModel[]> {
return this.getMany('jobs')
}
getResults(): Observable<AgentModel[]> {
getResults(): Observable<ResultModel[]> {
return this.getMany('map')
}
@ -109,11 +107,12 @@ export class ApiTableService {
return this.filterErrStatus(this.req(`${area} delete ${id}`))
}
create(item: string | null, area: Area): Observable<string[]> {
if (!item) {
item = '"{}"'
create<T extends ApiModel>(item: T | null, area: Area): Observable<string[]> {
var serialized = '"{}"'
if (item) {
serialized = JSON.stringify(item);
}
return this.filterErrStatus(this.req(`${area} create ${item}`))
return this.filterErrStatus(this.req(`${area} create '${serialized}'`))
}
createResult(item: string): Observable<string[]> {
@ -131,8 +130,8 @@ export class ApiTableService {
catchError(this.errorHandler.bind(this)))
}
errorHandler<R>(err: HttpErrorResponse, _: R) {
this.errorService.handle(err.message);
errorHandler(err: HttpErrorResponse, caught: any) {
this.errorService.handle(caught.data ?? err.message);
return throwError(() => new Error(err.message));
}
}

@ -3,7 +3,7 @@ use diesel::{pg::PgConnection, prelude::*, result::Error as DslError, Connection
use std::mem::drop;
use u_lib::{
db::PgAsyncPool,
models::{schema, Agent, AssignedJob, Job, JobModel, JobState, Payload},
models::{schema, Agent, AssignedJob, Job, JobMeta, JobState, Payload},
platform::Platform,
types::Id,
};
@ -50,7 +50,7 @@ pub struct UDB<'c> {
}
impl UDB<'_> {
pub fn insert_jobs(&mut self, jobs: &[JobModel]) -> Result<()> {
pub fn insert_jobs(&mut self, jobs: &[JobMeta]) -> Result<()> {
use schema::jobs;
diesel::insert_into(jobs::table)
@ -67,7 +67,7 @@ impl UDB<'_> {
.values(payloads)
.execute(self.conn)
.map(drop)
.map_err(with_err_ctx("Can't insert payloads"))
.map_err(with_err_ctx(format!("Can't insert payloads {payloads:?}")))
}
pub fn get_job(&mut self, id: Id) -> Result<Option<Job>> {
@ -76,14 +76,14 @@ impl UDB<'_> {
let maybe_job_with_payload = jobs::table
.left_join(payloads::table)
.filter(jobs::id.eq(id))
.first::<(JobModel, Option<Payload>)>(self.conn)
.first::<(JobMeta, Option<Payload>)>(self.conn)
.optional()
.map_err(with_err_ctx(format!("Can't get job {id}")))?;
Ok(maybe_job_with_payload.map(|(job, payload)| Job { job, payload }))
Ok(maybe_job_with_payload.map(|(job, payload)| Job { meta: job, payload }))
}
pub fn get_jobs(&mut self) -> Result<Vec<JobModel>> {
pub fn get_jobs(&mut self) -> Result<Vec<JobMeta>> {
use schema::jobs;
jobs::table
@ -125,12 +125,12 @@ impl UDB<'_> {
let maybe_job_with_payload = jobs::table
.left_join(payloads::table)
.filter(jobs::alias.eq(alias))
.first::<(JobModel, Option<Payload>)>(self.conn)
.first::<(JobMeta, Option<Payload>)>(self.conn)
.optional()
.map_err(with_err_ctx(format!("Can't get job by alias {alias}")))?;
Ok(maybe_job_with_payload.map(|(job, payload_meta)| Job {
job,
meta: job,
payload: payload_meta,
}))
}
@ -303,8 +303,8 @@ impl UDB<'_> {
Ok(())
}
pub fn update_job(&mut self, job: &JobModel) -> Result<()> {
job.save_changes::<JobModel>(self.conn)
pub fn update_job(&mut self, job: &JobMeta) -> Result<()> {
job.save_changes::<JobMeta>(self.conn)
.map_err(with_err_ctx(format!("Can't update job {job:?}")))?;
Ok(())
}

@ -134,7 +134,7 @@ impl Endpoints {
.get_job_by_alias("agent_hello")?
.expect("agent_hello job not found");
db.set_jobs_for_agent(id, &[job.job.id])?;
db.set_jobs_for_agent(id, &[job.meta.id])?;
}
}
@ -165,7 +165,7 @@ impl Endpoints {
.collect::<Result<Vec<Job>, Error>>()?;
let (jobs, payloads_opt): (Vec<_>, Vec<_>) =
jobs.into_iter().map(|j| (j.job, j.payload)).unzip();
jobs.into_iter().map(|j| (j.meta, j.payload)).unzip();
let payloads = payloads_opt
.into_iter()
@ -224,7 +224,7 @@ impl Endpoints {
let job_from_db = db.get_job_by_alias(&ident);
match job_from_db {
Ok(job) => match job {
Some(j) => Ok(j.job.id),
Some(j) => Ok(j.meta.id),
None => {
Err(Error::ProcessingError(format!("unknown ident {ident}")))
}
@ -303,8 +303,8 @@ impl Endpoints {
.map_err(From::from)
}
pub async fn update_job(repo: Arc<PgRepo>, job: Job) -> EndpResult<retypes::UpdateJob> {
repo.interact(move |mut db| db.update_job(&job.job))
pub async fn update_job(repo: Arc<PgRepo>, job: JobMeta) -> EndpResult<retypes::UpdateJob> {
repo.interact(move |mut db| db.update_job(&job.validate()?))
.await
.map_err(From::from)
}
@ -325,11 +325,9 @@ impl Endpoints {
match payload.data {
Some(data) => {
let mut well_formed_payload =
Payload::from_data(data, Some(&payload.name)).map_err(Error::from)?;
Payload::from_data(data, Some(payload.name)).map_err(Error::from)?;
well_formed_payload.id = payload.id;
debug!("wf payload: {well_formed_payload:?}");
repo.interact(move |mut db| db.update_payload(&well_formed_payload))
.await
.map_err(From::from)

@ -115,7 +115,7 @@ pub fn init_endpoints(
let update_job = path("update_job")
.and(with_db.clone())
.and(body::json::<Job>())
.and(body::json::<JobMeta>())
.and_then(Endpoints::update_job)
.map(ok);
@ -183,12 +183,12 @@ pub async fn preload_jobs(repo: &PgRepo) -> Result<(), ServerError> {
let job_alias = "agent_hello";
let if_job_exists = db.get_job_by_alias(job_alias)?;
if if_job_exists.is_none() {
let agent_hello = RawJob::builder()
let agent_hello = RawJob::default()
.with_type(JobType::Init)
.with_alias(job_alias)
.build()
.try_into_job()
.unwrap();
db.insert_jobs(&[agent_hello.job])?;
db.insert_jobs(&[agent_hello.meta])?;
}
Ok(())
})

@ -23,9 +23,9 @@ pub fn registered_agent(client: &HttpClient) -> RegisteredAgent {
let job_id = resp.job_id;
let job = client.get_job(job_id, BriefMode::No).await.unwrap();
assert_eq!(job.job.alias, Some("agent_hello".to_string()));
assert_eq!(job.meta.alias, Some("agent_hello".to_string()));
let mut agent_data = AssignedJob::from((&job.job, resp));
let mut agent_data = AssignedJob::from((&job.meta, resp));
agent_data.set_result(&agent);
client

@ -21,12 +21,12 @@ async fn setup_tasks() {
let agents: Vec<Agent> = Panel::check_output("agents read");
let agent_id = agents[0].id;
let job_alias = "passwd_contents";
let job = RawJob::builder()
let job = RawJob::default()
.with_alias(job_alias)
.with_raw_payload("cat /etc/passwd")
.with_shell("/bin/bash {}")
.with_target_platforms("*linux*")
.build()
.try_into_job()
.unwrap();
Panel::check_status(["jobs", "create", &to_string(&RawJob::from(job)).unwrap()]);
@ -54,12 +54,12 @@ async fn large_payload() {
let agent = &Panel::check_output::<Vec<Agent>>("agents read")[0];
let agent_id = agent.id;
let job_alias = "large_payload";
let job = RawJob::builder()
let job = RawJob::default()
.with_alias(job_alias)
.with_payload_path("./tests/bin/echoer")
.with_shell("{} type echo")
.with_target_platforms(&agent.platform)
.build()
.try_into_job()
.unwrap();
Panel::check_status(["jobs", "create", &to_string(&RawJob::from(job)).unwrap()]);

@ -21,30 +21,30 @@ use u_lib::models::{BriefMode, RawJob, RawPayload, MAX_READABLE_PAYLOAD_SIZE};
#[tokio::test]
async fn jobs_upload_update_get_del(client_panel: &HttpClient) {
let job_alias = "henlo";
let mut job = RawJob::builder()
let mut job = RawJob::default()
.with_shell("/bin/bash {}")
.with_raw_payload("echo henlo")
.with_alias(job_alias)
.build()
.try_into_job()
.unwrap();
let job_id = job.job.id;
let job_id = job.meta.id;
client_panel.upload_jobs([&job]).await.unwrap();
let fetched_job = client_panel.get_full_job(job_id).await.unwrap();
assert_eq!(job, fetched_job);
// update job's payload by edit existing does nothing,
// editing is only allowed from payload itself
*job.payload.as_mut().unwrap().data.as_mut().unwrap() = b"echo henlo2".to_vec();
client_panel.update_job(&job).await.unwrap();
let new_alias = "henlo2".to_string();
job.meta.alias = Some(new_alias.clone());
client_panel.update_job(&job.meta).await.unwrap();
let fetched_job = client_panel.get_full_job(job_id).await.unwrap();
assert_eq!(
fetched_job.payload.as_ref().unwrap().data.as_ref().unwrap(),
b"echo henlo"
);
assert_eq!(fetched_job.meta.alias, Some(new_alias));
client_panel.del(job_id).await.unwrap();
@ -58,7 +58,7 @@ async fn payloads_upload_update_get_del(client_panel: &HttpClient) {
let name = "test1".to_string();
let data = b"qweasdzxc".to_vec();
let payload = RawPayload {
name: name.clone(),
name: Some(name.clone()),
data: data.clone(),
};

@ -2,7 +2,7 @@ use std::fmt::Debug;
use std::net::SocketAddr;
use std::{collections::HashMap, time::Duration};
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use reqwest::{header, header::HeaderMap, Certificate, Client, Identity, Method, Url};
use serde::de::DeserializeOwned;
use serde_json::{from_str, Value};
@ -26,7 +26,7 @@ pub mod retypes {
pub type Report = ();
pub type GetJob = Job;
pub type GetBriefJob = Job;
pub type GetJobs = Vec<JobModel>;
pub type GetJobs = Vec<JobMeta>;
pub type GetAgents = Vec<Agent>;
pub type UpdateAgent = ();
pub type UpdateJob = ();
@ -58,18 +58,17 @@ impl HttpClient {
default_headers.insert(header::AUTHORIZATION, format!("Bearer {pwd}"));
}
// todo: don't rely only on dns resolve
let client = {
let client = Client::builder()
.identity(identity)
.default_headers(HeaderMap::try_from(&default_headers).unwrap())
.add_root_certificate(Certificate::from_pem(ROOT_CA_CERT).unwrap())
.timeout(Duration::from_secs(20));
async fn resolve(domain_name: &str) -> Result<SocketAddr> {
let dns_response = Client::new()
.request(
Method::GET,
format!("https://1.1.1.1/dns-query?name={server}&type=A"),
format!("https://1.1.1.1/dns-query?name={domain_name}&type=A"),
)
.header(header::ACCEPT, "application/dns-json")
.send()
@ -77,16 +76,22 @@ impl HttpClient {
.text()
.await?;
match from_str::<Value>(&dns_response).unwrap()["Answer"]
.get(0)
let ip = from_str::<Value>(&dns_response)?
.get("Answer")
.and_then(|a| a.get(0))
.and_then(|a| a.get("data"))
{
Some(ip) => {
.map(|ip| ip.to_owned())
.ok_or_else(|| anyhow!("can't extract dns answer"))?;
let raw_addr = format!("{}:{MASTER_PORT}", ip.as_str().unwrap());
let addr: SocketAddr = raw_addr.parse().unwrap();
client.resolve(server, addr)
Ok(raw_addr.parse().unwrap())
}
None => client,
let client = match resolve(server).await {
Ok(addr) => client.resolve(server, addr),
Err(e) => {
warn!("DNS error: {e}");
client
}
}
.build()
@ -192,7 +197,7 @@ impl HttpClient {
}
/// update job
pub async fn update_job(&self, job: &Job) -> Result<retypes::UpdateJob> {
pub async fn update_job(&self, job: &JobMeta) -> Result<retypes::UpdateJob> {
self.req_with_payload("update_job", job).await
}

@ -15,7 +15,7 @@ pub struct JobCache;
impl JobCache {
pub fn insert(job: Val) {
JOB_CACHE.write().insert(job.job.id, job);
JOB_CACHE.write().insert(job.meta.id, job);
}
pub fn contains(id: Id) -> bool {

@ -29,7 +29,7 @@ impl AnonymousJobBatch {
let jobs_ids: Vec<_> = jobs
.into_iter()
.map(|job| {
let job_id = job.job.id;
let job_id = job.meta.id;
(
job,
AssignedJobById {
@ -79,7 +79,11 @@ impl NamedJobBatch {
let jobs: Vec<_> = named_jobs
.into_iter()
.filter_map(|(alias, cmd)| {
match RawJob::builder().with_shell(cmd).with_alias(alias).build() {
match RawJob::default()
.with_shell(cmd)
.with_alias(alias)
.try_into_job()
{
Ok(jpm) => Some(jpm),
Err(e) => {
result.push_err(e);
@ -95,7 +99,7 @@ impl NamedJobBatch {
pub fn from_meta(named_jobs: Vec<Job>) -> Self {
let (job_names, jobs): (Vec<_>, Vec<_>) = named_jobs
.into_iter()
.map(|job| (job.job.alias.clone().unwrap(), job))
.map(|job| (job.meta.alias.clone().unwrap(), job))
.unzip();
Self {
runner: Some(AnonymousJobBatch::from_meta(jobs)),
@ -129,17 +133,17 @@ impl NamedJobBatch<true> {
}
pub async fn run_assigned_job(job: Job, ids: AssignedJobById) -> ExecResult {
let Job { job, payload } = job;
let mut result = AssignedJob::from((&job, ids));
match job.exec_type {
let Job { meta, payload } = job;
let mut result = AssignedJob::from((&meta, ids));
match meta.exec_type {
JobType::Shell => {
let (argv, _prepared_payload) = {
if let Some(payload) = payload {
let (prep_exec, prep_exec_path) = payload.prepare_executable()?;
let argv_with_exec = job.argv.replace("{}", &prep_exec_path);
let argv_with_exec = meta.argv.replace("{}", &prep_exec_path);
(argv_with_exec, Some(prep_exec))
} else {
(job.argv, None)
(meta.argv, None)
}
};
@ -227,11 +231,11 @@ mod tests {
#[case] payload: Option<&[u8]>,
#[case] expected_result: &str,
) -> TestResult {
let mut job = RawJob::builder().with_shell(cmd);
let mut raw_job = RawJob::default().with_shell(cmd);
if let Some(p) = payload {
job = job.with_raw_payload(p);
raw_job = raw_job.with_raw_payload(p);
}
let job = job.build().unwrap();
let job = raw_job.try_into_job().unwrap();
let result = AnonymousJobBatch::from_meta([job])
.wait_one()
.await
@ -310,11 +314,11 @@ mod tests {
#[case] payload: Option<&[u8]>,
#[case] err_str: &str,
) -> TestResult {
let mut job = RawJob::builder().with_shell(cmd);
let mut raw_job = RawJob::default().with_shell(cmd);
if let Some(p) = payload {
job = job.with_raw_payload(p);
raw_job = raw_job.with_raw_payload(p);
}
let err = job.build().unwrap_err();
let err = raw_job.try_into_job().unwrap_err();
let err_msg = unwrap_enum!(err, UError::JobBuildError);
assert!(err_msg.contains(err_str));
Ok(())
@ -323,15 +327,15 @@ mod tests {
#[tokio::test]
async fn test_different_job_types() -> TestResult {
let mut jobs = NamedJobBatch::from_meta(vec![
RawJob::builder()
RawJob::default()
.with_shell("sleep 3")
.with_alias("sleeper")
.build()
.try_into_job()
.unwrap(),
RawJob::builder()
RawJob::default()
.with_type(JobType::Init)
.with_alias("gatherer")
.build()
.try_into_job()
.unwrap(),
])
.wait()

@ -10,7 +10,7 @@ pub trait AsMsg: Clone + Serialize + Debug {}
impl AsMsg for Agent {}
impl AsMsg for AssignedJob {}
impl AsMsg for AssignedJobById {}
impl AsMsg for JobModel {}
impl AsMsg for JobMeta {}
impl AsMsg for Reportable {}
impl AsMsg for Payload {}
impl AsMsg for RawPayload {}

@ -1,4 +1,4 @@
use super::{JobModel, JobState, JobType};
use super::{JobMeta, JobState, JobType};
#[cfg(feature = "server")]
use crate::models::schema::*;
use crate::{
@ -60,8 +60,8 @@ pub struct AssignedJobById {
pub job_id: Id,
}
impl From<(&JobModel, AssignedJobById)> for AssignedJob {
fn from((job, ids): (&JobModel, AssignedJobById)) -> Self {
impl From<(&JobMeta, AssignedJobById)> for AssignedJob {
fn from((job, ids): (&JobMeta, AssignedJobById)) -> Self {
let AssignedJobById {
agent_id,
id,

@ -12,34 +12,14 @@ use diesel::{Identifiable, Insertable, Queryable};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(
feature = "server",
derive(Queryable, Identifiable, Insertable, AsChangeset),
diesel(table_name = jobs)
diesel(table_name = jobs),
diesel(treat_none_as_null = true)
)]
pub struct JobModel {
pub alias: Option<String>,
/// string like `bash -c {} -a 1 --arg2`,
/// where {} is replaced by executable's tmp path
pub argv: String,
pub id: Id,
pub exec_type: JobType,
/// target triple
pub target_platforms: String,
pub payload_id: Option<Id>,
/// cron-like string
pub schedule: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Job {
pub job: JobModel,
pub payload: Option<Payload>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct RawJob<'p> {
pub struct JobMeta {
#[serde(default)]
pub alias: Option<String>,
@ -59,17 +39,14 @@ pub struct RawJob<'p> {
pub target_platforms: String,
#[serde(default)]
pub payload_path: Option<String>,
#[serde(default)]
pub raw_payload: Option<Cow<'p, [u8]>>,
pub payload_id: Option<Id>,
/// cron-like string
#[serde(default)]
pub schedule: Option<String>,
}
impl Default for RawJob<'_> {
impl Default for JobMeta {
fn default() -> Self {
Self {
alias: None,
@ -77,113 +54,167 @@ impl Default for RawJob<'_> {
id: Id::new_v4(),
exec_type: JobType::default(),
target_platforms: String::new(),
payload_path: None,
raw_payload: None,
payload_id: None,
schedule: None,
}
}
}
impl fmt::Debug for RawJob<'_> {
impl fmt::Debug for JobMeta {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RawJob")
f.debug_struct("JobMeta")
.field("alias", &self.alias)
.field("argv", &self.argv)
.field("id", &self.id.to_string())
.field("exec_type", &self.exec_type)
.field("platform", &self.target_platforms)
.field("payload_path", &self.payload_path)
.field("raw_payload", &self.raw_payload)
.field("target_platforms", &self.target_platforms)
.field("payload_id", &self.payload_id.map(|id| id.to_string()))
.field("schedule", &self.schedule)
.finish()
}
}
impl JobMeta {
pub fn validate(mut self) -> UResult<Self> {
fn mk_err(msg: impl Into<String>) -> UError {
UError::JobBuildError(msg.into())
}
const ARGV_STR_LEN: usize = 2048;
if self.argv.is_empty() {
// TODO: fix detecting
self.argv = String::from("echo 'hello, world!'")
} else if self.argv.len() > ARGV_STR_LEN {
return Err(mk_err(format!(
"argv length limit ({ARGV_STR_LEN}) exceeded"
)));
}
let argv_parts = shlex::split(&self.argv).ok_or_else(|| mk_err("Shlex failed"))?;
let empty_err = mk_err("Empty argv");
if argv_parts.get(0).ok_or(empty_err.clone())?.is_empty() {
return Err(empty_err);
}
if self.payload_id.is_some() && !self.argv.contains("{}") {
return Err(mk_err("Argv contains no executable placeholder"));
}
if self.argv.contains("{}") && self.payload_id.is_none() {
return Err(mk_err(
"No payload provided, but argv contains executable placeholder",
));
}
if self.target_platforms.is_empty() {
self.target_platforms = "*".to_string();
}
if !platform::is_valid_glob(&self.target_platforms) {
return Err(mk_err(format!(
"Unknown platform '{}'",
self.target_platforms
)));
}
Ok(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Job {
pub meta: JobMeta,
pub payload: Option<Payload>,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct RawJob<'p> {
#[serde(default)]
payload_path: Option<String>,
#[serde(default)]
raw_payload: Option<Cow<'p, [u8]>>,
#[serde(default, flatten)]
meta: JobMeta,
}
impl From<Job> for RawJob<'_> {
fn from(job: Job) -> Self {
let Job {
job,
payload: payload_meta,
} = job;
let Job { meta, payload } = job;
RawJob {
alias: job.alias,
argv: job.argv,
id: job.id,
exec_type: job.exec_type,
target_platforms: job.target_platforms,
payload_path: payload_meta.map(|m| m.name),
payload_path: payload.map(|m| m.name),
raw_payload: None,
schedule: job.schedule,
meta,
}
}
}
impl<'p> RawJob<'p> {
pub fn validated(self) -> UResult<Job> {
JobBuilder { inner: self }.build()
pub fn try_into_job(self) -> UResult<Job> {
Job::try_from(self)
}
pub fn from_shell(cmd: impl Into<String>) -> UResult<Job> {
Self::builder().with_shell(cmd).build()
Self::default().with_shell(cmd).try_into_job()
}
pub fn builder() -> JobBuilder<'p> {
JobBuilder::default()
}
}
#[derive(Default)]
pub struct JobBuilder<'p> {
inner: RawJob<'p>,
}
impl<'p> JobBuilder<'p> {
pub fn with_shell(mut self, shell_cmd: impl Into<String>) -> Self {
self.inner.argv = shell_cmd.into();
self.inner.exec_type = JobType::Shell;
self.meta.argv = shell_cmd.into();
self.meta.exec_type = JobType::Shell;
self
}
pub fn with_raw_payload(mut self, raw_payload: impl AsPayload<'p>) -> Self {
self.inner.raw_payload = Some(raw_payload.as_payload());
self.inner.payload_path = None;
pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
self.meta.alias = Some(alias.into());
self
}
pub fn with_payload_path(mut self, path: impl Into<String>) -> Self {
self.inner.payload_path = Some(path.into());
self.inner.raw_payload = None;
pub fn with_type(mut self, e_type: JobType) -> Self {
self.meta.exec_type = e_type;
self
}
pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
self.inner.alias = Some(alias.into());
pub fn with_target_platforms(mut self, platform: impl Into<String>) -> Self {
self.meta.target_platforms = platform.into();
self
}
pub fn with_type(mut self, e_type: JobType) -> Self {
self.inner.exec_type = e_type;
pub fn with_raw_payload(mut self, raw_payload: impl AsPayload<'p>) -> Self {
self.raw_payload = Some(raw_payload.as_payload());
self.payload_path = None;
self
}
pub fn with_target_platforms(mut self, platform: impl Into<String>) -> Self {
self.inner.target_platforms = platform.into();
pub fn with_payload_path(mut self, path: impl Into<String>) -> Self {
self.payload_path = Some(path.into());
self.raw_payload = None;
self
}
}
pub fn build(self) -> UResult<Job> {
let mut inner = self.inner;
impl TryFrom<RawJob<'_>> for Job {
type Error = UError;
fn try_from(mut raw: RawJob) -> Result<Self, Self::Error> {
if raw.raw_payload.is_some() && raw.payload_path.is_some() {
return Err(UError::JobBuildError(
"Can't use both raw payload with payload path".to_string(),
));
}
fn _build(job: RawJob) -> UResult<Job> {
let payload = {
let payload_from_path = job
let payload_from_path = raw
.payload_path
.as_ref()
.map(|path| Payload::from_path(path))
.transpose()?;
if payload_from_path.is_none() {
job.raw_payload
raw.raw_payload
.as_ref()
.map(|data| Payload::from_data(data, None))
.transpose()?
@ -192,84 +223,13 @@ impl<'p> JobBuilder<'p> {
}
};
raw.meta.payload_id = payload.as_ref().map(|p| p.id);
Ok(Job {
job: JobModel {
alias: job.alias,
argv: job.argv,
id: job.id,
exec_type: job.exec_type,
target_platforms: job.target_platforms,
payload_id: payload.as_ref().map(|p| p.id),
schedule: job.schedule,
},
meta: raw.meta.validate()?,
payload,
})
}
match inner.exec_type {
JobType::Shell => {
const ARGV_STR_LEN: usize = 2048;
if inner.argv.is_empty() {
// TODO: fix detecting
inner.argv = String::from("echo 'hello, world!'")
} else if inner.argv.len() > ARGV_STR_LEN {
return Err(UError::JobBuildError(format!(
"argv length limit ({ARGV_STR_LEN}) exceeded"
)));
}
let argv_parts = shlex::split(&inner.argv)
.ok_or(UError::JobBuildError("Shlex failed".into()))?;
let empty_err = UError::JobBuildError("Empty argv".into());
if argv_parts.get(0).ok_or(empty_err.clone())?.is_empty() {
return Err(empty_err.into());
}
if inner.raw_payload.is_some() && inner.payload_path.is_some() {
return Err(UError::JobBuildError(
"Can't use both raw payload with payload path".to_string(),
));
}
match inner.payload_path.as_ref() {
Some(_) | None if inner.raw_payload.is_some() => {
if !inner.argv.contains("{}") {
return Err(UError::JobBuildError(
"Argv contains no executable placeholder".into(),
)
.into());
}
}
None => {
if inner.argv.contains("{}") && inner.raw_payload.is_none() {
return Err(UError::JobBuildError(
"No payload provided, but argv contains executable placeholder"
.into(),
)
.into());
}
}
_ => (),
};
if inner.target_platforms.is_empty() {
inner.target_platforms = "*".to_string();
}
if !platform::is_valid_glob(&inner.target_platforms) {
return Err(UError::JobBuildError(format!(
"Unknown platform '{}'",
inner.target_platforms
)));
}
_build(inner)
}
_ => _build(inner),
}
}
}
pub trait AsPayload<'p> {

@ -26,7 +26,8 @@ pub enum JobState {
Finished,
}
#[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Display)]
#[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(
feature = "server",
derive(DbEnum),

@ -12,13 +12,13 @@ use diesel::Identifiable;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawPayload {
pub name: String,
pub name: Option<String>,
pub data: Vec<u8>,
}
impl RawPayload {
pub fn into_payload(self) -> Result<Payload, UError> {
Payload::from_data(self.data, Some(&self.name))
Payload::from_data(self.data, self.name)
}
}
@ -71,11 +71,11 @@ impl Payload {
Ok(())
}
pub fn from_data(data: impl AsRef<[u8]>, name: Option<&str>) -> Result<Payload, UError> {
pub fn from_data(data: impl AsRef<[u8]>, name: Option<String>) -> Result<Payload, UError> {
let name = match name {
Some(name) => {
ufs::put(name, data).context("fr_put")?;
name.to_string()
ufs::put(&name, data).context("fr_put")?;
name
}
None => ufs::create_anonymous(data).context("fr_anon")?,
};

Loading…
Cancel
Save