initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/result
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1779560665,
|
||||||
|
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
67
flake.nix
Normal file
67
flake.nix
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
description = "nas_unlock";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
pname = "nix_unlock";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
app = pkgs.buildGoModule {
|
||||||
|
inherit pname version;
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
vendorHash = null;
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "Unlock your NAS via http";
|
||||||
|
license = licenses.asl20;
|
||||||
|
mainProgram = pname;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.default = app;
|
||||||
|
packages.${pname} = app;
|
||||||
|
defaultPackage = app;
|
||||||
|
apps.default = flake-utils.lib.mkApp { drv = app; };
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
name = "${pname}-dev";
|
||||||
|
|
||||||
|
packages = with pkgs; [
|
||||||
|
go
|
||||||
|
gopls
|
||||||
|
gotools
|
||||||
|
golangci-lint
|
||||||
|
delve
|
||||||
|
];
|
||||||
|
|
||||||
|
env = {
|
||||||
|
GOPATH = "${placeholder "out"}/go";
|
||||||
|
CGO_ENABLED = "0";
|
||||||
|
};
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "Go version: $(go version | awk '{print $3}')"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
devShell = self.devShells.${system}.default;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
155
main.go
Normal file
155
main.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var unlockCommand string
|
||||||
|
var unlockMutex sync.Mutex
|
||||||
|
|
||||||
|
type unlockPayload struct {
|
||||||
|
Passphrase string `json:"passphrase"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiKeyMiddleware(apiKey string, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
providedKey := r.Header.Get("X-API-Key")
|
||||||
|
|
||||||
|
match := subtle.ConstantTimeCompare([]byte(providedKey), []byte(apiKey))
|
||||||
|
if match != 1 {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func unlockZFSHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Header.Get("Content-Type") != "application/json" {
|
||||||
|
http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var payload unlockPayload
|
||||||
|
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 10*1024))
|
||||||
|
err := decoder.Decode(&payload)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Malformed json", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockMutex.Lock()
|
||||||
|
defer unlockMutex.Unlock()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, unlockCommand)
|
||||||
|
|
||||||
|
cmd.Stdin = strings.NewReader(payload.Passphrase)
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to unlock dataset", http.StatusInternalServerError)
|
||||||
|
log.Printf("Error: %v", err)
|
||||||
|
log.Printf("Output: %s", output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Dataset unlocked and mounted successfully.\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateUnlockCommand(command string) bool {
|
||||||
|
if !filepath.IsAbs(command) {
|
||||||
|
log.Printf("Unlock command path is not absolute\n")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := os.Stat(command)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
log.Printf("Command does not exist\n")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
unlockCommandFlag := flag.String("command", "", "what script to execute to unlock the volume")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *unlockCommandFlag == "" {
|
||||||
|
log.Fatalln("No script provided")
|
||||||
|
}
|
||||||
|
unlockCommand = *unlockCommandFlag
|
||||||
|
|
||||||
|
if !validateUnlockCommand(unlockCommand) {
|
||||||
|
log.Fatalf("Unlock command validation failed\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey := os.Getenv("API_KEY")
|
||||||
|
if apiKey == "" {
|
||||||
|
log.Fatalln("No API_KEY provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apiKey) <= 14 {
|
||||||
|
log.Fatalln("API_KEY must be at least 14 characters long")
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/api/unlock", apiKeyMiddleware(apiKey, http.HandlerFunc(unlockZFSHandler)))
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 31 * time.Second,
|
||||||
|
IdleTimeout: 31 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdownChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(shutdownChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("Server listening on %s", srv.Addr)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("Server startup failed: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-shutdownChan
|
||||||
|
log.Println("Shutdown signal received. Shutting down gracefully...")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
log.Fatalf("Server forced to shutdown: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Server exited properly")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user