Made the backup mechanism actually work, made filenames carry across client-server, added extensive testing suite, made the configurations actually work, updated the README, added "Makefiles for dummies" (main.go).
This commit is contained in:
parent
84e3071a25
commit
16a4153a0c
2
Makefile
2
Makefile
|
@ -20,5 +20,7 @@ uninstall:
|
||||||
rm -f $(DESTDIR)/bin/burgerbackup-client
|
rm -f $(DESTDIR)/bin/burgerbackup-client
|
||||||
rm -f $(DESTDIR)/bin/burgerbackup-server
|
rm -f $(DESTDIR)/bin/burgerbackup-server
|
||||||
|
|
||||||
|
test:
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILDDIR)
|
rm -rf $(BUILDDIR)
|
|
@ -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.
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -6,6 +6,6 @@ CRYPTO_KEY=supersecretkey
|
||||||
# How often the client should backup in seconds. Default is 86400 seconds (24 hours).
|
# 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.
|
# 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.
|
# The file to backup, relative to where the command is run.
|
||||||
FILE_LOCATION = /path/to/file
|
FILE_LOCATION = /path/to/file
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,8 +38,12 @@ func main() {
|
||||||
log.Fatalln("[FATAL] File is in quantum uncertainty:", err)
|
log.Fatalln("[FATAL] File is in quantum uncertainty:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
viper.SetConfigName(configPath)
|
viper.SetConfigType("ini")
|
||||||
viper.AddConfigPath("./")
|
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()
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
err := viper.ReadInConfig()
|
err := viper.ReadInConfig()
|
||||||
|
@ -46,32 +51,43 @@ func main() {
|
||||||
log.Fatalln("[FATAL] Error in config file at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
|
log.Fatalln("[FATAL] Error in config file at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
backupKey := viper.GetString("BACKUP_KEY")
|
backupKey := viper.GetString("config.BACKUP_KEY")
|
||||||
cryptoKey := viper.GetString("CRYPTO_KEY")
|
cryptoKey := viper.GetString("config.CRYPTO_KEY")
|
||||||
backupInterval := viper.GetInt("BACKUP_INTERVAL")
|
backupInterval := viper.GetInt("config.BACKUP_INTERVAL")
|
||||||
fileLocation := viper.GetString("FILE_LOCATION")
|
fileLocation := viper.GetString("config.FILE_LOCATION")
|
||||||
remoteURL := viper.GetString("REMOTE_URL")
|
remoteURL := viper.GetString("config.REMOTE_URL")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
err, errCode := client.PerformBackup(fileLocation, backupKey, cryptoKey, remoteURL)
|
err, errCode := client.PerformBackup(fileLocation, backupKey, cryptoKey, remoteURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errCode == 0 {
|
switch errCode {
|
||||||
log.Println("[CRITICAL] Unknown in performBackup() file read:", err)
|
case 0:
|
||||||
} else if errCode == 1 {
|
log.Println("[CRITICAL] Unknown in PerformBackup() file read:", err)
|
||||||
log.Println("[CRITICAL] Unknown in performBackup() content encryption:", err)
|
case 1:
|
||||||
} else if errCode == 2 {
|
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)
|
log.Println("[CRITICAL] Unknown in SendFileToServer() request creation:", err)
|
||||||
} else if errCode == 3 {
|
case 8:
|
||||||
log.Println("[CRITICAL] Unknown in SendFileToServer() hash creation:", err)
|
log.Println("[CRITICAL] Unknown in SendFileToServer() hash creation:", err)
|
||||||
} else if errCode == 4 {
|
case 9:
|
||||||
log.Println("[CRITICAL] Unknown in SendFileToServer() request execution:", err)
|
log.Println("[CRITICAL] Unknown in SendFileToServer() request execution:", err)
|
||||||
} else if errCode == 5 {
|
case 10:
|
||||||
log.Println("[CRITICAL] Unknown in SendFileToServer() response read:", err)
|
log.Println("[CRITICAL] Unknown in SendFileToServer() response read:", err)
|
||||||
} else if errCode == 6 {
|
case 11:
|
||||||
log.Println("[CRITICAL] Unknown in SendFileToServer() response marshalling:", err)
|
log.Println("[CRITICAL] Unknown in SendFileToServer() response marshalling:", err)
|
||||||
} else if errCode == 7 {
|
case 12:
|
||||||
log.Println("[CRITICAL] Server sent message in SendFileToServer():", err)
|
log.Println("[CRITICAL] Server sent message in SendFileToServer():", err)
|
||||||
} else {
|
default:
|
||||||
log.Println("[CRITICAL] Unknown error in main():", err)
|
log.Println("[CRITICAL] Unknown error in main():", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -2,8 +2,9 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"concord.hectabit.org/Hectabit/burgerbackup/lib/common"
|
"concord.hectabit.org/Hectabit/burgerbackup/lib/common"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,8 +46,12 @@ func main() {
|
||||||
log.Fatalln("[FATAL] File is in quantum uncertainty:", err)
|
log.Fatalln("[FATAL] File is in quantum uncertainty:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
viper.SetConfigName(configPath)
|
viper.SetConfigType("ini")
|
||||||
viper.AddConfigPath("./")
|
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()
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
err := viper.ReadInConfig()
|
err := viper.ReadInConfig()
|
||||||
|
@ -53,11 +59,11 @@ func main() {
|
||||||
log.Fatalln("[FATAL] Error in config file at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
|
log.Fatalln("[FATAL] Error in config file at", strconv.FormatInt(time.Now().Unix(), 10)+":", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
backupKey := viper.GetString("BACKUP_KEY")
|
backupKey := viper.GetString("config.BACKUP_KEY")
|
||||||
cryptoKey := viper.GetString("CRYPTO_KEY")
|
cryptoKey := viper.GetString("config.CRYPTO_KEY")
|
||||||
port := viper.GetInt("PORT")
|
port := viper.GetInt("config.PORT")
|
||||||
host := viper.GetString("HOST")
|
host := viper.GetString("config.HOST")
|
||||||
backupFolder := viper.GetString("BACKUP_FOLDER")
|
backupFolder := viper.GetString("config.BACKUP_FOLDER")
|
||||||
|
|
||||||
if host == "" {
|
if host == "" {
|
||||||
log.Fatalln("[FATAL] HOST is not set")
|
log.Fatalln("[FATAL] HOST is not set")
|
||||||
|
@ -128,7 +134,6 @@ func main() {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileName := "database_" + strconv.FormatInt(time.Now().Unix(), 10) + ".db"
|
|
||||||
encryptedFile, err := file.Open()
|
encryptedFile, err := file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(422, gin.H{"error": "Could not open file", "goErr": err.Error()})
|
c.JSON(422, gin.H{"error": "Could not open file", "goErr": err.Error()})
|
||||||
|
@ -157,14 +162,52 @@ func main() {
|
||||||
return
|
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
|
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()})
|
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)
|
log.Println("[ERROR] Error in /api/backup file write:", err)
|
||||||
return
|
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": ""})
|
c.JSON(200, gin.H{"success": "true", "timeTaken": time.Now().Unix(), "goErr": ""})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(401, gin.H{"error": "Incorrect backup key", "goErr": "incorrect backup key"})
|
c.JSON(401, gin.H{"error": "Incorrect backup key", "goErr": "incorrect backup key"})
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -3,56 +3,96 @@ package client
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"concord.hectabit.org/Hectabit/burgerbackup/lib/common"
|
"concord.hectabit.org/Hectabit/burgerbackup/lib/common"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PerformBackup(fileLocation string, backupKey string, cryptoKey string, remoteURL string) (error, int) {
|
func PerformBackup(fileLocation string, backupKey string, cryptoKey string, remoteURL string) (error, int) {
|
||||||
fileContent, err := os.ReadFile(fileLocation)
|
fileContent, err := os.ReadFile(fileLocation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[CRITICAL] Unknown in performBackup() file read:", err)
|
log.Println("[CRITICAL] Unknown in PerformBackup() file read:", err)
|
||||||
return err, 0
|
return err, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
cryptoKeyHashed := argon2.IDKey([]byte(cryptoKey), []byte("burgerbackup"), 1, 64*1024, 4, 32)
|
fileInfo, err := os.Stat(fileLocation)
|
||||||
encryptedContent, err := common.EncryptAES(cryptoKeyHashed, fileContent)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[CRITICAL] Unknown in performBack() content encryption", err)
|
log.Println("[CRITICAL] Unknown in PerformBackup() file stat:", err)
|
||||||
return err, 1
|
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))
|
encryptedFile := io.NopCloser(bytes.NewReader(encryptedContent))
|
||||||
err, errCode := SendFileToServer(encryptedFile, backupKey, remoteURL)
|
err, errCode := SendFileToServer(encryptedFile, backupKey, remoteURL, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, errCode + 2
|
return err, errCode + 4
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[INFO] Backup completed at:")
|
log.Println("[INFO] Backup completed at:")
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendFileToServer(file io.Reader, backupKey string, remoteURL string) (error, int) {
|
func SendFileToServer(file io.Reader, backupKey string, remoteURL string, filename string) (error, int) {
|
||||||
req, err := http.NewRequest("POST", remoteURL, file)
|
sendBody := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(sendBody)
|
||||||
|
part, err := writer.CreateFormFile("file", filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, 0
|
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)
|
salt, err := common.GenSalt(16)
|
||||||
hashedBackupKey, err := common.Hash(backupKey, salt)
|
hashedBackupKey, err := common.Hash(backupKey, salt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, 1
|
return err, 5
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", hashedBackupKey)
|
req.Header.Set("Authorization", hashedBackupKey)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
req.Header.Set("Content-Length", strconv.Itoa(sendBody.Len()))
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, 2
|
return err, 6
|
||||||
}
|
}
|
||||||
defer func(Body io.ReadCloser) {
|
defer func(Body io.ReadCloser) {
|
||||||
err := Body.Close()
|
err := Body.Close()
|
||||||
|
@ -63,13 +103,13 @@ func SendFileToServer(file io.Reader, backupKey string, remoteURL string) (error
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, 3
|
return err, 7
|
||||||
}
|
}
|
||||||
|
|
||||||
var response map[string]interface{}
|
var response map[string]interface{}
|
||||||
err = json.Unmarshal(body, &response)
|
err = json.Unmarshal(body, &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, 4
|
return err, 8
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
|
@ -77,7 +117,7 @@ func SendFileToServer(file io.Reader, backupKey string, remoteURL string) (error
|
||||||
if !ok {
|
if !ok {
|
||||||
err = "error not sent by server"
|
err = "error not sent by server"
|
||||||
}
|
}
|
||||||
return errors.New(err), 5
|
return errors.New(err), 9
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, -1
|
return nil, -1
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
Reference in New Issue