using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using LiteDB; using Tesses.VirtualFilesystem; using Tesses.VirtualFilesystem.Extensions; using Tesses.WebServer; using System.Xml; using System.Text; using System.Web; namespace TessesDedup { public class Dedup { public class HttpDirectoryServer : Server { Dedup dedup; public HttpDirectoryServer(Dedup dedup) { this.dedup=dedup; } public override async Task GetAsync(ServerContext ctx) { if (!ctx.RequestHeaders.TryGetFirst("Authorization", out var value)){ ctx.ResponseHeaders.Add("WWW-Authenticate", "Basic realm=\"Webdav realm\""); ctx.StatusCode = 401; await ctx.SendTextAsync("Unauthorized"); return; } string[] array = value.Split(' '); string[] array2 = Encoding.UTF8.GetString(Convert.FromBase64String(array[1])).Split(new char[1] { ':' }, 2); string usern = array2[0]; Account user=null; bool accessKeyMode=false; if(usern == "$access_key") accessKeyMode=true; else using(var db = dedup.Database) { user = db.Accounts.FindOne(e=>e.Username == usern ); } if(user == null && !accessKeyMode) { ctx.ResponseHeaders.Add("WWW-Authenticate", "Basic realm=\"Webdav realm\""); ctx.StatusCode = 401; await ctx.SendTextAsync("Unauthorized"); return; } if(!accessKeyMode && !user.PasswordCorrect(array2[1])) { ctx.ResponseHeaders.Add("WWW-Authenticate", "Basic realm=\"Webdav realm\""); ctx.StatusCode = 401; await ctx.SendTextAsync("Unauthorized"); return; } if(accessKeyMode) { using(var db = dedup.Database) { string key = array2[1]; var ak = db.AccessKeys.FindOne(e=>e.Key == key); if(ak != null) { user = db.Accounts.FindOne(e=>e.Id == ak.UserId); if(user == null) { ctx.ResponseHeaders.Add("WWW-Authenticate", "Basic realm=\"Webdav realm\""); ctx.StatusCode = 401; await ctx.SendTextAsync("Unauthorized"); return; } } else { ctx.ResponseHeaders.Add("WWW-Authenticate", "Basic realm=\"Webdav realm\""); ctx.StatusCode = 401; await ctx.SendTextAsync("Unauthorized"); return; } } } StringBuilder builder=new StringBuilder($"
../\n"); bool isDir=true; void Insert(string path,bool isDir=true) { builder.Append($"{HttpUtility.HtmlEncode(Path.GetFileName(path.TrimEnd('/')))}{(isDir?"/":"")}\n"); } if(ctx.UrlPath == "/") { ListdeviceNames = new List (); using(var db = dedup.Database) foreach(var item in db.Backups.Find(e=>e.AccountId == user.Id)) { if(!deviceNames.Contains(item.DeviceName)) deviceNames.Add(item.DeviceName); } foreach(var dev in deviceNames) { Insert($"{dev}/"); } } else { string[] path = ctx.UrlPath.Split(new char[]{'/'},StringSplitOptions.RemoveEmptyEntries); if(path.Length == 1) { List backups = new List (); using(var db = dedup.Database) foreach(var item in db.Backups.Find(e=>e.AccountId == user.Id)) { string backupName = $"{item.Tag} ({item.CreationDate.ToString("yyyyMMdd_HHmmss")})"; if(item.DeviceName == path[0] && !backups.Contains(backupName)) { backups.Add(backupName); } } foreach(var dev in backups) { Insert($"{dev}/"); } } else { var name = HttpUtility.UrlDecode(path[1]); using(var db = dedup.Database) foreach(var item in db.Backups.Find(e=>e.AccountId == user.Id)) { string backupName = $"{item.Tag} ({item.CreationDate.ToString("yyyyMMdd_HHmmss")})"; if(item.DeviceName == path[0] && name == backupName) { UnixPath upath = Special.Root; for(int i = 2;i
"); await ctx.SendTextAsync(builder.ToString()); } } } public IServer Server {get;} public DedupStorage Storage {get;} public DatabaseHandler Database => new DatabaseHandler(_db()); private Func_db; public class DatabaseHandler : IDisposable { public ILiteCollection Backups => Database.GetCollection ("backups"); public ILiteCollection Accounts => Database.GetCollection ("accounts"); public ILiteCollection AccessKeys=>Database.GetCollection ("accesskeys"); public ILiteDatabase Database {get;} public DatabaseHandler(ILiteDatabase db) { Database = db; } public void Dispose() { Database.Dispose(); } } RouteServer routeServer; IVirtualFilesystem fs; public Dedup(IServer www,IVirtualFilesystem storage,Func db) { _db = db; fs = storage; Storage = new DedupStorage(storage); MountableServer mnt=new MountableServer(www); HttpDirectoryServer webdav=new HttpDirectoryServer(this); mnt.Mount("/data/",webdav); routeServer=new RouteServer(mnt); Server = routeServer; routeServer.Add("/api/v1/Block",HasBlockAsync,"HEAD"); routeServer.Add("/api/v1/Block",PutBlockAsync,"PUT"); routeServer.Add("/api/v1/Backup",GetBackupAsync,"GET"); routeServer.Add("/api/v1/Backup",PutBackupAsync,"PUT"); routeServer.Add("/api/v1/Login",LoginAsync,"POST"); routeServer.Add("/api/v1/Logout",LogoutAsync,"POST"); routeServer.Add("/api/v1/Download",DownloadAsync,"GET"); routeServer.Add("/api/v1/Download",DownloadAsync,"HEAD"); routeServer.Add("/api/v1/AccessKey",AccessKeyAsync,"GET"); routeServer.Add("/api/v1/AccessKey",AccessKeyDeleteAsync,"DELETE"); routeServer.Add("/api/v1/Registered",RegisteredAsync,"GET"); routeServer.Add("/api/v1/Stats",StatsAsync,"GET"); } private async Task AccessKeyDeleteAsync(ServerContext ctx) { if(await Unauthenticated(ctx)) return; string key = GetAuthorizationKey(ctx); using(var db = Database){ var ak=db.AccessKeys.FindOne(e=>e.Key == key); if(ctx.QueryParams.TryGetFirstInt64("id",out var id)) { var item=db.AccessKeys.FindById(id); if(item != null && item.UserId == ak.UserId) { if(item.Id != ak.Id) { db.AccessKeys.Delete(id); await ctx.SendJsonAsync(new{success=true,reason="Success, it got deleted"}); } else { await ctx.SendJsonAsync(new{success=false,reason="Cannot delete this accesskey, must use Logout"}); } } else { await ctx.SendJsonAsync(new{success=false,reason="Either accesskey does not exist or it is not yours"}); } } } } private async Task AccessKeyAsync(ServerContext ctx) { if(await Unauthenticated(ctx)) return; string key = GetAuthorizationKey(ctx); using(var db = Database) { var ak=db.AccessKeys.FindOne(e=>e.Key == key); await ctx.SendJsonAsync(db.AccessKeys.Find(e=>e.UserId == ak.UserId && e.Id != ak.Id).ToList()); } } private string BytesToUnited(long sz) { if(sz == 1) return "1 byte"; if(sz < 1024) return $"{sz} bytes"; if(sz < Math.Pow(1024,2)) return $"{sz / 1024} kiB"; if(sz < Math.Pow(1024,3)) return $"{sz / (int)Math.Pow(1024,2)} MiB"; if(sz < Math.Pow(1024,4)) return $"{sz / (long)Math.Pow(1024,3)} GiB"; return $"{sz / (long)Math.Pow(1024,4)} TiB"; } private async Task StatsAsync(ServerContext ctx) { if(await Unauthenticated(ctx)) return; long blocks = 0; await foreach(var dir in fs.EnumerateDirectoriesAsync(Special.Root)) { await foreach(var dir2 in fs.EnumerateDirectoriesAsync(dir)) { await foreach(var file in fs.EnumerateFilesAsync(dir2)) { blocks++; } } } long bytes = blocks*DedupStorage.BlockLength; using(var db = Database) await ctx.SendJsonAsync(new{blocks,bytes,backups=db.Backups.Count(),label=BytesToUnited(bytes)}); } public bool Registered() { using(var db = Database) return db.Accounts.FindById(1) != null && !fs.FileExists(Special.Root/"ResetPassword.txt"); } private async Task RegisteredAsync(ServerContext ctx) { await ctx.SendJsonAsync(Registered()); } private async Task DownloadAsync(ServerContext ctx) { if(await Unauthenticated(ctx)) return; if(ctx.QueryParams.TryGetFirstInt64("id",out var id) && ctx.QueryParams.TryGetFirst("path",out var path)) { UnixPath upath = path; Backup bkp; using(var db = Database) bkp=db.Backups.FindById(id); if(bkp != null) { try { var ent=bkp.GetEntryFromPath(upath); ctx.WithFileName(ent.Name,true); await ctx.SendStreamAsync(Storage.OpenRead(ent),HeyRed.Mime.MimeTypesMap.GetMimeType(ent.Name)); }catch(DirectoryNotFoundException) { ctx.StatusCode = 404; await ctx.SendTextAsync($"Could not find directory {upath.Parent.Path}\r\n","text/plain"); } catch (FileNotFoundException) { ctx.StatusCode = 404; await ctx.SendTextAsync($"Could not find file {upath.Path}\r\n","text/plain"); } } else { ctx.StatusCode=404; await ctx.SendTextAsync("Backup not found\r\n"); } } } private async Task LogoutAsync(ServerContext ctx) { if(!ctx.RequestHeaders.ContainsKey("Content-Type")) ctx.RequestHeaders.Add("Content-Type","application/x-www-form-urlencoded"); ctx.ParseBody(); var key = GetAuthorizationKey(ctx); if(!string.IsNullOrWhiteSpace(key)) { using(var db = Database){ var item = db.AccessKeys.FindOne(e=>e.Key ==key); if(item != null) db.AccessKeys.Delete(item.Id); } } ctx.StatusCode = 204; await ctx.WriteHeadersAsync(); } private async Task LoginAsync(ServerContext ctx) { if(!ctx.RequestHeaders.ContainsKey("Content-Type")) ctx.RequestHeaders.Add("Content-Type","application/x-www-form-urlencoded"); ctx.ParseBody(); if(ctx.QueryParams.TryGetFirst("username",out var username) && ctx.QueryParams.TryGetFirst("password",out var password) && ctx.QueryParams.TryGetFirst("device_name",out var device_name)) { if(Registered()) { Account user; using(var db = Database) user=db.Accounts.FindOne(e=>e.Username == username); if(user != null) { if(user.PasswordCorrect(password)) { AccessKey ak = new AccessKey(); ak.NewKey(); ak.Id = 0; ak.UserId = user.Id; ak.DeviceName = device_name; ak.CreationDate=DateTime.Now; using(var db = Database) db.AccessKeys.Insert(ak); await ctx.SendJsonAsync(new{success=true, key=ak.Key}); } else { await ctx.SendJsonAsync(new {success=false}); } } else { await ctx.SendJsonAsync(new {success=false}); } } else { if(await fs.FileExistsAsync(Special.Root/"ResetPassword.txt")) await fs.DeleteFileAsync(Special.Root/"ResetPassword.txt"); Account user; using(var db = Database) user = db.Accounts.FindById(1); if(user != null) { user.Username = username; user.NewSalt(); user.PasswordHash=user.GetPasswordHash(password); user.Username = username; using(var db = Database) db.Accounts.Update(user); } else { Account acnt = new Account(); acnt.Id = 1; acnt.NewSalt(); acnt.PasswordHash=acnt.GetPasswordHash(password); acnt.Username = username; using(var db = Database) db.Accounts.Insert(acnt); } AccessKey ak = new AccessKey(); ak.NewKey(); ak.Id = 0; ak.UserId = 1; ak.DeviceName = device_name; using(var db = Database) db.AccessKeys.Insert(ak); await ctx.SendJsonAsync(new{success=true, key=ak.Key}); } } } private async Task PutBackupAsync(ServerContext ctx) { if(await Unauthenticated(ctx)) return; var item = await ctx.ReadJsonAsync (); item.Id = 0; string key = GetAuthorizationKey(ctx); AccessKey ak; using(var db = Database) ak=db.AccessKeys.FindOne(e=>e.Key == key); item.AccountId = ak.UserId; item.DeviceName = ak.DeviceName; using(var db = Database) db.Backups.Insert(item); ctx.StatusCode=204; await ctx.WriteHeadersAsync(); } private string GetAuthorizationKey(ServerContext ctx) { string key=""; if(ctx.RequestHeaders.TryGetFirst("Authorization",out var auth)) { if(auth.StartsWith("Bearer ")) { key = auth.Substring(7); } } if(ctx.RequestHeaders.TryGetFirst("X-Access-Key",out var ak)) { key = ak; } if(ctx.QueryParams.TryGetFirst("access_key",out var access_key)) { key=access_key; } return key; } private long GetAccountId(ServerContext ctx) { string key = GetAuthorizationKey(ctx); using(var db = Database) { var ak=db.AccessKeys.FindOne(e=>e.Key == key); if(ak != null) return ak.UserId; } return -1; } private async Task GetBackupAsync(ServerContext ctx) { if(await Unauthenticated(ctx)) return; bool noHashes=ctx.QueryParams.GetFirstBoolean("noHashes"); if(ctx.QueryParams.TryGetFirstInt64("id",out var id)) { Backup item; using(var db = Database) item = db.Backups.FindById(id); if(item != null) await ctx.SendJsonAsync(noHashes ? item.WithoutHashes() : item); else { ctx.StatusCode=404; await ctx.SendNotFoundAsync(); } } else { List backups=new List (); using(var db = Database) foreach(var item in db.Backups.FindAll()) { backups.Add(item.WithoutRoot()); } await ctx.SendJsonAsync(backups); } } private async Task PutBlockAsync(ServerContext ctx) { if(await Unauthenticated(ctx)) return; var vals=await ctx.ReadBytesAsync(); await ctx.SendTextAsync(Storage.WriteBlock(vals),"text/plain"); } private async Task HasBlockAsync(ServerContext ctx) { if(await Unauthenticated(ctx)) return; if(ctx.QueryParams.TryGetFirst("hash",out var hash)) { ctx.StatusCode = Storage.HasBlock(hash) ? 200 : 404; await ctx.WriteHeadersAsync(); } } private async Task Unauthenticated(ServerContext ctx) { if(GetAccountId(ctx) == -1) { ctx.StatusCode = 401; await ctx.SendTextAsync("Unauthorized use /api/v1/Login to Authenticate\r\nand use access_key query param or Authorization: Bearer YOURKEY on this endpoint\r\n","text/plain"); return true; } return false; } } public class DedupStorage { private class DedupStream : Stream { private DedupStorage dedupStorage; private FilesystemEntry fsE; public DedupStream(DedupStorage dedupStorage, FilesystemEntry fsE) { this.dedupStorage = dedupStorage; this.fsE = fsE; } public override bool CanRead => true; public override bool CanSeek => true; public override bool CanWrite => false; public override long Length => fsE.Length; public override long Position { get;set; } public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { int total_read = Math.Min(count,buffer.Length-offset); long end = Position + total_read; if(end > Length) {end = Length; total_read = (int)(Length-Position);} int read = 0; while(Position < end) { //get current hash for Position int currentFile = (int)(Position / BlockLength); //get current offset (in file) for Position int fileOffset = (int)(Position % BlockLength); int toReadFromFile = BlockLength - fileOffset; toReadFromFile = Math.Min(total_read-read,toReadFromFile); var blk=dedupStorage.ReadBlock(fsE.Hashes[currentFile]); Array.Copy(blk,fileOffset,buffer,offset+read,toReadFromFile); read+=toReadFromFile; Position+=toReadFromFile; } return read; } public override long Seek(long offset, SeekOrigin origin) { switch(origin) { case SeekOrigin.Begin: Position = offset; break; case SeekOrigin.Current: Position += offset; break; case SeekOrigin.End: Position = Length - offset; break; } return Position; } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } } public Stream OpenRead(FilesystemEntry fsE) { return new DedupStream(this,fsE); } public DedupStorage(IVirtualFilesystem fs) { this.vfs = fs; } public const int BlockReadSize = 1024; public const int BlockReads=4096; public const int BlockLength = BlockReadSize*BlockReads; IVirtualFilesystem vfs; public UnixPath GetHashPath(string hash) { if(hash.Length != 128) return Special.Root; string hashSlice1 = hash.Substring(0,2); string hashSlice2 = hash.Substring(2,2); string hashRest = hash.Substring(4,124); return Special.Root / hashSlice1 / hashSlice2 / hashRest; } Mutex mtx=new Mutex(); public bool HasBlock(string hash) { if(hash.Length != 128) return false; return vfs.FileExists(GetHashPath(hash)); } internal static string Sha512Hash(byte[] data) { using(var sha512=SHA512.Create()) { sha512.Initialize(); byte[] hash=sha512.ComputeHash(data); return BitConverter.ToString(hash).ToLower().Replace("-",""); } } public string WriteBlock(byte[] data) { if(data.Length != BlockLength) mtx.WaitOne(); string hash=Sha512Hash(data); if(!HasBlock(hash)) { string hashSlice1 = hash.Substring(0,2); string hashSlice2 = hash.Substring(2,2); vfs.CreateDirectory(Special.Root / hashSlice1); vfs.CreateDirectory(Special.Root / hashSlice1 / hashSlice2); vfs.WriteAllBytes(GetHashPath(hash),data); } mtx.ReleaseMutex(); return hash; } public byte[] ReadBlock(string hash) { if(HasBlock(hash)) { return vfs.ReadAllBytes(GetHashPath(hash)); } return new byte[0]; } } }