diff --git a/.air.toml b/.air.toml index 91be4cb..9520ff3 100644 --- a/.air.toml +++ b/.air.toml @@ -1,11 +1,11 @@ -root = "." +root = "./src" tmp_dir = "tmp" [build] bin = "./tmp/main" cmd = "go build -o ./tmp/main ./src/." delay = 1000 - exclude_dir = ["tmp", "vendor", "ui/dist", "ui/node_modules", "ui/src"] + exclude_dir = ["assets", "tmp", "vendor"] exclude_file = [] exclude_regex = [] exclude_unchanged = false diff --git a/.gitignore b/.gitignore index 56dfd25..d59a62b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ build tmp -/sebrauc.toml -/htpasswd diff --git a/.golangci.yaml b/.golangci.yaml index 2e06378..3898f1c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,6 +1,7 @@ linters: presets: - bugs + - unused - import - module @@ -13,17 +14,11 @@ linters: disable: - scopelint - - noctx linters-settings: lll: line-length: 88 - tab-width: 4 gocognit: min-complexity: 10 nestif: min-complexity: 3 - errcheck: - exclude-functions: - - "(*github.com/gin-gonic/gin.Context).Error" - - "(*github.com/gin-gonic/gin.Context).AbortWithError" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc4ae18..bf556ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,10 @@ repos: name: GolangCI Lint - id: go-test-repo-mod name: Backend tests - + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.4.1 + hooks: + - id: prettier - repo: local hooks: - id: tsc @@ -17,8 +20,3 @@ repos: args: ["-p", "./ui/tsconfig.json"] additional_dependencies: ["typescript@4.5.2"] pass_filenames: false - - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.4.1 - hooks: - - id: prettier diff --git a/ui/.prettierignore b/.prettierignore similarity index 66% rename from ui/.prettierignore rename to .prettierignore index d638edf..f06235c 100644 --- a/ui/.prettierignore +++ b/.prettierignore @@ -1,4 +1,2 @@ node_modules dist -tmp -.tmp diff --git a/.woodpecker.yml b/.woodpecker.yml deleted file mode 100644 index 69668a7..0000000 --- a/.woodpecker.yml +++ /dev/null @@ -1,14 +0,0 @@ -pipeline: - frontend: - image: node:16-alpine - commands: - - cd ui - - npm install -g pnpm - - pnpm install - - pnpm run build - backend: - image: golangci/golangci-lint:latest - commands: - - go get -t ./src/... - - golangci-lint run --timeout 5m - - go test -v ./src/... diff --git a/Makefile b/Makefile index 8da4406..ed465a0 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,19 @@ SRC_DIR=./src UI_DIR=./ui -APIDOC_FILE=./src/server/swagger/swagger.yaml - -VER=$(or ${VERSION},$(shell git tag --sort=-version:refname | head -n 1)) +VERSION=$(shell git tag --sort=-version:refname | head -n 1) setup: + go get -t ./src/... cd ${UI_DIR} && pnpm install test: go test -v ./src/... -lint: - golangci-lint run - cd ${UI_DIR} && npm run format && npm run lint - build-ui: - cd ${UI_DIR} && pnpm run build + cd ${UI_DIR} && VITE_VERSION=${VERSION} pnpm run build build-server: - go build -tags prod -ldflags "-s -w -X code.thetadev.de/TSGRain/SEBRAUC/src/util.version=${VER}" -o build/sebrauc ./src/. + go build -tags prod -ldflags "-s -w -X code.thetadev.de/TSGRain/SEBRAUC/src/util.version=${VERSION}" -o build/sebrauc ./src/. build: build-ui build-server - -generate-apidoc: - SWAGGER_GENERATE_EXTENSION=false swagger generate spec --scan-models -o ${APIDOC_FILE} - -generate-apiclient: - openapi-generator generate -i ${APIDOC_FILE} -g typescript-axios -o ${UI_DIR}/src/sebrauc-client -p "supportsES6=true" - cd ${UI_DIR} && npm run format - -clean: - rm -f build/* - rm -rf ${UI_DIR}/dist/** diff --git a/README.md b/README.md new file mode 100644 index 0000000..c074425 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +![SEBRAUC](ui/src/assets/logo_border.svg) diff --git a/README.rst b/README.rst deleted file mode 100644 index ecb1dfc..0000000 --- a/README.rst +++ /dev/null @@ -1,460 +0,0 @@ -.. image:: ui/src/assets/logo_border.svg - -SEBRAUC ist eine einfach zu bedienende Weboberfläche für den -`RAUC `__ Firmware-Updater, die es -erlaubt, Softwareupdates auf Embedded-Systemen wie dem Raspberry Pi zu -installieren. - -Ich habe die Anwendung für die TSGRain-Bewässerungssteuerung entwickelt, -da es noch keine einfache Möglichkeit gab, RAUC-Updates über eine -Weboberfläche zu installieren. - -Der Updatevorgang funktioniert ähnlich wie bei älteren -WLAN-Routern. Das ``*.raucb``-Firmwarepaket kann über den Browser an das -Gerät übertragen und installiert werden. Im Gegensatz zu OTA-basierten -Lösungen wie `hawkBit `__ -wird keine Internetverbindung, sondern nur ein lokaler WLAN-Access-Point -für die Datenübertragung benötigt. Das macht SEBRAUC ideal für fest -installierte Geräte an abgelegenen Orten wie unser -TSGRain-Bewässerungssystem. - -Funktion -######## - -Startseite -========== - -.. image:: _screenshots/update0.png - :width: 500 - -Auf der Startseite findet der Nutzer einen großen Upload-Button, mit dem -er ein ``*.raucb``-Image auswählen und hochladen kann. - -Update hochladen -================ - -.. image:: _screenshots/update1.png - :width: 500 - -Wurde eine Update-Datei ausgewählt, wird sie auf den Server -hochgeladen und zunächst temporär im Ordner ``/tmp/sebrauc`` gespeichert. -Dabei wird der Uploadfortschritt mit einer kreisförmigen Skala -angezeigt. - -Update installieren -=================== - -.. image:: _screenshots/update2.png - :width: 500 - -Ist der Uplodad der Datei abgeschlossen, beginnt die Installation. -SEBRAUC startet den RAUC-Updater mit dem entsprechenden -Kommandozeilenbefehl. - -Während des Updatevorgangs liefert RAUC folgende Konsolenausgabe: - -.. code-block:: - - installing - 0% Installing - 0% Determining slot states - 20% Determining slot states done. - 20% Checking bundle - 20% Verifying signature - 40% Verifying signature done. - 40% Checking bundle done. - 40% Checking manifest contents - 60% Checking manifest contents done. - 60% Determining target install group - 80% Determining target install group done. - 80% Updating slots - 80% Checking slot rootfs.0 - 90% Checking slot rootfs.0 done. - 90% Copying image to rootfs.0 - 100% Copying image to rootfs.0 done. - 100% Updating slots done. - 100% Installing done. - idle - Installing `/app/tsgrain-update-raspberrypi3.raucb` succeeded - -Aus dieser Konsolenausgabe lässt sich der aktuelle Fortschritt in -Prozent und der Status ermitteln. SEBRAUC leitet diese Daten -in Echtzeit an die Weboberfläche weiter. So kann der Nutzer live -den Updatevorgang mitverfolgen. - -Wenn RAUC eine Fehlermeldung ausgibt, wird diese mit einem roten -Warndreieck am unteren Bildschirmrand angezeigt. - - -Installation abgeschlossen -========================== - -.. image:: _screenshots/update3.png - :width: 500 - -Ist die Installation abgeschlossen, kehrt SEBRAUC zum Startbildschirm -zurück. Am unteren Rand erhält der Nutzer den Hinweis, dass das System -neu gestartet werden muss, um die neue Software in Betrieb zu nehmen. -Drückt man auf den :guilabel:`Restart`-Button, startet das System mit -einer Verzögerung von 3 Sekunden neu. - -Systeminformationen -=================== - -.. image:: _screenshots/sysinfo.png - :width: 500 - -Mit der Schaltfläche oben rechts lässt sich ein Informationsbildschirm -einblenden, der einige Statusinformationen über das System und den -RAUC-Updater anzeigt. -So lässt sich in Erfahrung bringen, welches Betriebssystem in welcher Version -gerade ausgeführt wird und welche der 2 Root-Partitionen gerade aktiv ist. - -Der RAUC-Updater hat eine Konfigurationsoption, mit der festgelegt werden -kann, welche Firmware und Firmware-Variante akzeptiert wird. Diese Werte -werden ebenfalls auf dem Systeminformationsbildschirm angezeigt, -damit der Nutzer sehen kann, welche Firmware-Images mit seinem Gerät -kompatibel sind. - - -Technische Details -################## - -SEBRAUC besteht aus zwei Komponenten: dem in Go geschriebenen Backend -und einer mit Preact und Typescript umgesetzten Single-Page-Weboberfläche. - -Das Backend steuert den RAUC-Updater und stellt die Web-API bereit, die vom -Frontend angesprochen wird. Ich habe für das Backend das -`Gin-Webframework `_ verwendet. Hierbei -handelt es sich um ein minimales und performantes Framework in der Programmiersprache -Go, vergleichbar mit Flask für Python oder Express für -Javascript. Es erlaubt die einfache Erstellung von Routen und Handlern -für Webanfragen, bietet aber im Gegensatz zu größeren Frameworks -wie Django keine eingebaute Datenbankintegration oder Administrationsoberfläche. - -Eine asynchrone Kommunikation vom Server zum Client mittels Websockets -kann mit dem Modul ``github.com/gorilla/websocket`` leicht in eine Gin-Anwendung -integriert werden. - -SEBRAUC verwendet eine solche Websocket-Verbindung, um den aktuellen Zustand -des Systems an das Frontend zu streamen. Dadurch erhält der Nutzer immer die aktuellen -Informationen, ohne dass die Seite neu aufgerufen werden oder die Daten -durch das Frontend in bestimmten Zeitabständen aktualisiert werden müssen. - -Beim Frontend kam das Javascript-Framework `Preact `_ -zum Einsatz, was eine kleinere und schnellere Alternative zu React darstellt. - -Da Preact in komprimierter Form nur 3kB groß ist, eignet es sich perfekt -für kleine Webanwendungen, die schnell laden sollen. So ist die fertige -SEBRAUC-Webanwendung inklusive Styles und Icons nur 22kB groß. Hätte ich hierfür -React oder Vue verwendet, würde die Webseite mindestens doppelt so groß sein. - -Der Autor Ryan Carniato hat in -`diesem Blogpost `_ -verschiedene Webframeworks, darunter React, Vue und Preact miteinander verglichen. -Dafür hat er eine simple Todo-Liste mit allen Frameworks gebaut und -die Größe des generierten Javascript-Codes verglichen. Laut diesem Vergleich ist -die React-Anwendung mit 37.5kB fast siebenmal so groß wie die nur 5.6kB große Preact-Anwendung. - -Um auch das Stylesheet der Anwendung kompakt zu halten, wurde auf fertige -CSS-Komponentenbibliotheken wie Bootstrap verzichtet und stattdessen -selbstgeschriebenes scss verwendet. - - -REST-API -======== - -Die Weboberfläche kommuniziert über REST-API-Anfragen mit dem Server. - -Die API ist im OpenAPI 2.0-Format dokumentiert (die yaml-Dokumentation -befindet sich unter ``src/server/swagger/swagger.yaml``). - -OpenAPI ist ein Standard für maschinenlesbare API-Dokumentationen, -aus denen mit geeigneten Tools menschenlesbare Dokumentationsseiten -sowie fertige API-Clients in verschiedenen Programmiersprachen -erzeugen lassen. - -In diesem Projekt verwende ich ein Tool (``go-swagger``), -das Kommentare und Structs aus dem Programmcode einlesen und daraus die -OpenAPI-Dokumentation generieren kann. Daraus wird dann der Code für den -Typescript-Client für die Weboberfläche generiert. - - -Konfiguration -############# - -Die Konfiguration erfolgt über eine Datei im ``*.json``, ``*.yaml`` oder -``*.toml``-Format. Eine Beispielkonfiguration findet sich -`hier `__. - -Alternativ kann die Konfiguration auch mittels Umgebungsvariablen angepasst -werden, wobei verschachtelte Datenstrukturen mit Unterstrichen -addressiert werden. Beispiel: - -:: - - SERVER_ADDRESS=80 SERVER_COMPRESSION_BROTLI=false ./sebrauc - -SEBRAUC sucht zuerst nach einer Konfigurationsdatei mit dem Namen -``sebrauc.{json,yml,yaml,toml}`` im aktuellen Arbeitsverzeichnis und -anschließend im Ordner ``/etc/sebrauc``. Der Pfad zur -Konfigurationsdatei kann auch manuell mit dem Flag ``-c`` angegeben -werden. - -.. list-table:: - :widths: 35 50 35 - :header-rows: 1 - - * - Option - - Beschreibung - - Standard - * - ``Tmpdir`` - - Temporäres Verzeichnis - - ``"/tmp/sebrauc"`` - * - ``Server.Address`` - - IP-Adresse, von der der Server Verbindungen - - akzeptiert. Leer lassen, um Verbindungen von - - allen Adressen zu akzeptieren. - - ``""`` - * - ``Server.Port`` - - Server-Port - - ``80`` - * - ``Server.Websocket.Ping`` - - Zeitabstand, in denen ein Ping vom - - Websocket-Server an den Client erfolgt. - - ``45`` - * - ``Server.Websocket.Timeout`` - - Timeout in Sekunden, nachdem der Server - - eine Websocket-Verbindung ohne Antwort trennt. - - ``45`` - * - ``Server.Compression.Gzip`` - - Webseiteninhalte mit Gzip komprimieren, - - wenn vom Browser unterstützt. - - Optionen: ``"false"``, ``"default"``, - - ``"min"``, ``"max"``, ``"1"``-``"9"`` - - ``"default"`` - * - ``Server.Compression.Brotli`` - - Webseiteninhalte mit Brotli komprimieren, - - wenn vom Browser unterstützt. - - Optionen: ``"false"``, ``"default"``, - - ``"min"``, ``"max"``, ``"1"``-``"11"`` - - ``"default"`` - * - ``Authentication.Enable`` - - HTTP Basic-Authentifizierung - - (Passwortabfrage im Browser) aktivieren. - - ``false`` - * - ``Authentication.PasswdFile`` - - Apache-kompatible Passwortdatei. - - Suche nach einer Passwortdatei - - unter `./htpasswd` oder `/etc/sebrauc/htpasswd`, - - wenn leergelassen. - - ``""`` - * - ``Sysinfo.ReleaseFile`` - - Datei, aus der OS-Informationen gelesen werden. - - ``"/etc/os-release"`` - * - ``Sysinfo.NameKey`` - - Schlüssel für den Namen des Betriebssystems - - aus der ReleaseFile. - - ``"NAME"`` - * - ``Sysinfo.VersionKey`` - - Schlüssel für die Version des Betriebssystems - - aus der ReleaseFile. - - ``"VERSION"`` - * - ``Sysinfo.HostnameFile`` - - Datei, aus der der Hostname gelesen wird. - - ``"/etc/hostname"`` - * - ``Sysinfo.UptimeFile`` - - Datei, aus der die Betriebszeit des Systems - - gelesen wird. - - ``"/proc/uptime"`` - * - ``Commands.RaucStatus`` - - RAUC-Befehl, um den Status im JSON-Format - - auszulesen. - - ``"rauc status`` - - ``--output-format=json"`` - * - ``Commands.RaucInstall`` - - RAUC-Befehl, um ein Update-Paket zu installieren. - - ``"rauc install"`` - * - ``Commands.Reboot`` - - Befehl, um das System neu zu starten. - - ``"shutdown -r 0"`` - - -Passwortdatei -============= - -Ist die Authentifizierung aktiviert, liest SEBRAUC Nutzernamen und -Passwörter aus Apache-kompatiblen Passwortdateien (standardmäßig unter -``./htpasswd`` oder ``/etc/sebrauc/htpasswd``). - -Die Passwörter können im Klartext oder als Hashes vorliegen. -Gehashte Passwörter lassen sich mit mit dem Befehl ``htpasswd -nB username`` -erzeugen. Hierfür wird unter Linux das Paket ``apache-tools`` benötigt. - -.. code-block:: - - # Klartextpasswort (nur zum Testen verwenden) - username:password - - # Gehashtes Passwort - test:$2y$05$EzzPOZprUhPE.1ru1gM8du0ZNpmsU40EFDZ1PmzZtBzkMHsJVK1au - - -Entwicklung und Build -##################### - -Systemvoraussetzungen -===================== - -Um SEBRAUC zu kompilieren werden folgende Programme benötigt: - -- Golang (Version 1.16 oder neuer) -- NodeJS -- PNPM-Paketmanager (installieren mit ``npm i -g pnpm``) - -Zur Entwicklung werden zusätzlich folgende Tools verwendet: - -- `golangci-lint `_ (Überprüft Go-Code auf - Formatierung, Code Style und bestimmete Fehler) -- `air `_ (Kompiliert und startet - Go-Anwendungen bei Codeänderungen neu) -- `go-swagger `_ - (Generierung einer OpenAPI-Dokumentation aus Go-Servercode, der nach - einem bestimmten Schema kommentiert wurde) -- `openapi-generator `_ - (Erzeugt fertige API-Clients aus OpenAPI-Dokumentationen) - -Befehle -======= - -Das Projekt enthät eine Makefile, mit der sich oft benötigte Befehle -schnell ausführen lassen. - -``make setup`` - Dieser Befehl muss vor dem Arbeiten an dem Projekt ausgeführt werden. - Hiermit werden sämtliche Abhängigkeiten für das Frontend - -``make lint`` - Überprüfe den Backend-Code mit ``golangci-lint`` auf Fehler und schlechte - Programmierpraktiken. - Zudem wird der Typescript-Code im Frontend mit dem Typescript-Compiler - auf Typfehler geprüft. - -``make build`` - Baue die Produktionsversion der Software. - Die Softwareversion, die in die Software einkompiliert wird, - wird aus dem aktuellsten Git-Tag ermittelt. - -``make test`` - Führt die Tests für das Backend aus. - -``make generate-apidoc`` - Liest die API-Dokumentation aus den Kommentaren im Quellcode ein - und produziert daraus eine Swagger-Dokumentationsdatei im yaml-Format. - -``make generate-apiclient`` - Erzeugt den Typescript-API-Client für das Frontend aus der Swagger - API-Dokumentation. Um die API-Dokumentation zu aktualisieren, muss - vorher ``make generate-apidoc`` ausgeführt werden. - - -Um die SEBRAUC-Software zu bauen, müssen nach dem Herunterladen des Repositorys -folgende Befehle ausgeführt werden: - -.. code-block:: sh - - # Abhängigkeiten installieren - make setup - - # Build für die CPU-Architektur des laufenden Systems - make build - - # Build für andere CPU-Architekturen (Cross-Compile) - GOARCH=arm make build - -Die kompilierte Anwendung befindet wird im Ordner ``build`` -abgelegt. - - -RAUC-Mock -========= - -Um SEBRAUC testweise auf einem Entwicklerrechner ohne den RAUC-Updater -laufen zu lassen, habe ich ein kleines Go-Programm geschrieben, das -die Konsolenausgabe von RAUC simuliert. - -Man kann dieses Programm anstelle der echten RAUC-Befehle in die -Konfigurationsdatei eintragen. - -.. code-block:: toml - - # Commands to be run by SEBRAUC - [Commands] - # RAUC status command (outputs updater status in json format) - RaucStatus = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock status --output-format=json" - # RAUC install command (installs FW image passed as argument) - RaucInstall = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock install" - -Wird SEBRAUC im Testmodus (d.h. ohne das Build-Flag ``-tags prod``) gebaut, -führt das Programm stets den Mock-RAUC-Befehl aus und ignoriert die konfigurierten -Befehle. Ob sich SEBRAUC im Testmodus befindet, lässt sich an der Hinweisnachricht -beim Start erkennen. - -.. code-block:: text - - Test mode active - no update operations are executed. - Build with -tags prod to enable live mode. - -Das Verhalten von ``rauc_mock`` kann mittels Umgebungsvariablen angepasst werden. -Ist die Variable ``RAUC_MOCK_FAIL`` gesetzt, wird eine fehlerhafte Installation -simuliert. Mit dem Setzen der Variable ``RAUC_MOCK_TEST`` reduziert man die Verzögerung, -mit der die Nachrichten angezeigt werden, von 500 auf 10 Millisekunden. Diese Option -dient dazu, automatische Tests zu beschleunigen, während die Wartezeit bei manuellen -Tests lang genug ist, um sämtliche Statusnachrichten lesen zu können. - - -Auto-Reload des Backends -======================== - -Wenn man am Backend-Code arbeitet und die Webseite oder API dabei gleichzeitig -testen möchte, hat man mit kompilierten Sprachen wie Go das Problem, dass sich -Codeänderungen nicht sofort auf das Verhalten der Anwendung auswirken. -Erst nach einer Rekompilierung und einem Neustart des Servers ist eine -Änderung wirksam. - -Glücklicherweise gibt es Tools wie ``air``, die die Quellcodedateien überwachen -und die Anwendung bei Änderung automatisch kompilieren und neu starten. - -Um den SEBRAUC-Server mit Auto-Reload zu starten, -muss man einfach den Befehl ``air`` im Stammverzeichnis des Projekts ausführen - - -Frontend -======== - -Um den Entwicklungsserver für das Frontend zu starten, navigiert man in das -``ui``-Verzeichnis und startet den Server mit dem Befehl ``npm run dev``. - -Im Entwicklungsmodus kommuniziert das Frontend mit dem Backend unter der -Adresse ``localhost:8080``. Man kann also parallel SEBRAUC mit entsprechender -Konfiguration (Port: 8080) starten, um das Frontend mit einem funktionsfähigen -Server zu testen. diff --git a/_screenshots/sysinfo.png b/_screenshots/sysinfo.png deleted file mode 100644 index a6ca4e2..0000000 Binary files a/_screenshots/sysinfo.png and /dev/null differ diff --git a/_screenshots/update0.png b/_screenshots/update0.png deleted file mode 100644 index faa7ebe..0000000 Binary files a/_screenshots/update0.png and /dev/null differ diff --git a/_screenshots/update1.png b/_screenshots/update1.png deleted file mode 100644 index 2196373..0000000 Binary files a/_screenshots/update1.png and /dev/null differ diff --git a/_screenshots/update2.png b/_screenshots/update2.png deleted file mode 100644 index 7dfcfed..0000000 Binary files a/_screenshots/update2.png and /dev/null differ diff --git a/_screenshots/update3.png b/_screenshots/update3.png deleted file mode 100644 index 399edef..0000000 Binary files a/_screenshots/update3.png and /dev/null differ diff --git a/go.mod b/go.mod index 92ad7c6..5b0a27c 100644 --- a/go.mod +++ b/go.mod @@ -3,29 +3,10 @@ module code.thetadev.de/TSGRain/SEBRAUC go 1.16 require ( - code.thetadev.de/TSGRain/ginzip v0.1.1 - github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db - github.com/fortytw2/leaktest v1.3.0 - github.com/gin-contrib/cors v1.3.1 - github.com/gin-gonic/gin v1.7.7 - github.com/go-errors/errors v1.4.1 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-cmp v0.5.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gofiber/fiber/v2 v2.21.0 + github.com/gofiber/websocket/v2 v2.0.12 github.com/google/uuid v1.3.0 - github.com/gorilla/websocket v1.4.1 - github.com/jinzhu/configor v1.2.1 - github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/stretchr/testify v1.7.0 - github.com/tg123/go-htpasswd v1.2.0 - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index c082427..6bcb5c4 100644 --- a/go.sum +++ b/go.sum @@ -1,128 +1,48 @@ -code.thetadev.de/TSGRain/ginzip v0.1.1 h1:+X0L6qumEZiKYSLmM+Q0LqKVHsKvdcg4CVzsEpvM7fk= -code.thetadev.de/TSGRain/ginzip v0.1.1/go.mod h1:BH7VkvpP83vPRyMQ8rLIjKycQwGzF+/mFV0BKzg+BuA= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= -github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db h1:oZ4U9IqO8NS+61OmGTBi8vopzqTRxwQeogyBHdrhjbc= -github.com/ekyoung/gin-nice-recovery v0.0.0-20160510022553-1654dca486db/go.mod h1:Pk7/9x6tyChFTkahDvLBQMlvdsWvfC+yU8HTT5VD314= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= -github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= -github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= -github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= -github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg= -github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/fasthttp/websocket v1.4.3-rc.9 h1:CWJH0vONrOatdKXZgkgbFKWllijD9aY50C5KfbSDcWk= +github.com/fasthttp/websocket v1.4.3-rc.9/go.mod h1:eXL2zqDbexYJxaCw8/PQlm7VcMK6uoGvwbYbTdt4dFo= +github.com/gofiber/fiber/v2 v2.20.1/go.mod h1:/LdZHMUXZvTTo7gU4+b1hclqCAdoQphNQ9bi9gutPyI= +github.com/gofiber/fiber/v2 v2.21.0 h1:tdRNrgqWqcHWBwE3o51oAleEVsil4Ro02zd2vMEuP4Q= +github.com/gofiber/fiber/v2 v2.21.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ= +github.com/gofiber/websocket/v2 v2.0.12 h1:jKwTrXiOut9UGOGEzFTAD6gq+/78mM3NcrI05VbxjAU= +github.com/gofiber/websocket/v2 v2.0.12/go.mod h1:lQRy0u5ACJfiez/e/bhGeYvM0/M940Y3NFw14U3/otI= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko= -github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8= +github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0= -github.com/tg123/go-htpasswd v1.2.0/go.mod h1:h7IzlfpvIWnVJhNZ0nQ9HaFxHb7pn5uFJYLlEUJa2sM= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.29.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= +github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= +github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE= +github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/openapi.yml b/openapi.yml new file mode 100644 index 0000000..a9332ce --- /dev/null +++ b/openapi.yml @@ -0,0 +1,107 @@ +openapi: "3.0.3" +info: + title: SEBRAUC + version: "0.0.1" +servers: + - url: http://localhost:8080/api +paths: + /status: + get: + responses: + "200": + description: OK + content: + "application/json": + schema: + $ref: "#/components/schemas/RaucStatus" + default: + description: "Server error" + content: + "application/json": + schema: + $ref: "#/components/schemas/StatusMessage" + /update: + post: + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + updateFile: + type: string + format: binary + responses: + "200": + description: "OK" + content: + "application/json": + schema: + $ref: "#/components/schemas/StatusMessage" + default: + description: "Server error" + content: + "application/json": + schema: + $ref: "#/components/schemas/StatusMessage" + + /reboot: + post: + responses: + "200": + description: "OK" + content: + "application/json": + schema: + $ref: "#/components/schemas/StatusMessage" + default: + description: "Server error" + content: + "application/json": + schema: + $ref: "#/components/schemas/StatusMessage" + +components: + schemas: + RaucStatus: + type: object + properties: + installing: + description: "True if the installer is running" + type: boolean + percent: + description: "Installation progress" + type: integer + minimum: 0 + maximum: 100 + message: + description: "Current installation step" + type: string + example: "Copying image to rootfs.0" + last_error: + description: "Installation error message" + type: string + example: "Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?" + log: + description: "Full command line output of the current installation" + type: string + example: "0% Installing\n0% Determining slot states\n20% Determining slot states done.\n" + required: + - installing + - percent + - message + - last_error + - log + + StatusMessage: + type: object + properties: + success: + description: "Is operation successful" + type: boolean + msg: + description: "Success message" + type: string + example: "Update started" + required: + - msg diff --git a/sebrauc.example.toml b/sebrauc.example.toml deleted file mode 100644 index 5f95ce0..0000000 --- a/sebrauc.example.toml +++ /dev/null @@ -1,44 +0,0 @@ -# SEBRAUC config file example - -# Temporary directory -# Update packages are stored temporarily under Tmpdir/update-.raucb -Tmpdir = "/tmp/sebrauc" - -# Webserver options -[Server] -# IP address to accept connections from -Address = "" -# Port to listen at -Port = 8080 - -# Websocket connection settings: Ping interval/Timeout in seconds -[Server.Websocket] -Ping = 45 -Timeout = 15 - -# Compression settings. Refer to code.thetadev.de/TSGRain/ginzip for details. -[Server.Compression] -Gzip = "default" -Brotli = "default" - -[Authentication] -Enable = true -PasswdFile = "htpasswd" - -# Where to obtain system info from -[Sysinfo] -ReleaseFile = "/etc/os-release" -# Keys to look for in the OS release file -NameKey = "NAME" -VersionKey = "VERSION" -HostnameFile = "/etc/hostname" -UptimeFile = "/proc/uptime" - -# Commands to be run by SEBRAUC -[Commands] -# RAUC status command (outputs updater status in json format) -RaucStatus = "rauc status --output-format=json" -# RAUC install command (installs FW image passed as argument) -RaucInstall = "rauc install" -# System reboot command -Reboot = "shutdown -r 0" diff --git a/src/assets/assets.go b/src/assets/assets.go new file mode 100644 index 0000000..8f2c9de --- /dev/null +++ b/src/assets/assets.go @@ -0,0 +1,8 @@ +package assets + +import ( + "embed" +) + +//go:embed files/** +var Assets embed.FS diff --git a/src/assets/files/index.html b/src/assets/files/index.html new file mode 100644 index 0000000..0554e60 --- /dev/null +++ b/src/assets/files/index.html @@ -0,0 +1,88 @@ + + + + + Chat Example + + + + +
+ +
+ + +
+ + diff --git a/src/config/config.go b/src/config/config.go deleted file mode 100644 index a46106d..0000000 --- a/src/config/config.go +++ /dev/null @@ -1,130 +0,0 @@ -package config - -import ( - "strings" - - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd" - "code.thetadev.de/TSGRain/SEBRAUC/src/util" - "code.thetadev.de/TSGRain/SEBRAUC/src/util/mode" - "github.com/jinzhu/configor" -) - -var ( - cfgFilePaths = []string{"sebrauc", "/etc/sebrauc/sebrauc"} - cfgFileTypes = []string{"toml", "yaml", "yml", "json"} -) - -// SEBRAUC config object -type Config struct { - // Temporary directory - // Update packages are stored temporarily under Tmpdir/update-.raucb - Tmpdir string - - // Webserver options - Server struct { - // IP address to accept connections from - Address string - // Port to listen at - Port int `default:"80"` - - // Websocket connection settings: Ping interval/Timeout in seconds - Websocket struct { - Ping int `default:"45"` - Timeout int `default:"15"` - } - - // Compression settings. Refer to code.thetadev.de/TSGRain/ginzip for details. - Compression struct { - Gzip string - Brotli string - } - } - - Authentication struct { - Enable bool `default:"false"` - PasswdFile string - } - - // Where to obtain system info from - Sysinfo struct { - ReleaseFile string `default:"/etc/os-release"` - // Keys to look for in the OS release file - NameKey string `default:"NAME"` - VersionKey string `default:"VERSION"` - HostnameFile string `default:"/etc/hostname"` - UptimeFile string `default:"/proc/uptime"` - } - - // Commands to be run by SEBRAUC - // Note that these are overriden when running in development mode - Commands struct { - // RAUC status command (outputs updater status in json format) - RaucStatus string `default:"rauc status --output-format=json"` - // RAUC install command (installs FW image passed as argument) - RaucInstall string `default:"rauc install"` - // System reboot command - Reboot string `default:"shutdown -r 0"` - } -} - -func findConfigFile(pathIn string) string { - fpath, err := util.FindFile(pathIn, cfgFilePaths, cfgFileTypes) - if err != nil { - if pathIn != "" { - panic("cfg file not found: " + err.Error()) - } - return "" - } - - return fpath -} - -func stripTrailingSlashes(pathIn string) string { - return strings.TrimRight(pathIn, "/") -} - -func loadConfig(cfgFile string) *Config { - cfg := new(Config) - cfgor := configor.New(&configor.Config{ - // Debug: true, - ENVPrefix: "SEBRAUC", - }) - - err := cfgor.Load(cfg, cfgFile) - if err != nil { - panic(err) - } - - // Override commands with testing options - if mode.IsDev() { - cfg.Commands.RaucStatus = testcmd.RaucStatus - cfg.Commands.RaucInstall = testcmd.RaucInstall - cfg.Commands.Reboot = testcmd.Reboot - } - - cfg.Tmpdir = stripTrailingSlashes(cfg.Tmpdir) - - return cfg -} - -// GetWithFlags returns the configuration extracted from cmdline args, -// env variables or config file. -func GetWithFlags(pathIn string, portIn int) *Config { - cfg := loadConfig(findConfigFile(pathIn)) - - // Override port with cmdline flag if set - if portIn > 0 { - cfg.Server.Port = portIn - } - - return cfg -} - -// Get returns the configuration extracted from env variables or config file. -func Get() *Config { - return loadConfig(findConfigFile("")) -} - -func GetDefault() *Config { - return loadConfig("") -} diff --git a/src/config/config_test.go b/src/config/config_test.go deleted file mode 100644 index 332437a..0000000 --- a/src/config/config_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures" - "code.thetadev.de/TSGRain/SEBRAUC/src/util/mode" - "github.com/stretchr/testify/assert" -) - -func TestDefault(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - cfg := Get() - - assert.Equal(t, "", cfg.Server.Address) - assert.Equal(t, 80, cfg.Server.Port) - - assert.Equal(t, 45, cfg.Server.Websocket.Ping) - assert.Equal(t, 15, cfg.Server.Websocket.Timeout) - - assert.Equal(t, "", cfg.Server.Compression.Gzip) - assert.Equal(t, "", cfg.Server.Compression.Brotli) - - assert.Equal(t, "", cfg.Tmpdir) - - assert.Equal(t, false, cfg.Authentication.Enable) - assert.Equal(t, "", cfg.Authentication.PasswdFile) - - assert.Equal(t, "/etc/os-release", cfg.Sysinfo.ReleaseFile) - assert.Equal(t, "NAME", cfg.Sysinfo.NameKey) - assert.Equal(t, "VERSION", cfg.Sysinfo.VersionKey) - assert.Equal(t, "/etc/hostname", cfg.Sysinfo.HostnameFile) - assert.Equal(t, "/proc/uptime", cfg.Sysinfo.UptimeFile) - - assert.Equal(t, "rauc status --output-format=json", cfg.Commands.RaucStatus) - assert.Equal(t, "rauc install", cfg.Commands.RaucInstall) - assert.Equal(t, "shutdown -r 0", cfg.Commands.Reboot) -} - -func TestConfigFile(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - cfile := filepath.Join(fixtures.GetTestfilesDir(), "sebrauc.toml") - cfg := GetWithFlags(cfile, 0) - - assert.Equal(t, "127.0.0.1", cfg.Server.Address) - assert.Equal(t, 8001, cfg.Server.Port) - - assert.Equal(t, 30, cfg.Server.Websocket.Ping) - assert.Equal(t, 10, cfg.Server.Websocket.Timeout) - - assert.Equal(t, "max", cfg.Server.Compression.Gzip) - assert.Equal(t, "false", cfg.Server.Compression.Brotli) - - assert.Equal(t, "/var/tmp", cfg.Tmpdir) - - assert.Equal(t, true, cfg.Authentication.Enable) - assert.Equal(t, "/etc/htpasswd", cfg.Authentication.PasswdFile) - - assert.Equal(t, "/etc/release", cfg.Sysinfo.ReleaseFile) - assert.Equal(t, "PRETTY_NAME", cfg.Sysinfo.NameKey) - assert.Equal(t, "VER", cfg.Sysinfo.VersionKey) - assert.Equal(t, "/etc/hn", cfg.Sysinfo.HostnameFile) - assert.Equal(t, "/proc/up", cfg.Sysinfo.UptimeFile) - - assert.Equal(t, "myrauc status --output-format=json", cfg.Commands.RaucStatus) - assert.Equal(t, "myrauc install", cfg.Commands.RaucInstall) - assert.Equal(t, "reboot", cfg.Commands.Reboot) -} - -func TestEnvvar(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - fixtures.ResetEnv() - defer fixtures.ResetEnv() - - os.Setenv("SEBRAUC_TMPDIR", "/var/tmp") - os.Setenv("SEBRAUC_SERVER_ADDRESS", "127.0.0.1") - os.Setenv("SEBRAUC_SERVER_PORT", "8001") - os.Setenv("SEBRAUC_SERVER_WEBSOCKET_PING", "30") - os.Setenv("SEBRAUC_SERVER_WEBSOCKET_TIMEOUT", "10") - os.Setenv("SEBRAUC_SERVER_COMPRESSION_GZIP", "max") - os.Setenv("SEBRAUC_SERVER_COMPRESSION_BROTLI", "false") - os.Setenv("SEBRAUC_AUTHENTICATION_ENABLE", "true") - os.Setenv("SEBRAUC_AUTHENTICATION_PASSWDFILE", "/etc/htpasswd") - os.Setenv("SEBRAUC_SYSINFO_RELEASEFILE", "/etc/release") - os.Setenv("SEBRAUC_SYSINFO_NAMEKEY", "PRETTY_NAME") - os.Setenv("SEBRAUC_SYSINFO_VERSIONKEY", "VER") - os.Setenv("SEBRAUC_SYSINFO_HOSTNAMEFILE", "/etc/hn") - os.Setenv("SEBRAUC_SYSINFO_UPTIMEFILE", "/proc/up") - os.Setenv("SEBRAUC_COMMANDS_RAUCSTATUS", "myrauc status --output-format=json") - os.Setenv("SEBRAUC_COMMANDS_RAUCINSTALL", "myrauc install") - os.Setenv("SEBRAUC_COMMANDS_REBOOT", "reboot") - - cfg := Get() - - assert.Equal(t, "127.0.0.1", cfg.Server.Address) - assert.Equal(t, 8001, cfg.Server.Port) - - assert.Equal(t, 30, cfg.Server.Websocket.Ping) - assert.Equal(t, 10, cfg.Server.Websocket.Timeout) - - assert.Equal(t, "max", cfg.Server.Compression.Gzip) - assert.Equal(t, "false", cfg.Server.Compression.Brotli) - - assert.Equal(t, "/var/tmp", cfg.Tmpdir) - - assert.Equal(t, true, cfg.Authentication.Enable) - assert.Equal(t, "/etc/htpasswd", cfg.Authentication.PasswdFile) - - assert.Equal(t, "/etc/release", cfg.Sysinfo.ReleaseFile) - assert.Equal(t, "PRETTY_NAME", cfg.Sysinfo.NameKey) - assert.Equal(t, "VER", cfg.Sysinfo.VersionKey) - assert.Equal(t, "/etc/hn", cfg.Sysinfo.HostnameFile) - assert.Equal(t, "/proc/up", cfg.Sysinfo.UptimeFile) - - assert.Equal(t, "myrauc status --output-format=json", cfg.Commands.RaucStatus) - assert.Equal(t, "myrauc install", cfg.Commands.RaucInstall) - assert.Equal(t, "reboot", cfg.Commands.Reboot) -} - -func TestDevMode(t *testing.T) { - cfg := Get() - - //nolint:lll - assert.Equal(t, "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock status --output-format=json", cfg.Commands.RaucStatus) - //nolint:lll - assert.Equal(t, "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock install", cfg.Commands.RaucInstall) - assert.Equal(t, "touch /tmp/sebrauc_reboot_test", cfg.Commands.Reboot) -} - -func TestFlags(t *testing.T) { - cfg := GetWithFlags("", 8001) - - assert.Equal(t, 8001, cfg.Server.Port) -} - -func TestStripTrailingSlashes(t *testing.T) { - tests := []struct { - in string - out string - }{ - {in: "/tmp", out: "/tmp"}, - {in: "/tmp/", out: "/tmp"}, - {in: "/tmp///", out: "/tmp"}, - } - - for i, tt := range tests { - t.Run(fmt.Sprint(i), func(t *testing.T) { - res := stripTrailingSlashes(tt.in) - assert.Equal(t, tt.out, res) - }) - } -} diff --git a/src/fixtures/rauc_mock/main.go b/src/fixtures/rauc_mock/main.go index 228aeda..4c9b884 100644 --- a/src/fixtures/rauc_mock/main.go +++ b/src/fixtures/rauc_mock/main.go @@ -41,54 +41,22 @@ LastError: Failed to check bundle identifier: Invalid identifier. ` + idle Installing ` + "/app/demo` failed" -const statusJson = `{"compatible":"TSGRain","variant":"dev","booted":"A",` + - `"boot_primary":"rootfs.0","slots":[{"rootfs.1":{"class":"rootfs",` + - `"device":"/dev/mmcblk0p3","type":"ext4","bootname":"B","state":"inactive",` + - `"parent":null,"mountpoint":null,"boot_status":"good"}},{"rootfs.0":` + - `{"class":"rootfs","device":"/dev/mmcblk0p2","type":"ext4","bootname":"A",` + - `"state":"booted","parent":null,"mountpoint":"/","boot_status":"good"}}]}` +func main() { + arg := "" + if len(os.Args) > 1 { + arg = os.Args[1] + } + + var lines string + switch arg { + case "fail": + lines = outputFailure + default: + lines = outputSuccess + } -func printLinesWithDelay(lines string, delay time.Duration) { for _, line := range strings.Split(lines, "\n") { fmt.Println(line) - time.Sleep(delay) - } -} - -func getBoolEnvvar(name string) bool { - val := strings.ToLower(os.Getenv(name)) - return val != "" && val != "false" && val != "0" -} - -func main() { - method := "" - if len(os.Args) > 1 { - method = os.Args[1] - } - - test := getBoolEnvvar("RAUC_MOCK_TEST") - failure := getBoolEnvvar("RAUC_MOCK_FAIL") - - delay := 500 * time.Millisecond - if test { - delay = 10 * time.Millisecond - } - - switch method { - case "install": - if failure { - printLinesWithDelay(outputFailure, delay) - } else { - printLinesWithDelay(outputSuccess, delay) - } - case "status": - if os.Args[2] != "--output-format=json" { - fmt.Println("output format must be json") - os.Exit(1) - } - fmt.Println(statusJson) - default: - fmt.Println("invalid method") - os.Exit(1) + time.Sleep(500 * time.Millisecond) } } diff --git a/src/fixtures/testcmd/testcmd.go b/src/fixtures/testcmd/testcmd.go deleted file mode 100644 index b157bea..0000000 --- a/src/fixtures/testcmd/testcmd.go +++ /dev/null @@ -1,8 +0,0 @@ -package testcmd - -//nolint:lll -const ( - RaucStatus = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock status --output-format=json" - RaucInstall = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock install" - Reboot = "touch /tmp/sebrauc_reboot_test" -) diff --git a/src/fixtures/testfiles/htpasswd b/src/fixtures/testfiles/htpasswd deleted file mode 100644 index c6382c7..0000000 --- a/src/fixtures/testfiles/htpasswd +++ /dev/null @@ -1,3 +0,0 @@ -plain:1234 -md5:$apr1$V2wxHBfb$gBU2yIYjTIeciKapglql6/ -bcrypt:$2y$05$f9rV6uTQEEnNR1saPksExOR31LauUZzpLDhpCrodAvxX3zZ6nLy12 diff --git a/src/fixtures/testfiles/os-release b/src/fixtures/testfiles/os-release deleted file mode 100644 index 84a41a1..0000000 --- a/src/fixtures/testfiles/os-release +++ /dev/null @@ -1,5 +0,0 @@ -ID=tsgrain -NAME="TSGRain distro" -VERSION="0.0.1" -VERSION_ID=0.0.1 -PRETTY_NAME="TSGRain distro 0.0.1" diff --git a/src/fixtures/testfiles/sebrauc.toml b/src/fixtures/testfiles/sebrauc.toml deleted file mode 100644 index 2700a3f..0000000 --- a/src/fixtures/testfiles/sebrauc.toml +++ /dev/null @@ -1,32 +0,0 @@ -# SEBRAUC config file for testing -# Dont use for real, the commands and paths are not correct - -Tmpdir = "/var/tmp/" - -[Server] -Address = "127.0.0.1" -Port = 8001 - -[Server.Websocket] -Ping = 30 -Timeout = 10 - -[Server.Compression] -Gzip = "max" -Brotli = "false" - -[Authentication] -Enable = true -PasswdFile = "/etc/htpasswd" - -[Sysinfo] -ReleaseFile = "/etc/release" -NameKey = "PRETTY_NAME" -VersionKey = "VER" -HostnameFile = "/etc/hn" -UptimeFile = "/proc/up" - -[Commands] -RaucStatus = "myrauc status --output-format=json" -RaucInstall = "myrauc install" -Reboot = "reboot" diff --git a/src/fixtures/testutil.go b/src/fixtures/testutil.go index 624eb29..ee2f352 100644 --- a/src/fixtures/testutil.go +++ b/src/fixtures/testutil.go @@ -3,11 +3,8 @@ package fixtures import ( "os" "path/filepath" - "strings" ) -var envPrefixes = []string{"SEBRAUC", "RAUC_MOCK"} - func doesFileExist(filepath string) bool { _, err := os.Stat(filepath) return !os.IsNotExist(err) @@ -41,20 +38,3 @@ func GetTestfilesDir() string { CdProjectRoot() return filepath.Join("src", "fixtures", "testfiles") } - -func ResetEnv() { - for _, envvar := range os.Environ() { - split := strings.SplitN(envvar, "=", 2) - if len(split) != 2 { - continue - } - - key := split[0] - - for _, prefix := range envPrefixes { - if strings.HasPrefix(key, prefix) { - _ = os.Unsetenv(key) - } - } - } -} diff --git a/src/fixtures/testutil_test.go b/src/fixtures/testutil_test.go index 79941f3..44b32ef 100644 --- a/src/fixtures/testutil_test.go +++ b/src/fixtures/testutil_test.go @@ -35,15 +35,3 @@ func TestCdProjectRoot(t *testing.T) { CdProjectRoot() assert.True(t, doesFileExist("go.sum")) } - -func TestResetEnv(t *testing.T) { - os.Setenv("RAUC_MOCK_TEST", "1") - os.Setenv("SEBRAUC_PORT", "8001") - - ResetEnv() - - _, exists := os.LookupEnv("RAUC_MOCK_TEST") - assert.False(t, exists) - _, exists = os.LookupEnv("SEBRAUC_PORT") - assert.False(t, exists) -} diff --git a/src/main.go b/src/main.go index 3fcd902..2e83122 100644 --- a/src/main.go +++ b/src/main.go @@ -1,45 +1,22 @@ package main import ( - "flag" "fmt" "log" - "os" - "code.thetadev.de/TSGRain/SEBRAUC/src/config" "code.thetadev.de/TSGRain/SEBRAUC/src/server" "code.thetadev.de/TSGRain/SEBRAUC/src/util" - "code.thetadev.de/TSGRain/SEBRAUC/src/util/mode" ) -const titleArt = ` _____ __________ ____ ___ __ ________ - / ___// ____/ __ )/ __ \/ | / / / / ____/ - \__ \/ __/ / __ / /_/ / /| |/ / / / / - ___/ / /___/ /_/ / _, _/ ___ / /_/ / /___ -/____/_____/_____/_/ |_/_/ |_\____/\____/ ` - func main() { - run(os.Args[1:]) -} + fmt.Println("SEBRAUC " + util.Version()) -func run(args []string) { - fmt.Println(titleArt + util.Version() + "\n") - - cmdFlags := flag.NewFlagSet("sebrauc", flag.ExitOnError) - port := cmdFlags.Int("p", 0, "HTTP port") - cfgPath := cmdFlags.String("c", "", "Config file path") - _ = cmdFlags.Parse(args) - - if mode.IsDev() { + if util.TestMode { fmt.Println("Test mode active - no update operations are executed.") fmt.Println("Build with -tags prod to enable live mode.") } - cfg := config.GetWithFlags(*cfgPath, *port) - - fmt.Printf("Starting server at %s:%d\n", cfg.Server.Address, cfg.Server.Port) - - srv := server.NewServer(cfg) + srv := server.NewServer(":8080") err := srv.Run() if err != nil { log.Fatalln(err) diff --git a/src/model/error.go b/src/model/error.go deleted file mode 100644 index b2f1396..0000000 --- a/src/model/error.go +++ /dev/null @@ -1,24 +0,0 @@ -package model - -// Error model -// -// The Error contains error relevant information. -// -//swagger:model Error -type Error struct { - // The general error message according to HTTP specification. - // - // required: true - // example: Unauthorized - Error string `json:"error"` - // The http error code. - // - // required: true - // example: 500 - StatusCode int `json:"status_code"` - // Concrete error message. - // - // required: true - // example: already running - Message string `json:"msg"` -} diff --git a/src/model/rauc_status.go b/src/model/rauc_status.go deleted file mode 100644 index eb0d92d..0000000 --- a/src/model/rauc_status.go +++ /dev/null @@ -1,34 +0,0 @@ -package model - -// RaucStatus model -// -// RaucStatus contains information about the current RAUC updater status. -// -//swagger:model RaucStatus -//nolint:lll -type RaucStatus struct { - // True if the installer is running - // required: true - Installing bool `json:"installing"` - - // Installation progress - // required: true - // minimum: 0 - // maximum: 100 - Percent int `json:"percent"` - - // Current installation step - // required: true - // example: Copying image to rootfs.0 - Message string `json:"message"` - - // Installation error message - // required: true - // example: Failed to check bundle identifier: Invalid identifier. - LastError string `json:"last_error"` - - // Full command line output of the current installation - // required: true - // example: 0% Installing 0% Determining slot states 20% Determining slot states done - Log string `json:"log"` -} diff --git a/src/model/status_message.go b/src/model/status_message.go deleted file mode 100644 index d01fe03..0000000 --- a/src/model/status_message.go +++ /dev/null @@ -1,17 +0,0 @@ -package model - -// StatusMessage model -// -// StatusMessage contains the status of an operation. -// -//swagger:model StatusMessage -type StatusMessage struct { - // Is operation successful? - // required: true - Success bool `json:"success"` - - // Status message text - // required: true - // example: Update started - Msg string `json:"msg"` -} diff --git a/src/model/system_info.go b/src/model/system_info.go deleted file mode 100644 index 6c923d7..0000000 --- a/src/model/system_info.go +++ /dev/null @@ -1,75 +0,0 @@ -package model - -// SystemInfo model -// -// SystemInfo contains information about the running system. -// -//swagger:model SystemInfo -type SystemInfo struct { - // Hostname of the system - // required: true - // example: raspberrypi3 - Hostname string `json:"hostname"` - - // Name of the os distribution - // required: true - // example: Poky - OsName string `json:"os_name"` - - // Operating system version - // required: true - // example: 1.0.2 - OsVersion string `json:"os_version"` - - // System uptime in seconds - // required: true - // example: 5832 - Uptime int `json:"uptime"` - - // Compatible firmware name - // required: true - // example: Poky - RaucCompatible string `json:"rauc_compatible"` - - // Compatible firmware variant - // required: true - // example: rpi-prod - RaucVariant string `json:"rauc_variant"` - - // List of RAUC root filesystems - // required: true - RaucRootfs map[string]Rootfs `json:"rauc_rootfs"` -} - -//swagger:model Rootfs -type Rootfs struct { - // Block device - // required: true - // example: /dev/mmcblk0p2 - Device string `json:"device"` - - // Filesystem - // required: true - // example: ext4 - Type string `json:"type"` - - // Mount path (null when not mounted) - // required: true - // nullable: true - // example: / - Mountpoint *string `json:"mountpoint"` - - // Is the filesystem bootable? - // required: true - Bootable bool `json:"bootable"` - - // Is the filesystem booted? - // required: true - Booted bool `json:"booted"` - - // Is the filesystem the next boot target? - // required: true - Primary bool `json:"primary"` - - Bootname string `json:"-"` -} diff --git a/src/rauc/rauc.go b/src/rauc/rauc.go index d4b81dc..7585285 100644 --- a/src/rauc/rauc.go +++ b/src/rauc/rauc.go @@ -9,7 +9,6 @@ import ( "strconv" "sync" - "code.thetadev.de/TSGRain/SEBRAUC/src/model" "code.thetadev.de/TSGRain/SEBRAUC/src/util" ) @@ -19,26 +18,32 @@ var ( ) type Rauc struct { - cmdRaucInstall string - bc util.Broadcaster - status model.RaucStatus - runningMtx sync.Mutex + broadcast chan string + status RaucStatus + runningMtx sync.Mutex } -func New(cmdRaucInstall string) *Rauc { - return &Rauc{ - cmdRaucInstall: cmdRaucInstall, +type RaucStatus struct { + Installing bool `json:"installing"` + Percent int `json:"percent"` + Message string `json:"message"` + LastError string `json:"last_error"` + Log string `json:"log"` +} + +func NewRauc(broadcast chan string) *Rauc { + r := &Rauc{ + broadcast: broadcast, } -} -func (r *Rauc) SetBroadcaster(bc util.Broadcaster) { - r.bc = bc - r.bcStatus() + r.broadcast <- r.GetStatusJson() + + return r } func (r *Rauc) completed(updateFile string) { r.status.Installing = false - r.bcStatus() + r.broadcast <- r.GetStatusJson() _ = os.Remove(updateFile) } @@ -60,12 +65,12 @@ func (r *Rauc) RunRauc(updateFile string) error { } // Reset installer - r.status = model.RaucStatus{ + r.status = RaucStatus{ Installing: true, } - r.bcStatus() + r.broadcast <- r.GetStatusJson() - cmd := util.CommandFromString(r.cmdRaucInstall + " " + updateFile) + cmd := util.CommandFromString(fmt.Sprintf("%s %s", util.UpdateCmd, updateFile)) readPipe, _ := cmd.StdoutPipe() cmd.Stderr = cmd.Stdout @@ -95,7 +100,7 @@ func (r *Rauc) RunRauc(updateFile string) error { } if hasUpdate { - r.bcStatus() + r.broadcast <- r.GetStatusJson() } } }() @@ -117,19 +122,11 @@ func (r *Rauc) RunRauc(updateFile string) error { return nil } -func (r *Rauc) GetStatus() model.RaucStatus { +func (r *Rauc) GetStatus() RaucStatus { return r.status } -func (r *Rauc) GetStatusJson() []byte { - statusJson, err := json.Marshal(r.status) - if err != nil { - return []byte{} - } - - return statusJson -} - -func (r *Rauc) bcStatus() { - r.bc.Broadcast(r.GetStatusJson()) +func (r *Rauc) GetStatusJson() string { + statusJson, _ := json.Marshal(r.status) + return string(statusJson) } diff --git a/src/rauc/rauc_test.go b/src/rauc/rauc_test.go deleted file mode 100644 index 19595cd..0000000 --- a/src/rauc/rauc_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package rauc - -import ( - "os" - "path/filepath" - "testing" - "time" - - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures" - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd" - "code.thetadev.de/TSGRain/SEBRAUC/src/util" - "github.com/stretchr/testify/assert" -) - -type broadcasterMock struct { - messages []string -} - -func (b *broadcasterMock) Broadcast(msg []byte) { - b.messages = append(b.messages, string(msg)) -} - -func TestRauc(t *testing.T) { - //nolint:lll - tests := []struct { - name string - fail string - messages []string - }{ - { - name: "ok", - fail: "", - messages: []string{ - "{\"installing\":false,\"percent\":0,\"message\":\"\",\"last_error\":\"\",\"log\":\"\"}", - "{\"installing\":true,\"percent\":0,\"message\":\"\",\"last_error\":\"\",\"log\":\"\"}", - "{\"installing\":true,\"percent\":0,\"message\":\"Installing\",\"last_error\":\"\",\"log\":\"0% Installing\\n\"}", - "{\"installing\":true,\"percent\":0,\"message\":\"Determining slot states\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n\"}", - "{\"installing\":true,\"percent\":20,\"message\":\"Determining slot states done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n\"}", - "{\"installing\":true,\"percent\":20,\"message\":\"Checking bundle\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n\"}", - "{\"installing\":true,\"percent\":20,\"message\":\"Verifying signature\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n\"}", - "{\"installing\":true,\"percent\":40,\"message\":\"Verifying signature done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n\"}", - "{\"installing\":true,\"percent\":40,\"message\":\"Checking bundle done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n\"}", - "{\"installing\":true,\"percent\":40,\"message\":\"Checking manifest contents\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n\"}", - "{\"installing\":true,\"percent\":60,\"message\":\"Checking manifest contents done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n\"}", - "{\"installing\":true,\"percent\":60,\"message\":\"Determining target install group\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n\"}", - "{\"installing\":true,\"percent\":80,\"message\":\"Determining target install group done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n\"}", - "{\"installing\":true,\"percent\":80,\"message\":\"Updating slots\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n\"}", - "{\"installing\":true,\"percent\":80,\"message\":\"Checking slot rootfs.0\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n\"}", - "{\"installing\":true,\"percent\":90,\"message\":\"Checking slot rootfs.0 done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n\"}", - "{\"installing\":true,\"percent\":90,\"message\":\"Copying image to rootfs.0\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n\"}", - "{\"installing\":true,\"percent\":100,\"message\":\"Copying image to rootfs.0 done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n100% Copying image to rootfs.0 done.\\n\"}", - "{\"installing\":true,\"percent\":100,\"message\":\"Updating slots done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n100% Copying image to rootfs.0 done.\\n100% Updating slots done.\\n\"}", - "{\"installing\":true,\"percent\":100,\"message\":\"Installing done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n100% Copying image to rootfs.0 done.\\n100% Updating slots done.\\n100% Installing done.\\n\"}", - "{\"installing\":false,\"percent\":100,\"message\":\"Installing done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n20% Verifying signature\\n40% Verifying signature done.\\n40% Checking bundle done.\\n40% Checking manifest contents\\n60% Checking manifest contents done.\\n60% Determining target install group\\n80% Determining target install group done.\\n80% Updating slots\\n80% Checking slot rootfs.0\\n90% Checking slot rootfs.0 done.\\n90% Copying image to rootfs.0\\n100% Copying image to rootfs.0 done.\\n100% Updating slots done.\\n100% Installing done.\\nInstalling `/app/tsgrain-update-raspberrypi3.raucb` succeeded\\n\"}", - }, - }, - { - name: "fail", - fail: "1", - messages: []string{ - "{\"installing\":false,\"percent\":0,\"message\":\"\",\"last_error\":\"\",\"log\":\"\"}", - "{\"installing\":true,\"percent\":0,\"message\":\"\",\"last_error\":\"\",\"log\":\"\"}", - "{\"installing\":true,\"percent\":0,\"message\":\"Installing\",\"last_error\":\"\",\"log\":\"0% Installing\\n\"}", - "{\"installing\":true,\"percent\":0,\"message\":\"Determining slot states\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n\"}", - "{\"installing\":true,\"percent\":20,\"message\":\"Determining slot states done.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n\"}", - "{\"installing\":true,\"percent\":20,\"message\":\"Checking bundle\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n\"}", - "{\"installing\":true,\"percent\":40,\"message\":\"Checking bundle failed.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n40% Checking bundle failed.\\n\"}", - "{\"installing\":true,\"percent\":100,\"message\":\"Installing failed.\",\"last_error\":\"\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n40% Checking bundle failed.\\n100% Installing failed.\\n\"}", - "{\"installing\":true,\"percent\":100,\"message\":\"Installing failed.\",\"last_error\":\"Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n40% Checking bundle failed.\\n100% Installing failed.\\nLastError: Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?\\n\"}", - "{\"installing\":false,\"percent\":100,\"message\":\"Installing failed.\",\"last_error\":\"Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?\",\"log\":\"0% Installing\\n0% Determining slot states\\n20% Determining slot states done.\\n20% Checking bundle\\n40% Checking bundle failed.\\n100% Installing failed.\\nLastError: Failed to check bundle identifier: Invalid identifier. Did you pass a valid RAUC bundle?\\nInstalling /app/demo` failed\\n\"}", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fixtures.ResetEnv() - defer fixtures.ResetEnv() - - os.Setenv("RAUC_MOCK_TEST", "1") - os.Setenv("RAUC_MOCK_FAIL", tt.fail) - - updater := New(testcmd.RaucInstall) - bc := &broadcasterMock{} - updater.SetBroadcaster(bc) - - testfile := createTmpfile() - - err := updater.RunRauc(testfile) - assert.NoError(t, err) - - // Dont run multiple updates concurrently - err = updater.RunRauc(testfile) - assert.ErrorIs(t, err, util.ErrAlreadyRunning) - - // Wait for updater to finish - for updater.GetStatus().Installing { - time.Sleep(50 * time.Millisecond) - } - - assert.False(t, util.DoesFileExist(testfile), "update file was not deleted") - - assert.Equal(t, tt.messages, bc.messages) - }) - } -} - -func createTmpfile() string { - tmpdir := util.GetTmpdir("") - - tmpfile := filepath.Join(tmpdir, "test.raucb") - _, err := os.Create(tmpfile) - if err != nil { - panic(err) - } - - return tmpfile -} diff --git a/src/server/hub.go b/src/server/hub.go new file mode 100644 index 0000000..77e1dfd --- /dev/null +++ b/src/server/hub.go @@ -0,0 +1,98 @@ +package server + +import ( + "log" + "sync" + + "github.com/gofiber/websocket/v2" +) + +type hubClient struct{} + +type MessageHub struct { + Broadcast chan string + + clients map[*websocket.Conn]hubClient + register chan *websocket.Conn + unregister chan *websocket.Conn + lastMessage string + + running bool + runningMtx sync.Mutex +} + +func NewHub() *MessageHub { + return &MessageHub{ + clients: make(map[*websocket.Conn]hubClient), + register: make(chan *websocket.Conn), + Broadcast: make(chan string, 5), + unregister: make(chan *websocket.Conn), + } +} + +func (hub *MessageHub) sendMessage(conn *websocket.Conn, message string) { + if err := conn.WriteMessage( + websocket.TextMessage, []byte(message)); err != nil { + log.Println("write error:", err) + + _ = conn.WriteMessage(websocket.CloseMessage, []byte{}) + _ = conn.Close() + delete(hub.clients, conn) + } +} + +func (hub *MessageHub) Run() { + hub.runningMtx.Lock() + isRunning := hub.running + hub.running = true + hub.runningMtx.Unlock() + + if isRunning { + return + } + + for { + select { + case conn := <-hub.register: + hub.clients[conn] = hubClient{} + log.Println("connection registered") + + case message := <-hub.Broadcast: + log.Println("message received:", message) + hub.lastMessage = message + + // Send the message to all clients + for conn := range hub.clients { + hub.sendMessage(conn, message) + } + + case conn := <-hub.unregister: + // Remove the client from the hub + delete(hub.clients, conn) + + log.Println("connection unregistered") + } + } +} + +func (hub *MessageHub) Handler(conn *websocket.Conn) { + // When the function returns, unregister the client and close the connection + defer func() { + hub.unregister <- conn + conn.Close() + }() + + // Register the client + hub.register <- conn + + if hub.lastMessage != "" { + hub.sendMessage(conn, hub.lastMessage) + } + + for { + _, _, err := conn.ReadMessage() + if err != nil { + return // Calls the deferred function, i.e. closes the connection on error + } + } +} diff --git a/src/server/middleware/authentication.go b/src/server/middleware/authentication.go deleted file mode 100644 index c2f33f2..0000000 --- a/src/server/middleware/authentication.go +++ /dev/null @@ -1,36 +0,0 @@ -package middleware - -import ( - "net/http" - - "code.thetadev.de/TSGRain/SEBRAUC/src/util" - "github.com/gin-gonic/gin" - "github.com/tg123/go-htpasswd" -) - -var pwdFilePaths = []string{"htpasswd", "/etc/sebrauc/htpasswd"} - -// Authentication requires HTTP basic auth or an active session -func Authentication(pwdFile string) gin.HandlerFunc { - fpath, err := util.FindFile(pwdFile, pwdFilePaths, nil) - if err != nil { - panic("passwd file not found: " + err.Error()) - } - - myauth, err := htpasswd.New(fpath, htpasswd.DefaultSystems, nil) - if err != nil { - panic(err) - } - - return func(c *gin.Context) { - if user, pass, ok := c.Request.BasicAuth(); ok { - if myauth.Match(user, pass) { - c.Set(gin.AuthUserKey, user) - return - } - } - - c.Header("WWW-Authenticate", "Basic realm=\"Authorization Required\"") - c.AbortWithStatus(http.StatusUnauthorized) - } -} diff --git a/src/server/middleware/authentication_test.go b/src/server/middleware/authentication_test.go deleted file mode 100644 index f430032..0000000 --- a/src/server/middleware/authentication_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package middleware - -import ( - "net/http" - "net/http/httptest" - "path/filepath" - "testing" - - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -func TestAuthentication(t *testing.T) { - testfiles := fixtures.GetTestfilesDir() - pwdfile := filepath.Join(testfiles, "htpasswd") - - router := gin.New() - router.Use(Authentication(pwdfile)) - router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "HelloWorld") }) - - tests := []struct { - name string - }{ - {name: "plain"}, - {name: "md5"}, - {name: "bcrypt"}, - } - - for _, tt := range tests { - t.Run(tt.name+"_ok", func(t *testing.T) { - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - req.SetBasicAuth(tt.name, "1234") - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "HelloWorld", w.Body.String()) - assert.Empty(t, w.Header().Get("WWW-Authenticate")) - }) - } - - t.Run("fail", func(t *testing.T) { - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - req.SetBasicAuth("plain", "asdf") - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Empty(t, w.Body.String()) - assert.Equal(t, "Basic realm=\"Authorization Required\"", - w.Header().Get("WWW-Authenticate")) - }) -} diff --git a/src/server/middleware/cache.go b/src/server/middleware/cache.go deleted file mode 100644 index 8c97b26..0000000 --- a/src/server/middleware/cache.go +++ /dev/null @@ -1,7 +0,0 @@ -package middleware - -import "github.com/gin-gonic/gin" - -func Cache(c *gin.Context) { - c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") -} diff --git a/src/server/middleware/cache_test.go b/src/server/middleware/cache_test.go deleted file mode 100644 index eaf7f84..0000000 --- a/src/server/middleware/cache_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package middleware - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -func TestCache(t *testing.T) { - router := gin.New() - router.Use(Cache) - router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "HelloWorld") }) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "HelloWorld", w.Body.String()) - assert.Equal(t, "public, max-age=604800, immutable", - w.Header().Get("Cache-Control")) -} diff --git a/src/server/middleware/compression.go b/src/server/middleware/compression.go deleted file mode 100644 index 0fb0100..0000000 --- a/src/server/middleware/compression.go +++ /dev/null @@ -1,14 +0,0 @@ -package middleware - -import ( - "code.thetadev.de/TSGRain/ginzip" - "github.com/gin-gonic/gin" -) - -func Compression(gzip, brotli string) gin.HandlerFunc { - opts := ginzip.DefaultOptions() - opts.GzipLevel = gzip - opts.BrotliLevel = brotli - - return ginzip.New(opts) -} diff --git a/src/server/middleware/error_handler.go b/src/server/middleware/error_handler.go deleted file mode 100644 index 94aac0f..0000000 --- a/src/server/middleware/error_handler.go +++ /dev/null @@ -1,58 +0,0 @@ -package middleware - -import ( - "errors" - "fmt" - "net/http" - - "code.thetadev.de/TSGRain/SEBRAUC/src/model" - "code.thetadev.de/TSGRain/SEBRAUC/src/util" - nice "github.com/ekyoung/gin-nice-recovery" - "github.com/gin-gonic/gin" -) - -// ErrorHandler creates a gin middleware for handling errors. -func ErrorHandler(isApi bool) gin.HandlerFunc { - return func(c *gin.Context) { - c.Next() - - if len(c.Errors) > 0 { - for _, e := range c.Errors { - writeError(c, e.Err, isApi) - } - } - } -} - -func PanicHandler(isApi bool) gin.HandlerFunc { - return nice.Recovery(func(c *gin.Context, err interface{}) { - writeError(c, fmt.Errorf("[PANIC] %s", err), isApi) - }) -} - -func writeError(c *gin.Context, err error, isApi bool) { - status := http.StatusInternalServerError - - var httpErr util.HttpError - if errors.As(err, &httpErr) { - status = httpErr.StatusCode() - } - - // only write error message if there is no content - if c.Writer.Size() != -1 { - c.Status(status) - return - } - - if isApi { - // Machine-readable JSON error message - c.JSON(status, &model.Error{ - Error: http.StatusText(status), - StatusCode: status, - Message: err.Error(), - }) - } else { - // Human-readable error message - c.String(status, "%d %s: %s", status, http.StatusText(status), err.Error()) - } -} diff --git a/src/server/middleware/error_handler_test.go b/src/server/middleware/error_handler_test.go deleted file mode 100644 index 4efedb1..0000000 --- a/src/server/middleware/error_handler_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package middleware - -import ( - "errors" - "net/http" - "net/http/httptest" - "testing" - - "code.thetadev.de/TSGRain/SEBRAUC/src/util" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -func TestErrorHandler(t *testing.T) { - tests := []struct { - name string - controller gin.HandlerFunc - isApi bool - expectResponse string - expectStatus int - }{ - { - name: "error", - controller: controllerError, - isApi: false, - expectResponse: "400 Bad Request: error test", - expectStatus: http.StatusBadRequest, - }, - { - name: "error_api", - controller: controllerError, - isApi: true, - //nolint:lll - expectResponse: `{"error":"Bad Request","status_code":400,"msg":"error test"}`, - expectStatus: http.StatusBadRequest, - }, - { - name: "generic_error", - controller: controllerErrorGeneric, - isApi: false, - expectResponse: "500 Internal Server Error: generic error", - expectStatus: http.StatusInternalServerError, - }, - { - name: "generic_error_api", - controller: controllerErrorGeneric, - isApi: true, - //nolint:lll - expectResponse: `{"error":"Internal Server Error","status_code":500,"msg":"generic error"}`, - expectStatus: http.StatusInternalServerError, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - router := gin.New() - router.Use(ErrorHandler(tt.isApi)) - router.GET("/", tt.controller) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, tt.expectStatus, w.Code) - assert.Equal(t, tt.expectResponse, w.Body.String()) - }) - } -} - -func TestPanicHandler(t *testing.T) { - tests := []struct { - name string - controller gin.HandlerFunc - isApi bool - expectResponse string - expectStatus int - }{ - { - name: "panic", - controller: controllerPanic, - isApi: false, - expectResponse: "500 Internal Server Error: [PANIC] panic message", - expectStatus: http.StatusInternalServerError, - }, - { - name: "panic_api", - controller: controllerPanic, - isApi: true, - //nolint:lll - expectResponse: `{"error":"Internal Server Error","status_code":500,"msg":"[PANIC] panic message"}`, - expectStatus: http.StatusInternalServerError, - }, - { - name: "panic_w_error", - controller: controllerPanicErr, - isApi: false, - expectResponse: "500 Internal Server Error: [PANIC] panic message in error", - expectStatus: http.StatusInternalServerError, - }, - { - name: "panic_w_error_api", - controller: controllerPanicErr, - isApi: true, - //nolint:lll - expectResponse: `{"error":"Internal Server Error","status_code":500,"msg":"[PANIC] panic message in error"}`, - expectStatus: http.StatusInternalServerError, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - router := gin.New() - router.Use(PanicHandler(tt.isApi)) - router.GET("/", tt.controller) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, tt.expectStatus, w.Code) - assert.Equal(t, tt.expectResponse, w.Body.String()) - }) - } -} - -func controllerError(c *gin.Context) { - c.Error(util.HttpErrNew("error test", http.StatusBadRequest)) -} - -func controllerErrorGeneric(c *gin.Context) { - c.Error(errors.New("generic error")) -} - -func controllerPanic(c *gin.Context) { - panic("panic message") -} - -func controllerPanicErr(c *gin.Context) { - panic(errors.New("panic message in error")) -} diff --git a/src/server/server.go b/src/server/server.go index fe41c45..b9ca070 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -1,266 +1,158 @@ -// SEBRAUC -// -// # REST API for the SEBRAUC firmware updater -// -// --- -// Schemes: http, https -// Version: 0.2.0 -// License: MIT -// -// swagger:meta package server import ( "errors" "fmt" "net/http" + "strings" "time" - "code.thetadev.de/TSGRain/SEBRAUC/src/config" - "code.thetadev.de/TSGRain/SEBRAUC/src/model" "code.thetadev.de/TSGRain/SEBRAUC/src/rauc" - "code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware" - "code.thetadev.de/TSGRain/SEBRAUC/src/server/stream" - "code.thetadev.de/TSGRain/SEBRAUC/src/server/swagger" - "code.thetadev.de/TSGRain/SEBRAUC/src/sysinfo" "code.thetadev.de/TSGRain/SEBRAUC/src/util" - "code.thetadev.de/TSGRain/SEBRAUC/src/util/mode" "code.thetadev.de/TSGRain/SEBRAUC/ui" - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/compress" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/filesystem" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/websocket/v2" "github.com/google/uuid" ) type SEBRAUCServer struct { - config *config.Config - streamer *stream.API - updater *rauc.Rauc - sysinfo *sysinfo.Sysinfo - tmpdir string + address string + raucUpdater *rauc.Rauc + hub *MessageHub + tmpdir string } -func NewServer(config *config.Config) *SEBRAUCServer { - updater := rauc.New(config.Commands.RaucInstall) - streamer := stream.New( - time.Duration(config.Server.Websocket.Ping)*time.Second, - time.Duration(config.Server.Websocket.Timeout)*time.Second, - []string{}, - ) - sysinfo := sysinfo.New( - config.Commands.RaucStatus, - config.Sysinfo.ReleaseFile, - config.Sysinfo.NameKey, - config.Sysinfo.VersionKey, - config.Sysinfo.HostnameFile, - config.Sysinfo.UptimeFile, - ) +type statusMessage struct { + Success bool `json:"success"` + Msg string `json:"msg"` +} - updater.SetBroadcaster(streamer) +func NewServer(address string) *SEBRAUCServer { + hub := NewHub() - tmpdir := util.GetTmpdir(config.Tmpdir) + raucUpdater := rauc.NewRauc(hub.Broadcast) + + tmpdir, err := util.GetTmpdir() + if err != nil { + panic(err) + } return &SEBRAUCServer{ - config: config, - updater: updater, - streamer: streamer, - sysinfo: sysinfo, - tmpdir: tmpdir, + address: address, + raucUpdater: raucUpdater, + hub: hub, + tmpdir: tmpdir, } } -func (srv *SEBRAUCServer) getRouter() *gin.Engine { - router := gin.New() - router.Use(gin.Logger()) - _ = router.SetTrustedProxies(nil) - - if mode.IsDev() { - router.Use(cors.Default()) - } - - router.Use(middleware.ErrorHandler(false), middleware.PanicHandler(false)) - router.NoRoute(func(c *gin.Context) { c.Error(util.ErrPageNotFound) }) - - if srv.config.Authentication.Enable { - router.Use(middleware.Authentication(srv.config.Authentication.PasswdFile)) - } - - api := router.Group("/api", - middleware.ErrorHandler(true), middleware.PanicHandler(true)) - - // API ROUTES - api.GET("/ws", srv.streamer.Handle) - api.GET("/status", srv.controllerStatus) - api.GET("/info", srv.controllerInfo) - - api.POST("/update", srv.controllerUpdate) - api.POST("/reboot", srv.controllerReboot) - - // Error routes for testing - if mode.IsDev() { - router.GET("/error", srv.controllerError) - router.GET("/panic", srv.controllerPanic) - - api.GET("/error", srv.controllerError) - api.GET("/panic", srv.controllerPanic) - } - - // UI - uiGroup := router.Group("/", middleware.Compression( - srv.config.Server.Compression.Gzip, - srv.config.Server.Compression.Brotli), - ) - ui.Register(uiGroup) - swagger.Register(uiGroup) - - return router -} - func (srv *SEBRAUCServer) Run() error { - router := srv.getRouter() + app := fiber.New(fiber.Config{ + AppName: "SEBRAUC", + BodyLimit: 1024 * 1024 * 1024, + ErrorHandler: errorHandler, + DisableStartupMessage: true, + }) - return router.Run(fmt.Sprintf("%s:%d", - srv.config.Server.Address, srv.config.Server.Port)) + app.Use(logger.New()) + + app.Use(compress.New(compress.Config{ + Next: func(c *fiber.Ctx) bool { + return strings.HasPrefix(c.Path(), "/api") + }, + })) + + // just for testing + app.Use("/api", cors.New()) + + app.Use("/api/ws", func(c *fiber.Ctx) error { + // IsWebSocketUpgrade returns true if the client + // requested upgrade to the WebSocket protocol. + if websocket.IsWebSocketUpgrade(c) { + c.Locals("allowed", true) + return c.Next() + } + return fiber.ErrUpgradeRequired + }) + + app.Use("/", filesystem.New(filesystem.Config{ + Root: http.FS(ui.Assets), + PathPrefix: ui.AssetsDir, + MaxAge: 7200, + })) + + // ROUTES + app.Get("/api/ws", websocket.New(srv.hub.Handler)) + app.Post("/api/update", srv.controllerUpdate) + app.Get("/api/status", srv.controllerStatus) + app.Post("/api/reboot", srv.controllerReboot) + + // Start messaging hub + go srv.hub.Run() + + return app.Listen(srv.address) } -// swagger:operation POST /update startUpdate -// -// # Start the update process -// -// --- -// consumes: -// - multipart/form-data -// produces: [application/json] -// parameters: -// - name: updateFile -// in: formData -// description: RAUC firmware image file (*.raucb) -// required: true -// type: file -// -// responses: -// -// 200: -// description: Ok -// schema: -// $ref: "#/definitions/StatusMessage" -// 409: -// description: already running -// schema: -// $ref: "#/definitions/Error" -// 500: -// description: Server Error -// schema: -// $ref: "#/definitions/Error" -func (srv *SEBRAUCServer) controllerUpdate(c *gin.Context) { +func (srv *SEBRAUCServer) controllerUpdate(c *fiber.Ctx) error { file, err := c.FormFile("updateFile") if err != nil { - c.Error(err) - return + return err } uid, err := uuid.NewRandom() if err != nil { - c.Error(err) - return + return err } updateFile := fmt.Sprintf("%s/update_%s.raucb", srv.tmpdir, uid.String()) - err = c.SaveUploadedFile(file, updateFile) + err = c.SaveFile(file, updateFile) if err != nil { - c.Error(err) - return + return err } - err = srv.updater.RunRauc(updateFile) + err = srv.raucUpdater.RunRauc(updateFile) if err == nil { writeStatus(c, true, "Update started") + } else if errors.Is(err, util.ErrAlreadyRunning) { + return fiber.NewError(fiber.StatusConflict, "already running") } else { - c.Error(err) - return + return err } + return nil } -// swagger:operation GET /status getStatus -// -// # Get the current status of the RAUC updater -// -// --- -// produces: [application/json] -// responses: -// -// 200: -// description: Ok -// schema: -// $ref: "#/definitions/RaucStatus" -// 500: -// description: Server Error -// schema: -// $ref: "#/definitions/Error" -func (srv *SEBRAUCServer) controllerStatus(c *gin.Context) { - c.JSON(http.StatusOK, srv.updater.GetStatus()) +func (srv *SEBRAUCServer) controllerStatus(c *fiber.Ctx) error { + c.Context().SetStatusCode(200) + _ = c.JSON(srv.raucUpdater.GetStatus()) + return nil } -// swagger:operation GET /info getInfo -// -// # Get the current system info -// -// --- -// produces: [application/json] -// responses: -// -// 200: -// description: Ok -// schema: -// $ref: "#/definitions/SystemInfo" -// 500: -// description: Server Error -// schema: -// $ref: "#/definitions/Error" -func (srv *SEBRAUCServer) controllerInfo(c *gin.Context) { - info, err := srv.sysinfo.GetSysinfo() - if err != nil { - c.Error(err) - } else { - c.JSON(http.StatusOK, info) - } -} - -// swagger:operation POST /reboot startReboot -// -// # Reboot the system -// -// --- -// produces: [application/json] -// responses: -// -// 200: -// description: Ok -// schema: -// $ref: "#/definitions/StatusMessage" -// 500: -// description: Server Error -// schema: -// $ref: "#/definitions/Error" -func (srv *SEBRAUCServer) controllerReboot(c *gin.Context) { - go util.Reboot(srv.config.Commands.Reboot, 5*time.Second) +func (srv *SEBRAUCServer) controllerReboot(c *fiber.Ctx) error { + go util.Reboot(5 * time.Second) writeStatus(c, true, "System is rebooting") + return nil } -// controllerError throws an error for testing -func (srv *SEBRAUCServer) controllerError(c *gin.Context) { - c.Error(util.HttpErrNew("error test", http.StatusBadRequest)) +func errorHandler(c *fiber.Ctx, err error) error { + // API error handling + if strings.HasPrefix(c.Path(), "/api") { + writeStatus(c, false, err.Error()) + } + return err } -// controllerPanic panics for testing -func (srv *SEBRAUCServer) controllerPanic(c *gin.Context) { - panic(errors.New("panic message")) -} - -func writeStatus(c *gin.Context, success bool, msg string) { - c.JSON(http.StatusOK, model.StatusMessage{ +func writeStatus(c *fiber.Ctx, success bool, msg string) { + _ = c.JSON(statusMessage{ Success: success, Msg: msg, }) + + if success { + c.Context().SetStatusCode(200) + } } diff --git a/src/server/server_test.go b/src/server/server_test.go deleted file mode 100644 index 30b25b1..0000000 --- a/src/server/server_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package server - -import ( - "bytes" - "encoding/json" - "io" - "mime/multipart" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "regexp" - "testing" - "time" - - "code.thetadev.de/TSGRain/SEBRAUC/src/config" - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures" - "code.thetadev.de/TSGRain/SEBRAUC/src/model" - "code.thetadev.de/TSGRain/SEBRAUC/src/util" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -type testServer struct { - srv *SEBRAUCServer - router *gin.Engine -} - -func newTestServer() *testServer { - return newTestServerCfg(config.GetDefault()) -} - -func newTestServerCfg(cfg *config.Config) *testServer { - sebraucServer := NewServer(cfg) - router := sebraucServer.getRouter() - - return &testServer{ - srv: sebraucServer, - router: router, - } -} - -func (srv *testServer) testRequest(t assert.TestingT, method string, url string, - body io.Reader, contentType string, -) *httptest.ResponseRecorder { - req, err := http.NewRequest(method, url, body) - req.Header.Set("Content-Type", contentType) - assert.Nil(t, err) - - w := httptest.NewRecorder() - srv.router.ServeHTTP(w, req) - - return w -} - -func TestUpdate(t *testing.T) { - fixtures.ResetEnv() - defer fixtures.ResetEnv() - util.RemoveTmpdir("") - - os.Setenv("RAUC_MOCK_TEST", "1") - - srv := newTestServer() - - updateContent := []byte("mock update file") - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile("updateFile", "update.raucb") - assert.Nil(t, err) - _, err = part.Write(updateContent) - assert.Nil(t, err) - err = writer.Close() - assert.Nil(t, err) - - w := srv.testRequest(t, "POST", "/api/update", body, writer.FormDataContentType()) - - assert.Equal(t, 200, w.Code) - assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) - assert.Equal(t, - `{"success":true,"msg":"Update started"}`, - w.Body.String(), - ) - - // Find update file - tmpdir := util.GetTmpdir("") - //nolint:lll - updateFileExp := regexp.MustCompile( - `update_[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\.raucb`) - updateFile := "" - - tmpdirObj, err := os.Open(tmpdir) - assert.Nil(t, err) - list, err := tmpdirObj.ReadDir(-1) - assert.Nil(t, err) - - for _, f := range list { - if updateFileExp.MatchString(f.Name()) { - updateFile = filepath.Join(tmpdir, f.Name()) - break - } - } - assert.NotEmpty(t, updateFile, "update file not found") - - // Check update file - content, err := os.ReadFile(updateFile) - assert.Nil(t, err) - assert.Equal(t, updateContent, content) - - // Wait for update to complete - time.Sleep(1000 * time.Millisecond) - - // Update file should be removed when update is completed - assert.NoFileExists(t, updateFile) - - // Get final status - w = srv.testRequest(t, "GET", "/api/status", nil, "") - - assert.Equal(t, 200, w.Code) - assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) - //nolint:lll - assert.Equal(t, - `{"installing":false,"percent":100,"message":"Installing done.","last_error":"","log":"0% Installing\n0% Determining slot states\n20% Determining slot states done.\n20% Checking bundle\n20% Verifying signature\n40% Verifying signature done.\n40% Checking bundle done.\n40% Checking manifest contents\n60% Checking manifest contents done.\n60% Determining target install group\n80% Determining target install group done.\n80% Updating slots\n80% Checking slot rootfs.0\n90% Checking slot rootfs.0 done.\n90% Copying image to rootfs.0\n100% Copying image to rootfs.0 done.\n100% Updating slots done.\n100% Installing done.\nInstalling `+"`/app/tsgrain-update-raspberrypi3.raucb`"+` succeeded\n"}`, - w.Body.String(), - ) -} - -func TestStatus(t *testing.T) { - srv := newTestServer() - w := srv.testRequest(t, "GET", "/api/status", nil, "") - - assert.Equal(t, 200, w.Code) - assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) - assert.Equal(t, - `{"installing":false,"percent":0,"message":"","last_error":"","log":""}`, - w.Body.String(), - ) -} - -func TestInfo(t *testing.T) { - srv := newTestServer() - w := srv.testRequest(t, "GET", "/api/info", nil, "") - - assert.Equal(t, 200, w.Code) - assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) - - var info model.SystemInfo - err := json.Unmarshal(w.Body.Bytes(), &info) - assert.Nil(t, err) - - assert.Equal(t, "TSGRain", info.RaucCompatible) - assert.Equal(t, "dev", info.RaucVariant) - assert.Len(t, info.RaucRootfs, 2) -} - -func TestReboot(t *testing.T) { - srv := newTestServer() - testfile := "/tmp/sebrauc_reboot_test" - _ = os.Remove(testfile) - - w := srv.testRequest(t, "POST", "/api/reboot", nil, "") - - assert.Equal(t, 200, w.Code) - assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) - assert.Equal(t, - `{"success":true,"msg":"System is rebooting"}`, - w.Body.String(), - ) - - time.Sleep(5100 * time.Millisecond) - - assert.FileExists(t, testfile) -} - -func TestAuth(t *testing.T) { - testfiles := fixtures.GetTestfilesDir() - - cfg := config.GetDefault() - cfg.Authentication.Enable = true - cfg.Authentication.PasswdFile = filepath.Join(testfiles, "htpasswd") - - srv := newTestServerCfg(cfg) - - t.Run("fail", func(t *testing.T) { - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - req.SetBasicAuth("plain", "asdf") - srv.router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Empty(t, w.Body.String()) - assert.Equal(t, "Basic realm=\"Authorization Required\"", - w.Header().Get("WWW-Authenticate")) - }) - - t.Run("ok", func(t *testing.T) { - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - req.SetBasicAuth("plain", "1234") - srv.router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.NotEmpty(t, w.Body.String()) - assert.Empty(t, w.Header().Get("WWW-Authenticate")) - }) -} diff --git a/src/server/stream/client.go b/src/server/stream/client.go deleted file mode 100644 index c00249f..0000000 --- a/src/server/stream/client.go +++ /dev/null @@ -1,119 +0,0 @@ -package stream - -import ( - "errors" - "fmt" - "time" - - "github.com/gorilla/websocket" -) - -const ( - writeWait = 2 * time.Second -) - -var ping = func(conn *websocket.Conn) error { - return conn.WriteMessage(websocket.PingMessage, nil) -} - -var writeBytes = func(conn *websocket.Conn, data []byte) error { - return conn.WriteMessage(websocket.TextMessage, data) -} - -type client struct { - conn *websocket.Conn - onClose func(*client) - write chan []byte - id uint - once once -} - -func newClient(conn *websocket.Conn, id uint, onClose func(*client)) *client { - return &client{ - conn: conn, - write: make(chan []byte, 1), - id: id, - onClose: onClose, - } -} - -// Close closes the connection. -func (c *client) Close() { - c.once.Do(func() { - c.conn.Close() - close(c.write) - }) -} - -// NotifyClose closes the connection and notifies that the connection was closed. -func (c *client) NotifyClose() { - c.once.Do(func() { - c.conn.Close() - close(c.write) - c.onClose(c) - }) -} - -// startWriteHandler starts listening on the client connection. -// As we do not need anything from the client, -// we ignore incoming messages. Leaves the loop on errors. -func (c *client) startReading(pongWait time.Duration) { - defer c.NotifyClose() - c.conn.SetReadLimit(64) - _ = c.conn.SetReadDeadline(time.Now().Add(pongWait)) - c.conn.SetPongHandler(func(appData string) error { - _ = c.conn.SetReadDeadline(time.Now().Add(pongWait)) - return nil - }) - for { - if _, _, err := c.conn.NextReader(); err != nil { - printWebSocketError("ReadError", err) - return - } - } -} - -// startWriteHandler starts the write loop. The method has the following tasks: -// * ping the client in the interval provided as parameter -// * write messages send by the channel to the client -// * on errors exit the loop. -func (c *client) startWriteHandler(pingPeriod time.Duration) { - pingTicker := time.NewTicker(pingPeriod) - defer func() { - c.NotifyClose() - pingTicker.Stop() - }() - - for { - select { - case message, ok := <-c.write: - if !ok { - return - } - - _ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if err := writeBytes(c.conn, message); err != nil { - printWebSocketError("WriteError", err) - return - } - case <-pingTicker.C: - _ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if err := ping(c.conn); err != nil { - printWebSocketError("PingError", err) - return - } - } - } -} - -func printWebSocketError(prefix string, err error) { - var closeError *websocket.CloseError - ok := errors.As(err, &closeError) - - if ok && closeError != nil && (closeError.Code == 1000 || closeError.Code == 1001) { - // normal closure - return - } - - fmt.Println("WebSocket:", prefix, err) -} diff --git a/src/server/stream/once.go b/src/server/stream/once.go deleted file mode 100644 index 2df2523..0000000 --- a/src/server/stream/once.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package stream - -import ( - "sync" - "sync/atomic" -) - -// Modified version of sync.Once -// (https://github.com/golang/go/blob/master/src/sync/once.go) -// This version unlocks the mutex early and therefore doesn't -// hold the lock while executing func f(). -type once struct { - m sync.Mutex - done uint32 -} - -func (o *once) Do(f func()) { - if atomic.LoadUint32(&o.done) == 1 { - return - } - if o.mayExecute() { - f() - } -} - -func (o *once) mayExecute() bool { - o.m.Lock() - defer o.m.Unlock() - if o.done == 0 { - atomic.StoreUint32(&o.done, 1) - return true - } - return false -} diff --git a/src/server/stream/once_test.go b/src/server/stream/once_test.go deleted file mode 100644 index 720f65b..0000000 --- a/src/server/stream/once_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package stream - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func Test_Execute(t *testing.T) { - executeOnce := once{} - execution := make(chan struct{}) - fExecute := func() { - execution <- struct{}{} - } - go executeOnce.Do(fExecute) - go executeOnce.Do(fExecute) - - select { - case <-execution: - // expected - case <-time.After(100 * time.Millisecond): - t.Fatal("Execute should be executed once") - } - - select { - case <-execution: - t.Fatal("should only execute once") - case <-time.After(100 * time.Millisecond): - // expected - } - - assert.False(t, executeOnce.mayExecute()) - - go executeOnce.Do(fExecute) - - select { - case <-execution: - t.Fatal("should only execute once") - case <-time.After(100 * time.Millisecond): - // expected - } -} diff --git a/src/server/stream/stream.go b/src/server/stream/stream.go deleted file mode 100644 index 6a91fbc..0000000 --- a/src/server/stream/stream.go +++ /dev/null @@ -1,161 +0,0 @@ -package stream - -import ( - "net/http" - "net/url" - "regexp" - "strings" - "sync" - "time" - - "code.thetadev.de/TSGRain/SEBRAUC/src/util" - "code.thetadev.de/TSGRain/SEBRAUC/src/util/mode" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" -) - -// The API provides a handler for a WebSocket stream API. -type API struct { - clients map[uint]*client - lock sync.RWMutex - pingPeriod time.Duration - pongTimeout time.Duration - upgrader *websocket.Upgrader - counter *util.Counter - lastBroadcast []byte -} - -// New creates a new instance of API. -// pingPeriod: is the interval, in which is server sends the a ping to the client. -// pongTimeout: is the duration after the connection will be terminated, -// when the client does not respond with the pong command. -func New(pingPeriod, pongTimeout time.Duration, allowedWebSocketOrigins []string) *API { - return &API{ - clients: make(map[uint]*client), - pingPeriod: pingPeriod, - pongTimeout: pingPeriod + pongTimeout, - upgrader: newUpgrader(allowedWebSocketOrigins), - counter: &util.Counter{}, - } -} - -// NotifyDeletedUser closes existing connections for the given user. -func (a *API) NotifyDeletedClient(userID uint) error { - a.lock.Lock() - defer a.lock.Unlock() - if client, ok := a.clients[userID]; ok { - client.Close() - delete(a.clients, userID) - } - return nil -} - -// Notify notifies the clients with the given userID that a new messages was created. -func (a *API) Notify(userID uint, msg []byte) { - a.lock.RLock() - defer a.lock.RUnlock() - if client, ok := a.clients[userID]; ok { - client.write <- msg - } -} - -func (a *API) Broadcast(msg []byte) { - a.lock.RLock() - defer a.lock.RUnlock() - for _, client := range a.clients { - client.write <- msg - } - a.lastBroadcast = msg -} - -func (a *API) remove(remove *client) { - a.lock.Lock() - defer a.lock.Unlock() - delete(a.clients, remove.id) -} - -func (a *API) register(client *client) { - a.lock.Lock() - defer a.lock.Unlock() - a.clients[client.id] = client - - // Send new clients the last broadcast so they get the current state - if a.lastBroadcast != nil { - client.write <- a.lastBroadcast - } -} - -func (a *API) Handle(ctx *gin.Context) { - conn, err := a.upgrader.Upgrade(ctx.Writer, ctx.Request, nil) - if err != nil { - ctx.Error(err) - return - } - - client := newClient(conn, a.counter.Increment(), a.remove) - a.register(client) - go client.startReading(a.pongTimeout) - go client.startWriteHandler(a.pingPeriod) -} - -// Close closes all client connections and stops answering new connections. -func (a *API) Close() { - a.lock.Lock() - defer a.lock.Unlock() - - for _, client := range a.clients { - client.Close() - } - for k := range a.clients { - delete(a.clients, k) - } -} - -func isAllowedOrigin(r *http.Request, allowedOrigins []*regexp.Regexp) bool { - origin := r.Header.Get("origin") - if origin == "" { - return true - } - - u, err := url.Parse(origin) - if err != nil { - return false - } - - if strings.EqualFold(u.Host, r.Host) { - return true - } - - for _, allowedOrigin := range allowedOrigins { - if allowedOrigin.Match([]byte(strings.ToLower(u.Hostname()))) { - return true - } - } - - return false -} - -func newUpgrader(allowedWebSocketOrigins []string) *websocket.Upgrader { - compiledAllowedOrigins := compileAllowedWebSocketOrigins(allowedWebSocketOrigins) - - return &websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { - if mode.IsDev() { - return true - } - return isAllowedOrigin(r, compiledAllowedOrigins) - }, - } -} - -func compileAllowedWebSocketOrigins(allowedOrigins []string) []*regexp.Regexp { - var compiledAllowedOrigins []*regexp.Regexp - for _, origin := range allowedOrigins { - compiledAllowedOrigins = append(compiledAllowedOrigins, - regexp.MustCompile(origin)) - } - - return compiledAllowedOrigins -} diff --git a/src/server/stream/stream_test.go b/src/server/stream/stream_test.go deleted file mode 100644 index 771e6b2..0000000 --- a/src/server/stream/stream_test.go +++ /dev/null @@ -1,472 +0,0 @@ -package stream - -import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "code.thetadev.de/TSGRain/SEBRAUC/src/util/mode" - "github.com/fortytw2/leaktest" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" -) - -func TestFailureOnNormalHttpRequest(t *testing.T) { - mode.Set(mode.TestDev) - defer mode.Set(mode.Dev) - - defer leaktest.Check(t)() - - server, api := bootTestServer() - defer server.Close() - defer api.Close() - - resp, err := http.Get(server.URL) - assert.Nil(t, err) - assert.Equal(t, 400, resp.StatusCode) - resp.Body.Close() -} - -func TestWriteMessageFails(t *testing.T) { - mode.Set(mode.TestDev) - defer mode.Set(mode.Dev) - - oldWrite := writeBytes - // try emulate an write error, mostly this should kill the ReadMessage - // goroutine first but you'll never know. - writeBytes = func(conn *websocket.Conn, data []byte) error { - return errors.New("asd") - } - defer func() { - writeBytes = oldWrite - }() - defer leaktest.Check(t)() - - server, api := bootTestServer() - defer server.Close() - defer api.Close() - - wsURL := wsURL(server.URL) - user := testClient(t, wsURL) - - // the server may take some time to register the client - time.Sleep(100 * time.Millisecond) - client := getClient(api, 1) - assert.NotNil(t, client) - - api.Notify(1, []byte("HI")) - user.expectNoMessage() -} - -func TestWritePingFails(t *testing.T) { - mode.Set(mode.TestDev) - defer mode.Set(mode.Dev) - - oldPing := ping - // try emulate an write error, mostly this should kill the ReadMessage - // gorouting first but you'll never know. - ping = func(conn *websocket.Conn) error { - return errors.New("asd") - } - defer func() { - ping = oldPing - }() - - defer leaktest.CheckTimeout(t, 10*time.Second)() - - server, api := bootTestServer() - defer api.Close() - defer server.Close() - - wsURL := wsURL(server.URL) - user := testClient(t, wsURL) - defer user.conn.Close() - - // the server may take some time to register the client - time.Sleep(100 * time.Millisecond) - client := getClient(api, 1) - - assert.NotNil(t, client) - - time.Sleep(api.pingPeriod) // waiting for ping - - api.Notify(1, []byte("HI")) - user.expectNoMessage() -} - -func TestPing(t *testing.T) { - mode.Set(mode.TestDev) - defer mode.Set(mode.Dev) - - server, api := bootTestServer() - defer server.Close() - defer api.Close() - - wsURL := wsURL(server.URL) - - user := createClient(t, wsURL) - defer user.conn.Close() - - ping := make(chan bool) - oldPingHandler := user.conn.PingHandler() - user.conn.SetPingHandler(func(appData string) error { - err := oldPingHandler(appData) - ping <- true - return err - }) - - startReading(user) - - expectNoMessage(user) - - select { - case <-time.After(2 * time.Second): - assert.Fail(t, "Expected ping but there was one :(") - case <-ping: - // expected - } - - expectNoMessage(user) - api.Notify(1, []byte("HI")) - user.expectMessage([]byte("HI")) -} - -func TestCloseClientOnNotReading(t *testing.T) { - mode.Set(mode.TestDev) - defer mode.Set(mode.Dev) - - server, api := bootTestServer() - defer server.Close() - defer api.Close() - - wsURL := wsURL(server.URL) - - ws, resp, err := websocket.DefaultDialer.Dial(wsURL, nil) - assert.Nil(t, err) - resp.Body.Close() - defer ws.Close() - - // the server may take some time to register the client - time.Sleep(100 * time.Millisecond) - assert.NotNil(t, getClient(api, 1)) - - time.Sleep(api.pingPeriod + api.pongTimeout) - - assert.Nil(t, getClient(api, 1)) -} - -func TestMessageDirectlyAfterConnect(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - defer leaktest.Check(t)() - server, api := bootTestServer() - defer server.Close() - defer api.Close() - - wsURL := wsURL(server.URL) - - user := testClient(t, wsURL) - defer user.conn.Close() - // the server may take some time to register the client - time.Sleep(100 * time.Millisecond) - api.Notify(1, []byte("msg")) - user.expectMessage([]byte("msg")) -} - -func TestDeleteClientShouldCloseConnection(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - defer leaktest.Check(t)() - server, api := bootTestServer() - defer server.Close() - defer api.Close() - - wsURL := wsURL(server.URL) - - user := testClient(t, wsURL) - defer user.conn.Close() - // the server may take some time to register the client - time.Sleep(100 * time.Millisecond) - api.Notify(1, []byte("HI")) - user.expectMessage([]byte("HI")) - - assert.Nil(t, api.NotifyDeletedClient(1)) - - api.Notify(1, []byte("HI")) - user.expectNoMessage() -} - -func TestNotify(t *testing.T) { - mode.Set(mode.TestDev) - defer mode.Set(mode.Dev) - - defer leaktest.Check(t)() - server, api := bootTestServer() - defer server.Close() - - wsURL := wsURL(server.URL) - - client1 := testClient(t, wsURL) - defer client1.conn.Close() - - client2 := testClient(t, wsURL) - defer client2.conn.Close() - - client3 := testClient(t, wsURL) - defer client3.conn.Close() - - // the server may take some time to register the client - time.Sleep(100 * time.Millisecond) - - api.Notify(1, []byte("msg")) - expectMessage([]byte("msg"), client1) - expectNoMessage(client2) - expectNoMessage(client3) - - assert.Nil(t, api.NotifyDeletedClient(1)) - - api.Notify(1, []byte("msg")) - expectNoMessage(client1) - expectNoMessage(client2) - expectNoMessage(client3) - - api.Notify(2, []byte("msg")) - expectNoMessage(client1) - expectMessage([]byte("msg"), client2) - expectNoMessage(client3) - - api.Notify(3, []byte("msg")) - expectNoMessage(client1) - expectNoMessage(client2) - expectMessage([]byte("msg"), client3) - - api.Close() -} - -func TestBroadcast(t *testing.T) { - defer leaktest.Check(t)() - server, api := bootTestServer() - defer server.Close() - - wsURL := wsURL(server.URL) - - client1 := testClient(t, wsURL) - defer client1.conn.Close() - - client2 := testClient(t, wsURL) - defer client2.conn.Close() - - client3 := testClient(t, wsURL) - defer client3.conn.Close() - - // the server may take some time to register the client - time.Sleep(100 * time.Millisecond) - - testMsg1 := []byte("hello1") - api.Broadcast(testMsg1) - expectMessage(testMsg1, client1, client2, client3) - - assert.Nil(t, api.NotifyDeletedClient(1)) - - testMsg2 := []byte("hello2") - api.Broadcast(testMsg2) - expectNoMessage(client1) - expectMessage(testMsg2, client2, client3) -} - -func TestLastBroadcast(t *testing.T) { - defer leaktest.Check(t)() - server, api := bootTestServer() - defer server.Close() - - wsURL := wsURL(server.URL) - - testMsg1 := []byte("hello1") - api.Broadcast(testMsg1) - - client1 := testClient(t, wsURL) - defer client1.conn.Close() - - client2 := testClient(t, wsURL) - defer client2.conn.Close() - - // the server may take some time to register the client - time.Sleep(100 * time.Millisecond) - - expectMessage(testMsg1, client1, client2) -} - -func Test_sameOrigin_returnsTrue(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - req := httptest.NewRequest("GET", "http://example.com/stream", nil) - req.Header.Set("Origin", "http://example.com") - actual := isAllowedOrigin(req, nil) - assert.True(t, actual) -} - -func Test_sameOrigin_returnsTrue_withCustomPort(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - req := httptest.NewRequest("GET", "http://example.com:8080/stream", nil) - req.Header.Set("Origin", "http://example.com:8080") - actual := isAllowedOrigin(req, nil) - assert.True(t, actual) -} - -func Test_isAllowedOrigin_withoutAllowedOrigins_failsWhenNotSameOrigin(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - req := httptest.NewRequest("GET", "http://example.com/stream", nil) - req.Header.Set("Origin", "http://gorify.example.com") - actual := isAllowedOrigin(req, nil) - assert.False(t, actual) -} - -func Test_isAllowedOriginMatching(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - compiledAllowedOrigins := compileAllowedWebSocketOrigins( - []string{"go.{4}\\.example\\.com", "go\\.example\\.com"}, - ) - - req := httptest.NewRequest("GET", "http://example.me/stream", nil) - req.Header.Set("Origin", "http://gorify.example.com") - assert.True(t, isAllowedOrigin(req, compiledAllowedOrigins)) - - req.Header.Set("Origin", "http://go.example.com") - assert.True(t, isAllowedOrigin(req, compiledAllowedOrigins)) - - req.Header.Set("Origin", "http://hello.example.com") - assert.False(t, isAllowedOrigin(req, compiledAllowedOrigins)) -} - -func Test_emptyOrigin_returnsTrue(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - req := httptest.NewRequest("GET", "http://example.com/stream", nil) - actual := isAllowedOrigin(req, nil) - assert.True(t, actual) -} - -func Test_otherOrigin_returnsFalse(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - req := httptest.NewRequest("GET", "http://example.com/stream", nil) - req.Header.Set("Origin", "http://otherexample.de") - actual := isAllowedOrigin(req, nil) - assert.False(t, actual) -} - -func Test_invalidOrigin_returnsFalse(t *testing.T) { - mode.Set(mode.Prod) - defer mode.Set(mode.Dev) - - req := httptest.NewRequest("GET", "http://example.com/stream", nil) - req.Header.Set("Origin", "http\\://otherexample.de") - actual := isAllowedOrigin(req, nil) - assert.False(t, actual) -} - -func Test_compileAllowedWebSocketOrigins(t *testing.T) { - assert.Equal(t, 0, len(compileAllowedWebSocketOrigins([]string{}))) - assert.Equal(t, 3, len(compileAllowedWebSocketOrigins([]string{"^.*$", "", "abc"}))) -} - -func getClient(api *API, user uint) *client { - api.lock.RLock() - defer api.lock.RUnlock() - - return api.clients[user] -} - -func testClient(t *testing.T, url string) *testingClient { - client := createClient(t, url) - startReading(client) - return client -} - -func startReading(client *testingClient) { - go func() { - for { - _, payload, err := client.conn.ReadMessage() - if err != nil { - return - } - - client.readMessage <- payload - } - }() -} - -func createClient(t *testing.T, url string) *testingClient { - ws, resp, err := websocket.DefaultDialer.Dial(url, nil) - assert.Nil(t, err) - resp.Body.Close() - - readMessages := make(chan []byte) - - return &testingClient{conn: ws, readMessage: readMessages, t: t} -} - -type testingClient struct { - conn *websocket.Conn - readMessage chan []byte - t *testing.T -} - -func (c *testingClient) expectMessage(expected []byte) { - select { - case <-time.After(50 * time.Millisecond): - assert.Fail(c.t, "Expected message but none was send :(") - case actual := <-c.readMessage: - assert.Equal(c.t, expected, actual) - } -} - -func expectMessage(expected []byte, clients ...*testingClient) { - for _, client := range clients { - client.expectMessage(expected) - } -} - -func expectNoMessage(clients ...*testingClient) { - for _, client := range clients { - client.expectNoMessage() - } -} - -func (c *testingClient) expectNoMessage() { - select { - case <-time.After(50 * time.Millisecond): - // no message == as expected - case msg := <-c.readMessage: - assert.Fail(c.t, "Expected NO message but there was one :(", fmt.Sprint(msg)) - } -} - -func bootTestServer() (*httptest.Server, *API) { - r := gin.New() - // ping every 500 ms, and the client has 500 ms to respond - api := New(500*time.Millisecond, 500*time.Millisecond, []string{}) - - r.GET("/", api.Handle) - server := httptest.NewServer(r) - return server, api -} - -func wsURL(httpURL string) string { - return "ws" + strings.TrimPrefix(httpURL, "http") -} diff --git a/src/server/swagger/swagger.go b/src/server/swagger/swagger.go deleted file mode 100644 index 733b372..0000000 --- a/src/server/swagger/swagger.go +++ /dev/null @@ -1,25 +0,0 @@ -package swagger - -import ( - _ "embed" - - "code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware" - "github.com/gin-gonic/gin" -) - -//go:embed swagger.html -var swaggerHtml []byte - -//go:embed swagger.yaml -var swaggerYaml []byte - -func Register(r gin.IRouter) { - swg := r.Group("/api/swagger", middleware.Cache) - - swg.GET("/", func(c *gin.Context) { - c.Data(200, "text/html", swaggerHtml) - }) - swg.GET("/swagger.yaml", func(c *gin.Context) { - c.Data(200, "text/yaml", swaggerYaml) - }) -} diff --git a/src/server/swagger/swagger.html b/src/server/swagger/swagger.html deleted file mode 100644 index cc6855f..0000000 --- a/src/server/swagger/swagger.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - SEBRAUC API documentation - - - - - - - - - - - diff --git a/src/server/swagger/swagger.yaml b/src/server/swagger/swagger.yaml deleted file mode 100644 index e21f243..0000000 --- a/src/server/swagger/swagger.yaml +++ /dev/null @@ -1,228 +0,0 @@ -definitions: - Error: - description: The Error contains error relevant information. - properties: - error: - description: The general error message according to HTTP specification. - example: Unauthorized - type: string - msg: - description: Concrete error message. - example: already running - type: string - status_code: - description: The http error code. - example: 500 - format: int64 - type: integer - required: - - error - - status_code - - msg - title: Error model - type: object - RaucStatus: - description: RaucStatus contains information about the current RAUC updater status. - properties: - installing: - description: True if the installer is running - type: boolean - last_error: - description: Installation error message - example: "Failed to check bundle identifier: Invalid identifier." - type: string - log: - description: Full command line output of the current installation - example: 0% Installing 0% Determining slot states 20% Determining slot states - done - type: string - message: - description: Current installation step - example: Copying image to rootfs.0 - type: string - percent: - description: Installation progress - format: int64 - maximum: 100 - minimum: 0 - type: integer - required: - - installing - - percent - - message - - last_error - - log - title: RaucStatus model - type: object - Rootfs: - properties: - bootable: - description: Is the filesystem bootable? - type: boolean - booted: - description: Is the filesystem booted? - type: boolean - device: - description: Block device - example: /dev/mmcblk0p2 - type: string - mountpoint: - description: Mount path (null when not mounted) - example: / - type: string - primary: - description: Is the filesystem the next boot target? - type: boolean - type: - description: Filesystem - example: ext4 - type: string - required: - - device - - type - - mountpoint - - bootable - - booted - - primary - type: object - StatusMessage: - description: StatusMessage contains the status of an operation. - properties: - msg: - description: Status message text - example: Update started - type: string - success: - description: Is operation successful? - type: boolean - required: - - success - - msg - title: StatusMessage model - type: object - SystemInfo: - description: SystemInfo contains information about the running system. - properties: - hostname: - description: Hostname of the system - example: raspberrypi3 - type: string - os_name: - description: Name of the os distribution - example: Poky - type: string - os_version: - description: Operating system version - example: 1.0.2 - type: string - rauc_compatible: - description: Compatible firmware name - example: Poky - type: string - rauc_rootfs: - additionalProperties: - $ref: "#/definitions/Rootfs" - description: List of RAUC root filesystems - type: object - rauc_variant: - description: Compatible firmware variant - example: rpi-prod - type: string - uptime: - description: System uptime in seconds - example: 5832 - format: int64 - type: integer - required: - - hostname - - os_name - - os_version - - uptime - - rauc_compatible - - rauc_variant - - rauc_rootfs - title: SystemInfo model - type: object -info: - description: REST API for the SEBRAUC firmware updater - license: - name: MIT - title: SEBRAUC - version: 0.2.0 -paths: - /info: - get: - description: Get the current system info - operationId: getInfo - produces: - - application/json - responses: - "200": - description: Ok - schema: - $ref: "#/definitions/SystemInfo" - "500": - description: Server Error - schema: - $ref: "#/definitions/Error" - /reboot: - post: - description: Reboot the system - operationId: startReboot - produces: - - application/json - responses: - "200": - description: Ok - schema: - $ref: "#/definitions/StatusMessage" - "500": - description: Server Error - schema: - $ref: "#/definitions/Error" - /status: - get: - description: Get the current status of the RAUC updater - operationId: getStatus - produces: - - application/json - responses: - "200": - description: Ok - schema: - $ref: "#/definitions/RaucStatus" - "500": - description: Server Error - schema: - $ref: "#/definitions/Error" - /update: - post: - consumes: - - multipart/form-data - description: Start the update process - operationId: startUpdate - parameters: - - description: RAUC firmware image file (*.raucb) - in: formData - name: updateFile - required: true - type: file - produces: - - application/json - responses: - "200": - description: Ok - schema: - $ref: "#/definitions/StatusMessage" - "409": - description: already running - schema: - $ref: "#/definitions/Error" - "500": - description: Server Error - schema: - $ref: "#/definitions/Error" -schemes: - - http - - https -swagger: "2.0" diff --git a/src/server/swagger/swagger_test.go b/src/server/swagger/swagger_test.go deleted file mode 100644 index 9fd3c03..0000000 --- a/src/server/swagger/swagger_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package swagger - -import ( - "bytes" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -func TestSwagger(t *testing.T) { - router := gin.New() - Register(router) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/swagger/", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, swaggerHtml, w.Body.Bytes()) - assert.NotEmpty(t, w.Header().Get("Cache-Control")) - - w = httptest.NewRecorder() - req, _ = http.NewRequest("GET", "/api/swagger/swagger.yaml", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, swaggerYaml, w.Body.Bytes()) - assert.NotEmpty(t, w.Header().Get("Cache-Control")) -} - -func TestSwaggerData(t *testing.T) { - assert.True(t, bytes.Contains(swaggerHtml, - []byte("https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js")), - "HTML data missing", - ) - - assert.True(t, bytes.Contains(swaggerYaml, - []byte("REST API for the SEBRAUC firmware updater")), - "YAML data missing", - ) -} diff --git a/src/sysinfo/sysinfo.go b/src/sysinfo/sysinfo.go deleted file mode 100644 index f2aabc0..0000000 --- a/src/sysinfo/sysinfo.go +++ /dev/null @@ -1,188 +0,0 @@ -package sysinfo - -import ( - "encoding/json" - "os" - "regexp" - "strconv" - "strings" - - "code.thetadev.de/TSGRain/SEBRAUC/src/model" - "code.thetadev.de/TSGRain/SEBRAUC/src/util" -) - -type Sysinfo struct { - cmdRaucStatus string - releaseFile string - hostnameFile string - uptimeFile string - rexpName *regexp.Regexp - rexpVersion *regexp.Regexp -} - -type raucInfo struct { - Compatible string `json:"compatible"` - Variant string `json:"variant"` - Booted string `json:"booted"` - BootPrimary string `json:"boot_primary"` - Slots []map[string]raucFS `json:"slots"` -} - -type raucFS struct { - Class string `json:"class"` - Device string `json:"device"` - Type string `json:"type"` - Bootname string `json:"bootname"` - State string `json:"state"` - Mountpoint *string `json:"mountpoint"` - BootStatus string `json:"boot_status"` -} - -type osRelease struct { - OsName string `json:"os_name"` - OsVersion string `json:"os_version"` -} - -var rexpUptime = regexp.MustCompile(`^\d+`) - -func New(cmdRaucStatus string, releaseFile string, nameKey string, versionKey string, - hostnameFile string, uptimeFile string, -) *Sysinfo { - return &Sysinfo{ - cmdRaucStatus: cmdRaucStatus, - releaseFile: releaseFile, - hostnameFile: hostnameFile, - uptimeFile: uptimeFile, - rexpName: regexp.MustCompile( - `(?m)^` + regexp.QuoteMeta(nameKey) + `="(.+)"`), - rexpVersion: regexp.MustCompile( - `(?m)^` + regexp.QuoteMeta(versionKey) + `="(.+)"`), - } -} - -func Default(cmdRaucStatus string) *Sysinfo { - return New(cmdRaucStatus, "/etc/os-release", "NAME", "VERSION", - "/etc/hostname", "/proc/uptime") -} - -func parseRaucInfo(raucInfoJson []byte) (raucInfo, error) { - res := raucInfo{} - err := json.Unmarshal(raucInfoJson, &res) - return res, err -} - -func (s *Sysinfo) parseOsRelease() (osRelease, error) { - osReleaseTxt, err := os.ReadFile(s.releaseFile) - if err != nil { - return osRelease{}, err - } - - nameMatch := s.rexpName.FindSubmatch(osReleaseTxt) - versionMatch := s.rexpVersion.FindSubmatch(osReleaseTxt) - - name := "" - if nameMatch != nil { - name = string(nameMatch[1]) - } - - version := "" - if versionMatch != nil { - version = string(versionMatch[1]) - } - - return osRelease{ - OsName: name, - OsVersion: version, - }, nil -} - -func mapRootfs(rinf raucInfo) map[string]model.Rootfs { - res := make(map[string]model.Rootfs) - - for _, slot := range rinf.Slots { - for name, fs := range slot { - if fs.Class == "rootfs" { - res[name] = model.Rootfs{ - Device: fs.Device, - Type: fs.Type, - Bootname: fs.Bootname, - Mountpoint: fs.Mountpoint, - Bootable: fs.BootStatus == "good", - Booted: fs.State == "booted", - Primary: rinf.BootPrimary == name, - } - } - } - } - return res -} - -func getFSNameFromBootname(rfslist map[string]model.Rootfs, bootname string) string { - for name, rfs := range rfslist { - if rfs.Bootname == bootname { - return name - } - } - return "n/a" -} - -func mapSysinfo(rinf raucInfo, osr osRelease, uptime int, - hostname string, -) model.SystemInfo { - rfslist := mapRootfs(rinf) - - return model.SystemInfo{ - Hostname: hostname, - OsName: osr.OsName, - OsVersion: osr.OsVersion, - Uptime: uptime, - RaucCompatible: rinf.Compatible, - RaucVariant: rinf.Variant, - RaucRootfs: rfslist, - } -} - -func (s *Sysinfo) getUptime() (int, error) { - uptimeRaw, err := os.ReadFile(s.uptimeFile) - if err != nil { - return 0, err - } - - uptimeChars := rexpUptime.Find(uptimeRaw) - return strconv.Atoi(string(uptimeChars)) -} - -func (s *Sysinfo) getHostname() string { - hostname, err := os.ReadFile(s.hostnameFile) - if err != nil { - return "" - } - return strings.TrimSpace(string(hostname)) -} - -func (s *Sysinfo) GetSysinfo() (model.SystemInfo, error) { - cmd := util.CommandFromString(s.cmdRaucStatus) - rinfJson, err := cmd.Output() - if err != nil { - return model.SystemInfo{}, err - } - - rinf, err := parseRaucInfo(rinfJson) - if err != nil { - return model.SystemInfo{}, err - } - - osinf, err := s.parseOsRelease() - if err != nil { - return model.SystemInfo{}, err - } - - uptime, err := s.getUptime() - if err != nil { - return model.SystemInfo{}, err - } - - hostname := s.getHostname() - - return mapSysinfo(rinf, osinf, uptime, hostname), nil -} diff --git a/src/sysinfo/sysinfo_test.go b/src/sysinfo/sysinfo_test.go deleted file mode 100644 index 575502e..0000000 --- a/src/sysinfo/sysinfo_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package sysinfo - -import ( - "path/filepath" - "testing" - - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures" - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd" - "code.thetadev.de/TSGRain/SEBRAUC/src/model" - "github.com/stretchr/testify/assert" -) - -const statusJson = `{"compatible":"TSGRain","variant":"dev","booted":"A",` + - `"boot_primary":"rootfs.0","slots":[{"rootfs.1":{"class":"rootfs",` + - `"device":"/dev/mmcblk0p3","type":"ext4","bootname":"B","state":"inactive",` + - `"parent":null,"mountpoint":null,"boot_status":"good"}},{"rootfs.0":` + - `{"class":"rootfs","device":"/dev/mmcblk0p2","type":"ext4","bootname":"A",` + - `"state":"booted","parent":null,"mountpoint":"/","boot_status":"good"}}]}` - -var mountRoot = "/" - -var expectedRaucInfo = raucInfo{ - Compatible: "TSGRain", - Variant: "dev", - Booted: "A", - BootPrimary: "rootfs.0", - Slots: []map[string]raucFS{ - { - "rootfs.1": { - Class: "rootfs", - Device: "/dev/mmcblk0p3", - Type: "ext4", - Bootname: "B", - State: "inactive", - Mountpoint: nil, - BootStatus: "good", - }, - }, - { - "rootfs.0": { - Class: "rootfs", - Device: "/dev/mmcblk0p2", - Type: "ext4", - Bootname: "A", - State: "booted", - Mountpoint: &mountRoot, - BootStatus: "good", - }, - }, - }, -} - -var expectedRootfsList = map[string]model.Rootfs{ - "rootfs.0": { - Device: "/dev/mmcblk0p2", - Type: "ext4", - Bootname: "A", - Mountpoint: &mountRoot, - Bootable: true, - Booted: true, - Primary: true, - }, - "rootfs.1": { - Device: "/dev/mmcblk0p3", - Type: "ext4", - Bootname: "B", - Mountpoint: nil, - Bootable: true, - Booted: false, - Primary: false, - }, -} - -func TestParseRaucInfo(t *testing.T) { - info, err := parseRaucInfo([]byte(statusJson)) - if err != nil { - panic(err) - } - - assert.Equal(t, expectedRaucInfo, info) -} - -func TestParseOsRelease(t *testing.T) { - testfiles := fixtures.GetTestfilesDir() - osReleaseFile := filepath.Join(testfiles, "os-release") - - si := New(testcmd.RaucStatus, osReleaseFile, "NAME", "VERSION", - "/etc/hostname", "/proc/uptime") - - osRel, err := si.parseOsRelease() - if err != nil { - panic(err) - } - - expected := osRelease{ - OsName: "TSGRain distro", - OsVersion: "0.0.1", - } - - assert.Equal(t, expected, osRel) -} - -func TestMapRootfsList(t *testing.T) { - rootfsList := mapRootfs(expectedRaucInfo) - - assert.Equal(t, expectedRootfsList, rootfsList) -} - -func TestGetFSNameFromBootname(t *testing.T) { - rootfsList := mapRootfs(expectedRaucInfo) - - assert.Equal(t, "rootfs.0", getFSNameFromBootname(rootfsList, "A")) - assert.Equal(t, "rootfs.1", getFSNameFromBootname(rootfsList, "B")) - assert.Equal(t, "n/a", getFSNameFromBootname(rootfsList, "C")) -} - -func TestGetSysinfo(t *testing.T) { - si := Default(testcmd.RaucStatus) - - sysinfo, err := si.GetSysinfo() - if err != nil { - panic(err) - } - - assert.Greater(t, sysinfo.Uptime, 0) - assert.Equal(t, "TSGRain", sysinfo.RaucCompatible) - assert.Equal(t, "dev", sysinfo.RaucVariant) - assert.Equal(t, expectedRootfsList, sysinfo.RaucRootfs) -} diff --git a/src/util/commands.go b/src/util/commands.go new file mode 100644 index 0000000..5bb0604 --- /dev/null +++ b/src/util/commands.go @@ -0,0 +1,10 @@ +//go:build prod + +package util + +const ( + RebootCmd = "shutdown -r 0" + UpdateCmd = "rauc install" + + TestMode = false +) diff --git a/src/util/commands_mock.go b/src/util/commands_mock.go new file mode 100644 index 0000000..b2a18f3 --- /dev/null +++ b/src/util/commands_mock.go @@ -0,0 +1,10 @@ +//go:build !prod + +package util + +const ( + RebootCmd = "touch /tmp/sebrauc_reboot_test" + UpdateCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock" + + TestMode = true +) diff --git a/src/util/counter.go b/src/util/counter.go deleted file mode 100644 index 9e264ee..0000000 --- a/src/util/counter.go +++ /dev/null @@ -1,30 +0,0 @@ -package util - -import "sync" - -type Counter struct { - count uint - mutex sync.RWMutex -} - -func (c *Counter) Get() uint { - c.mutex.RLock() - defer c.mutex.RUnlock() - - return c.count -} - -func (c *Counter) Reset() { - c.mutex.Lock() - defer c.mutex.Unlock() - - c.count = 0 -} - -func (c *Counter) Increment() uint { - c.mutex.Lock() - defer c.mutex.Unlock() - - c.count++ - return c.count -} diff --git a/src/util/counter_test.go b/src/util/counter_test.go deleted file mode 100644 index bfc3e9d..0000000 --- a/src/util/counter_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package util - -import ( - "sync" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCounter(t *testing.T) { - counter := Counter{} - - var wg sync.WaitGroup - - incrementer := func() { - for i := 0; i < 50; i++ { - counter.Increment() - } - wg.Done() - } - - for i := 0; i < 100; i++ { - wg.Add(1) - go incrementer() - } - - wg.Wait() - - assert.EqualValues(t, 5000, counter.Get()) -} diff --git a/src/util/errors.go b/src/util/errors.go index a229d66..20ee9e4 100644 --- a/src/util/errors.go +++ b/src/util/errors.go @@ -1,13 +1,8 @@ package util -import ( - "errors" - "net/http" -) +import "errors" var ( - ErrAlreadyRunning = HttpErrNew("rauc already running", http.StatusConflict) + ErrAlreadyRunning = errors.New("rauc already running") ErrFileDoesNotExist = errors.New("file does not exist") - ErrPageNotFound = HttpErrNew("page not found", http.StatusNotFound) - ErrUnauthorized = HttpErrNew("unauthorized", http.StatusUnauthorized) ) diff --git a/src/util/http_error.go b/src/util/http_error.go deleted file mode 100644 index c5d7cf8..0000000 --- a/src/util/http_error.go +++ /dev/null @@ -1,39 +0,0 @@ -package util - -import "errors" - -type HttpError interface { - error - StatusCode() int -} - -type httpErr struct { - err error - statusCode int -} - -func HttpErrWrap(e error, statusCode int) HttpError { - return &httpErr{ - err: e, - statusCode: statusCode, - } -} - -func HttpErrNew(msg string, statusCode int) HttpError { - return HttpErrWrap(errors.New(msg), statusCode) -} - -func (e *httpErr) Error() string { - if e.err == nil { - return "" - } - return e.err.Error() -} - -func (e *httpErr) Unwrap() error { - return e.err -} - -func (e *httpErr) StatusCode() int { - return e.statusCode -} diff --git a/src/util/mode/mode.go b/src/util/mode/mode.go deleted file mode 100644 index 6b0a3c8..0000000 --- a/src/util/mode/mode.go +++ /dev/null @@ -1,46 +0,0 @@ -package mode - -import "github.com/gin-gonic/gin" - -const ( - // Dev for development mode. - Dev = "dev" - // Prod for production mode. - Prod = "prod" - // TestDev used for tests. - TestDev = "testdev" -) - -var currentMode = Dev - -func init() { - Set(appmode) -} - -func Set(newMode string) { - currentMode = newMode - updateGinMode() -} - -// Get returns the current mode. -func Get() string { - return currentMode -} - -// IsDev returns true if the current mode is dev mode. -func IsDev() bool { - return Get() == Dev || Get() == TestDev -} - -func updateGinMode() { - switch Get() { - case Dev: - gin.SetMode(gin.DebugMode) - case TestDev: - gin.SetMode(gin.TestMode) - case Prod: - gin.SetMode(gin.ReleaseMode) - default: - panic("unknown mode") - } -} diff --git a/src/util/mode/mode_dev.go b/src/util/mode/mode_dev.go deleted file mode 100644 index 71cb6cd..0000000 --- a/src/util/mode/mode_dev.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build !prod -// +build !prod - -package mode - -const appmode = Dev diff --git a/src/util/mode/mode_prod.go b/src/util/mode/mode_prod.go deleted file mode 100644 index 571398d..0000000 --- a/src/util/mode/mode_prod.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build prod -// +build prod - -package mode - -const appmode = Prod diff --git a/src/util/mode/mode_test.go b/src/util/mode/mode_test.go deleted file mode 100644 index 8b06166..0000000 --- a/src/util/mode/mode_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package mode - -import ( - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -func TestDevMode(t *testing.T) { - Set(Dev) - assert.Equal(t, Get(), Dev) - assert.True(t, IsDev()) - assert.Equal(t, gin.Mode(), gin.DebugMode) -} - -func TestTestDevMode(t *testing.T) { - Set(TestDev) - assert.Equal(t, Get(), TestDev) - assert.True(t, IsDev()) - assert.Equal(t, gin.Mode(), gin.TestMode) -} - -func TestProdMode(t *testing.T) { - Set(Prod) - assert.Equal(t, Get(), Prod) - assert.False(t, IsDev()) - assert.Equal(t, gin.Mode(), gin.ReleaseMode) -} - -func TestInvalidMode(t *testing.T) { - assert.Panics(t, func() { - Set("asdasda") - }) -} diff --git a/src/util/types.go b/src/util/types.go deleted file mode 100644 index afd106b..0000000 --- a/src/util/types.go +++ /dev/null @@ -1,5 +0,0 @@ -package util - -type Broadcaster interface { - Broadcast(msg []byte) -} diff --git a/src/util/util.go b/src/util/util.go index e826e45..a64d409 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -1,7 +1,6 @@ package util import ( - "fmt" "os" "os/exec" "path/filepath" @@ -26,24 +25,10 @@ func CreateDirIfNotExists(dirpath string) error { return nil } -func GetTmpdir(tmpdirPath string) string { - tmpdir := tmpdirPath - // Default temporary directory - if tmpdirPath == "" { - tmpdir = filepath.Join(os.TempDir(), tmpdirName) - } - +func GetTmpdir() (string, error) { + tmpdir := filepath.Join(os.TempDir(), tmpdirName) err := CreateDirIfNotExists(tmpdir) - if err != nil { - panic(fmt.Sprintf("could not create tmpdir %s: %s", tmpdir, err)) - } - - return tmpdir -} - -func RemoveTmpdir(tmpdirPath string) { - tmpdir := GetTmpdir(tmpdirPath) - _ = os.RemoveAll(tmpdir) + return tmpdir, err } func CommandFromString(cmdString string) *exec.Cmd { @@ -56,41 +41,8 @@ func CommandFromString(cmdString string) *exec.Cmd { return exec.Command(parts[0], parts[1:]...) } -func Reboot(rebootCmd string, t time.Duration) { +func Reboot(t time.Duration) { time.Sleep(t) - cmd := CommandFromString(rebootCmd) + cmd := CommandFromString(RebootCmd) _ = cmd.Run() } - -func FindFile(explicitPath string, locations, endings []string) (string, error) { - if explicitPath != "" { - if !DoesFileExist(explicitPath) { - return "", fmt.Errorf("file %s not found", explicitPath) - } - return explicitPath, nil - } - - notFound := []string{} - - for _, f := range locations { - if endings != nil { - for _, t := range endings { - fpath := f + "." + t - - if DoesFileExist(fpath) { - return fpath, nil - } else { - notFound = append(notFound, fpath) - } - } - } else { - if DoesFileExist(f) { - return f, nil - } else { - notFound = append(notFound, f) - } - } - } - return "", fmt.Errorf("none of the following files found: %s", - strings.Join(notFound, "; ")) -} diff --git a/src/util/util_test.go b/src/util/util_test.go index 0ef6a32..eb28203 100644 --- a/src/util/util_test.go +++ b/src/util/util_test.go @@ -1,13 +1,11 @@ package util import ( - "errors" "os" "path/filepath" "testing" "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures" - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/testcmd" "github.com/stretchr/testify/assert" ) @@ -43,46 +41,27 @@ func TestDoesFileExist(t *testing.T) { } func TestTmpdir(t *testing.T) { - tests := []struct { - name string - path string - }{ - { - name: "default", - path: "", - }, - { - name: "custom", - path: filepath.Join(os.TempDir(), "customTmpdir"), - }, + td, err := GetTmpdir() + if err != nil { + panic(err) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - td := GetTmpdir(tt.path) - assert.DirExists(t, td) - - tfile := filepath.Join(td, "test.txt") - f, err := os.Create(tfile) - if err != nil { - panic(err) - } - - _, err = f.WriteString("Hello") - if err != nil { - panic(err) - } - err = f.Close() - if err != nil { - panic(err) - } - - assert.FileExists(t, tfile) - - RemoveTmpdir(tt.path) - assert.NoDirExists(t, td) - }) + tfile := filepath.Join(td, "test.txt") + f, err := os.Create(tfile) + if err != nil { + panic(err) } + + _, err = f.WriteString("Hello") + if err != nil { + panic(err) + } + err = f.Close() + if err != nil { + panic(err) + } + + assert.FileExists(t, tfile) } func TestCommandFromString(t *testing.T) { @@ -124,78 +103,7 @@ func TestReboot(t *testing.T) { testfile := "/tmp/sebrauc_reboot_test" _ = os.Remove(testfile) - Reboot(testcmd.Reboot, 0) + Reboot(0) assert.FileExists(t, testfile) } - -func TestFindFile(t *testing.T) { - testfiles := fixtures.GetTestfilesDir() - - //nolint:lll - tests := []struct { - name string - explicitPath string - locations []string - endings []string - expect string - expectErr error - }{ - { - name: "locations", - explicitPath: "", - locations: []string{filepath.Join(testfiles, "sebrauc")}, - endings: []string{"yml", "toml"}, - expect: filepath.Join(testfiles, "sebrauc.toml"), - expectErr: nil, - }, - { - name: "locations_nf", - explicitPath: "", - locations: []string{filepath.Join(testfiles, "banana")}, - endings: []string{"yml", "toml"}, - expect: "", - expectErr: errors.New("none of the following files found: src/fixtures/testfiles/banana.yml; src/fixtures/testfiles/banana.toml"), - }, - { - name: "no_endings", - explicitPath: "", - locations: []string{filepath.Join(testfiles, "banana"), filepath.Join(testfiles, "os-release")}, - endings: nil, - expect: filepath.Join(testfiles, "os-release"), - expectErr: nil, - }, - { - name: "no_endings_nf", - explicitPath: "", - locations: []string{filepath.Join(testfiles, "banana"), filepath.Join(testfiles, "apple")}, - endings: nil, - expect: "", - expectErr: errors.New("none of the following files found: src/fixtures/testfiles/banana; src/fixtures/testfiles/apple"), - }, - { - name: "explicit", - explicitPath: filepath.Join(testfiles, "sebrauc.toml"), - locations: []string{filepath.Join(testfiles, "banana")}, - endings: []string{"yml", "toml"}, - expect: filepath.Join(testfiles, "sebrauc.toml"), - expectErr: nil, - }, - { - name: "explicit_nf", - explicitPath: filepath.Join(testfiles, "banana.toml"), - locations: []string{filepath.Join(testfiles, "banana")}, - endings: []string{"yml", "toml"}, - expect: "", - expectErr: errors.New("file src/fixtures/testfiles/banana.toml not found"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fpath, err := FindFile(tt.explicitPath, tt.locations, tt.endings) - assert.Equal(t, tt.expectErr, err) - assert.Equal(t, tt.expect, fpath) - }) - } -} diff --git a/ui/.env.development b/ui/.env.development index 029df76..3eba399 100644 --- a/ui/.env.development +++ b/ui/.env.development @@ -1 +1,2 @@ +VITE_VERSION=dev VITE_API_HOST=127.0.0.1:8080 diff --git a/ui/index.html b/ui/index.html index 426ad0f..cf876c6 100644 --- a/ui/index.html +++ b/ui/index.html @@ -7,11 +7,7 @@ SEBRAUC -
- diff --git a/ui/package.json b/ui/package.json index 1d1def8..5efdbac 100644 --- a/ui/package.json +++ b/ui/package.json @@ -4,9 +4,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "serve": "vite preview", - "lint": "tsc", - "format": "prettier --write ../" + "serve": "vite preview" }, "dependencies": { "@mdi/js": "^6.5.95", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index fe7d84e..1110f3a 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: 5.4 +lockfileVersion: 5.3 specifiers: "@mdi/js": ^6.5.95 @@ -282,8 +282,6 @@ packages: } engines: {node: ">=6.0.0"} hasBin: true - dependencies: - "@babel/types": 7.16.0 dev: true /@babel/plugin-syntax-jsx/7.16.0: diff --git a/ui/src/components/Updater/SysinfoCard.tsx b/ui/src/components/Updater/SysinfoCard.tsx deleted file mode 100644 index 625ed7c..0000000 --- a/ui/src/components/Updater/SysinfoCard.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import {Component} from "preact" -import {SystemInfo} from "../../sebrauc-client" -import {sebraucApi} from "../../util/apiUrls" -import {secondsToString} from "../../util/functions" -import Icon from "../Icon/Icon" -import { - mdiAlphaVCircleOutline, - mdiCheckCircleOutline, - mdiCircleOutline, - mdiClockOutline, - mdiCloseCircleOutline, - mdiMonitor, - mdiPenguin, - mdiTagMultipleOutline, - mdiTagOutline, -} from "@mdi/js" -import colors from "../../util/colors" - -type Props = {} - -type State = { - sysinfo: SystemInfo -} - -export default class SysinfoCard extends Component { - private fetchTimeout: number | undefined - - constructor(props?: Props | undefined, context?: any) { - super(props, context) - this.fetchInfo() - } - - private fetchInfo = () => { - sebraucApi - .getInfo() - .then((response) => { - if (response.status == 200) { - this.setState({sysinfo: response.data}) - } else { - console.log("error fetching info", response.data) - this.fetchTimeout = window.setTimeout(this.fetchInfo, 3000) - } - }) - .catch((reason) => { - console.log("error fetching info", reason) - this.fetchTimeout = window.setTimeout(this.fetchInfo, 3000) - }) - } - - private renderSysinfo() { - return ( -
-
-

System information

- - - - - - - - - - - - - - - - - - - - - - - - - -
- Hostname - {this.state.sysinfo.hostname}
- Operating system - {this.state.sysinfo.os_name}
- OS version - {this.state.sysinfo.os_version}
- Uptime - {secondsToString(this.state.sysinfo.uptime)}
- Compatible FW - {this.state.sysinfo.rauc_compatible}
- Compatible FW - variant - {this.state.sysinfo.rauc_variant}
-
- -
-

Rootfs slots

-
- - - - - - - - - - - {Object.keys(this.state.sysinfo.rauc_rootfs).map( - (k, i) => { - const rfs = this.state.sysinfo.rauc_rootfs[k] - let icon = mdiCircleOutline - let iconColor = colors.BLUE - - if (!rfs.bootable) { - icon = mdiCloseCircleOutline - iconColor = colors.RED - } else if (rfs.primary) { - icon = mdiCheckCircleOutline - iconColor = colors.GREEN - } - - return ( - - - - - - - ) - } - )} - -
NameDeviceMountpoint
- - {k}{rfs.device}{rfs.mountpoint}
-
-
-
- ) - } - - private renderLoadingAnimation() { - return ( -
-

loading sysinfo...

-
- ) - } - - componentWillUnmount() { - if (this.fetchTimeout !== undefined) { - window.clearTimeout(this.fetchTimeout) - } - } - - render() { - if (this.state.sysinfo) { - return this.renderSysinfo() - } - return this.renderLoadingAnimation() - } -} diff --git a/ui/src/components/Updater/UpdaterView.tsx b/ui/src/components/Updater/UpdaterView.tsx deleted file mode 100644 index faf96c4..0000000 --- a/ui/src/components/Updater/UpdaterView.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import {mdiInformation, mdiUpload} from "@mdi/js" -import {Component} from "preact" -import Icon from "../Icon/Icon" -import SysinfoCard from "./SysinfoCard" -import UpdaterCard from "./UpdaterCard" -import "./Updater.scss" - -type Props = {} - -type State = { - flipped: boolean -} - -export default class UpdaterView extends Component { - constructor(props?: Props | undefined, context?: any) { - super(props, context) - - this.state = { - flipped: false, - } - } - - private flipCard = () => { - this.setState({flipped: !this.state.flipped}) - } - - render() { - return ( -
- - -
- {!this.state.flipped ? : } -
-
- ) - } -} diff --git a/ui/src/components/Updater/Alert.tsx b/ui/src/components/Upload/Alert.tsx similarity index 84% rename from ui/src/components/Updater/Alert.tsx rename to ui/src/components/Upload/Alert.tsx index 204a691..6eb16a9 100644 --- a/ui/src/components/Updater/Alert.tsx +++ b/ui/src/components/Upload/Alert.tsx @@ -1,7 +1,6 @@ import {Component} from "preact" import {mdiTriangleOutline} from "@mdi/js" import Icon from "../Icon/Icon" -import colors from "../../util/colors" type Props = { source?: string @@ -21,7 +20,7 @@ export default class Alert extends Component { return (
- + {msg}
diff --git a/ui/src/components/Updater/Reboot.tsx b/ui/src/components/Upload/Reboot.tsx similarity index 81% rename from ui/src/components/Updater/Reboot.tsx rename to ui/src/components/Upload/Reboot.tsx index 49a0232..c53666e 100644 --- a/ui/src/components/Updater/Reboot.tsx +++ b/ui/src/components/Upload/Reboot.tsx @@ -1,6 +1,7 @@ import {mdiCheckCircleOutline, mdiRestore} from "@mdi/js" +import axios, {AxiosError, AxiosResponse} from "axios" import {Component} from "preact" -import {sebraucApi} from "../../util/apiUrls" +import {apiUrl} from "../../util/apiUrls" import Icon from "../Icon/Icon" export default class Reboot extends Component { @@ -8,9 +9,9 @@ export default class Reboot extends Component { const res = confirm("Reboot the system?") if (!res) return - sebraucApi - .startReboot() - .then((response) => { + axios + .post(apiUrl + "/reboot") + .then((response: AxiosResponse) => { const msg = response.data.msg if (msg !== undefined) { @@ -19,7 +20,7 @@ export default class Reboot extends Component { alert("No response") } }) - .catch((error) => { + .catch((error: AxiosError) => { if (error.response) { const msg = error.response.data.msg diff --git a/ui/src/components/Updater/Updater.scss b/ui/src/components/Upload/Updater.scss similarity index 69% rename from ui/src/components/Updater/Updater.scss rename to ui/src/components/Upload/Updater.scss index fe3ace9..90b798e 100644 --- a/ui/src/components/Updater/Updater.scss +++ b/ui/src/components/Upload/Updater.scss @@ -1,11 +1,11 @@ -.updater-view { +.uploader { display: flex; flex-direction: column; justify-content: center; align-items: center; margin: 0 auto; - max-width: 600px; + max-width: 500px; width: 90%; > * { @@ -18,8 +18,8 @@ justify-content: center; align-items: center; - margin-top: 25px; - margin-bottom: 8px; + padding: 15px 8px; + margin: 8px 0; text-align: center; @@ -29,14 +29,6 @@ .top { font-size: 1.5em; } - - &.pad { - padding: 15px 0; - } - - &:first-of-type { - margin-top: 8px; - } } .alert { @@ -50,9 +42,3 @@ } } } - -.button-top-right { - position: absolute; - top: 20px; - right: 20px; -} diff --git a/ui/src/components/Updater/UpdaterCard.tsx b/ui/src/components/Upload/Updater.tsx similarity index 76% rename from ui/src/components/Updater/UpdaterCard.tsx rename to ui/src/components/Upload/Updater.tsx index 9a38b95..49057b1 100644 --- a/ui/src/components/Updater/UpdaterCard.tsx +++ b/ui/src/components/Upload/Updater.tsx @@ -5,11 +5,10 @@ import Dropzone from "../Dropzone/Dropzone" import ProgressCircle from "../ProgressCircle/ProgressCircle" import Icon from "../Icon/Icon" import "./Updater.scss" +import axios from "axios" import Alert from "./Alert" import Reboot from "./Reboot" -import {sebraucApi} from "../../util/apiUrls" -import colors from "../../util/colors" -import WebsocketClient from "../../util/websocket" +import {apiUrl, wsUrl} from "../../util/apiUrls" class UploadStatus { uploading = false @@ -51,21 +50,21 @@ type State = { wsConnected: boolean } -export default class UpdaterCard extends Component { +export default class Updater extends Component { private dropzoneRef = createRef() - private ws: WebsocketClient + private conn: WebSocket | undefined constructor(props?: Props | undefined, context?: any) { super(props, context) - this.ws = new WebsocketClient(this.onWsStatusUpdate, this.onWsMessage) - this.state = { uploadStatus: new UploadStatus(false), uploadFilename: "", raucStatus: new RaucStatus(), - wsConnected: this.ws.api().isConnected(), + wsConnected: false, } + + this.connectWebsocket() } private buttonClick = () => { @@ -78,13 +77,19 @@ export default class UpdaterCard extends Component { if (files.length === 0) return const newFile = files[0] + const formData = new FormData() + formData.append("updateFile", newFile) + this.setState({ uploadStatus: new UploadStatus(true, newFile.size, 0), uploadFilename: newFile.name, }) - sebraucApi - .startUpdate(newFile, { + axios + .post(apiUrl + "/update", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, onUploadProgress: (progressEvent: {loaded: number; total: number}) => { this.setState({ uploadStatus: UploadStatus.fromProgressEvent(progressEvent), @@ -106,16 +111,33 @@ export default class UpdaterCard extends Component { this.dropzoneRef.current?.reset() } - private onWsStatusUpdate = (wsConnected: boolean) => { - this.setState({wsConnected: wsConnected}) - } + private connectWebsocket = () => { + if (window.WebSocket) { + this.conn = new WebSocket(wsUrl) + this.conn.onopen = () => { + this.setState({wsConnected: true}) + console.log("WS connected") + } + this.conn.onclose = () => { + this.setState({wsConnected: false}) + console.log("WS connection closed") + window.setTimeout(this.connectWebsocket, 3000) + } + this.conn.onmessage = (evt) => { + var messages = evt.data.split("\n") + for (var i = 0; i < messages.length; i++) { + this.setState({ + raucStatus: Object.assign( + new RaucStatus(), + JSON.parse(messages[i]) + ), + }) - private onWsMessage = (evt: MessageEvent) => { - var messages = evt.data.split("\n") - for (var i = 0; i < messages.length; i++) { - this.setState({ - raucStatus: Object.assign(new RaucStatus(), JSON.parse(messages[i])), - }) + console.log(this.state.raucStatus) + } + } + } else { + console.log("Your browser does not support WebSockets") } } @@ -141,9 +163,9 @@ export default class UpdaterCard extends Component { } private circleColor(): string { - if (this.state.raucStatus.installing) return colors.RED - if (this.state.uploadStatus.uploading) return colors.GREEN - return colors.BLUE + if (this.state.raucStatus.installing) return "#FF0039" + if (this.state.uploadStatus.uploading) return "#148420" + return "#1f85de" } private circlePercentage(): number { @@ -153,10 +175,6 @@ export default class UpdaterCard extends Component { return 0 } - componentWillUnmount() { - this.ws.destroy() - } - render() { const acceptUploads = this.acceptUploads() const circleColor = this.circleColor() @@ -177,13 +195,12 @@ export default class UpdaterCard extends Component { topText = "Updating firmware" bottomText = this.state.raucStatus.message } else { - topText = "Firmware update" - bottomText = "Upload *.raucb FW package" + topText = "Upload firmware package" } return ( -
-
+
+

{topText}

@@ -198,7 +215,7 @@ export default class UpdaterCard extends Component { progress={circlePercentage} color={circleColor} > - diff --git a/ui/src/components/app.tsx b/ui/src/components/app.tsx index c2bd70c..25d26db 100644 --- a/ui/src/components/app.tsx +++ b/ui/src/components/app.tsx @@ -1,15 +1,15 @@ import {Component} from "preact" -import UpdaterView from "./Updater/UpdaterView" +import Updater from "./Upload/Updater" import logo from "../assets/logo.svg" -import {getConfig} from "../util/config" +import {version} from "../util/version" export default class App extends Component { render() { return (
- SEBRAUC - {getConfig().version} - + + {version} +
) } diff --git a/ui/src/sebrauc-client/.openapi-generator-ignore b/ui/src/sebrauc-client/.openapi-generator-ignore deleted file mode 100644 index 7e15242..0000000 --- a/ui/src/sebrauc-client/.openapi-generator-ignore +++ /dev/null @@ -1,27 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md - -/.gitignore -/.npmignore -/git_push.sh diff --git a/ui/src/sebrauc-client/.openapi-generator/FILES b/ui/src/sebrauc-client/.openapi-generator/FILES deleted file mode 100644 index 53250c0..0000000 --- a/ui/src/sebrauc-client/.openapi-generator/FILES +++ /dev/null @@ -1,5 +0,0 @@ -api.ts -base.ts -common.ts -configuration.ts -index.ts diff --git a/ui/src/sebrauc-client/.openapi-generator/VERSION b/ui/src/sebrauc-client/.openapi-generator/VERSION deleted file mode 100644 index e230c83..0000000 --- a/ui/src/sebrauc-client/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -5.3.0 \ No newline at end of file diff --git a/ui/src/sebrauc-client/api.ts b/ui/src/sebrauc-client/api.ts deleted file mode 100644 index b2106ea..0000000 --- a/ui/src/sebrauc-client/api.ts +++ /dev/null @@ -1,566 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * SEBRAUC - * REST API for the SEBRAUC firmware updater - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import {Configuration} from "./configuration" -import globalAxios, {AxiosPromise, AxiosInstance, AxiosRequestConfig} from "axios" -// Some imports not used depending on template conditions -// @ts-ignore -import { - DUMMY_BASE_URL, - assertParamExists, - setApiKeyToObject, - setBasicAuthToObject, - setBearerAuthToObject, - setOAuthToObject, - setSearchParams, - serializeDataIfNeeded, - toPathString, - createRequestFunction, -} from "./common" -// @ts-ignore -import { - BASE_PATH, - COLLECTION_FORMATS, - RequestArgs, - BaseAPI, - RequiredError, -} from "./base" - -/** - * The Error contains error relevant information. - * @export - * @interface ModelError - */ -export interface ModelError { - /** - * The general error message according to HTTP specification. - * @type {string} - * @memberof ModelError - */ - error: string - /** - * Concrete error message. - * @type {string} - * @memberof ModelError - */ - msg: string - /** - * The http error code. - * @type {number} - * @memberof ModelError - */ - status_code: number -} -/** - * RaucStatus contains information about the current RAUC updater status. - * @export - * @interface RaucStatus - */ -export interface RaucStatus { - /** - * True if the installer is running - * @type {boolean} - * @memberof RaucStatus - */ - installing: boolean - /** - * Installation error message - * @type {string} - * @memberof RaucStatus - */ - last_error: string - /** - * Full command line output of the current installation - * @type {string} - * @memberof RaucStatus - */ - log: string - /** - * Current installation step - * @type {string} - * @memberof RaucStatus - */ - message: string - /** - * Installation progress - * @type {number} - * @memberof RaucStatus - */ - percent: number -} -/** - * - * @export - * @interface Rootfs - */ -export interface Rootfs { - /** - * Is the filesystem bootable? - * @type {boolean} - * @memberof Rootfs - */ - bootable: boolean - /** - * Is the filesystem booted? - * @type {boolean} - * @memberof Rootfs - */ - booted: boolean - /** - * Block device - * @type {string} - * @memberof Rootfs - */ - device: string - /** - * Mount path (null when not mounted) - * @type {string} - * @memberof Rootfs - */ - mountpoint: string - /** - * Is the filesystem the next boot target? - * @type {boolean} - * @memberof Rootfs - */ - primary: boolean - /** - * Filesystem - * @type {string} - * @memberof Rootfs - */ - type: string -} -/** - * StatusMessage contains the status of an operation. - * @export - * @interface StatusMessage - */ -export interface StatusMessage { - /** - * Status message text - * @type {string} - * @memberof StatusMessage - */ - msg: string - /** - * Is operation successful? - * @type {boolean} - * @memberof StatusMessage - */ - success: boolean -} -/** - * SystemInfo contains information about the running system. - * @export - * @interface SystemInfo - */ -export interface SystemInfo { - /** - * Hostname of the system - * @type {string} - * @memberof SystemInfo - */ - hostname: string - /** - * Name of the os distribution - * @type {string} - * @memberof SystemInfo - */ - os_name: string - /** - * Operating system version - * @type {string} - * @memberof SystemInfo - */ - os_version: string - /** - * Compatible firmware name - * @type {string} - * @memberof SystemInfo - */ - rauc_compatible: string - /** - * List of RAUC root filesystems - * @type {{ [key: string]: Rootfs; }} - * @memberof SystemInfo - */ - rauc_rootfs: {[key: string]: Rootfs} - /** - * Compatible firmware variant - * @type {string} - * @memberof SystemInfo - */ - rauc_variant: string - /** - * System uptime in seconds - * @type {number} - * @memberof SystemInfo - */ - uptime: number -} - -/** - * DefaultApi - axios parameter creator - * @export - */ -export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { - return { - /** - * Get the current system info - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getInfo: async (options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/info` - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL) - let baseOptions - if (configuration) { - baseOptions = configuration.baseOptions - } - - const localVarRequestOptions = {method: "GET", ...baseOptions, ...options} - const localVarHeaderParameter = {} as any - const localVarQueryParameter = {} as any - - setSearchParams(localVarUrlObj, localVarQueryParameter) - let headersFromBaseOptions = - baseOptions && baseOptions.headers ? baseOptions.headers : {} - localVarRequestOptions.headers = { - ...localVarHeaderParameter, - ...headersFromBaseOptions, - ...options.headers, - } - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - } - }, - /** - * Get the current status of the RAUC updater - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getStatus: async (options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/status` - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL) - let baseOptions - if (configuration) { - baseOptions = configuration.baseOptions - } - - const localVarRequestOptions = {method: "GET", ...baseOptions, ...options} - const localVarHeaderParameter = {} as any - const localVarQueryParameter = {} as any - - setSearchParams(localVarUrlObj, localVarQueryParameter) - let headersFromBaseOptions = - baseOptions && baseOptions.headers ? baseOptions.headers : {} - localVarRequestOptions.headers = { - ...localVarHeaderParameter, - ...headersFromBaseOptions, - ...options.headers, - } - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - } - }, - /** - * Reboot the system - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - startReboot: async (options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/reboot` - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL) - let baseOptions - if (configuration) { - baseOptions = configuration.baseOptions - } - - const localVarRequestOptions = {method: "POST", ...baseOptions, ...options} - const localVarHeaderParameter = {} as any - const localVarQueryParameter = {} as any - - setSearchParams(localVarUrlObj, localVarQueryParameter) - let headersFromBaseOptions = - baseOptions && baseOptions.headers ? baseOptions.headers : {} - localVarRequestOptions.headers = { - ...localVarHeaderParameter, - ...headersFromBaseOptions, - ...options.headers, - } - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - } - }, - /** - * Start the update process - * @param {any} updateFile RAUC firmware image file (*.raucb) - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - startUpdate: async ( - updateFile: any, - options: AxiosRequestConfig = {} - ): Promise => { - // verify required parameter 'updateFile' is not null or undefined - assertParamExists("startUpdate", "updateFile", updateFile) - const localVarPath = `/update` - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL) - let baseOptions - if (configuration) { - baseOptions = configuration.baseOptions - } - - const localVarRequestOptions = {method: "POST", ...baseOptions, ...options} - const localVarHeaderParameter = {} as any - const localVarQueryParameter = {} as any - const localVarFormParams = new ((configuration && - configuration.formDataCtor) || - FormData)() - - if (updateFile !== undefined) { - localVarFormParams.append("updateFile", updateFile as any) - } - - localVarHeaderParameter["Content-Type"] = "multipart/form-data" - - setSearchParams(localVarUrlObj, localVarQueryParameter) - let headersFromBaseOptions = - baseOptions && baseOptions.headers ? baseOptions.headers : {} - localVarRequestOptions.headers = { - ...localVarHeaderParameter, - ...headersFromBaseOptions, - ...options.headers, - } - localVarRequestOptions.data = localVarFormParams - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - } - }, - } -} - -/** - * DefaultApi - functional programming interface - * @export - */ -export const DefaultApiFp = function (configuration?: Configuration) { - const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) - return { - /** - * Get the current system info - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getInfo( - options?: AxiosRequestConfig - ): Promise< - (axios?: AxiosInstance, basePath?: string) => AxiosPromise - > { - const localVarAxiosArgs = await localVarAxiosParamCreator.getInfo(options) - return createRequestFunction( - localVarAxiosArgs, - globalAxios, - BASE_PATH, - configuration - ) - }, - /** - * Get the current status of the RAUC updater - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getStatus( - options?: AxiosRequestConfig - ): Promise< - (axios?: AxiosInstance, basePath?: string) => AxiosPromise - > { - const localVarAxiosArgs = await localVarAxiosParamCreator.getStatus(options) - return createRequestFunction( - localVarAxiosArgs, - globalAxios, - BASE_PATH, - configuration - ) - }, - /** - * Reboot the system - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async startReboot( - options?: AxiosRequestConfig - ): Promise< - (axios?: AxiosInstance, basePath?: string) => AxiosPromise - > { - const localVarAxiosArgs = await localVarAxiosParamCreator.startReboot( - options - ) - return createRequestFunction( - localVarAxiosArgs, - globalAxios, - BASE_PATH, - configuration - ) - }, - /** - * Start the update process - * @param {any} updateFile RAUC firmware image file (*.raucb) - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async startUpdate( - updateFile: any, - options?: AxiosRequestConfig - ): Promise< - (axios?: AxiosInstance, basePath?: string) => AxiosPromise - > { - const localVarAxiosArgs = await localVarAxiosParamCreator.startUpdate( - updateFile, - options - ) - return createRequestFunction( - localVarAxiosArgs, - globalAxios, - BASE_PATH, - configuration - ) - }, - } -} - -/** - * DefaultApi - factory interface - * @export - */ -export const DefaultApiFactory = function ( - configuration?: Configuration, - basePath?: string, - axios?: AxiosInstance -) { - const localVarFp = DefaultApiFp(configuration) - return { - /** - * Get the current system info - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getInfo(options?: any): AxiosPromise { - return localVarFp - .getInfo(options) - .then((request) => request(axios, basePath)) - }, - /** - * Get the current status of the RAUC updater - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getStatus(options?: any): AxiosPromise { - return localVarFp - .getStatus(options) - .then((request) => request(axios, basePath)) - }, - /** - * Reboot the system - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - startReboot(options?: any): AxiosPromise { - return localVarFp - .startReboot(options) - .then((request) => request(axios, basePath)) - }, - /** - * Start the update process - * @param {any} updateFile RAUC firmware image file (*.raucb) - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - startUpdate(updateFile: any, options?: any): AxiosPromise { - return localVarFp - .startUpdate(updateFile, options) - .then((request) => request(axios, basePath)) - }, - } -} - -/** - * DefaultApi - object-oriented interface - * @export - * @class DefaultApi - * @extends {BaseAPI} - */ -export class DefaultApi extends BaseAPI { - /** - * Get the current system info - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof DefaultApi - */ - public getInfo(options?: AxiosRequestConfig) { - return DefaultApiFp(this.configuration) - .getInfo(options) - .then((request) => request(this.axios, this.basePath)) - } - - /** - * Get the current status of the RAUC updater - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof DefaultApi - */ - public getStatus(options?: AxiosRequestConfig) { - return DefaultApiFp(this.configuration) - .getStatus(options) - .then((request) => request(this.axios, this.basePath)) - } - - /** - * Reboot the system - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof DefaultApi - */ - public startReboot(options?: AxiosRequestConfig) { - return DefaultApiFp(this.configuration) - .startReboot(options) - .then((request) => request(this.axios, this.basePath)) - } - - /** - * Start the update process - * @param {any} updateFile RAUC firmware image file (*.raucb) - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof DefaultApi - */ - public startUpdate(updateFile: any, options?: AxiosRequestConfig) { - return DefaultApiFp(this.configuration) - .startUpdate(updateFile, options) - .then((request) => request(this.axios, this.basePath)) - } -} diff --git a/ui/src/sebrauc-client/base.ts b/ui/src/sebrauc-client/base.ts deleted file mode 100644 index cf13305..0000000 --- a/ui/src/sebrauc-client/base.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * SEBRAUC - * REST API for the SEBRAUC firmware updater - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import {Configuration} from "./configuration" -// Some imports not used depending on template conditions -// @ts-ignore -import globalAxios, {AxiosPromise, AxiosInstance, AxiosRequestConfig} from "axios" - -export const BASE_PATH = "http://localhost".replace(/\/+$/, "") - -/** - * - * @export - */ -export const COLLECTION_FORMATS = { - csv: ",", - ssv: " ", - tsv: "\t", - pipes: "|", -} - -/** - * - * @export - * @interface RequestArgs - */ -export interface RequestArgs { - url: string - options: AxiosRequestConfig -} - -/** - * - * @export - * @class BaseAPI - */ -export class BaseAPI { - protected configuration: Configuration | undefined - - constructor( - configuration?: Configuration, - protected basePath: string = BASE_PATH, - protected axios: AxiosInstance = globalAxios - ) { - if (configuration) { - this.configuration = configuration - this.basePath = configuration.basePath || this.basePath - } - } -} - -/** - * - * @export - * @class RequiredError - * @extends {Error} - */ -export class RequiredError extends Error { - name: "RequiredError" = "RequiredError" - constructor(public field: string, msg?: string) { - super(msg) - } -} diff --git a/ui/src/sebrauc-client/common.ts b/ui/src/sebrauc-client/common.ts deleted file mode 100644 index c0ce169..0000000 --- a/ui/src/sebrauc-client/common.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * SEBRAUC - * REST API for the SEBRAUC firmware updater - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import {Configuration} from "./configuration" -import {RequiredError, RequestArgs} from "./base" -import {AxiosInstance, AxiosResponse} from "axios" - -/** - * - * @export - */ -export const DUMMY_BASE_URL = "https://example.com" - -/** - * - * @throws {RequiredError} - * @export - */ -export const assertParamExists = function ( - functionName: string, - paramName: string, - paramValue: unknown -) { - if (paramValue === null || paramValue === undefined) { - throw new RequiredError( - paramName, - `Required parameter ${paramName} was null or undefined when calling ${functionName}.` - ) - } -} - -/** - * - * @export - */ -export const setApiKeyToObject = async function ( - object: any, - keyParamName: string, - configuration?: Configuration -) { - if (configuration && configuration.apiKey) { - const localVarApiKeyValue = - typeof configuration.apiKey === "function" - ? await configuration.apiKey(keyParamName) - : await configuration.apiKey - object[keyParamName] = localVarApiKeyValue - } -} - -/** - * - * @export - */ -export const setBasicAuthToObject = function ( - object: any, - configuration?: Configuration -) { - if (configuration && (configuration.username || configuration.password)) { - object["auth"] = { - username: configuration.username, - password: configuration.password, - } - } -} - -/** - * - * @export - */ -export const setBearerAuthToObject = async function ( - object: any, - configuration?: Configuration -) { - if (configuration && configuration.accessToken) { - const accessToken = - typeof configuration.accessToken === "function" - ? await configuration.accessToken() - : await configuration.accessToken - object["Authorization"] = "Bearer " + accessToken - } -} - -/** - * - * @export - */ -export const setOAuthToObject = async function ( - object: any, - name: string, - scopes: string[], - configuration?: Configuration -) { - if (configuration && configuration.accessToken) { - const localVarAccessTokenValue = - typeof configuration.accessToken === "function" - ? await configuration.accessToken(name, scopes) - : await configuration.accessToken - object["Authorization"] = "Bearer " + localVarAccessTokenValue - } -} - -/** - * - * @export - */ -export const setSearchParams = function (url: URL, ...objects: any[]) { - const searchParams = new URLSearchParams(url.search) - for (const object of objects) { - for (const key in object) { - if (Array.isArray(object[key])) { - searchParams.delete(key) - for (const item of object[key]) { - searchParams.append(key, item) - } - } else { - searchParams.set(key, object[key]) - } - } - } - url.search = searchParams.toString() -} - -/** - * - * @export - */ -export const serializeDataIfNeeded = function ( - value: any, - requestOptions: any, - configuration?: Configuration -) { - const nonString = typeof value !== "string" - const needsSerialization = - nonString && configuration && configuration.isJsonMime - ? configuration.isJsonMime(requestOptions.headers["Content-Type"]) - : nonString - return needsSerialization - ? JSON.stringify(value !== undefined ? value : {}) - : value || "" -} - -/** - * - * @export - */ -export const toPathString = function (url: URL) { - return url.pathname + url.search + url.hash -} - -/** - * - * @export - */ -export const createRequestFunction = function ( - axiosArgs: RequestArgs, - globalAxios: AxiosInstance, - BASE_PATH: string, - configuration?: Configuration -) { - return >( - axios: AxiosInstance = globalAxios, - basePath: string = BASE_PATH - ) => { - const axiosRequestArgs = { - ...axiosArgs.options, - url: (configuration?.basePath || basePath) + axiosArgs.url, - } - return axios.request(axiosRequestArgs) - } -} diff --git a/ui/src/sebrauc-client/configuration.ts b/ui/src/sebrauc-client/configuration.ts deleted file mode 100644 index 5e30cfa..0000000 --- a/ui/src/sebrauc-client/configuration.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * SEBRAUC - * REST API for the SEBRAUC firmware updater - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -export interface ConfigurationParameters { - apiKey?: - | string - | Promise - | ((name: string) => string) - | ((name: string) => Promise) - username?: string - password?: string - accessToken?: - | string - | Promise - | ((name?: string, scopes?: string[]) => string) - | ((name?: string, scopes?: string[]) => Promise) - basePath?: string - baseOptions?: any - formDataCtor?: new () => any -} - -export class Configuration { - /** - * parameter for apiKey security - * @param name security name - * @memberof Configuration - */ - apiKey?: - | string - | Promise - | ((name: string) => string) - | ((name: string) => Promise) - /** - * parameter for basic security - * - * @type {string} - * @memberof Configuration - */ - username?: string - /** - * parameter for basic security - * - * @type {string} - * @memberof Configuration - */ - password?: string - /** - * parameter for oauth2 security - * @param name security name - * @param scopes oauth2 scope - * @memberof Configuration - */ - accessToken?: - | string - | Promise - | ((name?: string, scopes?: string[]) => string) - | ((name?: string, scopes?: string[]) => Promise) - /** - * override base path - * - * @type {string} - * @memberof Configuration - */ - basePath?: string - /** - * base options for axios calls - * - * @type {any} - * @memberof Configuration - */ - baseOptions?: any - /** - * The FormData constructor that will be used to create multipart form data - * requests. You can inject this here so that execution environments that - * do not support the FormData class can still run the generated client. - * - * @type {new () => FormData} - */ - formDataCtor?: new () => any - - constructor(param: ConfigurationParameters = {}) { - this.apiKey = param.apiKey - this.username = param.username - this.password = param.password - this.accessToken = param.accessToken - this.basePath = param.basePath - this.baseOptions = param.baseOptions - this.formDataCtor = param.formDataCtor - } - - /** - * Check if the given MIME is a JSON MIME. - * JSON MIME examples: - * application/json - * application/json; charset=UTF8 - * APPLICATION/JSON - * application/vnd.company+json - * @param mime - MIME (Multipurpose Internet Mail Extensions) - * @return True if the given MIME is JSON, false otherwise. - */ - public isJsonMime(mime: string): boolean { - const jsonMime: RegExp = new RegExp( - "^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$", - "i" - ) - return ( - mime !== null && - (jsonMime.test(mime) || - mime.toLowerCase() === "application/json-patch+json") - ) - } -} diff --git a/ui/src/sebrauc-client/index.ts b/ui/src/sebrauc-client/index.ts deleted file mode 100644 index 3b3f852..0000000 --- a/ui/src/sebrauc-client/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * SEBRAUC - * REST API for the SEBRAUC firmware updater - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -export * from "./api" -export * from "./configuration" diff --git a/ui/src/style/index.scss b/ui/src/style/index.scss index 1f333d4..5774a08 100644 --- a/ui/src/style/index.scss +++ b/ui/src/style/index.scss @@ -1,5 +1,3 @@ -@use "table"; - html, body { height: 100%; diff --git a/ui/src/style/table.scss b/ui/src/style/table.scss deleted file mode 100644 index c522f5d..0000000 --- a/ui/src/style/table.scss +++ /dev/null @@ -1,47 +0,0 @@ -.table-wrapper { - width: 100%; - overflow-x: auto; -} - -.table { - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; - border-collapse: collapse; - margin: 0; - padding: 0; - width: 100%; - - &.no-bottom-border { - &, - > tr:last-child, - :not(thead) tr:last-child { - border-bottom: none; - } - } -} - -.table caption { - font-size: 1.5em; - margin: 0.5em 0 0.75em; -} - -.table tr { - border-bottom: 1px solid #ddd; - padding: 0.35em; -} - -.table th, -.table td { - padding: 0.625em; - text-align: left; - - .icon { - color: #1f85de; - } -} - -.table th { - font-size: 0.85em; - letter-spacing: 0.085em; - text-transform: uppercase; -} diff --git a/ui/src/util/apiUrls.ts b/ui/src/util/apiUrls.ts index 724d52b..4a9d51b 100644 --- a/ui/src/util/apiUrls.ts +++ b/ui/src/util/apiUrls.ts @@ -1,5 +1,3 @@ -import {Configuration, DefaultApi} from "../sebrauc-client" - let apiHost = document.location.host const httpProto = document.location.protocol const wsProto = httpProto === "https:" ? "wss:" : "ws:" @@ -11,10 +9,4 @@ if (import.meta.env.VITE_API_HOST !== undefined) { const apiUrl = `${httpProto}//${apiHost}/api` const wsUrl = `${wsProto}//${apiHost}/api/ws` -let apicfg = new Configuration({ - basePath: apiUrl, -}) - -const sebraucApi = new DefaultApi(apicfg) - -export {apiUrl, wsUrl, sebraucApi} +export {apiUrl, wsUrl} diff --git a/ui/src/util/colors.ts b/ui/src/util/colors.ts deleted file mode 100644 index 3202366..0000000 --- a/ui/src/util/colors.ts +++ /dev/null @@ -1,7 +0,0 @@ -class colors { - static readonly RED = "#FF0039" - static readonly GREEN = "#148420" - static readonly BLUE = "#1f85de" -} - -export default colors diff --git a/ui/src/util/config.ts b/ui/src/util/config.ts deleted file mode 100644 index 1e0e124..0000000 --- a/ui/src/util/config.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface Config { - version: string -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -declare global { - interface Window { - config?: any - } -} - -function isConfig(object: any): object is Config { - return typeof object === "object" && "version" in object -} - -export function getConfig(): Config { - if (isConfig(window.config)) { - return window.config - } - return { - version: "dev", - } -} diff --git a/ui/src/util/functions.ts b/ui/src/util/functions.ts deleted file mode 100644 index 18179e2..0000000 --- a/ui/src/util/functions.ts +++ /dev/null @@ -1,18 +0,0 @@ -function secondsToString(seconds: number): string { - const numyears = Math.floor(seconds / 31536000) - const numdays = Math.floor((seconds % 31536000) / 86400) - const numhours = Math.floor(((seconds % 31536000) % 86400) / 3600) - const numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60) - const numseconds = (((seconds % 31536000) % 86400) % 3600) % 60 - - let res = [] - if (numyears > 0) res.push(numyears + "yr") - if (numdays > 0) res.push(numdays + "d") - if (numhours > 0) res.push(numhours + "h") - if (numminutes > 0) res.push(numminutes + "m") - if (seconds < 60) res.push(numseconds + "s") - - return res.join(" ") -} - -export {secondsToString} diff --git a/ui/src/util/version.ts b/ui/src/util/version.ts new file mode 100644 index 0000000..9991968 --- /dev/null +++ b/ui/src/util/version.ts @@ -0,0 +1,7 @@ +let version = import.meta.env.VITE_VERSION + +if (version === undefined) { + version = "unknown" +} + +export {version} diff --git a/ui/src/util/websocket.ts b/ui/src/util/websocket.ts deleted file mode 100644 index 8d75673..0000000 --- a/ui/src/util/websocket.ts +++ /dev/null @@ -1,92 +0,0 @@ -import {wsUrl} from "./apiUrls" - -class WebsocketAPI { - private static ws: WebsocketAPI | undefined - - private conn: WebSocket | undefined - private wsConnected: boolean - - private clients: Set - - private constructor() { - this.clients = new Set() - this.wsConnected = false - - if (window.WebSocket) { - this.connect() - } else { - console.log("Your browser does not support WebSockets") - } - } - - private setStatus(wsConnected: boolean) { - if (wsConnected !== this.wsConnected) { - this.wsConnected = wsConnected - this.clients.forEach((client) => { - client.statusCallback(this.wsConnected) - }) - } - } - - private connect() { - this.conn = new WebSocket(wsUrl) - this.conn.onopen = () => { - this.setStatus(true) - console.log("WS connected") - } - this.conn.onclose = () => { - this.setStatus(false) - console.log("WS connection closed") - window.setTimeout(() => this.connect(), 3000) - } - this.conn.onmessage = (evt) => { - this.clients.forEach((client) => { - client.msgCallback(evt) - }) - } - } - - static Get(): WebsocketAPI { - if (this.ws === undefined) { - this.ws = new WebsocketAPI() - } - return this.ws - } - - isConnected(): boolean { - return this.wsConnected - } - - addClient(client: WebsocketClient) { - console.log("added client", client) - this.clients.add(client) - } - - removeClient(client: WebsocketClient) { - console.log("removed client", client) - this.clients.delete(client) - } -} - -export default class WebsocketClient { - statusCallback: (wsConnected: boolean) => void - msgCallback: (evt: MessageEvent) => void - - constructor( - statusCallback: (wsConnected: boolean) => void, - msgCallback: (evt: MessageEvent) => void - ) { - this.statusCallback = statusCallback - this.msgCallback = msgCallback - - this.api().addClient(this) - } - - api(): WebsocketAPI { - return WebsocketAPI.Get() - } - - destroy() { - this.api().removeClient(this) - } -} diff --git a/ui/ui.go b/ui/ui.go index d66f422..1d28f93 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,64 +1,10 @@ package ui import ( - "bytes" "embed" - "encoding/json" - "io/fs" - "net/http" - - "code.thetadev.de/TSGRain/SEBRAUC/src/server/middleware" - "code.thetadev.de/TSGRain/SEBRAUC/src/util" - "github.com/gin-gonic/gin" ) -const distDir = "dist" +const AssetsDir = "dist" //go:embed dist/** -var assets embed.FS - -type uiConfig struct { - Version string `json:"version"` -} - -func subFS(fsys fs.FS, dir string) fs.FS { - sub, err := fs.Sub(fsys, dir) - if err != nil { - panic(err) - } - return sub -} - -func distFS() fs.FS { - return subFS(assets, distDir) -} - -func Register(r gin.IRouter) { - indexHandler := getIndexHandler() - - uiAssets := r.Group("/assets", middleware.Cache) - - r.GET("/", indexHandler) - r.GET("/index.html", indexHandler) - - uiAssets.StaticFS("/", http.FS(subFS(distFS(), "assets"))) -} - -func getIndexHandler() gin.HandlerFunc { - content, err := fs.ReadFile(distFS(), "index.html") - if err != nil { - panic(err) - } - - uiConfigBytes, err := json.Marshal(uiConfig{ - Version: util.Version(), - }) - if err != nil { - panic(err) - } - content = bytes.ReplaceAll(content, []byte("\"%CONFIG%\""), uiConfigBytes) - - return func(c *gin.Context) { - c.Data(200, "text/html", content) - } -} +var Assets embed.FS diff --git a/ui/ui_test.go b/ui/ui_test.go deleted file mode 100644 index 6089bbd..0000000 --- a/ui/ui_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package ui - -import ( - "net/http" - "net/http/httptest" - "os" - "path" - "regexp" - "testing" - - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -func TestUI(t *testing.T) { - tests := []struct { - name string - path string - contains string - cached bool - }{ - { - name: "index_html", - path: "/", - contains: "SEBRAUC", - cached: false, - }, - { - name: "index_html2", - path: "/index.html", - contains: "SEBRAUC", - cached: false, - }, - { - name: "index_js", - path: path.Join("/assets", getIndexJS()), - contains: "SEBRAUC", - cached: true, - }, - } - - router := gin.New() - Register(router) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", tt.path, nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Contains(t, w.Body.String(), tt.contains) - - ccHeader := w.Header().Get("Cache-Control") - - if tt.cached { - assert.Equal(t, "public, max-age=604800, immutable", ccHeader) - } else { - assert.Equal(t, "", ccHeader) - } - }) - } -} - -func getIndexJS() string { - baseDir := "ui/dist/assets" - indexExp := regexp.MustCompile(`index\.[0-9a-f]{8}\.js`) - - fixtures.CdProjectRoot() - distDir, err := os.Open(baseDir) - if err != nil { - panic(err) - } - - list, err := distDir.Readdir(-1) - if err != nil { - panic(err) - } - - for _, f := range list { - if indexExp.MatchString(f.Name()) { - return f.Name() - } - } - panic("no index.js found") -} diff --git a/woodpecker.yml b/woodpecker.yml deleted file mode 100644 index 2271c55..0000000 --- a/woodpecker.yml +++ /dev/null @@ -1,14 +0,0 @@ -pipeline: - Frontend build: - image: node:16-alpine - commands: - - cd ui - - npm install -g pnpm - - pnpm install - - pnpm run build - Backend test: - image: golangci/golangci-lint:latest - commands: - - go get -t ./src/... - - golangci-lint run --timeout 5m - - go test -v ./src/...