tesses.backup/Tesses.Backup.Server/Class1.cs

485 lines
20 KiB
C#

/*
A simple Backup Client / Server
Copyright (C) 2023 Mike Nolan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
using System;
using System.Threading.Tasks;
using Tesses.VirtualFilesystem;
using Tesses.WebServer;
using Tesses.WebServer.Swagme;
using LiteDB;
using System.IO;
using LiteDB.Engine;
using Tesses.Backup.Models;
using System.Security.Cryptography;
using System.Collections.Generic;
namespace Tesses.Backup
{
public sealed class BackupServer : IDisposable
{
LiteDatabase db;
Stream strm;
MountableServer mountableServer;
ChangeableServer changeableServer=new ChangeableServer();
public IServer Server => mountableServer;
public ChangeableServer WebUi => changeableServer;
public IVirtualFilesystem Filesystem => fs;
private IVirtualFilesystem fs;
private ILiteCollection<Account> Accounts;
private ILiteCollection<Login> Logins;
private ILiteCollection<Tesses.Backup.Models.Backup> Backups;
private ILiteCollection<Device> Devices;
public BackupServer(IVirtualFilesystem fs)
{
this.fs = fs;
Directory.CreateDirectory("data");
bool exists=File.Exists("data/app.db");
db=new LiteDatabase("data/app.db");
Accounts=db.GetCollection<Account>("Accounts");
Logins = db.GetCollection<Login>("Logins");
Backups = db.GetCollection<Tesses.Backup.Models.Backup>("Backups");
Devices = db.GetCollection<Device>("Devices");
if(!exists)
{
Account account=new Account();
account.IsAdmin =true;
account.Username = "admin";
account.Password = "changeme";
Accounts.Insert(account);
}
mountableServer = new MountableServer(WebUi);
mountableServer.Mount("/api/v1/",CreateSwagme());
}
private IServer CreateSwagme()
{
SwagmeServer swag=new SwagmeServer();
swag.AbsoluteUrl=true;
swag.Add("/Backups",EnumerateBackupsAsync,new SwagmeDocumentation("Enumerate Backups for Device",""),"POST");
swag.Add("/Devices",EnumerateDevicesAsync,new SwagmeDocumentation("Enumerate Devices for Account",""),"POST");
swag.Add("/Backup",GetBackupAsync,new SwagmeDocumentation("Get Backup for Device",""),"POST");
swag.Add("/File",GetFileAsync,new SwagmeDocumentation("Get file",""));
swag.Add("/File",BackupFileAsync,new SwagmeDocumentation("Backup File",""),"PUT");
swag.Add("/Login",LoginAsync,new SwagmeDocumentation("Login",""),"POST");
swag.Add("/Logout",LogoutAsync,new SwagmeDocumentation("Logout",""),"POST");
swag.Add("/Device",DeviceAsync,new SwagmeDocumentation("Device",""),"POST");
swag.Add("/CreateBackup",CreateBackupAsync,new SwagmeDocumentation("Create Backup",""),"POST");
swag.Add("/ChangePassword",ChangePasswordAsync,new SwagmeDocumentation("Change Password",""),"POST");
swag.Add("/CreateUser",CreateUserAsync,new SwagmeDocumentation("Create User",""),"POST");
return swag;
}
private (Account account,bool success,string failTxt) GetAccountFromKey(string key)
{
var login=Logins.FindOne(e=>e.Key == key);
if(login != null)
{
var account = Accounts.FindOne(e=>e.Id == login.UserId);
if(account == null)
{
Logins.Delete(login.Id);
return (null,false,"No user account");
}
return (account,true,"");
}
return (null,false,"Invalid Session");
}
private async Task EnumerateBackupsAsync(ServerContext ctx)
{
var backupRequest= await ctx.ReadJsonAsync<EnumerateBackupsRequest>();
var (account,success,failTxt) = GetAccountFromKey(backupRequest.Key);
var backupResponse = new EnumerateBackupsResponse();
backupResponse.Success = success;
backupResponse.ErrorData = failTxt;
if(success)
{
var backups = Backups.Find(e=>e.UserId == account.Id);
foreach(var bkp in backups)
{
if(bkp.DeviceId == backupRequest.DeviceId)
{
BackupEntry entry=new BackupEntry();
entry.BackupId = bkp.Id;
entry.Creation = bkp.Creation;
backupResponse.Backups.Add(entry);
}
}
}
await ctx.SendJsonAsync(backupResponse);
}
private async Task EnumerateDevicesAsync(ServerContext ctx)
{
var deviceRequest= await ctx.ReadJsonAsync<EnumerateDeviceRequest>();
var (account,success,failTxt) = GetAccountFromKey(deviceRequest.Key);
var deviceResponse = new EnumerateDeviceResponse();
deviceResponse.Success = success;
deviceResponse.ErrorData = failTxt;
if(success)
{
var devices = Devices.Find(e=>e.UserId == account.Id);
foreach(var device in devices)
{
DeviceEntry entry=new DeviceEntry();
entry.DeviceId = device.Id;
entry.DeviceName = device.DeviceName;
deviceResponse.Devices.Add(entry);
}
}
await ctx.SendJsonAsync(deviceResponse);
}
private async Task DeviceAsync(ServerContext ctx)
{
var deviceRequest= await ctx.ReadJsonAsync<GetDeviceRequest>();
var (account,success,failTxt) = GetAccountFromKey(deviceRequest.Key);
var deviceResponse = new GetDeviceResponse();
deviceResponse.Success = success;
deviceResponse.ErrorData = failTxt;
if(success)
{
long? deviceId=null;
var devices = Devices.Find(e=>e.UserId == account.Id);
foreach(var item in devices)
{
if(item.DeviceName == deviceRequest.Name)
{
deviceId = item.Id;
break;
}
}
deviceResponse.Existing = deviceId.HasValue;
if(deviceId.HasValue)
{
deviceResponse.DeviceId = deviceId.Value;
}else{
Device dev = new Device();
dev.DeviceName = deviceRequest.Name;
dev.UserId = account.Id;
deviceResponse.DeviceId = Devices.Insert(dev).AsInt64;
}
}
await ctx.SendJsonAsync(deviceResponse);
}
private async Task LogoutAsync(ServerContext ctx)
{
var logoutRequest= await ctx.ReadJsonAsync<LogoutRequest>();
var login=Logins.FindOne(e=>e.Key == logoutRequest.Key);
var logoutResponse = new LogoutResponse();
logoutResponse.Success =false;
logoutResponse.ErrorData="Not logged in";
if(login != null)
{
logoutResponse.Success =true;
logoutResponse.ErrorData="";
Logins.Delete(login.Id);
}
await ctx.SendJsonAsync(logoutResponse);
}
private async Task LoginAsync(ServerContext ctx)
{
var loginRequest= await ctx.ReadJsonAsync<LoginRequest>();
var account = Accounts.FindOne(e=>e.Username == loginRequest.UserName);
var loginResponse=new LoginResponse();
if(account != null)
{
if(account.IsCorrectPassword(loginRequest.Password))
{
var login = Login.Create(account.Id);
loginResponse.Key = login.Key;
loginResponse.ErrorData="";
loginResponse.Success=true;
Logins.Insert(login);
}
else{
loginResponse.ErrorData = $"Username or password incorrect";
loginResponse.Success=false;
}
}else{
loginResponse.ErrorData = $"Username or password incorrect";
loginResponse.Success=false;
}
await ctx.SendJsonAsync(loginResponse);
}
private async Task BackupFileAsync(ServerContext ctx)
{
//key=KEY&device=Id&hash=HASH&mime=
string key;
string device;
string hash;
long deviceId;
if(ctx.QueryParams.TryGetFirst("key",out key) && ctx.QueryParams.TryGetFirst("device",out device) && ctx.QueryParams.TryGetFirst("hash",out hash) && long.TryParse(device,out deviceId))
{
var (account,success,failTxt) = GetAccountFromKey(key);
if(success)
{
UnixPath p = Special.Root / "backups" / account.Id.ToString() / deviceId.ToString() / "files" / hash + ".bin";
await Filesystem.CreateDirectoryAsync(p.Parent);
if(!Filesystem.FileExists(p))
{
using(var strm = Filesystem.Open(p,FileMode.Create,FileAccess.Write,FileShare.None))
{
await ctx.ReadToStreamAsync(strm);
}
}
}
}
}
private async Task GetFileAsync(ServerContext ctx)
{
//key=KEY&device=Id&hash=HASH&mime=
string key;
string device;
string hash;
long deviceId;
string mime;
if(ctx.QueryParams.TryGetFirst("key",out key) && ctx.QueryParams.TryGetFirst("device",out device) && ctx.QueryParams.TryGetFirst("hash",out hash) && long.TryParse(device,out deviceId))
{
if(!ctx.QueryParams.TryGetFirst("mime",out mime))
{
mime="application/octet-stream";
}
var (account,success,failTxt) = GetAccountFromKey(key);
if(success)
{
UnixPath p = Special.Root / "backups" / account.Id.ToString() / deviceId.ToString() / "files" / hash + ".bin";
if(!Filesystem.FileExists(p))
{
ctx.StatusCode = 404;
await ctx.SendTextAsync("The file you requested could not be found","text/plain");
return;
}
using(var strm = Filesystem.Open(p,FileMode.Open,FileAccess.Read,FileShare.Read))
{
await ctx.SendStreamAsync(strm,mime);
}
}
}
}
private async Task GetBackupAsync(ServerContext ctx)
{
var getBackupRequest= await ctx.ReadJsonAsync<EnumerateBackupRequest>();
var (account,success,failTxt) = GetAccountFromKey(getBackupRequest.Key);
var getBackupResponse = new EnumerateBackupResponse();
getBackupResponse.Success = success;
getBackupResponse.ErrorData = failTxt;
if(success)
{
var backup = Backups.FindOne(e=>e.Id == getBackupRequest.BackupId);
if(backup != null)
{
if(backup.UserId != account.Id)
{
getBackupResponse.ErrorData = "This backup is not yours";
getBackupResponse.Success =false;
await ctx.SendJsonAsync(getBackupResponse);
return;
}
getBackupResponse.Creation = backup.Creation;
getBackupResponse.BackupData = backup.Files;
}
else
{
getBackupResponse.ErrorData = "The backup does not exist";
getBackupResponse.Success =false;
}
}
await ctx.SendJsonAsync(getBackupResponse);
}
private async Task CreateBackupAsync(ServerContext ctx)
{
var createBackupRequest= await ctx.ReadJsonAsync<CreateBackupRequest>();
var (account,success,failTxt) = GetAccountFromKey(createBackupRequest.Key);
var createBackupResponse = new CreateBackupResponse();
createBackupResponse.Success = success;
createBackupResponse.ErrorData = failTxt;
if(success)
{
var device = Devices.FindOne(e=>e.Id ==createBackupRequest.DeviceId);
if(device != null)
{
if(device.UserId != account.Id)
{
createBackupResponse.ErrorData = "This device is not yours";
createBackupResponse.Success =false;
await ctx.SendJsonAsync(createBackupResponse);
return;
}
Models.Backup backup=new Models.Backup();
backup.Creation = DateTime.Now;
backup.Files.AddRange(createBackupRequest.BackupData);
backup.UserId = account.Id;
backup.DeviceId = device.Id;
CreateBackupList(account.Id,device.Id,createBackupRequest.BackupData,createBackupResponse.BackupData);
long id= Backups.Insert(backup).AsInt64;
createBackupResponse.BackupId = id;
}else{
createBackupResponse.ErrorData = "No Such Device";
createBackupResponse.Success =false;
}
}
await ctx.SendJsonAsync(createBackupResponse);
}
private void CreateBackupList(long account, long device, List<BackupFileEntry> toBackup, List<BackupFileEntry> filesToBackup)
{
foreach(var entry in toBackup)
{
if(!entry.IsFile)
{
BackupFileEntry dir = new BackupFileEntry();
dir.Created = entry.Created;
dir.Length = 0;
dir.Modified = entry.Modified;
dir.IsFile =false;
dir.Name = entry.Name;
CreateBackupList(account,device,entry.SubEntries,dir.SubEntries);
filesToBackup.Add(dir);
}else{
UnixPath p = Special.Root / "backups" / account.ToString() / device.ToString() / "files" / entry.Hash + ".bin";
if(!Filesystem.FileExists(p))
{
filesToBackup.Add(entry);
}
}
}
}
private async Task CreateUserAsync(ServerContext ctx)
{
var createUserRequest= await ctx.ReadJsonAsync<CreateUserRequest>();
var (account,success,failTxt) = GetAccountFromKey(createUserRequest.Key);
var createUserResponse = new CreateUserResponse();
createUserResponse.Success = success;
createUserResponse.ErrorData = failTxt;
if(success)
{
if(account.IsAdmin)
{
var _account_check = Accounts.FindOne(e=>e.Username == createUserRequest.Username);
if(_account_check != null)
{
createUserResponse.Success = false;
createUserResponse.ErrorData = "User already exists";
}
else
{
if(!string.IsNullOrWhiteSpace(createUserRequest.Password) && createUserRequest.Password.Length >= 8 && createUserRequest.Password == createUserRequest.ConfirmPassword)
{
var newAccount=new Account();
newAccount.Password=createUserRequest.Password;
newAccount.Username = createUserRequest.Username;
newAccount.IsAdmin = createUserRequest.IsAdmin;
Accounts.Insert(newAccount);
}
else
{
createUserResponse.ErrorData = $"Passwords do not match or are too short";
createUserResponse.Success=false;
}
}
}else{
createUserResponse.Success = false;
createUserResponse.ErrorData = "Only admins can create users";
}
}
await ctx.SendJsonAsync(createUserResponse);
}
private async Task ChangePasswordAsync(ServerContext ctx)
{
var loginRequest= await ctx.ReadJsonAsync<ChangePasswordRequest>();
var account = Accounts.FindOne(e=>e.Username == loginRequest.UserName);
var loginResponse=new ChangePasswordResponse();
if(account != null)
{
if(account.IsCorrectPassword(loginRequest.OldPassword))
{
if(!string.IsNullOrWhiteSpace(loginRequest.NewPassword) && loginRequest.NewPassword.Length >= 8 && loginRequest.NewPassword == loginRequest.ConfirmNewPassword)
{
loginResponse.Success=true;
account.Password=loginRequest.NewPassword;
Accounts.Update(account);
}
else
{
loginResponse.ErrorData = $"Passwords do not match or are too short";
loginResponse.Success=false;
}
}
else{
loginResponse.ErrorData = $"Username or password incorrect";
loginResponse.Success=false;
}
}else{
loginResponse.ErrorData = $"Username or password incorrect";
loginResponse.Success=false;
}
await ctx.SendJsonAsync(loginResponse);
}
public void Dispose()
{
db.Dispose();
strm.Dispose();
}
}
}