Webserver mit nc (für Status-Page)

Andy_m4

Well-Known Member
Das Web ist allgegenwärtig. :-)
Und natürlich kann man die Technik auch für eigene Zwecke nutzen. Beispielsweise für eine Website, die einem Systeminformationen (oder dergleichen) anzeigt.
Um sich deshalb aber nicht einen vollständigen Webserver (inkl. CGI etc.) aufzuhalsen, bauen wir uns den Webserver mit ein bisschen Shell-Skript + netcat selbst:
Bash:
#!/bin/sh

if [ $# -lt 2 ]; then
  echo "Usage: $0 <script> <port> [<bindaddress>]"
  echo "  <script>: Path to the script that generates the HTML output"
  echo "  <port>: Port number to listen on"
  echo "  <bindaddress>: Address to listen on (for Example: localhost)"
  exit 1
fi

SCRIPT=$1
PORT=$2
BINDADDR=${3:-}

echo "Status: http://${BINDADDR:-$(hostname)}:$PORT"
while true; do
  {
    now="$(env LANG=C date +'%a, %d %b %Y %H:%M:%S %Z')"
    echo "HTTP/1.1 200 OK"
    echo "Content-Type: text/html; charset=utf-8"
    echo "Cache-Control: max-age=5"
    echo "X-Robots-Tag: noarchive"
    echo "Date: $now"
    echo "Last-Modified: $now"
    echo "Expires: $(env LANG=C date -v+1M +'%a, %d %b %Y %H:%M:%S %Z')"
    echo "Connection: close"
    echo ""
    sh "$SCRIPT"
  } | nc -l ${BINDADDR:+$BINDADDR} "$PORT" -w 1 > /dev/null 2>&1
  if [ $? -ne 0 ]; then
    echo "Error: nc exited with code $?"
    exit 5
  fi
done
Der Webserver ignoriert alles was er vom Client (Browser) bekommt und gibt stumpf die Ausgabe des ihm übergebenen Shell-Skriptes zurück. Unter der Annahme das man das Webserver-Skript unter dem Namen serve-script-via-http.sh gespeichert hat, kann es also so aufrufen:

sh server-script-via-http.sh status-as-html.sh 8080 localhost

Damit läuft der Webserver auf http://localhost:8080 und piped die Ausgabe von status-as-html.sh via TCP/IP zum Webbrowser. Im Prinzip reicht es, wenn das Status-Skript einfach stumpf Text via echo ausgibt. Wenns plain text sein soll, muss man den Content-Type in Zeile 20 auf text/plain setzen, damit der Browser das vernünftig anzeigt.
Wir wollen aber natürlich lieber eine schicke HTML-Ausgabe. Also lassen wir es so und schreiben uns dann noch ein entsprechendes Status-Ausgabe-Skript (status-as-html.sh):

Bash:
#!/bin/sh

SERVERS="fritz.box mynas www.freebsd.org "
IFS=' '

check_server() {
  server=$1
  ping -c 1 -W 1 "$server" > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    echo "<li>$server: <span class=\"online\">reachable</span></li>"
  else
    echo "<li>$server: <span class=\"offline\">not reachable</span></li>"
  fi
}

# Funktion: make_number_human_readable
# Beschreibung: Konvertiert eine Zahl in eine lesbare Form mit Einheiten (B, K, M, G, T)
# Parameter: $1 - die Zahl, die konvertiert werden soll
make_number_human_readable() {
  local num=$1
  local unit="B"

  if [ $num -gt 1024 ]; then
    num=$(($num / 1024))
    unit="K"
  fi

  if [ $num -gt 1024 ]; then
    num=$(($num / 1024))
    unit="M"
  fi

  if [ $num -gt 1024 ]; then
    num=$(($num / 1024))
    unit="G"
  fi

  if [ $num -gt 1024 ]; then
    num=$(($num / 1024))
    unit="T"
  fi

  echo "${num} ${unit}"
}

# make-table-line Cell1 Cell2 Cell3 ...
# wird zu:
# <tr><td>Cell1</td><td>Cell2</td><td>Cell3</td> ... </tr>
make-table-line() {
  echo "<tr>"
  for param in "$@"; do
    echo "<td>$param</td>"
  done
  echo "</tr>"
}

# Funktion: show-status
# Beschreibung: Zeigt einen Status an, optional mit einem benutzerdefinierten HTML-Tag
# Parameter:
#   $1: Header-Text
#   $2: Status-Text
#   $3: Optionaler HTML-Tag (Default: keiner)
show-status() {
  local header="$1"
  local state="$2"
  local tag="$3"
  echo "<h2>$header</h2>"
  echo "<p>"
  if [ -n "$tag" ]; then
    echo "<$tag>"
    echo "$state"
    echo "</$tag>"
  else
    echo "$state"
  fi
  echo "</p>"
}


echo "<!DOCTYPE html> <html>
  <head>
    <meta http-equiv=\"refresh\" content=\"65\" >
    <title>Server Status</title>
    <style>
      h1 { font-size: 1.2em; font-weight:bolder;}
      h2 { font-size: 1.1em; font-weight:bolder;}
      span.online { color: green;}
      span.offline { color: red;}
      table {
         border-collapse: collapse;
         margin: 20px;
    }

    tr {
     border-bottom: 1px solid #ccc;
    }

    td {
      padding: 10px;
      border: 1px dotted #666;
    }

    .line {
      display: flex;
      flex-wrap: wrap;
    }

    .data {
    }

    .label {
      margin-right: 0.8em;
      margin-left: 0.8em;
    }

    .decoration {
      color: #666;
    }

    .data[data-tag=\"special\"] {
      color: #980081;
    }

    .data[data-tag=\"node\"] {
      color: #000BFE;
    }

    .data[data-tag=\"fstype\"] {
      color: #980017;
    }

    .data[data-tag=\"opts\"] {
      color: #333;
    }

    </style>
  </head>
  <body>"
echo "<a name='anfang'>"

# --------------------------------------------

echo "<h1>Systeminfo zu $(hostname)</h1>"
echo "<p>"
echo "<table>"
if [ $(id -u) -eq 0 ]; then # root?
  freebsd-update updatesready
  if [ $? -ne 2 ]; then
    make-table-line "FreeBSD-Update: " "Ready for install"
  fi
fi
make-table-line "Systemzeit:" "$(date)"
make-table-line "Uptime:" "$(uptime)"
make-table-line "System:" "$(uname -s) $(freebsd-version)"
make-table-line "RAM gesamt:" "$(make_number_human_readable $(sysctl -n hw.physmem))"
make-table-line "Swap gesamt:" "$(make_number_human_readable $(sysctl -n vm.swap_total))"
make-table-line "RAM frei:" "$(make_number_human_readable $(expr $(sysctl -n vm.v_free_target) \* $(sysctl -n hw.pagesize) ))"
echo "</table>"
echo "</p>"

show-status "Recent Systemmessages" "$(tail -n 5 /var/log/messages)" "pre"

show-status "ZPools" "$(zpool list)" "pre"

show-status "ZFS-Datasets" "$(zfs list)" "pre"

show-status "Mounts" "$(mount --libxo html | sort)"

show-status "Jails" "$(jls)" "pre"

# --------------------------------------------

echo "<h1>Servers reachability</h1>"
echo "<ul>"
  for server in $SERVERS; do
    echo "$(check_server $server)"
  done
echo "</ul>"

# --------------------------------------------

echo "<p>&nbsp;</p>"
echo "<p style='text-align: right;'><a href='#anfang'>hoch</a>&nbsp;&nbsp;</p>"
echo "</body></html>"
Ich glaube/hoffe, das meiste ist selbsterklärend. :-)
Die Skripte sind POSIX-Shell benutzen nur Tools aus dem FreeBSD-Base-System und sollten daher ohne weitere Packages/Ports laufen.

Theoretisch kann man sie direkt verwenden, aber in erster Linie dienen die der Veranschaulichung wie man so etwas machen kann.

Ein paar Hinweise noch:
  • der "Webserver" hat kein Verschlüsselung/SSL oder so. Es ist http-only. Möglicherweise zickt deshalb der Browser
  • man muss aufpassen bei Befehle die Seiteneffekte haben oder eine lange Laufzeit
  • man muss sich darüber im klaren sein, das der Webserver von "außen" (zumindest LAN) erreichbar ist, wenn man das localhost im Aufruf weglässt (was auch ein Security-Problem ist, je nachdem welche Informationen man rausgibt)
  • es wird erst die Seite erzeugt und dann netcat aufgerufen. Das kann dazu führen, das die Seite nicht aktuell ist. Das ist sie dann erst beim Reload. In soll der entsprechende Meta-Tag in status-as-html.sh in Zeile 82 dafür sorgen, das der Browser die Seite nach jeweils 65 Sekunden neu lädt (sozusagen Self-Refresh), so das man immer (halbwegs) aktuelle Werte hat, wenn man die Page ständig im Browser offen hat.
Fragen, Kritik, Ergänzungen sind natürlich im Thread Willkommen :-)
 
Das "Expires" kann weg - kein auch nur im Ansatz "moderner" browser (also etwas, das auch den meta refresh versteht), braucht diesen, wenn gleichzeitig Cache-Control da steht (die sich hier auch noch widersprechen).

(und dann koennte man auf OpenBSD arbeiten, dessen date kein +Nx kann. :-) )
 
Dir ist bewusst, dass du unnötigerweise inetd nachbaust?
Gute Anmerkung.
inetd hatte sogar noch ein paar Vorteile. Weil es das Skript tatsächlich erst startet, wenn ein Request reinkommt. Außerdem bringt es noch paar Features mit wie z.B. die Begrenzung der Requests oder gar eine IP-basierte Zugriffskontrolle.
Man kann also in der Tat auch ein inetd-Service (/etc/inetd.conf) anlegen a-la:
Code:
http-alt   stream  tcp     nowait  myuser  /bin/sh sh /path/to/status-as-html.sh
Man muss dann nur darauf achten, das status-as-html.sh auch den HTTP-Header zurück gibt (was oben durch serve-script-via-http.sh erledigt wird).

inetd hat aber auch ein, zwei Problemchen (aber vielleicht weißt Du ja da was).
  • Ich kann nicht einfach eine Port-Nummer angeben. Man muss einen Service-Namen aus /etc/services angeben. Das ist doof, wenn Service-Names mehrfach vergeben sind (wie das http-alt aus meiner Beispiel-Konfig.) und/oder das dann in der Konfiguration was irreführendes steht.
  • Außerdem muss ich inetd selbst als root starten, selbst wenn mein Service nicht auf einem privilegierten Port liegt (das Problem ist wohl eher, das man zu dem jeweiligen Service ein User angibt und den User-Context-Wechsel geht halt nicht so ohne Weiteres als non-root). Das kriegt man auch nicht gelöst ohne inetd selbst zu patchen.
 
Zuletzt bearbeitet:
Zurück
Oben