Initial commit

This commit is contained in:
2021-12-19 17:30:51 +01:00
commit a589014106
65 changed files with 7437 additions and 0 deletions

View File

@ -0,0 +1,296 @@
use std::fmt::{Debug, format};
use std::net::TcpStream;
use std::os::unix::process::ExitStatusExt;
use std::process::{Command, ExitStatus};
use std::time::SystemTime;
use log::{info, warn};
use structopt::StructOpt;
use structopt::clap::AppSettings;
use crate::configure::cli::parser;
use crate::shared::api::api::API;
use crate::shared::api::request;
use crate::shared::api::request::{ConfigGetResponse, ConfigNetworkMode, ConfigPostRequest, ConfigRunLevel};
type Result<T> = std::result::Result<T, failure::Error>;
trait Execute {
fn execute(self, api: &mut API) -> Result<()>;
}
#[derive(StructOpt)]
#[structopt(
name = "configure",
about = "A command line wrapper to control docker4ssh containers from within them",
settings = &[AppSettings::ArgRequiredElseHelp]
)]
struct Opts {
#[structopt(short, long, global = true, help = "Verbose output")]
verbose: bool,
#[structopt(subcommand)]
commands: Option<Root>
}
#[derive(StructOpt)]
#[structopt(
name = "ping",
about = "Ping the control socket"
)]
struct Ping {}
impl Execute for Ping {
fn execute(self, api: &mut API) -> Result<()> {
let start = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_nanos();
let result = request::PingRequest::new().request(api)?;
info!("Pong! Ping is {:.4}ms", ((result.received - start) as f64) / 1000.0 / 1000.0);
Ok(())
}
}
#[derive(StructOpt)]
#[structopt(
name = "error",
about = "Example error message sent from socket",
)]
struct Error {}
impl Execute for Error {
fn execute(self, api: &mut API) -> Result<()> {
request::ErrorRequest::new().request(api)?;
Ok(())
}
}
#[derive(StructOpt)]
#[structopt(
name = "info",
about = "Shows information about the current container",
)]
struct Info {}
impl Execute for Info {
fn execute(self, api: &mut API) -> Result<()> {
let result = request::InfoRequest:: new().request(api)?;
info!(concat!(
"\tContainer ID: {}"
), result.container_id);
Ok(())
}
}
#[derive(StructOpt)]
#[structopt(
name = "config",
about = "Get or set the behavior of the current container",
settings = &[AppSettings::ArgRequiredElseHelp]
)]
struct Config {
#[structopt(subcommand)]
commands: Option<ConfigCommands>
}
#[derive(StructOpt)]
enum ConfigCommands {
Get(ConfigGet),
Set(ConfigSet)
}
#[derive(StructOpt)]
#[structopt(
name = "get",
about = "Show the current container behavior"
)]
struct ConfigGet {}
impl Execute for ConfigGet {
fn execute(self, api: &mut API) -> Result<()> {
let response: ConfigGetResponse = request::ConfigGetRequest::new().request(api)?;
info!(concat!(
"\tNetwork Mode: {}\n",
"\tConfigurable: {}\n",
"\tRun Level: {}\n",
"\tStartup Information: {}\n",
"\tExit After: {}\n",
"\tKeep On Exit: {}"
), response.network_mode, response.configurable, response.run_level, response.startup_information, response.exit_after, response.keep_on_exit);
Ok(())
}
}
#[derive(StructOpt)]
#[structopt(
name = "set",
about = "Set the current container behavior",
settings = &[AppSettings::ArgRequiredElseHelp]
)]
struct ConfigSet {
#[structopt(long, help = "If the container should keep running even after the user exits", parse(try_from_str = parser::parse_network_mode))]
network_mode: Option<ConfigNetworkMode>,
#[structopt(long, help = "If the container should be configurable from within")]
configurable: Option<bool>,
#[structopt(long, help = "Set the container stop behavior", parse(try_from_str = parser::parse_config_run_level))]
run_level: Option<ConfigRunLevel>,
#[structopt(long, help = "If information about the container should be shown when a user connects")]
startup_information: Option<bool>,
#[structopt(long, help = "Process name after which the container should exit")]
exit_after: Option<String>,
#[structopt(long, help = "If the container should be not deleted after exit")]
keep_on_exit: Option<bool>
}
impl Execute for ConfigSet {
fn execute(self, api: &mut API) -> Result<()> {
let mut request = request::ConfigPostRequest::new();
if let Some(exit_after) = self.exit_after.as_ref() {
let program_runs = Command::new("pidof")
.arg("-s")
.arg(exit_after).status().unwrap().success();
if !program_runs {
warn!("NOTE: There is currently no process running with the name '{}'", exit_after);
}
}
request.body.network_mode = self.network_mode;
request.body.configurable = self.configurable;
request.body.run_level = self.run_level;
request.body.startup_information = self.startup_information;
request.body.exit_after = self.exit_after;
request.body.keep_on_exit = self.keep_on_exit;
request.request(api)?;
if let Some(keep_on_exit) = self.keep_on_exit {
if keep_on_exit {
if let Ok(auth) = request::AuthGetRequest::new().request(api) {
info!("To reconnect to this container, use the user '{}' for the ssh connection", &auth.user)
}
}
}
Ok(())
}
}
#[derive(StructOpt)]
#[structopt(
name = "auth",
about = "Get or set the container authentication",
settings = &[AppSettings::ArgRequiredElseHelp]
)]
struct Auth {
#[structopt(subcommand)]
commands: Option<AuthCommands>
}
#[derive(StructOpt)]
enum AuthCommands {
Get(AuthGet),
Set(AuthSet)
}
#[derive(StructOpt)]
#[structopt(
name = "get",
about = "Show the current username used for ssh authentication and if a password is set"
)]
struct AuthGet {}
impl Execute for AuthGet {
fn execute(self, api: &mut API) -> Result<()> {
let response = request::AuthGetRequest::new().request(api)?;
info!(concat!(
"\tUser: {}\n",
"\tHas Password: {}\n"
), response.user, response.has_password);
Ok(())
}
}
#[derive(StructOpt)]
#[structopt(
name = "set",
about = "Set the authentication settings",
settings = &[AppSettings::ArgRequiredElseHelp]
)]
struct AuthSet {
#[structopt(long, help = "The container username")]
user: Option<String>,
#[structopt(long, help = "The container password. If empty, the authentication gets removed")]
password: Option<String>
}
impl Execute for AuthSet {
fn execute(self, api: &mut API) -> Result<()> {
let mut request = request::AuthPostRequest::new();
request.body.user = self.user;
request.body.password = self.password.clone();
request.request(api)?;
if let Some(password) = self.password {
if password == "" {
warn!("No password was specified so the authentication got deleted")
}
}
Ok(())
}
}
#[derive(StructOpt)]
enum Root {
Auth(Auth),
Error(Error),
Info(Info),
Ping(Ping),
Config(Config)
}
pub fn cli(route: String) {
if let Some(subcommand) = Opts::from_args().commands {
let mut result: Result<()> = Ok(());
let mut api = API::new(route, String::new());
match subcommand {
Root::Auth(auth) => {
if let Some(subsubcommand) = auth.commands {
match subsubcommand {
AuthCommands::Get(auth_get) => {
result = auth_get.execute(&mut api)
}
AuthCommands::Set(auth_set) => {
result = auth_set.execute(&mut api)
}
}
}
},
Root::Error(error) => result = error.execute(&mut api),
Root::Info(info) => result = info.execute(&mut api),
Root::Ping(ping) => result = ping.execute(&mut api),
Root::Config(config) => {
if let Some(subsubcommand) = config.commands {
match subsubcommand {
ConfigCommands::Get(config_get) => {
result = config_get.execute(&mut api)
}
ConfigCommands::Set(config_set) => {
result = config_set.execute(&mut api)
}
}
}
}
}
if result.is_err() {
log::error!("{}", result.err().unwrap().to_string())
}
}
}

View File

@ -0,0 +1,4 @@
mod cli;
pub mod parser;
pub use cli::cli;

View File

@ -0,0 +1,23 @@
use std::f32::consts::E;
use std::fmt::format;
use crate::shared::api::request::{ConfigNetworkMode, ConfigRunLevel};
pub fn parse_network_mode(src: &str) -> Result<ConfigNetworkMode, String> {
match String::from(src).to_lowercase().as_str() {
"off" | "1" => Ok(ConfigNetworkMode::Off),
"full" | "2" => Ok(ConfigNetworkMode::Full),
"host" | "3" => Ok(ConfigNetworkMode::Host),
"docker" | "4" => Ok(ConfigNetworkMode::Docker),
"none" | "5" => Ok(ConfigNetworkMode::None),
_ => Err(format!("'{} is not a valid network mode. Choose from 'off', 'full', 'host', 'docker', 'none'", src))
}
}
pub fn parse_config_run_level(src: &str) -> Result<ConfigRunLevel, String> {
match String::from(src).to_lowercase().as_str() {
"user" | "1" => Ok(ConfigRunLevel::User),
"container" | "2" => Ok(ConfigRunLevel::Container),
"forever" | "3" => Ok(ConfigRunLevel::Forever),
_ => Err(format!("'{}' is not a valid run level. Choose from: 'user', 'container', 'forever'", src))
}
}

View File

@ -0,0 +1,19 @@
use std::fs;
use std::net::TcpStream;
use std::os::unix::net::UnixStream;
use std::process::exit;
use log::{LevelFilter, trace, warn, info, error};
use docker4ssh::configure::cli;
use docker4ssh::shared::logging::init_logger;
fn main() {
init_logger(LevelFilter::Debug);
match fs::read_to_string("/etc/docker4ssh") {
Ok(route) => cli(route),
Err(e) => {
error!("Failed to read /etc/docker4ssh: {}", e.to_string());
exit(1);
}
}
}

View File

@ -0,0 +1,3 @@
pub mod cli;
pub use cli::cli;

2
container/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod shared;
pub mod configure;

View File

@ -0,0 +1,157 @@
use std::collections::HashMap;
use std::io::{Read, Write};
use std::net::TcpStream;
use log::Level::Error;
use serde::Deserialize;
pub type Result<T> = std::result::Result<T, failure::Error>;
pub struct API {
route: String,
host: String,
}
impl API {
pub const fn new(route: String, host: String) -> Self {
API {
route,
host,
}
}
pub fn new_connection(&mut self) -> Result<TcpStream> {
match TcpStream::connect(&self.route) {
Ok(stream) => Ok(stream),
Err(e) => Err(failure::format_err!("Failed to connect to {}: {}", self.route, e.to_string()))
}
}
pub fn request(&mut self, request: &Request) -> Result<APIResult> {
let mut connection = self.new_connection()?;
connection.write_all(request.as_string().as_bytes())?;
let mut buf: String = String::new();
connection.read_to_string(&mut buf).map_err(|e| failure::err_msg(e.to_string()))?;
Ok(APIResult::new(request, buf))
}
pub fn request_with_err(&mut self, request: &Request) -> Result<APIResult> {
let result = self.request(request)?;
if result.result_code >= 400 {
let err: APIError = result.body()?;
Err(failure::err_msg(format!("Error {}: {}", result.result_code, err.message)))
} else {
Ok(result)
}
}
}
#[derive(Deserialize)]
pub struct APIError {
message: String
}
pub struct APIResult {
// TODO: Store the whole request instead of only the path
request_path: String,
result_code: i32,
result_body: String
}
impl APIResult {
fn new(request: &Request, raw_response: String) -> Self {
APIResult {
request_path: request.path.clone(),
// TODO: Parse http body better
result_code: raw_response[9..12].parse().unwrap(),
result_body: raw_response.split_once("\r\n\r\n").unwrap().1.to_string()
}
}
pub fn path(self) -> String {
self.request_path
}
pub fn code(&self) -> i32 {
return self.result_code
}
pub fn has_body(&self) -> bool {
self.result_body.len() > 0
}
pub fn body<'a, T: Deserialize<'a>>(&'a self) -> Result<T> {
let result: T = serde_json::from_str(&self.result_body).map_err(|e| {
// checks if the error has a body and if so, return it
if self.has_body() {
let error: APIError = serde_json::from_str(&self.result_body).unwrap_or_else(|ee| {
APIError{message: format!("could not deserialize response: {}", e.to_string())}
});
failure::format_err!("Failed to call '{}': {}", self.request_path, error.message)
} else {
failure::format_err!("Failed to call '{}': {}", self.request_path, e.to_string())
}
})?;
Ok(result)
}
}
pub enum Method {
GET,
POST
}
pub struct Request {
method: Method,
path: String,
headers: HashMap<String, String>,
body: String,
}
impl Request {
pub fn new(path: String) -> Self {
Request{
method: Method::GET,
path,
headers: Default::default(),
body: "".to_string(),
}
}
pub fn set_method(&mut self, method: Method) -> &Self {
self.method = method;
self
}
pub fn set_path(&mut self, path: String) -> &Self {
self.path = path;
self
}
pub fn set_header(&mut self, field: &str, value: String) -> &Self {
self.headers.insert(String::from(field), value);
self
}
pub fn set_body(&mut self, body: String) -> &Self {
self.body = body;
self.headers.insert("Content-Length".to_string(), self.body.len().to_string());
self
}
pub fn as_string(&self) -> String {
let method;
match self.method {
Method::GET => method = "GET",
Method::POST => method = "POST"
}
let headers_as_string = self.headers.iter().map(|f| format!("{}: {}", f.0, f.1)).collect::<String>();
return format!("{} {} HTTP/1.0\r\n\
{}\r\n\r\n\
{}\r\n", method, self.path, headers_as_string, self.body)
}
}

View File

@ -0,0 +1,2 @@
pub mod request;
pub mod api;

View File

@ -0,0 +1,220 @@
use std::fmt::{Display, Formatter};
use serde::{Deserialize, Serialize};
use serde::de::Unexpected::Str;
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::shared::api::api::{API, Method, Request, Result};
use crate::shared::api::api::Method::POST;
#[derive(Deserialize)]
pub struct PingResponse {
pub received: u128
}
pub struct PingRequest {
request: Request
}
impl PingRequest {
pub fn new() -> Self {
PingRequest {
request: Request::new(String::from("/ping"))
}
}
pub fn request(&self, api: &mut API) -> Result<PingResponse> {
let result: PingResponse = api.request_with_err(&self.request)?.body()?;
Ok(result)
}
}
pub struct ErrorRequest {
request: Request
}
impl ErrorRequest {
pub fn new() -> Self {
ErrorRequest {
request: Request::new(String::from("/error"))
}
}
pub fn request(&self, api: &mut API) -> Result<()> {
api.request_with_err(&self.request)?.body()?;
// should never call Ok
Ok(())
}
}
#[derive(Deserialize)]
pub struct InfoResponse {
pub container_id: String
}
pub struct InfoRequest {
request: Request
}
impl InfoRequest {
pub fn new() -> Self {
InfoRequest{
request: Request::new(String::from("/info"))
}
}
pub fn request(&self, api: &mut API) -> Result<InfoResponse> {
let result: InfoResponse = api.request_with_err(&self.request)?.body()?;
Ok(result)
}
}
#[derive(Debug, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum ConfigRunLevel {
User = 1,
Container = 2,
Forever = 3
}
impl Display for ConfigRunLevel {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum ConfigNetworkMode {
Off = 1,
Full = 2,
Host = 3,
Docker = 4,
None = 5
}
impl Display for ConfigNetworkMode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Deserialize)]
pub struct ConfigGetResponse {
pub network_mode: ConfigNetworkMode,
pub configurable: bool,
pub run_level: ConfigRunLevel,
pub startup_information: bool,
pub exit_after: String,
pub keep_on_exit: bool
}
pub struct ConfigGetRequest {
request: Request
}
impl ConfigGetRequest {
pub fn new() -> ConfigGetRequest {
ConfigGetRequest{
request: Request::new(String::from("/config"))
}
}
pub fn request(&self, api: &mut API) -> Result<ConfigGetResponse> {
let result: ConfigGetResponse = api.request_with_err(&self.request)?.body()?;
Ok(result)
}
}
#[derive(Serialize)]
pub struct ConfigPostBody {
pub network_mode: Option<ConfigNetworkMode>,
pub configurable: Option<bool>,
pub run_level: Option<ConfigRunLevel>,
pub startup_information: Option<bool>,
pub exit_after: Option<String>,
pub keep_on_exit: Option<bool>
}
pub struct ConfigPostRequest {
request: Request,
pub body: ConfigPostBody
}
impl ConfigPostRequest {
pub fn new() -> ConfigPostRequest {
let mut request = Request::new(String::from("/config"));
request.set_method(Method::POST);
ConfigPostRequest {
request,
body: ConfigPostBody{
network_mode: None,
configurable: None,
run_level: None,
startup_information: None,
exit_after: None,
keep_on_exit: None
}
}
}
pub fn request(&mut self, api: &mut API) -> Result<()> {
self.request.set_body(serde_json::to_string(&self.body)?);
api.request_with_err(&self.request)?;
Ok(())
}
}
#[derive(Deserialize)]
pub struct AuthGetResponse {
pub user: String,
pub has_password: bool
}
pub struct AuthGetRequest {
request: Request
}
impl AuthGetRequest {
pub fn new() -> AuthGetRequest {
AuthGetRequest{
request: Request::new(String::from("/auth"))
}
}
pub fn request(&self, api: &mut API) -> Result<AuthGetResponse> {
let result: AuthGetResponse = api.request_with_err(&self.request)?.body()?;
Ok(result)
}
}
#[derive(Serialize)]
pub struct AuthPostBody {
pub user: Option<String>,
pub password: Option<String>
}
pub struct AuthPostRequest {
request: Request,
pub body: AuthPostBody
}
impl AuthPostRequest {
pub fn new() -> AuthPostRequest {
let mut request = Request::new(String::from("/auth"));
request.set_method(POST);
AuthPostRequest {
request,
body: AuthPostBody{
user: None,
password: None
}
}
}
pub fn request(&mut self, api: &mut API) -> Result<()> {
self.request.set_body(serde_json::to_string(&self.body)?);
api.request_with_err(&self.request)?;
Ok(())
}
}

View File

@ -0,0 +1,19 @@
use log::{info, Metadata, Record};
pub struct Logger;
impl log::Log for Logger {
fn enabled(&self, metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
println!("{}", record.args().to_string())
}
}
fn flush(&self) {
todo!()
}
}

View File

@ -0,0 +1,11 @@
use log::{LevelFilter, SetLoggerError};
pub mod logger;
pub use logger::Logger;
static LOGGER: Logger = Logger;
pub fn init_logger(level: LevelFilter) -> Result<(), SetLoggerError> {
log::set_logger(&Logger).map(|()| log::set_max_level(level))
}

View File

@ -0,0 +1,2 @@
pub mod api;
pub mod logging;