Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 22
feat: Add systemd-managed pg-node service with secure API and core version flag#11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
b342be8e08b7da18ee97f3dd028475ce4c6be25306a32ac85e90dd72b00c5d1File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,263 @@ | ||||||
| #!/usr/bin/env bash | ||||||
| set -euo pipefail | ||||||
| log() { | ||||||
| printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2 | ||||||
| } | ||||||
| load_env_file() { | ||||||
| while IFS= read -r line || [[ -n "$line" ]]; do | ||||||
| line="${line%%#*}" | ||||||
| line="${line%%$'\r'*}" | ||||||
| [[ -z "${line//[[:space:]]/}" ]] && continue | ||||||
| if [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=[[:space:]]*(.*)$ ]]; then | ||||||
| local key=${BASH_REMATCH[1]} | ||||||
| local val=${BASH_REMATCH[2]} | ||||||
| val="${val#"${val%%[![:space:]]*}"}" # trim leading ws | ||||||
| val="${val%"${val##*[![:space:]]}"}" # trim trailing ws | ||||||
| if [[ "$val" =~ ^\".*\"$ || "$val" =~ ^\'.*\'$ ]]; then | ||||||
| val=${val:1:${#val}-2} | ||||||
| fi | ||||||
| export "$key=$val" | ||||||
| fi | ||||||
| done < "$ENV_FILE" | ||||||
| } | ||||||
| APP_NAME="pg-node" | ||||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||||||
| DEFAULT_ENV_FILE="/opt/$APP_NAME/.env" | ||||||
| LOCAL_ENV_FILE="$SCRIPT_DIR/.env" | ||||||
| ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}" | ||||||
| if [[ ! -f "$ENV_FILE" && -f "$LOCAL_ENV_FILE" ]]; then | ||||||
| ENV_FILE="$LOCAL_ENV_FILE" | ||||||
| fi | ||||||
| if [[ -f "$ENV_FILE" ]]; then | ||||||
| load_env_file | ||||||
| log "Loaded env file: $ENV_FILE" | ||||||
| else | ||||||
| log "Env file not found, using defaults: $ENV_FILE" | ||||||
| fi | ||||||
| API_PORT="${API_PORT:-3000}" | ||||||
| MAX_BODY=1048576 | ||||||
| API_KEY="${API_KEY:-}" | ||||||
| if [[ -z "$API_KEY" ]]; then | ||||||
| log "API_KEY must be set in the env file" | ||||||
| exit 1 | ||||||
| fi | ||||||
| if [[ -z "${SSL_CERT_FILE:-}" || -z "${SSL_KEY_FILE:-}" ]]; then | ||||||
| log "TLS required: set SSL_CERT_FILE and SSL_KEY_FILE in the env file" | ||||||
| exit 1 | ||||||
| fi | ||||||
| if [[ ! -r "$SSL_CERT_FILE" ]]; then | ||||||
| log "Cannot read SSL_CERT_FILE: $SSL_CERT_FILE" | ||||||
| exit 1 | ||||||
| fi | ||||||
| if [[ ! -r "$SSL_KEY_FILE" ]]; then | ||||||
| log "Cannot read SSL_KEY_FILE: $SSL_KEY_FILE" | ||||||
| exit 1 | ||||||
| fi | ||||||
| if ! command -v openssl >/dev/null 2>&1; then | ||||||
| log "openssl is required for TLS mode" | ||||||
| exit 1 | ||||||
| fi | ||||||
| log "TLS enforced with cert=$SSL_CERT_FILE key=$SSL_KEY_FILE on port $API_PORT" | ||||||
| log "API key protection enabled" | ||||||
| json_escape() { | ||||||
| local s=$1 | ||||||
| s=${s//\\/\\\\} | ||||||
| s=${s//\"/\\\"} | ||||||
| s=${s//$'\n'/\\n} | ||||||
| s=${s//$'\r'/\\r} | ||||||
| echo -n "$s" | ||||||
| } | ||||||
| status_text() { | ||||||
| case "$1" in | ||||||
| 200) echo -n "OK" ;; | ||||||
| 401) echo -n "Unauthorized" ;; | ||||||
| 400) echo -n "Bad Request" ;; | ||||||
| 404) echo -n "Not Found" ;; | ||||||
| *) echo -n "Internal Server Error" ;; | ||||||
| esac | ||||||
| } | ||||||
| respond() { | ||||||
| local code=$1 | ||||||
| local body=$2 | ||||||
| local text body_len | ||||||
| LAST_STATUS=$code | ||||||
| text=$(status_text "$code") | ||||||
| body_len=${#body} | ||||||
| printf 'HTTP/1.1 %s %s\r\n' "$code" "$text" | ||||||
| printf 'Content-Type: application/json\r\n' | ||||||
| printf 'Content-Length: %s\r\n' "$body_len" | ||||||
| printf 'Connection: close\r\n\r\n' | ||||||
| printf '%s' "$body" | ||||||
| } | ||||||
| handle_node_update(){ | ||||||
| log "Executing $APP_NAME update" | ||||||
| if ! $APP_NAME update 2>&1; then | ||||||
| log "update failed with exit code: $?" | ||||||
| respond 500 '{"detail":"update failed on server"}' | ||||||
| return | ||||||
| fi | ||||||
| respond 200 "{\"detail\":\"node updated successfully\"}" | ||||||
| } | ||||||
| handle_node_core_update(){ | ||||||
| local body="${1:-}" | ||||||
| local core_version="" | ||||||
| if ! command -v jq >/dev/null 2>&1; then | ||||||
| log "jq is required to parse core_version from request body" | ||||||
| respond 500 '{"detail":"jq not installed on server"}' | ||||||
| return | ||||||
| fi | ||||||
| if [[ -n "$body" ]]; then | ||||||
| if ! core_version=$(printf '%s' "$body" | jq -r '."core_version" // ""' 2>/dev/null); then | ||||||
| log "Failed to parse JSON body for core_version" | ||||||
| respond 400 '{"detail":"Invalid JSON body"}' | ||||||
| return | ||||||
| fi | ||||||
| fi | ||||||
| if [[ -n "$core_version" ]]; then | ||||||
| log "Executing $APP_NAME core-update with version: $core_version" | ||||||
| if $APP_NAME core-update --version "$core_version"; then | ||||||
| respond 200 "{\"detail\":\"node core updated successfully\"}" | ||||||
| else | ||||||
| log "core-update failed for version: $core_version" | ||||||
| respond 404 "{\"detail\":\"core-update failed for version $(json_escape "$core_version")\"}" | ||||||
| fi | ||||||
| fi | ||||||
| } | ||||||
| handle_geofiles_update(){ | ||||||
| local body="${1:-}" | ||||||
| local region="" flag="" | ||||||
| if [[ -n "$body" ]]; then | ||||||
| if ! command -v jq >/dev/null 2>&1; then | ||||||
| log "jq is required to parse region from request body" | ||||||
| respond 500 '{"detail":"jq not installed on server"}' | ||||||
| return | ||||||
| fi | ||||||
| if ! region=$(printf '%s' "$body" | jq -r '.region // empty' 2>/dev/null); then | ||||||
| log "Failed to parse JSON body for region" | ||||||
| respond 400 '{"detail":"Invalid JSON body"}' | ||||||
| return | ||||||
| fi | ||||||
| fi | ||||||
| if [[ -z "$region" ]]; then | ||||||
| respond 400 '{"detail":"region is required (iran, russia, china)"}' | ||||||
| return | ||||||
| fi | ||||||
| case "${region,,}" in | ||||||
| iran) flag="--iran" ;; | ||||||
| russia) flag="--russia" ;; | ||||||
| china) flag="--china" ;; | ||||||
| *) | ||||||
| log "Invalid region provided: $region" | ||||||
| respond 400 "{\"detail\":\"Unsupported region $(json_escape "$region")\"}" | ||||||
| return | ||||||
| ;; | ||||||
| esac | ||||||
| log "Executing $APP_NAME geofiles $flag" | ||||||
| if $APP_NAME geofiles "$flag"; then | ||||||
| respond 200 '{"detail":"geofiles updated successfully"}' | ||||||
| else | ||||||
| log "geofiles update failed" | ||||||
| respond 500 '{"detail":"geofiles update failed on server"}' | ||||||
| fi | ||||||
| } | ||||||
| handle_connection() { | ||||||
| local request_line method path version | ||||||
| if ! IFS= read -r request_line; then | ||||||
| return 0 | ||||||
| fi | ||||||
| request_line=${request_line%$'\r'} | ||||||
| read -r method path version <<<"$request_line" | ||||||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused variable Line 192 reads - read -r method path version <<<"$request_line"+ read -r method path _ <<<"$request_line"📝 Committable suggestion
Suggested change
🧰 Tools🪛 Shellcheck (0.11.0)[warning] 192-192: version appears unused. Verify use (or export if used externally). (SC2034) 🤖 Prompt for AI Agents | ||||||
| log "Request line: $request_line" | ||||||
| local header_line content_length=0 x_api_key="" header_name header_value | ||||||
| while IFS= read -r header_line; do | ||||||
| header_line=${header_line%$'\r'} | ||||||
| [[ -z "$header_line" ]] && break | ||||||
| if [[ "$header_line" =~ ^[Cc]ontent-[Ll]ength:\ ([0-9]+) ]]; then | ||||||
| content_length=${BASH_REMATCH[1]} | ||||||
| fi | ||||||
| header_name=${header_line%%:*} | ||||||
| header_value=${header_line#*:} | ||||||
| header_name=${header_name,,} | ||||||
| header_value=${header_value# } | ||||||
| if [[ "$header_name" == "x-api-key" ]]; then | ||||||
| x_api_key="$header_value" | ||||||
| fi | ||||||
| done | ||||||
| if [[ -z "$x_api_key" ]]; then | ||||||
| respond 401 '{"detail":"missing api key"}' | ||||||
| log "Unauthorized: missing x-api-key for $method $path" | ||||||
| return 0 | ||||||
| fi | ||||||
| if [[ "$x_api_key" != "$API_KEY" ]]; then | ||||||
| respond 401 '{"detail":"invalid api key"}' | ||||||
| log "Unauthorized: invalid x-api-key for $method $path" | ||||||
| return 0 | ||||||
| fi | ||||||
| local body="" | ||||||
| if (( content_length > 0 )); then | ||||||
| if (( content_length > MAX_BODY )); then | ||||||
| respond 400 '{"default":"Payload too large"}' | ||||||
| log "Body rejected: $content_length bytes (too large)" | ||||||
| return 0 | ||||||
| fi | ||||||
| IFS= read -r -N "$content_length" body || true | ||||||
| log "Body received: ${#body} bytes" | ||||||
| fi | ||||||
Comment on lines
+222
to
+231
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Inconsistent JSON response field naming—standardize on "detail". Line 225 uses if (( content_length > MAX_BODY )); then
- respond 400 '{"error":"Payload too large"}'+ respond 400 '{"detail":"Payload too large"}'
log "Body rejected: $content_length bytes (too large)"
return 0
fi🤖 Prompt for AI Agents | ||||||
| case "$method $path" in | ||||||
| "GET /") | ||||||
| respond 200 '{"status":"ok"}' | ||||||
| ;; | ||||||
| "POST /node/update") | ||||||
| handle_node_update | ||||||
| ;; | ||||||
| "POST /node/core_update") | ||||||
| handle_node_core_update "$body" | ||||||
| ;; | ||||||
| "POST /node/geofiles") | ||||||
| handle_geofiles_update "$body" | ||||||
| ;; | ||||||
| *) | ||||||
| respond 404 '{"detail":"Not found"}' | ||||||
| ;; | ||||||
| esac | ||||||
| log "Responded $LAST_STATUS to $method $path" | ||||||
| } | ||||||
| log "Bash REST API listening on https://localhost:${API_PORT}" | ||||||
| while true; do | ||||||
| coproc OPENSSL { openssl s_server -quiet -accept "$API_PORT" -cert "$SSL_CERT_FILE" -key "$SSL_KEY_FILE" -naccept 1; } | ||||||
| handle_connection <&"${OPENSSL[0]}" >&"${OPENSSL[1]}" || true | ||||||
| exec {OPENSSL[0]}>&- | ||||||
| exec {OPENSSL[1]}>&- | ||||||
| wait "$OPENSSL_PID" 2>/dev/null || true | ||||||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Variable OPENSSL_PID is undefined—coproc array indices are 0 and 1, not PID. Line 259 references Capture the process PID from while true; do
coproc OPENSSL { openssl s_server -quiet -accept "$API_PORT" -cert "$SSL_CERT_FILE" -key "$SSL_KEY_FILE" -naccept 1; }
+ local OPENSSL_PID=$!
handle_connection <&"${OPENSSL[0]}" >&"${OPENSSL[1]}" || true
exec {OPENSSL[0]}>&-
exec {OPENSSL[1]}>&-
wait "$OPENSSL_PID" 2>/dev/null || true
# Ensure cleanup even if wait fails kill "$OPENSSL_PID" 2>/dev/null || true sleep 0.1 # Brief pause to avoid rapid respawn storms
done🤖 Prompt for AI Agents | ||||||
| # Ensure cleanup even if wait fails | ||||||
| kill "$OPENSSL_PID" 2>/dev/null || true | ||||||
| sleep 0.1 # Brief pause to avoid rapid respawn storms | ||||||
| done | ||||||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.