diff --git a/Makefile b/Makefile index 8fb9ad1..0956b64 100644 --- a/Makefile +++ b/Makefile @@ -20,5 +20,7 @@ uninstall: rm -f $(DESTDIR)/bin/burgerbackup-client rm -f $(DESTDIR)/bin/burgerbackup-server +test: + clean: rm -rf $(BUILDDIR) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8cdd9a7 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Burgerbackup + +## What is this? + +It's a simple backup program that sends files to a server. +It does this periodically and is designed to have one daemon attached to each file you want to back up. + +No, you can't back up a directory. You have to back up each file individually. This is so I don't have to implement tarballs. + +It uses AES-256 GCM encryption with a ARGON2ID key derivation function. The key is derived from a password you specify in the config file. + +That's practically the most secure encryption you get without breaking some sort of NSA law. It may already be breaking some sort of NSA law. I don't know. I'm not a lawyer. + +## How does it work? + +1. You run the server on a machine you want to store your backups on. +2. You run the client on a machine you want to back up files from. +3. The client sends the server an authentication request, and a password hashed with a salt in SCRYPT. +4. The client AES encrypts the file and sends it to the server in the same request +5. The server verifies the hash with its own password, then decrypts the file it and stores it. +6. The client repeats from step 3 after a specified interval. + +## Installing +First, have Go installed. Latest version. What are we? Debian? + +Run as root +``` +CDIR=$PWD +cd /tmp +git clone https://concord.hectabit.org/hectabit/burgerbackup.git +cd burgerbackup +make install +cd $CDIR +CDIR= +``` + +This also puts your environment back to where it was before you started. How nice of me. + +## Compiling +Root not required. Just run +``` +git clone https://concord.hectabit.org/hectabit/burgerbackup.git +cd burgerbackup +make +``` +The binaries will be located in /dist/bin/ + +## Configuring + +### Config file +The default config file is located at /etc/burgerbackup/(client/server).ini. You can specify a different config file as a command line argument to either program. + +A default config file is provided in (REPO ROOT)/bin/(client/server)/config.ini.example. You can copy this file and modify it to your needs. + +Modifying the config is not optional, unless you want some idiot to send random files to your server. \ No newline at end of file diff --git a/all_test.go b/all_test.go new file mode 100644 index 0000000..700f198 --- /dev/null +++ b/all_test.go @@ -0,0 +1,25 @@ +package main_test + +import ( + "concord.hectabit.org/Hectabit/burgerbackup/tests" + "testing" +) + +func TestBld(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.Cleanup(t) +} + +func TestAES(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestAES(t) + tests.Cleanup(t) +} + +func TestBbk(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.TestBbk(t) + tests.Cleanup(t) +} diff --git a/bin/client/all_test.go b/bin/client/all_test.go new file mode 100644 index 0000000..700f198 --- /dev/null +++ b/bin/client/all_test.go @@ -0,0 +1,25 @@ +package main_test + +import ( + "concord.hectabit.org/Hectabit/burgerbackup/tests" + "testing" +) + +func TestBld(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.Cleanup(t) +} + +func TestAES(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestAES(t) + tests.Cleanup(t) +} + +func TestBbk(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.TestBbk(t) + tests.Cleanup(t) +} diff --git a/bin/client/config.ini.example b/bin/client/config.ini.example index ea747fb..8f88349 100644 --- a/bin/client/config.ini.example +++ b/bin/client/config.ini.example @@ -1,11 +1,11 @@ [config] # Used for authenticating the backup requests. Change to a random password, and keep the same on both the client and server. -BACKUP_KEY=supersecretkey +BACKUP_KEY = supersecretkey # Used for encrypting the backup files during transfer. Change to a random password, and keep the same on both the client and server. -CRYPTO_KEY=supersecretkey +CRYPTO_KEY = supersecretkey # How often the client should backup in seconds. Default is 86400 seconds (24 hours). -BACKUP_INTERVAL=86400 +BACKUP_INTERVAL = 86400 # The URL of the server to send the backups to. -REMOTE_URL=http://example.org:8080 +REMOTE_URL = http://example.org:8080/api/backup # The file to backup, relative to where the command is run. -FILE_LOCATION=/path/to/file \ No newline at end of file +FILE_LOCATION = /path/to/file \ No newline at end of file diff --git a/bin/client/main.go b/bin/client/main.go index 8670edf..72c1696 100644 --- a/bin/client/main.go +++ b/bin/client/main.go @@ -6,6 +6,7 @@ import ( "log" "os" "strconv" + "strings" "time" ) @@ -37,8 +38,12 @@ func main() { log.Fatalln("[FATAL] File is in quantum uncertainty:", err) } - viper.SetConfigName(configPath) - viper.AddConfigPath("./") + viper.SetConfigType("ini") + configPathSlice := strings.Split(configPath, "/") + configPathNoIni := strings.Split(configPathSlice[len(configPathSlice)-1], ".")[0] + viper.SetConfigName(configPathNoIni) + configPathNoFile := strings.Join(configPathSlice[:len(configPathSlice)-1], "/") + "/" + viper.AddConfigPath(configPathNoFile) viper.AutomaticEnv() err := viper.ReadInConfig() @@ -46,32 +51,43 @@ func main() { log.Fatalln("[FATAL] Error in config file at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) } - backupKey := viper.GetString("BACKUP_KEY") - cryptoKey := viper.GetString("CRYPTO_KEY") - backupInterval := viper.GetInt("BACKUP_INTERVAL") - fileLocation := viper.GetString("FILE_LOCATION") - remoteURL := viper.GetString("REMOTE_URL") + backupKey := viper.GetString("config.BACKUP_KEY") + cryptoKey := viper.GetString("config.CRYPTO_KEY") + backupInterval := viper.GetInt("config.BACKUP_INTERVAL") + fileLocation := viper.GetString("config.FILE_LOCATION") + remoteURL := viper.GetString("config.REMOTE_URL") for { err, errCode := client.PerformBackup(fileLocation, backupKey, cryptoKey, remoteURL) if err != nil { - if errCode == 0 { - log.Println("[CRITICAL] Unknown in performBackup() file read:", err) - } else if errCode == 1 { - log.Println("[CRITICAL] Unknown in performBackup() content encryption:", err) - } else if errCode == 2 { + switch errCode { + case 0: + log.Println("[CRITICAL] Unknown in PerformBackup() file read:", err) + case 1: + log.Println("[CRITICAL] Unknown in PerformBackup() file stat:", err) + case 2: + log.Println("[CRITICAL] Unknown in PerformBackup() content marshal:", err) + case 3: + log.Println("[CRITICAL] Unknown in PerformBackup() content encryption:", err) + case 4: + log.Println("[CRITICAL] Unknown in SendFileToServer() form writer creation:", err) + case 5: + log.Println("[CRITICAL] Unknown in SendFileToServer() IO Copying:", err) + case 6: + log.Println("[CRITICAL] Unknown in SendFileToServer() writer closing:", err) + case 7: log.Println("[CRITICAL] Unknown in SendFileToServer() request creation:", err) - } else if errCode == 3 { + case 8: log.Println("[CRITICAL] Unknown in SendFileToServer() hash creation:", err) - } else if errCode == 4 { + case 9: log.Println("[CRITICAL] Unknown in SendFileToServer() request execution:", err) - } else if errCode == 5 { + case 10: log.Println("[CRITICAL] Unknown in SendFileToServer() response read:", err) - } else if errCode == 6 { + case 11: log.Println("[CRITICAL] Unknown in SendFileToServer() response marshalling:", err) - } else if errCode == 7 { + case 12: log.Println("[CRITICAL] Server sent message in SendFileToServer():", err) - } else { + default: log.Println("[CRITICAL] Unknown error in main():", err) } } diff --git a/bin/server/all_test.go b/bin/server/all_test.go new file mode 100644 index 0000000..700f198 --- /dev/null +++ b/bin/server/all_test.go @@ -0,0 +1,25 @@ +package main_test + +import ( + "concord.hectabit.org/Hectabit/burgerbackup/tests" + "testing" +) + +func TestBld(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.Cleanup(t) +} + +func TestAES(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestAES(t) + tests.Cleanup(t) +} + +func TestBbk(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.TestBbk(t) + tests.Cleanup(t) +} diff --git a/bin/server/config.ini.example b/bin/server/config.ini.example index ed0bbcf..6498c9e 100644 --- a/bin/server/config.ini.example +++ b/bin/server/config.ini.example @@ -1,12 +1,12 @@ [config] # Used for authenticating the backup requests. Change to a random password, and keep the same on both the client and server. -BACKUP_KEY=supersecretkey +BACKUP_KEY = supersecretkey # Used for encrypting the backup files during transfer. Change to a random password, and keep the same on both the client and server. -CRYPTO_KEY=supersecretkey +CRYPTO_KEY = supersecretkey # The port burgerbackup runs on. Change to Port 80 if not using a reverse proxy. -PORT=8080 +PORT = 8080 # The host burgerbackup runs on. Change to 127.0.0.1 if using a reverse proxy. -HOST=0.0.0.0 +HOST = 0.0.0.0 # The folder where the backups are stored, relative to where the command is run. # It is recommended to use an absolute path -BACKUP_FOLDER=/path/to/backup/folder \ No newline at end of file +BACKUP_FOLDER = /path/to/backup/folder \ No newline at end of file diff --git a/bin/server/main.go b/bin/server/main.go index f46f024..0fbe8d1 100644 --- a/bin/server/main.go +++ b/bin/server/main.go @@ -2,8 +2,9 @@ package main import ( "concord.hectabit.org/Hectabit/burgerbackup/lib/common" + "encoding/base64" + "encoding/json" "errors" - "fmt" "github.com/gin-gonic/gin" "github.com/spf13/viper" "golang.org/x/crypto/argon2" @@ -13,6 +14,7 @@ import ( "net/http" "os" "strconv" + "strings" "time" ) @@ -44,8 +46,12 @@ func main() { log.Fatalln("[FATAL] File is in quantum uncertainty:", err) } - viper.SetConfigName(configPath) - viper.AddConfigPath("./") + viper.SetConfigType("ini") + configPathSlice := strings.Split(configPath, "/") + configPathNoIni := strings.Split(configPathSlice[len(configPathSlice)-1], ".")[0] + viper.SetConfigName(configPathNoIni) + configPathNoFile := strings.Join(configPathSlice[:len(configPathSlice)-1], "/") + "/" + viper.AddConfigPath(configPathNoFile) viper.AutomaticEnv() err := viper.ReadInConfig() @@ -53,11 +59,11 @@ func main() { log.Fatalln("[FATAL] Error in config file at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) } - backupKey := viper.GetString("BACKUP_KEY") - cryptoKey := viper.GetString("CRYPTO_KEY") - port := viper.GetInt("PORT") - host := viper.GetString("HOST") - backupFolder := viper.GetString("BACKUP_FOLDER") + backupKey := viper.GetString("config.BACKUP_KEY") + cryptoKey := viper.GetString("config.CRYPTO_KEY") + port := viper.GetInt("config.PORT") + host := viper.GetString("config.HOST") + backupFolder := viper.GetString("config.BACKUP_FOLDER") if host == "" { log.Fatalln("[FATAL] HOST is not set") @@ -128,7 +134,6 @@ func main() { } return } - fileName := "database_" + strconv.FormatInt(time.Now().Unix(), 10) + ".db" encryptedFile, err := file.Open() if err != nil { c.JSON(422, gin.H{"error": "Could not open file", "goErr": err.Error()}) @@ -157,14 +162,52 @@ func main() { return } + decryptedDataMap := make(map[string]interface{}) + err = json.Unmarshal(decryptedData, &decryptedDataMap) + if err != nil { + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgerbackup and refer to the documentation for more info. Your error code is: UNKNOWN-API-BACKUP-JSONERR", "goErr": err.Error()}) + log.Println("[ERROR] Error in /api/backup file json:", err) + return + } + + originalFileName, ok := decryptedDataMap["filename"].(string) + if !ok { + c.JSON(422, gin.H{"error": "Could not get filename", "goErr": "could not get filename"}) + return + } + + contentBase64, ok := decryptedDataMap["content"].(string) + if !ok { + c.JSON(422, gin.H{"error": "Could not get content", "goErr": "could not get content"}) + return + } + + content, err := base64.StdEncoding.DecodeString(contentBase64) + if err != nil { + c.JSON(422, gin.H{"error": "Could not decode base64 content", "goErr": err.Error()}) + return + } + + log.Println("[INFO] Received backup from", c.ClientIP(), "with filename", originalFileName) + fileNameSlice := strings.Split(originalFileName, ".") + + var fileName string + if len(fileNameSlice) >= 2 { + fileName = fileNameSlice[0] + "_" + strconv.FormatInt(time.Now().Unix(), 10) + "." + fileNameSlice[1] + } else if len(fileNameSlice) == 1 { + fileName = fileNameSlice[0] + "_" + strconv.FormatInt(time.Now().Unix(), 10) + } else { + fileName = "unnamed-backup_" + strconv.FormatInt(time.Now().Unix(), 10) + } + filePath := backupFolder + "/" + fileName - if err := os.WriteFile(filePath, decryptedData, 0644); err != nil { + if err := os.WriteFile(filePath, content, 0644); err != nil { c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgerbackup and refer to the documentation for more info. Your error code is: UNKNOWN-API-BACKUP-WRITEERR", "goErr": err.Error()}) log.Println("[ERROR] Error in /api/backup file write:", err) return } - fmt.Println("[INFO] Received and decrypted backup from", c.ClientIP()) + log.Println("[INFO] Successfully saved backup from", c.ClientIP()) c.JSON(200, gin.H{"success": "true", "timeTaken": time.Now().Unix(), "goErr": ""}) } else { c.JSON(401, gin.H{"error": "Incorrect backup key", "goErr": "incorrect backup key"}) diff --git a/lib/client/all_test.go b/lib/client/all_test.go new file mode 100644 index 0000000..76c1c8c --- /dev/null +++ b/lib/client/all_test.go @@ -0,0 +1,25 @@ +package client_test + +import ( + "concord.hectabit.org/Hectabit/burgerbackup/tests" + "testing" +) + +func TestBld(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.Cleanup(t) +} + +func TestAES(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestAES(t) + tests.Cleanup(t) +} + +func TestBbk(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.TestBbk(t) + tests.Cleanup(t) +} diff --git a/lib/client/main.go b/lib/client/main.go index a4874fe..ea52938 100644 --- a/lib/client/main.go +++ b/lib/client/main.go @@ -3,56 +3,96 @@ package client import ( "bytes" "concord.hectabit.org/Hectabit/burgerbackup/lib/common" + "encoding/base64" "encoding/json" "errors" "golang.org/x/crypto/argon2" "io" "log" + "mime/multipart" "net/http" "os" + "strconv" ) func PerformBackup(fileLocation string, backupKey string, cryptoKey string, remoteURL string) (error, int) { fileContent, err := os.ReadFile(fileLocation) if err != nil { - log.Println("[CRITICAL] Unknown in performBackup() file read:", err) + log.Println("[CRITICAL] Unknown in PerformBackup() file read:", err) return err, 0 } - cryptoKeyHashed := argon2.IDKey([]byte(cryptoKey), []byte("burgerbackup"), 1, 64*1024, 4, 32) - encryptedContent, err := common.EncryptAES(cryptoKeyHashed, fileContent) + fileInfo, err := os.Stat(fileLocation) if err != nil { - log.Println("[CRITICAL] Unknown in performBack() content encryption", err) + log.Println("[CRITICAL] Unknown in PerformBackup() file stat:", err) return err, 1 } + filename := fileInfo.Name() + + content := map[string]interface{}{ + "filename": filename, + "content": base64.StdEncoding.EncodeToString(fileContent), + } + + marshaledContent, err := json.Marshal(content) + if err != nil { + log.Println("[CRITICAL] Unknown in PerformBackup() content marshal:", err) + return err, 2 + } + + cryptoKeyHashed := argon2.IDKey([]byte(cryptoKey), []byte("burgerbackup"), 1, 64*1024, 4, 32) + encryptedContent, err := common.EncryptAES(cryptoKeyHashed, marshaledContent) + if err != nil { + log.Println("[CRITICAL] Unknown in PerformBackup() content encryption", err) + return err, 3 + } encryptedFile := io.NopCloser(bytes.NewReader(encryptedContent)) - err, errCode := SendFileToServer(encryptedFile, backupKey, remoteURL) + err, errCode := SendFileToServer(encryptedFile, backupKey, remoteURL, filename) if err != nil { - return err, errCode + 2 + return err, errCode + 4 } log.Println("[INFO] Backup completed at:") return nil, -1 } -func SendFileToServer(file io.Reader, backupKey string, remoteURL string) (error, int) { - req, err := http.NewRequest("POST", remoteURL, file) +func SendFileToServer(file io.Reader, backupKey string, remoteURL string, filename string) (error, int) { + sendBody := &bytes.Buffer{} + writer := multipart.NewWriter(sendBody) + part, err := writer.CreateFormFile("file", filename) if err != nil { return err, 0 } + _, err = io.Copy(part, file) + if err != nil { + return err, 1 + } + + err = writer.Close() + if err != nil { + return err, 3 + } + + req, err := http.NewRequest("POST", remoteURL, sendBody) + if err != nil { + return err, 4 + } + salt, err := common.GenSalt(16) hashedBackupKey, err := common.Hash(backupKey, salt) if err != nil { - return err, 1 + return err, 5 } req.Header.Set("Authorization", hashedBackupKey) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Content-Length", strconv.Itoa(sendBody.Len())) client := &http.Client{} resp, err := client.Do(req) if err != nil { - return err, 2 + return err, 6 } defer func(Body io.ReadCloser) { err := Body.Close() @@ -63,13 +103,13 @@ func SendFileToServer(file io.Reader, backupKey string, remoteURL string) (error body, err := io.ReadAll(resp.Body) if err != nil { - return err, 3 + return err, 7 } var response map[string]interface{} err = json.Unmarshal(body, &response) if err != nil { - return err, 4 + return err, 8 } if resp.StatusCode != 200 { @@ -77,7 +117,7 @@ func SendFileToServer(file io.Reader, backupKey string, remoteURL string) (error if !ok { err = "error not sent by server" } - return errors.New(err), 5 + return errors.New(err), 9 } return nil, -1 diff --git a/lib/common/all_test.go b/lib/common/all_test.go new file mode 100644 index 0000000..9796ce0 --- /dev/null +++ b/lib/common/all_test.go @@ -0,0 +1,25 @@ +package common_test + +import ( + "concord.hectabit.org/Hectabit/burgerbackup/tests" + "testing" +) + +func TestBld(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.Cleanup(t) +} + +func TestAES(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestAES(t) + tests.Cleanup(t) +} + +func TestBbk(t *testing.T) { + t.Logf("[INFO] Starting tests...\n") + tests.TestBld(t) + tests.TestBbk(t) + tests.Cleanup(t) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..0a813e6 --- /dev/null +++ b/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +// Hello, fellow programmer! This is a warning script. +// It is meant for idiots who don't read the README.md file. +// If you are reading this, you are probably one of those idiots. +// Please use the Makefile to build burgerbackup. + +// If you are trying to contribute to burgerbackup, then start by reading the README.md file. +// That might have a teensy weensy bit of useful information. +// ;) Have a great day (unless you are using this script in production). + +func main() { + fmt.Println("[WARN] This isn't how you are meant to build burgerbackup. Please use the Makefile.") + fmt.Println("[INFO] This script only exists as a warning and so go test has something to run.") + fmt.Print("[PROMPT] Would you like me to run the makefile for you? (y/n): ") + var input string + _, err := fmt.Scanln(&input) + if err != nil { + fmt.Println("[FATAL] Error reading input: " + err.Error()) + os.Exit(1) + } + if input == "y" { + fmt.Println("[INFO] Running make... (you should use the Makefile, this isn't how you are meant to build burgerbackup)") + err := exec.Command("make").Run() + if err != nil { + fmt.Println("[FATAL] Error running make: " + err.Error()) + os.Exit(1) + } else { + fmt.Println("[INFO] Shame on you for not using the Makefile.") + fmt.Println("[INFO] If you use this idiotic script in production, I will find you. I will capture you. And I will make you use the Makefile.") + } + } else { + fmt.Println("[INFO] Shame on you for not using the Makefile.") + fmt.Println("[INFO] If you use this idiotic script in production, I will find you. I will capture you. And I will make you use the Makefile.") + os.Exit(1) + } +} diff --git a/tests/main.go b/tests/main.go new file mode 100644 index 0000000..c936ed6 --- /dev/null +++ b/tests/main.go @@ -0,0 +1,145 @@ +package tests + +import ( + "concord.hectabit.org/Hectabit/burgerbackup/lib/common" + "golang.org/x/crypto/argon2" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestAES(t *testing.T) { + t.Logf("[INFO] Running test: encryption and decryption\n") + + cryptoKeyHashed := argon2.IDKey([]byte("supersecretkey"), []byte("burgerbackup"), 1, 64*1024, 4, 32) + encryptedContent, err := common.EncryptAES(cryptoKeyHashed, []byte("This is a test file for burgerbackup")) + if err != nil { + t.Fatalf("[FATAL] Error encrypting content: " + err.Error() + "\n") + } + + decryptedContent, err := common.DecryptAES(cryptoKeyHashed, encryptedContent) + if err != nil { + t.Fatalf("[FATAL] Error decrypting content: " + err.Error() + "\n") + } + + if string(decryptedContent) != "This is a test file for burgerbackup" { + t.Logf(string(decryptedContent) + "\n") + t.Fatalf("[FATAL] Decrypted content does not match original content\n") + } + + t.Logf("[INFO] Test encryption and decryption successful: " + string(decryptedContent) + "\n") +} + +func TestBld(t *testing.T) { + t.Logf("[INFO] Running test: package build\n") + + goList := exec.Command("go", "list", "-m", "-f", "\"{{.Dir}}\"") + directoryBytes, err := goList.Output() + if err != nil { + t.Fatalf("[FATAL] Error running go list: " + err.Error() + "\n") + } + directory := strings.TrimSuffix(strings.TrimPrefix(string(directoryBytes), "\""), "\"\n") + t.Logf("[INFO] Located go.mod at: " + directory + "\n") + err = os.RemoveAll("/tmp/burgerbackup") + if err != nil { + t.Fatalf("[FATAL] Error removing old build directory: " + err.Error() + "\n") + } + + output, err := exec.Command("make", "-C", directory, "BUILDDIR=/tmp/burgerbackup").Output() + if err != nil { + t.Logf("[FATAL] Error building the package: " + err.Error() + "\n") + t.Fatalf("[INFO] Make output: " + string(output) + "\n") + } + + t.Logf("[INFO] Test package build successful\n") +} + +func TestBbk(t *testing.T) { + var processes []*exec.Cmd + done := make(chan bool) + t.Logf("[INFO] Starting requisites for test: server and client backup\n") + + serverIni := "[config]\nPORT = 8080\nHOST = 127.0.0.1\nBACKUP_FOLDER = /tmp/burgerbackup/\nBACKUP_KEY = supersecretkey\nCRYPTO_KEY = supersecretkey" + err := os.WriteFile("/tmp/burgerbackup/server.ini", []byte(serverIni), 0644) + if err != nil { + t.Fatalf("[FATAL] Error writing server.ini: " + err.Error() + "\n") + } + + clientIni := "[config]\nFILE_LOCATION = /tmp/burgerbackup/testfile.txt\nREMOTE_URL = http://127.0.0.1:8080/api/backup\nBACKUP_KEY = supersecretkey\nCRYPTO_KEY = supersecretkey\nBACKUP_INTERVAL = 1" + err = os.WriteFile("/tmp/burgerbackup/client.ini", []byte(clientIni), 0644) + if err != nil { + t.Fatalf("[FATAL] Error writing client.ini: " + err.Error() + "\n") + } + + testFile := "This is a test file for burgerbackup" + err = os.WriteFile("/tmp/burgerbackup/testfile.txt", []byte(testFile), 0644) + if err != nil { + t.Fatalf("[FATAL] Error writing testfile: " + err.Error() + "\n") + } + + t.Logf("[INFO] Requisites for test: server and client backup have finished\n") + t.Logf("[INFO] Running test: server and client backup\n") + + go func() { + cmd := exec.Command("/tmp/burgerbackup/bin/server", "/tmp/burgerbackup/server.ini") + processes = append(processes, cmd) + err := cmd.Start() + if err != nil { + t.Fatalf("[FATAL] Error running server: " + err.Error() + "\n") + } + }() + + go func() { + cmd := exec.Command("/tmp/burgerbackup/bin/client", "/tmp/burgerbackup/server.ini") + processes = append(processes, cmd) + err := cmd.Start() + if err != nil { + t.Fatalf("[FATAL] Error running client: " + err.Error() + "\n") + } + }() + + go func() { + time.Sleep(2 * 1000000000) + for _, process := range processes { + err := process.Process.Kill() + if err != nil { + t.Fatalf("[FATAL] Error killing process: " + err.Error() + "\n") + } + } + t.Logf("[INFO] Server and client killed\n") + matches, err := filepath.Glob("/tmp/burgerbackup/testfile_*.txt") + if err != nil { + t.Fatalf("[FATAL] Error globbing testfiles: " + err.Error() + "\n") + } + if len(matches) > 0 { + matchesString := "" + for _, match := range matches { + matchesString += match + "," + } + t.Logf("[INFO] Testfiles found: " + matchesString + "\n") + t.Logf("[INFO] Test server and client backup successful\n") + done <- true + } else { + t.Fatalf("[FATAL] No testfiles found" + "\n") + } + }() + + finished := <-done + if finished { + return + } else { + t.Fatalf("[FATAL] Bit flip detected, please do not expose to cosmic radiation (impossible condition detected)\n") + } +} + +func Cleanup(t *testing.T) { + t.Logf("[INFO] Tests completed successfully, cleaning up...\n") + err := os.RemoveAll("/tmp/burgerbackup") + if err != nil { + t.Fatalf("[FATAL] Error cleaning up: " + err.Error() + "\n") + } + t.Logf("[INFO] Cleanup successful. Exiting...\n") +}