Skip to content
Merged
263 changes: 263 additions & 0 deletions pg-node-service.sh
Original file line numberDiff line numberDiff 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\"}"
}
Comment thread
ImMohammad20000 marked this conversation as resolved.

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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unused variable version captured from request line.

Line 192 reads version from the HTTP request line but never uses it. Either remove it or add parsing/validation logic if it's intended for future use.

- read -r method path version <<<"$request_line"+ read -r method path _ <<<"$request_line"
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
read -r method path version<<<"$request_line"
read -r method path _<<<"$request_line"
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 192-192: version appears unused. Verify use (or export if used externally).

(SC2034)

🤖 Prompt for AI Agents
In pg-node-service.sh around line 192, the read command currently captures an
unused variable `version` from the HTTP request line; remove the unused variable
or consume it safely. Replace the three-field read with a two-field read (method
and path) or use a throwaway placeholder (e.g., `_`) for the third field, or if
version was intended to be used, add parsing/validation logic and subsequent
usage (e.g., check HTTP version format) so the variable is actually consumed.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 {"error":"Payload too large"} while all other error responses use {"detail":"..."} (lines 120, 123, 127, 156, 162, 172, 182, 212, 218, 247). This inconsistency may confuse clients parsing responses. Standardize on "detail" for all error responses.

 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
In pg-node-service.sh around lines 222 to 231, the error response uses
{"error":"Payload too large"} while other errors use {"detail":"..."}; change
the response payload to use the "detail" field (e.g. {"detail":"Payload too
large"}) and update any surrounding log/message text if necessary so all error
JSON responses consistently use "detail".


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Variable OPENSSL_PID is undefined—coproc array indices are 0 and 1, not PID.

Line 259 references $OPENSSL_PID, but the coproc syntax only populates OPENSSL[0] (read fd) and OPENSSL[1] (write fd). The process PID is stored in $OPENSSL_PID only if explicitly captured via local ... <(...) pattern, which is not used here. The wait and kill commands on lines 259 and 261 fail silently but ineffectively.

Capture the process PID from $! immediately after the coproc declaration:

 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
In pg-node-service.sh around line 259, the script calls wait on $OPENSSL_PID but
the coproc creates file descriptors OPENSSL[0] and OPENSSL[1] not a PID; capture
the child PID immediately after the coproc by assigning OPENSSL_PID=$! (or
similar) right after the coproc declaration so subsequent wait and kill use the
real PID, then replace any uses of $OPENSSL_PID elsewhere (e.g., lines 259 and
261) to rely on that captured PID; ensure the variable is in scope where
wait/kill are invoked and handle absence safely (e.g., check non-empty) to avoid
silent failures.

# Ensure cleanup even if wait fails
kill "$OPENSSL_PID" 2>/dev/null || true
sleep 0.1 # Brief pause to avoid rapid respawn storms
done
Loading