From 4b2e5b33b6bb4b5c9f3e13df62dfc1deee536de6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 12 Jan 2024 22:47:10 -0600 Subject: [PATCH] add zig connector --- .gitignore | 2 + build.zig | 85 ++++++++++++++++++++++ build.zig.zon | 28 +++++++ flake.lock | 148 +++++++++++++++++++++++++++++++++---- flake.nix | 135 +++++++++++++++++++++++++++------- src/config.zig | 28 +++++++ src/connect.zig | 158 ++++++++++++++++++++++++++++++++++++++++ src/main.zig | 190 ++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 730 insertions(+), 44 deletions(-) create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/config.zig create mode 100644 src/connect.zig create mode 100644 src/main.zig diff --git a/.gitignore b/.gitignore index 9ec4e0f..3c9f657 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ __pycache__ /result /.venv +/zig-cache +/zig-out diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..72d198c --- /dev/null +++ b/build.zig @@ -0,0 +1,85 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const ssh_path = b.option( + []const u8, + "ssh", + "path to ssh binary", + ) orelse "ssh"; + + const telnet_path = b.option( + []const u8, + "telnet", + "path to telnet binary", + ) orelse "telnet"; + + const exe = b.addExecutable(.{ + .name = "hostapps", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const build_options = b.addOptions(); + build_options.addOption([]const u8, "ssh_path", ssh_path); + build_options.addOption([]const u8, "telnet_path", telnet_path); + + exe.root_module.addOptions("build_options", build_options); + + const anzi = b.dependency( + "anzi", + .{ + .target = target, + .optimize = optimize, + }, + ); + + exe.root_module.addImport("anzi", anzi.module("anzi")); + + const logz = b.dependency( + "logz", + .{ + .target = target, + .optimize = optimize, + }, + ); + + exe.root_module.addImport("logz", logz.module("logz")); + + const yazap = b.dependency( + "yazap", + .{ + .target = target, + .optimize = optimize, + }, + ); + + exe.root_module.addImport("yazap", yazap.module("yazap")); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..c6e91fb --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,28 @@ +.{ + .name = "hostapps", + .version = "0.1.0", + .minimum_zig_version = "0.12.0", + .dependencies = .{ + .logz = .{ + .url = "https://github.com/karlseguin/log.zig/archive/a70984c80eb67c448480377849ba2adb6e51cf73.tar.gz", + .hash = "122090c83a4e52b454e006da4a804b2210136ddec0dccfae00e7097314e5acfd01d0", + }, + .yazap = .{ + .url = "https://github.com/prajwalch/yazap/archive/5f0d5d8928d5cd1907760dc41fa6f05dd232aaa5.tar.gz", + .hash = "1220e4674826a70402974f13b7e2aaa4e9242e1b2b9d592015de9e21c7fa5fe200bd", + }, + .anzi = .{ + .url = "https://git.ocjtech.us/jeff/anzi/archive/0e9d395a55b16da1d5bf7bfb59e2cfa59a7f2630.tar.gz", + .hash = "12203f0ed986047bcd860b00496979a7734c2f462da4e56a72add4b17a1a7981f8ec", + }, + }, + .paths = .{ + // "", + // For example... + "build.zig", + "build.zig.zon", + "src", + // "LICENSE", + // "README.md", + }, +} diff --git a/flake.lock b/flake.lock index fd83d00..f8dbd2a 100644 --- a/flake.lock +++ b/flake.lock @@ -1,15 +1,31 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" }, "locked": { - "lastModified": 1694529238, - "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", - "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { @@ -18,6 +34,54 @@ "type": "github" } }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "zls", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1703887061, + "narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "langref": { + "flake": false, + "locked": { + "narHash": "sha256-mYdDCBdNEIeMbavdhSo8qXqW+3fqPC8BAich7W3umrI=", + "type": "file", + "url": "https://raw.githubusercontent.com/ziglang/zig/63bd2bff12992aef0ce23ae4b344e9cb5d65f05d/doc/langref.html.in" + }, + "original": { + "type": "file", + "url": "https://raw.githubusercontent.com/ziglang/zig/63bd2bff12992aef0ce23ae4b344e9cb5d65f05d/doc/langref.html.in" + } + }, "make-shell": { "locked": { "lastModified": 1634940815, @@ -41,11 +105,11 @@ ] }, "locked": { - "lastModified": 1693660503, - "narHash": "sha256-B/g2V4v6gjirFmy+I5mwB2bCYc0l3j5scVfwgl6WOl8=", + "lastModified": 1698974481, + "narHash": "sha256-yPncV9Ohdz1zPZxYHQf47S8S0VrnhV7nNhCawY46hDA=", "owner": "nix-community", "repo": "nix-github-actions", - "rev": "bd5bdbb52350e145c526108f4ef192eb8e554fa0", + "rev": "4bb5e752616262457bc7ca5882192a564c0472d2", "type": "github" }, "original": { @@ -56,11 +120,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1698318101, - "narHash": "sha256-gUihHt3yPD7bVqg+k/UVHgngyaJ3DMEBchbymBMvK1E=", + "lastModified": 1704722960, + "narHash": "sha256-mKGJ3sPsT6//s+Knglai5YflJUF2DGj7Ai6Ynopz0kI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "63678e9f3d3afecfeafa0acead6239cdb447574c", + "rev": "317484b1ead87b9c1b8ac5261a8d2dd748a0492d", "type": "github" }, "original": { @@ -83,11 +147,11 @@ "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1698640399, - "narHash": "sha256-mXzyx79/iFLZ0UDuSkqgFfejYRcSJfsCnJ9WlMusaI0=", + "lastModified": 1705060653, + "narHash": "sha256-puYyylgrBS4AFAHeyVRTjTUVD8DZdecJfymWJe7H438=", "owner": "nix-community", "repo": "poetry2nix", - "rev": "626111646fe236cb1ddc8191a48c75e072a82b7c", + "rev": "e0b44e9e2d3aa855d1dd77b06f067cd0e0c3860d", "type": "github" }, "original": { @@ -101,7 +165,9 @@ "flake-utils": "flake-utils", "make-shell": "make-shell", "nixpkgs": "nixpkgs", - "poetry2nix": "poetry2nix" + "poetry2nix": "poetry2nix", + "zig": "zig", + "zls": "zls" } }, "systems": { @@ -141,11 +207,11 @@ ] }, "locked": { - "lastModified": 1697388351, - "narHash": "sha256-63N2eBpKaziIy4R44vjpUu8Nz5fCJY7okKrkixvDQmY=", + "lastModified": 1699786194, + "narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "aae39f64f5ecbe89792d05eacea5cb241891292a", + "rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1", "type": "github" }, "original": { @@ -153,6 +219,56 @@ "repo": "treefmt-nix", "type": "github" } + }, + "zig": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1705040559, + "narHash": "sha256-6SjLyxWAVMfVfkz2x/3IlAJBJ0ywus6Hr9JrBbT9zCk=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "6022b38d2fd4e7504f1e8b6dcfccab9b655764a9", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + }, + "zls": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "gitignore": "gitignore", + "langref": "langref", + "nixpkgs": [ + "nixpkgs" + ], + "zig-overlay": [ + "zig" + ] + }, + "locked": { + "lastModified": 1705106613, + "narHash": "sha256-nqAXd8pEiEPJfjTrs0WB2UFOuBg5QRNbHWPDPGtrULI=", + "owner": "zigtools", + "repo": "zls", + "rev": "abe83cf2291381291d4c20629a5018f124c4bed2", + "type": "github" + }, + "original": { + "owner": "zigtools", + "repo": "zls", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index da5c75c..801332a 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,16 @@ make-shell = { url = "github:ursi/nix-make-shell"; }; + zig = { + url = "github:mitchellh/zig-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + zls = { + url = "github:zigtools/zls"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.zig-overlay.follows = "zig"; + inputs.flake-utils.follows = "flake-utils"; + }; }; outputs = { self, @@ -22,6 +32,8 @@ poetry2nix, flake-utils, make-shell, + zig, + zls, } @ inputs: flake-utils.lib.eachDefaultSystem ( @@ -39,33 +51,98 @@ }; inherit (poetry2nix.lib.mkPoetry2Nix {inherit pkgs;}) mkPoetryApplication overrides; in { - packages.hostapps = mkPoetryApplication { - python = pkgs.python311; - projectDir = ./.; - propagatedBuildInputs = [ - pkgs.inetutils - pkgs.openssh - pkgs.sshpass - ]; - overrides = overrides.withDefaults ( - self: super: { - pyansi = super.pyansi.overridePythonAttrs ( - old: { - buildInputs = old.buildInputs ++ [self.poetry]; - } - ); - # annotated-types = super.annotated-types.overridePythonAttrs ( - # old: { - # buildInputs = old.buildInputs ++ [self.hatchling]; - # } - # ); - # pydantic-core = super.pydantic-core.overridePythonAttrs ( - # old: { - # buildInputs = old.buildInputs ++ [self.maturin]; - # } - # ); - } - ); + packages = { + hostapps = mkPoetryApplication { + python = pkgs.python311; + projectDir = ./.; + propagatedBuildInputs = [ + pkgs.inetutils + pkgs.openssh + pkgs.sshpass + ]; + overrides = overrides.withDefaults ( + self: super: { + pyansi = super.pyansi.overridePythonAttrs ( + old: { + buildInputs = old.buildInputs ++ [self.poetry]; + } + ); + # annotated-types = super.annotated-types.overridePythonAttrs ( + # old: { + # buildInputs = old.buildInputs ++ [self.hatchling]; + # } + # ); + # pydantic-core = super.pydantic-core.overridePythonAttrs ( + # old: { + # buildInputs = old.buildInputs ++ [self.maturin]; + # } + # ); + } + ); + }; + hostapps-zig = let + cache = src: + pkgs.stdenvNoCC.mkDerivation { + inherit src; + name = "hostapps-zig-cache"; + buildInputs = [ + zig.packages.${pkgs.system}.master + ]; + dontUseZigInstall = true; + dontUseZigBuild = true; + dontConfigure = true; + dontFixup = true; + + preBuild = '' + export ZIG_GLOBAL_CACHE_DIR=$(mktemp -d) + ''; + + buildPhase = '' + runHook preBuild + zig build --fetch + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + cp -r --reflink=auto $ZIG_GLOBAL_CACHE_DIR/p $out + runHook postInstall + ''; + outputHash = "sha256-GUddHnPI5zAU2JpQ1R9qjWyrYF7ytmroQxbz2kjj6ro="; + outputHashMode = "recursive"; + }; + in + pkgs.stdenvNoCC.mkDerivation ( + attrs: { + pname = "hostapps-zig"; + version = "0.0.0"; + + src = ./.; + + buildInputs = [ + zig.packages.${pkgs.system}.master + ]; + + preBuild = '' + export ZIG_GLOBAL_CACHE_DIR=$(mktemp -d) + cp -r --reflink=auto ${cache attrs.src} $ZIG_GLOBAL_CACHE_DIR/p + chmod -R u+rwX $ZIG_GLOBAL_CACHE_DIR + ''; + + buildPhase = '' + runHook preBuild + zig build -Doptimize=ReleaseSafe + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out/bin + cp zig-out/bin/hostapps $out/bin + runHook postInstall + ''; + } + ); }; defaultPackage = self.packages.${system}.hostapps; devShells.default = let @@ -84,9 +161,11 @@ pkgs.openssh pkgs.poetry pkgs.sshpass + zig.packages.${system}.master + zls.packages.${system}.zls ]; env = { - NIX_PROJECT = project; + name = project; }; }; } diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..1f8662e --- /dev/null +++ b/src/config.zig @@ -0,0 +1,28 @@ +const std = @import("std"); + +pub const ConnectionType = enum { + ssh, + telnet, +}; + +pub const Config = struct { + name: []const u8, + type: ConnectionType, + comment: []const u8, + address: []const u8, + port: u16, + username: []const u8, + manufacturer: []const u8, + model: []const u8, + part_number: []const u8, + class: []const u8, + + const Self = @This(); + + pub fn read(alloc: std.mem.Allocator, path: []const u8) !Self { + const data = try std.fs.cwd().readFileAlloc(alloc, path, 2048); + const parsed = try std.json.parseFromSlice(Self, alloc, data, .{}); + const config = parsed.value; + return config; + } +}; diff --git a/src/connect.zig b/src/connect.zig new file mode 100644 index 0000000..b2f6460 --- /dev/null +++ b/src/connect.zig @@ -0,0 +1,158 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Config = @import("config.zig").Config; +const ansi = @import("anzi").ANSI(.{}); + +const kex_algorithms = [_][]const u8{ + "diffie-hellman-group14-sha1", + "diffie-hellman-group1-sha1", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group-exchange-sha256", + "ecdh-sha2-nistp256", +}; + +const ciphers = [_][]const u8{ + "aes256-cbc", + "aes192-cbc", + "3des-cbc", + "aes128-cbc", + "aes256-ctr", + "aes192-ctr", + "aes128-ctr", +}; + +const macs = [_][]const u8{ + "hmac-md5", + "hmac-sha1", + "hmac-sha2-256-etm@openssh.com", + "hmac-sha2-512-etm@openssh.com", + "hmac-sha2-256", + "hmac-sha2-512", +}; + +const options = [_]struct { key: []const u8, value: []const u8 }{ + .{ .key = "ControlMaster", .value = "no" }, + .{ .key = "ControlPath", .value = "none" }, + .{ .key = "ForwardX11", .value = "no" }, + .{ .key = "ForwardX11Trusted", .value = "no" }, + .{ .key = "HostKeyAlgorithms", .value = "+ssh-rsa" }, + .{ .key = "PubkeyAcceptedKeyTypes", .value = "+ssh-rsa" }, +}; + +pub fn connect( + allocator: std.mem.Allocator, + 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(); + defer arena.deinit(); + + const config = try Config.read(alloc, config_path); + + var path: []const u8 = undefined; + var args = std.ArrayList([]const u8).init(alloc); + + std.log.info("name: {s}", .{config.name}); + std.log.info("class {s}", .{config.class}); + std.log.info("type: {}", .{config.type}); + std.log.info("comment: {s}", .{config.comment}); + std.log.info("address: {s}", .{config.address}); + std.log.info("port: {d}", .{config.port}); + std.log.info("username: {s}", .{config.username}); + std.log.info("manufacturer: {s}", .{config.manufacturer}); + std.log.info("model: {s}", .{config.model}); + std.log.info("part number: {s}", .{config.part_number}); + + switch (config.type) { + .ssh => { + path = ssh_path; + try args.append("ssh"); + try args.append("-y"); + + if (identities) |i| { + for (i) |identity| { + try args.append("-i"); + try args.append(identity); + } + } + + if (proxy_jump) |p| { + try args.append("-o"); + try args.append(try std.fmt.allocPrint(alloc, "ProxyJump={s}", .{p})); + } + + for (options) |option| { + try args.append("-o"); + try args.append(try std.fmt.allocPrint(alloc, "{s}={s}", .{ option.key, option.value })); + } + + try args.append("-o"); + try args.append(try std.fmt.allocPrint(alloc, "Ciphers={s}", .{try std.mem.join(alloc, ",", &ciphers)})); + + try args.append("-o"); + try args.append(try std.fmt.allocPrint(alloc, "KexAlgorithms={s}", .{try std.mem.join(alloc, ",", &kex_algorithms)})); + + try args.append("-o"); + try args.append(try std.fmt.allocPrint(alloc, "MACs={s}", .{try std.mem.join(alloc, ",", &macs)})); + + try args.append("-l"); + try args.append(config.username); + + if (config.port != 22) { + try args.append("-p"); + try args.append(try std.fmt.allocPrint(alloc, "{d}", .{config.port})); + } + + try args.append(config.address); + }, + + .telnet => { + if (proxy_jump) |p| { + path = ssh_path; + try args.append("ssh"); + try args.append("-t"); + try args.append(try std.fmt.allocPrint(alloc, "ssh://{s}", .{p})); + try args.append("telnet"); + } else { + path = telnet_path; + try args.append("telnet"); + } + + try args.append(config.address); + + if (config.port != 23) { + try args.append(try std.fmt.allocPrint(alloc, "{d}", .{config.port})); + } + }, + } + + const pathZ = try alloc.dupeZ(u8, path); + const argsZ = try alloc.allocSentinel(?[*:0]const u8, args.items.len, null); + for (args.items, 0..) |arg, i| argsZ[i] = (try alloc.dupeZ(u8, arg)).ptr; + + for (argsZ, 0..) |arg, i| { + if (arg) |a| std.log.info("{d} {s}", .{ i, a }); + } + + if (builtin.output_mode == .Exe) { + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); + try stdout.print( + "{}", + .{ + ansi.IconNameAndWindowTitle{ + .icon_name = config.name, + .window_title = try std.fmt.allocPrint(alloc, "{s} \u{2013} {s}", .{ config.name, config.address }), + }, + }, + ); + try bw.flush(); + const envp = @as([*:null]const ?[*:0]const u8, @ptrCast(std.os.environ.ptr)); + return std.os.execveZ(pathZ, argsZ, envp); + } +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..73d3959 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,190 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const build_options = @import("build_options"); +const yazap = @import("yazap"); +const logz = @import("logz"); +const Config = @import("config.zig").Config; +const connect = @import("connect.zig"); + +const App = yazap.App; +const Arg = yazap.Arg; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const alloc = gpa.allocator(); + + try logz.setup(alloc, .{ + .level = .Debug, + .pool_size = 16, + .max_size = 4096, + .output = .stderr, + }); + + var app = App.init(alloc, "hostapps", "hostapps"); + defer app.deinit(); + + var root = app.rootCommand(); + 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| { + // if (!(connect_matches.containsArgs())) { + // try app.displaySubcommandHelp(); + // return; + // } + + var it = connect_matches.args.keyIterator(); + while (it.next()) |key| { + logz.info().string("key", key.*).log(); + } + 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; + }; + + std.log.info("id: {any}", .{identities}); + + if (identities) |i| { + logz.info().string("test", "test").log(); + 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); + } + } else { + logz.warn().string("message", "no identities").log(); + } + + 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; + // defer alloc.free(ssh_path); + } + } + + var telnet_path = connect_matches.getSingleValue("telnet-command") orelse build_options.telnet_path; + std.log.info("telnet path: {s}", .{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| { + std.log.info("expanded telnet path: {s}", .{v}); + telnet_path = v; + // defer alloc.free(telnet_path); + } + } + + std.log.info("ssh path: {s}", .{ssh_path}); + std.log.info("telnet path: {s}", .{telnet_path}); + 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]; + + std.log.debug("path {s}", .{full_path}); + 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; +}