157 lines
3.5 KiB
Go
157 lines
3.5 KiB
Go
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, "/run/wrappers/bin/sudo", unlockCommand)
|
|
|
|
cmd.Stdin = strings.NewReader(payload.Passphrase)
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
log.Printf("Output: %s", output)
|
|
|
|
if err != nil {
|
|
http.Error(w, "Failed to unlock dataset", http.StatusInternalServerError)
|
|
log.Printf("Error: %v", err)
|
|
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")
|
|
}
|