/* 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 . */ 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 Accounts; private ILiteCollection Logins; private ILiteCollection Backups; private ILiteCollection 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("Accounts"); Logins = db.GetCollection("Logins"); Backups = db.GetCollection("Backups"); Devices = db.GetCollection("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(); 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(); 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(); 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(); 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(); 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(); 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(); 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 toBackup, List 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(); 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(); 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(); } } }