| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | BRT_ACCESS_PID_FILE="${HOME}/.brt/access.pid" |
| 4 | BRT_ACCESS_INFO_FILE="${HOME}/.brt/access.info" |
| 5 | BRT_ACCESS_AUDIT_LOG="${BRT_ACCESS_AUDIT_LOG:-${HOME}/.brt/access.audit.log}" |
| 6 | BRT_ACCESS_ADMIN_SOCK="${BRT_ACCESS_ADMIN_SOCK:-${HOME}/.brt/access.admin.sock}" |
| 7 | |
| 8 | brt_access_cleanup() { |
| 9 | rm -f "$BRT_ACCESS_PID_FILE" "$BRT_ACCESS_INFO_FILE" "$BRT_ACCESS_ADMIN_SOCK" |
| 10 | } |
| 11 | |
| 12 | brt_access_port_pids() { |
| 13 | local port="$1" |
| 14 | if command -v lsof &>/dev/null; then |
| 15 | lsof -ti ":${port}" 2>/dev/null || true |
| 16 | fi |
| 17 | } |
| 18 | |
| 19 | brt_access_serve_pids() { |
| 20 | pgrep -f '[/ ]access\.py serve' 2>/dev/null || true |
| 21 | } |
| 22 | |
| 23 | brt_access_kill_pids() { |
| 24 | local sig="$1" |
| 25 | shift |
| 26 | local pid |
| 27 | for pid in "$@"; do |
| 28 | [[ -n "$pid" ]] || continue |
| 29 | kill "-${sig}" "$pid" 2>/dev/null || true |
| 30 | done |
| 31 | } |
| 32 | |
| 33 | brt_access_collect_pids() { |
| 34 | local port="$1" |
| 35 | local -a all_pids=() pids=() |
| 36 | local pid p keep |
| 37 | |
| 38 | if [[ -f "$BRT_ACCESS_PID_FILE" ]]; then |
| 39 | pid=$(tr -d '[:space:]' < "$BRT_ACCESS_PID_FILE") |
| 40 | [[ -n "$pid" ]] && all_pids+=("$pid") |
| 41 | fi |
| 42 | |
| 43 | while IFS= read -r pid; do |
| 44 | [[ -n "$pid" ]] && all_pids+=("$pid") |
| 45 | done < <(brt_access_serve_pids) |
| 46 | |
| 47 | if [[ -n "$port" ]]; then |
| 48 | while IFS= read -r pid; do |
| 49 | [[ -n "$pid" ]] && all_pids+=("$pid") |
| 50 | done < <(brt_access_port_pids "$port") |
| 51 | fi |
| 52 | |
| 53 | for pid in "${all_pids[@]}"; do |
| 54 | keep=1 |
| 55 | for p in "${pids[@]}"; do |
| 56 | [[ "$p" == "$pid" ]] && keep=0 && break |
| 57 | done |
| 58 | [[ "$keep" -eq 1 ]] && pids+=("$pid") |
| 59 | done |
| 60 | |
| 61 | if ((${#pids[@]})); then |
| 62 | printf '%s\n' "${pids[@]}" |
| 63 | fi |
| 64 | } |
| 65 | |
| 66 | brt_access_force_stop() { |
| 67 | local port="" extra_port="" pid keep killed=0 |
| 68 | local -a ports=() pids=() uniq=() remaining=() |
| 69 | |
| 70 | while [[ $# -gt 0 ]]; do |
| 71 | case "$1" in |
| 72 | -p|--port) extra_port="$2"; shift 2 ;; |
| 73 | *) shift ;; |
| 74 | esac |
| 75 | done |
| 76 | |
| 77 | if [[ -f "$BRT_ACCESS_INFO_FILE" ]]; then |
| 78 | # shellcheck disable=SC1090 |
| 79 | source "$BRT_ACCESS_INFO_FILE" |
| 80 | fi |
| 81 | [[ -n "$extra_port" ]] && port="$extra_port" |
| 82 | [[ -n "$port" ]] && ports+=("$port") |
| 83 | ports+=(8765) |
| 84 | |
| 85 | for port in "${ports[@]}"; do |
| 86 | while IFS= read -r pid; do |
| 87 | [[ -n "$pid" ]] && pids+=("$pid") |
| 88 | done < <(brt_access_collect_pids "$port") |
| 89 | done |
| 90 | |
| 91 | for pid in "${pids[@]}"; do |
| 92 | keep=1 |
| 93 | for port in "${uniq[@]}"; do |
| 94 | [[ "$port" == "$pid" ]] && keep=0 && break |
| 95 | done |
| 96 | [[ "$keep" -eq 1 ]] && uniq+=("$pid") |
| 97 | done |
| 98 | pids=() |
| 99 | if ((${#uniq[@]})); then |
| 100 | pids=("${uniq[@]}") |
| 101 | fi |
| 102 | |
| 103 | if [[ ${#pids[@]} -eq 0 ]]; then |
| 104 | brt_access_cleanup |
| 105 | brt_warn "No processes found to stop" |
| 106 | return 0 |
| 107 | fi |
| 108 | |
| 109 | brt_info "Stopping process(es) on access port: ${pids[*]}" |
| 110 | brt_access_kill_pids TERM "${pids[@]}" |
| 111 | sleep 0.4 |
| 112 | brt_access_kill_pids KILL "${pids[@]}" |
| 113 | sleep 0.2 |
| 114 | killed=${#pids[@]} |
| 115 | |
| 116 | for port in "${ports[@]}"; do |
| 117 | while IFS= read -r pid; do |
| 118 | [[ -n "$pid" ]] && remaining+=("$pid") |
| 119 | done < <(brt_access_port_pids "$port") |
| 120 | done |
| 121 | |
| 122 | if ((${#remaining[@]} > 0)); then |
| 123 | brt_warn "Port still in use (PID ${remaining[*]})" |
| 124 | brt_info "Try: kill -9 ${remaining[*]}" |
| 125 | brt_access_cleanup |
| 126 | return 1 |
| 127 | fi |
| 128 | |
| 129 | brt_access_cleanup |
| 130 | brt_ok "Force stopped ${killed} process(es), port free" |
| 131 | } |
| 132 | |
| 133 | brt_access_free_port() { |
| 134 | local port="$1" |
| 135 | local port_pids live_pid |
| 136 | |
| 137 | port_pids=$(brt_access_port_pids "$port" | tr '\n' ' ') |
| 138 | live_pid="" |
| 139 | if [[ -f "$BRT_ACCESS_PID_FILE" ]]; then |
| 140 | live_pid=$(tr -d '[:space:]' < "$BRT_ACCESS_PID_FILE") |
| 141 | if [[ -n "$live_pid" ]] && ! kill -0 "$live_pid" 2>/dev/null; then |
| 142 | live_pid="" |
| 143 | brt_access_cleanup |
| 144 | fi |
| 145 | fi |
| 146 | |
| 147 | if [[ -z "$port_pids" && -z "$live_pid" ]]; then |
| 148 | return 0 |
| 149 | fi |
| 150 | |
| 151 | if [[ -n "$port_pids" ]]; then |
| 152 | brt_warn "Port ${port} in use (PID ${port_pids}) — freeing..." |
| 153 | fi |
| 154 | |
| 155 | brt_access_force_stop -p "$port" |
| 156 | } |
| 157 | |
| 158 | brt_access_stop() { |
| 159 | local force=0 |
| 160 | while [[ $# -gt 0 ]]; do |
| 161 | case "$1" in |
| 162 | -f|--force) force=1; shift ;; |
| 163 | *) shift ;; |
| 164 | esac |
| 165 | done |
| 166 | |
| 167 | if [[ "$force" -eq 1 ]]; then |
| 168 | brt_access_force_stop "$@" |
| 169 | return |
| 170 | fi |
| 171 | |
| 172 | local pid="" |
| 173 | if [[ -f "$BRT_ACCESS_PID_FILE" ]]; then |
| 174 | pid=$(tr -d '[:space:]' < "$BRT_ACCESS_PID_FILE") |
| 175 | if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then |
| 176 | kill "$pid" 2>/dev/null && brt_ok "Stopped access server (PID ${pid})" |
| 177 | brt_access_cleanup |
| 178 | return 0 |
| 179 | fi |
| 180 | fi |
| 181 | |
| 182 | local orphans |
| 183 | orphans=$(brt_access_serve_pids | tr '\n' ' ') |
| 184 | if [[ -n "$orphans" ]]; then |
| 185 | brt_warn "PID file missing/stale but access server still running (${orphans})" |
| 186 | brt_info "Run: brt -access force-stop" |
| 187 | return 1 |
| 188 | fi |
| 189 | |
| 190 | brt_access_cleanup |
| 191 | brt_warn "No access server running" |
| 192 | } |
| 193 | |
| 194 | brt_access_start_server() { |
| 195 | local port="$1" |
| 196 | local token="$2" |
| 197 | |
| 198 | python3 "${BRT_LIB}/access.py" serve --port "$port" --token "$token" \ |
| 199 | --audit-log "$BRT_ACCESS_AUDIT_LOG" \ |
| 200 | --admin-sock "$BRT_ACCESS_ADMIN_SOCK" \ |
| 201 | > "${HOME}/.brt/access.stdout" 2> "${HOME}/.brt/access.log" & |
| 202 | local pid=$! |
| 203 | echo "$pid" > "$BRT_ACCESS_PID_FILE" |
| 204 | echo "$pid" |
| 205 | } |
| 206 | |
| 207 | brt_access_write_info() { |
| 208 | local token="$1" |
| 209 | local endpoint="$2" |
| 210 | local port="$3" |
| 211 | local pid="$4" |
| 212 | |
| 213 | cat > "$BRT_ACCESS_INFO_FILE" <<EOF |
| 214 | token=${token} |
| 215 | endpoint=${endpoint} |
| 216 | port=${port} |
| 217 | pid=${pid} |
| 218 | audit_log=${BRT_ACCESS_AUDIT_LOG} |
| 219 | admin_sock=${BRT_ACCESS_ADMIN_SOCK} |
| 220 | started=$(date -u +"%Y-%m-%dT%H:%M:%SZ") |
| 221 | EOF |
| 222 | } |
| 223 | |
| 224 | brt_access_console() { |
| 225 | local port="$1" |
| 226 | local token="$2" |
| 227 | local endpoint="$3" |
| 228 | local pid="$4" |
| 229 | |
| 230 | python3 "${BRT_LIB}/access.py" watch-logs \ |
| 231 | --audit-log "$BRT_ACCESS_AUDIT_LOG" \ |
| 232 | --admin-sock "$BRT_ACCESS_ADMIN_SOCK" \ |
| 233 | --interactive \ |
| 234 | --server-pid "$pid" \ |
| 235 | --endpoint "$endpoint" \ |
| 236 | --token "$token" \ |
| 237 | --owns-server |
| 238 | } |
| 239 | |
| 240 | brt_access_clear_logs() { |
| 241 | brt_require python3 |
| 242 | python3 "${BRT_LIB}/access.py" clear-logs --audit-log "$BRT_ACCESS_AUDIT_LOG" |
| 243 | } |
| 244 | |
| 245 | brt_access() { |
| 246 | local subcmd="${1:-}" |
| 247 | shift || true |
| 248 | |
| 249 | mkdir -p "${HOME}/.brt" |
| 250 | |
| 251 | case "$subcmd" in |
| 252 | serve) |
| 253 | local port=8765 |
| 254 | while [[ $# -gt 0 ]]; do |
| 255 | case "$1" in |
| 256 | -p|--port) port="$2"; shift 2 ;; |
| 257 | *) shift ;; |
| 258 | esac |
| 259 | done |
| 260 | |
| 261 | brt_require python3 |
| 262 | |
| 263 | brt_access_free_port "$port" || exit 1 |
| 264 | |
| 265 | local token |
| 266 | token=$(brt_random_token) |
| 267 | |
| 268 | local pid |
| 269 | pid=$(brt_access_start_server "$port" "$token") |
| 270 | |
| 271 | sleep 0.4 |
| 272 | if ! kill -0 "$pid" 2>/dev/null; then |
| 273 | brt_error "Access server failed to start (see ${HOME}/.brt/access.log)" |
| 274 | brt_access_cleanup |
| 275 | exit 1 |
| 276 | fi |
| 277 | |
| 278 | local endpoint |
| 279 | endpoint=$(brt_local_ips | head -1) |
| 280 | [[ -z "$endpoint" ]] && endpoint="127.0.0.1" |
| 281 | endpoint="${endpoint}:${port}" |
| 282 | |
| 283 | brt_access_write_info "$token" "$endpoint" "$port" "$pid" |
| 284 | |
| 285 | if [[ -t 0 && -t 1 ]]; then |
| 286 | brt_access_console "$port" "$token" "$endpoint" "$pid" || true |
| 287 | if kill -0 "$pid" 2>/dev/null; then |
| 288 | kill "$pid" 2>/dev/null || true |
| 289 | fi |
| 290 | brt_access_cleanup |
| 291 | else |
| 292 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |
| 293 | brt_ok "Secure terminal relay is running" |
| 294 | echo |
| 295 | echo " Endpoint: ${endpoint}" |
| 296 | echo " Token: ${token}" |
| 297 | echo |
| 298 | echo " Connect: brt -access connect ${endpoint} ${token}" |
| 299 | echo " Leave: brt -access leave (from another terminal while connected)" |
| 300 | echo " Logs: brt -access logs" |
| 301 | echo " Clear: brt -access clear-logs" |
| 302 | echo " Stop: brt -access stop" |
| 303 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |
| 304 | fi |
| 305 | ;; |
| 306 | logs) |
| 307 | brt_require python3 |
| 308 | if [[ ! -t 0 || ! -t 1 ]]; then |
| 309 | if [[ -f "$BRT_ACCESS_AUDIT_LOG" ]]; then |
| 310 | tail -n "${1:-50}" -f "$BRT_ACCESS_AUDIT_LOG" |
| 311 | else |
| 312 | brt_warn "No audit log at ${BRT_ACCESS_AUDIT_LOG}" |
| 313 | fi |
| 314 | return 0 |
| 315 | fi |
| 316 | local server_pid=0 |
| 317 | if [[ -f "$BRT_ACCESS_PID_FILE" ]]; then |
| 318 | server_pid=$(cat "$BRT_ACCESS_PID_FILE") |
| 319 | kill -0 "$server_pid" 2>/dev/null || server_pid=0 |
| 320 | fi |
| 321 | local endpoint="" token="" |
| 322 | if [[ -f "$BRT_ACCESS_INFO_FILE" ]]; then |
| 323 | # shellcheck disable=SC1090 |
| 324 | source "$BRT_ACCESS_INFO_FILE" |
| 325 | fi |
| 326 | python3 "${BRT_LIB}/access.py" watch-logs \ |
| 327 | --audit-log "$BRT_ACCESS_AUDIT_LOG" \ |
| 328 | --admin-sock "$BRT_ACCESS_ADMIN_SOCK" \ |
| 329 | --interactive \ |
| 330 | --server-pid "${server_pid:-0}" \ |
| 331 | --endpoint "${endpoint:-}" \ |
| 332 | --token "${token:-}" |
| 333 | ;; |
| 334 | clients) |
| 335 | brt_require python3 |
| 336 | python3 "${BRT_LIB}/access.py" clients --admin-sock "$BRT_ACCESS_ADMIN_SOCK" |
| 337 | ;; |
| 338 | leave) |
| 339 | brt_require python3 |
| 340 | python3 "${BRT_LIB}/access.py" leave |
| 341 | ;; |
| 342 | disconnect) |
| 343 | local session_id="${1:-}" |
| 344 | brt_require python3 |
| 345 | if [[ -z "$session_id" ]]; then |
| 346 | python3 "${BRT_LIB}/access.py" leave |
| 347 | else |
| 348 | python3 "${BRT_LIB}/access.py" disconnect "$session_id" \ |
| 349 | --admin-sock "$BRT_ACCESS_ADMIN_SOCK" |
| 350 | fi |
| 351 | ;; |
| 352 | connect) |
| 353 | local endpoint="${1:-}" |
| 354 | local token="${2:-}" |
| 355 | if [[ -z "$endpoint" || -z "$token" ]]; then |
| 356 | brt_error "Usage: brt -access connect <host:port> <token>" |
| 357 | exit 1 |
| 358 | fi |
| 359 | brt_require python3 |
| 360 | python3 "${BRT_LIB}/access.py" connect "$endpoint" "$token" |
| 361 | ;; |
| 362 | status) |
| 363 | if [[ -f "$BRT_ACCESS_INFO_FILE" ]]; then |
| 364 | brt_info "Access server info:" |
| 365 | cat "$BRT_ACCESS_INFO_FILE" |
| 366 | if [[ -f "$BRT_ACCESS_PID_FILE" ]]; then |
| 367 | local pid |
| 368 | pid=$(cat "$BRT_ACCESS_PID_FILE") |
| 369 | if kill -0 "$pid" 2>/dev/null; then |
| 370 | brt_ok "Running (PID ${pid})" |
| 371 | echo |
| 372 | brt_access clients 2>/dev/null || true |
| 373 | else |
| 374 | brt_warn "PID file exists but process not running" |
| 375 | fi |
| 376 | fi |
| 377 | else |
| 378 | brt_warn "No access server info found" |
| 379 | fi |
| 380 | ;; |
| 381 | clear-logs|clearlogs) |
| 382 | brt_access_clear_logs |
| 383 | ;; |
| 384 | stop|force-stop) |
| 385 | if [[ "$subcmd" == "force-stop" ]]; then |
| 386 | brt_access_force_stop "$@" |
| 387 | else |
| 388 | brt_access_stop "$@" |
| 389 | fi |
| 390 | ;; |
| 391 | *) |
| 392 | brt_error "Usage: brt -access <serve|connect|leave|clients|disconnect|logs|clear-logs|status|stop|force-stop>" |
| 393 | exit 1 |
| 394 | ;; |
| 395 | esac |
| 396 | } |
| 397 | |
| 398 | |