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| { 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; }