@jaspermayone.com's dotfiles
at main 490 lines 18 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.atelier.bore; 9 10 boreScript = pkgs.writeShellScript "bore" '' 11 CONFIG_FILE="bore.toml" 12 13 # Trap exit signals to ensure cleanup and exit immediately 14 trap 'exit 130' INT 15 trap 'exit 143' TERM 16 trap 'exit 129' HUP 17 18 # Enable immediate exit on error or pipe failure 19 set -e 20 set -o pipefail 21 22 # Check for flags 23 if [ "$1" = "--list" ] || [ "$1" = "-l" ]; then 24 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Active tunnels" 25 echo 26 27 tunnels=$(${pkgs.curl}/bin/curl -s https://${cfg.domain}/api/proxy/http) 28 29 if ! echo "$tunnels" | ${pkgs.jq}/bin/jq -e '.proxies | length > 0' >/dev/null 2>&1; then 30 ${pkgs.gum}/bin/gum style --foreground 117 "No active tunnels" 31 exit 0 32 fi 33 34 # Filter only online tunnels with valid conf 35 echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | if .type == "http" then "\(.name) https://\(.conf.subdomain).${cfg.domain} [http]" elif .type == "tcp" then "\(.name) tcp://\(.conf.remotePort) localhost:\(.conf.localPort) [tcp]" elif .type == "udp" then "\(.name) udp://\(.conf.remotePort) localhost:\(.conf.localPort) [udp]" else "\(.name) [\(.type)]" end' | while read -r line; do 36 ${pkgs.gum}/bin/gum style --foreground 35 " $line" 37 done 38 exit 0 39 fi 40 41 if [ "$1" = "--saved" ] || [ "$1" = "-s" ]; then 42 if [ ! -f "$CONFIG_FILE" ]; then 43 ${pkgs.gum}/bin/gum style --foreground 117 "No bore.toml found in current directory" 44 exit 0 45 fi 46 47 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Saved tunnels in bore.toml" 48 echo 49 50 # Parse TOML and show tunnels 51 while IFS= read -r line; do 52 if [[ "$line" =~ ^\[([^]]+)\] ]]; then 53 current_tunnel="''${BASH_REMATCH[1]}" 54 elif [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then 55 port="''${BASH_REMATCH[1]}" 56 elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then 57 protocol="''${BASH_REMATCH[1]}" 58 elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then 59 label="''${BASH_REMATCH[1]}" 60 proto_display="''${protocol:-http}" 61 ${pkgs.gum}/bin/gum style --foreground 35 " $current_tunnel localhost:$port [$proto_display] [$label]" 62 label="" 63 protocol="" 64 elif [[ -z "$line" ]] && [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then 65 proto_display="''${protocol:-http}" 66 ${pkgs.gum}/bin/gum style --foreground 35 " $current_tunnel localhost:$port [$proto_display]" 67 current_tunnel="" 68 port="" 69 protocol="" 70 fi 71 done < "$CONFIG_FILE" 72 73 # Handle last entry if file doesn't end with blank line 74 if [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then 75 proto_display="''${protocol:-http}" 76 if [[ -n "$label" ]]; then 77 ${pkgs.gum}/bin/gum style --foreground 35 " $current_tunnel localhost:$port [$proto_display] [$label]" 78 else 79 ${pkgs.gum}/bin/gum style --foreground 35 " $current_tunnel localhost:$port [$proto_display]" 80 fi 81 fi 82 exit 0 83 fi 84 85 # Get tunnel name/subdomain 86 if [ -n "$1" ]; then 87 tunnel_name="$1" 88 else 89 # Check if we have a bore.toml in current directory 90 if [ -f "$CONFIG_FILE" ]; then 91 # Count tunnels in TOML 92 tunnel_count=$(${pkgs.gnugrep}/bin/grep -c '^\[' "$CONFIG_FILE" 2>/dev/null || echo "0") 93 94 if [ "$tunnel_count" -gt 0 ]; then 95 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel" 96 echo 97 98 # Show choice between new or saved 99 choice=$(${pkgs.gum}/bin/gum choose "New tunnel" "Use saved tunnel") 100 101 if [ "$choice" = "Use saved tunnel" ]; then 102 # Extract tunnel names from TOML 103 saved_names=$(${pkgs.gnugrep}/bin/grep '^\[' "$CONFIG_FILE" | ${pkgs.gnused}/bin/sed 's/^\[\(.*\)\]$/\1/') 104 tunnel_name=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose) 105 106 if [ -z "$tunnel_name" ]; then 107 ${pkgs.gum}/bin/gum style --foreground 196 "No tunnel selected" 108 exit 1 109 fi 110 111 # Parse TOML for this tunnel's config 112 in_section=false 113 while IFS= read -r line; do 114 if [[ "$line" =~ ^\[([^]]+)\] ]]; then 115 if [[ "''${BASH_REMATCH[1]}" = "$tunnel_name" ]]; then 116 in_section=true 117 else 118 in_section=false 119 fi 120 elif [[ "$in_section" = true ]]; then 121 if [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then 122 port="''${BASH_REMATCH[1]}" 123 elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then 124 protocol="''${BASH_REMATCH[1]}" 125 elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then 126 label="''${BASH_REMATCH[1]}" 127 fi 128 fi 129 done < "$CONFIG_FILE" 130 131 proto_display="''${protocol:-http}" 132 ${pkgs.gum}/bin/gum style --foreground 35 " Loaded from bore.toml: $tunnel_name localhost:$port [$proto_display]''${label:+ [$label]}" 133 else 134 # New tunnel - prompt for protocol first to determine what to ask for 135 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp") 136 if [ -z "$protocol" ]; then 137 protocol="http" 138 fi 139 140 if [ "$protocol" = "http" ]; then 141 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ") 142 else 143 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ") 144 fi 145 146 if [ -z "$tunnel_name" ]; then 147 ${pkgs.gum}/bin/gum style --foreground 196 "No name provided" 148 exit 1 149 fi 150 fi 151 else 152 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel" 153 echo 154 # Prompt for protocol first 155 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp") 156 if [ -z "$protocol" ]; then 157 protocol="http" 158 fi 159 160 if [ "$protocol" = "http" ]; then 161 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ") 162 else 163 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ") 164 fi 165 166 if [ -z "$tunnel_name" ]; then 167 ${pkgs.gum}/bin/gum style --foreground 196 "No name provided" 168 exit 1 169 fi 170 fi 171 else 172 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel" 173 echo 174 # Prompt for protocol first 175 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp") 176 if [ -z "$protocol" ]; then 177 protocol="http" 178 fi 179 180 if [ "$protocol" = "http" ]; then 181 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ") 182 else 183 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ") 184 fi 185 186 if [ -z "$tunnel_name" ]; then 187 ${pkgs.gum}/bin/gum style --foreground 196 "No name provided" 188 exit 1 189 fi 190 fi 191 fi 192 193 # Validate tunnel name (only for http subdomains) 194 if [ "$protocol" = "http" ]; then 195 if ! echo "$tunnel_name" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then 196 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)" 197 exit 1 198 fi 199 fi 200 201 # Get port (skip if loaded from saved config) 202 if [ -z "$port" ]; then 203 if [ -n "$2" ]; then 204 port="$2" 205 else 206 port=$(${pkgs.gum}/bin/gum input --placeholder "8000" --prompt "Local port: ") 207 if [ -z "$port" ]; then 208 ${pkgs.gum}/bin/gum style --foreground 196 "No port provided" 209 exit 1 210 fi 211 fi 212 fi 213 214 # Validate port 215 if ! echo "$port" | ${pkgs.gnugrep}/bin/grep -qE '^[0-9]+$'; then 216 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid port (must be a number)" 217 exit 1 218 fi 219 220 # Get optional protocol, label and save flag (skip if loaded from saved config) 221 save_config=false 222 if [ -z "$label" ]; then 223 shift 2 2>/dev/null || true 224 while [[ $# -gt 0 ]]; do 225 case "$1" in 226 --protocol|-p) 227 protocol="$2" 228 shift 2 229 ;; 230 --label|-l) 231 label="$2" 232 shift 2 233 ;; 234 --save) 235 save_config=true 236 shift 237 ;; 238 *) 239 shift 240 ;; 241 esac 242 done 243 244 # Prompt for protocol if not provided via flag and not loaded from saved config and not already set 245 if [ -z "$protocol" ]; then 246 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp") 247 if [ -z "$protocol" ]; then 248 protocol="http" 249 fi 250 fi 251 252 # Prompt for label if not provided via flag and not loaded from saved config 253 if [ -z "$label" ]; then 254 # Allow multiple labels selection 255 labels=$(${pkgs.gum}/bin/gum choose --no-limit --header "Labels (select multiple):" "dev" "prod" "custom") 256 257 if [ -n "$labels" ]; then 258 # Check if custom was selected 259 if echo "$labels" | ${pkgs.gnugrep}/bin/grep -q "custom"; then 260 custom_label=$(${pkgs.gum}/bin/gum input --placeholder "my-label" --prompt "Custom label: ") 261 if [ -z "$custom_label" ]; then 262 ${pkgs.gum}/bin/gum style --foreground 196 "No custom label provided" 263 exit 1 264 fi 265 # Replace 'custom' with the actual custom label 266 labels=$(echo "$labels" | ${pkgs.gnused}/bin/sed "s/custom/$custom_label/") 267 fi 268 # Join labels with comma 269 label=$(echo "$labels" | ${pkgs.coreutils}/bin/tr '\n' ',' | ${pkgs.gnused}/bin/sed 's/,$//') 270 fi 271 fi 272 fi 273 274 # Default protocol to http if still not set 275 if [ -z "$protocol" ]; then 276 protocol="http" 277 fi 278 279 # Check if local port is accessible 280 if ! ${pkgs.netcat}/bin/nc -z 127.0.0.1 "$port" 2>/dev/null; then 281 ${pkgs.gum}/bin/gum style --foreground 214 "! Warning: Nothing listening on localhost:$port" 282 fi 283 284 # Save configuration if requested 285 if [ "$save_config" = true ]; then 286 # Check if tunnel already exists in TOML 287 if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$tunnel_name\]" "$CONFIG_FILE"; then 288 # Update existing entry 289 ${pkgs.gnused}/bin/sed -i "/^\[$tunnel_name\]/,/^\[/{ 290 s/^port[[:space:]]*=.*/port = $port/ 291 s/^protocol[[:space:]]*=.*/protocol = \"$protocol\"/ 292 ''${label:+s/^label[[:space:]]*=.*/label = \"$label\"/} 293 }" "$CONFIG_FILE" 294 else 295 # Append new entry 296 { 297 echo "" 298 echo "[$tunnel_name]" 299 echo "port = $port" 300 if [ "$protocol" != "http" ]; then 301 echo "protocol = \"$protocol\"" 302 fi 303 if [ -n "$label" ]; then 304 echo "label = \"$label\"" 305 fi 306 } >> "$CONFIG_FILE" 307 fi 308 309 ${pkgs.gum}/bin/gum style --foreground 35 " Configuration saved to bore.toml" 310 echo 311 fi 312 313 # Create config file 314 config_file=$(${pkgs.coreutils}/bin/mktemp) 315 trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT 316 317 # Encode label into proxy name if provided (format: tunnel_name[label1,label2]) 318 proxy_name="$tunnel_name" 319 if [ -n "$label" ]; then 320 proxy_name="''${tunnel_name}[''${label}]" 321 fi 322 323 # Build proxy configuration based on protocol 324 if [ "$protocol" = "http" ]; then 325 ${pkgs.coreutils}/bin/cat > $config_file <<EOF 326 serverAddr = "${cfg.serverAddr}" 327 serverPort = ${toString cfg.serverPort} 328 329 auth.method = "token" 330 auth.tokenSource.type = "file" 331 auth.tokenSource.file.path = "${cfg.authTokenFile}" 332 333 [[proxies]] 334 name = "$proxy_name" 335 type = "http" 336 localIP = "127.0.0.1" 337 localPort = $port 338 subdomain = "$tunnel_name" 339 EOF 340 elif [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then 341 # For TCP/UDP, enable admin API to query allocated port 342 admin_port=$(${pkgs.python3}/bin/python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()') 343 344 ${pkgs.coreutils}/bin/cat > $config_file <<EOF 345 serverAddr = "${cfg.serverAddr}" 346 serverPort = ${toString cfg.serverPort} 347 348 auth.method = "token" 349 auth.tokenSource.type = "file" 350 auth.tokenSource.file.path = "${cfg.authTokenFile}" 351 352 webServer.addr = "127.0.0.1" 353 webServer.port = $admin_port 354 355 [[proxies]] 356 name = "$proxy_name" 357 type = "$protocol" 358 localIP = "127.0.0.1" 359 localPort = $port 360 remotePort = 0 361 EOF 362 else 363 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid protocol: $protocol (must be http, tcp, or udp)" 364 exit 1 365 fi 366 367 # Start tunnel 368 echo 369 ${pkgs.gum}/bin/gum style --foreground 35 " Tunnel configured" 370 ${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port" 371 if [ "$protocol" = "http" ]; then 372 public_url="https://$tunnel_name.${cfg.domain}" 373 ${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url" 374 else 375 ${pkgs.gum}/bin/gum style --foreground 117 " Protocol: $protocol" 376 ${pkgs.gum}/bin/gum style --foreground 214 " Waiting for server to allocate port..." 377 fi 378 echo 379 ${pkgs.gum}/bin/gum style --foreground 214 "Connecting to ${cfg.serverAddr}:${toString cfg.serverPort}..." 380 echo 381 382 # For TCP/UDP, capture output to parse allocated port 383 if [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then 384 ${pkgs.frp}/bin/frpc -c $config_file 2>&1 | while IFS= read -r line; do 385 echo "$line" 386 387 # Look for successful proxy start 388 if echo "$line" | ${pkgs.gnugrep}/bin/grep -q "start proxy success"; then 389 sleep 1 390 391 proxy_status=$(${pkgs.curl}/bin/curl -s http://127.0.0.1:$admin_port/api/status 2>/dev/null || echo "{}") 392 393 remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".tcp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null) 394 if [ -z "$remote_addr" ] || [ "$remote_addr" = "null" ]; then 395 remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".udp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null) 396 fi 397 398 remote_port=$(echo "$remote_addr" | ${pkgs.gnugrep}/bin/grep -oP ':\K[0-9]+$') 399 400 if [ -n "$remote_port" ] && [ "$remote_port" != "null" ]; then 401 echo 402 ${pkgs.gum}/bin/gum style --foreground 35 " Tunnel established" 403 ${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port" 404 ${pkgs.gum}/bin/gum style --foreground 117 " Remote: ${cfg.serverAddr}:$remote_port" 405 ${pkgs.gum}/bin/gum style --foreground 117 " Type: $protocol" 406 echo 407 fi 408 fi 409 done 410 else 411 exec ${pkgs.frp}/bin/frpc -c $config_file 412 fi 413 ''; 414 415 bore = pkgs.stdenv.mkDerivation { 416 pname = "bore"; 417 version = "1.0"; 418 419 dontUnpack = true; 420 421 nativeBuildInputs = with pkgs; [ 422 pandoc 423 installShellFiles 424 ]; 425 426 manPageSrc = ./bore.1.md; 427 zshCompletionSrc = ./completions/bore.zsh; 428 429 buildPhase = '' 430 ${pkgs.pandoc}/bin/pandoc -s -t man $manPageSrc -o bore.1 431 ''; 432 433 installPhase = '' 434 mkdir -p $out/bin 435 436 # Install binary 437 cp ${boreScript} $out/bin/bore 438 chmod +x $out/bin/bore 439 440 # Install man page 441 installManPage bore.1 442 443 # Install completions 444 installShellCompletion --zsh --name _bore $zshCompletionSrc 445 ''; 446 447 meta = with lib; { 448 description = "Secure tunneling service CLI"; 449 homepage = "https://tun.hogwarts.channel"; 450 license = licenses.mit; 451 maintainers = [ ]; 452 }; 453 }; 454in 455{ 456 options.atelier.bore = { 457 enable = lib.mkEnableOption "bore tunneling service"; 458 459 serverAddr = lib.mkOption { 460 type = lib.types.str; 461 default = "tun.hogwarts.channel"; 462 description = "bore server address"; 463 }; 464 465 serverPort = lib.mkOption { 466 type = lib.types.port; 467 default = 7000; 468 description = "bore server port"; 469 }; 470 471 domain = lib.mkOption { 472 type = lib.types.str; 473 default = "tun.hogwarts.channel"; 474 description = "Domain for public tunnel URLs"; 475 }; 476 477 authTokenFile = lib.mkOption { 478 type = lib.types.nullOr lib.types.path; 479 default = null; 480 description = "Path to file containing authentication token"; 481 }; 482 }; 483 484 config = lib.mkIf cfg.enable { 485 home.packages = [ 486 pkgs.frp 487 bore 488 ]; 489 }; 490}