diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs index 3c58521e6..a8ac0dd1d 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs @@ -20,8 +20,8 @@ namespace Duplicati.GUI.TrayIcon { //When running the hosted instance we do not really care what port we are using, // so we just throw a few out there and try them - if (args == null || !args.Any(x => x.Trim().StartsWith("--" + Duplicati.Server.WebServer.OPTION_PORT + "=", StringComparison.InvariantCultureIgnoreCase))) - args = (args ?? new string[0]).Union(new string[] { "--" + Duplicati.Server.WebServer.OPTION_PORT + "=8200,8300,8400,8500,8600,8700,8800,8900,8989" }).ToArray(); + if (args == null || !args.Any(x => x.Trim().StartsWith("--" + Duplicati.Server.WebServer.Server.OPTION_PORT + "=", StringComparison.InvariantCultureIgnoreCase))) + args = (args ?? new string[0]).Union(new string[] { "--" + Duplicati.Server.WebServer.Server.OPTION_PORT + "=8200,8300,8400,8500,8600,8700,8800,8900,8989" }).ToArray(); Duplicati.Server.Program.Main(args); } catch (Exception ex) { diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs index c2c1ea5eb..a23c5bb72 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs @@ -10,12 +10,16 @@ namespace Duplicati.GUI.TrayIcon public class HttpServerConnection : IDisposable { private const string CONTROL_SCRIPT = "control.cgi"; + private const string LOGIN_SCRIPT = "login.cgi"; private const string STATUS_WINDOW = "index.html"; private const string EDIT_WINDOW = "edit-window.html"; + private const string AUTH_COOKIE = "session_auth"; private Uri m_controlUri; private string m_baseUri; - //private System.Net.NetworkCredential m_credentials; + private string m_password; + private bool m_saltedpassword; + private string m_authtoken; private static readonly System.Text.Encoding ENCODING = System.Text.Encoding.GetEncoding("utf-8"); public delegate void StatusUpdate(IServerStatus status); public event StatusUpdate StatusUpdated; @@ -34,13 +38,16 @@ namespace Duplicati.GUI.TrayIcon private object m_lock = new object(); private Queue> m_workQueue = new Queue>(); - public HttpServerConnection(Uri server, System.Net.NetworkCredential credentials) + public HttpServerConnection(Uri server, string password, bool saltedpassword) { m_baseUri = server.ToString(); if (!m_baseUri.EndsWith("/")) m_baseUri += "/"; m_controlUri = new Uri(m_baseUri + CONTROL_SCRIPT); + m_password = password; + m_saltedpassword = saltedpassword; + m_updateRequest = new Dictionary(); m_updateRequest["action"] = "get-current-state"; m_updateRequest["longpoll"] = "false"; @@ -145,7 +152,92 @@ namespace Duplicati.GUI.TrayIcon return string.Join("&", Array.ConvertAll(dict.Keys.ToArray(), key => string.Format("{0}={1}", Uri.EscapeUriString(key), Uri.EscapeUriString(dict[key])))); } + private class SaltAndNonce + { + public string Salt; + public string Nonce; + } + + private SaltAndNonce GetSaltAndNonce() + { + var req = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(m_baseUri + LOGIN_SCRIPT + "?get-nonce=1"); + req.Method = "GET"; + req.UserAgent = "Duplicati TrayIcon Monitor, v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); + + Duplicati.Library.Utility.AsyncHttpRequest areq = new Library.Utility.AsyncHttpRequest(req); + using(var r = (System.Net.HttpWebResponse)areq.GetResponse()) + using(var s = r.GetResponseStream()) + using (var sr = new System.IO.StreamReader(s, ENCODING, true)) + return Serializer.Deserialize(sr); + } + + private string PerformLogin(string password, string nonce) + { + System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(m_baseUri + LOGIN_SCRIPT + "?password=" + Duplicati.Library.Utility.Uri.UrlEncode(password)); + req.Method = "GET"; + req.UserAgent = "Duplicati TrayIcon Monitor, v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); + if (req.CookieContainer == null) + req.CookieContainer = new System.Net.CookieContainer(); + req.CookieContainer.Add(new System.Net.Cookie("session_nonce", nonce, "/", req.RequestUri.Host)); + + //Wrap it all in async stuff + Duplicati.Library.Utility.AsyncHttpRequest areq = new Library.Utility.AsyncHttpRequest(req); + using(var r = (System.Net.HttpWebResponse)areq.GetResponse()) + if (r.StatusCode == System.Net.HttpStatusCode.OK) + return r.Cookies["session_auth"].Value; + + return null; + } + + private string GetAuthToken() + { + var salt_nonce = GetSaltAndNonce(); + var sha256 = System.Security.Cryptography.SHA256.Create(); + var password = m_password; + + if (!m_saltedpassword) + { + var str = System.Text.Encoding.UTF8.GetBytes(m_password); + var buf = Convert.FromBase64String(salt_nonce.Salt); + sha256.TransformBlock(str, 0, str.Length, str, 0); + sha256.TransformFinalBlock(buf, 0, buf.Length); + password = Convert.ToBase64String(sha256.Hash); + sha256.Initialize(); + } + + var nonce = Convert.FromBase64String(salt_nonce.Nonce); + sha256.TransformBlock(nonce, 0, nonce.Length, nonce, 0); + var pwdbuf = Convert.FromBase64String(password); + sha256.TransformFinalBlock(pwdbuf, 0, pwdbuf.Length); + var pwd = Convert.ToBase64String(sha256.Hash); + + return PerformLogin(pwd, salt_nonce.Nonce); + } + + private T PerformRequest(Dictionary queryparams) + { + try + { + return PerformRequestInternal(queryparams); + } + catch (System.Net.WebException wex) + { + if ( + wex.Status == System.Net.WebExceptionStatus.ProtocolError && + ((System.Net.HttpWebResponse)wex.Response).StatusCode == System.Net.HttpStatusCode.Unauthorized && + !string.IsNullOrWhiteSpace(m_password) + ) + { + m_authtoken = GetAuthToken(); + return PerformRequestInternal(queryparams); + } + else + throw; + } + } + + private T PerformRequestInternal(Dictionary queryparams) { queryparams["format"] = "json"; @@ -158,6 +250,12 @@ namespace Duplicati.GUI.TrayIcon req.ContentType = "application/x-www-form-urlencoded ; charset=" + ENCODING.BodyName; req.Headers.Add("Accept-Charset", ENCODING.BodyName); req.UserAgent = "Duplicati TrayIcon Monitor, v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); + if (m_authtoken != null) + { + if (req.CookieContainer == null) + req.CookieContainer = new System.Net.CookieContainer(); + req.CookieContainer.Add(new System.Net.Cookie(AUTH_COOKIE, m_authtoken, "/", req.RequestUri.Host)); + } //Wrap it all in async stuff Duplicati.Library.Utility.AsyncHttpRequest areq = new Library.Utility.AsyncHttpRequest(req); @@ -265,7 +363,19 @@ namespace Duplicati.GUI.TrayIcon public string StatusWindowURL { - get { return m_baseUri + STATUS_WINDOW; } + get + { + try + { + if (m_authtoken != null) + return m_baseUri + STATUS_WINDOW + "?auth-token=" + GetAuthToken(); + } + catch + { + } + + return m_baseUri + STATUS_WINDOW; + } } public string EditWindowURL diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs index 2da96a5f6..376c375d3 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs @@ -124,6 +124,8 @@ namespace Duplicati.GUI.TrayIcon HostedInstanceKeeper hosted = null; bool openui = false; + string password = null; + bool saltedpassword = false; if (!Library.Utility.Utility.ParseBoolOption(options, NOHOSTEDSERVER_OPTION)) { try @@ -138,6 +140,8 @@ namespace Duplicati.GUI.TrayIcon // We have a hosted server, if this is the first run, // we should open the main page openui = Duplicati.Server.Program.IsFirstRun || Duplicati.Server.Program.ServerPortChanged; + password = Duplicati.Server.Program.DataConnection.ApplicationSettings.WebserverPassword; + saltedpassword = true; } using (hosted) @@ -156,7 +160,14 @@ namespace Duplicati.GUI.TrayIcon } } - using (Connection = new HttpServerConnection(new Uri(url), null)) + string pwd; + if (options.TryGetValue("webserver-password", out pwd)) + { + password = pwd; + saltedpassword = false; + } + + using (Connection = new HttpServerConnection(new Uri(url), password, saltedpassword)) { using(var tk = RunTrayIcon(toolkit)) { diff --git a/Duplicati/Server/Database/ApplicationSettings.cs b/Duplicati/Server/Database/ApplicationSettings.cs index 8bebcfd86..f7fd75c54 100644 --- a/Duplicati/Server/Database/ApplicationSettings.cs +++ b/Duplicati/Server/Database/ApplicationSettings.cs @@ -32,6 +32,8 @@ namespace Duplicati.Server.Database public const string LAST_WEBSERVER_PORT = "last-webserver-port"; public const string IS_FIRST_RUN = "is-first-run"; public const string SERVER_PORT_CHANGED = "server-port-changed"; + public const string SERVER_PASSPHRASE = "server-passphrase"; + public const string SERVER_PASSPHRASE_SALT = "server-passphras-salt"; } private Dictionary m_values; @@ -166,6 +168,51 @@ namespace Duplicati.Server.Database SaveSettings(); } } + + public string WebserverPassword + { + get + { + return m_values[CONST.SERVER_PASSPHRASE]; + } + } + + public string WebserverPasswordSalt + { + get + { + return m_values[CONST.SERVER_PASSPHRASE_SALT]; + } + } + + public void SetWebserverPassword(string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + m_values[CONST.SERVER_PASSPHRASE] = ""; + m_values[CONST.SERVER_PASSPHRASE_SALT] = ""; + SaveSettings(); + } + else + { + var prng = System.Security.Cryptography.RNGCryptoServiceProvider.Create(); + var buf = new byte[32]; + prng.GetBytes(buf); + var salt = Convert.ToBase64String(buf); + + var sha256 = System.Security.Cryptography.SHA256.Create(); + var str = System.Text.Encoding.UTF8.GetBytes(password); + + sha256.TransformBlock(str, 0, str.Length, str, 0); + sha256.TransformFinalBlock(buf, 0, buf.Length); + var pwd = Convert.ToBase64String(sha256.Hash); + + m_values[CONST.SERVER_PASSPHRASE] = pwd; + m_values[CONST.SERVER_PASSPHRASE_SALT] = salt; + + SaveSettings(); + } + } } } diff --git a/Duplicati/Server/Database/Connection.cs b/Duplicati/Server/Database/Connection.cs index 36c195204..41bc7a6da 100644 --- a/Duplicati/Server/Database/Connection.cs +++ b/Duplicati/Server/Database/Connection.cs @@ -196,7 +196,7 @@ namespace Duplicati.Server.Database values, @"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (?, ?, ?, ?)", (f) => { - if (WebServer.PASSWORD_PLACEHOLDER.Equals(f.Value)) + if (Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER.Equals(f.Value)) throw new Exception("Attempted to save a property with the placeholder password"); return new object[] { id, f.Filter ?? "", f.Name, f.Value ?? "" }; } @@ -392,7 +392,7 @@ namespace Duplicati.Server.Database @"INSERT INTO ""Backup"" (""Name"", ""Tags"", ""TargetURL"", ""DBPath"") VALUES (?,?,?,?)", (n) => { - if (n.TargetURL.IndexOf(WebServer.PASSWORD_PLACEHOLDER) >= 0) + if (n.TargetURL.IndexOf(Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER) >= 0) throw new Exception("Attempted to save a backup with the password placeholder"); return new object[] { diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj index e1ba1fb45..5cdaf46a3 100644 --- a/Duplicati/Server/Duplicati.Server.csproj +++ b/Duplicati/Server/Duplicati.Server.csproj @@ -93,7 +93,6 @@ True True - @@ -104,6 +103,18 @@ + + + + + + + + + + + + @@ -241,6 +252,8 @@ + +