2026-06-07 00:44:23 +02:00
2026-06-06 17:14:11 +02:00
2026-03-22 22:06:35 +01:00
2026-02-03 23:05:42 +01:00
2026-06-06 18:08:24 +02:00
2026-03-21 11:09:44 +01:00
2026-03-21 11:09:44 +01:00
2026-04-04 23:07:23 +02:00
2026-03-21 11:09:44 +01:00
2026-03-21 11:12:04 +01:00
2026-06-06 18:08:24 +02:00
2026-03-21 11:09:44 +01:00
2026-06-06 17:12:19 +02:00
2026-03-21 11:09:44 +01:00
2026-06-06 17:12:19 +02:00
2026-03-21 11:09:44 +01:00
2026-06-07 00:44:23 +02:00

appServer Portal UI

Übersichts-Webserver für server.schooltech.ch. Er fasst eine Reihe von Web-Diensten (Roboter-Steuerungen, Guacamole-Remotedesktops, Portainer, VS Code, Nextcloud …) unter einer Domain zusammen und macht sie über sprechende Subdomains erreichbar.

Alle Dienste laufen als Subdomain von .server.schooltech.ch egal ob sie auf dem Server selbst, auf einem Gerät im LAN oder hinter einem SSH-Tunnel stehen. Ein nginx-Reverse-Proxy nimmt jede Subdomain auf 443 entgegen, terminiert TLS (Let's Encrypt) und leitet an den passenden Upstream weiter.


Inhalt


Architektur-Übersicht

Alles hängt unter *.server.schooltech.ch. Der Unterschied zwischen den Diensten ist nicht die Domain, sondern wohin der Upstream zeigt:

<img src="doc/Architektur.png" width="900" alt=" Internet (HTTPS :443 / HTTP :80) │ ┌────────────────────────────────────────────────┐ │ nginx Reverse-Proxy (Container: appServer_PortalUI) │ ein vHost pro Subdomain .server.schooltech.ch │ TLS-Terminierung (Let's Encrypt) · 80→443 Redirect └────────────────────────────────────────────────┘ │ ┌───────────────┬───────────┼──────────────┬────────────────────┐ ▼ ▼ ▼ ▼ server. rp5. nextcloud. inf*/rp3*/tc*/robot*/fluidnc* schooltech.ch schooltech. schooltech. .server.schooltech.ch Portal-UI lokale Gerät im LAN über SSH-Tunnel-Hub (diese App) Container (direkte IP) appServer_TunnelHead │ │ │ │ ▼ ▼ ▼ ▼ public/ appServer_ 192.168.0.210 appServer_TunnelHead (SSH-Reverse-Tunnels) index.html guacamole / ├─ 99xx InformatikWeb (inf*)

  • Auth-API portainer ├─ 81xx RP3/SCARA (rp3*, fluidnc*) └─ 97xx ThinkCentre (tc*, robot*) ">

Bild: doc/Architektur.png · Vektor-Quelle: doc/Architektur.svg (für PDF/beliebige Skalierung).

Bausteine (Docker-Container, siehe docker-compose.yaml):

Container Image Rolle
appServer_PortalUI nginx:alpine Reverse-Proxy + Auslieferung der Portal-Seite (Ports 80/443)
AppServerAuth node:24-alpine Login-/Session-Service (auth/auth.js, Port 3000 intern)
appServer_TunnelHead linuxserver/openssh-server SSH-Hub: entfernte Geräte bauen Reverse-Tunnel hierher auf
appServer_guacamole abesnier/guacamole Lokaler Guacamole-Remotedesktop
appServer_LetsEncryptFetcher certbot/certbot Holt/erneuert die TLS-Zertifikate

Alle Container hängen im Docker-Netzwerk appRobotNet und sprechen sich per Container-Namen an (z. B. appserverauth:3000).


Die Portal-Seite (server.schooltech.ch)

Ruft man die nackte Domain server.schooltech.ch auf, erscheint die Portal-Oberfläche eine schlanke Single-Page-App aus public/:

  • public/index.html Grundgerüst: oben eine Navigationsleiste (<header> mit Logo „schooltech“, der Service-Navigation #services und einem Login/Logout-Button), darunter ein <iframe>, in dem der gewählte Dienst eingeblendet wird.
  • public/app.js die Logik:
    • hält eine Liste der verlinkten Dienste (services-Array),
    • prüft beim Laden via GET /api/status, ob eine Session besteht,
    • blendet nach dem Login für jeden Dienst einen Button in die Navigationsleiste ein,
    • öffnet beim Klick den Dienst im <iframe> (und meldet das Ereignis an POST /api/event),
    • Login/Logout laufen über POST /api/login bzw. POST /api/logout.
  • public/style.css Styling der Leiste, der Buttons und des Login-Dialogs.

Ablauf aus Nutzersicht:


┌──────────────────────────────────────────────────────────────┐
│ schooltech  [ Control GamePad ][ Guacamole ][ Simulation ]…  [Logout] │  ← Navigationsleiste
├──────────────────────────────────────────────────────────────┤
│                                                                │
│                  « ausgewählter Dienst im iFrame »             │
│                                                                │
└──────────────────────────────────────────────────────────────┘

Oben die Navigationsleiste mit Logo und Dienst-Buttons, darunter der gewählte Dienst im iFrame. Bild: doc/Portal.png · Quellen: doc/Portal.svg, doc/Portal.pdf.

  1. Seite öffnen → ist man nicht eingeloggt, zeigt der Button „Login“.
  2. Login (User/Passwort) → der Auth-Service setzt ein Session-Cookie für die gesamte Domain .server.schooltech.ch.
  3. Die Navigationsleiste füllt sich mit den Dienst-Buttons.
  4. Klick auf einen Button blendet den Dienst im iFrame ein.

Hinweis: Die im Portal angezeigten Buttons stehen hardcodiert in public/app.js (services-Array, aktuell 10 Einträge) sie werden nicht automatisch aus forwarding.conf erzeugt. Ein neuer Dienst in forwarding.conf taucht also erst dann in der Navigationsleiste auf, wenn er auch ins services-Array eingetragen wird (siehe Neuen Dienst hinzufügen).

Aktuell im Portal verlinkte Dienste (public/app.js):

Button Ziel-Subdomain
Control GamePad tccontrol.server.schooltech.ch
Guacamole rp5guac.server.schooltech.ch
Simulation tcSimulation.server.schooltech.ch
Video robotVideo.server.schooltech.ch
Homing robotHoming.server.schooltech.ch
RobotBase robotBase.server.schooltech.ch
RobotEllbow robotEllbow.server.schooltech.ch
RobotHand robotHand.server.schooltech.ch
RobotDriver robotDriver.server.schooltech.ch
VSCode robotVSCode.server.schooltech.ch

Die Dienste / Subdomains

Quelle der Wahrheit ist forwarding.conf. Aus jeder Zeile generiert connect-proxies.sh einen nginx-vHost. Gruppiert nach Upstream-Ziel:

Portal selbst

Subdomain Upstream Bemerkung
server.schooltech.ch local liefert die Portal-Seite aus public/

Lokale Container (auf dem Server / „RP5“)

Subdomain Upstream WebSocket
rp5Guac.server.schooltech.ch http://appServer_guacamole:8080
rp5Portainer.server.schooltech.ch http://portainer:9000

Gerät im LAN (direkte IP)

Subdomain Upstream WebSocket
nextcloud.server.schooltech.ch http://192.168.0.210:9183

Über appServer_TunnelHead (SSH-Tunnel-Hub)

Entfernte Geräte bauen einen SSH-Reverse-Tunnel zum appServer_TunnelHead auf; deren Web-Oberflächen erscheinen dort auf festen Ports.

InformatikWeb (99xx):

Subdomain Upstream WS
infPortainer.server.schooltech.ch http://appServer_TunnelHead:9903
infGuac.server.schooltech.ch http://appServer_TunnelHead:9980

RP3 Raspi für die SCARA-Roboter (81xx):

Subdomain Upstream WS
rp3Portainer.server.schooltech.ch http://appServer_TunnelHead:8100
rp3Guac.server.schooltech.ch http://appServer_TunnelHead:8180
fluidncRed.server.schooltech.ch http://appServer_TunnelHead:8120
fluidncWhite.server.schooltech.ch https://appServer_TunnelHead:8104

ThinkCentre MiniPC neben einem Roboter (97xx):

Subdomain Upstream WS Auth
tcGuac.server.schooltech.ch http://appServer_TunnelHead:9780
tcPortainer.server.schooltech.ch http://appServer_TunnelHead:9703
tcSimulation.server.schooltech.ch https://appServer_TunnelHead:9712
tcControl.server.schooltech.ch https://appServer_TunnelHead:9710
robotHoming.server.schooltech.ch https://appServer_TunnelHead:9793
robotVideo.server.schooltech.ch https://appServer_TunnelHead:9743
robotBase.server.schooltech.ch https://appServer_TunnelHead:9725
robotEllbow.server.schooltech.ch https://appServer_TunnelHead:9726
robotHand.server.schooltech.ch https://appServer_TunnelHead:9727
robotDriver.server.schooltech.ch https://appServer_TunnelHead:9798
robotVSCode.server.schooltech.ch http://appServer_TunnelHead:9744

Die Port-Konvention am Tunnel-Hub: 81xx → RP3, 97xx → ThinkCentre, 99xx → InformatikWeb.


forwarding.conf das Herzstück

Eine Zeile pro Subdomain. Spalten (durch Whitespace getrennt):

server_name   upstream_url   http_behavior   websockets   verify_upstream_tls   [cert_domain]   [listen_port]   [auth_required]
# Spalte Werte Default Bedeutung
1 server_name FQDN die Subdomain, z. B. tcGuac.server.schooltech.ch
2 upstream_url http(s)://host:port oder local Ziel; local = Portal-Seite statisch ausliefern
3 http_behavior redirect / sonst redirect redirect erzeugt zusätzlich einen 80→443-Redirect-vHost
4 websockets true / false false bei true werden Upgrade/Connection-Header durchgereicht
5 verify_upstream_tls true / false false nur bei https-Upstream: Zertifikat des Upstreams prüfen
6 cert_domain Ordnername = server_name Let's-Encrypt-Verzeichnis unter /etc/letsencrypt/live/<cert_domain>/
7 listen_port Port 443 abweichender HTTPS-Listen-Port
8 auth_required true / false false bei true schützt nginx den vHost via auth_request

Zeilen, die mit # beginnen, sowie Leerzeilen werden ignoriert.


connect-proxies.sh Konfig-Generator

Läuft automatisch beim Containerstart des nginx-Containers (liegt als /docker-entrypoint.d/40-connect-proxies.sh). Pro forwarding.conf-Zeile:

  1. Globale Map + Resolver schreiben (_globals.generated.conf) nötig für WebSockets und für die DNS-Auflösung der Upstreams zur Laufzeit. Außerdem ein 443-Default-Server (00-default-server.generated.conf), der unbekanntes SNI per ssl_reject_handshake abweist (siehe Hinweis unten).
  2. Alte generierte Configs löschen (*-https.generated.conf, *-http-redirect.generated.conf).
  3. Pro Dienst eine vHost-Datei erzeugen mit Fallunterscheidung:
    • local → statischer Server, der public/ ausliefert (+ /api/-Proxy zum Auth-Service).
    • Reverse-Proxyproxy_pass auf den Upstream; optional WebSocket-Header, optional proxy_ssl_verify, optional auth_request-Schutz.
    • DNS nicht auflösbar → statt Proxy ein 503-Platzhalter („Service nicht erreichbar“), damit nginx trotzdem startet.
    • Kein Zertifikat vorhanden → vHost wird übersprungen (keine 443-Conf); solche Anfragen fängt der 443-Default-Server ab (siehe Hinweis unten).
    • Bei http_behavior = redirect zusätzlich ein 80→443-Redirect (mit Ausnahme für die ACME-Challenge).
  4. Abschließend nginx -t zur Syntaxprüfung.

So ist das System robust: fehlt ein Zertifikat oder ein Backend, fällt nur der betroffene Dienst aus nginx selbst läuft weiter.

Anti-Leak / Default-Server: Ohne expliziten Default bedient nginx eine Anfrage mit unbekanntem server_name mit dem ersten 443-Block (alphabetisch das war ai.server.schooltech.ch). Damit eine Domain ohne passenden vHost (z. B. fehlendes Zertifikat) nicht still bei einem fremden Dienst landet, erzeugt das Skript einen Catch-all listen 443 ssl default_server; ssl_reject_handshake on; unbekanntes SNI bekommt einen sauberen TLS-Abbruch. server.schooltech.ch & Co. treffen ihren eigenen vHost weiterhin per SNI-Match.

Aktiver Pfad vs. Referenz: Die vHosts werden zur Laufzeit von connect-proxies.sh aus forwarding.conf erzeugt. Die statischen Dateien in nginxPages/ sind nicht in docker-compose.yaml gemountet und dienen nur als Referenz/Vorlage (z. B. 10-server-schooltech.conf zeigt eine Variante der Portal-vHost-Konfig).


Authentifizierung

Der Auth-Service (auth/auth.js, Express) verwaltet Login und Sessions:

  • POST /api/login prüft User/Passwort gegen auth/users.json (bcrypt-Hashes) und setzt bei Erfolg ein Cookie SESSIONID.
  • POST /api/logout löscht die Session und das Cookie.
  • GET /api/status sagt dem Frontend, ob eine Session aktiv ist.
  • GET /internal/auth Endpoint für nginx auth_request: liefert 200 bei gültiger Session, sonst 401.
  • POST /api/event einfaches Logging der Button-Klicks.

Das Cookie wird auf domain: .server.schooltech.ch gesetzt und gilt damit für alle Subdomains. Dienste mit auth_required = true (z. B. tcControl, robotVideo, robotDriver) lässt nginx nur durch, wenn auth_request gegen /internal/auth ein 200 zurückgibt sonst landet der Nutzer nicht auf dem Dienst.

Sessions liegen in-memory im Auth-Service ein Neustart des Containers meldet alle Nutzer ab.

Benutzer/Passwörter pflegt man in auth/users.json; einen neuen bcrypt-Hash erzeugt auth/cretePassword.js.


TLS / Let's Encrypt

  • Zertifikate liegen unter letsencrypt/conf/live/<domain>/ und werden in den nginx-Container gemountet (/etc/letsencrypt).
  • letsEncrypt.sh holt/erneuert per certbot certonly --webroot ein Zertifikat pro Domain (über den appServer_LetsEncryptFetcher-Container, HTTP-01-Challenge unter /.well-known/acme-challenge/).
  • letsEncrypt_crontab.txt enthält den Cron-Eintrag für die automatische Erneuerung.

Container & Betrieb

Alles wird per docker-compose.yaml orchestriert. Wichtige Host-Pfade (auf dem Server unter /home/chk/Documents/appServerPortalUI/):

Host-Pfad Mount im Container Zweck
nginx.conf …/conf.d/default.conf Basis-vHost (Port 80, ACME, /api/, SPA)
public/ …/nginx/html die Portal-Seite
forwarding.conf /etc/nginx/forwarding.conf Dienst-Definitionen
connect-proxies.sh /docker-entrypoint.d/40-… vHost-Generator
letsencrypt/conf /etc/letsencrypt Zertifikate
letsencrypt/www /var/www/certbot ACME-Webroot
auth/ /usr/src/app (Auth-Container) Auth-Service-Code

Start / Neuladen:

# Alles starten
docker compose up -d

# nginx-vHosts neu generieren (nach Änderung an forwarding.conf):
docker restart appServer_PortalUI

# Logs des Generators ansehen
docker logs appServer_PortalUI | grep connect-proxies

Neuen Dienst hinzufügen

  1. Zeile in forwarding.conf ergänzen, z. B.:
    meinDienst.server.schooltech.ch  http://appServer_TunnelHead:9999  redirect  true  false
    
  2. DNS: sicherstellen, dass die Subdomain auf den Server zeigt (Wildcard *.server.schooltech.ch oder A-Record).
  3. Zertifikat holen Domain in letsEncrypt.sh aufnehmen und Skript laufen lassen (sonst überspringt connect-proxies.sh den vHost).
  4. nginx neu starten: docker restart appServer_PortalUI.
  5. Optional im Portal sichtbar machen: Eintrag im services-Array in public/app.js ergänzen:
    { id: "mein", name: "Mein Dienst", url: "https://meinDienst.server.schooltech.ch/" }
    
  6. Optional Login erzwingen: in forwarding.conf als 8. Spalte … server.schooltech.ch 443 true setzen.

Dateiübersicht

appServerPortalUI/
├── docker-compose.yaml          # Orchestrierung aller Container
├── nginx.conf                   # Basis-vHost (Port 80, ACME, /api/, SPA)
├── forwarding.conf              # ← Dienst-Definitionen (Quelle der Wahrheit)
├── connect-proxies.sh           # generiert die nginx-vHosts beim Start
├── letsEncrypt.sh               # Zertifikate holen/erneuern
├── letsEncrypt_crontab.txt      # Cron-Eintrag für Auto-Renewal
├── public/                      # die Portal-Seite (server.schooltech.ch)
│   ├── index.html               #   Grundgerüst + Navigationsleiste
│   ├── app.js                   #   Logik: Login, Dienst-Buttons, iFrame
│   └── style.css                #   Styling
├── auth/                        # Auth-Service (Node/Express)
│   ├── auth.js                  #   Login/Logout/Status/internal-auth
│   ├── users.json               #   Benutzer + bcrypt-Hashes
│   ├── cretePassword.js         #   Hilfsskript: neuen Hash erzeugen
│   └── package.json
├── letsencrypt/                 # Zertifikate + ACME-Webroot
├── certs/                       # (selbstsignierte) Zertifikate
├── nginxPages/                  # Referenz-vHosts (NICHT gemountet)
└── doc/                         # Doku, Diagramme & Notizen
    ├── Architektur.svg          #   Architektur-Diagramm (Quelle für PDF/PNG)
    ├── Portal.svg / Portal.pdf  #   Ansicht der Portal-Seite
    └── AI_Gen.* , *_q?_*.txt    #   weitere Notizen / generierte Doku

forwarding_running_6_6_2026.conf ist ein Snapshot der aktuell laufenden Konfiguration und wird hier bewusst (noch) nicht behandelt.

Description
Server UI
Readme 2 MiB
Languages
JavaScript 47.4%
Shell 38.5%
CSS 7.1%
HTML 7%