lib/access.sh

10.1 KB · 398 lines · bash

1#!/usr/bin/env bash
2
3BRT_ACCESS_PID_FILE="${HOME}/.brt/access.pid"
4BRT_ACCESS_INFO_FILE="${HOME}/.brt/access.info"
5BRT_ACCESS_AUDIT_LOG="${BRT_ACCESS_AUDIT_LOG:-${HOME}/.brt/access.audit.log}"
6BRT_ACCESS_ADMIN_SOCK="${BRT_ACCESS_ADMIN_SOCK:-${HOME}/.brt/access.admin.sock}"
7
8brt_access_cleanup() {
9 rm -f "$BRT_ACCESS_PID_FILE" "$BRT_ACCESS_INFO_FILE" "$BRT_ACCESS_ADMIN_SOCK"
10}
11
12brt_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
19brt_access_serve_pids() {
20 pgrep -f '[/ ]access\.py serve' 2>/dev/null || true
21}
22
23brt_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
33brt_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
66brt_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
133brt_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
158brt_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
194brt_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
207brt_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
214token=${token}
215endpoint=${endpoint}
216port=${port}
217pid=${pid}
218audit_log=${BRT_ACCESS_AUDIT_LOG}
219admin_sock=${BRT_ACCESS_ADMIN_SOCK}
220started=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
221EOF
222}
223
224brt_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
240brt_access_clear_logs() {
241 brt_require python3
242 python3 "${BRT_LIB}/access.py" clear-logs --audit-log "$BRT_ACCESS_AUDIT_LOG"
243}
244
245brt_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