Skip to content

Commit

Permalink
Move policies to server and add more options
Browse files Browse the repository at this point in the history
This improves security and prevents incidents where we destroy old backups
due to an over-trigger of backups (destroying too many old copies).

Client can no longer specify copy count, this must be done at the server.
  • Loading branch information
davidgfnet committed Nov 25, 2020
1 parent 94dbce8 commit dc6a21f
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 13 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

all:
g++ -o server server.cc -lcrypto -lssl -lpthread -lconfig -O2 -std=c++17
g++ -o server server.cc -lcrypto -lssl -lpthread -lconfig -O2 -std=c++17 -Wall

21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,28 @@ password = "s3cur3p4ss";
port = 12345;
dir = "/my/backup/dir";
keyfile = "key.pem";
cerfile = "cert.pem";
certfile = "cert.pem";
max-connections = 50;
user = "nobody";
backup-targets = (
{
name = "pictures_backup";
max-copies = 3;
rate-limit = {
period = 24; // Max 1 backup every 24h
copies = 1;
};
},
{
name = "docs_backup";
max-copies = 10;
rate-limit = {
period = 720; // Max 5 backups per month
copies = 5;
};
}
);
```

If `user` is specified, the server expectes to be run as root, and will
Expand Down
2 changes: 0 additions & 2 deletions bscli.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ def check_host_name(peercert, name):
parser.add_argument('--pass', dest='pwd', required=True, help='String password to authenticate with server')
parser.add_argument('--name', dest='name', required=True, help='Name of the backup to issue')
parser.add_argument('--file', dest='pfile', required=True, help='File to backup')
parser.add_argument('--copies', dest='ncopy', required=True, type=int, help='Number of copies to keep')
parser.add_argument('--nocert', dest='nocert', action="store_true", help='Ignore SSL cert errors (DISCOURAGED!)')
args = parser.parse_args()

Expand Down Expand Up @@ -78,7 +77,6 @@ def check_host_name(peercert, name):
sock.write(npad(args.name.encode("utf-8")))
sock.write(filehash)
sock.write(struct.pack("!Q", filesize))
sock.write(struct.pack("!Q", args.ncopy))

# Now push the file!
with open(args.pfile, 'rb') as f:
Expand Down
94 changes: 85 additions & 9 deletions server.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include <iostream>
#include <chrono>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
Expand All @@ -27,8 +29,17 @@

#define RET_ERR(x) { std::cerr << x << std::endl; return 1; }


struct t_target {
// Rotation copy count
unsigned maxcopies;
// Rate limiting
unsigned rl_period, rl_copies;
};

// Make this global to simplify things
std::string backup_dir, master_pass;
std::unordered_map<std::string, t_target> targets;

uint64_t read64n(const uint8_t *b) {
uint64_t r = 0;
Expand All @@ -51,7 +62,7 @@ std::string gettodn() {
char fmtime[128];
auto tp = std::chrono::system_clock::now();
const std::time_t t = std::chrono::system_clock::to_time_t(tp);
std::strftime(fmtime, sizeof(fmtime), "%Y-%m-%d_%H-%M-%S", std::localtime(&t));
std::strftime(fmtime, sizeof(fmtime), "%Y-%m-%d_%H-%M-%S", std::gmtime(&t));
return fmtime;
}

Expand Down Expand Up @@ -84,6 +95,23 @@ std::vector<std::string> listdir(std::string sdir) {
return ret;
}

std::vector<uint64_t> listbackupts(std::string sdir) {
std::vector<uint64_t> ret;
for (auto fp : listdir(sdir)) {
auto p = fp.find_last_of('/');
std::string fn = p == std::string::npos ? fp : fp.substr(p+1);
if (fn.size() == 28 && fn[19] == '_') {
std::tm t;
std::istringstream ss(fn.substr(0, 19));
if (ss >> std::get_time(&t, "%Y-%m-%d_%H-%M-%S")) {
std::time_t time_stamp = timegm(&t);
ret.push_back(time_stamp);
}
}
}
return ret;
}

void backup_cleanup(std::string dir, unsigned max_copies) {
auto files = listdir(dir);
std::sort(files.begin(), files.end(), std::greater<std::string>()); // Sort from most recent to oldest
Expand Down Expand Up @@ -126,7 +154,7 @@ class BackupHandler {

// Simple read() wrapper
int sslread(char *buffer, unsigned size) {
int offset = 0;
unsigned offset = 0;
while (offset < size) {
int r = SSL_read(ssl, &buffer[offset], size - offset);
if (!r)
Expand Down Expand Up @@ -159,16 +187,13 @@ class BackupHandler {

// Receive the header, with info about the backup
char bname[256], hash[32];
uint8_t maxcopies[8], filesize[8];
uint8_t filesize[8];
if (sizeof(bname) != sslread(bname, sizeof(bname)))
return {false, "Failed parsing request header"};
if (sizeof(hash) != sslread(hash, sizeof(hash)))
return {false, "Failed parsing request header"};
if (sizeof(filesize) != sslread((char*)filesize, sizeof(filesize)))
return {false, "Failed parsing request header"};
if (sizeof(maxcopies) != sslread((char*)maxcopies, sizeof(maxcopies)))
return {false, "Failed parsing request header"};
uint64_t mc = read64n(maxcopies);
uint64_t fs = read64n(filesize);
bname[sizeof(bname)-1] = 0;
std::string backupname(bname), backuphash(hash, sizeof(hash));
Expand All @@ -177,10 +202,26 @@ class BackupHandler {
std::replace(backupname.begin(), backupname.end(), '/', '_');
std::cout << "Got backup for " << backupname << " (" << fs << " bytes) with hash " << tohex(backuphash) << std::endl;

// Check the backup policy and enforce any rate limits
if (!targets.count(backupname))
return {false, "Backup is not defined in the server config"};
const t_target *t = &targets.at(backupname);

std::string subdirp = backup_dir + "/" + backupname;
std::string fullpath = subdirp + "/" + gettodn() + "_" + tohex(backuphash).substr(0, 8);
std::string tmpfullpath = fullpath + ".part";

if (t->rl_period && t->rl_copies) {
auto tsl = listbackupts(subdirp);
// Find the number of backups in the last rl_period hours
unsigned cnt = 0;
for (auto ts: tsl)
if ((signed)ts > time(0) - t->rl_period * 3600)
cnt++;
if (cnt >= t->rl_copies)
return {false, "Too many backups: " + std::to_string(cnt)};
}

// Create dir just in case
mkdir(subdirp.c_str(), 0750);

Expand All @@ -195,7 +236,7 @@ class BackupHandler {
auto rr = sslread(tmp, std::min((uint64_t)sizeof(tmp), fs - received));
if (rr <= 0)
break;
if (rr != fwrite(tmp, 1, rr, fo))
if (rr != (int)fwrite(tmp, 1, rr, fo))
break;
received += rr;
SHA256_Update(&sctx, tmp, rr);
Expand All @@ -217,7 +258,7 @@ class BackupHandler {
rename(tmpfullpath.c_str(), fullpath.c_str());

// Now cleanup the extra files we might have
backup_cleanup(subdirp, mc);
backup_cleanup(subdirp, t->maxcopies);

return {true, "Backup stored successfully"};
}
Expand Down Expand Up @@ -320,7 +361,7 @@ int main(int argc, char **argv) {
RET_ERR("Error reading config file");

// Read config vars
int max_connections = 10, port = 8080;
unsigned max_connections = 10, port = 8080;
const char *tmp_;
config_lookup_int(&cfg, "max-connections", (int*)&max_connections);
config_lookup_int(&cfg, "port", (int*)&port);
Expand All @@ -330,6 +371,41 @@ int main(int argc, char **argv) {
if (!config_lookup_string(&cfg, "dir", &tmp_))
RET_ERR("'dir' missing in config file");
backup_dir = tmp_;

// Read backup targets from config
config_setting_t *targets_cfg = config_lookup(&cfg, "backup-targets");
if (!targets_cfg)
RET_ERR("Missing 'backup-targets' config array definition");
int tgtcnt = config_setting_length(targets_cfg);
if (!tgtcnt)
RET_ERR("backup-targets must have at least one entry");

for (int i = 0; i < tgtcnt; i++) {
config_setting_t *entry = config_setting_get_elem(targets_cfg, i);
config_setting_t *tgtname = config_setting_get_member(entry, "name");
config_setting_t *tgtmaxc = config_setting_get_member(entry, "max-copies");
config_setting_t *tgtrl = config_setting_get_member(entry, "rate-limit");

if (!tgtname || !tgtmaxc)
RET_ERR("name and max-copies must be specified in each backup target entry");

unsigned rl_period = 0, rl_copies = 0;
if (tgtrl) {
config_setting_t *mperiod = config_setting_get_member(tgtrl, "period");
config_setting_t *mcopies = config_setting_get_member(tgtrl, "copies");
if (mperiod && mcopies) {
rl_period = (unsigned)config_setting_get_int(mperiod);
rl_copies = (unsigned)config_setting_get_int(mcopies);
}
}

targets[config_setting_get_string(tgtname)] = {
.maxcopies = (unsigned)config_setting_get_int(tgtmaxc),
.rl_period = rl_period,
.rl_copies = rl_copies,
};
}
std::cerr << "Parsed config, found " << targets.size() << " backup targets" << std::endl;

// Setup SSL stuff, use defaults mostly
signal(SIGPIPE, SIG_IGN);
Expand Down

0 comments on commit dc6a21f

Please sign in to comment.