This commit is contained in:
Jeffrey C. Ollie 2024-01-28 20:20:37 -06:00
parent 6c60ecd1c6
commit 265c035bd4
Signed by: jeff
GPG key ID: 6F86035A6D97044E
6 changed files with 187 additions and 194 deletions

View file

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const path = @import("src/path.zig");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
@ -8,13 +9,27 @@ pub fn build(b: *std.Build) void {
[]const u8, []const u8,
"ssh", "ssh",
"path to ssh binary", "path to ssh binary",
) orelse "ssh"; ) orelse ssh: {
const p = path.expandPath(b.allocator, "ssh") catch {
break :ssh "ssh";
} orelse {
break :ssh "ssh";
};
break :ssh p;
};
const telnet_path = b.option( const telnet_path = b.option(
[]const u8, []const u8,
"telnet", "telnet",
"path to telnet binary", "path to telnet binary",
) orelse "telnet"; ) orelse telnet: {
const p = path.expandPath(b.allocator, "telnet") catch {
break :telnet "telnet";
} orelse {
break :telnet "telnet";
};
break :telnet p;
};
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "hostapps", .name = "hostapps",
@ -49,15 +64,15 @@ pub fn build(b: *std.Build) void {
exe.root_module.addImport("logz", logz.module("logz")); exe.root_module.addImport("logz", logz.module("logz"));
const yazap = b.dependency( const @"zig-cli" = b.dependency(
"yazap", "zig-cli",
.{ .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}, },
); );
exe.root_module.addImport("yazap", yazap.module("yazap")); exe.root_module.addImport("zig-cli", @"zig-cli".module("zig-cli"));
b.installArtifact(exe); b.installArtifact(exe);

View file

@ -4,25 +4,26 @@
.minimum_zig_version = "0.12.0", .minimum_zig_version = "0.12.0",
.dependencies = .{ .dependencies = .{
.logz = .{ .logz = .{
.url = "https://github.com/karlseguin/log.zig/archive/a70984c80eb67c448480377849ba2adb6e51cf73.tar.gz", .url = "https://github.com/karlseguin/log.zig/archive/eb86766b91cf2ca0a6e87dd8568fab80695cf2e8.tar.gz",
.hash = "122090c83a4e52b454e006da4a804b2210136ddec0dccfae00e7097314e5acfd01d0", .hash = "1220b6cf56de826aead943a6db2a62ed40d81a150c24dcb019b3eb88673c82a6e7b7",
},
.yazap = .{
.url = "https://github.com/prajwalch/yazap/archive/5f0d5d8928d5cd1907760dc41fa6f05dd232aaa5.tar.gz",
.hash = "1220e4674826a70402974f13b7e2aaa4e9242e1b2b9d592015de9e21c7fa5fe200bd",
}, },
// .yazap = .{
// .url = "https://github.com/prajwalch/yazap/archive/5f0d5d8928d5cd1907760dc41fa6f05dd232aaa5.tar.gz",
// .hash = "1220e4674826a70402974f13b7e2aaa4e9242e1b2b9d592015de9e21c7fa5fe200bd",
// },
.anzi = .{ .anzi = .{
.url = "https://git.ocjtech.us/jeff/anzi/archive/0e9d395a55b16da1d5bf7bfb59e2cfa59a7f2630.tar.gz", .url = "https://git.ocjtech.us/jeff/anzi/archive/0e9d395a55b16da1d5bf7bfb59e2cfa59a7f2630.tar.gz",
.hash = "12203f0ed986047bcd860b00496979a7734c2f462da4e56a72add4b17a1a7981f8ec", .hash = "12203f0ed986047bcd860b00496979a7734c2f462da4e56a72add4b17a1a7981f8ec",
}, },
.@"zig-cli" = .{
.url = "https://github.com/sam701/zig-cli/archive/50201f0086da689cd104c2fef350f3dbe894eea7.tar.gz",
// .hash = "12208a4377c7699927605e5a797bad5ae2ba8be4b49f68b9f522b3a755597a21f1cf",
.hash = "12208a4377c7699927605e5a797bad5ae2ba8be4b49f68b9f522b3a755597a21f1cf",
},
}, },
.paths = .{ .paths = .{
// "",
// For example...
"build.zig", "build.zig",
"build.zig.zon", "build.zig.zon",
"src", "src",
// "LICENSE",
// "README.md",
}, },
} }

View file

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Config = @import("config.zig").Config; const Config = @import("config.zig").Config;
const options = @import("options.zig");
const ansi = @import("anzi").ANSI(.{}); const ansi = @import("anzi").ANSI(.{});
const kex_algorithms = [_][]const u8{ const kex_algorithms = [_][]const u8{
@ -30,7 +31,7 @@ const macs = [_][]const u8{
"hmac-sha2-512", "hmac-sha2-512",
}; };
const options = [_]struct { key: []const u8, value: []const u8 }{ const ssh_options = [_]struct { key: []const u8, value: []const u8 }{
.{ .key = "ControlMaster", .value = "no" }, .{ .key = "ControlMaster", .value = "no" },
.{ .key = "ControlPath", .value = "none" }, .{ .key = "ControlPath", .value = "none" },
.{ .key = "ForwardX11", .value = "no" }, .{ .key = "ForwardX11", .value = "no" },
@ -39,19 +40,14 @@ const options = [_]struct { key: []const u8, value: []const u8 }{
.{ .key = "PubkeyAcceptedKeyTypes", .value = "+ssh-rsa" }, .{ .key = "PubkeyAcceptedKeyTypes", .value = "+ssh-rsa" },
}; };
pub fn connect( pub fn connect() !void {
allocator: std.mem.Allocator, var arena = std.heap.ArenaAllocator.init(options.options.alloc);
config_path: []const u8,
identities: ?[][]const u8,
proxy_jump: ?[]const u8,
ssh_path: []const u8,
telnet_path: []const u8,
) !void {
var arena = std.heap.ArenaAllocator.init(allocator);
const alloc = arena.allocator(); const alloc = arena.allocator();
defer arena.deinit(); defer arena.deinit();
const config = try Config.read(alloc, config_path); std.log.info("config: {}", .{options.options});
const config = try Config.read(options.options.alloc, options.options.config);
var path: []const u8 = undefined; var path: []const u8 = undefined;
var args = std.ArrayList([]const u8).init(alloc); var args = std.ArrayList([]const u8).init(alloc);
@ -69,23 +65,21 @@ pub fn connect(
switch (config.type) { switch (config.type) {
.ssh => { .ssh => {
path = ssh_path; path = options.options.ssh_path;
try args.append("ssh"); try args.append("ssh");
try args.append("-y"); try args.append("-y");
if (identities) |i| { for (options.options.identities) |identity| {
for (i) |identity| { try args.append("-i");
try args.append("-i"); try args.append(identity);
try args.append(identity);
}
} }
if (proxy_jump) |p| { if (options.options.proxy_jump) |p| {
try args.append("-o"); try args.append("-o");
try args.append(try std.fmt.allocPrint(alloc, "ProxyJump={s}", .{p})); try args.append(try std.fmt.allocPrint(alloc, "ProxyJump={s}", .{p}));
} }
for (options) |option| { for (ssh_options) |option| {
try args.append("-o"); try args.append("-o");
try args.append(try std.fmt.allocPrint(alloc, "{s}={s}", .{ option.key, option.value })); try args.append(try std.fmt.allocPrint(alloc, "{s}={s}", .{ option.key, option.value }));
} }
@ -111,14 +105,14 @@ pub fn connect(
}, },
.telnet => { .telnet => {
if (proxy_jump) |p| { if (options.options.proxy_jump) |p| {
path = ssh_path; path = options.options.ssh_path;
try args.append("ssh"); try args.append("ssh");
try args.append("-t"); try args.append("-t");
try args.append(try std.fmt.allocPrint(alloc, "ssh://{s}", .{p})); try args.append(try std.fmt.allocPrint(alloc, "ssh://{s}", .{p}));
try args.append("telnet"); try args.append("telnet");
} else { } else {
path = telnet_path; path = options.options.telnet_path;
try args.append("telnet"); try args.append("telnet");
} }

View file

@ -1,169 +1,18 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const build_options = @import("build_options"); const build_options = @import("build_options");
const yazap = @import("yazap"); const cli = @import("zig-cli");
const logz = @import("logz");
const Config = @import("config.zig").Config;
const connect = @import("connect.zig"); const connect = @import("connect.zig");
const options = @import("options.zig");
const App = yazap.App;
const Arg = yazap.Arg;
pub fn main() !void { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator(); const alloc = gpa.allocator();
try logz.setup(alloc, .{ options.options.alloc = alloc;
.level = .Debug,
.pool_size = 16,
.max_size = 4096,
.output = .stderr,
});
var app = App.init(alloc, "hostapps", "hostapps"); var iter = try std.process.argsWithAllocator(alloc);
defer app.deinit(); defer iter.deinit();
var root = app.rootCommand(); return cli.run(options.app, alloc);
try root.addArg(Arg.booleanOption("version", null, "Print version and exit"));
var connect_command = app.createCommand("connect", "connect to remote host");
try connect_command.addArg(Arg.singleValueOption("config", null, "Path to configuration file"));
try connect_command.addArg(Arg.multiValuesOption("identity", null, "Path to identity file", 8));
try connect_command.addArg(Arg.singleValueOption("proxy-jump", null, "Proxy jump"));
try connect_command.addArg(Arg.singleValueOption("ssh-command", null, "Path to ssh command"));
try connect_command.addArg(Arg.singleValueOption("telnet-command", null, "Path to telnet command"));
try root.addSubcommand(connect_command);
const matches = try app.parseProcess();
if (!(matches.containsArgs())) {
try app.displayHelp();
return;
}
if (matches.containsArg("version")) {
std.log.info("version number", .{});
return;
}
if (matches.subcommandMatches("connect")) |connect_matches| {
const config_path = connect_matches.getSingleValue("config") orelse {
try app.displaySubcommandHelp();
return;
};
const identities = id: {
const ids = connect_matches.getMultiValues("identity");
if (ids != null) break :id ids;
const id = connect_matches.getSingleValue("identity");
if (id) |i| {
break :id @as(?[][]const u8, @constCast(&[_][]const u8{i}));
}
break :id null;
};
if (identities) |i| {
for (i) |identity| {
logz.info().string("identity", identity).log();
const fullpath = fp: {
if (identity[0] == '~') {
const home = std.os.getenv("HOME") orelse {
logz.err().src(@src()).string("message", "unable to get HOME environment variable to expand tilde").log();
return;
};
var fp = try alloc.alloc(u8, home.len + identity.len - 1);
@memcpy(fp[0..home.len], home);
@memcpy(fp[home.len..], identity[1..]);
break :fp fp;
}
break :fp identity;
};
logz.info().string("fullpath", fullpath).log();
std.fs.cwd().access(fullpath, .{ .mode = .read_only }) catch |err| {
logz.err().src(@src()).err(err).string("identity", identity).string("fullpath", fullpath).log();
return;
};
if (identity.ptr != fullpath.ptr) alloc.free(fullpath);
}
}
const proxy_jump = connect_matches.getSingleValue("proxy-jump");
var ssh_path = connect_matches.getSingleValue("ssh-command") orelse build_options.ssh_path;
if (!std.fs.path.isAbsolute(ssh_path)) {
const expanded = expandPath(alloc, ssh_path) catch |err| expanded: {
std.log.warn("failed to expand ssh path={s} err={}", .{ ssh_path, err });
break :expanded null;
};
if (expanded) |v| {
ssh_path = v;
}
}
var telnet_path = connect_matches.getSingleValue("telnet-command") orelse build_options.telnet_path;
if (!std.fs.path.isAbsolute(telnet_path)) {
const expanded = expandPath(alloc, telnet_path) catch |err| expanded: {
std.log.warn("failed to expand telnet path={s} err={}", .{ telnet_path, err });
break :expanded null;
};
if (expanded) |v| {
telnet_path = v;
}
}
try connect.connect(alloc, config_path, identities, proxy_jump, ssh_path, telnet_path);
return;
}
}
fn expandPath(alloc: std.mem.Allocator, cmd: []const u8) !?[]u8 {
const PATH = switch (builtin.os.tag) {
.windows => blk: {
const win_path = std.os.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse return null;
const path = try std.unicode.utf16leToUtf8Alloc(alloc, win_path);
break :blk path;
},
else => std.os.getenvZ("PATH") orelse return null,
};
defer if (builtin.os.tag == .windows) alloc.free(PATH);
var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var it = std.mem.tokenizeScalar(u8, PATH, std.fs.path.delimiter);
var seen_eaccess = false;
while (it.next()) |search_path| {
const path_len = search_path.len + cmd.len + 1;
if (path_buf.len < path_len) return error.PathTooLong;
@memcpy(path_buf[0..search_path.len], search_path);
path_buf[search_path.len] = std.fs.path.sep;
@memcpy(path_buf[search_path.len + 1 ..][0..cmd.len], cmd);
path_buf[path_len] = 0;
const full_path = path_buf[0..path_len :0];
const stat = std.fs.cwd().statFile(full_path) catch |err| switch (err) {
error.FileNotFound => continue,
error.AccessDenied => {
seen_eaccess = true;
continue;
},
else => return err,
};
if (stat.kind != .directory and isExecutable(stat.mode)) {
std.log.debug("executable: {s}", .{full_path});
return try alloc.dupe(u8, full_path);
}
}
if (seen_eaccess) return error.AccessDenied;
return null;
}
fn isExecutable(mode: std.fs.File.Mode) bool {
if (builtin.os.tag == .windows) return true;
return mode & 0o0111 != 0;
} }

84
src/options.zig Normal file
View file

@ -0,0 +1,84 @@
const std = @import("std");
const cli = @import("zig-cli");
const build_options = @import("build_options");
const connect = @import("connect.zig");
pub const Options = struct {
alloc: std.mem.Allocator = undefined,
config: []const u8 = undefined,
identities: []const []const u8 = undefined,
proxy_jump: ?[]const u8 = null,
ssh_path: []const u8,
telnet_path: []const u8,
};
pub var options: Options = .{
.ssh_path = build_options.ssh_path,
.telnet_path = build_options.telnet_path,
};
var config_option = cli.Option{
.long_name = "config",
.help = "config file",
.value_ref = cli.mkRef(&options.config),
.required = true,
};
var identity_option = cli.Option{
.long_name = "identity",
.help = "ssh identity",
.value_ref = cli.mkRef(&options.identities),
};
var proxy_jump_option = cli.Option{
.long_name = "proxy-jump",
.help = "proxy jump",
.value_ref = cli.mkRef(&options.proxy_jump),
};
var ssh_command_option = cli.Option{
.long_name = "ssh-command",
.help = "ssh command",
.value_ref = cli.mkRef(&options.ssh_path),
};
var telnet_command_option = cli.Option{
.long_name = "telnet-command",
.help = "telnet command",
.value_ref = cli.mkRef(&options.telnet_path),
};
var connect_command = cli.Command{
.name = "connect",
.description = cli.Description{
.one_line = "connect",
},
.options = &.{
&config_option,
&identity_option,
&proxy_jump_option,
&ssh_command_option,
&telnet_command_option,
},
.target = cli.CommandTarget{
.action = cli.CommandAction{
.exec = connect.connect,
},
},
};
pub const app = &cli.App{
.command = cli.Command{
.name = "hostapps",
.description = cli.Description{
.one_line = "hostapps",
},
.target = cli.CommandTarget{
.subcommands = &.{
&connect_command,
},
},
},
.version = "0.1.0",
.author = "Jeffrey C. Ollie <jcollie@dmacc.edu>",
};

50
src/path.zig Normal file
View file

@ -0,0 +1,50 @@
const std = @import("std");
const builtin = @import("builtin");
pub fn expandPath(alloc: std.mem.Allocator, cmd: []const u8) !?[]u8 {
const PATH = switch (builtin.os.tag) {
.windows => blk: {
const win_path = std.os.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse return null;
const path = try std.unicode.utf16leToUtf8Alloc(alloc, win_path);
break :blk path;
},
else => std.os.getenvZ("PATH") orelse return null,
};
defer if (builtin.os.tag == .windows) alloc.free(PATH);
var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var it = std.mem.tokenizeScalar(u8, PATH, std.fs.path.delimiter);
var seen_eaccess = false;
while (it.next()) |search_path| {
const path_len = search_path.len + cmd.len + 1;
if (path_buf.len < path_len) return error.PathTooLong;
@memcpy(path_buf[0..search_path.len], search_path);
path_buf[search_path.len] = std.fs.path.sep;
@memcpy(path_buf[search_path.len + 1 ..][0..cmd.len], cmd);
path_buf[path_len] = 0;
const full_path = path_buf[0..path_len :0];
const stat = std.fs.cwd().statFile(full_path) catch |err| switch (err) {
error.FileNotFound => continue,
error.AccessDenied => {
seen_eaccess = true;
continue;
},
else => return err,
};
if (stat.kind != .directory and isExecutable(stat.mode)) {
std.log.debug("executable: {s}", .{full_path});
return try alloc.dupe(u8, full_path);
}
}
if (seen_eaccess) return error.AccessDenied;
return null;
}
pub fn isExecutable(mode: std.fs.File.Mode) bool {
if (builtin.os.tag == .windows) return true;
return mode & 0o0111 != 0;
}