diff options
| author | Collin Williams <96917990+bluedragon1221@users.noreply.github.com> | 2026-04-29 15:20:57 -0500 |
|---|---|---|
| committer | Collin Williams <96917990+bluedragon1221@users.noreply.github.com> | 2026-04-29 15:20:57 -0500 |
| commit | 9c9a49a855951662da62bcdec45f69ddc0099e5c (patch) | |
| tree | 1f7852971693d9e49d16a77aff51b87d39940b1b | |
| parent | 21de12cd6e63adf463674942f1a8f9b658f47945 (diff) | |
changes
26 files changed, 271 insertions, 948 deletions
@@ -21,7 +21,6 @@ Guidance for coding agents working in this repository. - `lib/nix-furnace/mkSystem.nix`: system composition. - `lib/lib.nix`: shared helpers (`my-lib`). - `pkgs/yo/`: Ruby deployment/build helper. -- `pkgs/yossh/`: Lua SSH tooling and tests. - `pkgs/jta/`: Go module. - `justfile`: command shortcuts. @@ -77,10 +76,6 @@ Go tests (`pkgs/jta`): - Run single test name: `go test ./... -run '^TestName$'` - Run single test in current package: `go test . -run '^TestName$'` -Lua tests (`pkgs/yossh`): -- Run all defined tests: `lua pkgs/yossh/tests/unit_tests.lua` -- Current single-test method is manual (temporarily call one test in `main()`). - ## 7) Architectural Conventions - Preserve homogenous module layout: - `options.nix` for option definitions. diff --git a/hosts/ganymede/config.nix b/hosts/ganymede/config.nix index 03235f0..94d67ec 100644 --- a/hosts/ganymede/config.nix +++ b/hosts/ganymede/config.nix @@ -74,6 +74,8 @@ listenAddr = "0.0.0.0"; }; + ngircd.enable = true; + qbittorrent = { enable = true; privateUrl = "bittorrent.ganymede"; @@ -96,6 +98,11 @@ publicUrl = "jta.williamsfam.us.com"; }; + ganyupload = { + enable = true; + publicUrl = "upld.williamsfam.us.com"; + }; + caddy = { enable = true; envFile = config.collinux.secrets."caddy-env".path; diff --git a/hosts/mercury/hjem.nix b/hosts/mercury/hjem.nix index 7084251..6ecbb8e 100644 --- a/hosts/mercury/hjem.nix +++ b/hosts/mercury/hjem.nix @@ -7,6 +7,7 @@ prismlauncher mpv + irssi opencode diff --git a/hosts/mercury/nixos.nix b/hosts/mercury/nixos.nix index 7db48b4..a47375a 100644 --- a/hosts/mercury/nixos.nix +++ b/hosts/mercury/nixos.nix @@ -1,6 +1,7 @@ { lib, inputs, + pkgs, ... }: { imports = [ @@ -23,6 +24,61 @@ }; programs.kdeconnect.enable = true; + security.soteria.enable = true; + + virtualisation.podman = { + enable = true; + dockerCompat = true; + }; + users.users.collin = { + extraGroups = ["podman"]; + subGidRanges = [ + { + count = 65536; + startGid = 100000; + } + ]; + subUidRanges = [ + { + count = 65536; + startUid = 100000; + } + ]; + }; + + services.jupyter = { + enable = true; + ip = "127.0.0.1"; + port = 8888; + + user = "collin"; # to access my files + + package = pkgs.python313Packages.jupyter; + command = "jupyter lab --ServerApp.token='' --ServerApp.password=''"; + password = ""; + + notebookDir = "~/brain/notes/schoolyear2025/physics"; + + kernels = { + python3 = let + python = pkgs.python313.withPackages (ps: + with ps; [ + numpy + pandas + matplotlib + sympy + ipywidgets + ipydatagrid + ipykernel + ]); + in { + language = "python"; + displayName = "Python (Physics)"; + argv = ["${python}/bin/python" "-m" "ipykernel_launcher" "-f" "{connection_file}"]; + }; + }; + }; + fileSystems."/home/collin/ganymede" = { device = "collin@ganymede:/media"; fsType = "fuse.sshfs"; diff --git a/modules/services/nixos/default.nix b/modules/services/nixos/default.nix index 32efb1c..c3652b5 100644 --- a/modules/services/nixos/default.nix +++ b/modules/services/nixos/default.nix @@ -9,9 +9,11 @@ ./cgit ./jta + ./ganyupload ./polaris.nix ./agate.nix ./minecraft.nix + ./ngircd.nix ./copyparty.nix ./qbittorrent.nix ]; diff --git a/modules/services/nixos/ganyupload/README b/modules/services/nixos/ganyupload/README new file mode 100644 index 0000000..ac9a50d --- /dev/null +++ b/modules/services/nixos/ganyupload/README @@ -0,0 +1,13 @@ +Ganyupload - File Upload Service + +Upload files using curl with a PUT request: + + curl -X PUT --data-binary @myfile.txt https://upld.williamsfam.us.com/myfile.txt + +The server will respond with "ok" on success. + +You can use any path you want: + + curl -X PUT --data-binary @document.pdf https://upld.williamsfam.us.com/docs/document.pdf + +Nested directories will be created automatically. diff --git a/modules/services/nixos/ganyupload/default.nix b/modules/services/nixos/ganyupload/default.nix new file mode 100644 index 0000000..0138100 --- /dev/null +++ b/modules/services/nixos/ganyupload/default.nix @@ -0,0 +1,47 @@ +{ + pkgs, + config, + lib, + ... +}: let + cfg = config.collinux.services.ganyupload; + + package = pkgs.callPackage ./pkg.nix {}; +in { + imports = [ + (import ../mkCaddyCfg.nix cfg) + ]; + + config = lib.mkIf cfg.enable { + users.groups."ganyupload" = {}; + users.users."ganyupload" = { + isSystemUser = true; + group = "ganyupload"; + home = "/var/lib/ganyupload"; + createHome = true; + homeMode = "755"; + }; + + systemd.services."ganyupload" = { + description = "Ganymede File Upload Service"; + restartIfChanged = true; + wants = ["network-online.target" "caddy.service"]; + after = ["network-online.target" "caddy.service"]; + + environment = { + PORT = toString cfg.port; + UPLOAD_DIR = "/media/ganyupload"; + }; + + serviceConfig = { + User = "ganyupload"; + Type = "simple"; + + WorkingDirectory = "/var/lib/ganyupload"; + ExecStart = "${package}/bin/ganyupload"; + }; + + wantedBy = ["multi-user.target"]; + }; + }; +} diff --git a/modules/services/nixos/ganyupload/go.mod b/modules/services/nixos/ganyupload/go.mod new file mode 100644 index 0000000..715e1b7 --- /dev/null +++ b/modules/services/nixos/ganyupload/go.mod @@ -0,0 +1,3 @@ +module git.ganymede/ganyupload + +go 1.25.5 diff --git a/modules/services/nixos/ganyupload/main.go b/modules/services/nixos/ganyupload/main.go new file mode 100644 index 0000000..6aa219e --- /dev/null +++ b/modules/services/nixos/ganyupload/main.go @@ -0,0 +1,104 @@ +package main + +import ( + _ "embed" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" +) + +//go:embed README +var readme string + +var uploadDir = "." + +func handleRoot(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write([]byte(readme)) +} + +func handleUpload(w http.ResponseWriter, r *http.Request) { + // Show README on GET / + if r.Method == http.MethodGet && r.URL.Path == "/" { + handleRoot(w, r) + return + } + + if r.Method != http.MethodPut { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + // Extract filename from URL path, preventing path traversal + filename := strings.TrimPrefix(r.URL.Path, "/") + if filename == "" { + http.Error(w, "filename required", http.StatusBadRequest) + return + } + + // Prevent path traversal + filename = filepath.Clean(filename) + if strings.HasPrefix(filename, "..") { + http.Error(w, "invalid filename", http.StatusBadRequest) + return + } + + fullPath := filepath.Join(uploadDir, filename) + + // Ensure the directory exists + dir := filepath.Dir(fullPath) + if err := os.MkdirAll(dir, 0755); err != nil { + log.Printf("failed to create directory: %v", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // Create the file + f, err := os.Create(fullPath) + if err != nil { + log.Printf("failed to create file: %v", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + defer f.Close() + + // Copy request body to file + written, err := io.Copy(f, r.Body) + if err != nil { + log.Printf("failed to write file: %v", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusCreated) + w.Write([]byte("ok\n")) + + log.Printf("uploaded %s (%d bytes)", filename, written) +} + +func main() { + if envUploadDir := os.Getenv("UPLOAD_DIR"); envUploadDir != "" { + uploadDir = envUploadDir + } + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + http.HandleFunc("/", handleUpload) + + log.Printf("starting ganyupload on port %s, upload directory: %s", port, uploadDir) + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatalf("server error: %v", err) + } +} diff --git a/modules/services/nixos/ganyupload/pkg.nix b/modules/services/nixos/ganyupload/pkg.nix new file mode 100644 index 0000000..18be8ed --- /dev/null +++ b/modules/services/nixos/ganyupload/pkg.nix @@ -0,0 +1,6 @@ +{pkgs ? import <nixpkgs> {system = "x86_64-linux";}, ...}: +pkgs.buildGoModule { + name = "ganyupload"; + src = ./.; + vendorHash = null; +} diff --git a/modules/services/nixos/ngircd.nix b/modules/services/nixos/ngircd.nix index 014dd68..d06a497 100644 --- a/modules/services/nixos/ngircd.nix +++ b/modules/services/nixos/ngircd.nix @@ -6,31 +6,28 @@ cfg = config.collinux.services.ngircd; in lib.mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [cfg.port]; + services.ngircd = { enable = true; config = '' [Global] - Name = irc.williamsfam.us.com + Name = williamsfam.us.com Info = Ganymede IRC Chat AdminInfo1 = Collin - Listen = 127.0.0.1 + Listen = 0.0.0.0 Ports = ${toString cfg.port} - [Options] - PAM = yes - PAMIsOptional = no + [Channel] + Name = #general + AutoJoin = yes - [Operator] - Name = collin - Password = # users authenticate with PAM - Mask = *@* + [Options] + RequireAuth = no + Ident = no + AllowedHosts = * + PAM = no ''; }; - - users.users.ngircd.extraGroups = ["shadow"]; - - security.pam.services.ngircd = { - unixAuth = true; - }; } diff --git a/modules/services/options.nix b/modules/services/options.nix index bb43180..8918781 100644 --- a/modules/services/options.nix +++ b/modules/services/options.nix @@ -114,6 +114,11 @@ in { default_port = 8072; }; + ganyupload = webserviceOptions { + service_name = "ganyupload"; + default_port = 8073; + }; + copyparty = (webserviceOptions { service_name = "copyparty"; @@ -141,6 +146,14 @@ in { }; }; + ngircd = { + enable = mkEnableOption "ncircd IRC server"; + port = mkOption { + type = lib.types.port; + default = 6667; + }; + }; + minecraft = { enable = mkEnableOption "Minecraft bedrock server"; listenAddr = mkOption { diff --git a/modules/system/nixos/boot.nix b/modules/system/nixos/boot.nix index 709a1ca..71bac70 100644 --- a/modules/system/nixos/boot.nix +++ b/modules/system/nixos/boot.nix @@ -77,7 +77,7 @@ in { enable = true; mutable = true; # necessary for installing secrets into etc }; - system.nixos-init.enable = true; + # system.nixos-init.enable = true; # store journald logs in memory services.journald.extraConfig = '' diff --git a/modules/system/nixos/polkit.nix b/modules/system/nixos/polkit.nix index 29a06f4..5bd8e33 100644 --- a/modules/system/nixos/polkit.nix +++ b/modules/system/nixos/polkit.nix @@ -69,7 +69,5 @@ in { defaultDenyRule ]; }; - - soteria.enable = true; }; } diff --git a/modules/terminal/hjem/programs/broot.nix b/modules/terminal/hjem/programs/broot.nix index 4836d50..59f16aa 100644 --- a/modules/terminal/hjem/programs/broot.nix +++ b/modules/terminal/hjem/programs/broot.nix @@ -57,7 +57,7 @@ { name = "justfile"; key = "ctrl-j"; - execution = ''just --choose --chooser "fzf --height=25% --color=bg:-1 --preview 'just --show {}'"''; + execution = "just --choose"; working_dir = "{root}"; leave_broot = false; } diff --git a/modules/terminal/hjem/programs/helix.nix b/modules/terminal/hjem/programs/helix.nix index 89d18f4..db6beba 100644 --- a/modules/terminal/hjem/programs/helix.nix +++ b/modules/terminal/hjem/programs/helix.nix @@ -33,8 +33,10 @@ command = "${pkgs.superhtml}/bin/superhtml"; args = ["lsp"]; }; - golsp = { - command = "${pkgs.gopls}/bin/gopls"; + golsp.command = "${pkgs.gopls}/bin/gopls"; + tinymist = { + command = "${pkgs.tinymist}/bin/tinymist"; + config.exportPdf = "onType"; }; dhall-lsp-server.command = "${pkgs.dhall-lsp-server}/bin/dhall-lsp-server"; }; diff --git a/modules/user/nixos/default.nix b/modules/user/nixos/default.nix index 68238d6..fbc6001 100644 --- a/modules/user/nixos/default.nix +++ b/modules/user/nixos/default.nix @@ -15,7 +15,7 @@ in { extraGroups = ["networkmanager" "disks" "input" "video" "dialout" "kvm"] ++ (lib.optional cfg.isAdmin "wheel"); }; }; - services.userborn.enable = true; + # services.userborn.enable = true; # sudo security = { diff --git a/pkgs/yossh/README.md b/pkgs/yossh/README.md deleted file mode 100644 index 56179ae..0000000 --- a/pkgs/yossh/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# Yo SSH Host Initiator (Y.O.S.H.I) -Yoshi is a lua library that wraps the `ssh` client, allowing you to define complex host configurations using the power of a full programming langage (lua) instead of the `ssh_config` dsl. - -Obligatory code example (`~/bin/yossh`): -```lua -#!/usr/bin/env lua -yoshi = require'yoshi' - -local function isHome() - local _, _, code = os.execute("nc -z -w1 192.168.50.2 2222") - return code == 0 -end - -yoshi.hosts["ganymede"] = function() - if isHome() then - return yoshi.ssh{ - HostName = "192.168.50.2", - Port = 2222, - LocalForward = 8010 - } - else - return yoshi.ssh{ - HostName = "williamsfam.us.com", - LocalForward = 8010, - DynamicFoward = 9090 - } - end -end - -yoshi.hosts["io"] = function() - if isHome() then - return yoshi.ssh{HostName = "192.168.50.3"} - else - return yoshi.ssh{ - HostName = "192.168.50.3", - User = "admin", - ProxyJump = "collin@ganymede", - } - end -end - -yoshi.run(arg) -``` - -``` -$ yossh io -Connection to 192.168.50.2 2222 port [tcp/ethernet-ip-1] succeeded! -+ ssh admin@192.168.50.3 -io:~$ -``` - -## 🍄 Getting Started -First, clone this repo, and make sure you have ssh and lua installed. -``` -sudo apt install ssh luajit5.4 # or whatever -git clone https://github.com/bluedragon1221/yoshi -``` - -Next, place `yoshi.lua` in a place that lua can find it. -Consult the [lua documentation](https://www.lua.org/pil/8.1.html) for help. - -Now you can write a simple configuration. -As an example, let's look at the equivelant of this `ssh_config` block: -``` -Host box - HostName 192.168.50.3 - User yoshi -``` - -Using Yoshi, it would look like this: -```lua --- yoshi_cfg.lua -yoshi = require'yoshi' - -yoshi.hosts["box"] = yoshi.ssh{ - HostName = "192.168.50.3", - User = "yoshi" -} - -yoshi.run(arg) -``` - -> (To create more complicated configurations, read the [docs](./doc/lua_doc.md)) - -To test your Yoshi configuration, you can run `lua yoshi_cfg.lua`. -This would be annoying to type every time you need to access your remote server, though! -So we'll place the script in the `$PATH` (ex. `~/bin/yossh` or something), and use a shabang to tell the script to execute with lua: -```lua -#!/usr/bin/env lua -yoshi = require'yoshi' - -yoshi.hosts["box"] = yoshi.ssh{ - HostName = "192.168.50.3", - User = "yoshi" -} - -yoshi.run(arg) -``` - -Assuming `~/bin` is in the `$PATH` and you made `yossh` executable with `chmod +x`, running `yossh` in the terminal will give you a nice help page: -```bash -$ yossh -yossh - smart SSH wrapper - -USAGE: - yossh <host> - yossh <user@host> - yossh <host:port> - yossh <user@host:port> - -AVAILABLE HOSTS: - - box -``` - -And you can ssh into `box`: -```bash -$ yossh box -+ ssh yoshi@192.168.50.3 -io:~$ whoami -yoshi -``` - -## 📖 Docs -- [`doc/lua_doc.md`](doc/lua_doc.md) contains comprehensive docs for the Yoshi lua api -- [`doc/nix.md`](doc/nix.md) discusses packaging your Yoshi configuration with nix - ---- - -[](https://brainmade.org) diff --git a/pkgs/yossh/default.nix b/pkgs/yossh/default.nix deleted file mode 100644 index 4ea0127..0000000 --- a/pkgs/yossh/default.nix +++ /dev/null @@ -1,52 +0,0 @@ -{ - pkgs, - inputs, - ... -}: let - yossh = - pkgs.writeText "yossh.lua" - # lua - '' - yoshi = dofile'${./yoshi.lua}' - - local function isHome() - local _, _, code = os.execute("nc -z -w1 192.168.50.2 2222") - return code == 0 - end - - yoshi.hosts["ganymede"] = function() - if isHome() then - return yoshi.ssh{ - HostName = "192.168.50.2", - DynamicForward = 9090, - Port = 2222 - } - else - return yoshi.ssh{ - HostName = "williamsfam.us.com", - DynamicForward = 9090 - } - end - end - - yoshi.hosts["io"] = function() - if isHome() then - return yoshi.ssh{ - HostName = "192.168.50.3", - User = "admin", - } - else - return yoshi.ssh{ - HostName = "192.168.50.3", - User = "admin", - ProxyJump = "collin@ganymede", - } - end - end - - yoshi.run(arg) - ''; -in - pkgs.writeShellScriptBin "yossh" '' - exec ${pkgs.lua}/bin/lua ${yossh} "$@" - '' diff --git a/pkgs/yossh/doc/lua_doc.md b/pkgs/yossh/doc/lua_doc.md deleted file mode 100644 index b029f56..0000000 --- a/pkgs/yossh/doc/lua_doc.md +++ /dev/null @@ -1,118 +0,0 @@ -# Lua Documentation -Lua API documentation for Yoshi - -## `yoshi.hosts` -- Description: List of defined ssh hosts. Each entry must be a function that returns an `sshCommand` when called. -- Type: table of `function(): sshCommand` -- Example: -```lua -yoshi.hosts["webserver"] = function() - -- this is just a random example to show why you might want yoshi.hosts[host] to be a function - if os.getenv("IM_FEELING") == "sad" then - return yoshi.ssh{HostName = "sad.webserver.local"} - else - return yoshi.ssh{HostName = "happy.webserver.local"} - end -end -``` - -## `yoshi.ssh` -- Description: Constructs an `sshCommand` -- Type: `function(table): sshCommand` -- Example: -```lua -yoshi.ssh{ - HostName = "webserver.local", - Port = 2227, - ProxyJump = "bastion" -} -``` - -## `yoshi.sshCommand.User` -- Description: Specifies the target user in the ssh connection. Analogous to the `User` key in `ssh_config` -- Type: string or nil -- Example: `yoshi`, `collin123` - -## `yoshi.sshCommand.HostName` -- Description: Specifies the target host in the ssh connection. The only required element to `yoshi.sshCommand`. Analogous to the `HostName` key in `ssh_config` -- Type: string -- Example: `192.168.0.27`, `github.com`, `140.82.114.3` - -## `yoshi.sshCommand.Port` -- Description: Specifies the target port in the ssh connection. Defaults to `22` if unspecified. Analagous to the `Port` key in `ssh_config` -- Type: integer, `0 <= i <= 65535` -- Examples: `22`, `2222`, `2225` - -## `yoshi.sshCommand.RemoteCommand` -- Description: Specifies a command to run on the target host instead of the default shell. Analagous to the `RemoteCommand` key in `ssh_config` -- Type: string -- Examples: `whoami`, `nixos-rebuild switch --flake /etc/nixos`, `tmux new-session -A` - -## `yoshi.sshCommand.SessionType` -- Description: Specifies the type of ssh session. "none" means that no shell access is provded. Analogous to the `SessionType` key in `ssh_config` -- Type: "subsystem", "none", or "default" -- Examples: `"none"` - -## `yoshi.sshCommand.LocalForward` -- Description: Specifies a port on the target host to forward to the local host in the format `{local_port, target_port}`. If no local port is specified, will assume host port and local port are the same. Analagous to the `LocalForward` key in `ssh_config` or the `-L` flag on the command line -- Type: `table[2] of integer` or integer, `0 <= i <= 65535` -- Examples: `8010`, `{8010, 80}` - -## `yoshi.sshCommand.DynamicForward` -- Description: Specifies a port on the local host to serve as a SOCKS5 proxy, forwarding all traffic through the target host. Analagous to the `DynamicForward` key in `ssh_config` or the `-D` option on the command line -- Type: integer, `0 <= i <= 65535` -- Examples: `9090` - -## `yoshi.sshCommand.ProxyJump` -- Description: Specifies a host to act as a gateway to the target host if not directly accessable by the local host. It will inherit `User`, `Port`, and `HostName` options from `yoshi.hosts[host]` if it exists. Analagous to the `ProxyJump` key in `ssh_config` or the `-J` flag on the command line -- Type: string in the form `user@host:port` -- Example: `box`, `yoshi@box`, `box:27`, `yoshi@box:27` -- Full Example: -```lua -yoshi.hosts["bastion"] = sshCommand.ssh{ - HostName = "bastion.mywebsite.com", - Port = 23, -} - -yoshi.hosts["server"] = sshCommand.ssh{ - HostName = "192.168.0.2", - ProxyJump = "myuser@bastion" -} - -assert_eq( - yoshi.getHost("server"):toCommand(), - "ssh -J myuser@bastion.mywebsite.com:23 server" -) -``` - -## `yoshi.sshCommand:toCommand` -- Description: Formats `self` as a valid `ssh` command to be run in the terminal. -- Type: `sshCommand:function(): string` -- Example: -```lua -os.execute(yoshi.getHost("io"):toCommand()) -``` - -## `yoshi.getHost` -- Description: Looks up and evaluates `yoshi.hosts[hostname]`, returning the resulting `sshCommand`. Raises an error if the host is not found or the host function returns nil. -- Type: `function(string): sshCommand` -- Example: -```lua -yoshi.hosts["webserver"] = function() - return yoshi.ssh{ - HostName = "webserver.local", - Port = 22, - ProxyJump = "bastion" - } -end - -os.execute(yoshi.getHost("webserver"):toCommand()) -``` - -## `yoshi.run` -- Description: Executes the ssh command associated with the given target host as provided by `arg`, a list of commandline arguments. If no arguments are provided, prints a help page. The first argument is a path of the form `[jumphost/.../]host`, where any leading path segments are used as explicit proxy jump hops. Subsequent arguments may include ssh flags such as `-D`, `-L`, `-N`, or `-- command`. -- Type: `function(table of string, table of any)` -- Example: -```lua -yoshi.run(arg, {dry_run = true}) -- arg is the global name for the lua program's commandline arguments -``` diff --git a/pkgs/yossh/doc/nix.md b/pkgs/yossh/doc/nix.md deleted file mode 100644 index 9631392..0000000 --- a/pkgs/yossh/doc/nix.md +++ /dev/null @@ -1,44 +0,0 @@ -# Packing your Yoshi configuration with nix -If you want to declare Yoshi in your nixos configuration, here's how. - -First, add Yoshi to your nix flake: -```nix -{ - inputs = { - yoshi = { - url = "github:bluedragon1221/yoshi"; - flake = false; - }; - }; -} -``` - -Next, write a package for Yoshi. This is how I achieved it: -```sh -{ - pkgs, - inputs, - ... -}: let - yossh = pkgs.writeText "yossh.lua" '' - yoshi = dofile'${inputs.yoshi-lua}/yoshi.lua' - - yoshi.hosts["box"] = yoshi.ssh{ - HostName = "192.168.50.3", - User = "yoshi" - } - - yoshi.run(arg) - ''; -in - pkgs.writeShellScriptBin "yossh" '' - exec ${pkgs.lua}/bin/lua ${yossh} "$@" - '' -``` - -Then add the package in `environment.systemPackages`: -```nix -environment.systemPackages = [ - (pkgs.callPackage ./yossh.nix {inherit inputs;}) -] -``` diff --git a/pkgs/yossh/examples/home_wifi.lua b/pkgs/yossh/examples/home_wifi.lua deleted file mode 100755 index 8df2aad..0000000 --- a/pkgs/yossh/examples/home_wifi.lua +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh -yoshi = dofile'yoshi.lua' - -local function isHome() - local ok, reason, code = os.execute("nc -z -w1 192.168.50.2 2222") - return code == 0 -end - -yoshi.hosts["ganymede"] = function() - if isHome() then - return yoshi.ssh{ - HostName = "192.168.50.2", - Port = 2222, - LocalForward = 8010 - } - else - return yoshi.ssh{ - HostName = "williamsfam.us.com", - LocalForward = 8010, - DynamicForward = 9090 - } - end -end - -yoshi.hosts["io"] = function() - return yoshi.ssh{ - HostName = "192.168.50.3", - User = "admin", - ProxyJump = isHome() or "collin@ganymede", - } -end - -yoshi.run(arg, {dry_run = true}) diff --git a/pkgs/yossh/examples/mega_bastion.lua b/pkgs/yossh/examples/mega_bastion.lua deleted file mode 100644 index d4eb3ee..0000000 --- a/pkgs/yossh/examples/mega_bastion.lua +++ /dev/null @@ -1,61 +0,0 @@ --- situation: fictional network where there are three departments: --- - tech (172.16.10.0/24) --- - HR (172.16.20.0/24) --- - Secretary (172.16.40.0/24) --- Each department has --- - an ssh bastion server at .1:22 (only way to access other devices on the subnet from outside the subnet) --- - a shared sftp server at .2:25 --- --- This exact yoshi script would be deployed to all personal computers inside the network. -local yoshi = dofile"yoshi.lua" - -local function inNetwork() - -- company internal landing page - local _, _, code = os.execute("nc -z -w1 172.16.0.1 443") - return code == 0 -end - -local function currDept() - local ip_addr = io.popen("hostname -I") - local _, _, third_octet, _ = string.gmatch(ip_addr, "%d%.%d%.%d%.%d") - - return third_octet -end - -local depts = { - tech = 10, - hr = 20, - secretary = 40 -} - -yoshi.hosts["global-bastion"] = yoshi.ssh{ - HostName = "bastion.company-website.com", - Port = 2222 -} - -for name, subnet in pairs(depts) do - yoshi.hosts[name.."-bastion"] = function() - return yoshi.ssh{ - HostName = "172.16."..subnet..".1", - ProxyJump = inNetwork() or "global-bastion" - } - end - - yoshi.hosts[name.."-fileserver"] = function() - return yoshi.ssh{ - HostName = "172.16."..subnet..".2", - SessionType = "none", - LocalForward = {2225, 25}, - ProxyJump = inNetwork() and currDept() == subnet or name.."-bastion" - } - end -end - -yoshi.hosts[name.."-db"] = yoshi.ssh{ - HostName = "172.16.10.3", - SessionType = "none", - LocalForward = 5432, - ProxyJump = inNetwork() and currDept() == subnet or "tech-bastion" -} - -yoshi.run(arg, {dry_run = true}) diff --git a/pkgs/yossh/examples/test.lua b/pkgs/yossh/examples/test.lua deleted file mode 100644 index a594305..0000000 --- a/pkgs/yossh/examples/test.lua +++ /dev/null @@ -1,18 +0,0 @@ -yoshi = dofile'yoshi.lua' - -yoshi.hosts["you"] = function() - return yoshi.ssh{ - HostName = "192.168.50.2", - DynamicForward = 7320, - Port = 27 - } -end - -yoshi.hosts["me"] = function() - return yoshi.ssh{ - HostName = "test.local", - ProxyJump = "jumper@you" - } -end - -yoshi.run(arg, {dry_run = true}) diff --git a/pkgs/yossh/tests/unit_tests.lua b/pkgs/yossh/tests/unit_tests.lua deleted file mode 100644 index 0ce044c..0000000 --- a/pkgs/yossh/tests/unit_tests.lua +++ /dev/null @@ -1,190 +0,0 @@ --- ChatGPT wrote my unit tests - -local yoshi = dofile'yoshi.lua' -local tests = {} - ----------------------------------------------------------------- --- Minimal assert functions ----------------------------------------------------------------- - -local function fail(msg) - error("TEST FAILED: " .. msg, 2) -end - -local function assertEqual(a, b, msg) - if a ~= b then fail(msg or string.format("%s ~= %s", tostring(a), tostring(b))) end -end - -local function assertNotEqual(a, b, msg) - if a == b then fail(msg or string.format("%s == %s", tostring(a), tostring(b))) end -end - -local function assertDeepEqual(a, b, msg) - local function compare(x, y) - if type(x) ~= type(y) then return false end - if type(x) ~= "table" then return x == y end - local k_checked = {} - for k,v in pairs(x) do - if not compare(v, y[k]) then return false end - k_checked[k] = true - end - for k,_ in pairs(y) do - if not k_checked[k] then return false end - end - return true - end - if not compare(a,b) then fail(msg or "Tables not equal") end -end - ----------------------------------------------------------------- --- Helpers ----------------------------------------------------------------- - --- reset hosts between tests -local function resetHosts() - yoshi.hosts = {} -end - ----------------------------------------------------------------- --- Tests ----------------------------------------------------------------- - -function tests.test_constructor() - local cmd = yoshi.ssh{ - HostName = "localhost", - User = "me", - Port = 2222, - SessionType = "none", - DynamicForward = 8080, - LocalForward = 8000, - RemoteCommand = "ls -la", - } - - assertEqual(cmd.HostName, "localhost") - assertEqual(cmd.User, "me") - assertEqual(cmd.Port, 2222) - assertEqual(cmd.SessionType, "none") - assertEqual(cmd.DynamicForward, 8080) - assertDeepEqual(cmd.LocalForward, {8000,8000}) - assertEqual(cmd.RemoteCommand, "ls -la") -end - -function tests.test_proxyjump() - resetHosts() - - yoshi.hosts["bastion"] = function() - return yoshi.ssh{HostName="bastion.internal", User="bastionuser"} - end - - local cmd = yoshi.ssh{ - HostName = "server.internal", - User = "admin", - ProxyJump = "bastion", - } - - assertEqual(#cmd.ProxyJumps, 1) - assertEqual(cmd.ProxyJumps[1].host, "bastion.internal") - assertEqual(cmd.ProxyJumps[1].user, "bastionuser") -end - -function tests.test_nested_proxyjump() - resetHosts() - yoshi.hosts["jump1"] = function() - return yoshi.ssh{HostName="jump1.host", ProxyJump="jump2"} - end - yoshi.hosts["jump2"] = function() - return yoshi.ssh{HostName="jump2.host", User="jumpuser"} - end - - local cmd = yoshi.ssh{ - HostName = "target.host", - ProxyJump = "jump1" - } - - local hops = cmd.ProxyJumps - assertEqual(#hops, 2) - assertEqual(hops[1].host, "jump2.host") - assertEqual(hops[1].user, "jumpuser") - assertEqual(hops[2].host, "jump1.host") -end - -function tests.test_cli_hop_override() - resetHosts() - yoshi.hosts["ganymede"] = function() return yoshi.ssh{HostName="192.168.50.2"} end - yoshi.hosts["io"] = function() return yoshi.ssh{HostName="192.168.50.3", ProxyJump="ganymede"} end - - local arg = {"collin@ganymede/io"} - local target = yoshi.getHost("io") - local path = {"collin@ganymede"} -- simulate split_path(arg[1]) - - local acc = {} - for i = 1, #path do - local hop = yoshi.parse_user_host_port(path[i]) - yoshi.collect_proxy_jumps(hop, acc) - end - - target.ProxyJumps = acc - assertEqual(#target.ProxyJumps, 1) - assertEqual(target.ProxyJumps[1].user, "collin") - assertEqual(target.ProxyJumps[1].host, "192.168.50.2") -end - -function tests.test_toCommand() - resetHosts() - local cmd = yoshi.ssh{ - HostName = "localhost", - User = "me", - Port = 2222, - DynamicForward = 8080, - LocalForward = 8000, - SessionType = "none", - RemoteCommand = "ls" - } - - local text = cmd:toCommand() - assert(text:match("ssh")) - assert(text:match("-D 8080")) - assert(text:match("-L 8000:127.0.0.1:8000")) - assert(text:match("-N")) - assert(text:match("me@localhost")) - assert(text:match("ls")) -end - -function tests.test_LocalForward_tuple() - local cmd = yoshi.ssh{ - HostName="localhost", - LocalForward={9000,9001} - } - local text = cmd:toCommand() - assert(text:match("-L 9000:127.0.0.1:9001")) -end - -function tests.test_DynamicForward() - local cmd = yoshi.ssh{ - HostName="localhost", - DynamicForward=9999 - } - local text = cmd:toCommand() - assert(text:match("-D 9999")) -end - -function tests.test_RemoteCommand() - local cmd = yoshi.ssh{HostName="localhost", RemoteCommand="echo hello"} - local text = cmd:toCommand() - assert(text:match("echo hello")) -end - ----------------------------------------------------------------- --- Run tests ----------------------------------------------------------------- - -local function main() - tests.test_constructor() - tests.test_proxyjump() - tests.test_nested_proxyjump() - tests.test_cli_hop_override() - tests.test_toCommand() - tests.test_LocalForward_tuple() - tests.test_DynamicForward() - tests.test_RemoteCommand () -end diff --git a/pkgs/yossh/yoshi.lua b/pkgs/yossh/yoshi.lua deleted file mode 100644 index ce2086a..0000000 --- a/pkgs/yossh/yoshi.lua +++ /dev/null @@ -1,276 +0,0 @@ -local SSHTarget = {} - -function SSHTarget:clone() - return { - user = self.user, - host = self.host, - port = self.port - } -end - -local function parse_user_host_port(input) - local user, rest = input:match("^(.-)@(.*)$") - if not rest then rest = input end - - local host, port_str = rest:match("^(.-):(%d+)$") - local port - if not host then - host = rest - else - port = tonumber(port_str) - end - - return { - user = user, - host = host, - port = port, - } -end - -local function split_path(path) - local t = {} - for seg in path:gmatch("[^/]+") do - table.insert(t, seg) - end - return t -end - -local yoshi = { - hosts = {} -} - -local SSHConfig = {} -local SSHCommand = {} - -function SSHCommand:clone() - local new_proxy_jumps - if self.proxy_jumps then - new_proxy_jumps = {} - for _, j in ipairs(self.proxy_jumps) do - table.insert(new_proxy_jumps, { - user = j.user, - host = j.host, - port = j.port, - }) - end - end - - return setmetatable({ - target = { - user = self.target.user, - host = self.target.host, - port = self.target.port, - }, - session_type = self.session_type, - dynamic_forward = self.dynamic_forward, - local_forward = self.local_forward and { self.local_forward[1], self.local_forward[2] } or nil, - remote_command = self.remote_command, - proxy_jumps = new_proxy_jumps, - }, { __index = SSHCommand }) -end - -function SSHCommand:overlayTarget(target) - local new = self:clone() - if target.user then - new.target.user = target.user - end - - if target.port then - new.target.port = target.port - end - - return new -end - -function yoshi.getHost(hostname) - local base = yoshi.hosts[hostname] - if not base then - error("Unknown host: " .. hostname) - end - - local cmd = base() - if cmd then - return cmd - else - error("Calling host returned nil") - end -end - -function yoshi.ssh(o) - local local_forward - local l = o.LocalForward - if type(l) == "number" then - local_forward = { l, l } - else - local_forward = l - end - - local proxy_jumps = {} - if o.ProxyJump then - local u = parse_user_host_port(o.ProxyJump) - local jmp = yoshi.getHost(u.host):overlayTarget(u) - - table.insert(proxy_jumps, jmp.target) - if #jmp.proxy_jumps ~= 0 then - for _, p in ipairs(jmp.proxy_jumps) do - table.insert(proxy_jumps, p) - end - end - end - - local self = setmetatable({ - target = { - user = o.User, - host = o.HostName, - port = o.Port or 22, - }, - session_type = o.SessionType or "default", - dynamic_forward = o.DynamicForward, - local_forward = local_forward, - remote_command = o.RemoteCommand, - - proxy_jumps = proxy_jumps, - }, { __index = SSHCommand }) - - return self -end - -function SSHCommand:toCommand() - local ret = { "ssh" } - - if self.dynamic_forward then - table.insert(ret, "-D") - table.insert(ret, tostring(self.dynamic_forward)) - end - - if self.local_forward then - table.insert(ret, "-L") - local lf = self.local_forward - table.insert(ret, - lf[1] .. ":127.0.0.1:" .. lf[2]) - - end - - if self.session_type == "none" then - table.insert(ret, "-N") - end - - if self.proxy_jumps and #self.proxy_jumps > 0 then - local js = {} - for _, j in ipairs(self.proxy_jumps) do - local s = (j.user and j.user .. "@" or "") .. j.host - if j.port and j.port ~= 22 then s = s .. ":" .. j.port end - table.insert(js, s) - end - table.insert(ret, "-J") - table.insert(ret, table.concat(js, ",")) - end - - if self.target.port and self.target.port ~= 22 then - table.insert(ret, "-p") - table.insert(ret, tostring(self.target.port)) - end - - table.insert(ret, (self.target.user and self.target.user .. "@" or "") .. self.target.host) - - if self.remote_command then - table.insert(ret, "-t") - table.insert(ret, "'" .. self.remote_command .. "'") - end - - return table.concat(ret, " ") -end - -function SSHCommand:overlayCliOpts(cli_args) - local new = self:clone() - - local i = 2 - while i <= #cli_args do - local a = cli_args[i] - - if a == "-D" then - new.dynamic_forward = tonumber(cli_args[i + 1]) - i = i + 2 - - elseif a == "-L" then - local spec = cli_args[i + 1] - - local port = tonumber(spec) - if port then - new.local_forward = { port, port } - i = i + 2 - - else - local l, r = spec:match("^(%d+):.*:(%d+)$") - if not l or not r then - error("Invalid -L spec: " .. spec) - end - new.local_forward = { tonumber(l), tonumber(r) } - i = i + 2 - end - - elseif a == "-N" then - new.session_type = "none" - i = i + 1 - - elseif a == "--" then - local parts = {} - for j = i + 1, #cli_args do - table.insert(parts, cli_args[j]) - end - new.remote_command = table.concat(parts, " ") - break - - else - error("Unknown or unsupported ssh option: " .. a) - end - end - - return new -end - -local function show_help() - print("Usage: ssh <[jumphost/...]host> [options] [-- command]") - print("\nAvailable hosts:") - for h, _ in pairs(yoshi.hosts) do - print(" - " .. h) - end -end - -function yoshi.run(cli_opts, opts) - if not cli_opts then - error("Please pass `arg` to yoshi.run (ex. `yoshi.run(arg)`)") - end - - if cli_opts[1] == nil then - show_help() - os.exit(0) - end - - local path = split_path(cli_opts[1]) - local destName = path[#path] - - local target = yoshi.getHost(destName) - - local acc = {} - if #path > 1 then - for i = 1, #path - 1 do - local hop = parse_user_host_port(path[i]) - table.insert(acc, { - user = hop.user, - host = hop.host, - port = hop.port, - }) - end - target.proxy_jumps = acc - end - - local cmd = target:overlayCliOpts(cli_opts) - print("+ " .. cmd:toCommand()) - - if not opts or not opts.dry_run then - os.execute(cmd) - end -end - -return yoshi |
