Compare commits

..

1 Commits

  1. 15
      .cargo/config.toml
  2. 8
      .env
  3. 3
      .env.private.sample
  4. 10
      .gitignore
  5. 3322
      Cargo.lock
  6. 22
      Cargo.toml
  7. 26
      Makefile
  8. 96
      Makefile.toml
  9. 13
      bin/migrator/Cargo.toml
  10. 140
      bin/migrator/src/database.rs
  11. 88
      bin/migrator/src/main.rs
  12. 82
      bin/migrator/src/query_helper.rs
  13. 16
      bin/u_agent/Cargo.toml
  14. 2
      bin/u_agent/build.rs
  15. 215
      bin/u_agent/src/lib.rs
  16. 8
      bin/u_agent/src/main.rs
  17. 29
      bin/u_panel/Cargo.toml
  18. 176
      bin/u_panel/src/argparse.rs
  19. 17
      bin/u_panel/src/gui/error.rs
  20. 16
      bin/u_panel/src/gui/fe/.browserslistrc
  21. 16
      bin/u_panel/src/gui/fe/.editorconfig
  22. 48
      bin/u_panel/src/gui/fe/.gitignore
  23. 27
      bin/u_panel/src/gui/fe/README.md
  24. 114
      bin/u_panel/src/gui/fe/angular.json
  25. 44
      bin/u_panel/src/gui/fe/karma.conf.js
  26. 43
      bin/u_panel/src/gui/fe/package.json
  27. 18
      bin/u_panel/src/gui/fe/src/app/app-routing.module.ts
  28. 6
      bin/u_panel/src/gui/fe/src/app/app.component.html
  29. 0
      bin/u_panel/src/gui/fe/src/app/app.component.less
  30. 35
      bin/u_panel/src/gui/fe/src/app/app.component.spec.ts
  31. 15
      bin/u_panel/src/gui/fe/src/app/app.component.ts
  32. 73
      bin/u_panel/src/gui/fe/src/app/app.module.ts
  33. 64
      bin/u_panel/src/gui/fe/src/app/components/dialogs/agent-info-dialog/agent-info-dialog.component.html
  34. 21
      bin/u_panel/src/gui/fe/src/app/components/dialogs/agent-info-dialog/agent-info-dialog.component.ts
  35. 12
      bin/u_panel/src/gui/fe/src/app/components/dialogs/assign-job-dialog/assign-job-dialog.component.html
  36. 36
      bin/u_panel/src/gui/fe/src/app/components/dialogs/assign-job-dialog/assign-job-dialog.component.ts
  37. 18
      bin/u_panel/src/gui/fe/src/app/components/dialogs/base-info-dialog.component.less
  38. 5
      bin/u_panel/src/gui/fe/src/app/components/dialogs/index.ts
  39. 48
      bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.html
  40. 28
      bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.ts
  41. 20
      bin/u_panel/src/gui/fe/src/app/components/dialogs/new-payload-dialog/new-payload-dialog.component.html
  42. 43
      bin/u_panel/src/gui/fe/src/app/components/dialogs/new-payload-dialog/new-payload-dialog.component.ts
  43. 34
      bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.html
  44. 20
      bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.ts
  45. 47
      bin/u_panel/src/gui/fe/src/app/components/dialogs/result-info-dialog/result-info-dialog.component.html
  46. 20
      bin/u_panel/src/gui/fe/src/app/components/dialogs/result-info-dialog/result-info-dialog.component.ts
  47. 0
      bin/u_panel/src/gui/fe/src/app/components/global-error/global-error.component.html
  48. 0
      bin/u_panel/src/gui/fe/src/app/components/global-error/global-error.component.less
  49. 34
      bin/u_panel/src/gui/fe/src/app/components/global-error/global-error.component.ts
  50. 7
      bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.html
  51. 0
      bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.less
  52. 21
      bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.ts
  53. 78
      bin/u_panel/src/gui/fe/src/app/components/tables/agent-table/agent-table.component.html
  54. 42
      bin/u_panel/src/gui/fe/src/app/components/tables/agent-table/agent-table.component.ts
  55. 32
      bin/u_panel/src/gui/fe/src/app/components/tables/base-table/base-table.component.less
  56. 59
      bin/u_panel/src/gui/fe/src/app/components/tables/base-table/base-table.component.ts
  57. 5
      bin/u_panel/src/gui/fe/src/app/components/tables/index.ts
  58. 83
      bin/u_panel/src/gui/fe/src/app/components/tables/job-table/job-table.component.html
  59. 72
      bin/u_panel/src/gui/fe/src/app/components/tables/job-table/job-table.component.ts
  60. 63
      bin/u_panel/src/gui/fe/src/app/components/tables/payload-table/payload-table.component.html
  61. 67
      bin/u_panel/src/gui/fe/src/app/components/tables/payload-table/payload-table.component.ts
  62. 81
      bin/u_panel/src/gui/fe/src/app/components/tables/result-table/result-table.component.html
  63. 36
      bin/u_panel/src/gui/fe/src/app/components/tables/result-table/result-table.component.ts
  64. 1
      bin/u_panel/src/gui/fe/src/app/index.ts
  65. 16
      bin/u_panel/src/gui/fe/src/app/models/agent.model.ts
  66. 20
      bin/u_panel/src/gui/fe/src/app/models/index.ts
  67. 16
      bin/u_panel/src/gui/fe/src/app/models/job.model.ts
  68. 12
      bin/u_panel/src/gui/fe/src/app/models/payload.model.ts
  69. 18
      bin/u_panel/src/gui/fe/src/app/models/result.model.ts
  70. 142
      bin/u_panel/src/gui/fe/src/app/services/api.service.ts
  71. 17
      bin/u_panel/src/gui/fe/src/app/services/error.service.ts
  72. 1
      bin/u_panel/src/gui/fe/src/app/services/index.ts
  73. 3
      bin/u_panel/src/gui/fe/src/app/utils.ts
  74. 4
      bin/u_panel/src/gui/fe/src/environments/environment.prod.ts
  75. 17
      bin/u_panel/src/gui/fe/src/environments/environment.ts
  76. BIN
      bin/u_panel/src/gui/fe/src/favicon.ico
  77. 18
      bin/u_panel/src/gui/fe/src/index.html
  78. 12
      bin/u_panel/src/gui/fe/src/main.ts
  79. 53
      bin/u_panel/src/gui/fe/src/polyfills.ts
  80. 4
      bin/u_panel/src/gui/fe/src/styles.less
  81. 26
      bin/u_panel/src/gui/fe/src/test.ts
  82. 15
      bin/u_panel/src/gui/fe/tsconfig.app.json
  83. 32
      bin/u_panel/src/gui/fe/tsconfig.json
  84. 18
      bin/u_panel/src/gui/fe/tsconfig.spec.json
  85. 96
      bin/u_panel/src/gui/mod.rs
  86. 156
      bin/u_panel/src/main.rs
  87. 2
      bin/u_run/Cargo.toml
  88. 45
      bin/u_server/Cargo.toml
  89. 487
      bin/u_server/src/db.rs
  90. 98
      bin/u_server/src/error.rs
  91. 103
      bin/u_server/src/filters.rs
  92. 476
      bin/u_server/src/handlers.rs
  93. 15
      bin/u_server/src/main.rs
  94. 65
      bin/u_server/src/storages/files.rs
  95. 78
      bin/u_server/src/storages/mapper.rs
  96. 5
      bin/u_server/src/storages/mod.rs
  97. 350
      bin/u_server/src/u_server.rs
  98. 3
      bin/u_server/src/worker.rs
  99. 12
      certs/gen_certs.sh
  100. 52
      deploy/podman-compose.yml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,15 +0,0 @@
[build]
rustflags = [
"-L/usr/lib/musl/lib",
"-L/home/ortem/src/rust/unki/static/lib",
"--remap-path-prefix=/home/ortem/src/rust/unki=src",
"--remap-path-prefix=/home/ortem/.cargo=cargo"
]
target = "x86_64-unknown-linux-musl"
[env]
STATIC_PREFIX = "static"
PQ_LIB_STATIC_X86_64_UNKNOWN_LINUX_MUSL = "true"
PG_CONFIG_X86_64_UNKNOWN_LINUX_GNU = { value = "static/bin/pg_config", relative = true }
OPENSSL_STATIC = "true"
OPENSSL_DIR = { value = "static", relative = true }

@ -1,6 +1,4 @@
POSTGRES_HOST=u_db DB_HOST=u_db
POSTGRES_DATABASE=u_db DB_NAME=u_db
POSTGRES_USER=u_ser DB_USER=postgres
POSTGRES_PORT=5432
RUST_BACKTRACE=1 RUST_BACKTRACE=1
U_SERVER=u_server

@ -1,3 +1,4 @@
# remove '.sample' to activate # remove '.sample' to activate
ADMIN_AUTH_TOKEN= ADMIN_AUTH_TOKEN=
POSTGRES_PASSWORD= DB_PASSWORD=
POSTGRES_PASSWORD=${DB_PASSWORD}

10
.gitignore vendored

@ -1,14 +1,10 @@
target/ target/
**/*.rs.bk
.idea/ .idea/
data/ data/
certs/
static/
.vscode/
release/
**/node_modules/
**/*.rs.bk
**/*.pyc **/*.pyc
certs/*
*.log *.log
echoer echoer
.env.private .env.private
*.lock

3322
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,34 +1,16 @@
[workspace] [workspace]
members = [ members = [
"bin/migrator",
"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-tests", "lib/u_api_proc_macro",
"integration"
] ]
resolver = "2"
[workspace.dependencies]
anyhow = "=1.0.63"
deadpool-diesel = "0.4.0"
diesel = { version = "2", features = ["postgres", "uuid"] }
mime_guess = "2.0"
openssl = "0.10"
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "=1.0.31"
tokio = { version = "1.11", features = ["macros"] }
tracing = "0.1.35"
tracing-appender = "0.2.0"
tracing-subscriber = { version = "0.3.0", features = ["env-filter"]}
uuid = "1.2.1"
[profile.release] [profile.release]
panic = "abort" panic = "abort"
strip = "symbols"
[profile.dev] [profile.dev]
debug = true # Добавляет флаг `-g` для компилятора; debug = true # Добавляет флаг `-g` для компилятора;

@ -0,0 +1,26 @@
.PHONY: _pre_build debug release run clean unit integration test
CARGO=./scripts/cargo_musl.sh
clean:
${CARGO} clean
_pre_build:
docker build -t unki/musllibs ./muslrust
debug: _pre_build
${CARGO} build
release: _pre_build
${CARGO} build --release
run: build
${CARGO} run
unit:
${CARGO} test --lib
integration:
cd ./integration && ./integration_tests.sh
test: unit integration

@ -1,96 +0,0 @@
# i need to preserve --release in args, not to pass cargo make -p release
# due to cargo failing to parse "" argument
env_scripts = ['''
#!@duckscript
args = array ${1} ${2} ${3} ${4} ${5} ${6} ${7}
set_env PROFILE_OVERRIDE debug
for arg in ${args}
e = eq ${arg} "--release"
if ${e}
set_env PROFILE_OVERRIDE release
end
end
profile = get_env PROFILE_OVERRIDE
echo PROFILE_OVERRIDE=${profile}
''']
[config]
default_to_workspace = false
[env]
TARGET = "x86_64-unknown-linux-musl"
CARGO = "cargo"
[tasks.build_static_libs]
script = "./scripts/build_musl_libs.sh"
[tasks.build_frontend]
script = '''
cd ./bin/u_panel/src/gui/fe
ng build
'''
[tasks.clean]
command = "${CARGO}"
args = ["clean"]
[tasks.cargo_build]
dependencies = ["build_static_libs", "build_frontend"]
command = "${CARGO}"
args = ["build", "--target", "${TARGET}", "${@}"]
[tasks.cargo_update]
command = "${CARGO}"
args = ["update"]
[tasks.release_tasks]
condition = { env = { PROFILE_OVERRIDE = "release"} }
script = '''
BINS=$(ls ./target/${TARGET}/${PROFILE_OVERRIDE}/u_* -1 | grep -v ".d")
echo "Stripping..."
strip $BINS
echo "Packing..."
upx -9 $BINS
'''
[tasks.build]
dependencies = ["cargo_build", "release_tasks"]
clear = true
[tasks.run]
disabled = true
[tasks.run_front]
script = '''
cd ./bin/u_panel/src/gui/fe
ng serve
'''
[tasks.unit-tests]
command = "${CARGO}"
args = ["test", "--target", "${TARGET}", "--lib", "--", "${@}"]
[tasks.ut]
alias = "unit-tests"
[tasks.integration-tests]
dependencies = ["cargo_update"]
script = '''
[[ ! -d "./target/${TARGET}/${PROFILE_OVERRIDE}" ]] && echo 'No target folder. Build project first' && exit 1
cd ./integration-tests
bash integration_tests.sh ${@}
'''
[tasks.it]
alias = "integration-tests"
[tasks.test]
dependencies = ["unit", "integration-tests"]
[tasks.gen_schema]
script = './scripts/gen_schema.sh'
[tasks.deploy]
script = './scripts/deploy.sh'

@ -1,13 +0,0 @@
[package]
name = "migrator"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
diesel = { workspace = true, features = ["postgres", "serde_json"] }
diesel_migrations = { version = "2.0.0", features = ["postgres"] }
openssl = { workspace = true }
u_lib = { path = "../../lib/u_lib" }
url = "2.3.1"

@ -1,140 +0,0 @@
use super::query_helper;
use diesel::dsl::sql;
use diesel::sql_types::Bool;
use diesel::*;
use std::env;
use std::error::Error;
type DatabaseResult<T> = Result<T, Box<dyn Error>>;
pub enum Backend {
Pg,
}
impl Backend {
pub fn for_url(database_url: &str) -> Self {
match database_url {
_ if database_url.starts_with("postgres://")
|| database_url.starts_with("postgresql://") =>
{
Backend::Pg
}
_ => panic!(
"At least one backend must be specified for use with this crate. \
You may omit the unneeded dependencies in the following command. \n\n \
ex. `cargo install diesel_cli --no-default-features --features mysql postgres sqlite` \n"
),
}
}
}
pub enum InferConnection {
Pg(PgConnection),
}
impl InferConnection {
pub fn establish(database_url: &str) -> DatabaseResult<Self> {
match Backend::for_url(database_url) {
Backend::Pg => PgConnection::establish(database_url).map(InferConnection::Pg),
}
.map_err(Into::into)
}
}
pub fn reset_database() -> DatabaseResult<()> {
drop_database(&database_url())?;
setup_database()
}
pub fn setup_database() -> DatabaseResult<()> {
let database_url = database_url();
create_database_if_needed(&database_url)?;
Ok(())
}
pub fn drop_database_command() -> DatabaseResult<()> {
drop_database(&database_url())
}
/// Creates the database specified in the connection url. It returns an error
/// it was unable to create the database.
fn create_database_if_needed(database_url: &str) -> DatabaseResult<()> {
match Backend::for_url(database_url) {
Backend::Pg => {
if PgConnection::establish(database_url).is_err() {
let (database, postgres_url) = change_database_of_url(database_url, "postgres");
println!("Creating database: {}", database);
let mut conn = PgConnection::establish(&postgres_url)?;
query_helper::create_database(&database).execute(&mut conn)?;
}
}
}
Ok(())
}
/// Drops the database specified in the connection url. It returns an error
/// if it was unable to drop the database.
fn drop_database(database_url: &str) -> DatabaseResult<()> {
match Backend::for_url(database_url) {
Backend::Pg => {
let (database, postgres_url) = change_database_of_url(database_url, "postgres");
let mut conn = PgConnection::establish(&postgres_url)?;
if pg_database_exists(&mut conn, &database)? {
println!("Dropping database: {}", database);
query_helper::drop_database(&database)
.if_exists()
.execute(&mut conn)?;
}
}
}
Ok(())
}
table! {
pg_database (datname) {
datname -> Text,
datistemplate -> Bool,
}
}
fn pg_database_exists(conn: &mut PgConnection, database_name: &str) -> QueryResult<bool> {
use self::pg_database::dsl::*;
pg_database
.select(datname)
.filter(datname.eq(database_name))
.filter(datistemplate.eq(false))
.get_result::<String>(conn)
.optional()
.map(|x| x.is_some())
}
/// Returns true if the `__diesel_schema_migrations` table exists in the
/// database we connect to, returns false if it does not.
pub fn schema_table_exists(database_url: &str) -> DatabaseResult<bool> {
match InferConnection::establish(database_url).unwrap() {
InferConnection::Pg(mut conn) => select(sql::<Bool>(
"EXISTS \
(SELECT 1 \
FROM information_schema.tables \
WHERE table_name = '__diesel_schema_migrations')",
))
.get_result(&mut conn),
}
.map_err(Into::into)
}
pub fn database_url() -> String {
env::var("DATABASE_URL").unwrap()
}
fn change_database_of_url(database_url: &str, default_database: &str) -> (String, String) {
let base = ::url::Url::parse(database_url).unwrap();
let database = base.path_segments().unwrap().last().unwrap().to_owned();
let mut new_url = base.join(default_database).unwrap();
new_url.set_query(base.query());
(database, new_url.into())
}

@ -1,88 +0,0 @@
// due to linking errors
extern crate openssl;
// don't touch anything
extern crate diesel;
// in this block
pub mod database;
pub mod query_helper;
use diesel::migration::Migration;
use diesel::{migration, pg::PgConnection, Connection};
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use std::error::Error;
use u_lib::config::DBEnv;
use u_lib::db::generate_postgres_url;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let action = action::parse_command_line()?;
let dbconfig = DBEnv::load()?;
database::setup_database().unwrap();
let conn = PgConnection::establish(&generate_postgres_url(&dbconfig))?;
run(action, conn)
}
fn run(action: action::Action, mut conn: PgConnection) -> migration::Result<()> {
use action::Action::*;
match action {
ListPending => {
let list = conn.pending_migrations(MIGRATIONS)?;
if list.is_empty() {
println!("No pending migrations.");
}
for mig in list {
println!("Pending migration: {}", mig.name());
}
}
MigrateUp => {
let list = conn.run_pending_migrations(MIGRATIONS)?;
if list.is_empty() {
println!("No pending migrations.");
}
for mig in list {
println!("Applied migration: {}", mig);
}
}
MigrateDown => {
let mig = conn.revert_last_migration(MIGRATIONS)?;
println!("Reverted migration: {}", mig);
}
}
Ok(())
}
mod action {
pub enum Action {
ListPending,
MigrateUp,
MigrateDown,
}
impl TryFrom<&str> for Action {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"" | "list" => Ok(Action::ListPending),
"up" => Ok(Action::MigrateUp),
"down" => Ok(Action::MigrateDown),
_ => Err(()),
}
}
}
pub fn parse_command_line() -> Result<Action, String> {
let action_str = std::env::args().nth(1).unwrap_or_default();
let action = action_str.as_str().try_into().map_err(|_| {
format!(
"unrecognized command line argument: {} (expected 'up', 'down', 'list')",
action_str
)
})?;
Ok(action)
}
}

@ -1,82 +0,0 @@
use diesel::backend::Backend;
use diesel::query_builder::*;
use diesel::result::QueryResult;
use diesel::RunQueryDsl;
#[derive(Debug, Clone)]
pub struct DropDatabaseStatement {
db_name: String,
if_exists: bool,
}
impl DropDatabaseStatement {
pub fn new(db_name: &str) -> Self {
DropDatabaseStatement {
db_name: db_name.to_owned(),
if_exists: false,
}
}
pub fn if_exists(self) -> Self {
DropDatabaseStatement {
if_exists: true,
..self
}
}
}
impl<DB: Backend> QueryFragment<DB> for DropDatabaseStatement {
fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, DB>) -> QueryResult<()> {
out.push_sql("DROP DATABASE ");
if self.if_exists {
out.push_sql("IF EXISTS ");
}
out.push_identifier(&self.db_name)?;
Ok(())
}
}
impl<Conn> RunQueryDsl<Conn> for DropDatabaseStatement {}
impl QueryId for DropDatabaseStatement {
type QueryId = ();
const HAS_STATIC_QUERY_ID: bool = false;
}
#[derive(Debug, Clone)]
pub struct CreateDatabaseStatement {
db_name: String,
}
impl CreateDatabaseStatement {
pub fn new(db_name: &str) -> Self {
CreateDatabaseStatement {
db_name: db_name.to_owned(),
}
}
}
impl<DB: Backend> QueryFragment<DB> for CreateDatabaseStatement {
fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, DB>) -> QueryResult<()> {
out.push_sql("CREATE DATABASE ");
out.push_identifier(&self.db_name)?;
Ok(())
}
}
impl<Conn> RunQueryDsl<Conn> for CreateDatabaseStatement {}
impl QueryId for CreateDatabaseStatement {
type QueryId = ();
const HAS_STATIC_QUERY_ID: bool = false;
}
pub fn drop_database(db_name: &str) -> DropDatabaseStatement {
DropDatabaseStatement::new(db_name)
}
pub fn create_database(db_name: &str) -> CreateDatabaseStatement {
CreateDatabaseStatement::new(db_name)
}

@ -2,15 +2,19 @@
name = "u_agent" name = "u_agent"
version = "0.1.0" version = "0.1.0"
authors = ["plazmoid <kronos44@mail.ru>"] authors = ["plazmoid <kronos44@mail.ru>"]
edition = "2021" edition = "2018"
# 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]
log = { version = "0.4", features = ["release_max_level_off"] } tokio = { version = "1.2.0", features = ["macros", "rt-multi-thread", "process", "time"] }
reqwest = { workspace = true }
sysinfo = "0.10.5" sysinfo = "0.10.5"
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process", "time"] } log = "^0.4"
uuid = { workspace = true } env_logger = "0.8.3"
u_lib = { path = "../../lib/u_lib", features = ["agent"] } uuid = "0.6.5"
reqwest = { version = "0.11", features = ["json"] }
openssl = "*"
u_lib = { version = "*", path = "../../lib/u_lib" }
[build-dependencies]
openssl = "*"

@ -3,6 +3,6 @@ use std::path::PathBuf;
fn main() { fn main() {
let server_cert = PathBuf::from("../../certs/ca.crt"); let server_cert = PathBuf::from("../../certs/ca.crt");
if !server_cert.exists() { if !server_cert.exists() {
panic!("CA certificate doesn't exist. Create it first with scripts/gen_certs.sh"); panic!("CA certificate doesn't exist. Create it first with certs/gen_certs.sh");
} }
} }

@ -1,167 +1,86 @@
// TODO:
// поддержка питона
// резолв адреса управляющего сервера через DoT
// кроссплатформенность (реализовать интерфейс для винды и никсов)
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate env_logger;
use tokio::runtime::Builder; use std::env;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
use u_lib::models::PreparedJob;
use u_lib::scheduler::SCHEDULER;
use u_lib::u_runner::{IdentifiableFuture, URunner};
use u_lib::{ use u_lib::{
api::HttpClient, api::ClientHandler,
builder::JobBuilder,
cache::JobCache, cache::JobCache,
config::{get_self_id, EndpointsEnv, AGENT_ITERATION_INTERVAL}, executor::pop_completed,
error::ErrChan, models::{AssignedJob, ExecResult},
logging::init_logger, UID,
messaging::Reportable, //daemonize
models::AssignedJobById,
}; };
async fn process_request(assigned_jobs: Vec<AssignedJobById>, client: &HttpClient) { #[macro_export]
for asgn_job in assigned_jobs { macro_rules! retry_until_ok {
if !JobCache::contains(asgn_job.job_id) { ( $body:expr ) => {
info!("Fetching job: {}", &asgn_job.job_id); loop {
let mut fetched_job = loop { match $body {
//todo: use payload cache Ok(r) => break r,
match client.get_full_job(asgn_job.job_id).await { Err(e) => error!("{:?}", e),
Ok(result) => break result,
Err(err) => {
debug!("{:?} \nretrying...", err);
sleep(AGENT_ITERATION_INTERVAL).await;
}
}
}; };
if let Some(payload) = &mut fetched_job.payload { sleep(Duration::from_secs(5)).await;
if let Err(e) = payload.maybe_split_payload() {
ErrChan::send(e, "pay").await;
continue;
}
}
JobCache::insert(fetched_job);
} }
};
let job = match JobCache::get(asgn_job.job_id).as_deref() {
Some(job) => job.clone(),
None => continue,
};
info!("Scheduling job {}", job.meta.id.to_string());
let schedule = match job.meta.schedule.clone() {
Some(sched) => {
if sched.is_empty() {
None
} else {
match sched.as_str().try_into() {
Ok(s) => Some(s),
Err(err) => {
ErrChan::send(err, "sch").await;
continue;
}
}
}
}
None => None,
};
SCHEDULER
.add_job(schedule, PreparedJob { job, ids: asgn_job })
.await;
}
} }
async fn error_reporting(client: HttpClient) { pub async fn process_request(job_requests: Vec<AssignedJob>, client: &ClientHandler) {
while let Some(err) = ErrChan::recv().await { if job_requests.len() > 0 {
let _ = client.report([Reportable::Error(err.clone())]).await; for jr in &job_requests {
} if !JobCache::contains(&jr.job_id) {
} info!("Fetching job: {}", &jr.job_id);
let fetched_job = retry_until_ok!(client.get_jobs(Some(jr.job_id)).await)
async fn agent_loop(client: HttpClient) { .pop()
let self_id = get_self_id(); .unwrap();
JobCache::insert(fetched_job);
match client.get_personal_jobs(self_id).await { }
Ok(jobs) => {
process_request(jobs, &client).await;
} }
Err(err) => ErrChan::send(err, "pro").await, info!(
} "Scheduling jobs: {}",
job_requests
let result: Vec<Reportable> = URunner::pop_completed() .iter()
.await .map(|j| j.job_id.to_string())
.into_iter() .collect::<Vec<String>>()
.map(|result| match result { .join(", ")
Ok(r) => Reportable::Assigned(r), );
Err(e) => Reportable::Error(e), let mut builder = JobBuilder::from_request(job_requests);
}) let errors = builder.pop_errors();
.collect(); if errors.len() > 0 {
error!(
if !result.is_empty() { "Some errors encountered: \n{}",
if let Err(err) = client.report(result).await { errors
ErrChan::send(err, "rep").await; .iter()
.map(|j| j.to_string())
.collect::<Vec<String>>()
.join("\n")
);
} }
builder.unwrap_one().spawn().await;
} }
} }
pub fn run_forever() -> ! { pub async fn run_forever() {
let env = EndpointsEnv::load(); //daemonize();
env_logger::init();
if cfg!(debug_assertions) { let arg_ip = env::args().nth(1);
let logfile_uid = format!( let instance = ClientHandler::new(arg_ip.as_deref());
"u_agent-{}", info!("Connecting to the server");
get_self_id() loop {
.hyphenated() let job_requests: Vec<AssignedJob> =
.to_string() retry_until_ok!(instance.get_personal_jobs(Some(*UID)).await).into_builtin_vec();
.split("-") process_request(job_requests, &instance).await;
.next() let result: Vec<ExecResult> = pop_completed().await.into_iter().collect();
.unwrap() if result.len() > 0 {
); retry_until_ok!(instance.report(&result).await);
init_logger(Some(&logfile_uid)); }
} else { sleep(Duration::from_secs(5)).await;
#[cfg(unix)]
u_lib::unix::daemonize()
} }
info!("Starting agent {}", get_self_id());
Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let client = loop {
match HttpClient::new(&env.u_server, None).await {
Ok(client) => break client,
Err(e) => {
error!("client init failed: {}", e);
sleep(Duration::from_secs(5)).await;
continue;
}
}
};
{
let client = client.clone();
SCHEDULER
.add_job(Some("*/3 * * * * * *".try_into().unwrap()), move || {
let client = client.clone();
IdentifiableFuture::from_fut_with_ident("error_reporting", async move {
error_reporting(client.clone()).await
})
})
.await;
}
{
let client = client.clone();
SCHEDULER
.add_job(Some("*/3 * * * * * *".try_into().unwrap()), move || {
let client = client.clone();
IdentifiableFuture::from_fut_with_ident("agent_loop", async move {
agent_loop(client).await
})
})
.await;
}
SCHEDULER.start_blocking().await
})
} }

@ -1,3 +1,7 @@
fn main() { use tokio;
u_agent::run_forever(); use u_agent::run_forever;
#[tokio::main]
async fn main() {
run_forever().await;
} }

@ -2,27 +2,18 @@
name = "u_panel" name = "u_panel"
version = "0.1.0" version = "0.1.0"
authors = ["plazmoid <kronos44@mail.ru>"] authors = ["plazmoid <kronos44@mail.ru>"]
edition = "2021" edition = "2018"
# 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-cors = "0.6.1" tokio = { version = "1.2.0", features = ["macros", "rt-multi-thread", "process"] }
actix-web = "4.1"
anyhow = { workspace = true }
futures-util = "0.3.21"
mime_guess = { workspace = true }
once_cell = "1.8.0"
rust-embed = { version = "6.3.0", features = ["debug-embed", "compression"] }
serde = { workspace = true }
serde_json = { workspace = true }
strum = { version = "0.22.0", features = ["derive"] }
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-appender = { workspace = true }
shlex = "1.1.0"
structopt = "0.3.21" structopt = "0.3.21"
thiserror = "1.0.31" log = "^0.4"
uuid = { workspace = true } env_logger = "0.7.1"
u_lib = { version = "*", path = "../../lib/u_lib", features = ["panel"] } uuid = "0.6.5"
reqwest = { version = "0.11", features = ["json"] }
openssl = "*"
u_lib = { version = "*", path = "../../lib/u_lib" }
serde_json = "1.0.4"
serde = { version = "1.0.114", features = ["derive"] }

@ -1,176 +0,0 @@
use serde_json::{from_str, to_value, Value};
use structopt::StructOpt;
use u_lib::{
api::HttpClient, messaging::AsMsg, models::*, types::Id, types::PanelResult, UError, UResult,
};
#[derive(StructOpt, Debug)]
pub struct Args {
#[structopt(subcommand)]
cmd: Cmd,
#[structopt(short, long, default_value)]
brief: Brief,
}
#[derive(StructOpt, Debug)]
enum Cmd {
Agents(RUD),
Jobs(CRUD),
Map(AssignedCRUD),
Payloads(PayloadCRUD),
Ping,
Serve,
}
#[derive(StructOpt, Debug)]
enum CRUD {
Create {
item: String,
},
#[structopt(flatten)]
RUD(RUD),
}
#[derive(StructOpt, Debug)]
enum AssignedCRUD {
Create {
item: String,
},
#[structopt(flatten)]
RUD(RUD),
}
#[derive(StructOpt, Debug)]
enum PayloadCRUD {
Create {
item: String,
},
Read {
id: Option<String>,
},
Update {
item: String,
},
Delete {
#[structopt(parse(try_from_str = parse::uuid))]
id: Id,
},
}
#[derive(StructOpt, Debug)]
enum RUD {
Read {
#[structopt(parse(try_from_str = parse::uuid))]
id: Option<Id>,
},
Update {
item: String,
},
Delete {
#[structopt(parse(try_from_str = parse::uuid))]
id: Id,
},
}
mod parse {
use super::*;
pub fn uuid(src: &str) -> Result<Id, String> {
Id::parse_str(src).map_err(|e| e.to_string())
}
}
pub fn into_value<M: AsMsg>(data: M) -> Value {
to_value(data).unwrap()
}
pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult<Value> {
let catcher: UResult<Value> = (|| async {
Ok(match args.cmd {
Cmd::Agents(action) => match action {
RUD::Read { id } => into_value(client.get_agents(id).await?),
RUD::Update { item } => {
let agent = from_str::<Agent>(&item)
.map_err(|e| UError::DeserializeError(e.to_string(), item))?;
into_value(client.update_agent(&agent).await?)
}
RUD::Delete { id } => into_value(client.del(id).await?),
},
Cmd::Jobs(action) => match action {
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.try_into_job()?;
if let Some(payload) = &mut job.payload {
payload.join_payload()?;
}
into_value(client.upload_jobs([&job]).await?)
}
CRUD::RUD(RUD::Read { id }) => match id {
Some(id) => into_value(vec![client.get_job(id, args.brief).await?]),
None => into_value(client.get_jobs().await?),
},
CRUD::RUD(RUD::Update { item }) => {
let raw_job = from_str::<JobMeta>(&item)
.map_err(|e| UError::DeserializeError(e.to_string(), item))?;
let job = raw_job.validate()?;
// if let Some(payload) = &mut job.payload {
// payload.join_payload()?;
// }
into_value(client.update_job(&job).await?)
}
CRUD::RUD(RUD::Delete { id }) => into_value(client.del(id).await?),
},
Cmd::Map(action) => match action {
AssignedCRUD::Create { item } => {
let payload = serde_json::from_str::<Vec<AssignedJobById>>(&item)
.map_err(|e| UError::DeserializeError(e.to_string(), item))?;
into_value(client.assign_jobs(&payload).await?)
}
AssignedCRUD::RUD(RUD::Read { id }) => {
into_value(client.get_assigned_jobs(id).await?)
}
AssignedCRUD::RUD(RUD::Update { item }) => {
let assigned = from_str::<AssignedJob>(&item)
.map_err(|e| UError::DeserializeError(e.to_string(), item))?;
into_value(client.update_result(&assigned).await?)
}
AssignedCRUD::RUD(RUD::Delete { id }) => into_value(client.del(id).await?),
},
Cmd::Payloads(action) => match action {
PayloadCRUD::Create { item } => {
let payload = from_str::<RawPayload>(&item)
.map_err(|e| UError::DeserializeError(e.to_string(), item))?;
into_value(client.upload_payload(&payload).await?)
}
PayloadCRUD::Read { id } => match id {
None => into_value(client.get_payloads().await?),
Some(id) => into_value(vec![client.get_payload(id, args.brief).await?]),
},
PayloadCRUD::Update { item } => {
let payload = from_str::<Payload>(&item)
.map_err(|e| UError::DeserializeError(e.to_string(), item))?;
into_value(client.update_payload(&payload).await?)
}
PayloadCRUD::Delete { id } => into_value(client.del(id).await?),
},
Cmd::Ping => into_value(client.ping().await?),
Cmd::Serve => {
crate::gui::serve(client)
.await
.map_err(|e| UError::PanelError(format!("{e:?}")))?;
Value::Null
}
})
})()
.await;
match catcher {
Ok(r) => PanelResult::Ok(r),
Err(e) => PanelResult::Err(e),
}
}

@ -1,17 +0,0 @@
use actix_web::http::StatusCode;
use actix_web::ResponseError;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Arg parse error: {0}")]
ArgparseError(#[from] structopt::clap::Error),
#[error("Just an error: {0}")]
JustError(String),
}
impl ResponseError for Error {
fn status_code(&self) -> actix_web::http::StatusCode {
StatusCode::BAD_REQUEST
}
}

@ -1,16 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR

@ -1,16 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

@ -1,48 +0,0 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
package-lock.json

@ -1,27 +0,0 @@
# Fe
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.1.2.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

@ -1,114 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"fe": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "less"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/fe",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "less",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.less"
],
"scripts": []
},
"configurations": {
"production": {
"baseHref": "/core/",
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "fe:build:production"
},
"development": {
"browserTarget": "fe:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "fe:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "less",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.less"
],
"scripts": []
}
}
}
}
},
"defaultProject": "fe"
}

@ -1,44 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/fe'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

@ -1,43 +0,0 @@
{
"name": "fe",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.1.0",
"@angular/cdk": "^13.3.9",
"@angular/common": "~13.1.0",
"@angular/compiler": "~13.1.0",
"@angular/core": "~13.1.0",
"@angular/forms": "~13.1.0",
"@angular/material": "^13.3.9",
"@angular/platform-browser": "~13.1.0",
"@angular/platform-browser-dynamic": "~13.1.0",
"@angular/router": "~13.1.0",
"@types/uuid": "^8.3.4",
"rxjs": "~7.4.0",
"tslib": "^2.3.0",
"uuid": "^8.3.2",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^13.3.9",
"@angular/cli": "~13.1.2",
"@angular/compiler-cli": "~13.1.0",
"@types/jasmine": "~3.10.0",
"@types/node": "^12.11.1",
"jasmine-core": "~3.10.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.5.2"
}
}

@ -1,18 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { JobComponent, ResultComponent, AgentComponent, PayloadComponent } from './components/tables';
//import { AgentInfoDialogComponent } from './core/tables/dialogs/agent-info-dialog.component';
const routes: Routes = [
{ path: '', redirectTo: 'agents', pathMatch: 'full' },
{ path: 'agents', component: AgentComponent },
{ path: 'jobs', component: JobComponent },
{ path: 'payloads', component: PayloadComponent },
{ path: 'results', component: ResultComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

@ -1,6 +0,0 @@
<nav mat-tab-nav-bar animationDuration="0ms" mat-align-tabs="center">
<a mat-tab-link *ngFor="let tab of tabs" routerLink={{tab.link}} routerLinkActive #rla="routerLinkActive"
[active]="rla.isActive" [routerLinkActiveOptions]="{ exact: true }">{{tab.name}}</a>
</nav>
<router-outlet></router-outlet>
<global-error></global-error>

@ -1,35 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'fe'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('fe');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('fe app is running!');
});
});

@ -1,15 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less']
})
export class AppComponent {
tabs = [
{ name: 'Agents', link: '/agents' },
{ name: 'Jobs', link: '/jobs' },
{ name: 'Results', link: '/results' },
{ name: 'Payloads', link: '/payloads' }
];
}

@ -1,73 +0,0 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTableModule } from '@angular/material/table';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatButtonModule } from '@angular/material/button'
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { HttpClientModule } from '@angular/common/http';
import { MatDialogModule } from '@angular/material/dialog';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { FormsModule } from '@angular/forms';
import { AgentComponent, JobComponent, ResultComponent, PayloadComponent } from './components/tables';
import {
AgentInfoDialogComponent,
AssignJobDialogComponent,
JobInfoDialogComponent,
ResultInfoDialogComponent,
PayloadInfoDialogComponent
} from './components/dialogs';
import { APP_BASE_HREF } from '@angular/common';
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';
import { NewPayloadDialogComponent } from './components/dialogs/new-payload-dialog/new-payload-dialog.component';
@NgModule({
declarations: [
AppComponent,
AgentComponent,
JobComponent,
ResultComponent,
AgentInfoDialogComponent,
JobInfoDialogComponent,
ResultInfoDialogComponent,
AssignJobDialogComponent,
PayloadComponent,
PayloadInfoDialogComponent,
GlobalErrorComponent,
PayloadOverviewComponent,
NewPayloadDialogComponent
],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule,
MatTabsModule,
MatTableModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatDialogModule,
MatProgressSpinnerModule,
MatIconModule,
MatTooltipModule,
MatSnackBarModule,
MatSelectModule,
MatListModule,
MatGridListModule,
FormsModule,
BrowserAnimationsModule
],
providers: [{ provide: APP_BASE_HREF, useValue: '/' }],
bootstrap: [AppComponent]
})
export class AppModule { }

@ -1,64 +0,0 @@
<h2 mat-dialog-title *ngIf="is_preview">Agent info</h2>
<h2 mat-dialog-title *ngIf="!is_preview">Editing agent info</h2>
<mat-dialog-content>
<p>
<mat-form-field class="info-dlg-field" cdkFocusInitial>
<mat-label>ID</mat-label>
<input matInput disabled value="{{data.id}}">
</mat-form-field>
</p>
<p>
<mat-form-field class="info-dlg-field">
<mat-label>Alias</mat-label>
<input matInput [readonly]="is_preview" [(ngModel)]="data.alias">
</mat-form-field>
</p>
<p>
<mat-form-field class="info-dlg-field">
<mat-label>Username</mat-label>
<input matInput [readonly]="is_preview" [(ngModel)]="data.username">
</mat-form-field>
</p>
<p>
<mat-form-field class="info-dlg-field">
<mat-label>Hostname</mat-label>
<input matInput [readonly]="is_preview" [(ngModel)]="data.hostname">
</mat-form-field>
</p>
<p>
<mat-form-field class="info-dlg-field">
<mat-label>Host info</mat-label>
<textarea matInput cdkTextareaAutosize [readonly]="is_preview" [(ngModel)]="data.host_info">
</textarea>
</mat-form-field>
</p>
<p>
<mat-form-field class="info-dlg-field">
<mat-label>Platform</mat-label>
<input matInput [readonly]="is_preview" [(ngModel)]="data.platform">
</mat-form-field>
</p>
<p>
<mat-form-field class="info-dlg-field">
<mat-label>Is root</mat-label>
<input matInput disabled value="{{data.is_root}}">
</mat-form-field>
</p>
<p>
<mat-form-field class="info-dlg-field">
<mat-label>Registration time</mat-label>
<input matInput disabled value="{{data.regtime.secs_since_epoch * 1000 | date:'long'}}">
</mat-form-field>
</p>
<p>
<mat-form-field class="info-dlg-field">
<mat-label>Last active time</mat-label>
<input matInput disabled value="{{data.last_active.secs_since_epoch * 1000 | date:'long'}}">
</mat-form-field>
</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button *ngIf="is_preview" (click)="is_preview = false">Edit</button>
<button mat-raised-button *ngIf="!is_preview" (click)="updateAgent()">Save</button>
<button mat-button mat-dialog-close>Cancel</button>
</mat-dialog-actions>

@ -1,21 +0,0 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { AgentModel } from '../../../models/agent.model';
import { EventEmitter } from '@angular/core';
@Component({
selector: 'agent-info-dialog',
templateUrl: 'agent-info-dialog.component.html',
styleUrls: ['../base-info-dialog.component.less']
})
export class AgentInfoDialogComponent {
is_preview = true;
onSave = new EventEmitter();
constructor(@Inject(MAT_DIALOG_DATA) public data: AgentModel) { }
updateAgent() {
console.log(this.data);
this.onSave.emit(this.data);
}
}

@ -1,12 +0,0 @@
<h2 mat-dialog-title>Assign job</h2>
<mat-dialog-content>
<mat-selection-list #jobsList [(ngModel)]="selected_rows">
<mat-list-option *ngFor="let row of rows" [value]="row">
{{row}}
</mat-list-option>
</mat-selection-list>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close (click)="assignSelectedJobs()">Assign</button>
<button mat-button mat-dialog-close>Cancel</button>
</mat-dialog-actions>

@ -1,36 +0,0 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { AssignedJobByIdModel } from 'src/app/models';
import { ApiTableService } from '../../../services';
@Component({
selector: 'assign-job-dialog',
templateUrl: 'assign-job-dialog.component.html',
styleUrls: []
})
export class AssignJobDialogComponent {
rows: string[] = [];
selected_rows: string[] = [];
constructor(
@Inject(MAT_DIALOG_DATA) public agent_id: string,
private dataSource: ApiTableService,
) {
dataSource.getJobs().subscribe(resp => {
this.rows = resp.map(j => `${j.id} ${j.alias}`)
})
}
assignSelectedJobs() {
const assigned_jobs: AssignedJobByIdModel[] = this.selected_rows.map(row => {
const job_id = row.split(' ', 1)[0];
return {
job_id: job_id,
agent_id: this.agent_id
}
});
this.dataSource.createResult(assigned_jobs).subscribe(_ => {
alert("Created")
});
}
}

@ -1,18 +0,0 @@
.info-dlg-field {
width: 100%;
}
div.info-dialog-forms-box {
width: 100%;
margin-right: 10px;
}
div.info-dialog-forms-box-smol {
width: 30%;
float: left;
margin-right: 10px;
}
.code {
font-family: "Roboto Mono", monospace;
}

@ -1,5 +0,0 @@
export * from './agent-info-dialog/agent-info-dialog.component';
export * from './result-info-dialog/result-info-dialog.component';
export * from './job-info-dialog/job-info-dialog.component';
export * from './assign-job-dialog/assign-job-dialog.component';
export * from './payload-info-dialog/payload-info-dialog.component';

@ -1,48 +0,0 @@
<h2 mat-dialog-title *ngIf="isPreview">Job info</h2>
<h2 mat-dialog-title *ngIf="!isPreview">Editing job info</h2>
<mat-dialog-content>
<div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field">
<mat-label>ID</mat-label>
<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.meta.alias">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Args</mat-label>
<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.meta.exec_type">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Platform</mat-label>
<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.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.meta.payload_id">
<mat-option *ngFor="let pld of allPayloads" [value]="pld[0]">{{ pld[1] }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="info-dialog-forms-box">
<payload-overview *ngIf="data.payload" [preview]="true" [payload]="data.payload.data"></payload-overview>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button *ngIf="isPreview" (click)="isPreview = false">Edit</button>
<button mat-raised-button *ngIf="!isPreview" (click)="updateJob()">Save</button>
<button mat-button mat-dialog-close>Cancel</button>
</mat-dialog-actions>

@ -1,28 +0,0 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { EventEmitter } from '@angular/core';
import { Job, JobModel } from '../../../models/job.model';
import { ApiTableService } from 'src/app/services';
@Component({
selector: 'job-info-dialog',
templateUrl: 'job-info-dialog.component.html',
styleUrls: ['../base-info-dialog.component.less']
})
export class JobInfoDialogComponent {
//[id, name]
isPreview = true;
allPayloads: [string | null, string][] = [[null, "none"]];
onSave = new EventEmitter<JobModel>();
constructor(@Inject(MAT_DIALOG_DATA) public data: Job, dataSource: ApiTableService) {
dataSource.getPayloads().subscribe(resp => {
this.allPayloads = this.allPayloads.concat(resp.map(r => [r.id, r.name]))
})
}
updateJob() {
this.onSave.emit(this.data.meta);
}
}

@ -1,20 +0,0 @@
<h2 mat-dialog-title>New payload</h2>
<mat-dialog-content>
<div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field" cdkFocusInitial>
<mat-label>Name</mat-label>
<input matInput [(ngModel)]="payload.name">
</mat-form-field>
<input type="file" class="file-input" (change)="onFileSelected($event)" #fileUpload>
</div>
<div class="info-dialog-forms-box">
<mat-form-field class="info-dlg-field" *ngIf="!uploadMode">
<mat-label>Data</mat-label>
<textarea matInput [(ngModel)]="decodedPayload"></textarea>
</mat-form-field>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button (click)="save()">Save</button>
<button mat-button mat-dialog-close>Close</button>
</mat-dialog-actions>

@ -1,43 +0,0 @@
import { Component, EventEmitter, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { NewPayloadModel } from 'src/app/models/payload.model';
@Component({
selector: 'new-payload-dialog',
templateUrl: 'new-payload-dialog.component.html',
styleUrls: ['../base-info-dialog.component.less']
})
export class NewPayloadDialogComponent {
decodedPayload = "";
uploadMode = false;
onSave = new EventEmitter<NewPayloadModel>();
constructor(@Inject(MAT_DIALOG_DATA) public payload: NewPayloadModel) { }
save() {
if (this.payload.data.length == 0) {
this.payload.data = Array.from(new TextEncoder().encode(this.decodedPayload));
}
this.onSave.emit(this.payload);
}
onFileSelected(event: any) {
const file: File = event.target.files[0];
if (file) {
this.uploadMode = true
const reader = new FileReader();
reader.onload = e => {
this.payload.name = file.name;
const result = e.target?.result;
if (result instanceof ArrayBuffer) {
const d = Array.from(new Uint8Array(result));
this.payload.data = d;
console.log(this.payload.data)
} else {
alert!("no file")
}
}
reader.readAsArrayBuffer(file)
}
}
}

@ -1,34 +0,0 @@
<h2 mat-dialog-title *ngIf="isPreview">Payload</h2>
<h2 mat-dialog-title *ngIf="!isPreview">Editing payload</h2>
<mat-dialog-content>
<div class="info-dialog-forms-box">
<div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field" cdkFocusInitial>
<mat-label>ID</mat-label>
<input matInput disabled value="{{payload.id}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Name</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="payload.name">
</mat-form-field>
</div>
<div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field">
<mat-label>MIME-type</mat-label>
<input matInput disabled value="{{payload.mime_type}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Size</mat-label>
<input matInput disabled value="{{payload.size}}">
</mat-form-field>
</div>
</div>
<div class="info-dialog-forms-box">
<payload-overview [preview]="isPreview" [payload]="payload.data"></payload-overview>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button *ngIf="isPreview" (click)="isPreview = false">Edit</button>
<button mat-raised-button *ngIf="!isPreview" (click)="updatePayload()">Save</button>
<button mat-button mat-dialog-close>Close</button>
</mat-dialog-actions>

@ -1,20 +0,0 @@
import { Component, EventEmitter, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { PayloadModel } from 'src/app/models/payload.model';
@Component({
selector: 'payload-info-dialog',
templateUrl: 'payload-info-dialog.component.html',
styleUrls: ['../base-info-dialog.component.less']
})
export class PayloadInfoDialogComponent {
isPreview = true;
onSave = new EventEmitter<PayloadModel>();
constructor(@Inject(MAT_DIALOG_DATA) public payload: PayloadModel) { }
updatePayload() {
this.onSave.emit(this.payload);
}
}

@ -1,47 +0,0 @@
<h2 mat-dialog-title>Result</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}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Job ID</mat-label>
<input matInput readonly value="{{data.job_id}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Agent ID</mat-label>
<input matInput readonly value="{{data.agent_id}}">
</mat-form-field>
</div>
<div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field">
<mat-label>Alias</mat-label>
<input matInput readonly value="{{data.alias}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>State</mat-label>
<input matInput readonly value="{{data.state}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Return code</mat-label>
<input matInput readonly value="{{data.retcode}}">
</mat-form-field>
</div>
<div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field">
<mat-label>Created</mat-label>
<input matInput readonly value="{{data.created.secs_since_epoch * 1000 | date:'long'}}">
</mat-form-field>
<mat-form-field class="info-dlg-field">
<mat-label>Updated</mat-label>
<input matInput readonly value="{{data.updated.secs_since_epoch * 1000 | date:'long'}}">
</mat-form-field>
</div>
<div class="info-dialog-forms-box">
<payload-overview [preview]="true" [payload]="data.result"></payload-overview>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Close</button>
</mat-dialog-actions>

@ -1,20 +0,0 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ResultModel } from '../../../models/result.model';
@Component({
selector: 'result-info-dialog',
templateUrl: 'result-info-dialog.component.html',
styleUrls: ['../base-info-dialog.component.less']
})
export class ResultInfoDialogComponent {
decodedResult: string;
constructor(@Inject(MAT_DIALOG_DATA) public data: ResultModel) {
if (data.result !== null) {
this.decodedResult = new TextDecoder().decode(new Uint8Array(data.result))
} else {
this.decodedResult = ""
}
}
}

@ -1,34 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
import { ErrorService } from 'src/app/services/error.service';
@Component({
selector: 'global-error',
templateUrl: './global-error.component.html',
styleUrls: ['./global-error.component.less']
})
export class GlobalErrorComponent implements OnInit {
constructor(
private snackBar: MatSnackBar,
private errorService: ErrorService
) { }
ngOnInit() {
this.errorService.error$.subscribe(err => {
const _config = (duration: number): MatSnackBarConfig => {
return {
horizontalPosition: 'right',
verticalPosition: 'bottom',
duration
}
}
const error = true;
const cfg = error ? _config(0) : _config(2000)
if (err != '') {
this.snackBar.open(err, 'Ok', cfg)
}
})
}
}

@ -1,7 +0,0 @@
<mat-form-field style="box-sizing:border-box; width:100%" class="info-dlg-field" floatLabel="always">
<mat-label>Payload data</mat-label>
<textarea class="code" matInput cdkTextareaAutosize="true" *ngIf="!isTooBigPayload" [readonly]="isPreview"
[(ngModel)]="decodedPayload">
</textarea>
<input matInput *ngIf="isTooBigPayload" disabled placeholder="Payload is too big to display">
</mat-form-field>

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

@ -1,78 +0,0 @@
<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)="applyFilter($event)" #input>
</mat-form-field>
<button id="refresh_btn" mat-raised-button color="primary" (click)="loadTableData()">Refresh</button>
<table mat-table fixedLayout="true" [dataSource]="table_data" class="data-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</th>
<td mat-cell *matCellDef="let row">
{{row.username}}
</td>
</ng-container>
<ng-container matColumnDef="hostname">
<th mat-header-cell *matHeaderCellDef>Hostname</th>
<td mat-cell *matCellDef="let row">
{{row.hostname}}
</td>
</ng-container>
<ng-container matColumnDef="last_active">
<th mat-header-cell *matHeaderCellDef>Last active</th>
<td mat-cell *matCellDef="let row">
{{row.last_active.secs_since_epoch * 1000 | date:'long'}}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let row">
<button mat-icon-button (click)="assignJobs(row.id)">
<mat-icon>add_task</mat-icon>
</button>
|
<button mat-icon-button routerLink='.' [queryParams]="{id: row.id}">
<mat-icon>more_horiz</mat-icon>
</button>
|
<button mat-icon-button (click)="deleteItem(row.id)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="data-table-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>

@ -1,42 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { TableComponent } from '../base-table/base-table.component';
import { AgentModel, Area } from '../../../models';
import { AssignJobDialogComponent, AgentInfoDialogComponent } from '../../dialogs';
@Component({
selector: 'agent-table',
templateUrl: './agent-table.component.html',
styleUrls: ['../base-table/base-table.component.less'],
})
export class AgentComponent extends TableComponent<AgentModel> implements OnInit {
area = 'agents' as Area
displayedColumns = ['id', 'alias', 'username', 'hostname', 'last_active', 'actions']
showItemDialog(id: string) {
this.dataSource.getAgent(id).subscribe(resp => {
const dialog = this.infoDialog.open(AgentInfoDialogComponent, {
data: resp,
width: '1000px',
});
const saveSub = dialog.componentInstance.onSave.subscribe(result => {
this.dataSource.updateAgent(result).subscribe(_ => {
alert('Saved')
this.loadTableData()
})
})
dialog.afterClosed().subscribe(result => {
saveSub.unsubscribe()
this.router.navigate(['.'], { relativeTo: this.route })
})
})
}
assignJobs(id: string) {
const dialog = this.infoDialog.open(AssignJobDialogComponent, {
data: id,
width: '1000px',
});
}
}

@ -1,32 +0,0 @@
.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;
}
.data-table-row {
height: 30px;
}
.data-table-row:hover {
background: whitesmoke;
}

@ -1,59 +0,0 @@
import { OnInit, Directive, Component } from '@angular/core';
import { ApiTableService } from '../../..';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { ApiModel, Area } from '../../../models';
import { ActivatedRoute, Router } from '@angular/router';
@Directive()
export abstract class TableComponent<T extends ApiModel> implements OnInit {
abstract area: Area;
table_data: MatTableDataSource<T> = new MatTableDataSource;
isLoadingResults = true;
constructor(
public dataSource: ApiTableService,
public infoDialog: MatDialog,
public route: ActivatedRoute,
public router: Router,
) { }
ngOnInit() {
this.loadTableData();
this.route.queryParams.subscribe(params => {
const id = params['id']
const new_item = params['new']
if (id) {
this.showItemDialog(id);
}
if (new_item) {
this.showItemDialog(null);
}
})
//interval(10000).subscribe(_ => this.loadTableData());
}
loadTableData() {
this.isLoadingResults = true;
this.dataSource.getMany(this.area).subscribe(resp => {
this.isLoadingResults = false;
this.table_data.data = resp;
})
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.table_data.filter = filterValue.trim().toLowerCase();
}
deleteItem(id: string) {
if (confirm(`Delete ${id}?`)) {
this.dataSource.delete(id, this.area).subscribe(_ => { })
this.loadTableData()
}
}
abstract displayedColumns: string[];
abstract showItemDialog(id: string | null): void;
}

@ -1,5 +0,0 @@
export * from './agent-table/agent-table.component';
export * from './base-table/base-table.component';
export * from './job-table/job-table.component';
export * from './payload-table/payload-table.component';
export * from './result-table/result-table.component';

@ -1,83 +0,0 @@
<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)="applyFilter($event)" #input>
</mat-form-field>
<button id="refresh_btn" mat-raised-button color="basic" (click)="loadTableData()">Refresh</button>
<button id="new_btn" mat-raised-button color="primary" routerLink='.' [queryParams]="{new: true}">Add
job</button>
<table mat-table fixedLayout="true" [dataSource]="table_data" class="data-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="argv">
<th mat-header-cell *matHeaderCellDef>Cmd-line args</th>
<td mat-cell *matCellDef="let row">
{{row.argv}}
</td>
</ng-container>
<ng-container matColumnDef="platform">
<th mat-header-cell *matHeaderCellDef>Platform</th>
<td mat-cell *matCellDef="let row">
{{row.target_platforms}}
</td>
</ng-container>
<ng-container matColumnDef="schedule">
<th mat-header-cell *matHeaderCellDef>Schedule</th>
<td mat-cell *matCellDef="let row">
{{row.schedule}}
</td>
</ng-container>
<ng-container matColumnDef="exec_type">
<th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let row">
{{row.exec_type}}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let row">
<button mat-icon-button routerLink='.' [queryParams]="{id: row.id}">
<mat-icon>more_horiz</mat-icon>
</button>
|
<button mat-icon-button (click)="deleteItem(row.id)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="data-table-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>

@ -1,72 +0,0 @@
import { Component, OnInit } from '@angular/core';
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',
templateUrl: './job-table.component.html',
styleUrls: ['../base-table/base-table.component.less'],
providers: [{ provide: 'area', useValue: 'jobs' }]
})
export class JobComponent extends TableComponent<JobModel> {
area = 'jobs' as Area;
displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions']
showItemDialog(id: string | null) {
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',
});
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("Updated")
this.loadTableData()
})
}
dialog.close()
})
dialog.afterClosed().subscribe(result => {
saveSub.unsubscribe()
this.router.navigate(['.'], { relativeTo: this.route })
})
})
}
}

@ -1,63 +0,0 @@
<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)="applyFilter($event)" #input>
</mat-form-field>
<button id="refresh_btn" mat-raised-button color="primary" (click)="loadTableData()">Refresh</button>
<button id="new_btn" mat-raised-button color="primary" routerLink='.' [queryParams]="{new: true}">Add
payload</button>
<table mat-table fixedLayout="true" [dataSource]="table_data" class="data-table" matSort matSortActive="id"
matSortDisableClear matSortDirection="desc">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Name</th>
<td mat-cell *matCellDef="let row">
{{row.name}}
</td>
</ng-container>
<ng-container matColumnDef="mime_type">
<th mat-header-cell *matHeaderCellDef>MIME-type</th>
<td mat-cell *matCellDef="let row">
{{row.mime_type}}
</td>
</ng-container>
<ng-container matColumnDef="size">
<th mat-header-cell *matHeaderCellDef>Size</th>
<td mat-cell *matCellDef="let row">
{{row.size}}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let row">
<button mat-icon-button routerLink='.' [queryParams]="{id: row.id}">
<mat-icon>more_horiz</mat-icon>
</button>
|
<button mat-icon-button (click)="deleteItem(row.id)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="data-table-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>

@ -1,67 +0,0 @@
import { Component } from '@angular/core';
import { Area } from 'src/app/models';
import { NewPayloadModel, PayloadModel } from 'src/app/models/payload.model';
import { PayloadInfoDialogComponent } from '../../dialogs';
import { NewPayloadDialogComponent } from '../../dialogs/new-payload-dialog/new-payload-dialog.component';
import { TableComponent } from '../base-table/base-table.component';
@Component({
selector: 'payload-table',
templateUrl: './payload-table.component.html',
styleUrls: ['../base-table/base-table.component.less'],
providers: [{ provide: 'area', useValue: 'payloads' }]
})
export class PayloadComponent extends TableComponent<PayloadModel> {
area = 'payloads' as Area
displayedColumns = ["name", "mime_type", "size", 'actions'];
showItemDialog(id: string | null) {
if (id === null) {
const payload: NewPayloadModel = {
name: "",
data: []
}
const dialog = this.infoDialog.open(NewPayloadDialogComponent, {
data: payload,
width: '1000px',
});
dialog.componentInstance.onSave.subscribe(result => {
this.dataSource.createPayload(result)
.subscribe(_ => {
alert("Created")
this.loadTableData()
})
dialog.close()
})
dialog.afterClosed().subscribe(_ => {
this.router.navigate(['.'], { relativeTo: this.route })
})
} else {
this.dataSource.getPayload(id as string).subscribe(resp => {
const dialog = this.infoDialog.open(PayloadInfoDialogComponent, {
data: resp,
width: '1000px',
});
const saveSub = dialog.componentInstance.onSave.subscribe(result => {
this.dataSource.updatePayload(result)
.subscribe(_ => {
alert("Updated")
this.loadTableData()
})
dialog.close()
})
dialog.afterClosed().subscribe(_ => {
saveSub.unsubscribe()
this.router.navigate(['.'], { relativeTo: this.route })
})
})
}
}
}

@ -1,81 +0,0 @@
<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)="applyFilter($event)" #input>
</mat-form-field>
<button id="refresh_btn" mat-raised-button color="primary" (click)="loadTableData()">Refresh</button>
<table mat-table fixedLayout="true" [dataSource]="table_data" class="data-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="agent_id">
<th mat-header-cell *matHeaderCellDef>Agent</th>
<td mat-cell *matCellDef="let row">
<a routerLink='/agents' [queryParams]="{id: row.agent_id}">{{row.agent_id}}</a>
</td>
</ng-container>
<ng-container matColumnDef="job_id">
<th mat-header-cell *matHeaderCellDef>Job</th>
<td mat-cell *matCellDef="let row">
<a routerLink='/jobs' [queryParams]="{id: row.job_id}">{{row.job_id}}</a>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef>State</th>
<td mat-cell *matCellDef="let row">
{{row.state}} {{(row.state === "Finished") ? '(' + row.retcode + ')' : ''}}
</td>
</ng-container>
<ng-container matColumnDef="last_updated">
<th mat-header-cell *matHeaderCellDef>Last updated</th>
<td mat-cell *matCellDef="let row">
{{row.updated.secs_since_epoch * 1000| date:'long'}}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let row">
<button mat-icon-button routerLink='.' [queryParams]="{id: row.id}">
<mat-icon>more_horiz</mat-icon>
</button>
|
<button mat-icon-button (click)="deleteItem(row.id)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row class="data-table-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>

@ -1,36 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { TableComponent } from '../base-table/base-table.component';
import { Area, ResultModel } from '../../../models';
import { ResultInfoDialogComponent } from '../../dialogs';
@Component({
selector: 'results-table',
templateUrl: './result-table.component.html',
styleUrls: ['../base-table/base-table.component.less'],
providers: [{ provide: 'area', useValue: 'map' }]
})
export class ResultComponent extends TableComponent<ResultModel> {
area = 'map' as Area
displayedColumns = [
'id',
'alias',
'agent_id',
'job_id',
'state',
'last_updated',
'actions'
];
showItemDialog(id: string) {
this.dataSource.getResult(id).subscribe(resp => {
const dialog = this.infoDialog.open(ResultInfoDialogComponent, {
data: resp,
width: '1000px',
});
dialog.afterClosed().subscribe(_ => {
this.router.navigate(['.'], { relativeTo: this.route })
})
})
}
}

@ -1 +0,0 @@
export * from './services';

@ -1,16 +0,0 @@
import { UTCDate } from ".";
export interface AgentModel {
alias: string | null,
hostname: string,
host_info: 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,
}

@ -1,20 +0,0 @@
import { AgentModel } from './agent.model';
import { JobModel } from './job.model';
import { PayloadModel } from './payload.model';
import { ResultModel } from './result.model';
export * from './agent.model';
export * from './result.model';
export * from './job.model';
export * from './payload.model';
export interface UTCDate {
secs_since_epoch: number,
nanos_since_epoch: number
}
export type Area = "agents" | "jobs" | "map" | "payloads";
export type ApiModel = AgentModel | JobModel | ResultModel | PayloadModel | Empty;
export interface Empty { }

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

@ -1,12 +0,0 @@
export interface PayloadModel {
id: string,
mime_type: string,
name: string,
size: number,
data: number[] | null
}
export interface NewPayloadModel {
name: string,
data: number[]
}

@ -1,18 +0,0 @@
import { UTCDate } from ".";
export interface ResultModel {
agent_id: string,
alias: string,
created: UTCDate,
id: string,
job_id: string,
result: number[] | null,
state: "Queued" | "Running" | "Finished",
retcode: number | null,
updated: UTCDate,
}
export interface AssignedJobByIdModel {
job_id: string,
agent_id: string
}

@ -1,142 +0,0 @@
import { environment } from 'src/environments/environment';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, map, catchError, throwError } from 'rxjs';
import { ApiModel, PayloadModel, Empty, Area, AgentModel, JobModel, ResultModel, Job, NewPayloadModel, AssignedJobByIdModel } from '../models';
import { Injectable, Inject } from '@angular/core';
import { ErrorService } from './error.service';
type Status = "ok" | "err";
interface ServerResponse<T extends ApiModel> {
status: Status,
data: T | string
}
@Injectable({
providedIn: 'root'
})
export class ApiTableService {
constructor(
private http: HttpClient,
private errorService: ErrorService
) {
}
requestUrl = `${environment.server}/cmd/`;
req<R extends ApiModel>(cmd: string): Observable<ServerResponse<R>> {
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> {
const request = `${area} read ${id}` + (brief !== null ? `-b=${brief}` : '')
const resp = this.req<T[]>(request).pipe(
map(resp => {
if (resp.data.length === 0) {
return {
status: 'err' as Status,
data: `${id} not found in ${area}`
}
}
return {
status: resp.status,
data: resp.data[0]
}
}));
return this.filterErrStatus(resp)
}
getAgent(id: string): Observable<AgentModel> {
return this.getOne(id, 'agents')
}
getJob(id: string): Observable<Job> {
return this.getOne(id, 'jobs')
}
getResult(id: string): Observable<ResultModel> {
return this.getOne(id, 'map')
}
getPayload(id: string): Observable<PayloadModel> {
return this.getOne(id, 'payloads')
}
getMany(area: Area): Observable<any[]> {
return this.filterErrStatus(this.req(`${area} read`))
}
getAgents(): Observable<AgentModel[]> {
return this.getMany('agents')
}
getJobs(): Observable<JobModel[]> {
return this.getMany('jobs')
}
getResults(): Observable<ResultModel[]> {
return this.getMany('map')
}
getPayloads(): Observable<PayloadModel[]> {
return this.getMany('payloads')
}
update<T extends ApiModel>(item: T, area: Area): Observable<Empty> {
return this.filterErrStatus(this.req(`${area} update '${JSON.stringify(item)}'`))
}
updateAgent(item: AgentModel): Observable<Empty> {
return this.update(item, 'agents')
}
updateJob(item: JobModel): Observable<Empty> {
return this.update(item, 'jobs')
}
updateResult(item: ResultModel): Observable<Empty> {
return this.update(item, 'map')
}
updatePayload(item: PayloadModel): Observable<Empty> {
return this.update(item, 'payloads')
}
delete(id: string, area: Area): Observable<Empty> {
return this.filterErrStatus(this.req(`${area} delete ${id}`))
}
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 '${serialized}'`))
}
createResult(item: AssignedJobByIdModel[]): Observable<string[]> {
return this.create(item, 'map')
}
createPayload(item: NewPayloadModel): Observable<string[]> {
return this.create(item, 'payloads')
}
filterErrStatus<R extends ApiModel>(obs: Observable<ServerResponse<R>>): Observable<R> {
return obs.pipe(
map(r => {
if (r.status == 'err') {
throw new Error(r.data as string)
}
return r.data as R
}),
catchError(this.errorHandler.bind(this)))
}
errorHandler(err: HttpErrorResponse, caught: any) {
var error = err.error.data !== undefined ? JSON.stringify(err.error.data) : err.message;
this.errorService.handle(error);
return throwError(() => new Error());
}
}

@ -1,17 +0,0 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ErrorService {
error$ = new Subject<string>();
handle(msg: string) {
this.error$.next(msg)
}
clear() {
this.handle('')
}
}

@ -1 +0,0 @@
export * from './api.service'

@ -1,3 +0,0 @@
export function epochToStr(epoch: number): string {
return new Date(epoch * 1000).toLocaleString('en-GB')
}

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

@ -1,17 +0,0 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
server: "http://127.0.0.1:7799",
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

@ -1,18 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Fe</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

@ -1,12 +0,0 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

@ -1,53 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

@ -1,4 +0,0 @@
/* You can add global styles to this file, and also import other style files */
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

@ -1,26 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
<T>(id: string): T;
keys(): string[];
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

@ -1,15 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

@ -1,32 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"module": "es2020",
"lib": [
"es2020",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

@ -1,18 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

@ -1,96 +0,0 @@
mod error;
use crate::{process_cmd, Args};
use actix_cors::Cors;
use actix_web::{get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder};
use error::Error;
use futures_util::StreamExt;
use rust_embed::RustEmbed;
use std::borrow::Cow;
use structopt::StructOpt;
use u_lib::{api::HttpClient, unwrap_enum};
#[derive(RustEmbed)]
#[folder = "./src/gui/fe/dist/fe/"]
struct Files;
impl Files {
pub fn get_static(path: impl AsRef<str>) -> Option<&'static [u8]> {
let file = Self::get(path.as_ref())?.data;
Some(unwrap_enum!(file, Cow::Borrowed))
}
}
async fn spa_main() -> impl Responder {
let index = Files::get_static("index.html").unwrap();
HttpResponse::Ok().body(index)
}
#[get("/core/{path}")]
async fn resources_adapter(path: web::Path<(String,)>) -> impl Responder {
let path = path.into_inner().0;
let mimetype = mime_guess::from_path(&path).first_or_octet_stream();
match Files::get_static(path) {
Some(data) => HttpResponse::Ok()
.content_type(mimetype.to_string())
.body(data),
None => HttpResponse::NotFound().finish(),
}
}
#[post("/cmd/")]
async fn send_cmd(
mut body: web::Payload,
client: web::Data<HttpClient>,
) -> Result<impl Responder, Error> {
let mut bytes = web::BytesMut::new();
while let Some(item) = body.next().await {
bytes.extend_from_slice(
&item.map_err(|e| Error::JustError(format!("payload loading failure: {e}")))?,
);
}
let cmd = String::from_utf8(bytes.to_vec())
.map_err(|_| Error::JustError("cmd contains non-utf8 data".to_string()))?;
let mut cmd = shlex::split(&cmd).ok_or(Error::JustError("argparse failed".to_string()))?;
info!("cmd: {:?}", cmd);
cmd.insert(0, String::from("u_panel"));
let parsed_cmd = Args::from_iter_safe(cmd)?;
let result = process_cmd(client.as_ref().clone(), parsed_cmd).await;
let result_string = result.to_string();
let response = if result.is_ok() {
HttpResponse::Ok().body(result_string)
} else {
HttpResponse::BadRequest().body(result_string)
};
Ok(response)
}
pub async fn serve(client: HttpClient) -> anyhow::Result<()> {
info!("Connecting to u_server...");
client.ping().await?;
let addr = "127.0.0.1:7799";
info!("Connected, instanciating u_panel at http://{}", addr);
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.wrap(Cors::permissive())
.app_data(web::Data::new(client.clone()))
.service(send_cmd)
.service(resources_adapter)
.service(web::resource("/").to(spa_main))
.service(web::resource("/{_}").to(spa_main))
})
.bind(addr)?
.run()
.await?;
Ok(())
}

@ -1,25 +1,143 @@
mod argparse; use std::env;
mod gui; use std::fmt;
use structopt::StructOpt;
use u_lib::{
api::ClientHandler, datatypes::DataResult, messaging::AsMsg, models::JobMeta, utils::init_env,
UResult,
};
use uuid::Uuid;
#[macro_use] #[derive(StructOpt, Debug)]
extern crate tracing; struct Args {
#[structopt(subcommand)]
cmd: Cmd,
#[structopt(long)]
json: bool,
}
use anyhow::Result as AnyResult; #[derive(StructOpt, Debug)]
use argparse::{process_cmd, Args}; enum Cmd {
use structopt::StructOpt; Agents(LD),
use u_lib::api::HttpClient; Jobs(JobALD),
use u_lib::config::AccessEnv; Jobmap(JobMapALD),
use u_lib::logging::init_logger; }
#[derive(StructOpt, Debug)]
enum JobALD {
Add {
#[structopt(long, parse(try_from_str = parse_uuid))]
agent: Option<Uuid>,
#[structopt(long)]
alias: String,
#[structopt(subcommand)]
cmd: JobCmd,
},
#[structopt(flatten)]
LD(LD),
}
#[derive(StructOpt, Debug)]
enum JobCmd {
#[structopt(external_subcommand)]
Cmd(Vec<String>),
}
#[actix_web::main] #[derive(StructOpt, Debug)]
async fn main() -> AnyResult<()> { enum JobMapALD {
init_logger(None); Add {
#[structopt(parse(try_from_str = parse_uuid))]
agent_uid: Uuid,
let env = AccessEnv::load()?; job_idents: Vec<String>,
let client = HttpClient::new(&env.u_server, Some(env.admin_auth_token)).await?; },
let args = Args::from_args(); List {
let result = process_cmd(client, args).await.to_string(); #[structopt(parse(try_from_str = parse_uuid))]
uid: Option<Uuid>,
},
Delete {
#[structopt(parse(try_from_str = parse_uuid))]
uid: Uuid,
},
}
#[derive(StructOpt, Debug)]
enum LD {
List {
#[structopt(parse(try_from_str = parse_uuid))]
uid: Option<Uuid>,
},
Delete {
#[structopt(parse(try_from_str = parse_uuid))]
uid: Uuid,
},
}
fn parse_uuid(src: &str) -> Result<Uuid, String> {
Uuid::parse_str(src).map_err(|e| e.to_string())
}
async fn process_cmd(args: Args) {
struct Printer {
json: bool,
}
impl Printer {
pub fn print<Msg: AsMsg + fmt::Display>(&self, data: UResult<Msg>) {
if self.json {
let data = match data {
Ok(r) => DataResult::Ok(r),
Err(e) => DataResult::Err(e),
};
println!("{}", serde_json::to_string_pretty(&data).unwrap());
} else {
match data {
Ok(r) => println!("{}", r),
Err(e) => eprintln!("Error: {}", e),
}
}
}
}
let token = env::var("ADMIN_AUTH_TOKEN").expect("Authentication token is not set");
let cli_handler = ClientHandler::new(None).password(token);
let printer = Printer { json: args.json };
match args.cmd {
Cmd::Agents(action) => match action {
LD::List { uid } => printer.print(cli_handler.get_agents(uid).await),
LD::Delete { uid } => printer.print(cli_handler.del(Some(uid)).await),
},
Cmd::Jobs(action) => match action {
JobALD::Add {
cmd: JobCmd::Cmd(cmd),
alias,
agent: _agent,
} => {
let job = JobMeta::builder()
.with_shell(cmd.join(" "))
.with_alias(alias)
.build()
.unwrap();
printer.print(cli_handler.upload_jobs(&[job]).await);
}
JobALD::LD(LD::List { uid }) => printer.print(cli_handler.get_jobs(uid).await),
JobALD::LD(LD::Delete { uid }) => printer.print(cli_handler.del(Some(uid)).await),
},
Cmd::Jobmap(action) => match action {
JobMapALD::Add {
agent_uid,
job_idents,
} => printer.print(cli_handler.set_jobs(Some(agent_uid), &job_idents).await),
JobMapALD::List { uid } => printer.print(cli_handler.get_agent_jobs(uid).await),
JobMapALD::Delete { uid } => printer.print(cli_handler.del(Some(uid)).await),
},
}
}
println!("{result}"); #[tokio::main]
Ok(()) async fn main() {
init_env();
let args: Args = Args::from_args();
process_cmd(args).await;
} }

@ -7,5 +7,5 @@ edition = "2018"
# 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]
libc = "^0.2"
nix = "0.17" nix = "0.17"
libc = "^0.2"

@ -1,29 +1,40 @@
[package] [package]
authors = ["plazmoid <kronos44@mail.ru>"] authors = ["plazmoid <kronos44@mail.ru>"]
edition = "2021" edition = "2018"
name = "u_server" name = "u_server"
version = "0.1.0" version = "0.1.0"
[dependencies] [dependencies]
anyhow = { workspace = true } log = "0.4.11"
diesel = { workspace = true } simplelog = "0.10"
deadpool-diesel = { workspace = true } thiserror = "*"
hyper = "0.14"
mime_guess = { workspace = true }
once_cell = "1.7.2"
openssl = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true, features = ["macros"] }
uuid = { workspace = true, features = ["serde", "v4"] }
u_lib = { path = "../../lib/u_lib", features = ["server"] }
warp = { version = "0.3.1", features = ["tls"] } warp = { version = "0.3.1", features = ["tls"] }
serde_qs = { version = "0.12.0", features = ["warp"] } uuid = { version = "0.6.5", features = ["serde", "v4"] }
once_cell = "1.7.2"
hyper = "0.14"
mockall = "0.9.1"
mockall_double = "0.2"
openssl = "*"
sha2 = "0.9.5"
[dependencies.diesel]
features = ["postgres", "uuid"]
version = "1.4.5"
[dependencies.serde]
features = ["derive"]
version = "1.0.114"
[dependencies.tokio]
features = ["macros"]
version = "1.9"
[dependencies.u_lib]
path = "../../lib/u_lib"
version = "*"
[dev-dependencies] [dev-dependencies]
rstest = "0.12" test-case = "1.1.0"
[lib] [lib]
name = "u_server_lib" name = "u_server_lib"

@ -1,377 +1,234 @@
use crate::error::Error; use diesel::{pg::PgConnection, prelude::*, result::Error as DslError};
use diesel::{pg::PgConnection, prelude::*, result::Error as DslError, Connection}; use once_cell::sync::OnceCell;
use std::collections::{HashMap, HashSet}; use std::{
use std::mem::drop; env,
sync::{Arc, Mutex, MutexGuard},
};
use u_lib::{ use u_lib::{
db::PgAsyncPool, models::{schema, Agent, AssignedJob, JobMeta, JobState},
models::{schema, Agent, AssignedJob, AssignedJobById, Job, JobMeta, JobState, Payload}, ULocalError, ULocalResult,
platform::Platform,
types::Id,
}; };
use uuid::Uuid; use uuid::Uuid;
type Result<T> = std::result::Result<T, Error>; pub struct UDB {
pub conn: PgConnection,
pub struct PgRepo {
pool: PgAsyncPool,
} }
impl PgRepo { static DB: OnceCell<Arc<Mutex<UDB>>> = OnceCell::new();
pub fn new(pool: PgAsyncPool) -> PgRepo {
PgRepo { pool } #[cfg_attr(test, automock)]
} impl UDB {
pub fn lock_db() -> MutexGuard<'static, UDB> {
pub async fn interact<F, R>(&self, f: F) -> Result<R> DB.get_or_init(|| {
where let _getenv = |v| env::var(v).unwrap();
F: for<'c> FnOnce(UDB<'c>) -> Result<R>, let db_host = _getenv("DB_HOST");
F: Send + 'static, let db_name = _getenv("DB_NAME");
R: Send + 'static, let db_user = _getenv("DB_USER");
{ let db_password = _getenv("DB_PASSWORD");
let connection = self.pool.get().await?; let db_url = format!(
connection "postgres://{}:{}@{}/{}",
.interact(|conn| f(UDB { conn })) db_user, db_password, db_host, db_name
.await );
.expect("deadpool interaction failed") let conn = PgConnection::establish(&db_url).unwrap();
} let instance = UDB { conn };
Arc::new(Mutex::new(instance))
pub async fn transaction<F, R>(&self, f: F) -> Result<R> })
where .lock()
F: for<'c> FnOnce(UDB<'c>) -> Result<R>, .unwrap()
F: Send + 'static, }
R: Send + 'static,
{ pub fn insert_jobs(&self, job_metas: &[JobMeta]) -> ULocalResult<()> {
let conn = self.pool.get().await?;
conn.interact(|c| c.transaction(|conn| f(UDB { conn })))
.await
.expect("deadpool interaction failed")
}
}
pub struct UDB<'c> {
conn: &'c mut PgConnection,
}
impl UDB<'_> {
pub fn insert_jobs(&mut self, jobs: &[JobMeta]) -> Result<()> {
use schema::jobs; use schema::jobs;
diesel::insert_into(jobs::table) diesel::insert_into(jobs::table)
.values(jobs) .values(job_metas)
.execute(self.conn) .execute(&self.conn)?;
.map(drop) Ok(())
.map_err(with_err_ctx("Can't insert jobs"))
}
pub fn insert_payload(&mut self, payload: &Payload) -> Result<()> {
use schema::payloads;
diesel::insert_into(payloads::table)
.values(payload)
.execute(self.conn)
.map(drop)
.map_err(with_err_ctx(format!("Can't insert payload {payload:?}")))
}
pub fn get_job(&mut self, id: Id) -> Result<Option<Job>> {
use schema::{jobs, payloads};
let maybe_job_with_payload = jobs::table
.left_join(payloads::table)
.filter(jobs::id.eq(id))
.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 { meta: job, payload }))
} }
pub fn get_jobs(&mut self) -> Result<Vec<JobMeta>> { pub fn get_jobs(&self, uid: Option<Uuid>) -> ULocalResult<Vec<JobMeta>> {
use schema::jobs; use schema::jobs;
let result = if uid.is_some() {
jobs::table jobs::table
.load(self.conn) .filter(jobs::id.eq(uid.unwrap()))
.map_err(with_err_ctx("Can't get jobs")) .get_results::<JobMeta>(&self.conn)?
} } else {
jobs::table.load::<JobMeta>(&self.conn)?
pub fn get_payload(&mut self, id: Id) -> Result<Option<Payload>> { };
use schema::payloads; Ok(result)
payloads::table
.filter(payloads::id.eq(id))
.first(self.conn)
.optional()
.map_err(with_err_ctx(format!("Can't get payload {id}")))
}
pub fn get_payload_by_name(&mut self, name: String) -> Result<Option<Payload>> {
use schema::payloads;
payloads::table
.filter(payloads::name.eq(&name))
.first(self.conn)
.optional()
.map_err(with_err_ctx(format!("Can't get payload by name {name}")))
}
pub fn get_payloads(&mut self) -> Result<Vec<Payload>> {
use schema::payloads;
payloads::table
.load(self.conn)
.map_err(with_err_ctx("Can't get payloads"))
}
pub fn payload_exists(&mut self, payload_id: Id) -> Result<bool> {
use schema::payloads;
payloads::table
.find(payload_id)
.first::<Payload>(self.conn)
.optional()
.map(|r| r.is_some())
.map_err(with_err_ctx("Can't check payload {payload_id}"))
} }
pub fn get_job_by_alias(&mut self, alias: &str) -> Result<Option<Job>> { pub fn find_job_by_alias(&self, alias: &str) -> ULocalResult<JobMeta> {
use schema::{jobs, payloads}; use schema::jobs;
let result = jobs::table
let maybe_job_with_payload = jobs::table
.left_join(payloads::table)
.filter(jobs::alias.eq(alias)) .filter(jobs::alias.eq(alias))
.first::<(JobMeta, Option<Payload>)>(self.conn) .first::<JobMeta>(&self.conn)?;
.optional() Ok(result)
.map_err(with_err_ctx(format!("Can't get job by alias {alias}")))?;
Ok(maybe_job_with_payload.map(|(job, payload_meta)| Job {
meta: job,
payload: payload_meta,
}))
}
pub fn insert_result(&mut self, result: &AssignedJob) -> Result<()> {
use schema::results;
diesel::insert_into(results::table)
.values(result)
.execute(self.conn)
.map_err(with_err_ctx(format!("Can't insert result {result:?}")))?;
Ok(())
} }
pub fn get_agent(&mut self, id: Id) -> Result<Option<Agent>> { pub fn insert_agent(&self, agent: &Agent) -> ULocalResult<()> {
use schema::agents; use schema::agents;
diesel::insert_into(agents::table)
agents::table .values(agent)
.filter(agents::id.eq(id)) .on_conflict(agents::id)
.first(self.conn) .do_update()
.optional() .set(agent)
.map_err(with_err_ctx(format!("Can't get agent {id:?}"))) .execute(&self.conn)?;
Ok(())
} }
pub fn get_agents(&mut self) -> Result<Vec<Agent>> { pub fn get_agents(&self, uid: Option<Uuid>) -> ULocalResult<Vec<Agent>> {
use schema::agents; use schema::agents;
let result = if uid.is_some() {
agents::table agents::table
.load::<Agent>(self.conn) .filter(agents::id.eq(uid.unwrap()))
.map_err(with_err_ctx(format!("Can't get agents"))) .load::<Agent>(&self.conn)?
} else {
agents::table.load::<Agent>(&self.conn)?
};
Ok(result)
} }
pub fn update_job_status(&mut self, id: Id, status: JobState) -> Result<()> { pub fn update_job_status(&self, uid: Uuid, status: JobState) -> ULocalResult<()> {
use schema::results; use schema::results;
diesel::update(results::table) diesel::update(results::table)
.filter(results::id.eq(id)) .filter(results::id.eq(uid))
.set(results::state.eq(status)) .set(results::state.eq(status))
.execute(self.conn) .execute(&self.conn)?;
.map_err(with_err_ctx(format!("Can't update status of job {id}")))?;
Ok(()) Ok(())
} }
//TODO: filters possibly could work in a wrong way, check //TODO: filters possibly could work in a wrong way, check
pub fn get_assigned_jobs( pub fn get_exact_jobs(
&mut self, &self,
id: Option<Id>, uid: Option<Uuid>,
personal: bool, personal: bool,
) -> Result<Vec<AssignedJob>> { ) -> ULocalResult<Vec<AssignedJob>> {
use schema::results; use schema::results;
let mut q = results::table.into_boxed(); let mut q = results::table.into_boxed();
/*if id.is_some() { /*if uid.is_some() {
q = q.filter(results::agent_id.eq(id.unwrap())) q = q.filter(results::agent_id.eq(uid.unwrap()))
}*/ }*/
if personal { if personal {
q = q.filter( q = q.filter(
results::state results::state
.eq(JobState::Queued) .eq(JobState::Queued)
.and(results::agent_id.eq(id.unwrap())), .and(results::agent_id.eq(uid.unwrap())),
) )
} else if id.is_some() { } else if uid.is_some() {
q = q q = q
.filter(results::agent_id.eq(id.unwrap())) .filter(results::agent_id.eq(uid.unwrap()))
.or_filter(results::job_id.eq(id.unwrap())) .or_filter(results::job_id.eq(uid.unwrap()))
.or_filter(results::id.eq(id.unwrap())) .or_filter(results::id.eq(uid.unwrap()))
} }
let result = q let result = q.load::<AssignedJob>(&self.conn)?;
.load::<AssignedJob>(self.conn)
.map_err(with_err_ctx("Can't get exact jobs"))?;
Ok(result) Ok(result)
} }
// todo: move to handlers pub fn set_jobs_for_agent(
pub fn assign_jobs(&mut self, assigned_jobs: &[AssignedJobById]) -> Result<()> { &self,
use schema::{jobs, results}; agent_uid: &Uuid,
job_uids: &[Uuid],
struct JobBriefMeta { ) -> ULocalResult<Vec<Uuid>> {
alias: Option<String>, use schema::{agents::dsl::agents, jobs::dsl::jobs, results};
target_platform: String, if let Err(DslError::NotFound) = agents.find(agent_uid).first::<Agent>(&self.conn) {
return Err(ULocalError::NotFound(agent_uid.to_string()));
} }
let not_found_jobs = job_uids
let assigned_job_ids = HashSet::<Uuid>::from_iter(assigned_jobs.iter().map(|a| a.job_id)); .iter()
.filter_map(|job_uid| {
let jobs_meta = HashMap::<Id, JobBriefMeta>::from_iter( if let Err(DslError::NotFound) = jobs.find(job_uid).first::<JobMeta>(&self.conn) {
jobs::table Some(job_uid.to_string())
.select((jobs::id, jobs::alias, jobs::target_platforms)) } else {
.filter(jobs::id.eq_any(&assigned_job_ids)) None
.load::<(Id, Option<String>, String)>(self.conn)
.map_err(with_err_ctx(format!(
"Can't find jobs {:?}",
assigned_job_ids
)))?
.into_iter()
.map(|(id, alias, target_platform)| {
(
id,
JobBriefMeta {
alias,
target_platform,
},
)
}),
);
let existing_job_ids = HashSet::from_iter(jobs_meta.keys().copied());
if assigned_job_ids != existing_job_ids {
return Err(Error::ProcessingError(format!(
"Jobs not found: {:?}",
assigned_job_ids.difference(&existing_job_ids),
)));
}
for ajob in assigned_jobs {
let meta = &jobs_meta[&ajob.job_id];
let agent_platform = match self.get_agent(ajob.agent_id)? {
Some(agent) => Platform::new(&agent.platform),
None => {
return Err(Error::ProcessingError(format!(
"Agent {} not found",
ajob.agent_id
)))
} }
}; })
if !agent_platform.matches(&meta.target_platform) { .collect::<Vec<String>>();
return Err(Error::InsuitablePlatform( if not_found_jobs.len() > 0 {
agent_platform.into_string(), return Err(ULocalError::NotFound(not_found_jobs.join(", ")));
meta.target_platform.clone(),
));
}
} }
let job_requests = job_uids
let job_requests = assigned_jobs .iter()
.into_iter() .map(|job_uid| {
.map(|a| AssignedJob { info!("set_jobs_for_agent: set {} for {}", job_uid, agent_uid);
job_id: a.job_id, AssignedJob {
agent_id: a.agent_id, job_id: *job_uid,
alias: jobs_meta[&a.job_id].alias.clone(), agent_id: *agent_uid,
..Default::default() ..Default::default()
}
}) })
.collect::<Vec<AssignedJob>>(); .collect::<Vec<AssignedJob>>();
diesel::insert_into(results::table) diesel::insert_into(results::table)
.values(&job_requests) .values(&job_requests)
.execute(self.conn) .execute(&self.conn)?;
.map(drop) let assigned_uids = job_requests.iter().map(|aj| aj.id).collect();
.map_err(with_err_ctx("Can't assign jobs")) Ok(assigned_uids)
} }
pub fn del_jobs(&mut self, ids: &[Id]) -> Result<()> { pub fn del_jobs(&self, uids: &Vec<Uuid>) -> ULocalResult<usize> {
use schema::jobs; use schema::jobs;
let mut affected = 0;
diesel::delete(jobs::table) for &uid in uids {
.filter(jobs::id.eq_any(ids)) let deleted = diesel::delete(jobs::table)
.execute(self.conn) .filter(jobs::id.eq(uid))
.map(drop) .execute(&self.conn)?;
.map_err(with_err_ctx("Can't delete jobs")) affected += deleted;
}
Ok(affected)
} }
pub fn del_results(&mut self, ids: &[Id]) -> Result<()> { pub fn del_results(&self, uids: &Vec<Uuid>) -> ULocalResult<usize> {
use schema::results; use schema::results;
let mut affected = 0;
diesel::delete(results::table) for &uid in uids {
.filter(results::id.eq_any(ids)) let deleted = diesel::delete(results::table)
.execute(self.conn) .filter(results::id.eq(uid))
.map(drop) .execute(&self.conn)?;
.map_err(with_err_ctx("Can't delete results")) affected += deleted;
} }
Ok(affected)
pub fn del_agents(&mut self, ids: &[Id]) -> Result<()> {
use schema::agents;
diesel::delete(agents::table)
.filter(agents::id.eq_any(ids))
.execute(self.conn)
.map(drop)
.map_err(with_err_ctx("Can't delete agents"))
}
pub fn del_payloads(&mut self, ids: &[Id]) -> Result<()> {
use schema::payloads;
diesel::delete(payloads::table)
.filter(payloads::id.eq_any(ids))
.execute(self.conn)
.map(drop)
.map_err(with_err_ctx("Can't delete payloads"))
} }
pub fn upsert_agent(&mut self, agent: &Agent) -> Result<()> { pub fn del_agents(&self, uids: &Vec<Uuid>) -> ULocalResult<usize> {
use schema::agents; use schema::agents;
let mut affected = 0;
diesel::insert_into(agents::table) for &uid in uids {
.values(agent) let deleted = diesel::delete(agents::table)
.on_conflict(agents::id) .filter(agents::id.eq(uid))
.do_update() .execute(&self.conn)?;
.set(agent) affected += deleted;
.execute(self.conn) }
.map_err(with_err_ctx(format!("Can't insert agent {agent:?}")))?; Ok(affected)
Ok(())
}
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(())
}
pub fn update_payload(&mut self, payload: &Payload) -> Result<()> {
payload
.save_changes::<Payload>(self.conn)
.map_err(with_err_ctx(format!("Can't update payload {payload:?}")))?;
Ok(())
}
pub fn update_result(&mut self, result: &AssignedJob) -> Result<()> {
debug!(
"updating result: id = {}, job_id = {}, agent_id = {}",
result.id, result.job_id, result.agent_id
);
result
.save_changes::<AssignedJob>(self.conn)
.map_err(with_err_ctx(format!("Can't update result {result:?}")))?;
Ok(())
} }
} }
/*
fn with_err_ctx(msg: impl AsRef<str>) -> impl Fn(DslError) -> Error { #[cfg(test)]
move |err| Error::DBErrorCtx(format!("{}, reason: {err}", msg.as_ref())) 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,98 +0,0 @@
use diesel::result::Error as DslError;
use thiserror::Error;
use u_lib::{ufs, UError};
use warp::{
http::StatusCode,
reject::Reject,
reply::{with_status, Response},
Reply,
};
#[derive(Error, Debug)]
pub enum Error {
#[error("Configs error: {0}")]
ConfigError(#[from] u_lib::config::Error),
#[error("Processing error: {0}")]
ProcessingError(String),
#[error(transparent)]
DBError(#[from] DslError),
#[error("DB error: {0}")]
DBErrorCtx(String),
#[error("Deadpool error: {0}")]
DeadpoolError(#[from] deadpool_diesel::PoolError),
#[error(transparent)]
FSError(#[from] ufs::Error),
#[error("Job cannot be ran on this platform. Expected: {0}, got: {1}")]
InsuitablePlatform(String, String),
#[error("{0}\nContext: {1}")]
Contexted(Box<Error>, String),
#[error(transparent)]
UError(#[from] UError),
#[error("Runtime error: {0}")]
Runtime(String),
}
impl Reject for Error {}
pub struct RejResponse {
message: String,
status: StatusCode,
}
impl RejResponse {
pub fn not_found(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
status: StatusCode::NOT_FOUND,
}
}
pub fn bad_request(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
status: StatusCode::BAD_REQUEST,
}
}
pub fn internal() -> Self {
Self {
message: "INTERNAL SERVER ERROR".to_string(),
status: StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl Reply for RejResponse {
fn into_response(self) -> Response {
with_status(self.message, self.status).into_response()
}
}
impl From<anyhow::Error> for Error {
fn from(e: anyhow::Error) -> Self {
let ctx = e
.chain()
.rev()
.skip(1)
.map(|cause| format!("ctx: {}", cause))
.collect::<Vec<_>>()
.join("\n");
match e.downcast::<Error>() {
Ok(err) => Error::Contexted(Box::new(err), ctx),
Err(err) => match err.downcast::<ufs::Error>() {
Ok(err) => Error::Contexted(Box::new(Error::FSError(err)), ctx),
Err(err) => Error::Runtime(err.to_string()),
},
}
}
}

@ -0,0 +1,103 @@
use crate::handlers::Endpoints;
use serde::de::DeserializeOwned;
use std::convert::Infallible;
use std::env;
use u_lib::{
messaging::{AsMsg, BaseMessage},
models::*,
};
use uuid::Uuid;
use warp::{body, Filter, Rejection, Reply};
fn get_content<M>() -> impl Filter<Extract = (BaseMessage<'static, M>,), Error = Rejection> + Clone
where
M: AsMsg + Sync + Send + DeserializeOwned + 'static,
{
body::content_length_limit(1024 * 64).and(body::json::<BaseMessage<M>>())
}
pub fn make_filters() -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
let infallible_none = |_| async { Ok::<(Option<Uuid>,), Infallible>((None,)) };
let get_agents = warp::get()
.and(warp::path("get_agents"))
.and(
warp::path::param::<Uuid>()
.map(Some)
.or_else(infallible_none),
)
.and_then(Endpoints::get_agents);
let upload_jobs = warp::post()
.and(warp::path("upload_jobs"))
.and(get_content::<Vec<JobMeta>>())
.and_then(Endpoints::upload_jobs);
let get_jobs = warp::get()
.and(warp::path("get_jobs"))
.and(
warp::path::param::<Uuid>()
.map(Some)
.or_else(infallible_none),
)
.and_then(Endpoints::get_jobs);
let get_agent_jobs = warp::get()
.and(warp::path("get_agent_jobs"))
.and(
warp::path::param::<Uuid>()
.map(Some)
.or_else(infallible_none),
)
.and_then(|uid| Endpoints::get_agent_jobs(uid));
let get_personal_jobs = warp::get()
.and(warp::path("get_personal_jobs"))
.and(warp::path::param::<Uuid>().map(Some))
.and_then(|uid| Endpoints::get_personal_jobs(uid));
let del = warp::get()
.and(warp::path("del"))
.and(warp::path::param::<Uuid>())
.and_then(Endpoints::del);
let set_jobs = warp::post()
.and(warp::path("set_jobs"))
.and(warp::path::param::<Uuid>())
.and(get_content::<Vec<String>>())
.and_then(Endpoints::set_jobs);
let report = warp::post()
.and(warp::path("report"))
.and(get_content::<Vec<ExecResult>>())
.and_then(Endpoints::report);
let download = warp::get()
.and(warp::path("dl"))
.and(warp::path::param::<Uuid>())
.and_then(Endpoints::download);
let download_request = warp::post()
.and(warp::path("dlr"))
.and(get_content::<String>())
.and_then(Endpoints::download_request);
let auth_token = format!("Bearer {}", env::var("ADMIN_AUTH_TOKEN").unwrap()).into_boxed_str();
let auth_header = warp::header::exact("authorization", Box::leak(auth_token));
let auth_zone = (get_agents
.or(get_jobs)
.or(upload_jobs)
.or(del)
.or(set_jobs)
.or(get_agent_jobs))
.and(auth_header);
let agent_zone = get_jobs
.or(get_personal_jobs)
.or(report)
.or(download)
.or(download_request);
auth_zone.or(agent_zone)
}

@ -1,343 +1,197 @@
use std::sync::Arc; use crate::{
db::UDB,
storages::{FileIndex, Mapper},
};
use diesel::SaveChangesDsl;
use hyper::Body;
use serde::Serialize;
use u_lib::{
messaging::{AsMsg, BaseMessage, DownloadInfo},
models::{Agent, AgentState, AssignedJob, ExecResult, JobMeta, JobState},
ULocalError,
};
use uuid::Uuid;
use warp::{
http::{Response, StatusCode},
Rejection, Reply,
};
pub fn build_response<S: Into<Body>>(code: StatusCode, body: S) -> Response<Body> {
Response::builder().status(code).body(body.into()).unwrap()
}
use crate::db::{PgRepo, UDB}; pub fn build_ok<S: Into<Body>>(body: S) -> Response<Body> {
use crate::error::Error; build_response(StatusCode::OK, body)
use serde::Deserialize; }
use u_lib::{api::api_types, messaging::Reportable, models::*, types::Id};
use warp::reject::not_found;
use warp::Rejection;
type EndpResult<T> = Result<T, Rejection>; pub fn build_err<S: ToString>(body: S) -> Response<Body> {
build_response(StatusCode::BAD_REQUEST, body.to_string())
}
pub fn build_not_found() -> Response<Body> {
build_response(StatusCode::NOT_FOUND, "")
}
#[derive(Deserialize)] pub fn build_message<M: AsMsg + Serialize>(m: M) -> Response<Body> {
pub struct PayloadFlags { warp::reply::json(&m.as_message()).into_response()
brief: Brief,
} }
pub struct Endpoints; pub struct Endpoints;
#[cfg_attr(test, automock)]
impl Endpoints { impl Endpoints {
pub async fn get_agents(repo: Arc<PgRepo>, id: Option<Id>) -> EndpResult<api_types::GetAgents> { pub async fn add_agent(msg: Agent) -> Result<Response<Body>, Rejection> {
repo.interact(move |mut db| { info!("hnd: add_agent");
Ok(match id { UDB::lock_db()
Some(id) => { .insert_agent(&msg)
if let Some(agent) = db.get_agent(id)? { .map(|_| build_ok(""))
vec![agent] .or_else(|e| Ok(build_err(e)))
} else { }
vec![]
} pub async fn get_agents(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> {
} info!("hnd: get_agents");
None => db.get_agents()?, UDB::lock_db()
}) .get_agents(uid)
}) .map(|m| build_message(m))
.await .or_else(|e| Ok(build_err(e)))
.map_err(From::from) }
}
pub async fn get_jobs(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> {
pub async fn get_job( info!("hnd: get_jobs");
repo: Arc<PgRepo>, UDB::lock_db()
id: Id, .get_jobs(uid)
params: Option<PayloadFlags>, .map(|m| build_message(m))
) -> EndpResult<api_types::GetJob> { .or_else(|e| Ok(build_err(e)))
let Some(mut job) = repo.interact(move |mut db| db.get_job(id)).await? else { }
return Err(not_found());
}; pub async fn get_agent_jobs(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> {
info!("hnd: get_agent_jobs");
Ok(match params.map(|p| p.brief) { UDB::lock_db()
Some(Brief::Yes) => job, .get_exact_jobs(uid, false)
Some(Brief::Auto) | None => { .map(|m| build_message(m))
if let Some(payload) = &mut job.payload { .or_else(|e| Ok(build_err(e)))
payload.maybe_join_payload().map_err(Error::from)?; }
}
job pub async fn get_personal_jobs(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> {
info!("hnd: get_personal_jobs");
let agents = UDB::lock_db().get_agents(uid).unwrap();
if agents.len() == 0 {
let db = UDB::lock_db();
db.insert_agent(&Agent::with_id(uid.unwrap())).unwrap();
let job = db.find_job_by_alias("agent_hello").unwrap();
if let Err(e) = db.set_jobs_for_agent(&uid.unwrap(), &[job.id]) {
return Ok(build_err(e));
} }
Some(Brief::No) => { }
if let Some(payload) = &mut job.payload { let result = UDB::lock_db().get_exact_jobs(uid, true);
payload.join_payload().map_err(Error::from)?; match result {
} Ok(r) => {
job let db = UDB::lock_db();
} for j in r.iter() {
}) db.update_job_status(j.id, JobState::Running).unwrap();
}
pub async fn get_jobs(repo: Arc<PgRepo>) -> EndpResult<api_types::GetJobs> {
repo.interact(move |mut db| db.get_jobs())
.await
.map_err(From::from)
}
pub async fn get_assigned_jobs(
repo: Arc<PgRepo>,
id: Option<Id>,
) -> EndpResult<api_types::GetAgentJobs> {
repo.interact(move |mut db| db.get_assigned_jobs(id, false))
.await
.map_err(From::from)
}
pub async fn get_payloads(repo: Arc<PgRepo>) -> EndpResult<api_types::GetPayloads> {
repo.interact(move |mut db| db.get_payloads())
.await
.map_err(From::from)
}
pub async fn get_payload(
repo: Arc<PgRepo>,
name_or_id: String,
params: Option<PayloadFlags>,
) -> EndpResult<api_types::GetPayload> {
let mut payload = match repo
.interact(move |mut db| match Id::parse_str(&name_or_id) {
Ok(id) => db.get_payload(id),
Err(_) => db.get_payload_by_name(name_or_id),
})
.await?
{
Some(p) => p,
None => return Err(not_found()),
};
Ok(match params.map(|p| p.brief) {
Some(Brief::Yes) => {
payload.data = None;
payload
}
None | Some(Brief::Auto) => {
payload.maybe_join_payload().map_err(Error::from)?;
payload
}
_ => {
payload.join_payload().map_err(Error::from)?;
payload
}
})
}
pub async fn get_personal_jobs(
repo: Arc<PgRepo>,
agent_id: Id,
) -> EndpResult<api_types::GetPersonalJobs> {
repo.transaction(move |mut db| {
let agent = db.get_agent(agent_id)?;
match agent {
Some(mut agent) => {
agent.touch();
db.upsert_agent(&agent)?;
}
None => {
let mut new_agent = Agent::empty();
new_agent.id = agent_id;
db.upsert_agent(&new_agent)?;
let job = db
.get_job_by_alias("agent_hello")?
.expect("agent_hello job not found");
let assigned_job = AssignedJobById {
agent_id,
job_id: job.meta.id,
..Default::default()
};
db.assign_jobs(&[assigned_job])?;
} }
Ok(build_message(r))
} }
Err(e) => Ok(build_err(e)),
let assigned_jobs = db.get_assigned_jobs(Some(agent_id), true)?; }
for job in &assigned_jobs {
db.update_job_status(job.id, JobState::Running)?;
}
Ok(assigned_jobs
.into_iter()
.map(|j| AssignedJobById::from(&j))
.collect())
})
.await
.map_err(From::from)
} }
pub async fn upload_jobs( pub async fn upload_jobs(
repo: Arc<PgRepo>, msg: BaseMessage<'static, Vec<JobMeta>>,
jobs: Vec<Job>, ) -> Result<Response<Body>, Rejection> {
) -> EndpResult<api_types::UploadJobs> { info!("hnd: upload_jobs");
let mut checked_jobs = vec![]; UDB::lock_db()
for mut job in jobs { .insert_jobs(&msg.into_inner())
if let Some(payload) = &mut job.payload { .map(|_| build_ok(""))
payload.maybe_split_payload().map_err(Error::from)?; .or_else(|e| Ok(build_err(e)))
} else if let Some(pld_id) = job.meta.payload_id { }
if !repo
.interact(move |mut db| db.payload_exists(pld_id)) pub async fn del(uid: Uuid) -> Result<Response<Body>, Rejection> {
.await? info!("hnd: del");
{ let db = UDB::lock_db();
Err(Error::ProcessingError(format!( let del_fns = &[UDB::del_agents, UDB::del_jobs, UDB::del_results];
"Payload {pld_id} not found" for del_fn in del_fns {
)))? let affected = del_fn(&db, &vec![uid]).unwrap();
} if affected > 0 {
return Ok(build_message(affected as i32));
} }
checked_jobs.push(job)
} }
Ok(build_message(0))
let (jobs, payloads_opt): (Vec<_>, Vec<_>) = checked_jobs
.into_iter()
.map(|j| (j.meta, j.payload))
.unzip();
let payloads = payloads_opt
.into_iter()
.filter_map(|p| p)
.collect::<Vec<_>>();
repo.transaction(move |mut db| {
for payload in payloads {
db.insert_payload(&payload)?;
}
db.insert_jobs(&jobs)
})
.await
.map_err(From::from)
}
pub async fn upload_payload(
repo: Arc<PgRepo>,
raw_payload: RawPayload,
) -> EndpResult<api_types::UploadPayloads> {
let payloads = raw_payload.into_payload().map_err(Error::from)?;
repo.interact(move |mut db| db.insert_payload(&payloads))
.await
.map_err(From::from)
}
pub async fn del(repo: Arc<PgRepo>, id: Id) -> EndpResult<()> {
repo.transaction(move |mut db| {
[
UDB::del_agents,
UDB::del_jobs,
UDB::del_results,
UDB::del_payloads,
]
.iter()
.map(|f| f(&mut db, &[id]))
.collect::<Result<(), Error>>()
})
.await
.map_err(From::from)
} }
pub async fn assign_jobs( pub async fn set_jobs(
repo: Arc<PgRepo>, agent_uid: Uuid,
assigned_jobs: Vec<AssignedJobById>, msg: BaseMessage<'static, Vec<String>>,
) -> EndpResult<()> { ) -> Result<Response<Body>, Rejection> {
repo.transaction(move |mut db| { info!("hnd: set_jobs_by_alias, agent: {}", agent_uid);
db.assign_jobs(&assigned_jobs)?; let jobs: Result<Vec<Uuid>, ULocalError> = msg
Ok(()) .into_inner()
}) .into_iter()
.await .map(|ident| {
.map_err(From::from) info!("hnd: set_jobs_by_alias, job: {}", ident);
Uuid::parse_str(&ident)
.or_else(|_| UDB::lock_db().find_job_by_alias(&ident).map(|j| j.id))
})
.collect();
match jobs {
Ok(j) => UDB::lock_db()
.set_jobs_for_agent(&agent_uid, &j)
.map(|assigned_uids| build_message(assigned_uids))
.or_else(|e| Ok(build_err(e))),
Err(e) => Ok(build_err(e)),
}
} }
pub async fn report( pub async fn report(
repo: Arc<PgRepo>, msg: BaseMessage<'static, Vec<ExecResult>>,
msg: Vec<Reportable>, ) -> Result<Response<Body>, Rejection> {
agent_id: Id, info!("hnd: report");
) -> EndpResult<api_types::Report> { let id = msg.id;
repo.transaction(move |mut db| { let mut failed = vec![];
for entry in msg { for entry in msg.into_inner() {
match entry { match entry {
Reportable::Assigned(mut result) => { ExecResult::Assigned(res) => {
let result_agent_id = &result.agent_id; if id != res.agent_id {
if agent_id != *result_agent_id { continue;
warn!(
"Agent ids are not equal! actual id: {agent_id}, \
id from job: {result_agent_id}"
);
continue;
}
result.touch();
info!("agent {agent_id} updated job {}", result.id);
match result.exec_type {
JobType::Init => {
result.state = JobState::Finished;
let mut agent: Agent = match result.deserialize() {
Ok(a) => a,
Err(e) => {
error!(
"Error deserializing agent \
data from {agent_id}: {e}"
);
continue;
}
};
agent.state = AgentState::Active;
db.upsert_agent(&agent)?;
}
JobType::Shell => result.state = JobState::Finished,
JobType::Stats => result.state = JobState::Finished,
JobType::Terminate => (),
JobType::Update => (),
}
db.update_result(&result)?;
} }
Reportable::Error(e) => { let db = UDB::lock_db();
error!("agent {agent_id} reported: {e}"); if let Err(e) = res
.save_changes::<AssignedJob>(&db.conn)
.map_err(ULocalError::from)
{
failed.push(e.to_string())
} }
Reportable::Dummy => (),
} }
ExecResult::Agent(mut a) => {
a.state = AgentState::Active;
Self::add_agent(a).await?;
}
ExecResult::Dummy => (),
} }
Ok(()) }
}) if failed.len() > 0 {
.await let err_msg = ULocalError::ProcessingError(failed.join(", "));
.map_err(From::from) return Ok(build_err(err_msg));
} }
Ok(build_ok(""))
pub async fn update_agent(
repo: Arc<PgRepo>,
agent: Agent,
) -> EndpResult<api_types::UpdateAgent> {
repo.interact(move |mut db| db.upsert_agent(&agent))
.await
.map_err(From::from)
}
pub async fn update_job(repo: Arc<PgRepo>, job: JobMeta) -> EndpResult<api_types::UpdateJob> {
repo.interact(move |mut db| db.update_job(&job.validate()?))
.await
.map_err(From::from)
} }
pub async fn update_assigned_job( pub async fn download_request(
repo: Arc<PgRepo>, msg: BaseMessage<'static, String>,
assigned: AssignedJob, ) -> Result<Response<Body>, Rejection> {
) -> EndpResult<api_types::UpdateResult> { let agent_id = msg.id;
repo.interact(move |mut db| db.update_result(&assigned)) FileIndex::
.await
.map_err(From::from)
} }
pub async fn update_payload( pub async fn download(uid: Uuid) -> Result<Response<Body>, Rejection> {
repo: Arc<PgRepo>, let data = match Mapper::get(uid) {
payload: Payload, Some(mut r) => {
) -> EndpResult<api_types::UpdatePayload> { let mut buf = vec![];
debug!("update payload: {payload:?}"); r.read_to_end(&mut buf).unwrap();
match payload.data { build_ok(buf)
Some(data) => {
let mut well_formed_payload =
Payload::from_data(data, Some(payload.name)).map_err(Error::from)?;
well_formed_payload.id = payload.id;
repo.interact(move |mut db| db.update_payload(&well_formed_payload))
.await
.map_err(From::from)
} }
None => repo None => build_not_found(),
.interact(move |mut db| db.update_payload(&payload)) };
.await Ok(data)
.map_err(From::from),
}
} }
} }

@ -1,17 +1,6 @@
// due to linking errors use u_server_lib::serve;
extern crate openssl;
// don't touch anything
extern crate diesel;
// in this block
#[macro_use]
extern crate tracing;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
u_lib::logging::init_logger(Some("u_server")); serve().await;
if let Err(e) = u_server_lib::serve().await {
error!("U_SERVER error: {}", e);
}
} }

@ -0,0 +1,65 @@
use crate::worker::Worker;
use once_cell::sync::Lazy;
use sha2::{Digest, Sha512};
use std::fs;
use std::path::PathBuf;
use u_lib::utils::{Hexlify, SharedStorage};
pub struct FileMeta {
hashsum: String,
path: PathBuf,
name: String,
}
static FILES: Lazy<SharedStorage<String, FileMeta>> = SharedStorage::new();
pub struct FileIndex;
impl FileIndex {
pub fn search_by_name(pat: impl Into<String>) -> Option<String> {
let name = pat.into();
let (key, _) = FILES.lock().iter().find(|(_, v)| v.name == name)?;
Some(*key)
}
pub fn get(key: impl Into<String>) -> Option<FileMeta> {
let name = key.into();
FILES.get(&name).and_then(|r| Some(*r))
}
}
impl Worker for FileIndex {
fn process(&mut self) {
let hexlify = |data: &[u8]| format!("{:x}", Hexlify(data));
let mut hasher = Sha512::new();
for file in fs::read_dir("./files/hosted/").unwrap() {
let file = match file {
Ok(f) => f.path(),
Err(e) => {
warn!("Error reading file: {}", e);
continue;
}
};
if file.is_file() {
let file_data = fs::read(file).unwrap();
hasher.update(file_data);
let hashsum = hasher.finalize_reset();
let shrinked_sum = hashsum
.iter()
.cloned()
.take(32)
.map(|b| b ^ 0x5a)
.collect::<Vec<u8>>();
let shrinked_sum = hexlify(&shrinked_sum);
let hashsum = hexlify(&hashsum);
let name = file.file_name().unwrap().to_string_lossy().into_owned();
let meta = FileMeta {
hashsum,
name,
path: file,
};
FILES.lock().insert(shrinked_sum, meta);
}
}
}
}

@ -0,0 +1,78 @@
use once_cell::sync::Lazy;
use std::collections::HashSet;
use std::io::Read;
use std::iter::Iterator;
use std::time::{Duration, SystemTime};
use u_lib::utils::SharedStorage;
use uuid::Uuid;
type Data = Box<dyn Read + Send + Sync>;
/// This struct holds uids of various readable objects (files, streams, etc...)
/// with timeout and permissions for any uids to read data
struct MappedData {
//ids that allowed to read the data
for_ids: HashSet<Uuid>,
created: SystemTime,
timeout: Duration,
data: Data,
}
impl MappedData {
pub fn new(
ids: impl Iterator<Item = Uuid>,
data: impl Read + Send + Sync + 'static,
timeout: Duration,
) -> Self {
MappedData {
for_ids: ids.collect(),
created: SystemTime::now(),
timeout,
data: Box::new(data),
}
}
}
static MAPPER: Lazy<SharedStorage<Uuid, MappedData>> = SharedStorage::new();
pub struct Mapper;
impl Mapper {
fn remove_overdue() {
MAPPER
.lock()
.retain(|_, v| v.created.elapsed().unwrap() < v.timeout);
}
pub fn get(item_uid: Uuid, getter_uid: Uuid) -> Option<Data> {
Self::remove_overdue();
let allowed_ids = MAPPER.lock().get(&item_uid)?.for_ids;
if allowed_ids.contains(&getter_uid) {
MAPPER.lock().remove(&item_uid).map(|d| d.data)
} else {
None
}
}
pub fn set(
for_ids: impl Iterator<Item = Uuid>,
data: impl Read + Send + Sync + 'static,
timeout: Option<Duration>,
) -> Uuid {
Self::remove_overdue();
let uid = Uuid::new_v4();
let timeout = timeout.unwrap_or(Duration::from_secs(60));
let mapped = MappedData::new(for_ids, data, timeout);
MAPPER.lock().insert(uid, mapped);
uid
}
}
// init:
// fill index struct with files allowed to download (probably auto-refresh index on file adding)
// hashmap<stripped_hash_of_file_name, PathBuf>
// download:
// 1) agent sends download_request with hash of wanted file
// 2) handler checks if this agent is allowed to dl file
// 3) if all ok, create in entry in mapper and return its uid and hashsum to agent
// 4) agent downloads file by uid, uid destructed after that

@ -0,0 +1,5 @@
pub mod files;
pub mod mapper;
pub use files::FileIndex;
pub use mapper::Mapper;

@ -1,300 +1,136 @@
#[macro_use] #[macro_use]
extern crate tracing; extern crate log;
#[macro_use]
extern crate mockall;
#[macro_use]
extern crate mockall_double;
// because of linking errors
extern crate openssl;
#[macro_use]
extern crate diesel;
//
mod db; mod db;
mod error; mod filters;
mod handlers; mod handlers;
mod storages;
use crate::handlers::{Endpoints, PayloadFlags}; mod worker;
use db::PgRepo;
use error::{Error as ServerError, RejResponse}; use db::UDB;
use std::{convert::Infallible, sync::Arc}; use filters::make_filters;
use u_lib::{ use std::thread;
config, use std::time::Duration;
db::async_pool, use storages::FileIndex;
messaging::{AsMsg, Reportable}, use u_lib::{config::MASTER_PORT, models::*, utils::init_env};
models::*, use warp::Filter;
types::Id, use worker::Worker;
};
use warp::{ fn init_workers() {
body, let workers: Vec<Box<dyn Worker + Send>> = vec![Box::new(FileIndex)];
log::{custom, Info}, thread::spawn(move || loop {
reply::{json, Json, Response}, workers.into_iter().for_each(|w| w.process());
Filter, Rejection, Reply, thread::sleep(Duration::from_secs(5));
}; });
const DEFAULT_RESPONSE: &str = "null";
fn into_message<M: AsMsg>(msg: M) -> Json {
json(&msg)
} }
pub fn init_endpoints( fn prefill_jobs() {
auth_token: &str, let agent_hello = JobMeta::builder()
db: PgRepo, .with_type(misc::JobType::Manage)
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone { .with_alias("agent_hello")
fn make_optional<T>( .build()
f: impl Filter<Extract = (T,), Error = Rejection> + Clone, .unwrap();
) -> impl Filter<Extract = (Option<T>,), Error = Infallible> + Clone { let update_agent = JobMeta::builder()
f.map(Some) .with_type(misc::JobType::Update)
.or_else(|_| async { Ok::<(Option<T>,), Infallible>((None,)) }) .with_alias("update_agent")
} .build()
.unwrap();
let path = |p: &'static str| warp::post().and(warp::path(p)); let all = [agent_hello, update_agent];
let create_qs_cfg = || serde_qs::Config::new(1, true); UDB::lock_db().insert_jobs(&all).ok();
let with_db = {
let adb = Arc::new(db);
warp::any().map(move || adb.clone())
};
let get_agents = path("get_agents")
.and(with_db.clone())
.and(make_optional(warp::path::param::<Id>()))
.and_then(Endpoints::get_agents)
.map(into_message);
let upload_jobs = path("upload_jobs")
.and(with_db.clone())
.and(body::json::<Vec<Job>>())
.and_then(Endpoints::upload_jobs)
.map(into_message);
let get_job = path("get_job")
.and(with_db.clone())
.and(warp::path::param::<Id>())
.and(make_optional(serde_qs::warp::query::<PayloadFlags>(
create_qs_cfg(),
)))
.and_then(Endpoints::get_job)
.map(into_message);
let get_jobs = path("get_jobs")
.and(with_db.clone())
.and_then(Endpoints::get_jobs)
.map(into_message);
let get_assigned_jobs = path("get_assigned_jobs")
.and(with_db.clone())
.and(make_optional(warp::path::param::<Id>()))
.and_then(Endpoints::get_assigned_jobs)
.map(into_message);
let get_personal_jobs = path("get_personal_jobs")
.and(with_db.clone())
.and(warp::path::param::<Id>())
.and_then(Endpoints::get_personal_jobs)
.map(into_message);
let del = path("del")
.and(with_db.clone())
.and(warp::path::param::<Id>())
.and_then(Endpoints::del)
.map(ok);
let assign_jobs = path("assign_jobs")
.and(with_db.clone())
.and(body::json::<Vec<AssignedJobById>>())
.and_then(Endpoints::assign_jobs)
.map(into_message);
let report = path("report")
.and(with_db.clone())
.and(body::json::<Vec<Reportable>>())
.and(warp::header("User-Agent"))
.and_then(Endpoints::report)
.map(ok);
let update_agent = path("update_agent")
.and(with_db.clone())
.and(body::json::<Agent>())
.and_then(Endpoints::update_agent)
.map(ok);
let update_job = path("update_job")
.and(with_db.clone())
.and(body::json::<JobMeta>())
.and_then(Endpoints::update_job)
.map(ok);
let update_assigned_job = path("update_result")
.and(with_db.clone())
.and(body::json::<AssignedJob>())
.and_then(Endpoints::update_assigned_job)
.map(ok);
let get_payloads = path("get_payloads")
.and(with_db.clone())
.and_then(Endpoints::get_payloads)
.map(into_message);
let get_payload = path("get_payload")
.and(with_db.clone())
.and(warp::path::param::<String>())
.and(make_optional(serde_qs::warp::query::<PayloadFlags>(
create_qs_cfg(),
)))
.and_then(Endpoints::get_payload)
.map(into_message);
let upload_payload = path("upload_payload")
.and(with_db.clone())
.and(body::json::<RawPayload>())
.and_then(Endpoints::upload_payload)
.map(ok);
let update_payload = path("update_payload")
.and(with_db.clone())
.and(body::json::<Payload>())
.and_then(Endpoints::update_payload)
.map(ok);
let ping = path("ping").map(|| DEFAULT_RESPONSE);
let auth_token = format!("Bearer {auth_token}",).into_boxed_str();
let auth_header = warp::header::exact("authorization", Box::leak(auth_token));
let auth_zone = (get_agents
.or(get_job.clone())
.or(get_jobs.clone())
.or(get_payloads)
.or(get_payload)
.or(upload_jobs)
.or(upload_payload)
.or(del)
.or(assign_jobs)
.or(get_assigned_jobs)
.or(update_agent)
.or(update_job)
.or(update_assigned_job)
.or(update_payload)
.or(ping))
.and(auth_header);
let agent_zone = get_job.or(get_jobs).or(get_personal_jobs).or(report);
auth_zone.or(agent_zone)
} }
pub async fn preload_jobs(repo: &PgRepo) -> Result<(), ServerError> { fn init_logger() {
repo.interact(|mut db| { use simplelog::*;
let job_alias = "agent_hello"; use std::fs::OpenOptions;
let if_job_exists = db.get_job_by_alias(job_alias)?; const LOGFILE: &str = "u_server.log";
if if_job_exists.is_none() { let log_cfg = ConfigBuilder::new()
let agent_hello = RawJob::default() .set_time_format_str("%x %X")
.with_type(JobType::Init) .set_time_to_local(true)
.with_alias(job_alias) .build();
.try_into_job() let logfile = OpenOptions::new()
.unwrap(); .append(true)
db.insert_jobs(&[agent_hello.meta])?; .create(true)
} .open(LOGFILE)
Ok(()) .unwrap();
}) let level = LevelFilter::Info;
.await let loggers = vec![
WriteLogger::new(level, log_cfg.clone(), logfile) as Box<dyn SharedLogger>,
TermLogger::new(level, log_cfg, TerminalMode::Stderr, ColorChoice::Auto),
];
CombinedLogger::init(loggers).unwrap();
} }
pub async fn serve() -> Result<(), ServerError> { fn init_all() {
let env = config::DBEnv::load()?; init_logger();
let pool = async_pool(&env); init_env();
let db = PgRepo::new(pool); prefill_jobs();
}
preload_jobs(&db).await?;
let env = config::AccessEnv::load()?;
let routes = init_endpoints(&env.admin_auth_token, db)
.recover(handle_rejection)
.with(custom(logger));
let server_cert = include_bytes!("../../../certs/server.crt");
let server_key = include_bytes!("../../../certs/server.key");
let ca = include_bytes!("../../../certs/ca.crt");
warp::serve(routes) pub async fn serve() {
init_all();
let routes = make_filters();
warp::serve(routes.with(warp::log("warp")))
.tls() .tls()
.cert(server_cert) .cert_path("./certs/server.crt")
.key(server_key) .key_path("./certs/server.key")
.client_auth_required(ca) .client_auth_required_path("./certs/ca.crt")
.run(([0, 0, 0, 0], config::MASTER_PORT)) .run(([0, 0, 0, 0], MASTER_PORT))
.await; .await;
Ok(())
}
async fn handle_rejection(rej: Rejection) -> Result<Response, Infallible> {
let resp = if let Some(err) = rej.find::<ServerError>() {
error!("{:?}", err);
RejResponse::bad_request(err.to_string())
} else if rej.is_not_found() {
RejResponse::not_found("not found placeholder")
} else {
error!("{:?}", rej);
RejResponse::internal()
};
Ok(resp.into_response())
} }
fn logger(info: Info<'_>) {
info!(target: "warp",
"{raddr} {agent_id} \"{path}\" {status}",
raddr = info.remote_addr().unwrap_or(([0, 0, 0, 0], 0).into()),
path = info.path(),
agent_id = info.user_agent()
.map(|id: &str| id.splitn(3, '-')
.take(2)
.collect::<String>()
)
.unwrap_or_else(|| "NO_AGENT_UID".to_string()),
status = info.status()
);
}
fn ok<T>(_: T) -> impl Reply {
DEFAULT_RESPONSE
}
/*
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[double]
use crate::handlers::Endpoints; use crate::handlers::Endpoints;
use handlers::build_ok; use handlers::build_ok;
use u_lib::messaging::{AsMsg, BaseMessage, Reportable}; use mockall::predicate::*;
use test_case::test_case;
use u_lib::messaging::{AsMsg, BaseMessage};
use uuid::Uuid; use uuid::Uuid;
use warp::test; use warp::test::request;
#[rstest] #[test_case(Some(Uuid::new_v4()))]
#[case(Some(Uuid::new_v4()))] #[test_case(None => panics)]
#[should_panic]
#[case(None)]
#[tokio::test] #[tokio::test]
async fn test_get_agent_jobs_unauthorized(#[case] uid: Option<Uuid>) { async fn test_get_agent_jobs_unauthorized(uid: Option<Uuid>) {
let mock = Endpoints::faux(); let mock = Endpoints::get_agent_jobs_context();
when!(mock.get_agent_jobs).then_return(Ok(build_ok(""))); mock.expect().with(eq(uid)).returning(|_| Ok(build_ok("")));
//mock.expect().with(eq(uid)).returning(|_| Ok(build_ok(""))); request()
test::request()
.path(&format!( .path(&format!(
"/get_agent_jobs/{}", "/get_agent_jobs/{}",
uid.map(|u| u.simple().to_string()).unwrap_or(String::new()) uid.map(|u| u.simple().to_string()).unwrap_or(String::new())
)) ))
.method("GET") .method("GET")
.filter(&init_filters("")) .filter(&make_filters())
.await .await
.unwrap(); .unwrap();
mock.checkpoint();
} }
#[tokio::test] #[tokio::test]
async fn test_report_unauth_successful() { async fn test_report_unauth_successful() {
let mock = Endpoints::report(); let mock = Endpoints::report_context();
mock.expect() mock.expect()
.withf(|msg: &BaseMessage<'_, Vec<Reportable>>| msg.inner_ref()[0] == Reportable::Dummy) .withf(|msg: &BaseMessage<'_, Vec<ExecResult>>| msg.inner_ref()[0] == ExecResult::Dummy)
.returning(|_| Ok(build_ok(""))); .returning(|_| Ok(build_ok("")));
test::request() request()
.path("/report/") .path("/report/")
.method("POST") .method("POST")
.json(&vec![Reportable::Dummy].as_message()) .json(&vec![ExecResult::Dummy].as_message())
.filter(&init_filters("")) .filter(&make_filters())
.await .await
.unwrap(); .unwrap();
mock.checkpoint(); mock.checkpoint();
} }
} }
*/

@ -0,0 +1,3 @@
pub trait Worker {
fn process(&mut self);
}

@ -1,24 +1,16 @@
set -ex set -ex
source $(dirname $0)/rootdir.sh #set ROOTDIR DIR=.
DIR=$ROOTDIR/certs
V3_CFG=$DIR/v3.ext V3_CFG=$DIR/v3.ext
mkdir -p $DIR
cat > $V3_CFG << EOF cat > $V3_CFG << EOF
authorityKeyIdentifier=keyid,issuer authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign, cRLSign keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign, cRLSign
subjectAltName = @alt_names
[alt_names]
DNS.1 = ortem.xyz
DNS.2 = u_server
DNS.3 = localhost
EOF EOF
openssl req -x509 -newkey rsa:4096 -keyout $DIR/ca.key -out $DIR/ca.crt -nodes -days 365 -subj "/CN=root" openssl req -x509 -newkey rsa:4096 -keyout $DIR/ca.key -out $DIR/ca.crt -nodes -days 365 -subj "/CN=root"
openssl req -newkey rsa:4096 -keyout $DIR/alice.key -out $DIR/alice.csr -nodes -days 365 -subj "/CN=alice" openssl req -newkey rsa:4096 -keyout $DIR/alice.key -out $DIR/alice.csr -nodes -days 365 -subj "/CN=alice"
openssl req -newkey rsa:4096 -keyout $DIR/server.key -out $DIR/server.csr -nodes -days 365 -subj "/CN=ortem.xyz" openssl req -newkey rsa:4096 -keyout $DIR/server.key -out $DIR/server.csr -nodes -days 365 -subj "/CN=u_server"
openssl x509 -req -in $DIR/alice.csr -CA $DIR/ca.crt -CAkey $DIR/ca.key -out $DIR/alice.crt -set_serial 01 -days 365 -extfile $V3_CFG openssl x509 -req -in $DIR/alice.csr -CA $DIR/ca.crt -CAkey $DIR/ca.key -out $DIR/alice.crt -set_serial 01 -days 365 -extfile $V3_CFG
openssl x509 -req -in $DIR/server.csr -CA $DIR/ca.crt -CAkey $DIR/ca.key -out $DIR/server.crt -set_serial 01 -days 365 -extfile $V3_CFG openssl x509 -req -in $DIR/server.csr -CA $DIR/ca.crt -CAkey $DIR/ca.key -out $DIR/server.crt -set_serial 01 -days 365 -extfile $V3_CFG
openssl pkcs12 -export -out $DIR/alice.p12 -inkey $DIR/alice.key -in $DIR/alice.crt -passin pass: -passout pass: openssl pkcs12 -export -out $DIR/alice.p12 -inkey $DIR/alice.key -in $DIR/alice.crt -passin pass: -passout pass:

@ -1,52 +0,0 @@
version: "3.4"
networks:
u_net:
services:
u_server:
image: localhost/unki/u_server
networks:
- u_net
volumes:
- ./u_server:/unki/u_server
- ./logs:/unki/logs:rw
working_dir: /unki
command: /unki/u_server
depends_on:
u_db:
condition: service_healthy
ports:
- 9990:9990
env_file:
- ./.env
- ./.env.private
environment:
RUST_LOG: warp=info,u_server_lib=debug
healthcheck:
test: ss -tlpn | grep 9990
interval: 5s
timeout: 2s
retries: 2
u_db:
image: localhost/unki/u_db
networks:
- u_net
env_file:
- ./.env
- ./.env.private
volumes:
- ./migrator:/migrator
- ./data:/var/lib/postgresql/data
- type: bind
source: ./u_db_entrypoint.sh
target: /u_db_entrypoint.sh
command: /u_db_entrypoint.sh
healthcheck:
# test if db's port is open and db is created
test: ss -tlpn | grep 5432 && psql -lqt -U $${POSTGRES_USER} | grep -qw $${POSTGRES_DATABASE}
interval: 5s
timeout: 5s
retries: 3

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save