using System; using System.Collections.Generic; using System.Linq; using System.Text; using HttpServer.MVC; using HttpServer.HttpModules; using System.IO; using Duplicati.Server.Serialization; namespace Duplicati.Server { public class WebServer { /// /// Option for changing the webroot folder /// public const string OPTION_WEBROOT = "webservice-webroot"; /// /// Option for changing the webservice listen port /// public const string OPTION_PORT = "webservice-port"; /// /// The single webserver instance /// private HttpServer.HttpServer m_server; /// /// The webserver listening port /// public readonly int Port; /// /// Sets up the webserver and starts it /// /// A set of options public WebServer(IDictionary options) { int port; string portstring; IEnumerable ports = null; options.TryGetValue(OPTION_PORT, out portstring); if (!string.IsNullOrEmpty(portstring)) ports = from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) where int.TryParse(n, out port) select int.Parse(n); if (ports == null || !ports.Any()) ports = new int[] { 8080 }; // If we are in hosted mode with no specified port, // then try different ports Exception e = null; foreach(var p in ports) try { // Due to the way the server is initialized, // we cannot try to start it again on another port, // so we create a new server for each attempt var server = CreateServer(options); server.Start(System.Net.IPAddress.Any, p); m_server = server; this.Port = p; return; } catch (System.Net.Sockets.SocketException ex) { } throw new Exception("Unable to open a socket for listening, tried ports: " + string.Join(",", from n in ports select n.ToString())); } private static HttpServer.HttpServer CreateServer(IDictionary options) { HttpServer.HttpServer server = new HttpServer.HttpServer(); server.Add(new DynamicHandler()); string webroot = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); #if DEBUG //For debug we go "../../../.." to get out of "GUI/Duplicati.GUI.TrayIcon/bin/debug" string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", "..")); tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Server"); if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) webroot = tmpwebroot; else { //If we are running the server standalone, we only need to exit "bin/Debug" tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..")); if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) webroot = tmpwebroot; } if (Library.Utility.Utility.IsClientOSX) { string osxTmpWebRoot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", "..", "..", "..", "..")); osxTmpWebRoot = System.IO.Path.Combine(osxTmpWebRoot, "Server"); if (System.IO.Directory.Exists(System.IO.Path.Combine(osxTmpWebRoot, "webroot"))) webroot = osxTmpWebRoot; } #endif webroot = System.IO.Path.Combine(webroot, "webroot"); if (options.ContainsKey(OPTION_WEBROOT)) { string userroot = options[OPTION_WEBROOT]; #if DEBUG //In debug mode we do not care where the path points #else //In release mode we check that the user supplied path is located // in the same folders as the running application, to avoid users // that inadvertently expose top level folders if (!string.IsNullOrWhiteSpace(userroot) && ( userroot.StartsWith(Library.Utility.Utility.AppendDirSeparator(System.Reflection.Assembly.GetExecutingAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparision) || userroot.StartsWith(Library.Utility.Utility.AppendDirSeparator(Program.StartupPath), Library.Utility.Utility.ClientFilenameStringComparision) ) ) #endif { webroot = userroot; } } FileModule fh = new FileModule("/", webroot); fh.AddDefaultMimeTypes(); fh.MimeTypes.Add("htc", "text/x-component"); fh.MimeTypes.Add("json", "application/json"); fh.MimeTypes.Add("map", "application/json"); server.Add(fh); server.Add(new IndexHtmlHandler(System.IO.Path.Combine(webroot, "index.html"))); #if DEBUG //For debugging, it is nice to know when we get a 404 server.Add(new DebugReportHandler()); #endif return server; } private class BodyWriter : System.IO.StreamWriter, IDisposable { private HttpServer.IHttpResponse m_resp; // We override the format provider so all JSON output uses US format public override IFormatProvider FormatProvider { get { return System.Globalization.CultureInfo.InvariantCulture; } } public BodyWriter(HttpServer.IHttpResponse resp) : base(resp.Body, resp.Encoding) { m_resp = resp; } protected override void Dispose (bool disposing) { if (!m_resp.HeadersSent) { base.Flush(); m_resp.ContentLength = base.BaseStream.Length; m_resp.Send(); } base.Dispose(disposing); } } private class IndexHtmlHandler : HttpModule { private string m_defaultdoc; public IndexHtmlHandler(string defaultdoc) { m_defaultdoc = defaultdoc; } public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) { if ((request.Uri.AbsolutePath == "/" || request.Uri.AbsolutePath == "/index.html" || request.Uri.AbsolutePath == "/index.htm") && System.IO.File.Exists(m_defaultdoc)) { response.Status = System.Net.HttpStatusCode.OK; response.Reason = "OK"; response.ContentType = "text/html"; using (var fs = System.IO.File.OpenRead(m_defaultdoc)) { response.ContentLength = fs.Length; response.Body = fs; response.Send(); } return true; } return false; } } private class DebugReportHandler : HttpModule { public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) { System.Diagnostics.Trace.WriteLine(string.Format("Rejecting request for {0}", request.Uri)); return false; } } private class DynamicHandler : HttpModule { private delegate void ProcessSub(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter writer); private readonly Dictionary SUPPORTED_METHODS; public DynamicHandler() { SUPPORTED_METHODS = new Dictionary(System.StringComparer.InvariantCultureIgnoreCase); //Make a list of all supported actions SUPPORTED_METHODS.Add("supported-actions", ListSupportedActions); SUPPORTED_METHODS.Add("system-info", ListSystemInfo); SUPPORTED_METHODS.Add("list-backups", ListBackups); SUPPORTED_METHODS.Add("get-current-state", GetCurrentState); SUPPORTED_METHODS.Add("get-progress-state", GetProgressState); SUPPORTED_METHODS.Add("list-application-settings", ListApplicationSettings); SUPPORTED_METHODS.Add("list-options", ListCoreOptions); SUPPORTED_METHODS.Add("send-command", SendCommand); SUPPORTED_METHODS.Add("get-backup-defaults", GetBackupDefaults); SUPPORTED_METHODS.Add("get-folder-contents", GetFolderContents); SUPPORTED_METHODS.Add("get-backup", GetBackup); SUPPORTED_METHODS.Add("add-backup", AddBackup); SUPPORTED_METHODS.Add("update-backup", UpdateBackup); SUPPORTED_METHODS.Add("delete-backup", DeleteBackup); SUPPORTED_METHODS.Add("validate-path", ValidatePath); SUPPORTED_METHODS.Add("list-tags", ListTags); SUPPORTED_METHODS.Add("test-backend", TestBackend); } public override bool Process (HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) { //We use the fake entry point /control.cgi to listen for requests //This ensures that the rest of the webserver can just serve plain files if (!request.Uri.AbsolutePath.Equals("/control.cgi", StringComparison.InvariantCultureIgnoreCase)) return false; HttpServer.HttpInput input = request.Method.ToUpper() == "POST" ? request.Form : request.QueryString; string action = input["action"].Value ?? ""; //Lookup the actual handler method ProcessSub method; SUPPORTED_METHODS.TryGetValue(action, out method); if (method == null) { response.Status = System.Net.HttpStatusCode.NotImplemented; response.Reason = "Unsupported action: " + (action == null ? "" : ""); response.Send(); } else { //Default setup response.Status = System.Net.HttpStatusCode.OK; response.Reason = "OK"; #if DEBUG response.ContentType = "text/plain"; #else response.ContentType = "text/json"; #endif using (BodyWriter bw = new BodyWriter(response)) { try { method(request, response, session, bw); } catch (Exception ex) { if (!response.HeadersSent) { response.Status = System.Net.HttpStatusCode.InternalServerError; response.Reason = ex.Message; response.ContentType = "text/plain"; bw.WriteLine("Internal error"); #if DEBUG bw.Write("Stacktrace: " + ex.ToString()); #endif bw.Flush(); } } } } return true; } private void ReportError(HttpServer.IHttpResponse response, BodyWriter bw, string message) { response.Status = System.Net.HttpStatusCode.InternalServerError; response.Reason = message; OutputObject(bw, new { Error = message }); } private void OutputObject (BodyWriter b, object o) { Serializer.SerializeJson(b, o); } private void TestBackend(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { HttpServer.HttpInput input = request.Method.ToUpper() == "POST" ? request.Form : request.QueryString; if (input["url"] == null || input["url"].Value == null) { ReportError(response, bw, "The path parameter was not set"); return; } try { using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(input["url"].Value, new Dictionary())) b.Test(); OutputObject(bw, new { Status = "OK" }); } catch (Exception ex) { ReportError(response, bw, ex.Message); } } private void ListSystemInfo(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { OutputObject(bw, new { APIVersion = 1, ServerVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), ServerVersionName = License.VersionNumbers.Version, ServerTime = DateTime.Now, OSType = Library.Utility.Utility.IsClientLinux ? (Library.Utility.Utility.IsClientOSX ? "OSX" : "Linux") : "Windows", DirectorySeparator = System.IO.Path.DirectorySeparatorChar, PathSeparator = System.IO.Path.PathSeparator, CaseSensitiveFilesystem = Duplicati.Library.Utility.Utility.IsFSCaseSensitive, MonoVersion = Duplicati.Library.Utility.Utility.IsMono ? Duplicati.Library.Utility.Utility.MonoVersion.ToString() : null, MachineName = System.Environment.MachineName, NewLine = System.Environment.NewLine, CLRVersion = System.Environment.Version.ToString(), CLROSInfo = new { Platform = System.Environment.OSVersion.Platform.ToString(), ServicePack = System.Environment.OSVersion.ServicePack, Version = System.Environment.OSVersion.Version.ToString(), VersionString = System.Environment.OSVersion.VersionString }, Options = Serializable.ServerSettings.Options, CompressionModules = Serializable.ServerSettings.CompressionModules, EncryptionModules = Serializable.ServerSettings.EncryptionModules, BackendModules = Serializable.ServerSettings.BackendModules, GenericModules = Serializable.ServerSettings.GenericModules, WebModules = Serializable.ServerSettings.WebModules }); } private void ListSupportedActions(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { OutputObject(bw, new { Version = 1, Methods = SUPPORTED_METHODS.Keys }); } private void ListBackups (HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { var schedules = Program.DataConnection.Schedules; var backups = Program.DataConnection.Backups; var all = from n in backups select new AddOrUpdateBackupData() { Backup = (Database.Backup)n, Schedule = (from x in schedules where x.Tags != null && x.Tags.Contains("ID=" + n.ID) select (Database.Schedule)x).FirstOrDefault() }; OutputObject(bw, all.ToArray()); } private void ListTags(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { var r = from n in Serializable.ServerSettings.CompressionModules .Union(Serializable.ServerSettings.EncryptionModules) .Union(Serializable.ServerSettings.BackendModules) .Union(Serializable.ServerSettings.GenericModules) select n.Key.ToLower(); // Append all known tags r = r.Union(from n in Program.DataConnection.Backups select n.Tags into p from x in p select x.ToLower()); OutputObject(bw, r); } private void ValidatePath(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { HttpServer.HttpInput input = request.Method.ToUpper() == "POST" ? request.Form : request.QueryString; if (input["path"] == null || input["path"].Value == null) { ReportError(response, bw, "The path parameter was not set"); return; } string path = input["path"].Value; if (path.StartsWith("%") && path.EndsWith("%")) { if (SpecialFolders.Nodes.Any(x => x.id == path)) OutputObject(bw, null); } if (!path.StartsWith("/")) { ReportError(response, bw, "The path parameter must start with a forward-slash"); return; } try { if (System.IO.Path.IsPathRooted(path) && System.IO.Directory.Exists(path)) { OutputObject(bw, null); return; } } catch { } ReportError(response, bw, "File or folder not found"); return; } private void GetFolderContents(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { HttpServer.HttpInput input = request.Method.ToUpper() == "POST" ? request.Form : request.QueryString; if(input["path"] == null || input["path"].Value == null) { ReportError(response, bw, "The path parameter was not set"); return; } bool skipFiles = Library.Utility.Utility.ParseBool(input["onlyfolders"].Value, false); string path = input["path"].Value; if (!path.StartsWith("/")) { ReportError(response, bw, "The path parameter must start with a forward-slash"); return; } try { if (!path.EndsWith("/")) path += '/'; IEnumerable res; if (!Library.Utility.Utility.IsClientLinux && path.Equals("/")) { res = from di in System.IO.DriveInfo.GetDrives() where di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable select new Serializable.TreeNode() { id = "/" + di.RootDirectory.FullName.Replace('\\', '/'), text = di.RootDirectory.FullName.Replace('\\', ' ') + "(" + di.DriveType + ")", iconCls = "x-tree-icon-drive" }; } else { //Helper function for finding out if a folder has sub elements Func hasSubElements = (p) => skipFiles ? Directory.EnumerateDirectories(p).Any() : Directory.EnumerateFileSystemEntries(p).Any(); //Helper function for dealing with exceptions when accessing off-limits folders Func isEmptyFolder = (p) => { try { return !hasSubElements(p); } catch { } return true; }; //Helper function for dealing with exceptions when accessing off-limits folders Func canAccess = (p) => { try { hasSubElements(p); return true; } catch { } return false; }; res = from s in System.IO.Directory.EnumerateFileSystemEntries(Library.Utility.Utility.IsClientLinux ? path : path.Substring(1).Replace('/', '\\')) let attr = System.IO.File.GetAttributes(s) let isSymlink = (attr & FileAttributes.ReparsePoint) != 0 let isFolder = (attr & FileAttributes.Directory) != 0 let isFile = !isFolder let isHidden = (attr & FileAttributes.Hidden) != 0 let accesible = isFile || canAccess(s) let isLeaf = isFile || !accesible || isEmptyFolder(s) let rawid = isFolder ? Library.Utility.Utility.AppendDirSeparator(s) : s where !skipFiles || isFolder select new Serializable.TreeNode() { id = Library.Utility.Utility.IsClientLinux ? rawid : "/" + rawid.Replace('\\', '/'), text = System.IO.Path.GetFileName(s), iconCls = isFolder ? (accesible ? "x-tree-icon-parent" : "x-tree-icon-locked") : "x-tree-icon-leaf", leaf = isLeaf }; } if (path.Equals("/")) { // Prepend special folders res = SpecialFolders.Nodes.Union(res); } OutputObject(bw, res); } catch (Exception ex) { ReportError(response, bw, "Failed to process the path: " + ex.Message); } } private bool LongPollCheck(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, BodyWriter bw, EventPollNotify poller, ref long id, out bool isError) { HttpServer.HttpInput input = request.Method.ToUpper() == "POST" ? request.Form : request.QueryString; if (Library.Utility.Utility.ParseBool(input["longpoll"].Value, false)) { long lastEventId; if (!long.TryParse(input["lasteventid"].Value, out lastEventId)) { ReportError(response, bw, "When activating long poll, the request must include the last event id"); isError = true; return false; } TimeSpan ts; try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); } catch (Exception ex) { ReportError(response, bw, "Invalid duration: " + ex.Message); isError = true; return false; } if (ts <= TimeSpan.FromSeconds(10) || ts.TotalMilliseconds > int.MaxValue) { ReportError(response, bw, "Invalid duration, must be at least 10 seconds, and less than " + int.MaxValue + " milliseconds"); isError = true; return false; } isError = false; id = poller.Wait(lastEventId, (int)ts.TotalMilliseconds); return true; } isError = false; return false; } private void GetProgressState(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { if (Program.GenerateProgressState == null) { ReportError(response, bw, "No active backup"); } else { var ev = Program.GenerateProgressState(); OutputObject(bw, ev); } } private void GetCurrentState (HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { bool isError; long id = 0; if (LongPollCheck(request, response, bw, Program.StatusEventNotifyer, ref id, out isError)) { //Make sure we do not report a higher number than the eventnotifyer says var st = new Serializable.ServerStatus(); st.LastEventID = id; OutputObject(bw, st); } else if (!isError) { OutputObject(bw, new Serializable.ServerStatus()); } } private void ListCoreOptions(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { OutputObject(bw, new Duplicati.Library.Main.Options(new Dictionary()).SupportedCommands); } private void ListApplicationSettings(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { OutputObject(bw, Program.DataConnection.ApplicationSettings); } private static void MergeJsonObjects(Newtonsoft.Json.Linq.JObject self, Newtonsoft.Json.Linq.JObject other) { foreach(var p in other.Properties()) { var sp = self.Property(p.Name); if (sp == null) self.Add(p); else { switch (p.Type) { // Primitives override case Newtonsoft.Json.Linq.JTokenType.Boolean: case Newtonsoft.Json.Linq.JTokenType.Bytes: case Newtonsoft.Json.Linq.JTokenType.Comment: case Newtonsoft.Json.Linq.JTokenType.Constructor: case Newtonsoft.Json.Linq.JTokenType.Date: case Newtonsoft.Json.Linq.JTokenType.Float: case Newtonsoft.Json.Linq.JTokenType.Guid: case Newtonsoft.Json.Linq.JTokenType.Integer: case Newtonsoft.Json.Linq.JTokenType.String: case Newtonsoft.Json.Linq.JTokenType.TimeSpan: case Newtonsoft.Json.Linq.JTokenType.Uri: case Newtonsoft.Json.Linq.JTokenType.None: case Newtonsoft.Json.Linq.JTokenType.Null: case Newtonsoft.Json.Linq.JTokenType.Undefined: self.Replace(p); break; // Arrays merge case Newtonsoft.Json.Linq.JTokenType.Array: if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Array) sp.Value = new Newtonsoft.Json.Linq.JArray(((Newtonsoft.Json.Linq.JArray)sp.Value).Union((Newtonsoft.Json.Linq.JArray)p.Value)); else { var a = new Newtonsoft.Json.Linq.JArray(sp.Value); sp.Value = new Newtonsoft.Json.Linq.JArray(a.Union((Newtonsoft.Json.Linq.JArray)p.Value)); } break; // Objects merge case Newtonsoft.Json.Linq.JTokenType.Object: if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Object) MergeJsonObjects((Newtonsoft.Json.Linq.JObject)sp.Value, (Newtonsoft.Json.Linq.JObject)p.Value); else sp.Value = p.Value; break; // Ignore other stuff default: break; } } } } private void GetBackupDefaults(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { // Start with a scratch object var o = new Newtonsoft.Json.Linq.JObject(); // Add application wide settings o.Add("ApplicationOptions", new Newtonsoft.Json.Linq.JArray(Program.DataConnection.Settings)); try { // Add built-in defaults Newtonsoft.Json.Linq.JObject n; using(var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + ".newbackup.json"))) n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd()); MergeJsonObjects(o, n); } catch { } try { // Add install defaults/overrides, if present var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "newbackup.json"); if (System.IO.File.Exists(path)) { Newtonsoft.Json.Linq.JObject n; n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(path)); MergeJsonObjects(o, n); } } catch { } OutputObject(bw, new { success = true, data = o }); } private void GetBackup(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { HttpServer.HttpInput input = request.Method.ToUpper() == "POST" ? request.Form : request.QueryString; long id; if (!long.TryParse(input["id"].Value, out id)) ReportError(response, bw, "Invalid or missing backup id"); else { var bk = Program.DataConnection.GetBackup(id); if (bk == null) ReportError(response, bw, "Invalid or missing backup id"); else { var scheduleId = Program.DataConnection.GetScheduleIDsFromTags(new string[] { "ID=" + id }); var schedule = scheduleId.Any() ? Program.DataConnection.GetSchedule(scheduleId.First()) : null; var sourcenames = bk.Sources.Distinct().Select(x => { var sp = SpecialFolders.TranslateToDisplayString(x); if (sp != null) return new KeyValuePair(x, sp); try { var n = System.IO.Path.GetFileName(x); if (!string.IsNullOrWhiteSpace(n)) return new KeyValuePair(x, System.IO.Path.GetFileName(n)); } catch { } if (x.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString()) && x.Length > 1) return new KeyValuePair(x, x.Substring(0, x.Length - 1)); else return new KeyValuePair(x, x); }).ToDictionary(x => x.Key, x => x.Value); OutputObject(bw, new { success = true, data = new { Schedule = schedule, Backup = bk, DisplayNames = sourcenames } }); } } } private void UpdateBackup(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { string str = request.Form["data"].Value; if (string.IsNullOrWhiteSpace(str)) { ReportError(response, bw, "Missing backup object"); return; } AddOrUpdateBackupData data = null; try { data = Serializer.Deserialize(new StringReader(str)); if (data.Backup == null) { ReportError(response, bw, "Data object had no backup entry"); return; } if (data.Backup.ID <= 0) { ReportError(response, bw, "Invalid or missing backup id"); return; } lock(Program.DataConnection.m_lock) { var backup = Program.DataConnection.GetBackup(data.Backup.ID); if (backup == null) { ReportError(response, bw, "Invalid or missing backup id"); return; } if (Program.DataConnection.Backups.Where(x => x.Name.Equals(data.Backup.Name, StringComparison.InvariantCultureIgnoreCase) && x.ID != data.Backup.ID).Any()) { ReportError(response, bw, "There already exists a backup with the name: " + data.Backup.Name); return; } Program.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); } OutputObject(bw, new { status = "OK" }); } catch (Exception ex) { if (data == null) ReportError(response, bw, string.Format("Unable to parse backup or schedule object: {0}", ex.Message)); else ReportError(response, bw, string.Format("Unable to save backup or schedule: {0}", ex.Message)); } } private class AddOrUpdateBackupData { public Database.Schedule Schedule {get; set;} public Database.Backup Backup {get; set;} } private void AddBackup(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { string str = request.Form["data"].Value; if (string.IsNullOrWhiteSpace(str)) { ReportError(response, bw, "Missing backup object"); return; } AddOrUpdateBackupData data = null; try { data = Serializer.Deserialize(new StringReader(str)); if (data.Backup == null) { ReportError(response, bw, "Data object had no backup entry"); return; } data.Backup.ID = -1; lock(Program.DataConnection.m_lock) { if (Program.DataConnection.Backups.Where(x => x.Name.Equals(data.Backup.Name, StringComparison.InvariantCultureIgnoreCase)).Any()) { ReportError(response, bw, "There already exists a backup with the name: " + data.Backup.Name); return; } Program.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); } OutputObject(bw, new { status = "OK" }); } catch (Exception ex) { if (data == null) ReportError(response, bw, string.Format("Unable to parse backup or schedule object: {0}", ex.Message)); else ReportError(response, bw, string.Format("Unable to save schedule or backup object: {0}", ex.Message)); } } private void DeleteBackup(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { HttpServer.HttpInput input = request.Method.ToUpper() == "POST" ? request.Form : request.QueryString; long id; if (!long.TryParse(input["id"].Value, out id)) { ReportError(response, bw, "Invalid or missing backup id"); return; } var backup = Program.DataConnection.GetBackup(id); if (backup == null) { ReportError(response, bw, "Invalid or missing backup id"); return; } if (Program.WorkThread.Active) { try { //TODO: It's not safe to access the values like this, //because the runner thread might interfere if (Program.WorkThread.CurrentTask.Item1 == id) { bool force; if (!bool.TryParse(input["force"].Value, out force)) force = false; if (!force) { OutputObject(bw, new { status = "failed", reason = "backup-in-progress" }); return; } bool hasPaused = Program.LiveControl.State == LiveControls.LiveControlState.Paused; Program.LiveControl.Pause(); try { for (int i = 0; i < 10; i++) if (Program.WorkThread.Active) { var t = Program.WorkThread.CurrentTask; if (t != null && t.Item1 == id) System.Threading.Thread.Sleep(1000); else break; } else break; } finally { } if (Program.WorkThread.Active) { var t = Program.WorkThread.CurrentTask; if (t == null && t.Item1 == id) { if (hasPaused) Program.LiveControl.Resume(); OutputObject(bw, new { status = "failed", reason = "backup-unstoppable" }); return; } } if (hasPaused) Program.LiveControl.Resume(); } } catch (Exception ex) { OutputObject(bw, new { status = "error", message = ex.Message }); return; } } Program.DataConnection.DeleteBackup(backup); //We have fiddled with the schedules Program.Scheduler.Reschedule(); OutputObject(bw, new { status = "OK" }); } private void SendCommand(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, BodyWriter bw) { HttpServer.HttpInput input = request.Method.ToUpper() == "POST" ? request.Form : request.QueryString; string command = input["command"].Value ?? ""; long id; switch (command.ToLowerInvariant()) { case "pause": if (input.Contains("duration") && !string.IsNullOrWhiteSpace(input["duration"].Value)) { TimeSpan ts; try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); } catch (Exception ex) { ReportError(response, bw, ex.Message); return; } if (ts.TotalMilliseconds > 0) Program.LiveControl.Pause(ts); else Program.LiveControl.Pause(); } else { Program.LiveControl.Pause(); } OutputObject(bw, new { Status = "OK" }); return; case "resume": Program.LiveControl.Resume(); OutputObject(bw, new { Status = "OK" }); return; case "stop": OutputObject(bw, new { Status = "OK" }); return; case "abort": OutputObject(bw, new { Status = "OK" }); return; case "run": case "run-backup": { Duplicati.Server.Serialization.Interface.IBackup backup = null; if (long.TryParse(input["id"].Value, out id)) backup = Program.DataConnection.GetBackup(id); if (backup == null) { ReportError(response, bw, string.Format("No backup found for id: {0}", input["id"].Value)); return; } Program.WorkThread.AddTask(new Tuple(id, DuplicatiOperation.Backup)); } OutputObject(bw, new { Status = "OK" }); return; case "clear-warning": Program.HasWarning = false; OutputObject(bw, new { Status = "OK" }); return; case "clear-error": Program.HasError = false; OutputObject(bw, new { Status = "OK" }); return; default: var m = Duplicati.Library.DynamicLoader.WebLoader.Modules.Where(x => x.Key.Equals(command, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); if (m == null) { ReportError(response, bw, string.Format("Unsupported command {0}", command)); return; } OutputObject(bw, new { Status = "OK", Result = m.Execute(input.Where(x => !x.Name.Equals("command", StringComparison.InvariantCultureIgnoreCase)).ToDictionary(x => x.Name, x => x.Value)) }); return; } } } } }