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