Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
621aada0d1 | ||
|
|
e5a3fed155 | ||
|
|
67b133a206 | ||
|
|
5b1df6fa61 | ||
|
|
f50b49c56e | ||
|
|
e05888e557 | ||
|
|
c36ec0cbc5 | ||
|
|
b968dba9ce | ||
|
|
7a85b1763f | ||
|
|
599b4124f2 | ||
|
|
037e9e179f | ||
|
|
b9d6084973 | ||
|
|
c52f08bc6e | ||
|
|
85d5b1d74e | ||
|
|
105f6e16ff | ||
|
|
b7cac4cd51 | ||
|
|
b55ba094b6 | ||
|
|
92f1de07ae | ||
|
|
27ce549e75 | ||
|
|
09d30b92e0 | ||
|
|
bc0a47f768 | ||
|
|
75bab41034 | ||
|
|
94d6ab76ee | ||
|
|
974f6bff2f | ||
|
|
0ae9990f18 | ||
|
|
18ff96c5fe | ||
|
|
6917030417 | ||
|
|
44a45fcd04 | ||
|
|
f7656f464e | ||
|
|
627c270025 | ||
|
|
5d0d9fbc80 | ||
|
|
92fea213f9 | ||
|
|
4fe138097b | ||
|
|
e25df96d23 | ||
|
|
d7345ad993 | ||
|
|
85368266ed | ||
|
|
5e9ec4ee98 | ||
|
|
fef9efe4d4 | ||
|
|
2c42108df3 | ||
|
|
f91c4c7328 | ||
|
|
54237b1cbb | ||
|
|
c0f834f511 | ||
|
|
a87984806e | ||
|
|
1c40025666 | ||
|
|
f9170602d3 | ||
|
|
6cebb6b84f | ||
|
|
8f56220f2f | ||
|
|
be9a3667e6 | ||
|
|
c023e29020 | ||
|
|
8a02dc014e | ||
|
|
3f51536073 | ||
|
|
d2e91f8db6 | ||
|
|
caf7d53583 | ||
|
|
d3a8ecacad | ||
|
|
b9031e9f20 | ||
|
|
21fe0f9cfb | ||
|
|
e0d93da2f5 | ||
|
|
1acda72561 | ||
|
|
0855d7fe4d | ||
|
|
d6fe8c1a7d | ||
|
|
fa7d5e45f7 | ||
|
|
8efab7f746 | ||
|
|
3f29331dc6 | ||
|
|
92b82df78b | ||
|
|
fd9b1a1c5b | ||
|
|
bbff638212 | ||
|
|
c9f8e5786c | ||
|
|
4392d84da4 | ||
|
|
a47af06bb7 | ||
|
|
34821192fc | ||
|
|
3487f2fdf4 | ||
|
|
d3ec5f54fe | ||
|
|
0f1d381c7f | ||
|
|
edc0fb5e4b | ||
|
|
25a7667e36 | ||
|
|
d463e6bda7 | ||
|
|
cddf0917e3 | ||
|
|
6945270d49 | ||
|
|
3f29642f6c | ||
|
|
c0b8129924 | ||
|
|
fe90f8ce3d | ||
|
|
829c434c64 | ||
|
|
8b8707c811 | ||
|
|
47ca64ee63 | ||
|
|
e88c30edd1 | ||
|
|
4294ee3611 | ||
|
|
1920b0684d | ||
|
|
773f3acfb3 | ||
|
|
270f2f9cea | ||
|
|
25bb4f3701 | ||
|
|
0e12adb210 | ||
|
|
1d0d7e0152 | ||
|
|
ca425808c5 | ||
|
|
6878a601a9 | ||
|
|
9f79025744 | ||
|
|
2e7b8fc468 | ||
|
|
a937e8712e | ||
|
|
8625f14744 | ||
|
|
2773efec93 | ||
|
|
8fa4875885 | ||
|
|
6e16e1d9ed | ||
|
|
097375a6e8 | ||
|
|
70741dc99c | ||
|
|
0fefb316eb | ||
|
|
f9f6dfee6b | ||
|
|
85c2864a9b | ||
|
|
23a9c72115 | ||
|
|
c45c1b3961 | ||
|
|
d78b9f2c63 | ||
|
|
6041c8f1d9 | ||
|
|
5d53eddb32 | ||
|
|
32e852a392 |
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
|
||||
namespace Duplicati.WebserverCore.Abstractions;
|
||||
|
||||
public interface IScheduler
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes scheduler
|
||||
/// </summary>
|
||||
/// <param name="worker">The worker thread</param>
|
||||
void Init(WorkerThread<Runner.IRunnerData> worker);
|
||||
|
||||
IList<Tuple<long, string>> GetSchedulerQueueIds();
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the thread. Any items still in queue will be removed
|
||||
/// </summary>
|
||||
/// <param name="wait">True if the call should block until the thread has exited, false otherwise</param>
|
||||
void Terminate(bool wait);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the event that is triggered when the schedule changes
|
||||
/// </summary>
|
||||
void SubScribeToNewSchedule(Action handler);
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot copy of the current schedule list
|
||||
/// </summary>
|
||||
List<KeyValuePair<DateTime, ISchedule>> Schedule { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution
|
||||
/// </summary>
|
||||
List<Runner.IRunnerData> WorkerQueue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Forces the scheduler to re-evaluate the order.
|
||||
/// Call this method if something changes
|
||||
/// </summary>
|
||||
void Reschedule();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Server;
|
||||
|
||||
namespace Duplicati.Library.RestAPI.Abstractions;
|
||||
|
||||
public interface IWorkerThreadsManager
|
||||
{
|
||||
void Spawn(Action<Runner.IRunnerData> item);
|
||||
|
||||
Tuple<long, string>? CurrentTask { get; }
|
||||
WorkerThread<Runner.IRunnerData>? WorkerThread { get; }
|
||||
void UpdateThrottleSpeeds();
|
||||
|
||||
long AddTask(Runner.IRunnerData data, bool skipQueue = false);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
|
||||
namespace Duplicati.Library.RestAPI;
|
||||
|
||||
public static class BackupImportExportHandler
|
||||
{
|
||||
public static void RemovePasswords(IBackup backup)
|
||||
{
|
||||
backup.SanitizeSettings();
|
||||
backup.SanitizeTargetUrl();
|
||||
}
|
||||
|
||||
public static byte[] ExportToJSON(Connection connection, IBackup backup, string passphrase)
|
||||
{
|
||||
Server.Serializable.ImportExportStructure ipx = connection.PrepareBackupForExport(backup);
|
||||
|
||||
byte[] data;
|
||||
using (var ms = new System.IO.MemoryStream())
|
||||
{
|
||||
using (var sw = new System.IO.StreamWriter(ms))
|
||||
{
|
||||
Serializer.SerializeJson(sw, ipx, true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(passphrase))
|
||||
{
|
||||
ms.Position = 0;
|
||||
using (var ms2 = new System.IO.MemoryStream())
|
||||
{
|
||||
using (var m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary<string, string>()))
|
||||
{
|
||||
m.Encrypt(ms, ms2);
|
||||
data = ms2.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
data = ms.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static Server.Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func<string> getPassword, Dictionary<string, string> advancedOptions)
|
||||
{
|
||||
// This removes the ID and DBPath from the backup configuration.
|
||||
Server.Serializable.ImportExportStructure importedStructure = LoadConfiguration(configurationFile, importMetadata, getPassword);
|
||||
|
||||
// This will create the Duplicati-server.sqlite database file if it doesn't exist.
|
||||
using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions))
|
||||
{
|
||||
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists.");
|
||||
}
|
||||
|
||||
string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule);
|
||||
if (!string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
throw new InvalidOperationException(error);
|
||||
}
|
||||
|
||||
// This creates a new ID and DBPath.
|
||||
connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule);
|
||||
}
|
||||
|
||||
return importedStructure;
|
||||
}
|
||||
|
||||
public static Server.Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func<string> getPassword)
|
||||
{
|
||||
Server.Serializable.ImportExportStructure ipx;
|
||||
|
||||
var buf = new byte[3];
|
||||
using (var fs = System.IO.File.OpenRead(filename))
|
||||
{
|
||||
Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length);
|
||||
|
||||
fs.Position = 0;
|
||||
if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S')
|
||||
{
|
||||
using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary<string, string>()))
|
||||
{
|
||||
using (var m2 = m.Decrypt(fs))
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(m2))
|
||||
{
|
||||
ipx = Serializer.Deserialize<Server.Serializable.ImportExportStructure>(sr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(fs))
|
||||
{
|
||||
ipx = Serializer.Deserialize<Server.Serializable.ImportExportStructure>(sr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ipx.Backup == null)
|
||||
{
|
||||
throw new Exception("No backup found in document");
|
||||
}
|
||||
|
||||
if (ipx.Backup.Metadata == null)
|
||||
{
|
||||
ipx.Backup.Metadata = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
if (!importMetadata)
|
||||
{
|
||||
ipx.Backup.Metadata.Clear();
|
||||
}
|
||||
|
||||
ipx.Backup.ID = null;
|
||||
ipx.Backup.DBPath = null;
|
||||
|
||||
if (ipx.Schedule != null)
|
||||
{
|
||||
ipx.Schedule.ID = -1;
|
||||
}
|
||||
|
||||
return ipx;
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,8 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
protected void SetDBPath(string path) => this.DBPath = path;
|
||||
|
||||
/// <summary>
|
||||
/// The backup ID
|
||||
/// </summary>
|
||||
|
||||
@@ -62,14 +62,14 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
internal void ExecuteWithCommand(Action<System.Data.IDbCommand> f)
|
||||
public void ExecuteWithCommand(Action<System.Data.IDbCommand> f)
|
||||
{
|
||||
lock (m_lock)
|
||||
using (var cmd = m_connection.CreateCommand())
|
||||
f(cmd);
|
||||
}
|
||||
|
||||
internal Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup)
|
||||
public Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup)
|
||||
{
|
||||
var scheduleId = GetScheduleIDsFromTags(new string[] { "ID=" + backup.ID });
|
||||
return new Serializable.ImportExportStructure()
|
||||
@@ -213,7 +213,7 @@ namespace Duplicati.Server.Database
|
||||
@"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (?, ?, ?, ?)",
|
||||
(f) =>
|
||||
{
|
||||
if (Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER.Equals(f.Value))
|
||||
if (FIXMEGlobal.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 ?? "" };
|
||||
}
|
||||
@@ -281,7 +281,7 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
internal IBackup GetBackup(string id)
|
||||
public IBackup GetBackup(string id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
@@ -313,7 +313,7 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
internal ISchedule GetSchedule(long id)
|
||||
public ISchedule GetSchedule(long id)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
@@ -334,7 +334,7 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
internal Boolean IsUnencryptedOrPassphraseStored(long id)
|
||||
public bool IsUnencryptedOrPassphraseStored(long id)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
@@ -355,7 +355,7 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
internal long[] GetScheduleIDsFromTags(string[] tags)
|
||||
public long[] GetScheduleIDsFromTags(string[] tags)
|
||||
{
|
||||
if (tags == null || tags.Length == 0)
|
||||
return new long[0];
|
||||
@@ -486,7 +486,7 @@ namespace Duplicati.Server.Database
|
||||
return null;
|
||||
}
|
||||
|
||||
internal void UpdateBackupDBPath(IBackup item, string path)
|
||||
public void UpdateBackupDBPath(IBackup item, string path)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
@@ -507,7 +507,7 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
FIXMEGlobal.IncrementLastDataUpdateID();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
@@ -549,7 +549,7 @@ namespace Duplicati.Server.Database
|
||||
(n) =>
|
||||
{
|
||||
|
||||
if (n.TargetURL.IndexOf(Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0)
|
||||
if (n.TargetURL.IndexOf(FIXMEGlobal.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0)
|
||||
throw new Exception("Attempted to save a backup with the password placeholder");
|
||||
if (update && long.Parse(n.ID) <= 0)
|
||||
throw new Exception("Invalid update, cannot update application settings through update method");
|
||||
@@ -610,7 +610,7 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
|
||||
tr.Commit();
|
||||
FIXMEGlobal.IncrementLastDataUpdateID();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
}
|
||||
@@ -623,7 +623,7 @@ namespace Duplicati.Server.Database
|
||||
{
|
||||
AddOrUpdateSchedule(item, tr);
|
||||
tr.Commit();
|
||||
FIXMEGlobal.IncrementLastDataUpdateID();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
}
|
||||
@@ -686,7 +686,7 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
FIXMEGlobal.IncrementLastDataUpdateID();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
@@ -706,7 +706,7 @@ namespace Duplicati.Server.Database
|
||||
lock (m_lock)
|
||||
DeleteFromDb("Schedule", ID);
|
||||
|
||||
FIXMEGlobal.IncrementLastDataUpdateID();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
@@ -796,7 +796,7 @@ namespace Duplicati.Server.Database
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning);
|
||||
}
|
||||
|
||||
FIXMEGlobal.IncrementLastNotificationUpdateID();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastNotificationUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
return true;
|
||||
@@ -836,7 +836,7 @@ namespace Duplicati.Server.Database
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = true;
|
||||
}
|
||||
|
||||
FIXMEGlobal.IncrementLastNotificationUpdateID();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastNotificationUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Token Family table
|
||||
*/
|
||||
CREATE TABLE "TokenFamily" (
|
||||
"ID" TEXT PRIMARY KEY,
|
||||
"UserId" TEXT NOT NULL,
|
||||
"Counter" INTEGER NOT NULL,
|
||||
"LastUpdated" INTEGER NOT NULL
|
||||
);
|
||||
@@ -154,5 +154,15 @@ CREATE TABLE "TempFile" (
|
||||
"Expires" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "Version" ("Version") VALUES (6);
|
||||
/*
|
||||
Token Family table
|
||||
*/
|
||||
CREATE TABLE "TokenFamily" (
|
||||
"ID" TEXT PRIMARY KEY,
|
||||
"UserId" TEXT NOT NULL,
|
||||
"Counter" INTEGER NOT NULL,
|
||||
"LastUpdated" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "Version" ("Version") VALUES (7);
|
||||
|
||||
|
||||
@@ -24,14 +24,15 @@ using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Duplicati.Library.Common;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
|
||||
namespace Duplicati.Server.Database
|
||||
{
|
||||
public class ServerSettings
|
||||
{
|
||||
private static class CONST
|
||||
public static class CONST
|
||||
{
|
||||
public const string STARTUP_DELAY = "startup-delay";
|
||||
public const string DOWNLOAD_SPEED_LIMIT = "max-download-speed";
|
||||
@@ -42,8 +43,6 @@ namespace Duplicati.Server.Database
|
||||
public const string SERVER_PORT_CHANGED = "server-port-changed";
|
||||
public const string SERVER_PASSPHRASE = "server-passphrase";
|
||||
public const string SERVER_PASSPHRASE_SALT = "server-passphrase-salt";
|
||||
public const string SERVER_PASSPHRASETRAYICON = "server-passphrase-trayicon";
|
||||
public const string SERVER_PASSPHRASETRAYICONHASH = "server-passphrase-trayicon-hash";
|
||||
public const string UPDATE_CHECK_LAST = "last-update-check";
|
||||
public const string UPDATE_CHECK_INTERVAL = "update-check-interval";
|
||||
public const string UPDATE_CHECK_NEW_VERSION = "update-check-latest";
|
||||
@@ -54,9 +53,11 @@ namespace Duplicati.Server.Database
|
||||
public const string HAS_FIXED_INVALID_BACKUPID = "has-fixed-invalid-backup-id";
|
||||
public const string UPDATE_CHANNEL = "update-channel";
|
||||
public const string USAGE_REPORTER_LEVEL = "usage-reporter-level";
|
||||
public const string HAS_ASKED_FOR_PASSWORD_PROTECTION = "has-asked-for-password-protection";
|
||||
public const string DISABLE_TRAY_ICON_LOGIN = "disable-tray-icon-login";
|
||||
public const string SERVER_ALLOWED_HOSTNAMES = "allowed-hostnames";
|
||||
public const string JWT_CONFIG = "jwt-config";
|
||||
public const string PBKDF_CONFIG = "pbkdf-config";
|
||||
public const string AUTOGENERATED_PASSPHRASE = "autogenerated-passphrase";
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, string> settings;
|
||||
@@ -99,12 +100,12 @@ namespace Duplicati.Server.Database
|
||||
else
|
||||
settings[k.Key] = newsettings[k.Key];
|
||||
|
||||
// Prevent user from logging themselves out, by disabling the login and not knowing the password
|
||||
if (DisableTrayIconLogin && AutogeneratedPassphrase)
|
||||
settings[CONST.DISABLE_TRAY_ICON_LOGIN] = false.ToString();
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
|
||||
if (newsettings.Keys.Contains(CONST.SERVER_PASSPHRASE))
|
||||
GenerateWebserverPasswordTrayIcon();
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
@@ -118,13 +119,17 @@ namespace Duplicati.Server.Database
|
||||
Value = n.Value
|
||||
}, Database.Connection.SERVER_SETTINGS_ID);
|
||||
|
||||
FIXMEGlobal.IncrementLastDataUpdateID();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
if (FIXMEGlobal.IsServerStarted)
|
||||
{
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
// If throttle options were changed, update now
|
||||
FIXMEGlobal.WorkerThreadsManager.UpdateThrottleSpeeds();
|
||||
}
|
||||
|
||||
// In case the usage reporter is enabled or disabled, refresh now
|
||||
FIXMEGlobal.StartOrStopUsageReporter();
|
||||
// If throttle options were changed, update now
|
||||
FIXMEGlobal.UpdateThrottleSpeeds();
|
||||
if (FIXMEGlobal.StartOrStopUsageReporter != null)
|
||||
FIXMEGlobal.StartOrStopUsageReporter();
|
||||
}
|
||||
|
||||
public string StartupDelayDuration
|
||||
@@ -204,20 +209,6 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasAskedForPasswordProtection
|
||||
{
|
||||
get
|
||||
{
|
||||
return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION);
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (databaseConnection.m_lock)
|
||||
settings[CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION] = value.ToString();
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public bool UnackedError
|
||||
{
|
||||
get
|
||||
@@ -274,6 +265,14 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutogeneratedPassphrase
|
||||
{
|
||||
get
|
||||
{
|
||||
return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.AUTOGENERATED_PASSPHRASE], false);
|
||||
}
|
||||
}
|
||||
|
||||
public int LastWebserverPort
|
||||
{
|
||||
get
|
||||
@@ -293,51 +292,144 @@ namespace Duplicati.Server.Database
|
||||
}
|
||||
}
|
||||
|
||||
public string WebserverPassword
|
||||
/// <summary>
|
||||
/// This class is used to store the PBKDF configuration parameters
|
||||
/// </summary>
|
||||
private record PbkdfConfig(string Algorithm, int Version, string Salt, int Iterations, string HashAlorithm, string Hash)
|
||||
{
|
||||
get
|
||||
/// <summary>
|
||||
/// The version to embed in the configuration
|
||||
/// </summary>
|
||||
private const int _Version = 1;
|
||||
/// <summary>
|
||||
/// The algorithm to use
|
||||
/// </summary>
|
||||
private const string _Algorithm = "PBKDF2";
|
||||
/// <summary>
|
||||
/// The hash algorithm to use
|
||||
/// </summary>
|
||||
private const string _HashAlorithm = "SHA256";
|
||||
/// <summary>
|
||||
/// The number of iterations to use
|
||||
/// </summary>
|
||||
private const int _Iterations = 10000;
|
||||
/// <summary>
|
||||
/// The size of the hash
|
||||
/// </summary>
|
||||
private const int _HashSize = 32;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PBKDF2 configuration with a random salt
|
||||
/// </summary>
|
||||
/// <param name="password">The password to hash</param>
|
||||
public static PbkdfConfig CreatePBKDF2(string password)
|
||||
{
|
||||
return settings[CONST.SERVER_PASSPHRASE];
|
||||
var prng = RandomNumberGenerator.Create();
|
||||
var buf = new byte[_HashSize];
|
||||
prng.GetBytes(buf);
|
||||
|
||||
var salt = Convert.ToBase64String(buf);
|
||||
var pbkdf2 = new Rfc2898DeriveBytes(password, buf, _Iterations, new HashAlgorithmName(_HashAlorithm));
|
||||
var pwd = Convert.ToBase64String(pbkdf2.GetBytes(_HashSize));
|
||||
|
||||
return new PbkdfConfig(_Algorithm, _Version, salt, _Iterations, _HashAlorithm, pwd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a password against a PBKDF2 configuration
|
||||
/// </summary>
|
||||
/// <param name="password">The password to verify</param>
|
||||
/// <returns>True if the password matches the configuration</returns>
|
||||
public bool VerifyPassword(string password)
|
||||
{
|
||||
var pbkdf2 = new Rfc2898DeriveBytes(password, Convert.FromBase64String(Salt), Iterations, new HashAlgorithmName(HashAlorithm));
|
||||
var pwd = Convert.ToBase64String(pbkdf2.GetBytes(_HashSize));
|
||||
|
||||
return pwd == Hash;
|
||||
}
|
||||
}
|
||||
|
||||
public string WebserverPasswordSalt
|
||||
/// <summary>
|
||||
/// Verifies a password against the stored PBKDF configuration
|
||||
/// </summary>
|
||||
public bool VerifyWebserverPassword(string password)
|
||||
{
|
||||
get
|
||||
{
|
||||
return settings[CONST.SERVER_PASSPHRASE_SALT];
|
||||
}
|
||||
var config = settings[CONST.PBKDF_CONFIG];
|
||||
if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(config))
|
||||
return false;
|
||||
|
||||
return JsonSerializer.Deserialize<PbkdfConfig>(config).VerifyPassword(LegacyPreparePassword(password));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a password by pre-hashing it with a legacy salt, if needed
|
||||
/// </summary>
|
||||
/// <param name="password">The password to hash</param>
|
||||
/// <returns>The hashed password</returns>
|
||||
private string LegacyPreparePassword(string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(settings[CONST.SERVER_PASSPHRASE_SALT]))
|
||||
return password;
|
||||
|
||||
var buf = Convert.FromBase64String(settings[CONST.SERVER_PASSPHRASE_SALT]);
|
||||
var sha256 = SHA256.Create();
|
||||
var str = Encoding.UTF8.GetBytes(password);
|
||||
|
||||
sha256.TransformBlock(str, 0, str.Length, str, 0);
|
||||
sha256.TransformFinalBlock(buf, 0, buf.Length);
|
||||
return Convert.ToBase64String(sha256.Hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrades the password to a PBKDF configuration, if using the legacy password setup
|
||||
/// </summary>
|
||||
public void UpgradePasswordToKBDF()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(settings[CONST.PBKDF_CONFIG]))
|
||||
return;
|
||||
|
||||
// Generate a random password if one is not set
|
||||
var password = settings[CONST.SERVER_PASSPHRASE];
|
||||
var autogenerated = false;
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
password = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
settings[CONST.SERVER_PASSPHRASE_SALT] = null;
|
||||
autogenerated = true;
|
||||
}
|
||||
|
||||
// This will create a new PBKDF2 configuration
|
||||
// In case the password already exists in the database,
|
||||
// it will need use the pre-salted password as the password
|
||||
var config = PbkdfConfig.CreatePBKDF2(password);
|
||||
lock (databaseConnection.m_lock)
|
||||
{
|
||||
settings[CONST.PBKDF_CONFIG] = JsonSerializer.Serialize(config);
|
||||
settings[CONST.SERVER_PASSPHRASE] = null;
|
||||
settings[CONST.AUTOGENERATED_PASSPHRASE] = autogenerated.ToString();
|
||||
if (autogenerated)
|
||||
settings[CONST.DISABLE_TRAY_ICON_LOGIN] = false.ToString();
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the webserver password
|
||||
/// </summary>
|
||||
/// <param name="password">The password to set</param>
|
||||
public void SetWebserverPassword(string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
throw new Exception("Disabling password protection is not supported");
|
||||
|
||||
var config = PbkdfConfig.CreatePBKDF2(password);
|
||||
lock (databaseConnection.m_lock)
|
||||
{
|
||||
lock (databaseConnection.m_lock)
|
||||
{
|
||||
settings[CONST.SERVER_PASSPHRASE] = "";
|
||||
settings[CONST.SERVER_PASSPHRASE_SALT] = "";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var prng = RandomNumberGenerator.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);
|
||||
|
||||
lock (databaseConnection.m_lock)
|
||||
{
|
||||
settings[CONST.SERVER_PASSPHRASE] = pwd;
|
||||
settings[CONST.SERVER_PASSPHRASE_SALT] = salt;
|
||||
}
|
||||
settings[CONST.SERVER_PASSPHRASE] = null;
|
||||
settings[CONST.SERVER_PASSPHRASE_SALT] = null;
|
||||
settings[CONST.AUTOGENERATED_PASSPHRASE] = false.ToString();
|
||||
settings[CONST.PBKDF_CONFIG] = JsonSerializer.Serialize(config);
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
@@ -351,37 +443,17 @@ namespace Duplicati.Server.Database
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
public string WebserverPasswordTrayIcon => settings[CONST.SERVER_PASSPHRASETRAYICON];
|
||||
|
||||
public string WebserverPasswordTrayIconHash => settings[CONST.SERVER_PASSPHRASETRAYICONHASH];
|
||||
|
||||
public string AllowedHostnames => settings[CONST.SERVER_ALLOWED_HOSTNAMES];
|
||||
|
||||
public void GenerateWebserverPasswordTrayIcon()
|
||||
public string JWTConfig
|
||||
{
|
||||
var password = "";
|
||||
var pwd = "";
|
||||
|
||||
if (!string.IsNullOrEmpty(settings[CONST.SERVER_PASSPHRASE]))
|
||||
get => settings[CONST.JWT_CONFIG];
|
||||
set
|
||||
{
|
||||
password = Guid.NewGuid().ToString();
|
||||
var buf = Convert.FromBase64String(settings[CONST.SERVER_PASSPHRASE_SALT]);
|
||||
|
||||
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);
|
||||
pwd = Convert.ToBase64String(sha256.Hash);
|
||||
lock (databaseConnection.m_lock)
|
||||
settings[CONST.JWT_CONFIG] = value;
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
lock (databaseConnection.m_lock)
|
||||
{
|
||||
settings[CONST.SERVER_PASSPHRASETRAYICON] = password;
|
||||
settings[CONST.SERVER_PASSPHRASETRAYICONHASH] = pwd;
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
public DateTime LastUpdateCheck
|
||||
|
||||
@@ -14,15 +14,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Win32.SystemEvents" Version="7.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="HttpServer">
|
||||
<HintPath>..\thirdparty\HttpServer\HttpServer.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Duplicati\CommandLine\CLI\Duplicati.CommandLine.csproj" />
|
||||
<ProjectReference Include="..\Duplicati\Library\Common\Duplicati.Library.Common.csproj" />
|
||||
@@ -33,5 +26,9 @@
|
||||
<ProjectReference Include="..\Duplicati\Server\Duplicati.Server.Serialization\Duplicati.Server.Serialization.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="WebServer\Kestrel\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Duplicati.Server
|
||||
{
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
using Duplicati.Server;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.RestAPI.Abstractions;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Duplicati.Library.RestAPI
|
||||
{
|
||||
@@ -11,17 +15,18 @@ namespace Duplicati.Library.RestAPI
|
||||
*/
|
||||
public static class FIXMEGlobal
|
||||
{
|
||||
/// <summary>
|
||||
/// The placeholder for passwords in the UI
|
||||
/// </summary>
|
||||
public const string PASSWORD_PLACEHOLDER = "**********";
|
||||
|
||||
public static IServiceProvider Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is the only access to the database
|
||||
/// </summary>
|
||||
public static Server.Database.Connection DataConnection;
|
||||
|
||||
/// <summary>
|
||||
/// The controller interface for pause/resume and throttle options
|
||||
/// </summary>
|
||||
public static LiveControls LiveControl;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate method for creating a copy of the current progress state
|
||||
/// </summary>
|
||||
@@ -30,24 +35,28 @@ namespace Duplicati.Library.RestAPI
|
||||
/// <summary>
|
||||
/// The status event signaler, used to control long polling of status updates
|
||||
/// </summary>
|
||||
public static readonly EventPollNotify StatusEventNotifyer = new EventPollNotify();
|
||||
public static EventPollNotify StatusEventNotifyer => Provider.GetRequiredService<EventPollNotify>();
|
||||
|
||||
/// <summary>
|
||||
/// For keeping and incrementing last last events Ids of db save and last notification
|
||||
/// </summary>
|
||||
public static INotificationUpdateService NotificationUpdateService => Provider.GetRequiredService<INotificationUpdateService>();
|
||||
/// <summary>
|
||||
/// Checks if the server has started and is listening for events
|
||||
/// </summary>
|
||||
public static bool IsServerStarted => Provider != null;
|
||||
|
||||
/// <summary>
|
||||
/// This is the working thread
|
||||
/// </summary>
|
||||
public static Duplicati.Library.Utility.WorkerThread<Runner.IRunnerData> WorkThread;
|
||||
public static WorkerThread<Runner.IRunnerData> WorkThread =>
|
||||
Provider.GetRequiredService<IWorkerThreadsManager>().WorkerThread;
|
||||
|
||||
public static Func<long> PeekLastDataUpdateID;
|
||||
public static Func<long> PeekLastNotificationUpdateID;
|
||||
|
||||
public static Action IncrementLastDataUpdateID;
|
||||
|
||||
public static Action IncrementLastNotificationUpdateID;
|
||||
public static IWorkerThreadsManager WorkerThreadsManager =>
|
||||
Provider.GetRequiredService<IWorkerThreadsManager>();
|
||||
|
||||
public static Action StartOrStopUsageReporter;
|
||||
|
||||
public static Action UpdateThrottleSpeeds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder where Duplicati data is stored
|
||||
/// </summary>
|
||||
@@ -56,7 +65,7 @@ namespace Duplicati.Library.RestAPI
|
||||
/// <summary>
|
||||
/// This is the scheduling thread
|
||||
/// </summary>
|
||||
public static Scheduler Scheduler;
|
||||
public static IScheduler Scheduler => Provider.GetRequiredService<IScheduler>();
|
||||
|
||||
/// <summary>
|
||||
/// The log redirect handler
|
||||
@@ -68,7 +77,7 @@ namespace Duplicati.Library.RestAPI
|
||||
/// <summary>
|
||||
/// The update poll thread.
|
||||
/// </summary>
|
||||
public static UpdatePollThread UpdatePoller;
|
||||
public static UpdatePollThread UpdatePoller => Provider.GetRequiredService<UpdatePollThread>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -93,5 +102,6 @@ namespace Duplicati.Library.RestAPI
|
||||
/// This is the lock to be used before manipulating the shared resources
|
||||
/// </summary>
|
||||
public static readonly object MainLock = new object();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,9 @@
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using Duplicati.Library.Common;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.IO;
|
||||
using Duplicati.Server.Database;
|
||||
|
||||
namespace Duplicati.Server
|
||||
{
|
||||
@@ -32,7 +30,7 @@ namespace Duplicati.Server
|
||||
/// This class keeps track of the users modifications regarding
|
||||
/// throttling and pause/resume
|
||||
/// </summary>
|
||||
public class LiveControls
|
||||
public class LiveControls : ILiveControls
|
||||
{
|
||||
/// <summary>
|
||||
/// The tag used for logging
|
||||
@@ -89,6 +87,8 @@ namespace Duplicati.Server
|
||||
/// </summary>
|
||||
public LiveControlState State { get { return m_state; } }
|
||||
|
||||
public bool IsPaused => State == LiveControlState.Paused;
|
||||
|
||||
/// <summary>
|
||||
/// The internal variable that tracks the the priority
|
||||
/// </summary>
|
||||
@@ -163,18 +163,34 @@ namespace Duplicati.Server
|
||||
/// <summary>
|
||||
/// The timer that is activated after a pause period.
|
||||
/// </summary>
|
||||
private readonly System.Threading.Timer m_waitTimer;
|
||||
private System.Threading.Timer m_waitTimer;
|
||||
|
||||
/// <summary>
|
||||
/// The time that the current pause is expected to expire
|
||||
/// </summary>
|
||||
private DateTime m_waitTimeExpiration = new DateTime(0);
|
||||
|
||||
/// <summary>
|
||||
/// The connection to use
|
||||
/// </summary>
|
||||
private readonly Connection m_connection;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the LiveControl
|
||||
/// </summary>
|
||||
public LiveControls(Database.ServerSettings settings)
|
||||
/// <param name="connection">The connection to use</param>
|
||||
public LiveControls(Connection connection)
|
||||
{
|
||||
m_connection = connection;
|
||||
Init();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the LiveControl
|
||||
/// </summary>
|
||||
private void Init()
|
||||
{
|
||||
var settings = m_connection.ApplicationSettings;
|
||||
m_state = LiveControlState.Running;
|
||||
m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
|
||||
|
||||
@@ -385,7 +401,7 @@ namespace Duplicati.Server
|
||||
{
|
||||
long delayTicks = (m_suspendMinimumPause - DateTime.Now).Ticks;
|
||||
|
||||
var appset = FIXMEGlobal.DataConnection.ApplicationSettings;
|
||||
var appset = m_connection.ApplicationSettings;
|
||||
if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0")
|
||||
try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); }
|
||||
catch { }
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace Duplicati.Library.RestAPI;
|
||||
|
||||
public interface INotificationUpdateService
|
||||
{
|
||||
/// <summary>
|
||||
/// An event ID that increases whenever the database is updated
|
||||
/// </summary>
|
||||
long LastDataUpdateId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An event ID that increases whenever a notification is updated
|
||||
/// </summary>
|
||||
long LastNotificationUpdateId { get; }
|
||||
|
||||
void IncrementLastDataUpdateId();
|
||||
void IncrementLastNotificationUpdateId();
|
||||
}
|
||||
|
||||
public class NotificationUpdateService : INotificationUpdateService
|
||||
{
|
||||
/// <summary>
|
||||
/// An event ID that increases whenever the database is updated
|
||||
/// </summary>
|
||||
public long LastDataUpdateId { get; private set; } = 0;
|
||||
|
||||
private readonly object _lastDataUpdateIdLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// An event ID that increases whenever a notification is updated
|
||||
/// </summary>
|
||||
public long LastNotificationUpdateId { get; private set; } = 0;
|
||||
|
||||
private readonly object _lastNotificationUpdateIdLock = new();
|
||||
|
||||
public void IncrementLastDataUpdateId()
|
||||
{
|
||||
lock (_lastDataUpdateIdLock)
|
||||
{
|
||||
LastDataUpdateId++;
|
||||
}
|
||||
}
|
||||
|
||||
public void IncrementLastNotificationUpdateId()
|
||||
{
|
||||
lock (_lastNotificationUpdateIdLock)
|
||||
{
|
||||
LastNotificationUpdateId++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Utility;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Acknowledgements : IRESTMethodGET, IRESTMethodDocumented
|
||||
{
|
||||
private class GetResponse
|
||||
{
|
||||
public string Status;
|
||||
public string Acknowledgements;
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var path = SystemIO.IO_OS.PathCombine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "acknowledgements.txt");
|
||||
info.OutputOK(new GetResponse() {
|
||||
Status = "OK",
|
||||
Acknowledgements = System.IO.File.ReadAllText(path)
|
||||
});
|
||||
}
|
||||
public string Description { get { return "Gets all acknowledgements"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(GetResponse)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,742 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Server.Serialization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Backup : IRESTMethodGET, IRESTMethodPUT, IRESTMethodPOST, IRESTMethodDELETE, IRESTMethodDocumented
|
||||
{
|
||||
public class GetResponse
|
||||
{
|
||||
public class GetResponseData
|
||||
{
|
||||
public Serialization.Interface.ISchedule Schedule;
|
||||
public Serialization.Interface.IBackup Backup;
|
||||
public Dictionary<string, string> DisplayNames;
|
||||
}
|
||||
|
||||
public bool success;
|
||||
|
||||
public GetResponseData data;
|
||||
}
|
||||
|
||||
private void SearchFiles(IBackup backup, string filterstring, RequestInfo info)
|
||||
{
|
||||
var filter = filterstring;
|
||||
var timestring = info.Request.QueryString["time"].Value;
|
||||
var allversion = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["all-versions"].Value, false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(timestring) && !allversion)
|
||||
{
|
||||
info.ReportClientError("Invalid or missing time", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var prefixonly = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["prefix-only"].Value, false);
|
||||
var foldercontents = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["folder-contents"].Value, false);
|
||||
var time = new DateTime();
|
||||
if (!allversion)
|
||||
time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now);
|
||||
|
||||
var r = Runner.Run(Runner.CreateListTask(backup, new string[] { filter }, prefixonly, allversion, foldercontents, time), false) as Duplicati.Library.Interface.IListResults;
|
||||
|
||||
var result = new Dictionary<string, object>();
|
||||
|
||||
foreach(HttpServer.HttpInputItem n in info.Request.QueryString)
|
||||
result[n.Name] = n.Value;
|
||||
|
||||
result["Filesets"] = r.Filesets;
|
||||
result["Files"] = r.Files
|
||||
// Group directories first - support either directory separator here as we may be restoring data from an alternate platform
|
||||
.OrderByDescending(f => (f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("/", StringComparison.Ordinal)) || (!f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("\\", StringComparison.Ordinal)))
|
||||
// Sort both groups (directories and files) alphabetically
|
||||
.ThenBy(f => f.Path);
|
||||
|
||||
info.OutputOK(result);
|
||||
|
||||
}
|
||||
|
||||
private void ListFileSets(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var input = info.Request.QueryString;
|
||||
var extra = new Dictionary<string, string>
|
||||
{
|
||||
["list-sets-only"] = "true"
|
||||
};
|
||||
if (input["include-metadata"].Value != null)
|
||||
extra["list-sets-only"] = (!Library.Utility.Utility.ParseBool(input["include-metadata"].Value, false)).ToString();
|
||||
if (input["from-remote-only"].Value != null)
|
||||
extra["no-local-db"] = Library.Utility.Utility.ParseBool(input["from-remote-only"].Value, false).ToString();
|
||||
|
||||
var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, backup, extra), false) as Duplicati.Library.Interface.IListResults;
|
||||
|
||||
if (r.EncryptedFiles && backup.Settings.Any(x => string.Equals("--no-encryption", x.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
info.ReportServerError("encrypted-storage");
|
||||
else
|
||||
info.OutputOK(r.Filesets);
|
||||
}
|
||||
|
||||
private void FetchLogData(IBackup backup, RequestInfo info)
|
||||
{
|
||||
using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath))
|
||||
using(var cmd = con.CreateCommand())
|
||||
info.OutputOK(LogData.DumpTable(cmd, "LogData", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value));
|
||||
}
|
||||
|
||||
private void FetchRemoteLogData(IBackup backup, RequestInfo info)
|
||||
{
|
||||
using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath))
|
||||
using(var cmd = con.CreateCommand())
|
||||
{
|
||||
var dt = LogData.DumpTable(cmd, "RemoteOperation", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value);
|
||||
|
||||
// Unwrap raw data to a string
|
||||
foreach(var n in dt)
|
||||
try { n["Data"] = System.Text.Encoding.UTF8.GetString((byte[])n["Data"]); }
|
||||
catch { }
|
||||
|
||||
info.OutputOK(dt);
|
||||
}
|
||||
}
|
||||
private void IsDBUsedElseWhere(IBackup backup, RequestInfo info)
|
||||
{
|
||||
info.OutputOK(new { inuse = Library.Main.DatabaseLocator.IsDatabasePathInUse(backup.DBPath) });
|
||||
}
|
||||
|
||||
public static void RemovePasswords(IBackup backup)
|
||||
{
|
||||
backup.SanitizeSettings();
|
||||
backup.SanitizeTargetUrl();
|
||||
}
|
||||
|
||||
private void Export(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false);
|
||||
var argsonly = Library.Utility.Utility.ParseBool(info.Request.QueryString["argsonly"].Value, false);
|
||||
var exportPasswords = Library.Utility.Utility.ParseBool(info.Request.QueryString["export-passwords"].Value, false);
|
||||
if (!exportPasswords)
|
||||
{
|
||||
Backup.RemovePasswords(backup);
|
||||
}
|
||||
|
||||
if (cmdline)
|
||||
{
|
||||
info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) });
|
||||
}
|
||||
else if (argsonly)
|
||||
{
|
||||
var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup));
|
||||
|
||||
info.OutputOK(new {
|
||||
Backend = parts.First(),
|
||||
Arguments = parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)),
|
||||
Options = parts.Skip(1).Where(x => x.StartsWith("--", StringComparison.Ordinal))
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var passphrase = info.Request.QueryString["passphrase"].Value;
|
||||
byte[] data = Backup.ExportToJSON(backup, passphrase);
|
||||
|
||||
string filename = Library.Utility.Uri.UrlEncode(backup.Name) + "-duplicati-config.json";
|
||||
if (!string.IsNullOrWhiteSpace(passphrase))
|
||||
{
|
||||
filename += ".aes";
|
||||
}
|
||||
|
||||
info.Response.ContentLength = data.Length;
|
||||
info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename));
|
||||
info.Response.ContentType = "application/octet-stream";
|
||||
|
||||
info.BodyWriter.SetOK();
|
||||
info.Response.SendHeaders();
|
||||
info.Response.SendBody(data);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] ExportToJSON(IBackup backup, string passphrase)
|
||||
{
|
||||
Serializable.ImportExportStructure ipx = FIXMEGlobal.DataConnection.PrepareBackupForExport(backup);
|
||||
|
||||
byte[] data;
|
||||
using (MemoryStream ms = new System.IO.MemoryStream())
|
||||
{
|
||||
using (StreamWriter sw = new System.IO.StreamWriter(ms))
|
||||
{
|
||||
Serializer.SerializeJson(sw, ipx, true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(passphrase))
|
||||
{
|
||||
ms.Position = 0;
|
||||
using (MemoryStream ms2 = new System.IO.MemoryStream())
|
||||
{
|
||||
using (Library.Encryption.AESEncryption m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary<string, string>()))
|
||||
{
|
||||
m.Encrypt(ms, ms2);
|
||||
data = ms2.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
data = ms.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private void RestoreFiles(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var input = info.Request.Form;
|
||||
|
||||
string[] filters = parsePaths(input["paths"].Value ?? string.Empty);
|
||||
|
||||
var passphrase = string.IsNullOrEmpty(input["passphrase"].Value) ? null : input["passphrase"].Value;
|
||||
|
||||
var time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(input["time"].Value, DateTime.Now);
|
||||
var restoreTarget = input["restore-path"].Value;
|
||||
var overwrite = Duplicati.Library.Utility.Utility.ParseBool(input["overwrite"].Value, false);
|
||||
|
||||
var permissions = Duplicati.Library.Utility.Utility.ParseBool(input["permissions"].Value, false);
|
||||
var skip_metadata = Duplicati.Library.Utility.Utility.ParseBool(input["skip-metadata"].Value, false);
|
||||
|
||||
var task = Runner.CreateRestoreTask(backup, filters, time, restoreTarget, overwrite, permissions, skip_metadata, passphrase);
|
||||
|
||||
FIXMEGlobal.WorkThread.AddTask(task);
|
||||
|
||||
info.OutputOK(new { TaskID = task.TaskID });
|
||||
}
|
||||
|
||||
private void CreateReport(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var task = Runner.CreateTask(DuplicatiOperation.CreateReport, backup);
|
||||
FIXMEGlobal.WorkThread.AddTask(task);
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
||||
}
|
||||
|
||||
private void ReportRemoteSize(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var task = Runner.CreateTask(DuplicatiOperation.ListRemote, backup);
|
||||
FIXMEGlobal.WorkThread.AddTask(task);
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
||||
}
|
||||
|
||||
private void Repair(IBackup backup, RequestInfo info)
|
||||
{
|
||||
DoRepair(backup, info, false);
|
||||
}
|
||||
|
||||
private void RepairUpdate(IBackup backup, RequestInfo info)
|
||||
{
|
||||
DoRepair(backup, info, true);
|
||||
}
|
||||
|
||||
private void Vacuum(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var task = Runner.CreateTask(DuplicatiOperation.Vacuum, backup);
|
||||
FIXMEGlobal.WorkThread.AddTask(task);
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
||||
}
|
||||
|
||||
private void Verify(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var task = Runner.CreateTask(DuplicatiOperation.Verify, backup);
|
||||
FIXMEGlobal.WorkThread.AddTask(task);
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
info.OutputOK(new {Status = "OK", ID = task.TaskID});
|
||||
}
|
||||
|
||||
private void Compact(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var task = Runner.CreateTask(DuplicatiOperation.Compact, backup);
|
||||
FIXMEGlobal.WorkThread.AddTask(task);
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
||||
}
|
||||
|
||||
private string[] parsePaths(string paths)
|
||||
{
|
||||
string[] filters;
|
||||
var rawpaths = (paths ?? string.Empty).Trim();
|
||||
|
||||
// We send the file list as a JSON array to avoid encoding issues with the path separator
|
||||
// as it is an allowed character in file and path names.
|
||||
// We also accept the old way, for compatibility with the greeno theme
|
||||
if (!string.IsNullOrWhiteSpace(rawpaths) && rawpaths.StartsWith("[", StringComparison.Ordinal) && rawpaths.EndsWith("]", StringComparison.Ordinal))
|
||||
filters = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(rawpaths);
|
||||
else
|
||||
filters = paths.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
private void DoRepair(IBackup backup, RequestInfo info, bool repairUpdate)
|
||||
{
|
||||
var input = info.Request.Form;
|
||||
string[] filters = null;
|
||||
var extra = new Dictionary<string, string>();
|
||||
if (input["only-paths"].Value != null)
|
||||
extra["repair-only-paths"] = (Library.Utility.Utility.ParseBool(input["only-paths"].Value, false)).ToString();
|
||||
if (input["time"].Value != null)
|
||||
extra["time"] = input["time"].Value;
|
||||
if (input["version"].Value != null)
|
||||
extra["version"] = input["version"].Value;
|
||||
if (input["paths"].Value != null)
|
||||
filters = parsePaths(input["paths"].Value);
|
||||
|
||||
var task = Runner.CreateTask(repairUpdate ? DuplicatiOperation.RepairUpdate : DuplicatiOperation.Repair, backup, extra, filters);
|
||||
FIXMEGlobal.WorkThread.AddTask(task);
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
info.OutputOK(new {Status = "OK", ID = task.TaskID});
|
||||
}
|
||||
|
||||
private void RunBackup(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
var bt = t == null ? null : t.Backup;
|
||||
if (bt != null && backup.ID == bt.ID)
|
||||
{
|
||||
// Already running
|
||||
}
|
||||
else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x => {
|
||||
var bn = x?.Backup;
|
||||
return bn == null || bn.ID == backup.ID;
|
||||
}))
|
||||
{
|
||||
// Already in queue
|
||||
}
|
||||
else
|
||||
{
|
||||
FIXMEGlobal.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true);
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
info.OutputOK();
|
||||
}
|
||||
|
||||
private void IsActive(IBackup backup, RequestInfo info)
|
||||
{
|
||||
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
var bt = t?.Backup;
|
||||
if (bt != null && backup.ID == bt.ID)
|
||||
{
|
||||
info.OutputOK(new { Status = "OK", Active = true });
|
||||
return;
|
||||
}
|
||||
else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x =>
|
||||
{
|
||||
var bn = x?.Backup;
|
||||
return bn == null || bn.ID == backup.ID;
|
||||
}))
|
||||
{
|
||||
info.OutputOK(new { Status = "OK", Active = true });
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
info.OutputOK(new { Status = "OK", Active = false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDatabasePath(IBackup backup, RequestInfo info, bool move)
|
||||
{
|
||||
var np = info.Request.Form["path"].Value;
|
||||
if (string.IsNullOrWhiteSpace(np))
|
||||
info.ReportClientError("No target path supplied", System.Net.HttpStatusCode.BadRequest);
|
||||
else if (!Path.IsPathRooted(np))
|
||||
info.ReportClientError("Target path is relative, please supply a fully qualified path", System.Net.HttpStatusCode.BadRequest);
|
||||
else
|
||||
{
|
||||
if (move && (File.Exists(np) || Directory.Exists(np)))
|
||||
info.ReportClientError("A file already exists at the new location", System.Net.HttpStatusCode.Conflict);
|
||||
else
|
||||
{
|
||||
if (move)
|
||||
File.Move(backup.DBPath, np);
|
||||
|
||||
FIXMEGlobal.DataConnection.UpdateBackupDBPath(backup, np);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var parts = (key ?? "").Split(new char[] { '/' }, 2);
|
||||
var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First());
|
||||
if (bk == null)
|
||||
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
|
||||
else
|
||||
{
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
var operation = parts.Last().Split(new char[] {'/'}).First().ToLowerInvariant();
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case "files":
|
||||
var filter = parts.Last().Split(new char[] { '/' }, 2).Skip(1).FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(info.Request.QueryString["filter"].Value))
|
||||
filter = info.Request.QueryString["filter"].Value;
|
||||
SearchFiles(bk, filter, info);
|
||||
return;
|
||||
case "log":
|
||||
FetchLogData(bk, info);
|
||||
return;
|
||||
case "remotelog":
|
||||
FetchRemoteLogData(bk, info);
|
||||
return;
|
||||
case "filesets":
|
||||
ListFileSets(bk, info);
|
||||
return;
|
||||
case "export":
|
||||
Export(bk, info);
|
||||
return;
|
||||
case "isdbusedelsewhere":
|
||||
IsDBUsedElseWhere(bk, info);
|
||||
return;
|
||||
case "isactive":
|
||||
IsActive(bk, info);
|
||||
return;
|
||||
default:
|
||||
info.ReportClientError(string.Format("Invalid component: {0}", operation), System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var scheduleId = FIXMEGlobal.DataConnection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID });
|
||||
var schedule = scheduleId.Any() ? FIXMEGlobal.DataConnection.GetSchedule(scheduleId.First()) : null;
|
||||
var sourcenames = SpecialFolders.GetSourceNames(bk);
|
||||
|
||||
//TODO: Filter out the password in both settings and the target url
|
||||
|
||||
info.OutputOK(new GetResponse()
|
||||
{
|
||||
success = true,
|
||||
data = new GetResponse.GetResponseData {
|
||||
Schedule = schedule,
|
||||
Backup = bk,
|
||||
DisplayNames = sourcenames
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
var parts = (key ?? "").Split(new char[] { '/' }, 2);
|
||||
var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First());
|
||||
if (bk == null)
|
||||
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
|
||||
else
|
||||
{
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
var operation = parts.Last().Split(new char[] { '/' }).First().ToLowerInvariant();
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case "deletedb":
|
||||
System.IO.File.Delete(bk.DBPath);
|
||||
info.OutputOK();
|
||||
return;
|
||||
|
||||
case "movedb":
|
||||
UpdateDatabasePath(bk, info, true);
|
||||
return;
|
||||
|
||||
case "updatedb":
|
||||
UpdateDatabasePath(bk, info, false);
|
||||
return;
|
||||
|
||||
case "restore":
|
||||
RestoreFiles(bk, info);
|
||||
return;
|
||||
|
||||
case "createreport":
|
||||
CreateReport(bk, info);
|
||||
return;
|
||||
|
||||
case "repair":
|
||||
Repair(bk, info);
|
||||
return;
|
||||
|
||||
case "repairupdate":
|
||||
RepairUpdate(bk, info);
|
||||
return;
|
||||
|
||||
case "vacuum":
|
||||
Vacuum(bk, info);
|
||||
return;
|
||||
|
||||
case "verify":
|
||||
Verify(bk, info);
|
||||
return;
|
||||
|
||||
case "compact":
|
||||
Compact(bk, info);
|
||||
return;
|
||||
|
||||
case "start":
|
||||
case "run":
|
||||
RunBackup(bk, info);
|
||||
return;
|
||||
|
||||
case "report-remote-size":
|
||||
ReportRemoteSize(bk, info);
|
||||
return;
|
||||
|
||||
case "copytotemp":
|
||||
var ipx = Serializer.Deserialize<Database.Backup>(new StringReader(Newtonsoft.Json.JsonConvert.SerializeObject(bk)));
|
||||
|
||||
using(var tf = new Duplicati.Library.Utility.TempFile())
|
||||
ipx.DBPath = tf;
|
||||
ipx.ID = null;
|
||||
|
||||
info.OutputOK(new { status = "OK", ID = FIXMEGlobal.DataConnection.RegisterTemporaryBackup(ipx) });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
public void PUT(string key, RequestInfo info)
|
||||
{
|
||||
string str = info.Request.Form["data"].Value;
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
str = System.Threading.Tasks.Task.Run(async () =>
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
||||
|
||||
return await sr.ReadToEndAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
Backups.AddOrUpdateBackupData data = null;
|
||||
try
|
||||
{
|
||||
data = Serializer.Deserialize<Backups.AddOrUpdateBackupData>(new StringReader(str));
|
||||
if (data.Backup == null)
|
||||
{
|
||||
info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
data.Backup.ID = key;
|
||||
|
||||
if (string.IsNullOrEmpty(data.Backup.ID))
|
||||
{
|
||||
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (data.Backup.IsTemporary)
|
||||
{
|
||||
var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID);
|
||||
if (backup.IsTemporary)
|
||||
throw new InvalidDataException("External is temporary but internal is not?");
|
||||
|
||||
FIXMEGlobal.DataConnection.UpdateTemporaryBackup(backup);
|
||||
info.OutputOK();
|
||||
}
|
||||
else
|
||||
{
|
||||
lock(FIXMEGlobal.DataConnection.m_lock)
|
||||
{
|
||||
var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID);
|
||||
if (backup == null)
|
||||
{
|
||||
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase) && x.ID != data.Backup.ID))
|
||||
{
|
||||
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict);
|
||||
return;
|
||||
}
|
||||
|
||||
var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule);
|
||||
if (!string.IsNullOrWhiteSpace(err))
|
||||
{
|
||||
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO: Merge in real passwords where the placeholder is found
|
||||
FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule);
|
||||
|
||||
}
|
||||
|
||||
info.OutputOK();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
|
||||
else
|
||||
info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public void DELETE(string key, RequestInfo info)
|
||||
{
|
||||
var backup = FIXMEGlobal.DataConnection.GetBackup(key);
|
||||
if (backup == null)
|
||||
{
|
||||
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
var delete_remote_files = Library.Utility.Utility.ParseBool(info.Request.Param["delete-remote-files"].Value, false);
|
||||
|
||||
if (delete_remote_files)
|
||||
{
|
||||
var captcha_token = info.Request.Param["captcha-token"].Value;
|
||||
var captcha_answer = info.Request.Param["captcha-answer"].Value;
|
||||
if (string.IsNullOrWhiteSpace(captcha_token) || string.IsNullOrWhiteSpace(captcha_answer))
|
||||
{
|
||||
info.ReportClientError("Missing captcha", System.Net.HttpStatusCode.Unauthorized);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Captcha.SolvedCaptcha(captcha_token, "DELETE /backup/" + backup.ID, captcha_answer))
|
||||
{
|
||||
info.ReportClientError("Invalid captcha", System.Net.HttpStatusCode.Forbidden);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (FIXMEGlobal.WorkThread.Active)
|
||||
{
|
||||
try
|
||||
{
|
||||
//TODO: It's not safe to access the values like this,
|
||||
//because the runner thread might interfere
|
||||
var nt = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
if (backup.Equals(nt == null ? null : nt.Backup))
|
||||
{
|
||||
bool force;
|
||||
if (!bool.TryParse(info.Request.QueryString["force"].Value, out force))
|
||||
force = false;
|
||||
|
||||
if (!force)
|
||||
{
|
||||
info.OutputError(new { status = "failed", reason = "backup-in-progress" });
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasPaused = FIXMEGlobal.LiveControl.State == LiveControls.LiveControlState.Paused;
|
||||
FIXMEGlobal.LiveControl.Pause();
|
||||
|
||||
for(int i = 0; i < 10; i++)
|
||||
if (FIXMEGlobal.WorkThread.Active)
|
||||
{
|
||||
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
if (backup.Equals(t == null ? null : t.Backup))
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
else
|
||||
break;
|
||||
}
|
||||
else
|
||||
break;
|
||||
|
||||
if (FIXMEGlobal.WorkThread.Active)
|
||||
{
|
||||
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
if (backup.Equals(t == null ? null : t.Backup))
|
||||
{
|
||||
if (hasPaused)
|
||||
FIXMEGlobal.LiveControl.Resume();
|
||||
info.OutputError(new { status = "failed", reason = "backup-unstoppable" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPaused)
|
||||
FIXMEGlobal.LiveControl.Resume();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
info.OutputError(new { status = "error", message = ex.Message });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var extra = new Dictionary<string, string>();
|
||||
if (!string.IsNullOrWhiteSpace(info.Request.Param["delete-local-db"].Value))
|
||||
extra["delete-local-db"] = info.Request.Param["delete-local-db"].Value;
|
||||
if (delete_remote_files)
|
||||
extra["delete-remote-files"] = "true";
|
||||
|
||||
var task = Runner.CreateTask(DuplicatiOperation.Delete, backup, extra);
|
||||
FIXMEGlobal.WorkThread.AddTask(task);
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
||||
}
|
||||
public string Description { get { return "Retrieves, updates or deletes an existing backup and schedule"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(GetResponse)),
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)),
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Delete, typeof(long))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Library.Utility;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class BackupDefaults : IRESTMethodGET
|
||||
{
|
||||
|
||||
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<BackupDefaults>();
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
// Start with a scratch object
|
||||
var o = new Newtonsoft.Json.Linq.JObject();
|
||||
|
||||
// Add application wide settings
|
||||
o.Add("ApplicationOptions", new Newtonsoft.Json.Linq.JArray(
|
||||
from n in FIXMEGlobal.DataConnection.Settings
|
||||
select Newtonsoft.Json.Linq.JObject.FromObject(n)
|
||||
));
|
||||
|
||||
try
|
||||
{
|
||||
// Add built-in defaults
|
||||
Newtonsoft.Json.Linq.JObject n;
|
||||
using(var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(FIXMEGlobal), "newbackup.json")))
|
||||
n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd());
|
||||
|
||||
MergeJsonObjects(o, n);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to locate embeded backup defaults");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Add install defaults/overrides, if present
|
||||
var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, "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 (Exception e)
|
||||
{
|
||||
Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to process newbackup.json");
|
||||
}
|
||||
|
||||
info.OutputOK(new
|
||||
{
|
||||
success = true,
|
||||
data = o
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Server.Serialization;
|
||||
using System.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Backups : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
|
||||
{
|
||||
public class AddOrUpdateBackupData
|
||||
{
|
||||
public Boolean IsUnencryptedOrPassphraseStored { get; set;}
|
||||
public Database.Schedule Schedule { get; set;}
|
||||
public Database.Backup Backup { get; set;}
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var schedules = FIXMEGlobal.DataConnection.Schedules;
|
||||
var backups = FIXMEGlobal.DataConnection.Backups;
|
||||
|
||||
var all = from n in backups
|
||||
select new AddOrUpdateBackupData {
|
||||
IsUnencryptedOrPassphraseStored = FIXMEGlobal.DataConnection.IsUnencryptedOrPassphraseStored(long.Parse(n.ID)),
|
||||
Backup = (Database.Backup)n,
|
||||
Schedule =
|
||||
(from x in schedules
|
||||
where x.Tags != null && x.Tags.Contains("ID=" + n.ID)
|
||||
select (Database.Schedule)x).FirstOrDefault()
|
||||
};
|
||||
|
||||
info.BodyWriter.OutputOK(all.ToArray());
|
||||
}
|
||||
|
||||
private void ImportBackup(RequestInfo info)
|
||||
{
|
||||
var output_template = "<html><body><script type=\"text/javascript\">var jso = 'JSO'; var rp = null; try { rp = parent['CBM']; } catch (e) {}; if (rp) { rp('MSG', jso); } else { alert; rp('MSG'); };</script></body></html>";
|
||||
//output_template = "<html><body><script type=\"text/javascript\">alert('MSG');</script></body></html>";
|
||||
try
|
||||
{
|
||||
var input = info.Request.Form;
|
||||
var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false);
|
||||
var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false);
|
||||
var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false);
|
||||
output_template = output_template.Replace("CBM", input["callback"].Value);
|
||||
if (cmdline)
|
||||
{
|
||||
info.Response.ContentType = "text/html";
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", "Import from commandline not yet implemented"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var file = info.Request.Form.GetFile("config");
|
||||
if (file == null)
|
||||
throw new Exception("No file uploaded");
|
||||
|
||||
Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value);
|
||||
if (direct)
|
||||
{
|
||||
lock (FIXMEGlobal.DataConnection.m_lock)
|
||||
{
|
||||
var basename = ipx.Backup.Name;
|
||||
var c = 0;
|
||||
while (c++ < 100 && FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
ipx.Backup.Name = basename + " (" + c.ToString() + ")";
|
||||
|
||||
if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
info.BodyWriter.SetOK();
|
||||
info.Response.ContentType = "text/html";
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", "There already exists a backup with the name: " + basename.Replace("\'", "\\'")));
|
||||
}
|
||||
|
||||
var err = FIXMEGlobal.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule);
|
||||
if (!string.IsNullOrWhiteSpace(err))
|
||||
{
|
||||
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule);
|
||||
}
|
||||
|
||||
info.Response.ContentType = "text/html";
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", "OK"));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var sw = new StringWriter())
|
||||
{
|
||||
Serializer.SerializeJson(sw, ipx, true);
|
||||
output_template = output_template.Replace("'JSO'", sw.ToString());
|
||||
}
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", "Import completed, but a browser issue prevents loading the contents. Try using the direct import method instead."));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError("", "Failed to import backup", ex);
|
||||
info.Response.ContentType = "text/html";
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", ex.Message.Replace("\'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n")));
|
||||
}
|
||||
}
|
||||
|
||||
public static Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func<string> getPassword, Dictionary<string, string> advancedOptions)
|
||||
{
|
||||
// This removes the ID and DBPath from the backup configuration.
|
||||
Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword);
|
||||
|
||||
// This will create the Duplicati-server.sqlite database file if it doesn't exist.
|
||||
using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions))
|
||||
{
|
||||
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists.");
|
||||
}
|
||||
|
||||
string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule);
|
||||
if (!string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
throw new InvalidOperationException(error);
|
||||
}
|
||||
|
||||
// This creates a new ID and DBPath.
|
||||
connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule);
|
||||
}
|
||||
|
||||
return importedStructure;
|
||||
}
|
||||
|
||||
private static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func<string> getPassword)
|
||||
{
|
||||
Serializable.ImportExportStructure ipx;
|
||||
|
||||
var buf = new byte[3];
|
||||
using (var fs = System.IO.File.OpenRead(filename))
|
||||
{
|
||||
Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length);
|
||||
|
||||
fs.Position = 0;
|
||||
if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S')
|
||||
{
|
||||
using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary<string, string>()))
|
||||
{
|
||||
using (var m2 = m.Decrypt(fs))
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(m2))
|
||||
{
|
||||
ipx = Serializer.Deserialize<Serializable.ImportExportStructure>(sr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(fs))
|
||||
{
|
||||
ipx = Serializer.Deserialize<Serializable.ImportExportStructure>(sr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ipx.Backup == null)
|
||||
{
|
||||
throw new Exception("No backup found in document");
|
||||
}
|
||||
|
||||
if (ipx.Backup.Metadata == null)
|
||||
{
|
||||
ipx.Backup.Metadata = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
if (!importMetadata)
|
||||
{
|
||||
ipx.Backup.Metadata.Clear();
|
||||
}
|
||||
|
||||
ipx.Backup.ID = null;
|
||||
ipx.Backup.DBPath = null;
|
||||
|
||||
if (ipx.Schedule != null)
|
||||
{
|
||||
ipx.Schedule.ID = -1;
|
||||
}
|
||||
|
||||
return ipx;
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
if ("import".Equals(key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ImportBackup(info);
|
||||
return;
|
||||
}
|
||||
|
||||
AddOrUpdateBackupData data = null;
|
||||
try
|
||||
{
|
||||
var str = info.Request.Form["data"].Value;
|
||||
if (string.IsNullOrWhiteSpace(str)) {
|
||||
str = System.Threading.Tasks.Task.Run(async () =>
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
||||
|
||||
return await sr.ReadToEndAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
data = Serializer.Deserialize<AddOrUpdateBackupData>(new StringReader(str));
|
||||
if (data.Backup == null)
|
||||
{
|
||||
info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
data.Backup.ID = null;
|
||||
|
||||
if (Duplicati.Library.Utility.Utility.ParseBool(info.Request.Form["temporary"].Value, false))
|
||||
{
|
||||
using(var tf = new Duplicati.Library.Utility.TempFile())
|
||||
data.Backup.DBPath = tf;
|
||||
|
||||
data.Backup.Filters = data.Backup.Filters ?? new Duplicati.Server.Serialization.Interface.IFilter[0];
|
||||
data.Backup.Settings = data.Backup.Settings ?? new Duplicati.Server.Serialization.Interface.ISetting[0];
|
||||
|
||||
FIXMEGlobal.DataConnection.RegisterTemporaryBackup(data.Backup);
|
||||
|
||||
info.OutputOK(new { status = "OK", ID = data.Backup.ID });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Library.Utility.Utility.ParseBool(info.Request.Form["existing_db"].Value, false))
|
||||
{
|
||||
data.Backup.DBPath = Library.Main.DatabaseLocator.GetDatabasePath(data.Backup.TargetURL, null, false, false);
|
||||
if (string.IsNullOrWhiteSpace(data.Backup.DBPath))
|
||||
throw new Exception("Unable to find remote db path?");
|
||||
}
|
||||
|
||||
|
||||
lock(FIXMEGlobal.DataConnection.m_lock)
|
||||
{
|
||||
if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict);
|
||||
return;
|
||||
}
|
||||
|
||||
var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule);
|
||||
if (!string.IsNullOrWhiteSpace(err))
|
||||
{
|
||||
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule);
|
||||
}
|
||||
|
||||
info.OutputOK(new { status = "OK", ID = data.Backup.ID });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
|
||||
else
|
||||
info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string Description { get { return "Return a list of current backups and their schedules"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])),
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Post, typeof(AddOrUpdateBackupData))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class BugReport : IRESTMethodGET
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
long id;
|
||||
long.TryParse(key, out id);
|
||||
|
||||
var tf = FIXMEGlobal.DataConnection.GetTempFiles().FirstOrDefault(x => x.ID == id);
|
||||
if (tf == null)
|
||||
{
|
||||
info.ReportClientError("Invalid or missing bugreport id", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!System.IO.File.Exists(tf.Path))
|
||||
{
|
||||
info.ReportClientError("File is missing", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
var filename = "bugreport.zip";
|
||||
using(var fs = System.IO.File.OpenRead(tf.Path))
|
||||
{
|
||||
info.Response.ContentLength = fs.Length;
|
||||
info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename));
|
||||
info.Response.ContentType = "application/octet-stream";
|
||||
|
||||
info.BodyWriter.SetOK();
|
||||
info.Response.SendHeaders();
|
||||
fs.CopyTo(info.Response.Body);
|
||||
info.Response.Send();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Captcha : IRESTMethodGET, IRESTMethodPOST
|
||||
{
|
||||
private class CaptchaEntry
|
||||
{
|
||||
public readonly string Answer;
|
||||
public readonly string Target;
|
||||
public int Attempts;
|
||||
public readonly DateTime Expires;
|
||||
|
||||
public CaptchaEntry(string answer, string target)
|
||||
{
|
||||
Answer = answer;
|
||||
Target = target;
|
||||
Attempts = 4;
|
||||
Expires = DateTime.Now.AddMinutes(2);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly object m_lock = new object();
|
||||
private static readonly Dictionary<string, CaptchaEntry> m_captchas = new Dictionary<string, CaptchaEntry>();
|
||||
|
||||
public static bool SolvedCaptcha(string token, string target, string answer)
|
||||
{
|
||||
lock(m_lock)
|
||||
{
|
||||
CaptchaEntry tp;
|
||||
m_captchas.TryGetValue(token ?? string.Empty, out tp);
|
||||
if (tp == null)
|
||||
return false;
|
||||
|
||||
if (tp.Attempts > 0)
|
||||
tp.Attempts--;
|
||||
|
||||
return tp.Attempts >= 0 && string.Equals(tp.Answer, answer, StringComparison.OrdinalIgnoreCase) && tp.Target == target && tp.Expires >= DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
string answer = null;
|
||||
lock (m_lock)
|
||||
{
|
||||
CaptchaEntry tp;
|
||||
m_captchas.TryGetValue(key, out tp);
|
||||
if (tp != null && tp.Expires > DateTime.Now)
|
||||
answer = tp.Answer;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(answer))
|
||||
{
|
||||
info.ReportClientError("No such entry", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var bmp = CaptchaUtil.CreateCaptcha(answer))
|
||||
using (var ms = new System.IO.MemoryStream())
|
||||
{
|
||||
info.Response.ContentType = "image/jpeg";
|
||||
info.Response.ContentLength = ms.Length;
|
||||
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
|
||||
ms.Position = 0;
|
||||
|
||||
info.Response.ContentType = "image/jpeg";
|
||||
info.Response.ContentLength = ms.Length;
|
||||
info.Response.SendHeaders();
|
||||
ms.CopyTo(info.Response.Body);
|
||||
info.Response.Send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
var target = info.Request.Param["target"].Value;
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6);
|
||||
var nonce = Guid.NewGuid().ToString();
|
||||
|
||||
string token;
|
||||
using (var ms = new System.IO.MemoryStream())
|
||||
{
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(answer + nonce);
|
||||
ms.Write(bytes, 0, bytes.Length);
|
||||
ms.Position = 0;
|
||||
using(var hasher = Library.Utility.HashFactory.CreateHasher(Library.Utility.HashFactory.SHA256)){
|
||||
token = Library.Utility.Utility.Base64PlainToBase64Url(Convert.ToBase64String(hasher.ComputeHash(ms)));
|
||||
}
|
||||
}
|
||||
|
||||
lock (m_lock)
|
||||
{
|
||||
var expired = m_captchas.Where(x => x.Value.Expires < DateTime.Now).Select(x => x.Key).ToArray();
|
||||
foreach (var x in expired)
|
||||
m_captchas.Remove(x);
|
||||
|
||||
if (m_captchas.Count > 3)
|
||||
{
|
||||
info.ReportClientError("Too many captchas, wait 2 minutes and try again", System.Net.HttpStatusCode.ServiceUnavailable);
|
||||
return;
|
||||
}
|
||||
|
||||
m_captchas[token] = new CaptchaEntry(answer, target);
|
||||
}
|
||||
|
||||
info.OutputOK(new
|
||||
{
|
||||
token = token
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var answer = info.Request.Param["answer"].Value;
|
||||
var target = info.Request.Param["target"].Value;
|
||||
if (string.IsNullOrWhiteSpace(answer))
|
||||
{
|
||||
info.ReportClientError("Missing answer parameter", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SolvedCaptcha(key, target, answer))
|
||||
info.OutputOK();
|
||||
else
|
||||
info.ReportClientError("Incorrect", System.Net.HttpStatusCode.Forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Changelog : IRESTMethodGET, IRESTMethodDocumented
|
||||
{
|
||||
private class GetResponse
|
||||
{
|
||||
public string Status;
|
||||
public string Version;
|
||||
public string Changelog;
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var fromUpdate = info.Request.QueryString["from-update"].Value;
|
||||
if (!Library.Utility.Utility.ParseBool(fromUpdate, false))
|
||||
{
|
||||
var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "changelog.txt");
|
||||
info.OutputOK(new GetResponse() {
|
||||
Status = "OK",
|
||||
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
|
||||
Changelog = System.IO.File.ReadAllText(path)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var updateInfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
|
||||
if (updateInfo == null)
|
||||
{
|
||||
info.ReportClientError("No update found", System.Net.HttpStatusCode.NotFound);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.OutputOK(new GetResponse() {
|
||||
Status = "OK",
|
||||
Version = updateInfo.Version,
|
||||
Changelog = updateInfo.ChangeInfo
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get { return "Gets the current changelog"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(GetResponse)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class CommandLine : IRESTMethodGET, IRESTMethodPOST
|
||||
{
|
||||
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<CommandLine>();
|
||||
|
||||
private class LogWriter : System.IO.TextWriter
|
||||
{
|
||||
private readonly ActiveRun m_target;
|
||||
private readonly StringBuilder m_sb = new StringBuilder();
|
||||
private int m_newlinechars = 0;
|
||||
|
||||
public LogWriter(ActiveRun target)
|
||||
{
|
||||
m_target = target;
|
||||
}
|
||||
|
||||
public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } }
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
lock(m_target.Lock)
|
||||
{
|
||||
m_sb.Append(value);
|
||||
if (NewLine[m_newlinechars] == value)
|
||||
{
|
||||
m_newlinechars++;
|
||||
if (m_newlinechars == NewLine.Length)
|
||||
WriteLine(string.Empty);
|
||||
}
|
||||
else
|
||||
m_newlinechars = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteLine(string value)
|
||||
{
|
||||
value = value ?? string.Empty;
|
||||
lock(m_target.Lock)
|
||||
{
|
||||
m_target.LastAccess = DateTime.Now;
|
||||
|
||||
//Avoid writing the log if it does not exist
|
||||
if (m_target.IsLogDisposed)
|
||||
{
|
||||
FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (m_sb.Length != 0)
|
||||
{
|
||||
m_target.Log.Add(m_sb + value);
|
||||
m_sb.Length = 0;
|
||||
m_newlinechars = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_target.Log.Add(value);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This can happen on a very unlucky race where IsLogDisposed is set right after the check
|
||||
FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ActiveRun
|
||||
{
|
||||
public readonly string ID = Guid.NewGuid().ToString();
|
||||
public DateTime LastAccess = DateTime.Now;
|
||||
public readonly Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList();
|
||||
public Runner.IRunnerData Task;
|
||||
public LogWriter Writer;
|
||||
public readonly object Lock = new object();
|
||||
public bool Finished = false;
|
||||
public bool Started = false;
|
||||
public bool IsLogDisposed = false;
|
||||
public System.Threading.Thread Thread;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, ActiveRun> m_activeItems = new Dictionary<string, ActiveRun>();
|
||||
private System.Threading.Tasks.Task m_cleanupTask;
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
string[] args;
|
||||
var str = System.Threading.Tasks.Task.Run(async () =>
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
||||
|
||||
return await sr.ReadToEndAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
args = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(str);
|
||||
|
||||
var k = new ActiveRun();
|
||||
k.Writer = new LogWriter(k);
|
||||
|
||||
m_activeItems[k.ID] = k;
|
||||
StartCleanupTask();
|
||||
|
||||
k.Task = Runner.CreateCustomTask((sink) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
k.Thread = System.Threading.Thread.CurrentThread;
|
||||
k.Started = true;
|
||||
|
||||
var code = Duplicati.CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c => {
|
||||
k.Task.SetController(c);
|
||||
c.AppendSink(sink);
|
||||
}, args);
|
||||
k.Writer.WriteLine("Return code: {0}", code);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var rx = ex;
|
||||
if (rx is System.Reflection.TargetInvocationException)
|
||||
rx = rx.InnerException;
|
||||
|
||||
if (rx is Library.Interface.UserInformationException)
|
||||
k.Log.Add(rx.Message);
|
||||
else
|
||||
k.Log.Add(rx.ToString());
|
||||
|
||||
throw rx;
|
||||
}
|
||||
finally
|
||||
{
|
||||
k.Finished = true;
|
||||
k.Thread = null;
|
||||
}
|
||||
});
|
||||
|
||||
FIXMEGlobal.WorkThread.AddTask(k.Task);
|
||||
|
||||
info.OutputOK(new
|
||||
{
|
||||
ID = k.ID
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!key.EndsWith("/abort", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
info.ReportClientError("Only abort commands are allowed", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
key = key.Substring(0, key.Length - "/abort".Length);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.ReportClientError("No task key found", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
ActiveRun t;
|
||||
if (!m_activeItems.TryGetValue(key, out t))
|
||||
{
|
||||
info.OutputError(code: System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
var tt = t.Task;
|
||||
if (tt != null)
|
||||
tt.Abort();
|
||||
|
||||
var tr = t.Thread;
|
||||
if (tr != null)
|
||||
tr.Interrupt();
|
||||
|
||||
info.OutputOK();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCleanupTask()
|
||||
{
|
||||
if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled)
|
||||
m_cleanupTask = RunCleanupAsync();
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task RunCleanupAsync()
|
||||
{
|
||||
while (m_activeItems.Count > 0)
|
||||
{
|
||||
var oldest = m_activeItems.Values
|
||||
.OrderBy(x => x.LastAccess)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (oldest != null)
|
||||
{
|
||||
// If the task has finished, we just wait a little to allow the UI to pick it up
|
||||
var timeout = oldest.Finished ? TimeSpan.FromMinutes(5) : TimeSpan.FromDays(1);
|
||||
if (DateTime.Now - oldest.LastAccess > timeout)
|
||||
{
|
||||
oldest.IsLogDisposed = true;
|
||||
m_activeItems.Remove(oldest.ID);
|
||||
oldest.Log.Dispose();
|
||||
|
||||
// Fix all expired, or stop running
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await System.Threading.Tasks.Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.OutputOK(
|
||||
Duplicati.CommandLine.Program.SupportedCommands
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
ActiveRun t;
|
||||
if (!m_activeItems.TryGetValue(key, out t))
|
||||
{
|
||||
info.OutputError(code: System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
int pagesize;
|
||||
int offset;
|
||||
|
||||
int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize);
|
||||
int.TryParse(info.Request.QueryString["offset"].Value, out offset);
|
||||
pagesize = Math.Max(10, Math.Min(500, pagesize));
|
||||
offset = Math.Max(0, offset);
|
||||
var items = new List<string>();
|
||||
long count;
|
||||
bool started;
|
||||
bool finished;
|
||||
|
||||
lock(t.Lock)
|
||||
{
|
||||
t.LastAccess = DateTime.Now;
|
||||
count = t.Log.Count;
|
||||
offset = Math.Min((int)count, offset);
|
||||
items.AddRange(t.Log.Skip(offset).Take(pagesize));
|
||||
finished = t.Finished;
|
||||
started = t.Started;
|
||||
}
|
||||
|
||||
info.OutputOK(new
|
||||
{
|
||||
Pagesize = pagesize,
|
||||
Offset = offset,
|
||||
Count = count,
|
||||
Items = items,
|
||||
Finished = finished,
|
||||
Started = started
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using Duplicati.Library.Snapshots;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Common;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Filesystem : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var parts = (key ?? "").Split(new char[] { '/' });
|
||||
var path = Duplicati.Library.Utility.Uri.UrlDecode((parts.Length == 2 ? parts.FirstOrDefault() : key ?? ""));
|
||||
var command = parts.Length == 2 ? parts.Last() : null;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
path = info.Request.QueryString["path"].Value;
|
||||
|
||||
Process(command, path, info);
|
||||
}
|
||||
|
||||
private void Process(string command, string path, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
info.ReportClientError("No path parameter was found", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
bool skipFiles = Library.Utility.Utility.ParseBool(info.Request.QueryString["onlyfolders"].Value, false);
|
||||
bool showHidden = Library.Utility.Utility.ParseBool(info.Request.QueryString["showhidden"].Value, false);
|
||||
|
||||
string specialpath = null;
|
||||
string specialtoken = null;
|
||||
|
||||
if (path.StartsWith("%", StringComparison.Ordinal))
|
||||
{
|
||||
var ix = path.IndexOf("%", 1, StringComparison.Ordinal);
|
||||
if (ix > 0)
|
||||
{
|
||||
var tk = path.Substring(0, ix + 1);
|
||||
var node = SpecialFolders.Nodes.FirstOrDefault(x => x.id.Equals(tk, StringComparison.OrdinalIgnoreCase));
|
||||
if (node != null)
|
||||
{
|
||||
specialpath = node.resolvedpath;
|
||||
specialtoken = node.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path = SpecialFolders.ExpandEnvironmentVariables(path);
|
||||
|
||||
if ((OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()) && !path.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
info.ReportClientError("The path parameter must start with a forward-slash", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
if ("validate".Equals(command, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (System.IO.Path.IsPathRooted(path) && (System.IO.Directory.Exists(path) || System.IO.File.Exists(path)))
|
||||
{
|
||||
info.OutputOK();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
info.ReportServerError("File or folder not found", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
info.ReportClientError(string.Format("No such operation found: {0}", command), System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (path != "" && path != "/")
|
||||
path = Util.AppendDirSeparator(path);
|
||||
|
||||
IEnumerable<Serializable.TreeNode> res;
|
||||
|
||||
if (!(OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()) && (path.Equals("/") || path.Equals("")))
|
||||
{
|
||||
res = DriveInfo.GetDrives()
|
||||
.Where(di =>
|
||||
(di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable)
|
||||
&& di.IsReady // Only try to create TreeNode entries for drives who were ready 'now'
|
||||
)
|
||||
.Select(TryCreateTreeNodeForDrive) // This will try to create a TreeNode for selected drives
|
||||
.Where(tn => tn != null); // This filters out such entries that could not be created
|
||||
}
|
||||
else
|
||||
{
|
||||
res = ListFolderAsNodes(path, skipFiles, showHidden);
|
||||
}
|
||||
|
||||
if ((path.Equals("/") || path.Equals("")) && specialtoken == null)
|
||||
{
|
||||
// Prepend special folders
|
||||
res = SpecialFolders.Nodes.Union(res);
|
||||
}
|
||||
|
||||
if (specialtoken != null)
|
||||
{
|
||||
res = res.Select(x =>
|
||||
{
|
||||
x.resolvedpath = x.id;
|
||||
x.id = specialtoken + x.id.Substring(specialpath.Length);
|
||||
return x;
|
||||
});
|
||||
}
|
||||
|
||||
// We have to resolve the query before giving it to OutputOK
|
||||
// If we do not do this, and the query throws an exception when OutputOK resolves it,
|
||||
// the exception would not be handled properly
|
||||
res = res.ToList();
|
||||
|
||||
info.OutputOK(res);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
info.ReportClientError("Failed to process the path: " + ex.Message, System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to create a new TreeNode instance for the given DriveInfo instance.
|
||||
///
|
||||
/// <remarks>
|
||||
/// If an exception occurs during creation (most likely the device became unavailable), a null is returned instead.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
/// <param name="driveInfo">DriveInfo to try create a TreeNode for. Cannot be null.</param>
|
||||
/// <returns>A new TreeNode instance on success; null if an exception occurred during creation.</returns>
|
||||
private static Serializable.TreeNode TryCreateTreeNodeForDrive(DriveInfo driveInfo)
|
||||
{
|
||||
if (driveInfo == null) throw new ArgumentNullException(nameof(driveInfo));
|
||||
|
||||
try
|
||||
{
|
||||
// Try to create the TreeNode
|
||||
// This may still fail as the drive might become unavailable in the meanwhile
|
||||
return new Serializable.TreeNode
|
||||
{
|
||||
id = driveInfo.RootDirectory.FullName,
|
||||
text =
|
||||
(
|
||||
string.IsNullOrWhiteSpace(driveInfo.VolumeLabel)
|
||||
? driveInfo.RootDirectory.FullName.Replace('\\', ' ')
|
||||
: driveInfo.VolumeLabel + " - " + driveInfo.RootDirectory.FullName.Replace('\\', ' ')
|
||||
) + "(" + driveInfo.DriveType + ")",
|
||||
iconCls = "x-tree-icon-drive"
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Drive became unavailable in the meanwhile or another exception occurred
|
||||
// Return a null as fall back
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Serializable.TreeNode> ListFolderAsNodes(string entrypath, bool skipFiles, bool showHidden)
|
||||
{
|
||||
//Helper function for finding out if a folder has sub elements
|
||||
Func<string, bool> hasSubElements = (p) => skipFiles ? Directory.EnumerateDirectories(p).Any() : Directory.EnumerateFileSystemEntries(p).Any();
|
||||
|
||||
//Helper function for dealing with exceptions when accessing off-limits folders
|
||||
Func<string, bool> isEmptyFolder = (p) =>
|
||||
{
|
||||
try { return !hasSubElements(p); }
|
||||
catch { }
|
||||
return true;
|
||||
};
|
||||
|
||||
//Helper function for dealing with exceptions when accessing off-limits folders
|
||||
Func<string, bool> canAccess = (p) =>
|
||||
{
|
||||
try { hasSubElements(p); return true; }
|
||||
catch { }
|
||||
return false;
|
||||
};
|
||||
|
||||
Func<string, long> getFileSize = (p) =>
|
||||
{
|
||||
try { return new FileInfo(p).Length; }
|
||||
catch { }
|
||||
return -1;
|
||||
};
|
||||
|
||||
foreach (var s in SystemIO.IO_OS.EnumerateFileSystemEntries(entrypath)
|
||||
// Group directories first
|
||||
.OrderByDescending(f => SystemIO.IO_OS.GetFileAttributes(f) & FileAttributes.Directory)
|
||||
// Sort both groups (directories and files) alphabetically
|
||||
.ThenBy(f => f))
|
||||
{
|
||||
Serializable.TreeNode tn = null;
|
||||
try
|
||||
{
|
||||
var attr = SystemIO.IO_OS.GetFileAttributes(s);
|
||||
var isSymlink = SystemIO.IO_OS.IsSymlink(s, attr);
|
||||
var isFolder = (attr & FileAttributes.Directory) != 0;
|
||||
var isFile = !isFolder;
|
||||
var isHidden = (attr & FileAttributes.Hidden) != 0;
|
||||
bool isSystem = (attr & FileAttributes.System) != 0;
|
||||
bool isTemporary = (attr & FileAttributes.Temporary) != 0;
|
||||
long fileSize = -1;
|
||||
|
||||
var accessible = isFile || canAccess(s);
|
||||
var isLeaf = isFile || !accessible || isEmptyFolder(s);
|
||||
|
||||
var rawid = isFolder ? Util.AppendDirSeparator(s) : s;
|
||||
if (skipFiles && !isFolder)
|
||||
continue;
|
||||
|
||||
if (!showHidden && isHidden)
|
||||
continue;
|
||||
|
||||
if (isFile)
|
||||
{
|
||||
fileSize = getFileSize(s);
|
||||
}
|
||||
|
||||
tn = new Serializable.TreeNode()
|
||||
{
|
||||
id = rawid,
|
||||
text = SystemIO.IO_OS.PathGetFileName(s),
|
||||
hidden = isHidden,
|
||||
symlink = isSymlink,
|
||||
temporary = isTemporary,
|
||||
systemFile = isSystem,
|
||||
fileSize = fileSize,
|
||||
iconCls = isFolder ? (accessible ? (isSymlink ? "x-tree-icon-symlink" : "x-tree-icon-parent") : "x-tree-icon-locked") : "x-tree-icon-leaf",
|
||||
leaf = isLeaf
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (tn != null)
|
||||
yield return tn;
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
Process(key, info.Request.Form["path"].Value, info);
|
||||
}
|
||||
|
||||
public string Description { get { return "Enumerates the server filesystem"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(string[])),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Help : IRESTMethodGET
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
foreach(var m in RESTHandler.Modules.Keys.OrderBy(x => x))
|
||||
{
|
||||
var mod = RESTHandler.Modules[m];
|
||||
if (mod == this)
|
||||
continue;
|
||||
|
||||
var desc = mod.GetType().Name;
|
||||
if (mod is IRESTMethodDocumented documented)
|
||||
desc = documented.Description;
|
||||
sb.AppendFormat(ITEM_TEMPLATE, RESTHandler.API_URI_PATH, m, mod.GetType().Name, desc);
|
||||
}
|
||||
|
||||
|
||||
var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, "API Information", "", sb));
|
||||
|
||||
info.Response.ContentType = "text/html";
|
||||
info.Response.ContentLength = data.Length;
|
||||
info.Response.Body.Write(data, 0, data.Length);
|
||||
info.Response.Send();
|
||||
}
|
||||
else
|
||||
{
|
||||
IRESTMethod m;
|
||||
RESTHandler.Modules.TryGetValue(key, out m);
|
||||
if (m == null)
|
||||
{
|
||||
info.Response.Status = System.Net.HttpStatusCode.NotFound;
|
||||
info.Response.Reason = "Module not found";
|
||||
}
|
||||
else
|
||||
{
|
||||
var desc = "";
|
||||
if (m is IRESTMethodDocumented doc)
|
||||
{
|
||||
desc = doc.Description;
|
||||
foreach(var t in doc.Types)
|
||||
sb.AppendFormat(METHOD_TEMPLATE, t.Key, JsonConvert.SerializeObject(t.Value)); //TODO: Format the type
|
||||
}
|
||||
|
||||
var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, m.GetType().Name, desc, sb));
|
||||
|
||||
info.Response.ContentType = "text/html";
|
||||
info.Response.ContentLength = data.Length;
|
||||
info.Response.Body.Write(data, 0, data.Length);
|
||||
info.Response.Send();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const string TEMPLATE = @"
|
||||
<html><head><title>{0}</title></head>
|
||||
<body>
|
||||
<h1>{0}</h1>
|
||||
<p>{1}</p>
|
||||
<ul>
|
||||
{2}
|
||||
</ul>
|
||||
</body>
|
||||
";
|
||||
private const string ITEM_TEMPLATE = @"
|
||||
<li><a href=""{0}/help/{1}"">{2}</a>: {3}</li>
|
||||
";
|
||||
|
||||
private const string METHOD_TEMPLATE = @"
|
||||
<b>{0}:</b><br><code>{1}</code><br>
|
||||
";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Interface;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
using Duplicati.Library.Snapshots;
|
||||
using Duplicati.Library.Common;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class HyperV : IRESTMethodGET, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
// Early exit in case we are non-windows to prevent attempting to load Windows-only components
|
||||
if (OperatingSystem.IsWindows())
|
||||
RealGET(key, info);
|
||||
else
|
||||
info.OutputOK(new string[0]);
|
||||
}
|
||||
|
||||
// Make sure the JIT does not attempt to inline this call and thus load
|
||||
// referenced types from System.Management here
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
private void RealGET(string key, RequestInfo info)
|
||||
{
|
||||
var hypervUtility = new HyperVUtility();
|
||||
|
||||
if (!hypervUtility.IsHyperVInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
|
||||
{
|
||||
info.OutputOK(new string[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
hypervUtility.QueryHyperVGuestsInfo();
|
||||
info.OutputOK(hypervUtility.Guests.Select(x => new { id = x.ID, name = x.Name }).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
hypervUtility.QueryHyperVGuestsInfo(true);
|
||||
var foundVMs = hypervUtility.Guests.FindAll(x => x.ID.Equals(new Guid(key)));
|
||||
|
||||
if (foundVMs.Count == 1)
|
||||
info.OutputOK(foundVMs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList());
|
||||
else
|
||||
info.ReportClientError(string.Format("Cannot find VM with ID {0}.", key), System.Net.HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
info.ReportServerError("Failed to enumerate Hyper-V virtual machines: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get { return "Return a list of Hyper-V virtual machines"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(ICommandLineArgument[]))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public interface IRESTMethod
|
||||
{
|
||||
}
|
||||
|
||||
public interface IRESTMethodDocumented
|
||||
{
|
||||
string Description { get; }
|
||||
IEnumerable<KeyValuePair<string, Type>> Types { get; }
|
||||
}
|
||||
|
||||
public interface IRESTMethodGET : IRESTMethod
|
||||
{
|
||||
void GET(string key, RequestInfo info);
|
||||
}
|
||||
public interface IRESTMethodPUT : IRESTMethod
|
||||
{
|
||||
void PUT(string key, RequestInfo info);
|
||||
}
|
||||
|
||||
public interface IRESTMethodPOST : IRESTMethod
|
||||
{
|
||||
void POST(string key, RequestInfo info);
|
||||
}
|
||||
|
||||
public interface IRESTMethodDELETE : IRESTMethod
|
||||
{
|
||||
void DELETE(string key, RequestInfo info);
|
||||
}
|
||||
|
||||
public interface IRESTMethodPATCH : IRESTMethod
|
||||
{
|
||||
void PATCH(string key, RequestInfo info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Licenses : IRESTMethodGET
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var exefolder = System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location);
|
||||
var path = System.IO.Path.Combine(exefolder, "licenses");
|
||||
if (OperatingSystem.IsMacOS() && !Directory.Exists(path))
|
||||
{
|
||||
// Go up one, as the licenses cannot be in the binary folder in MacOS Packages
|
||||
exefolder = Path.GetDirectoryName(exefolder);
|
||||
var test = Path.Combine(exefolder, "Licenses");
|
||||
if (Directory.Exists(test))
|
||||
path = test;
|
||||
}
|
||||
info.OutputOK(Duplicati.License.LicenseReader.ReadLicenses(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class LogData : IRESTMethodGET, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if ("poll".Equals(key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var input = info.Request.QueryString;
|
||||
var level_str = input["level"].Value ?? "";
|
||||
var id_str = input["id"].Value ?? "";
|
||||
|
||||
int pagesize;
|
||||
if (!int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize))
|
||||
pagesize = 100;
|
||||
|
||||
pagesize = Math.Max(1, Math.Min(500, pagesize));
|
||||
|
||||
Library.Logging.LogMessageType level;
|
||||
long id;
|
||||
|
||||
long.TryParse(id_str, out id);
|
||||
Enum.TryParse(level_str, true, out level);
|
||||
|
||||
info.OutputOK(FIXMEGlobal.LogHandler.AfterID(id, level, pagesize));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
List<Dictionary<string, object>> res = null;
|
||||
FIXMEGlobal.DataConnection.ExecuteWithCommand(x =>
|
||||
{
|
||||
res = DumpTable(x, "ErrorLog", "Timestamp", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value);
|
||||
});
|
||||
|
||||
info.OutputOK(res);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static List<Dictionary<string, object>> DumpTable(System.Data.IDbCommand cmd, string tablename, string pagingfield, string offset_str, string pagesize_str)
|
||||
{
|
||||
var result = new List<Dictionary<string, object>>();
|
||||
|
||||
long pagesize;
|
||||
if (!long.TryParse(pagesize_str, out pagesize))
|
||||
pagesize = 100;
|
||||
|
||||
pagesize = Math.Max(10, Math.Min(500, pagesize));
|
||||
|
||||
cmd.CommandText = "SELECT * FROM \"" + tablename + "\"";
|
||||
long offset = 0;
|
||||
if (!string.IsNullOrWhiteSpace(offset_str) && long.TryParse(offset_str, out offset) && !string.IsNullOrEmpty(pagingfield))
|
||||
{
|
||||
var p = cmd.CreateParameter();
|
||||
p.Value = offset;
|
||||
cmd.Parameters.Add(p);
|
||||
|
||||
cmd.CommandText += " WHERE \"" + pagingfield + "\" < ?";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(pagingfield))
|
||||
cmd.CommandText += " ORDER BY \"" + pagingfield + "\" DESC";
|
||||
cmd.CommandText += " LIMIT " + pagesize.ToString();
|
||||
|
||||
using(var rd = cmd.ExecuteReader())
|
||||
{
|
||||
var names = new List<string>();
|
||||
for(var i = 0; i < rd.FieldCount; i++)
|
||||
names.Add(rd.GetName(i));
|
||||
|
||||
while (rd.Read())
|
||||
{
|
||||
var dict = new Dictionary<string, object>();
|
||||
for(int i = 0; i < names.Count; i++)
|
||||
dict[names[i]] = rd.GetValue(i);
|
||||
|
||||
result.Add(dict);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string Description { get { return "Retrieves system log data"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Dictionary<string, string>[])),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Interface;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
using Duplicati.Library.Snapshots;
|
||||
using Duplicati.Library.Common;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
|
||||
public class MSSQL : IRESTMethodGET, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
// Early exit in case we are non-windows to prevent attempting to load Windows-only components
|
||||
if (OperatingSystem.IsWindows())
|
||||
RealGET(key, info);
|
||||
else
|
||||
info.OutputOK(new string[0]);
|
||||
}
|
||||
|
||||
// Make sure the JIT does not attempt to inline this call and thus load
|
||||
// referenced types from System.Management here
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
private void RealGET(string key, RequestInfo info)
|
||||
{
|
||||
var mssqlUtility = new MSSQLUtility();
|
||||
|
||||
if (!mssqlUtility.IsMSSQLInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
|
||||
{
|
||||
info.OutputOK(new string[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mssqlUtility.QueryDBsInfo();
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
info.OutputOK(mssqlUtility.DBs.Select(x => new { id = x.ID, name = x.Name }).ToList());
|
||||
else
|
||||
{
|
||||
var foundDBs = mssqlUtility.DBs.FindAll(x => x.ID.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (foundDBs.Count == 1)
|
||||
info.OutputOK(foundDBs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList());
|
||||
else
|
||||
info.ReportClientError(string.Format("Cannot find DB with ID {0}.", key), System.Net.HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
info.ReportServerError("Failed to enumerate Microsoft SQL Server databases: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get { return "Return a list of Microsoft SQL Server databases"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(ICommandLineArgument[]))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Notification : IRESTMethodGET, IRESTMethodDELETE
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
long id;
|
||||
if (!long.TryParse(key, out id))
|
||||
{
|
||||
info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id);
|
||||
if (el == null)
|
||||
info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound);
|
||||
else
|
||||
info.OutputOK(el);
|
||||
}
|
||||
|
||||
public void DELETE(string key, RequestInfo info)
|
||||
{
|
||||
long id;
|
||||
if (!long.TryParse(key, out id))
|
||||
{
|
||||
info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id);
|
||||
if (el == null)
|
||||
info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound);
|
||||
else
|
||||
{
|
||||
FIXMEGlobal.DataConnection.DismissNotification(id);
|
||||
info.OutputOK();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Notifications : IRESTMethodGET
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
info.OutputOK(FIXMEGlobal.DataConnection.GetNotifications());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class ProgressState : IRESTMethodGET, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if (FIXMEGlobal.GenerateProgressState == null)
|
||||
info.ReportClientError("No active backup", System.Net.HttpStatusCode.NotFound);
|
||||
else
|
||||
info.OutputOK(FIXMEGlobal.GenerateProgressState());
|
||||
}
|
||||
|
||||
public string Description { get { return "Return the progress of the currently running operation."; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Serialization.Interface.IProgressEventData))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Duplicati.Library.Interface;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class RemoteOperation : IRESTMethodGET, IRESTMethodPOST
|
||||
{
|
||||
private void LocateDbUri(string uri, RequestInfo info)
|
||||
{
|
||||
var path = Library.Main.DatabaseLocator.GetDatabasePath(uri, null, false, false);
|
||||
info.OutputOK(new {
|
||||
Exists = !string.IsNullOrWhiteSpace(path),
|
||||
Path = path
|
||||
});
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ParseUrlOptions(Library.Utility.Uri uri)
|
||||
{
|
||||
var qp = uri.QueryParameters;
|
||||
|
||||
var opts = Runner.GetCommonOptions();
|
||||
foreach (var k in qp.Keys.Cast<string>())
|
||||
opts[k] = qp[k];
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
private IEnumerable<IGenericModule> ConfigureModules(IDictionary<string, string> opts)
|
||||
{
|
||||
// TODO: This works because the generic modules are implemented
|
||||
// with pre .NetCore logic, using static methods
|
||||
// The modules are created to allow multipe dispose,
|
||||
// which violates the .Net patterns
|
||||
var modules = (from n in Library.DynamicLoader.GenericLoader.Modules
|
||||
where n is Library.Interface.IConnectionModule
|
||||
select n).ToArray();
|
||||
|
||||
foreach (var n in modules)
|
||||
n.Configure(opts);
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
|
||||
private class TupleDisposeWrapper : IDisposable
|
||||
{
|
||||
public IBackend Backend { get; set; }
|
||||
public IEnumerable<IGenericModule> Modules { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Backend.Dispose();
|
||||
DisposeModules();
|
||||
}
|
||||
|
||||
public void DisposeModules()
|
||||
{
|
||||
foreach (var n in Modules)
|
||||
if (n is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private TupleDisposeWrapper GetBackend(string url)
|
||||
{
|
||||
var uri = new Library.Utility.Uri(url);
|
||||
var opts = ParseUrlOptions(uri);
|
||||
var modules = ConfigureModules(opts);
|
||||
var backend = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(url, new Dictionary<string, string>());
|
||||
return new TupleDisposeWrapper() { Backend = backend, Modules = modules };
|
||||
}
|
||||
|
||||
private void CreateFolder(string uri, RequestInfo info)
|
||||
{
|
||||
using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary<string, string>()))
|
||||
b.CreateFolder();
|
||||
|
||||
info.OutputOK();
|
||||
}
|
||||
|
||||
private void UploadFile(string uri, RequestInfo info)
|
||||
{
|
||||
var data = info.Request.QueryString["data"].Value;
|
||||
var remotename = info.Request.QueryString["filename"].Value;
|
||||
|
||||
using(var ms = new System.IO.MemoryStream())
|
||||
using(var b = GetBackend(uri))
|
||||
{
|
||||
using(var tf = new Library.Utility.TempFile())
|
||||
{
|
||||
System.IO.File.WriteAllText(tf, data);
|
||||
b.Backend.PutAsync(remotename, tf, CancellationToken.None).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
info.OutputOK();
|
||||
}
|
||||
|
||||
private void ListFolder(string uri, RequestInfo info)
|
||||
{
|
||||
using(var b = GetBackend(uri))
|
||||
info.OutputOK(b.Backend.List());
|
||||
}
|
||||
|
||||
private void TestConnection(string url, RequestInfo info)
|
||||
{
|
||||
bool autoCreate = info.Request.Param.Contains("autocreate")
|
||||
? Library.Utility.Utility.ParseBool(info.Request.Param["autocreate"].Value, false)
|
||||
: false;
|
||||
|
||||
TupleDisposeWrapper wrapper = null;
|
||||
|
||||
try
|
||||
{
|
||||
wrapper = GetBackend(url);
|
||||
|
||||
using (var b = wrapper.Backend)
|
||||
{
|
||||
try { b.Test(); }
|
||||
catch (FolderMissingException)
|
||||
{
|
||||
if (!autoCreate)
|
||||
throw;
|
||||
|
||||
b.CreateFolder();
|
||||
b.Test();
|
||||
}
|
||||
info.OutputOK();
|
||||
}
|
||||
}
|
||||
catch (Duplicati.Library.Interface.FolderMissingException)
|
||||
{
|
||||
if (!autoCreate) {
|
||||
info.ReportServerError("missing-folder");
|
||||
} else {
|
||||
info.ReportServerError("error-creating-folder");
|
||||
}
|
||||
}
|
||||
catch (Duplicati.Library.Utility.SslCertificateValidator.InvalidCertificateException icex)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(icex.Certificate))
|
||||
info.ReportServerError(icex.Message);
|
||||
else
|
||||
info.ReportServerError("incorrect-cert:" + icex.Certificate);
|
||||
}
|
||||
catch (Duplicati.Library.Utility.HostKeyException hex)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hex.ReportedHostKey))
|
||||
info.ReportServerError(hex.Message);
|
||||
else
|
||||
{
|
||||
info.ReportServerError(string.Format(
|
||||
@"incorrect-host-key:""{0}"", accepted-host-key:""{1}""",
|
||||
hex.ReportedHostKey,
|
||||
hex.AcceptedHostKey
|
||||
));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (wrapper != null)
|
||||
wrapper.DisposeModules();
|
||||
}
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var parts = (key ?? "").Split(new char[] { '/' }, 2);
|
||||
|
||||
if (parts.Length <= 1)
|
||||
{
|
||||
info.ReportClientError("No url or operation supplied", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var url = Library.Utility.Uri.UrlDecode(parts.First());
|
||||
var operation = parts.Last().ToLowerInvariant();
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case "dbpath":
|
||||
LocateDbUri(url, info);
|
||||
return;
|
||||
case "list":
|
||||
ListFolder(url, info);
|
||||
return;
|
||||
case "create":
|
||||
CreateFolder(url, info);
|
||||
return;
|
||||
case "test":
|
||||
TestConnection(url, info);
|
||||
return;
|
||||
default:
|
||||
info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
string url;
|
||||
|
||||
using(var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
||||
url = sr.ReadToEnd();
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case "dbpath":
|
||||
LocateDbUri(url, info);
|
||||
return;
|
||||
case "list":
|
||||
ListFolder(url, info);
|
||||
return;
|
||||
case "create":
|
||||
CreateFolder(url, info);
|
||||
return;
|
||||
case "put":
|
||||
UploadFile(url, info);
|
||||
return;
|
||||
case "test":
|
||||
TestConnection(url, info);
|
||||
return;
|
||||
default:
|
||||
info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class RequestInfo : IDisposable
|
||||
{
|
||||
public HttpServer.IHttpRequest Request { get; private set; }
|
||||
public HttpServer.IHttpResponse Response { get; private set; }
|
||||
public HttpServer.Sessions.IHttpSession Session { get; private set; }
|
||||
public BodyWriter BodyWriter { get; private set; }
|
||||
public RequestInfo(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
Request = request;
|
||||
Response = response;
|
||||
Session = session;
|
||||
BodyWriter = new BodyWriter(response, request);
|
||||
}
|
||||
|
||||
public void ReportServerError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError)
|
||||
{
|
||||
Response.Status = code;
|
||||
Response.Reason = message;
|
||||
|
||||
BodyWriter.WriteJsonObject(new { Error = message });
|
||||
}
|
||||
|
||||
public void ReportClientError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
ReportServerError(message, code);
|
||||
}
|
||||
|
||||
public bool LongPollCheck(EventPollNotify poller, ref long id, out bool isError)
|
||||
{
|
||||
HttpServer.HttpInput input = String.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? Request.Form : Request.QueryString;
|
||||
if (Library.Utility.Utility.ParseBool(input["longpoll"].Value, false))
|
||||
{
|
||||
long lastEventId;
|
||||
if (!long.TryParse(input["lasteventid"].Value, out lastEventId))
|
||||
{
|
||||
ReportClientError("When activating long poll, the request must include the last event id", System.Net.HttpStatusCode.BadRequest);
|
||||
isError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
TimeSpan ts;
|
||||
try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReportClientError("Invalid duration: " + ex.Message, System.Net.HttpStatusCode.BadRequest);
|
||||
isError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ts <= TimeSpan.FromSeconds(10) || ts.TotalMilliseconds > int.MaxValue)
|
||||
{
|
||||
ReportClientError("Invalid duration, must be at least 10 seconds, and less than " + int.MaxValue + " milliseconds", System.Net.HttpStatusCode.BadRequest);
|
||||
isError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
isError = false;
|
||||
id = poller.Wait(lastEventId, (int)ts.TotalMilliseconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
isError = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OutputOK(object item = null)
|
||||
{
|
||||
BodyWriter.OutputOK(item);
|
||||
}
|
||||
|
||||
public void OutputError(object item = null, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError, string reason = null)
|
||||
{
|
||||
Response.Status = code;
|
||||
Response.Reason = reason ?? "Error";
|
||||
if(item == null && reason != null)
|
||||
{
|
||||
item = new { Error = reason };
|
||||
}
|
||||
BodyWriter.WriteJsonObject(item);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (BodyWriter != null)
|
||||
{
|
||||
var bw = BodyWriter;
|
||||
BodyWriter = null;
|
||||
bw.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class ServerSetting : IRESTMethodGET, IRESTMethodPUT, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
info.OutputOK(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate == null ? "False" : "True");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
var prop = FIXMEGlobal.DataConnection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase));
|
||||
info.OutputOK(prop == null ? null : prop.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
var prop = typeof(Database.ServerSettings).GetProperty(key);
|
||||
if (prop == null)
|
||||
info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found");
|
||||
else
|
||||
info.OutputOK(prop.GetValue(FIXMEGlobal.DataConnection.ApplicationSettings));
|
||||
}
|
||||
}
|
||||
|
||||
public void PUT(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Can only update SSL certificate from commandline");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
var settings = FIXMEGlobal.DataConnection.Settings.ToList();
|
||||
|
||||
var prop = settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase));
|
||||
if (prop == null)
|
||||
settings.Add(prop = new Database.Setting() { Name = key, Value = info.Request.Form["data"].Value });
|
||||
else
|
||||
prop.Value = info.Request.Form["data"].Value;
|
||||
|
||||
FIXMEGlobal.DataConnection.Settings = settings.ToArray();
|
||||
|
||||
info.OutputOK(prop == null ? null : prop.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
var prop = typeof(Database.ServerSettings).GetProperty(key);
|
||||
if (prop == null)
|
||||
info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found");
|
||||
else
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
dict[key] = info.Request.Form["data"].Value;
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(dict, false);
|
||||
info.OutputOK();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get { return "Return a list of settings for the server"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(string)),
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Put, typeof(string))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Server.Database;
|
||||
using System.IO;
|
||||
using Duplicati.Server.Serialization;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class ServerSettings : IRESTMethodGET, IRESTMethodPATCH, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
// Join server settings and global settings
|
||||
var adv_props =
|
||||
FIXMEGlobal.DataConnection.GetSettings(Database.Connection.SERVER_SETTINGS_ID)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
|
||||
.Union(
|
||||
FIXMEGlobal.DataConnection.Settings
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name.StartsWith("--", StringComparison.Ordinal))
|
||||
);
|
||||
|
||||
var dict = new Dictionary<string, string>();
|
||||
foreach (var n in adv_props)
|
||||
dict[n.Name] = n.Value;
|
||||
|
||||
string sslcert;
|
||||
dict.TryGetValue("server-ssl-certificate", out sslcert);
|
||||
dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString();
|
||||
|
||||
info.OutputOK(dict);
|
||||
}
|
||||
|
||||
public void PATCH(string key, RequestInfo info)
|
||||
{
|
||||
string str = info.Request.Form["data"].Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
str = System.Threading.Tasks.Task.Run(async () =>
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
||||
|
||||
return await sr.ReadToEndAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, string> data = null;
|
||||
try
|
||||
{
|
||||
data = Serializer.Deserialize<Dictionary<string, string>>(new StringReader(str));
|
||||
if (data == null)
|
||||
{
|
||||
info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Split into server settings and global settings
|
||||
|
||||
var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value);
|
||||
var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal));
|
||||
|
||||
serversettings.Remove("server-ssl-certificate");
|
||||
serversettings.Remove("ServerSSLCertificate");
|
||||
|
||||
if (serversettings.Any())
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false);
|
||||
|
||||
if (globalsettings.Any())
|
||||
{
|
||||
// Update based on inputs
|
||||
var existing = FIXMEGlobal.DataConnection.Settings.ToDictionary(x => x.Name, x => x);
|
||||
foreach (var g in globalsettings)
|
||||
if (g.Value == null)
|
||||
existing.Remove(g.Key);
|
||||
else
|
||||
{
|
||||
if (existing.ContainsKey(g.Key))
|
||||
existing[g.Key].Value = g.Value;
|
||||
else
|
||||
existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value };
|
||||
}
|
||||
|
||||
FIXMEGlobal.DataConnection.Settings = existing.Select(x => x.Value).ToArray();
|
||||
}
|
||||
|
||||
info.OutputOK();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
|
||||
else
|
||||
info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get { return "Return a list of settings for the server"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Database.ServerSettings))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class ServerState : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
bool isError;
|
||||
long id = 0;
|
||||
long.TryParse(key, out id);
|
||||
|
||||
if (info.LongPollCheck(FIXMEGlobal.StatusEventNotifyer, ref id, out isError))
|
||||
{
|
||||
//Make sure we do not report a higher number than the eventnotifier says
|
||||
var st = new Serializable.ServerStatus();
|
||||
st.LastEventID = id;
|
||||
info.OutputOK(st);
|
||||
}
|
||||
else if (!isError)
|
||||
{
|
||||
info.OutputOK(new Serializable.ServerStatus());
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
var input = info.Request.Form;
|
||||
switch ((key ?? "").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)
|
||||
{
|
||||
info.ReportClientError(ex.Message, System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
if (ts.TotalMilliseconds > 0)
|
||||
FIXMEGlobal.LiveControl.Pause(ts);
|
||||
else
|
||||
FIXMEGlobal.LiveControl.Pause();
|
||||
}
|
||||
else
|
||||
{
|
||||
FIXMEGlobal.LiveControl.Pause();
|
||||
}
|
||||
|
||||
info.OutputOK();
|
||||
return;
|
||||
|
||||
case "resume":
|
||||
FIXMEGlobal.LiveControl.Resume();
|
||||
info.OutputOK();
|
||||
return;
|
||||
|
||||
default:
|
||||
info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get { return "Return the state of the server. This method can be long-polled."; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Serializable.ServerStatus)),
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Post, typeof(Serializable.ServerStatus))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Common;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class SystemInfo : IRESTMethodGET, IRESTMethodDocumented
|
||||
{
|
||||
public string Description { get { return "Gets various system properties"; } }
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, SystemData(null).GetType())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.BodyWriter.OutputOK(SystemData(info));
|
||||
}
|
||||
else if (key.Equals("filtergroups", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
info.BodyWriter.OutputOK(FilterGroups());
|
||||
}
|
||||
else
|
||||
{
|
||||
info.OutputError(code: System.Net.HttpStatusCode.NotFound, reason: "Not found");
|
||||
}
|
||||
}
|
||||
|
||||
private static object SystemData(RequestInfo info)
|
||||
{
|
||||
var browserlanguage = RESTHandler.ParseDefaultRequestCulture(info) ?? System.Globalization.CultureInfo.InvariantCulture;
|
||||
|
||||
return new
|
||||
{
|
||||
APIVersion = 1,
|
||||
PasswordPlaceholder = Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER,
|
||||
ServerVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
|
||||
ServerVersionName = Duplicati.License.VersionNumbers.Version,
|
||||
ServerVersionType = Duplicati.Library.AutoUpdater.UpdaterManager.SelfVersion.ReleaseType,
|
||||
StartedBy = FIXMEGlobal.Origin,
|
||||
DefaultUpdateChannel = Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel,
|
||||
DefaultUsageReportLevel = Duplicati.Library.UsageReporter.Reporter.DefaultReportLevel,
|
||||
ServerTime = DateTime.Now,
|
||||
OSType = OperatingSystem.IsWindows() ? "Windows" : (OperatingSystem.IsMacOS() ? "OSX" : "Linux"),
|
||||
DirectorySeparator = System.IO.Path.DirectorySeparatorChar,
|
||||
PathSeparator = System.IO.Path.PathSeparator,
|
||||
CaseSensitiveFilesystem = Duplicati.Library.Utility.Utility.IsFSCaseSensitive,
|
||||
MachineName = System.Environment.MachineName,
|
||||
PackageTypeId = Duplicati.Library.AutoUpdater.UpdaterManager.PackageTypeId,
|
||||
UserName = OperatingSystem.IsWindows() ? System.Security.Principal.WindowsIdentity.GetCurrent().Name : System.Environment.UserName,
|
||||
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,
|
||||
ConnectionModules = Serializable.ServerSettings.ConnectionModules,
|
||||
ServerModules = Serializable.ServerSettings.ServerModules,
|
||||
UsingAlternateUpdateURLs = Duplicati.Library.AutoUpdater.AutoUpdateSettings.UsesAlternateURLs,
|
||||
LogLevels = Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)),
|
||||
SpecialFolders = from n in SpecialFolders.Nodes select new { ID = n.id, Path = n.resolvedpath },
|
||||
BrowserLocale = new
|
||||
{
|
||||
Code = browserlanguage.Name,
|
||||
EnglishName = browserlanguage.EnglishName,
|
||||
DisplayName = browserlanguage.NativeName
|
||||
},
|
||||
SupportedLocales =
|
||||
Library.Localization.LocalizationService.SupportedCultures
|
||||
.Select(x => new
|
||||
{
|
||||
Code = x,
|
||||
EnglishName = new System.Globalization.CultureInfo(x).EnglishName,
|
||||
DisplayName = new System.Globalization.CultureInfo(x).NativeName
|
||||
}
|
||||
),
|
||||
BrowserLocaleSupported = Library.Localization.LocalizationService.isCultureSupported(browserlanguage)
|
||||
};
|
||||
}
|
||||
|
||||
private static object FilterGroups()
|
||||
{
|
||||
return new { FilterGroups = Library.Utility.FilterGroups.GetFilterStringMap() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Interface;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class SystemWideSettings : IRESTMethodGET, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
info.OutputOK(new Duplicati.Library.Main.Options(new Dictionary<string, string>()).SupportedCommands);
|
||||
}
|
||||
|
||||
public string Description { get { return "Return a list of settings that can be applied to all backups on a system-wide basis"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(ICommandLineArgument[]))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Tags : IRESTMethodGET, IRESTMethodDocumented
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var r =
|
||||
from n in
|
||||
Serializable.ServerSettings.CompressionModules
|
||||
.Union(Serializable.ServerSettings.EncryptionModules)
|
||||
.Union(Serializable.ServerSettings.BackendModules)
|
||||
.Union(Serializable.ServerSettings.GenericModules)
|
||||
select n.Key.ToLower(CultureInfo.InvariantCulture);
|
||||
|
||||
// Append all known tags
|
||||
r = r.Union(from n in FIXMEGlobal.DataConnection.Backups select n.Tags into p from x in p select x.ToLower(CultureInfo.InvariantCulture));
|
||||
info.OutputOK(r);
|
||||
}
|
||||
|
||||
public string Description { get { return "Gets the list of tags"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(string[])),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Task : IRESTMethodGET, IRESTMethodPOST
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var parts = (key ?? "").Split(new char[] { '/' }, 2);
|
||||
long taskid;
|
||||
if (long.TryParse(parts.FirstOrDefault(), out taskid))
|
||||
{
|
||||
var task = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
var tasks = FIXMEGlobal.WorkThread.CurrentTasks;
|
||||
|
||||
if (task != null && task.TaskID == taskid)
|
||||
{
|
||||
info.OutputOK(new { Status = "Running" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (tasks.FirstOrDefault(x => x.TaskID == taskid) == null)
|
||||
{
|
||||
KeyValuePair<long, Exception>[] matches;
|
||||
lock(FIXMEGlobal.MainLock)
|
||||
matches = FIXMEGlobal.TaskResultCache.Where(x => x.Key == taskid).ToArray();
|
||||
|
||||
if (matches.Length == 0)
|
||||
info.ReportClientError("No such task found", System.Net.HttpStatusCode.NotFound);
|
||||
else
|
||||
info.OutputOK(new {
|
||||
Status = matches[0].Value == null ? "Completed" : "Failed",
|
||||
ErrorMessage = matches[0].Value == null ? null : matches[0].Value.Message,
|
||||
Exception = matches[0].Value == null ? null : matches[0].Value.ToString()
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
info.OutputOK(new { Status = "Waiting" });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
var parts = (key ?? "").Split(new char[] { '/' }, 2);
|
||||
long taskid;
|
||||
if (parts.Length == 2 && long.TryParse(parts.First(), out taskid))
|
||||
{
|
||||
var task = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
var tasks = FIXMEGlobal.WorkThread.CurrentTasks;
|
||||
|
||||
if (task != null)
|
||||
tasks.Insert(0, task);
|
||||
|
||||
task = tasks.FirstOrDefault(x => x.TaskID == taskid);
|
||||
if (task == null)
|
||||
{
|
||||
info.ReportClientError("No such task", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (parts.Last().ToLowerInvariant())
|
||||
{
|
||||
case "stopaftercurrentfile":
|
||||
task.Stop(allowCurrentFileToFinish: true);
|
||||
info.OutputOK();
|
||||
return;
|
||||
|
||||
case "stopnow":
|
||||
task.Stop(allowCurrentFileToFinish: false);
|
||||
info.OutputOK();
|
||||
return;
|
||||
|
||||
case "abort":
|
||||
task.Abort();
|
||||
info.OutputOK();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
info.ReportClientError("Invalid or missing task id", System.Net.HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Tasks : IRESTMethodGET
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var cur = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
var n = FIXMEGlobal.WorkThread.CurrentTasks;
|
||||
|
||||
if (cur != null)
|
||||
n.Insert(0, cur);
|
||||
|
||||
info.OutputOK(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Server.Serialization;
|
||||
using System.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class UISettings : IRESTMethodGET, IRESTMethodPOST, IRESTMethodPATCH
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.OutputOK(FIXMEGlobal.DataConnection.GetUISettingsSchemes());
|
||||
}
|
||||
else
|
||||
{
|
||||
info.OutputOK(FIXMEGlobal.DataConnection.GetUISettings(key));
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
PATCH(key, info);
|
||||
}
|
||||
|
||||
public void PATCH(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.ReportClientError("Scheme is missing", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
IDictionary<string, string> data;
|
||||
try
|
||||
{
|
||||
data = Serializer.Deserialize<Dictionary<string, string>>(new StreamReader(info.Request.Body));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
info.ReportClientError(string.Format("Unable to parse settings object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
info.ReportClientError("Unable to parse settings object", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.Request.Method == "POST")
|
||||
FIXMEGlobal.DataConnection.SetUISettings(key, data);
|
||||
else
|
||||
FIXMEGlobal.DataConnection.UpdateUISettings(key, data);
|
||||
info.OutputOK();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Updates : IRESTMethodPOST
|
||||
{
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
switch ((key ?? "").ToLowerInvariant())
|
||||
{
|
||||
case "check":
|
||||
FIXMEGlobal.UpdatePoller.CheckNow();
|
||||
info.OutputOK();
|
||||
return;
|
||||
|
||||
default:
|
||||
info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class WebModule : IRESTMethodPOST
|
||||
{
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
var m = Duplicati.Library.DynamicLoader.WebLoader.Modules.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||
if (m == null)
|
||||
{
|
||||
info.ReportClientError(string.Format("No such command {0}", key), System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
info.OutputOK(new {
|
||||
Status = "OK",
|
||||
Result = m.Execute(info.Request.Form.Where(x => !x.Name.Equals("command", StringComparison.OrdinalIgnoreCase)
|
||||
).ToDictionary(x => x.Name, x => x.Value))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class WebModules : IRESTMethodGET
|
||||
{
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
info.OutputOK(Duplicati.Library.DynamicLoader.WebLoader.Modules);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,7 +690,7 @@ namespace Duplicati.Server
|
||||
if (ex is UserInformationException exception)
|
||||
messageid = exception.HelpID;
|
||||
|
||||
FIXMEGlobal.IncrementLastDataUpdateID();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.DataConnection.RegisterNotification(
|
||||
NotificationType.Error,
|
||||
backup.IsTemporary ?
|
||||
@@ -856,6 +856,12 @@ namespace Duplicati.Server
|
||||
? string.Format("Got {0} warning(s)", result.Warnings.Count())
|
||||
: string.Format("Got {0} error(s)", result.Errors.Count());
|
||||
|
||||
// If there is only one error or warning, show the message
|
||||
if (result.ParsedResult == ParsedResultType.Warning && result.Warnings.Count() == 1)
|
||||
message = $"Warning: {result.Warnings.Single()}";
|
||||
else if (result.ParsedResult == ParsedResultType.Error && result.Errors.Count() == 1)
|
||||
message = $"Error: {result.Errors.Single()}";
|
||||
|
||||
FIXMEGlobal.DataConnection.RegisterNotification(
|
||||
type,
|
||||
title,
|
||||
@@ -873,7 +879,7 @@ namespace Duplicati.Server
|
||||
if (!backup.IsTemporary)
|
||||
FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
|
||||
|
||||
FIXMEGlobal.IncrementLastDataUpdateID();
|
||||
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
@@ -942,7 +948,7 @@ namespace Duplicati.Server
|
||||
return filter;
|
||||
}
|
||||
|
||||
internal static Dictionary<string, string> GetCommonOptions()
|
||||
public static Dictionary<string, string> GetCommonOptions()
|
||||
{
|
||||
return
|
||||
(from n in FIXMEGlobal.DataConnection.Settings
|
||||
|
||||
@@ -29,6 +29,9 @@ using System.Threading;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
// TODO: Rewrite this class.
|
||||
// It should just signal what new backups to run, and not mix with the worker thread.
|
||||
|
||||
namespace Duplicati.Server
|
||||
{
|
||||
/// <summary>
|
||||
@@ -41,19 +44,23 @@ namespace Duplicati.Server
|
||||
/// <summary>
|
||||
/// The thread that runs the scheduler
|
||||
/// </summary>
|
||||
private readonly Thread m_thread;
|
||||
private Thread m_thread;
|
||||
|
||||
/// <summary>
|
||||
/// A termination flag
|
||||
/// </summary>
|
||||
private volatile bool m_terminate;
|
||||
|
||||
/// <summary>
|
||||
/// The worker thread that is invoked to do work
|
||||
/// </summary>
|
||||
private readonly WorkerThread<Runner.IRunnerData> m_worker;
|
||||
private WorkerThread<Runner.IRunnerData> m_worker;
|
||||
|
||||
/// <summary>
|
||||
/// The wait event
|
||||
/// </summary>
|
||||
private readonly AutoResetEvent m_event;
|
||||
private AutoResetEvent m_event;
|
||||
|
||||
/// <summary>
|
||||
/// The data synchronization lock
|
||||
/// </summary>
|
||||
@@ -72,13 +79,20 @@ namespace Duplicati.Server
|
||||
/// <summary>
|
||||
/// List of update tasks, used to set the timestamp on the schedule once completed
|
||||
/// </summary>
|
||||
private readonly Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>> m_updateTasks;
|
||||
private Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>> m_updateTasks;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new scheduler
|
||||
/// </summary>
|
||||
public Scheduler()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes scheduler
|
||||
/// </summary>
|
||||
/// <param name="worker">The worker thread</param>
|
||||
public Scheduler(WorkerThread<Server.Runner.IRunnerData> worker)
|
||||
public void Init(WorkerThread<Runner.IRunnerData> worker)
|
||||
{
|
||||
m_thread = new Thread(new ThreadStart(Runner));
|
||||
m_worker = worker;
|
||||
@@ -93,6 +107,13 @@ namespace Duplicati.Server
|
||||
m_thread.Start();
|
||||
}
|
||||
|
||||
public IList<Tuple<long, string>> GetSchedulerQueueIds()
|
||||
{
|
||||
return (from n in WorkerQueue
|
||||
where n.Backup != null
|
||||
select new Tuple<long, string>(n.TaskID, n.Backup.ID)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the scheduler to re-evaluate the order.
|
||||
/// Call this method if something changes
|
||||
@@ -119,10 +140,7 @@ namespace Duplicati.Server
|
||||
/// </summary>
|
||||
public List<Runner.IRunnerData> WorkerQueue
|
||||
{
|
||||
get
|
||||
{
|
||||
return (from t in m_worker.CurrentTasks where t != null select t).ToList();
|
||||
}
|
||||
get { return (from t in m_worker.CurrentTasks where t != null select t).ToList(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,8 +154,13 @@ namespace Duplicati.Server
|
||||
|
||||
if (wait)
|
||||
{
|
||||
try { m_thread.Join(); }
|
||||
catch { }
|
||||
try
|
||||
{
|
||||
m_thread.Join();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +172,8 @@ namespace Duplicati.Server
|
||||
/// <param name="repetition">The repetition interval</param>
|
||||
/// <param name="allowedDays">The days the backup is allowed to run</param>
|
||||
/// <returns>The next valid date, or throws an exception if no such date can be found</returns>
|
||||
public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays)
|
||||
public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition,
|
||||
DayOfWeek[] allowedDays)
|
||||
{
|
||||
var res = basetime;
|
||||
|
||||
@@ -215,7 +239,6 @@ namespace Duplicati.Server
|
||||
t.Item1.LastRun = t.Item3;
|
||||
FIXMEGlobal.DataConnection.AddOrUpdateSchedule(t.Item1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void OnStartingWork(WorkerThread<Runner.IRunnerData> worker, Runner.IRunnerData task)
|
||||
@@ -277,11 +300,13 @@ namespace Duplicati.Server
|
||||
start = DateTime.UtcNow;
|
||||
last = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
start = GetNextValidTime(start, last, sc.Repeat, sc.AllowedDays);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex);
|
||||
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date",
|
||||
ex);
|
||||
}
|
||||
|
||||
//If time is exceeded, run it now
|
||||
@@ -289,14 +314,16 @@ namespace Duplicati.Server
|
||||
{
|
||||
var jobsToRun = new List<Server.Runner.IRunnerData>();
|
||||
//TODO: Cache this to avoid frequent lookups
|
||||
foreach (var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct().Select(x => x.ToString()))
|
||||
foreach (var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct()
|
||||
.Select(x => x.ToString()))
|
||||
{
|
||||
//See if it is already queued
|
||||
var tmplst = from n in m_worker.CurrentTasks
|
||||
where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup
|
||||
select n.Backup;
|
||||
var tastTemp = m_worker.CurrentTask;
|
||||
if (tastTemp != null && tastTemp.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup)
|
||||
if (tastTemp != null && tastTemp.Operation ==
|
||||
Duplicati.Server.Serialization.DuplicatiOperation.Backup)
|
||||
tmplst = tmplst.Union(new[] { tastTemp.Backup });
|
||||
|
||||
//If it is not already in queue, put it there
|
||||
@@ -307,13 +334,18 @@ namespace Duplicati.Server
|
||||
{
|
||||
Dictionary<string, string> options = Duplicati.Server.Runner.GetCommonOptions();
|
||||
Duplicati.Server.Runner.ApplyOptions(entry, options);
|
||||
if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
|
||||
if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery &&
|
||||
(Duplicati.Library.Utility.Power.PowerSupply.GetSource() ==
|
||||
Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
|
||||
{
|
||||
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "BackupDisabledOnBattery", "Scheduled backup disabled while on battery power.");
|
||||
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG,
|
||||
"BackupDisabledOnBattery",
|
||||
"Scheduled backup disabled while on battery power.");
|
||||
}
|
||||
else
|
||||
{
|
||||
jobsToRun.Add(Server.Runner.CreateTask(Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry));
|
||||
jobsToRun.Add(Server.Runner.CreateTask(
|
||||
Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,11 +354,15 @@ namespace Duplicati.Server
|
||||
// Calculate next time, by finding the first entry later than now
|
||||
try
|
||||
{
|
||||
start = GetNextValidTime(start, new DateTime(Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks), DateTimeKind.Utc), sc.Repeat, sc.AllowedDays);
|
||||
start = GetNextValidTime(start,
|
||||
new DateTime(
|
||||
Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks),
|
||||
DateTimeKind.Utc), sc.Repeat, sc.AllowedDays);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex);
|
||||
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(),
|
||||
"Scheduler failed to find next date", ex);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -336,7 +372,8 @@ namespace Duplicati.Server
|
||||
lock (m_lock)
|
||||
{
|
||||
// The actual last run time will be updated when the StartingWork event is raised.
|
||||
m_updateTasks[lastJob] = new Tuple<ISchedule, DateTime, DateTime>(sc, start, DateTime.UtcNow);
|
||||
m_updateTasks[lastJob] =
|
||||
new Tuple<ISchedule, DateTime, DateTime>(sc, start, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +404,7 @@ namespace Duplicati.Server
|
||||
scheduled.Remove(c);
|
||||
|
||||
//Raise event if needed
|
||||
// TODO: This triggers a new data event and a reconnect with long-poll
|
||||
if (NewSchedule != null)
|
||||
NewSchedule(this, null);
|
||||
|
||||
@@ -385,6 +423,7 @@ namespace Duplicati.Server
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: This should be handled with events, instead of one wakeup per minute
|
||||
//No tasks, check back later
|
||||
waittime = 60 * 1000;
|
||||
}
|
||||
@@ -409,6 +448,5 @@ namespace Duplicati.Server
|
||||
else
|
||||
return Array.IndexOf<DayOfWeek>(allowedDays, localTime.DayOfWeek) >= 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,21 +29,21 @@ namespace Duplicati.Server.Serializable
|
||||
/// <summary>
|
||||
/// This class collects all reportable status properties into a single class that can be exported as JSON
|
||||
/// </summary>
|
||||
public class ServerStatus : Duplicati.Server.Serialization.Interface.IServerStatus
|
||||
public class ServerStatus(LiveControls liveControls) : Duplicati.Server.Serialization.Interface.IServerStatus
|
||||
{
|
||||
public LiveControlState ProgramState
|
||||
{
|
||||
get { return EnumConverter.Convert<LiveControlState>(FIXMEGlobal.LiveControl.State); }
|
||||
get { return EnumConverter.Convert<LiveControlState>(liveControls.State); }
|
||||
}
|
||||
|
||||
public string UpdatedVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
public string UpdatedVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
var u = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
|
||||
if (u == null)
|
||||
return null;
|
||||
|
||||
|
||||
Version v;
|
||||
if (!Version.TryParse(u.Version, out v))
|
||||
return null;
|
||||
@@ -51,7 +51,7 @@ namespace Duplicati.Server.Serializable
|
||||
if (v <= System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
|
||||
return null;
|
||||
|
||||
return u.Displayname;
|
||||
return u.Displayname;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ namespace Duplicati.Server.Serializable
|
||||
|
||||
public Tuple<long, string> ActiveTask
|
||||
{
|
||||
get
|
||||
{
|
||||
get
|
||||
{
|
||||
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
if (t == null)
|
||||
return null;
|
||||
@@ -85,18 +85,18 @@ namespace Duplicati.Server.Serializable
|
||||
{
|
||||
return (
|
||||
from n in FIXMEGlobal.Scheduler.Schedule
|
||||
let backupid = (from t in n.Value.Tags
|
||||
where t != null && t.StartsWith("ID=", StringComparison.Ordinal)
|
||||
select t.Substring("ID=".Length)).FirstOrDefault()
|
||||
where !string.IsNullOrWhiteSpace(backupid)
|
||||
select new Tuple<string, DateTime>(backupid, n.Key)
|
||||
let backupid = (from t in n.Value.Tags
|
||||
where t != null && t.StartsWith("ID=", StringComparison.Ordinal)
|
||||
select t.Substring("ID=".Length)).FirstOrDefault()
|
||||
where !string.IsNullOrWhiteSpace(backupid)
|
||||
select new Tuple<string, DateTime>(backupid, n.Key)
|
||||
).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool HasWarning { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning; } }
|
||||
public bool HasError { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError; } }
|
||||
|
||||
|
||||
public SuggestedStatusIcon SuggestedStatusIcon
|
||||
{
|
||||
get
|
||||
@@ -105,12 +105,12 @@ namespace Duplicati.Server.Serializable
|
||||
{
|
||||
if (this.ProgramState == LiveControlState.Paused)
|
||||
return SuggestedStatusIcon.Paused;
|
||||
|
||||
|
||||
if (this.HasError)
|
||||
return SuggestedStatusIcon.ReadyError;
|
||||
if (this.HasWarning)
|
||||
return SuggestedStatusIcon.ReadyWarning;
|
||||
|
||||
|
||||
return SuggestedStatusIcon.Ready;
|
||||
}
|
||||
else
|
||||
@@ -123,26 +123,25 @@ namespace Duplicati.Server.Serializable
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime EstimatedPauseEnd
|
||||
public DateTime EstimatedPauseEnd
|
||||
{
|
||||
get
|
||||
{
|
||||
return FIXMEGlobal.LiveControl.EstimatedPauseEnd;
|
||||
get
|
||||
{
|
||||
return liveControls.EstimatedPauseEnd;
|
||||
}
|
||||
}
|
||||
|
||||
private long m_lastEventID = FIXMEGlobal.StatusEventNotifyer.EventNo;
|
||||
|
||||
public long LastEventID
|
||||
{
|
||||
public long LastEventID
|
||||
{
|
||||
get { return m_lastEventID; }
|
||||
set { m_lastEventID = value; }
|
||||
}
|
||||
|
||||
public long LastDataUpdateID { get { return FIXMEGlobal.PeekLastDataUpdateID(); } }
|
||||
|
||||
public long LastNotificationUpdateID { get { return FIXMEGlobal.PeekLastNotificationUpdateID(); } }
|
||||
public long LastDataUpdateID => FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
|
||||
|
||||
public long LastNotificationUpdateID => FIXMEGlobal.NotificationUpdateService.LastNotificationUpdateId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace Duplicati.Server
|
||||
Nodes = lst.ToArray();
|
||||
}
|
||||
|
||||
internal static Dictionary<string, string> GetSourceNames(Serialization.Interface.IBackup backup)
|
||||
public static Dictionary<string, string> GetSourceNames(Serialization.Interface.IBackup backup)
|
||||
{
|
||||
if (backup.Sources == null || backup.Sources.Length == 0)
|
||||
return new Dictionary<string, string>();
|
||||
|
||||
@@ -2,8 +2,10 @@ using System.Collections.Generic;
|
||||
using Duplicati.Library.Localization.Short;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.Strings {
|
||||
public static class Program {
|
||||
namespace Duplicati.Server.Strings
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static string AnotherInstanceDetected { get { return LC.L(@"Another instance is running, and was notified"); } }
|
||||
public static string DatabaseOpenError(string message) { return LC.L(@"Failed to create, open or upgrade the database.
|
||||
Error message: {0}", message); }
|
||||
@@ -42,11 +44,13 @@ Error message: {0}", error); }
|
||||
public static string ServerencryptionkeyLong(string envname, string decryptionoption) { return LC.L(@"This option sets the encryption key used to scramble the local settings database. This option can also be set with the environment variable {0}. Use the option --{1} to disable the database scrambling.", envname, decryptionoption); }
|
||||
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
|
||||
public static string TempdirLong { get { return LC.L(@"This option can be used to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
|
||||
}
|
||||
internal static class Scheduler {
|
||||
public static string WebserverResetJwtConfigDescription { get { return LC.L(@"Resets the JWT configuration, invalidating any issued login tokens"); } }
|
||||
}
|
||||
internal static class Scheduler
|
||||
{
|
||||
public static string InvalidTimeSetupError(System.DateTime startdate, string interval, string alloweddays) { return LC.L(@"Unable to find a valid date, given the start date {0}, the repetition interval {1} and the allowed days {2}", startdate, interval, alloweddays); }
|
||||
}
|
||||
internal static class Server
|
||||
public static class Server
|
||||
{
|
||||
public static string DefectSSLCertInDatabase { get { return @"Unable to create SSL certificate using data from database. Starting without SSL."; } }
|
||||
public static string StartedServer(string ip, int port) { return LC.L(@"Server has started and is listening on {0}, port {1}", ip, port); }
|
||||
|
||||
@@ -1,209 +1,211 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server.Serialization;
|
||||
|
||||
namespace Duplicati.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// The thread that checks on the update server if new versions are available
|
||||
/// </summary>
|
||||
public class UpdatePollThread
|
||||
{
|
||||
private readonly Thread m_thread;
|
||||
private volatile bool m_terminated = false;
|
||||
private volatile bool m_forceCheck = false;
|
||||
private readonly object m_lock = new object();
|
||||
private readonly AutoResetEvent m_waitSignal;
|
||||
private double m_downloadProgress;
|
||||
|
||||
public bool IsUpdateRequested { get; private set; } = false;
|
||||
|
||||
public UpdatePollerStates ThreadState { get; private set; }
|
||||
public double DownloadProgess
|
||||
{
|
||||
get { return m_downloadProgress; }
|
||||
|
||||
private set
|
||||
{
|
||||
var oldv = m_downloadProgress;
|
||||
m_downloadProgress = value;
|
||||
if ((int)(oldv * 100) != (int)(value * 100))
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
}
|
||||
|
||||
public UpdatePollThread()
|
||||
{
|
||||
m_waitSignal = new AutoResetEvent(false);
|
||||
ThreadState = UpdatePollerStates.Waiting;
|
||||
m_thread = new Thread(Run);
|
||||
m_thread.IsBackground = true;
|
||||
m_thread.Name = "UpdatePollThread";
|
||||
m_thread.Start();
|
||||
}
|
||||
|
||||
public void CheckNow()
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
m_forceCheck = true;
|
||||
m_waitSignal.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
m_terminated = true;
|
||||
m_waitSignal.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Reschedule()
|
||||
{
|
||||
m_waitSignal.Set();
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
// Wait on startup
|
||||
m_waitSignal.WaitOne(TimeSpan.FromMinutes(1), true);
|
||||
|
||||
while (!m_terminated)
|
||||
{
|
||||
var nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck;
|
||||
|
||||
var maxcheck = TimeSpan.FromDays(7);
|
||||
try
|
||||
{
|
||||
maxcheck = Library.Utility.Timeparser.ParseTimeSpan(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateCheckInterval);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// If we have some weirdness, just check now
|
||||
if (nextCheck - DateTime.UtcNow > maxcheck)
|
||||
nextCheck = DateTime.UtcNow - TimeSpan.FromSeconds(1);
|
||||
|
||||
if (nextCheck < DateTime.UtcNow || m_forceCheck)
|
||||
{
|
||||
lock (m_lock)
|
||||
m_forceCheck = false;
|
||||
|
||||
ThreadState = UpdatePollerStates.Checking;
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
DateTime started = DateTime.UtcNow;
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.LastUpdateCheck = started;
|
||||
nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck;
|
||||
|
||||
Library.AutoUpdater.ReleaseType rt;
|
||||
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateChannel, true, out rt))
|
||||
rt = Duplicati.Library.AutoUpdater.ReleaseType.Unknown;
|
||||
|
||||
// Choose the default channel in case we have unknown
|
||||
rt = rt == Duplicati.Library.AutoUpdater.ReleaseType.Unknown ? Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel : rt;
|
||||
|
||||
try
|
||||
{
|
||||
var update = Duplicati.Library.AutoUpdater.UpdaterManager.CheckForUpdate(rt);
|
||||
if (update != null)
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = update;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// It could be that we have registered an update from a more unstable channel,
|
||||
// but the user has switched to a more stable channel.
|
||||
// In that case we discard the old update to avoid offering it.
|
||||
if (FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion != null)
|
||||
{
|
||||
Library.AutoUpdater.ReleaseType updatert;
|
||||
var updatertstring = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.ReleaseType;
|
||||
if (string.Equals(updatertstring, "preview", StringComparison.OrdinalIgnoreCase))
|
||||
updatertstring = Library.AutoUpdater.ReleaseType.Experimental.ToString();
|
||||
|
||||
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(updatertstring, true, out updatert))
|
||||
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
|
||||
|
||||
if (updatert == Duplicati.Library.AutoUpdater.ReleaseType.Unknown)
|
||||
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
|
||||
|
||||
if (updatert > rt)
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = null;
|
||||
}
|
||||
|
||||
var updatedinfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
|
||||
if (updatedinfo != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(updatedinfo.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
|
||||
{
|
||||
var package = updatedinfo.FindPackage();
|
||||
|
||||
FIXMEGlobal.DataConnection.RegisterNotification(
|
||||
NotificationType.Information,
|
||||
"Found update",
|
||||
updatedinfo.Displayname,
|
||||
null,
|
||||
null,
|
||||
"update:new",
|
||||
null,
|
||||
"NewUpdateFound",
|
||||
null,
|
||||
(self, all) =>
|
||||
{
|
||||
return all.FirstOrDefault(x => x.Action == "update:new") ?? self;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadProgess = 0;
|
||||
|
||||
if (ThreadState != UpdatePollerStates.Waiting)
|
||||
{
|
||||
ThreadState = UpdatePollerStates.Waiting;
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
var waitTime = nextCheck - DateTime.UtcNow;
|
||||
|
||||
// Guard against spin-loop
|
||||
if (waitTime.TotalSeconds < 5)
|
||||
waitTime = TimeSpan.FromSeconds(5);
|
||||
|
||||
// Guard against year-long waits
|
||||
// A re-check does not cause an update check
|
||||
if (waitTime.TotalDays > 1)
|
||||
waitTime = TimeSpan.FromDays(1);
|
||||
|
||||
m_waitSignal.WaitOne(waitTime, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization;
|
||||
|
||||
namespace Duplicati.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// The thread that checks on the update server if new versions are available
|
||||
/// </summary>
|
||||
public class UpdatePollThread(Connection connection, EventPollNotify eventPollNotify)
|
||||
{
|
||||
private Thread m_thread;
|
||||
private volatile bool m_terminated = false;
|
||||
private volatile bool m_forceCheck = false;
|
||||
private readonly object m_lock = new object();
|
||||
private AutoResetEvent m_waitSignal;
|
||||
private double m_downloadProgress;
|
||||
|
||||
public bool IsUpdateRequested { get; private set; } = false;
|
||||
|
||||
public UpdatePollerStates ThreadState { get; private set; }
|
||||
public double DownloadProgess
|
||||
{
|
||||
get { return m_downloadProgress; }
|
||||
|
||||
private set
|
||||
{
|
||||
var oldv = m_downloadProgress;
|
||||
m_downloadProgress = value;
|
||||
if ((int)(oldv * 100) != (int)(value * 100))
|
||||
eventPollNotify.SignalNewEvent();
|
||||
}
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
m_waitSignal = new AutoResetEvent(false);
|
||||
ThreadState = UpdatePollerStates.Waiting;
|
||||
m_thread = new Thread(Run)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "UpdatePollThread"
|
||||
};
|
||||
m_thread.Start();
|
||||
}
|
||||
|
||||
public void CheckNow()
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
m_forceCheck = true;
|
||||
m_waitSignal.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
m_terminated = true;
|
||||
m_waitSignal.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Reschedule()
|
||||
{
|
||||
m_waitSignal.Set();
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
// Wait on startup
|
||||
m_waitSignal.WaitOne(TimeSpan.FromMinutes(1), true);
|
||||
|
||||
while (!m_terminated)
|
||||
{
|
||||
var nextCheck = connection.ApplicationSettings.NextUpdateCheck;
|
||||
|
||||
var maxcheck = TimeSpan.FromDays(7);
|
||||
try
|
||||
{
|
||||
maxcheck = Library.Utility.Timeparser.ParseTimeSpan(connection.ApplicationSettings.UpdateCheckInterval);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// If we have some weirdness, just check now
|
||||
if (nextCheck - DateTime.UtcNow > maxcheck)
|
||||
nextCheck = DateTime.UtcNow - TimeSpan.FromSeconds(1);
|
||||
|
||||
if (nextCheck < DateTime.UtcNow || m_forceCheck)
|
||||
{
|
||||
lock (m_lock)
|
||||
m_forceCheck = false;
|
||||
|
||||
ThreadState = UpdatePollerStates.Checking;
|
||||
eventPollNotify.SignalNewEvent();
|
||||
|
||||
DateTime started = DateTime.UtcNow;
|
||||
connection.ApplicationSettings.LastUpdateCheck = started;
|
||||
nextCheck = connection.ApplicationSettings.NextUpdateCheck;
|
||||
|
||||
Library.AutoUpdater.ReleaseType rt;
|
||||
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(connection.ApplicationSettings.UpdateChannel, true, out rt))
|
||||
rt = Duplicati.Library.AutoUpdater.ReleaseType.Unknown;
|
||||
|
||||
// Choose the default channel in case we have unknown
|
||||
rt = rt == Duplicati.Library.AutoUpdater.ReleaseType.Unknown ? Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel : rt;
|
||||
|
||||
try
|
||||
{
|
||||
var update = Duplicati.Library.AutoUpdater.UpdaterManager.CheckForUpdate(rt);
|
||||
if (update != null)
|
||||
connection.ApplicationSettings.UpdatedVersion = update;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// It could be that we have registered an update from a more unstable channel,
|
||||
// but the user has switched to a more stable channel.
|
||||
// In that case we discard the old update to avoid offering it.
|
||||
if (connection.ApplicationSettings.UpdatedVersion != null)
|
||||
{
|
||||
Library.AutoUpdater.ReleaseType updatert;
|
||||
var updatertstring = connection.ApplicationSettings.UpdatedVersion.ReleaseType;
|
||||
if (string.Equals(updatertstring, "preview", StringComparison.OrdinalIgnoreCase))
|
||||
updatertstring = Library.AutoUpdater.ReleaseType.Experimental.ToString();
|
||||
|
||||
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(updatertstring, true, out updatert))
|
||||
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
|
||||
|
||||
if (updatert == Duplicati.Library.AutoUpdater.ReleaseType.Unknown)
|
||||
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
|
||||
|
||||
if (updatert > rt)
|
||||
connection.ApplicationSettings.UpdatedVersion = null;
|
||||
}
|
||||
|
||||
var updatedinfo = connection.ApplicationSettings.UpdatedVersion;
|
||||
if (updatedinfo != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(updatedinfo.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
|
||||
{
|
||||
var package = updatedinfo.FindPackage();
|
||||
|
||||
connection.RegisterNotification(
|
||||
NotificationType.Information,
|
||||
"Found update",
|
||||
updatedinfo.Displayname,
|
||||
null,
|
||||
null,
|
||||
"update:new",
|
||||
null,
|
||||
"NewUpdateFound",
|
||||
null,
|
||||
(self, all) =>
|
||||
{
|
||||
return all.FirstOrDefault(x => x.Action == "update:new") ?? self;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadProgess = 0;
|
||||
|
||||
if (ThreadState != UpdatePollerStates.Waiting)
|
||||
{
|
||||
ThreadState = UpdatePollerStates.Waiting;
|
||||
eventPollNotify.SignalNewEvent();
|
||||
}
|
||||
|
||||
var waitTime = nextCheck - DateTime.UtcNow;
|
||||
|
||||
// Guard against spin-loop
|
||||
if (waitTime.TotalSeconds < 5)
|
||||
waitTime = TimeSpan.FromSeconds(5);
|
||||
|
||||
// Guard against year-long waits
|
||||
// A re-check does not cause an update check
|
||||
if (waitTime.TotalDays > 1)
|
||||
waitTime = TimeSpan.FromDays(1);
|
||||
|
||||
m_waitSignal.WaitOne(waitTime, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using HttpServer;
|
||||
using HttpServer.HttpModules;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class AuthenticationHandler : HttpModule
|
||||
{
|
||||
private const string AUTH_COOKIE_NAME = "session-auth";
|
||||
private const string NONCE_COOKIE_NAME = "session-nonce";
|
||||
|
||||
private const string XSRF_COOKIE_NAME = "xsrf-token";
|
||||
private const string XSRF_HEADER_NAME = "X-XSRF-Token";
|
||||
|
||||
private const string TRAYICONPASSWORDSOURCE_HEADER = "X-TrayIcon-PasswordSource";
|
||||
|
||||
public const string LOGIN_SCRIPT_URI = "/login.cgi";
|
||||
public const string LOGOUT_SCRIPT_URI = "/logout.cgi";
|
||||
public const string CAPTCHA_IMAGE_URI = RESTHandler.API_URI_PATH + "/captcha/";
|
||||
|
||||
private const int XSRF_TIMEOUT_MINUTES = 10;
|
||||
private const int AUTH_TIMEOUT_MINUTES = 10;
|
||||
|
||||
private readonly ConcurrentDictionary<string, DateTime> m_activeTokens = new ConcurrentDictionary<string, DateTime>();
|
||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, string>> m_activeNonces = new ConcurrentDictionary<string, Tuple<DateTime, string>>();
|
||||
private readonly ConcurrentDictionary<string, DateTime> m_activexsrf = new ConcurrentDictionary<string, DateTime>();
|
||||
|
||||
readonly System.Security.Cryptography.RandomNumberGenerator m_prng = System.Security.Cryptography.RNGCryptoServiceProvider.Create();
|
||||
|
||||
private string FindXSRFToken(HttpServer.IHttpRequest request)
|
||||
{
|
||||
string xsrftoken = request.Headers[XSRF_HEADER_NAME] ?? "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken))
|
||||
{
|
||||
var xsrfq = request.Form[XSRF_HEADER_NAME] ?? request.Form[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
|
||||
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken))
|
||||
{
|
||||
var xsrfq = request.QueryString[XSRF_HEADER_NAME] ?? request.QueryString[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
|
||||
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
|
||||
}
|
||||
|
||||
return xsrftoken;
|
||||
}
|
||||
|
||||
private bool AddXSRFTokenToRespone(HttpServer.IHttpResponse response)
|
||||
{
|
||||
if (m_activexsrf.Count > 500)
|
||||
return false;
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
m_prng.GetBytes(buf);
|
||||
var token = Convert.ToBase64String(buf);
|
||||
|
||||
m_activexsrf.AddOrUpdate(token, key => expires, (key, existingExpires) =>
|
||||
{
|
||||
// Simulate the original behavior => if the random token, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(XSRF_COOKIE_NAME, token, expires));
|
||||
return true;
|
||||
}
|
||||
|
||||
private string FindAuthCookie(HttpServer.IHttpRequest request)
|
||||
{
|
||||
var authcookie = request.Cookies[AUTH_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(AUTH_COOKIE_NAME)];
|
||||
var authform = request.Form["auth-token"] ?? request.Form[Library.Utility.Uri.UrlEncode("auth-token")];
|
||||
var authquery = request.QueryString["auth-token"] ?? request.QueryString[Library.Utility.Uri.UrlEncode("auth-token")];
|
||||
|
||||
var auth_token = string.IsNullOrWhiteSpace(authcookie?.Value) ? null : authcookie.Value;
|
||||
if (!string.IsNullOrWhiteSpace(authquery?.Value))
|
||||
auth_token = authquery.Value;
|
||||
if (!string.IsNullOrWhiteSpace(authform?.Value))
|
||||
auth_token = authform.Value;
|
||||
|
||||
return auth_token;
|
||||
}
|
||||
|
||||
private bool HasXSRFCookie(HttpServer.IHttpRequest request)
|
||||
{
|
||||
// Clean up expired XSRF cookies
|
||||
foreach (var k in (from n in m_activexsrf where DateTime.UtcNow > n.Value select n.Key))
|
||||
m_activexsrf.TryRemove(k, out _);
|
||||
|
||||
var xsrfcookie = request.Cookies[XSRF_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(XSRF_COOKIE_NAME)];
|
||||
var value = xsrfcookie == null ? null : xsrfcookie.Value;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
if (m_activexsrf.ContainsKey(value))
|
||||
{
|
||||
m_activexsrf[value] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
return true;
|
||||
}
|
||||
else if (m_activexsrf.ContainsKey(Library.Utility.Uri.UrlDecode(value)))
|
||||
{
|
||||
m_activexsrf[Library.Utility.Uri.UrlDecode(value)] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
HttpServer.HttpInput input = String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString;
|
||||
|
||||
var auth_token = FindAuthCookie(request);
|
||||
var xsrf_token = FindXSRFToken(request);
|
||||
|
||||
if (!HasXSRFCookie(request))
|
||||
{
|
||||
var cookieAdded = AddXSRFTokenToRespone(response);
|
||||
|
||||
if (!cookieAdded)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
response.Reason = "Too Many Concurrent Request, try again later";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGOUT_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(auth_token))
|
||||
{
|
||||
// Remove the active auth token
|
||||
m_activeTokens.TryRemove(auth_token, out _);
|
||||
}
|
||||
|
||||
response.Status = System.Net.HttpStatusCode.NoContent;
|
||||
response.Reason = "OK";
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (LOGIN_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Remove expired nonces
|
||||
foreach(var k in (from n in m_activeNonces where DateTime.UtcNow > n.Value.Item1 select n.Key))
|
||||
m_activeNonces.TryRemove(k, out _);
|
||||
|
||||
if (input["get-nonce"] != null && !string.IsNullOrWhiteSpace(input["get-nonce"].Value))
|
||||
{
|
||||
if (m_activeNonces.Count > 50)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
response.Reason = "Too many active login attempts";
|
||||
return true;
|
||||
}
|
||||
|
||||
var password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword;
|
||||
|
||||
if (request.Headers[TRAYICONPASSWORDSOURCE_HEADER] == "database")
|
||||
password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordTrayIconHash;
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddMinutes(AUTH_TIMEOUT_MINUTES);
|
||||
m_prng.GetBytes(buf);
|
||||
var nonce = Convert.ToBase64String(buf);
|
||||
|
||||
var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
sha256.TransformBlock(buf, 0, buf.Length, buf, 0);
|
||||
buf = Convert.FromBase64String(password);
|
||||
sha256.TransformFinalBlock(buf, 0, buf.Length);
|
||||
var pwd = Convert.ToBase64String(sha256.Hash);
|
||||
|
||||
m_activeNonces.AddOrUpdate(nonce, key => new Tuple<DateTime, string>(expires, pwd), (key, existingValue) =>
|
||||
{
|
||||
// Simulate the original behavior => if the nonce, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(NONCE_COOKIE_NAME, nonce, expires));
|
||||
using(var bw = new BodyWriter(response, request))
|
||||
{
|
||||
bw.OutputOK(new {
|
||||
Status = "OK",
|
||||
Nonce = nonce,
|
||||
Salt = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordSalt
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (input["password"] != null && !string.IsNullOrWhiteSpace(input["password"].Value))
|
||||
{
|
||||
var nonce_el = request.Cookies[NONCE_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(NONCE_COOKIE_NAME)];
|
||||
var nonce = nonce_el == null || string.IsNullOrWhiteSpace(nonce_el.Value) ? "" : nonce_el.Value;
|
||||
var urldecoded = nonce == null ? "" : Duplicati.Library.Utility.Uri.UrlDecode(nonce);
|
||||
if (m_activeNonces.ContainsKey(urldecoded))
|
||||
nonce = urldecoded;
|
||||
|
||||
if (!m_activeNonces.ContainsKey(nonce))
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Unauthorized";
|
||||
response.ContentType = "application/json";
|
||||
return true;
|
||||
}
|
||||
|
||||
var pwd = m_activeNonces[nonce].Item2;
|
||||
|
||||
// Remove the nonce
|
||||
m_activeNonces.TryRemove(nonce, out _);
|
||||
|
||||
if (pwd != input["password"].Value)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Unauthorized";
|
||||
response.ContentType = "application/json";
|
||||
return true;
|
||||
}
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddHours(1);
|
||||
m_prng.GetBytes(buf);
|
||||
var token = Duplicati.Library.Utility.Utility.Base64UrlEncode(buf);
|
||||
while (token.Length > 0 && token.EndsWith("=", StringComparison.Ordinal))
|
||||
token = token.Substring(0, token.Length - 1);
|
||||
|
||||
m_activeTokens.AddOrUpdate(token, key => expires, (key, existingValue) =>
|
||||
{
|
||||
// Simulate the original behavior => if the token, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(AUTH_COOKIE_NAME, token, expires));
|
||||
|
||||
using(var bw = new BodyWriter(response, request))
|
||||
bw.OutputOK();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var limitedAccess =
|
||||
request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase)
|
||||
;
|
||||
|
||||
// Override to allow the CAPTCHA call to go through
|
||||
if (request.Uri.AbsolutePath.StartsWith(CAPTCHA_IMAGE_URI, StringComparison.OrdinalIgnoreCase) && request.Method == "GET")
|
||||
limitedAccess = false;
|
||||
|
||||
if (limitedAccess)
|
||||
{
|
||||
if (xsrf_token != null && m_activexsrf.ContainsKey(xsrf_token))
|
||||
{
|
||||
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
m_activexsrf[xsrf_token] = expires;
|
||||
response.Cookies.Add(new ResponseCookie(XSRF_COOKIE_NAME, xsrf_token, expires));
|
||||
}
|
||||
else
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.BadRequest;
|
||||
response.Reason = "Missing XSRF Token. Please reload the page";
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword))
|
||||
return false;
|
||||
|
||||
foreach(var k in (from n in m_activeTokens where DateTime.UtcNow > n.Value select n.Key))
|
||||
m_activeTokens.TryRemove(k, out _);
|
||||
|
||||
|
||||
// If we have a valid token, proceed
|
||||
if (!string.IsNullOrWhiteSpace(auth_token))
|
||||
{
|
||||
DateTime expires;
|
||||
var found = m_activeTokens.TryGetValue(auth_token, out expires);
|
||||
if (!found)
|
||||
{
|
||||
auth_token = Duplicati.Library.Utility.Uri.UrlDecode(auth_token);
|
||||
found = m_activeTokens.TryGetValue(auth_token, out expires);
|
||||
}
|
||||
|
||||
if (found && DateTime.UtcNow < expires)
|
||||
{
|
||||
expires = DateTime.UtcNow.AddHours(1);
|
||||
|
||||
m_activeTokens[auth_token] = expires;
|
||||
response.Cookies.Add(new ResponseCookie(AUTH_COOKIE_NAME, auth_token, expires));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ("/".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase) || "/index.html".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
response.Redirect("/login.html");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (limitedAccess)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Not logged in";
|
||||
response.AddHeader("Location", "login.html");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Server.Serialization;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class BodyWriter : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly HttpServer.IHttpResponse m_resp;
|
||||
private readonly string m_jsonp;
|
||||
private static readonly object SUCCESS_RESPONSE = new { Status = "OK" };
|
||||
private readonly System.IO.StreamWriter m_bodyStreamWriter;
|
||||
|
||||
public BodyWriter(HttpServer.IHttpResponse resp, HttpServer.IHttpRequest request)
|
||||
: this(resp, request.QueryString["jsonp"].Value)
|
||||
{
|
||||
}
|
||||
|
||||
public BodyWriter(HttpServer.IHttpResponse resp, string jsonp)
|
||||
{
|
||||
m_bodyStreamWriter = new System.IO.StreamWriter(resp.Body, resp.Encoding);
|
||||
m_resp = resp;
|
||||
m_jsonp = jsonp;
|
||||
if (!m_resp.HeadersSent)
|
||||
m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
}
|
||||
public void SetOK()
|
||||
{
|
||||
m_resp.Reason = "OK";
|
||||
m_resp.Status = System.Net.HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
public void OutputOK(object result = null)
|
||||
{
|
||||
SetOK();
|
||||
WriteJsonObject(result ?? SUCCESS_RESPONSE);
|
||||
}
|
||||
|
||||
public void WriteJsonObject(object o)
|
||||
{
|
||||
if (!m_resp.HeadersSent)
|
||||
m_resp.ContentType = "application/json";
|
||||
|
||||
Task.Run(async () => {
|
||||
|
||||
if (!string.IsNullOrEmpty(m_jsonp))
|
||||
{
|
||||
await m_bodyStreamWriter.WriteAsync(m_jsonp);
|
||||
await m_bodyStreamWriter.WriteAsync('(');
|
||||
}
|
||||
|
||||
var oldCulture = CultureInfo.CurrentCulture;
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
await Serializer.SerializeJsonAsync(m_bodyStreamWriter, o, true);
|
||||
CultureInfo.CurrentCulture = oldCulture;
|
||||
|
||||
if (!string.IsNullOrEmpty(m_jsonp))
|
||||
{
|
||||
await m_bodyStreamWriter.WriteAsync(')');
|
||||
await m_bodyStreamWriter.FlushAsync();
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Task.Run(async () => {
|
||||
await DisposeAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private bool disposed = false;
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if(disposed) return;
|
||||
disposed = true;
|
||||
|
||||
if (!m_resp.HeadersSent)
|
||||
{
|
||||
await m_bodyStreamWriter.FlushAsync();
|
||||
m_resp.ContentLength = m_bodyStreamWriter.BaseStream.Length;
|
||||
m_resp.Send();
|
||||
}
|
||||
await m_bodyStreamWriter.DisposeAsync();
|
||||
}
|
||||
|
||||
internal void Flush()
|
||||
{
|
||||
Task.Run(async () => {
|
||||
await m_bodyStreamWriter.FlushAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
internal void Write(string v)
|
||||
{
|
||||
Task.Run(async () => {
|
||||
await m_bodyStreamWriter.WriteAsync(v);
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Text;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for creating Captcha images
|
||||
/// </summary>
|
||||
public static class CaptchaUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// A lookup string with characters to use
|
||||
/// </summary>
|
||||
private static readonly string DEFAULT_CHARS = "ACDEFGHJKLMNPQRTUVWXY34679";
|
||||
|
||||
/// <summary>
|
||||
/// A range of possible brush colors
|
||||
/// </summary>
|
||||
private static readonly Brush[] BRUSH_COLORS =
|
||||
typeof(Brushes)
|
||||
.GetProperties(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)
|
||||
.Where(x => x.PropertyType == typeof(Brush))
|
||||
.Select(x => x.GetValue(null, null) as Brush)
|
||||
.Where(x => x != null)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Approximate the size in pixels of text drawn at the given fontsize
|
||||
/// </summary>
|
||||
private static int ApproxTextWidth(string text, FontFamily fontfamily, int fontsize)
|
||||
{
|
||||
using (var font = new Font(fontfamily, fontsize, GraphicsUnit.Pixel))
|
||||
using (var graphics = Graphics.FromImage(new Bitmap(1, 1))) {
|
||||
return (int) graphics.MeasureString(text, font).Width;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a random answer.
|
||||
/// </summary>
|
||||
/// <returns>The random answer.</returns>
|
||||
/// <param name="allowedchars">The list of allowed chars, supply a character multiple times to change frequency.</param>
|
||||
/// <param name="minlength">The minimum answer length.</param>
|
||||
/// <param name="maxlength">The maximum answer length.</param>
|
||||
public static string CreateRandomAnswer(string allowedchars = null, int minlength = 10, int maxlength = 12)
|
||||
{
|
||||
allowedchars = allowedchars ?? DEFAULT_CHARS;
|
||||
var rnd = new Random();
|
||||
var len = rnd.Next(Math.Min(minlength, maxlength), Math.Max(minlength, maxlength) + 1);
|
||||
if (len <= 0)
|
||||
throw new ArgumentException($"The values ${minlength} and ${maxlength} gave a final length of {len} and it must be greater than 0");
|
||||
|
||||
return new string(Enumerable.Range(0, len).Select(x => allowedchars[rnd.Next(0, allowedchars.Length)]).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a captcha image.
|
||||
/// </summary>
|
||||
/// <returns>The captcha image.</returns>
|
||||
/// <param name="answer">The captcha solution string.</param>
|
||||
/// <param name="size">The size of the image, omit to get a size based on the string.</param>
|
||||
/// <param name="fontsize">The size of the font used to create the captcha, in pixels.</param>
|
||||
public static Bitmap CreateCaptcha(string answer, Size size = default(Size), int fontsize = 40)
|
||||
{
|
||||
var fontfamily = FontFamily.GenericSansSerif;
|
||||
var text_width = ApproxTextWidth(answer, fontfamily, fontsize);
|
||||
if (size.Width == 0 || size.Height == 0)
|
||||
size = new Size((int) (text_width * 1.2), (int) (fontsize * 1.2));
|
||||
|
||||
var bmp = new Bitmap(size.Width, size.Height);
|
||||
var rnd = new Random();
|
||||
var stray_x = fontsize / 2;
|
||||
var stray_y = size.Height / 4;
|
||||
var ans_stray_x = fontsize / 3;
|
||||
var ans_stray_y = size.Height / 6;
|
||||
using (var graphics = Graphics.FromImage(bmp))
|
||||
using (var font1 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel))
|
||||
using (var font2 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel))
|
||||
using (var font3 = new HatchBrush(HatchStyle.Shingle, Color.GhostWhite, Color.DarkBlue))
|
||||
{
|
||||
graphics.Clear(Color.White);
|
||||
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
|
||||
|
||||
// Apply a some background string to make it hard to do OCR
|
||||
foreach (var color in new[] { Color.Yellow, Color.LightGreen, Color.GreenYellow })
|
||||
using (var brush = new SolidBrush(color))
|
||||
graphics.DrawString(CreateRandomAnswer(minlength: answer.Length, maxlength: answer.Length), font2, brush, rnd.Next(-stray_x, stray_x), rnd.Next(-stray_y, stray_y));
|
||||
|
||||
|
||||
var spacing = (size.Width / fontsize) + rnd.Next(0, stray_x);
|
||||
|
||||
// Create a vertical background lines
|
||||
for (var i = rnd.Next(0, stray_x); i < size.Width; i += spacing)
|
||||
using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)]))
|
||||
graphics.DrawLine(pen, i + rnd.Next(-stray_x, stray_x), rnd.Next(0, stray_y), i + rnd.Next(-stray_x, stray_x), size.Height - rnd.Next(0, stray_y));
|
||||
|
||||
spacing = (size.Height / fontsize) + rnd.Next(0, stray_y);
|
||||
// Create a horizontal background lines
|
||||
for (var i = rnd.Next(0, stray_y); i < size.Height; i += spacing)
|
||||
using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)]))
|
||||
graphics.DrawLine(pen, rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y), size.Width - rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y));
|
||||
|
||||
// Draw the actual answer
|
||||
graphics.DrawString(answer, font1, font3, ((size.Width - text_width) / 2) + rnd.Next(-ans_stray_x, ans_stray_x), ((size.Height - fontsize) / 2) + rnd.Next(-ans_stray_y, ans_stray_y));
|
||||
|
||||
return bmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using HttpServer;
|
||||
using HttpServer.HttpModules;
|
||||
using HttpServer.Exceptions;
|
||||
using Duplicati.Library.Common.IO;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
internal class IndexHtmlHandler : HttpModule
|
||||
{
|
||||
private readonly string m_webroot;
|
||||
|
||||
private static readonly string[] ForbiddenChars = new string[] {"\\", "..", ":"}.Union(from n in System.IO.Path.GetInvalidPathChars() select n.ToString()).Distinct().ToArray();
|
||||
private static readonly string DirSep = Util.DirectorySeparatorString;
|
||||
|
||||
public IndexHtmlHandler(string webroot) { m_webroot = webroot; }
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
var path = this.GetPath(request.Uri);
|
||||
var html = System.IO.Path.Combine(path, "index.html");
|
||||
var htm = System.IO.Path.Combine(path, "index.htm");
|
||||
|
||||
if (System.IO.Directory.Exists(path) && (System.IO.File.Exists(html) || System.IO.File.Exists(htm)))
|
||||
{
|
||||
if (!request.Uri.AbsolutePath.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
response.Redirect(request.Uri.AbsolutePath + "/");
|
||||
return true;
|
||||
}
|
||||
|
||||
response.Status = System.Net.HttpStatusCode.OK;
|
||||
response.Reason = "OK";
|
||||
response.ContentType = "text/html; charset=utf-8";
|
||||
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
|
||||
using (var fs = System.IO.File.OpenRead(System.IO.File.Exists(html) ? html : htm))
|
||||
{
|
||||
response.ContentLength = fs.Length;
|
||||
response.Body = fs;
|
||||
response.Send();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetPath(Uri uri)
|
||||
{
|
||||
if (ForbiddenChars.Any(x => uri.AbsolutePath.Contains(x)))
|
||||
throw new BadRequestException("Illegal path");
|
||||
var uripath = Uri.UnescapeDataString(uri.AbsolutePath);
|
||||
while(uripath.Length > 0 && (uripath.StartsWith("/", StringComparison.Ordinal) || uripath.StartsWith(DirSep, StringComparison.Ordinal)))
|
||||
uripath = uripath.Substring(1);
|
||||
return System.IO.Path.Combine(m_webroot, uripath.Replace('/', System.IO.Path.DirectorySeparatorChar));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HttpServer.HttpModules;
|
||||
|
||||
using Duplicati.Server.WebServer.RESTMethods;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class RESTHandler : HttpModule
|
||||
{
|
||||
public const string API_URI_PATH = "/api/v1";
|
||||
public static readonly int API_URI_SEGMENTS = API_URI_PATH.Split(new char[] {'/'}).Length;
|
||||
|
||||
private static readonly Dictionary<string, IRESTMethod> _modules = new Dictionary<string, IRESTMethod>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static IDictionary<string, IRESTMethod> Modules { get { return _modules; } }
|
||||
|
||||
/// <summary>
|
||||
/// Loads all REST modules in the Duplicati.Server.WebServer.RESTMethods namespace
|
||||
/// </summary>
|
||||
static RESTHandler()
|
||||
{
|
||||
var lst =
|
||||
from n in typeof(IRESTMethod).Assembly.GetTypes()
|
||||
where
|
||||
n.Namespace == typeof(IRESTMethod).Namespace
|
||||
&&
|
||||
typeof(IRESTMethod).IsAssignableFrom(n)
|
||||
&&
|
||||
!n.IsAbstract
|
||||
&&
|
||||
!n.IsInterface
|
||||
select n;
|
||||
|
||||
foreach(var t in lst)
|
||||
{
|
||||
var m = (IRESTMethod)Activator.CreateInstance(t);
|
||||
_modules.Add(t.Name.ToLowerInvariant(), m);
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandleControlCGI(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, Type module)
|
||||
{
|
||||
var method = request.Method;
|
||||
if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"]))
|
||||
method = request.Headers["X-HTTP-Method-Override"];
|
||||
|
||||
DoProcess(request, response, session, method, module.Name.ToLowerInvariant(), (String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString)["id"].Value);
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, System.Globalization.CultureInfo> _cultureCache = new ConcurrentDictionary<string, System.Globalization.CultureInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static System.Globalization.CultureInfo ParseRequestCulture(RequestInfo info)
|
||||
{
|
||||
// Inject the override
|
||||
return ParseRequestCulture(string.Format("{0},{1}", info.Request.Headers["X-UI-Language"], info.Request.Headers["Accept-Language"]));
|
||||
}
|
||||
|
||||
public static System.Globalization.CultureInfo ParseDefaultRequestCulture(RequestInfo info)
|
||||
{
|
||||
if (info == null)
|
||||
return null;
|
||||
return ParseRequestCulture(info.Request.Headers["Accept-Language"]);
|
||||
}
|
||||
|
||||
private static System.Globalization.CultureInfo ParseRequestCulture(string acceptheader)
|
||||
{
|
||||
acceptheader = acceptheader ?? string.Empty;
|
||||
|
||||
// Lock-free read
|
||||
System.Globalization.CultureInfo ci;
|
||||
if (_cultureCache.TryGetValue(acceptheader, out ci))
|
||||
return ci;
|
||||
|
||||
// Lock-free assignment, we might compute the value twice
|
||||
return _cultureCache[acceptheader] =
|
||||
// Parse headers like "Accept-Language: da, en-gb;q=0.8, en;q=0.7"
|
||||
acceptheader
|
||||
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x =>
|
||||
{
|
||||
var opts = x.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var lang = opts.FirstOrDefault();
|
||||
var weight =
|
||||
opts.Where(y => y.StartsWith("q=", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(y =>
|
||||
{
|
||||
float f;
|
||||
float.TryParse(y.Substring(2), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f);
|
||||
return f;
|
||||
}).FirstOrDefault();
|
||||
|
||||
// Set the default weight=1
|
||||
if (weight <= 0.001 && weight >= 0)
|
||||
weight = 1;
|
||||
|
||||
return new KeyValuePair<string, float>(lang, weight);
|
||||
})
|
||||
// Handle priority
|
||||
.OrderByDescending(x => x.Value)
|
||||
.Select(x => x.Key)
|
||||
.Distinct()
|
||||
// Filter invalid/unsupported items
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x) && Library.Localization.LocalizationService.ParseCulture(x) != null)
|
||||
.Select(x => Library.Localization.LocalizationService.ParseCulture(x))
|
||||
// And get the first that works
|
||||
.FirstOrDefault();
|
||||
|
||||
}
|
||||
|
||||
public static void DoProcess(RequestInfo info, string method, string module, string key)
|
||||
{
|
||||
var ci = ParseRequestCulture(info);
|
||||
|
||||
using (Library.Localization.LocalizationService.TemporaryContext(ci))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ci != null)
|
||||
info.Response.AddHeader("Content-Language", ci.Name);
|
||||
|
||||
IRESTMethod mod;
|
||||
_modules.TryGetValue(module, out mod);
|
||||
|
||||
if (mod == null)
|
||||
{
|
||||
info.Response.Status = System.Net.HttpStatusCode.NotFound;
|
||||
info.Response.Reason = "No such module";
|
||||
}
|
||||
else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get)
|
||||
{
|
||||
if (info.Request.Form != HttpServer.HttpForm.EmptyForm)
|
||||
{
|
||||
if (info.Request.QueryString == HttpServer.HttpInput.Empty)
|
||||
{
|
||||
var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
r.SetValue(info.Request, new HttpServer.HttpInput("formdata"));
|
||||
}
|
||||
|
||||
foreach (HttpServer.HttpInputItem v in info.Request.Form)
|
||||
if (!info.Request.QueryString.Contains(v.Name))
|
||||
info.Request.QueryString.Add(v.Name, v.Value);
|
||||
}
|
||||
|
||||
get.GET(key, info);
|
||||
}
|
||||
else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put)
|
||||
put.PUT(key, info);
|
||||
else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post)
|
||||
{
|
||||
if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty)
|
||||
{
|
||||
var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] {typeof(HttpServer.HttpForm)}, null);
|
||||
r.Invoke(info.Request, new object[] {new HttpServer.HttpForm(info.Request.QueryString)});
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (HttpServer.HttpInputItem v in info.Request.QueryString)
|
||||
if (!info.Request.Form.Contains(v.Name))
|
||||
info.Request.Form.Add(v.Name, v.Value);
|
||||
}
|
||||
|
||||
post.POST(key, info);
|
||||
}
|
||||
else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete)
|
||||
delete.DELETE(key, info);
|
||||
else if (method == "PATCH" && mod is IRESTMethodPATCH patch)
|
||||
patch.PATCH(key, info);
|
||||
else
|
||||
{
|
||||
info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed;
|
||||
info.Response.Reason = "Method is not allowed";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex);
|
||||
Console.WriteLine(ex);
|
||||
|
||||
try
|
||||
{
|
||||
if (!info.Response.HeadersSent)
|
||||
{
|
||||
info.Response.Status = System.Net.HttpStatusCode.InternalServerError;
|
||||
info.Response.Reason = "Error";
|
||||
info.Response.ContentType = "text/plain";
|
||||
|
||||
var wex = ex;
|
||||
while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex)
|
||||
wex = wex.InnerException;
|
||||
|
||||
info.BodyWriter.WriteJsonObject(new
|
||||
{
|
||||
Message = wex.Message,
|
||||
Type = wex.GetType().Name,
|
||||
#if DEBUG
|
||||
Stacktrace = wex.ToString()
|
||||
#endif
|
||||
});
|
||||
info.BodyWriter.Flush();
|
||||
}
|
||||
}
|
||||
catch (Exception flex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError("", "Reporting error gave error", flex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void DoProcess(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, string method, string module, string key)
|
||||
{
|
||||
using(var reqinfo = new RequestInfo(request, response, session))
|
||||
DoProcess(reqinfo, method, module, key);
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
if (!request.Uri.AbsolutePath.StartsWith(API_URI_PATH, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var module = request.Uri.Segments.Skip(API_URI_SEGMENTS).FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(module))
|
||||
module = "help";
|
||||
|
||||
module = module.Trim('/');
|
||||
|
||||
var key = string.Join("", request.Uri.Segments.Skip(API_URI_SEGMENTS + 1)).Trim('/');
|
||||
|
||||
var method = request.Method;
|
||||
if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"]))
|
||||
method = request.Headers["X-HTTP-Method-Override"];
|
||||
|
||||
DoProcess(request, response, session, method, module, key);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,441 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HttpServer.HttpModules;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
/// <summary>
|
||||
/// The tag used for logging
|
||||
/// </summary>
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<Server>();
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webroot folder
|
||||
/// </summary>
|
||||
public const string OPTION_WEBROOT = "webservice-webroot";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen port
|
||||
/// </summary>
|
||||
public const string OPTION_PORT = "webservice-port";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen interface
|
||||
/// </summary>
|
||||
public const string OPTION_INTERFACE = "webservice-interface";
|
||||
|
||||
/// <summary>
|
||||
/// The default path to the web root
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_WEBROOT = "webroot";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening port
|
||||
/// </summary>
|
||||
public const int DEFAULT_OPTION_PORT = 8200;
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile";
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate key
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening interface
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_INTERFACE = "loopback";
|
||||
|
||||
/// <summary>
|
||||
/// The single webserver instance
|
||||
/// </summary>
|
||||
private readonly HttpServer.HttpServer m_server;
|
||||
|
||||
/// <summary>
|
||||
/// The webserver listening port
|
||||
/// </summary>
|
||||
public readonly int Port;
|
||||
|
||||
/// <summary>
|
||||
/// A string that is sent out instead of password values
|
||||
/// </summary>
|
||||
public const string PASSWORD_PLACEHOLDER = "**********";
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the webserver and starts it
|
||||
/// </summary>
|
||||
/// <param name="options">A set of options</param>
|
||||
public Server(IDictionary<string, string> options)
|
||||
{
|
||||
string portstring;
|
||||
IEnumerable<int> 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 _)
|
||||
select int.Parse(n);
|
||||
|
||||
if (ports == null || !ports.Any())
|
||||
ports = new int[] { DEFAULT_OPTION_PORT };
|
||||
|
||||
string interfacestring;
|
||||
System.Net.IPAddress listenInterface;
|
||||
options.TryGetValue(OPTION_INTERFACE, out interfacestring);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface;
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = DEFAULT_OPTION_INTERFACE;
|
||||
|
||||
if (interfacestring.Trim() == "*" || interfacestring.Trim().Equals("any", StringComparison.OrdinalIgnoreCase) || interfacestring.Trim().Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
listenInterface = System.Net.IPAddress.Any;
|
||||
else if (interfacestring.Trim() == "loopback")
|
||||
listenInterface = System.Net.IPAddress.Loopback;
|
||||
else
|
||||
listenInterface = System.Net.IPAddress.Parse(interfacestring);
|
||||
|
||||
string certificateFile;
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out certificateFile);
|
||||
|
||||
string certificateFilePassword;
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out certificateFilePassword);
|
||||
|
||||
X509Certificate2 cert = null;
|
||||
bool certValid = false;
|
||||
|
||||
if (certificateFile == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
cert = FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate;
|
||||
|
||||
if (cert != null)
|
||||
certValid = cert.HasPrivateKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase);
|
||||
}
|
||||
}
|
||||
else if (certificateFile.Length == 0)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(certificateFilePassword))
|
||||
cert = new X509Certificate2(certificateFile, "", X509KeyStorageFlags.Exportable);
|
||||
else
|
||||
cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable);
|
||||
|
||||
certValid = cert.HasPrivateKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
// If we are in hosted mode with no specified port,
|
||||
// then try different ports
|
||||
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);
|
||||
|
||||
if (certValid)
|
||||
{
|
||||
server.Start(listenInterface, p, cert, System.Security.Authentication.SslProtocols.None, null, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
server.Start(listenInterface, p);
|
||||
}
|
||||
|
||||
m_server = server;
|
||||
m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
|
||||
this.Port = p;
|
||||
|
||||
if (interfacestring != FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface)
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring;
|
||||
|
||||
if (certValid && !cert.Equals(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate))
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = cert;
|
||||
|
||||
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p));
|
||||
|
||||
return;
|
||||
}
|
||||
catch (System.Net.Sockets.SocketException)
|
||||
{
|
||||
}
|
||||
|
||||
throw new Exception(Strings.Server.ServerStartFailure(ports));
|
||||
}
|
||||
|
||||
private static void AddMimeTypes(FileModule fm)
|
||||
{
|
||||
fm.AddDefaultMimeTypes();
|
||||
fm.MimeTypes["htc"] = "text/x-component";
|
||||
fm.MimeTypes["json"] = "application/json";
|
||||
fm.MimeTypes["map"] = "application/json";
|
||||
fm.MimeTypes["htm"] = "text/html; charset=utf-8";
|
||||
fm.MimeTypes["html"] = "text/html; charset=utf-8";
|
||||
fm.MimeTypes["hbs"] = "application/x-handlebars-template";
|
||||
fm.MimeTypes["woff"] = "application/font-woff";
|
||||
fm.MimeTypes["woff2"] = "application/font-woff";
|
||||
}
|
||||
|
||||
private static HttpServer.HttpServer CreateServer(IDictionary<string, string> options)
|
||||
{
|
||||
HttpServer.HttpServer server = new HttpServer.HttpServer();
|
||||
|
||||
server.Add(new HostHeaderChecker());
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SYNO_DSM_AUTH") ?? string.Empty, "1"))
|
||||
server.Add(new SynologyAuthenticationHandler());
|
||||
|
||||
server.Add(new AuthenticationHandler());
|
||||
|
||||
server.Add(new RESTHandler());
|
||||
|
||||
string webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;
|
||||
string install_webroot = System.IO.Path.Combine(webroot, "webroot");
|
||||
|
||||
#if DEBUG
|
||||
// Easy test for extensions while debugging
|
||||
install_webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;
|
||||
|
||||
if (!System.IO.Directory.Exists(System.IO.Path.Combine(webroot, "webroot")))
|
||||
{
|
||||
//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;
|
||||
}
|
||||
}
|
||||
#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(Util.AppendDirSeparator(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison))
|
||||
#endif
|
||||
{
|
||||
webroot = userroot;
|
||||
install_webroot = webroot;
|
||||
}
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "customized")))
|
||||
{
|
||||
var customized_files = new CacheControlFileHandler("/customized/", System.IO.Path.Combine(install_webroot, "customized"));
|
||||
AddMimeTypes(customized_files);
|
||||
server.Add(customized_files);
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "oem")))
|
||||
{
|
||||
var oem_files = new CacheControlFileHandler("/oem/", System.IO.Path.Combine(install_webroot, "oem"));
|
||||
AddMimeTypes(oem_files);
|
||||
server.Add(oem_files);
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "package")))
|
||||
{
|
||||
var proxy_files = new CacheControlFileHandler("/proxy/", System.IO.Path.Combine(install_webroot, "package"));
|
||||
AddMimeTypes(proxy_files);
|
||||
server.Add(proxy_files);
|
||||
}
|
||||
|
||||
var fh = new CacheControlFileHandler("/", webroot, true);
|
||||
AddMimeTypes(fh);
|
||||
server.Add(fh);
|
||||
|
||||
server.Add(new IndexHtmlHandler(webroot));
|
||||
#if DEBUG
|
||||
//For debugging, it is nice to know when we get a 404
|
||||
server.Add(new DebugReportHandler());
|
||||
#endif
|
||||
return server;
|
||||
}
|
||||
|
||||
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 CacheControlFileHandler : FileModule
|
||||
{
|
||||
public CacheControlFileHandler(string baseUri, string basePath, bool useLastModifiedHeader = false)
|
||||
: base(baseUri, basePath, useLastModifiedHeader)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
if (!this.CanHandle(request.Uri))
|
||||
return false;
|
||||
|
||||
if (request.Uri.AbsolutePath.EndsWith("index.html", StringComparison.Ordinal) || request.Uri.AbsolutePath.EndsWith("index.htm", StringComparison.Ordinal))
|
||||
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
else
|
||||
response.AddHeader("Cache-Control", "max-age=" + (60 * 60 * 24));
|
||||
return base.Process(request, response, session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Module for injecting host header verification
|
||||
/// </summary>
|
||||
private class HostHeaderChecker : HttpModule
|
||||
{
|
||||
/// <summary>
|
||||
/// The hostnames that we allow
|
||||
/// </summary>
|
||||
private string[] m_lastSplitNames;
|
||||
|
||||
/// <summary>
|
||||
/// The string used to generate m_lastSplitNames;
|
||||
/// </summary>
|
||||
private string m_lastAllowed;
|
||||
|
||||
/// <summary>
|
||||
/// A regex to detect potential IPv4 addresses.
|
||||
/// Note that this also detects things that are not valid IPv4.
|
||||
/// </summary>
|
||||
private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}");
|
||||
/// <summary>
|
||||
/// A regex to detect potential IPv6 addresses.
|
||||
/// Note that this also detects things that are not valid IPv6.
|
||||
/// </summary>
|
||||
private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?");
|
||||
|
||||
/// <summary>
|
||||
/// The hostnames that are always allowed
|
||||
/// </summary>
|
||||
private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" };
|
||||
|
||||
/// <summary>
|
||||
/// Process the received request
|
||||
/// </summary>
|
||||
/// <returns>A flag indicating if the request is handled.</returns>
|
||||
/// <param name="request">The received request.</param>
|
||||
/// <param name="response">The response object.</param>
|
||||
/// <param name="session">The session state.</param>
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
string[] h = null;
|
||||
var hstring = FIXMEGlobal.DataConnection.ApplicationSettings.AllowedHostnames;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(hstring))
|
||||
{
|
||||
h = m_lastSplitNames;
|
||||
if (hstring != m_lastAllowed)
|
||||
{
|
||||
m_lastAllowed = hstring;
|
||||
h = m_lastSplitNames = (hstring ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
if (h == null || h.Length == 0)
|
||||
h = null;
|
||||
}
|
||||
|
||||
// For some reason, the web server strips out the host header
|
||||
var host = request.Headers["Host"];
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = request.Uri.Host;
|
||||
|
||||
// This should not happen
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
response.Reason = "Invalid request, missing host header";
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
var msg = System.Text.Encoding.ASCII.GetBytes(response.Reason);
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = msg.Length;
|
||||
response.Body.Write(msg, 0, msg.Length);
|
||||
response.Send();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the hostnames we always allow
|
||||
if (DEFAULT_ALLOWED.Contains(host, StringComparer.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Then the user specified ones
|
||||
if (h != null && h.Contains(host, StringComparer.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Disable checks if we have an asterisk
|
||||
if (h != null && Array.IndexOf(h, "*") >= 0)
|
||||
return false;
|
||||
|
||||
// Finally, check if we have a potential IP address
|
||||
var v4 = IPV4.Match(host);
|
||||
var v6 = IPV6.Match(host);
|
||||
|
||||
if ((v4.Success && v4.Length == host.Length) || (v6.Success && v6.Length == host.Length))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Verify that the hostname is indeed a valid IP address
|
||||
System.Net.IPAddress.Parse(host);
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
|
||||
// Failed to find a valid header
|
||||
response.Reason = $"The host header sent by the client is not allowed";
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
var txt = System.Text.Encoding.ASCII.GetBytes(response.Reason);
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = txt.Length;
|
||||
response.Body.Write(txt, 0, txt.Length);
|
||||
response.Send();
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using HttpServer.HttpModules;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for enforcing the built-in authentication on Synology DSM
|
||||
/// </summary>
|
||||
public class SynologyAuthenticationHandler : HttpModule
|
||||
{
|
||||
/// <summary>
|
||||
/// The path to the login.cgi script
|
||||
/// </summary>
|
||||
private readonly string LOGIN_CGI = GetEnvArg("SYNO_LOGIN_CGI", "/usr/syno/synoman/webman/login.cgi");
|
||||
/// <summary>
|
||||
/// The path to the authenticate.cgi script
|
||||
/// </summary>
|
||||
private readonly string AUTH_CGI = GetEnvArg("SYNO_AUTHENTICATE_CGI", "/usr/syno/synoman/webman/modules/authenticate.cgi");
|
||||
/// <summary>
|
||||
/// A flag indicating if only admins are allowed
|
||||
/// </summary>
|
||||
private readonly bool ADMIN_ONLY = !(GetEnvArg("SYNO_ALL_USERS", "0") == "1");
|
||||
/// <summary>
|
||||
/// A flag indicating if the XSRF token should be fetched automatically
|
||||
/// </summary>
|
||||
private readonly bool AUTO_XSRF = GetEnvArg("SYNO_AUTO_XSRF", "1") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// A flag indicating that the auth-module is fully disabled
|
||||
/// </summary>
|
||||
private readonly bool FULLY_DISABLED;
|
||||
|
||||
/// <summary>
|
||||
/// Re-evaluate the logins periodically to ensure it is still valid
|
||||
/// </summary>
|
||||
private readonly TimeSpan CACHE_TIMEOUT = TimeSpan.FromMinutes(3);
|
||||
|
||||
/// <summary>
|
||||
/// A cache of previously authenticated logins
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, DateTime> m_logincache = new ConcurrentDictionary<string, DateTime>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="T:Duplicati.Server.WebServer.SynologyAuthenticationHandler"/> class.
|
||||
/// </summary>
|
||||
public SynologyAuthenticationHandler()
|
||||
{
|
||||
Console.WriteLine("Enabling Synology integrated authentication handler");
|
||||
var disable = false;
|
||||
if (!File.Exists(LOGIN_CGI))
|
||||
{
|
||||
Console.WriteLine("Disabling webserver as the login script is not found: {0}", LOGIN_CGI);
|
||||
disable = true;
|
||||
}
|
||||
if (!File.Exists(AUTH_CGI))
|
||||
{
|
||||
Console.WriteLine("Disabling webserver as the auth script is not found: {0}", AUTH_CGI);
|
||||
disable = true;
|
||||
}
|
||||
|
||||
FULLY_DISABLED = disable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the request
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the request is handled <c>false</c> otherwise.</returns>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="response">The response.</param>
|
||||
/// <param name="session">The session.</param>
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
if (FULLY_DISABLED)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
response.Reason = "The system is incorrectly configured";
|
||||
return true;
|
||||
}
|
||||
|
||||
var limitedAccess =
|
||||
request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase)
|
||||
||
|
||||
request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGIN_SCRIPT_URI, StringComparison.OrdinalIgnoreCase)
|
||||
||
|
||||
request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGOUT_SCRIPT_URI, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!limitedAccess)
|
||||
return false;
|
||||
|
||||
var tmpenv = new Dictionary<string, string>();
|
||||
|
||||
tmpenv["REMOTE_ADDR"] = request.RemoteEndPoint.Address.ToString();
|
||||
tmpenv["REMOTE_PORT"] = request.RemoteEndPoint.Port.ToString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"]))
|
||||
tmpenv["REMOTE_ADDR"] = request.Headers["X-Real-IP"];
|
||||
if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"]))
|
||||
tmpenv["REMOTE_PORT"] = request.Headers["X-Real-Port"];
|
||||
|
||||
var loginid = request.Cookies["id"]?.Value;
|
||||
if (!string.IsNullOrWhiteSpace(loginid))
|
||||
tmpenv["HTTP_COOKIE"] = "id=" + loginid;
|
||||
|
||||
var xsrftoken = request.Headers["X-Syno-Token"];
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken))
|
||||
xsrftoken = request.QueryString["SynoToken"]?.Value;
|
||||
|
||||
var cachestring = BuildCacheKey(tmpenv, xsrftoken);
|
||||
|
||||
DateTime cacheExpires;
|
||||
if (m_logincache.TryGetValue(cachestring, out cacheExpires) && cacheExpires > DateTime.Now)
|
||||
{
|
||||
// We do not refresh the cache, as we need to ask the synology auth system periodically
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken) && AUTO_XSRF)
|
||||
{
|
||||
var authre = new Regex(@"""SynoToken""\s?\:\s?""(?<token>[^""]+)""");
|
||||
try
|
||||
{
|
||||
var resp = ShellExec(LOGIN_CGI, env: tmpenv).Result;
|
||||
|
||||
var m = authre.Match(resp);
|
||||
if (m.Success)
|
||||
xsrftoken = m.Groups["token"].Value;
|
||||
else
|
||||
throw new Exception("Unable to get XSRF token");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.InternalServerError;
|
||||
response.Reason = "The system is incorrectly configured";
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(xsrftoken))
|
||||
tmpenv["HTTP_X_SYNO_TOKEN"] = xsrftoken;
|
||||
|
||||
cachestring = BuildCacheKey(tmpenv, xsrftoken);
|
||||
|
||||
var username = GetEnvArg("SYNO_USERNAME");
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
try
|
||||
{
|
||||
username = ShellExec(AUTH_CGI, shell: false, exitcode: 0, env: tmpenv).Result;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.InternalServerError;
|
||||
response.Reason = "The system is incorrectly configured";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
response.Reason = "Permission denied, not logged in";
|
||||
return true;
|
||||
}
|
||||
|
||||
username = username.Trim();
|
||||
|
||||
if (ADMIN_ONLY)
|
||||
{
|
||||
var groups = GetEnvArg("SYNO_GROUP_IDS");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(groups))
|
||||
{
|
||||
groups = ShellExec("id", "-G '" + username.Trim().Replace("'", "\\'") + "'", exitcode: 0).Result ?? string.Empty;
|
||||
groups = groups.Replace(Environment.NewLine, String.Empty);
|
||||
}
|
||||
|
||||
if (!groups.Split(new char[] { ' ' }).Contains("101"))
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
response.Reason = "Administrator login required";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// We are now authenticated, add to cache
|
||||
m_logincache[cachestring] = DateTime.Now + CACHE_TIMEOUT;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a cache key from the environment data
|
||||
/// </summary>
|
||||
/// <returns>The cache key.</returns>
|
||||
/// <param name="values">The environment.</param>
|
||||
/// <param name="xsrftoken">The XSRF token.</param>
|
||||
private static string BuildCacheKey(Dictionary<string, string> values, string xsrftoken)
|
||||
{
|
||||
if (!values.ContainsKey("REMOTE_ADDR") || !values.ContainsKey("REMOTE_PORT") || !values.ContainsKey("HTTP_COOKIE"))
|
||||
return null;
|
||||
|
||||
return string.Format("{0}:{1}/{2}?{3}", values["REMOTE_ADDR"], values["REMOTE_PORT"], values["HTTP_COOKIE"], xsrftoken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs an external command
|
||||
/// </summary>
|
||||
/// <returns>The stdout data.</returns>
|
||||
/// <param name="command">The executable</param>
|
||||
/// <param name="args">The executable and the arguments.</param>
|
||||
/// <param name="shell">If set to <c>true</c> use the shell context for execution.</param>
|
||||
/// <param name="exitcode">Set the value to check for a particular exitcode.</param>
|
||||
private static async Task<string> ShellExec(string command, string args = null, bool shell = false, int exitcode = -1, Dictionary<string, string> env = null)
|
||||
{
|
||||
var psi = new ProcessStartInfo()
|
||||
{
|
||||
FileName = command,
|
||||
Arguments = shell ? null : args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = shell,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = false
|
||||
};
|
||||
|
||||
if (env != null)
|
||||
foreach (var pk in env)
|
||||
psi.EnvironmentVariables[pk.Key] = pk.Value;
|
||||
|
||||
using (var p = System.Diagnostics.Process.Start(psi))
|
||||
{
|
||||
if (shell && args != null)
|
||||
await p.StandardInput.WriteLineAsync(args);
|
||||
|
||||
var res = p.StandardOutput.ReadToEndAsync();
|
||||
|
||||
var tries = 10;
|
||||
var ms = (int)TimeSpan.FromSeconds(0.5).TotalMilliseconds;
|
||||
while (tries > 0 && !p.HasExited)
|
||||
{
|
||||
tries--;
|
||||
p.WaitForExit(ms);
|
||||
}
|
||||
|
||||
if (!p.HasExited)
|
||||
try { p.Kill(); }
|
||||
catch { }
|
||||
|
||||
if (!p.HasExited || (p.ExitCode != exitcode && exitcode != -1))
|
||||
throw new Exception(string.Format("Exit code was: {0}, stdout: {1}", p.ExitCode, res));
|
||||
return await res;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the environment variable argument.
|
||||
/// </summary>
|
||||
/// <returns>The environment variable.</returns>
|
||||
/// <param name="key">The name of the environment variable.</param>
|
||||
/// <param name="default">The default value.</param>
|
||||
private static string GetEnvArg(string key, string @default = null)
|
||||
{
|
||||
var res = Environment.GetEnvironmentVariable(key);
|
||||
return string.IsNullOrWhiteSpace(res) ? @default : res.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
Backup: {
|
||||
Settings: [
|
||||
{Name: "encryption-module", Value: "aes"},
|
||||
{Name: "compression-module", Value: "zip"},
|
||||
{Name: "dblock-size", Value: "50mb"},
|
||||
{Name: "keep-time", Value: ""}
|
||||
]
|
||||
},
|
||||
Schedule: {
|
||||
Repeat: "1D",
|
||||
Time: "13:00:00",
|
||||
AllowedDays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
}
|
||||
}
|
||||
"Backup": {
|
||||
"Settings": [
|
||||
{ "Name": "encryption-module", "Value": "aes" },
|
||||
{ "Name": "compression-module", "Value": "zip" },
|
||||
{ "Name": "dblock-size", "Value": "50mb" },
|
||||
{ "Name": "keep-time", "Value": "" }
|
||||
]
|
||||
},
|
||||
"Schedule": {
|
||||
"Repeat": "1D",
|
||||
"Time": "13:00:00",
|
||||
"AllowedDays": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using Duplicati.Library.AutoUpdater;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server.Serializable;
|
||||
using Duplicati.Server.WebServer.RESTMethods;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -63,7 +63,7 @@ namespace Duplicati.CommandLine.ConfigurationImporter
|
||||
{ "server-datafolder", serverDatafolder }
|
||||
};
|
||||
|
||||
ImportExportStructure importedStructure = Backups.ImportBackup(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: "), advancedOptions);
|
||||
ImportExportStructure importedStructure = BackupImportExportHandler.ImportBackup(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: "), advancedOptions);
|
||||
Console.WriteLine($"Imported \"{importedStructure.Backup.Name}\" with ID {importedStructure.Backup.ID} and local database at {importedStructure.Backup.DBPath}.");
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Edge;
|
||||
|
||||
namespace Duplicati.Browser.Test.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages a browser instance using Selenium
|
||||
/// </summary>
|
||||
public class BrowserDriver : IDisposable
|
||||
{
|
||||
private readonly Lazy<IWebDriver> _currentWebDriverLazy = new(CreateWebDriver);
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// The Selenium IWebDriver instance
|
||||
/// </summary>
|
||||
public IWebDriver Current => _currentWebDriverLazy.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the Selenium web driver (opens a browser)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static EdgeDriver CreateWebDriver()
|
||||
{
|
||||
//We use the Chrome browser
|
||||
var chromeDriverService = EdgeDriverService.CreateDefaultService(".", "msedgedriver");
|
||||
|
||||
return new EdgeDriver(chromeDriverService, new EdgeOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the Selenium web driver (closing the browser)
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentWebDriverLazy.IsValueCreated)
|
||||
{
|
||||
Current.Quit();
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DriverExtensions
|
||||
{
|
||||
public static int SingleLoopWait = 100;
|
||||
public static TimeSpan DefaultMaxWait = TimeSpan.FromSeconds(30);
|
||||
|
||||
public static IWebElement WaitForElement(this ISearchContext driver, By selector, TimeSpan? maxWait = null)
|
||||
{
|
||||
maxWait ??= DefaultMaxWait;
|
||||
var i = 0;
|
||||
while (i < maxWait.Value.TotalMilliseconds)
|
||||
{
|
||||
try
|
||||
{
|
||||
return driver.FindElement(selector);
|
||||
}
|
||||
catch (NoSuchElementException)
|
||||
{
|
||||
//swallow and wait for another loop
|
||||
}
|
||||
|
||||
i += SingleLoopWait;
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(SingleLoopWait));
|
||||
}
|
||||
|
||||
return driver.FindElement(selector);
|
||||
}
|
||||
|
||||
public static bool WaitForElementBeing(this IWebElement element, Func<IWebElement, bool> predicate,
|
||||
TimeSpan? maxWait = null)
|
||||
{
|
||||
maxWait ??= DefaultMaxWait;
|
||||
var i = 0;
|
||||
while (i < maxWait.Value.TotalMilliseconds)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (predicate.Invoke(element))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (NoSuchElementException)
|
||||
{
|
||||
//swallow and wait for another loop
|
||||
}
|
||||
|
||||
i += SingleLoopWait;
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(SingleLoopWait));
|
||||
}
|
||||
|
||||
throw new Exception($"Timout of waiting for element to satisfy predicate: {predicate}!");
|
||||
}
|
||||
|
||||
public static void SendKeysAndCheck(this IWebElement element, string text, TimeSpan? maxWait = null)
|
||||
{
|
||||
maxWait ??= DefaultMaxWait;
|
||||
var i = 0;
|
||||
while (i < maxWait.Value.TotalMilliseconds)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (element.GetAttribute("value") != text)
|
||||
{
|
||||
element.SendKeys(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (NoSuchElementException)
|
||||
{
|
||||
//swallow and wait for another loop
|
||||
}
|
||||
|
||||
i += SingleLoopWait;
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(SingleLoopWait));
|
||||
}
|
||||
|
||||
throw new Exception("Could not set text value of element!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="SpecFlow.Plus.LivingDocPlugin" Version="3.7.10" />
|
||||
<PackageReference Include="SpecFlow.NUnit" Version="3.7.13" />
|
||||
<PackageReference Include="nunit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageReference Include="Selenium.Support" Version="3.141.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="95.0.4638.5401" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Drivers\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="msedgedriver">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,22 @@
|
||||
@Duplicati
|
||||
Feature: Duplicati local backup
|
||||
Link to a feature: [Calculator](Duplicati.Browser.Test/Features/LocalBackup.feature)
|
||||
***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)**
|
||||
|
||||
Scenario: Configure Backup
|
||||
Given there is local backup defined
|
||||
When you run the backup
|
||||
Then the result should be 120
|
||||
|
||||
|
||||
Scenario Outline: Add two numbers permutations
|
||||
Given the first number is <First number>
|
||||
And the second number is <Second number>
|
||||
When the two numbers are added
|
||||
Then the result should be <Expected result>
|
||||
|
||||
Examples:
|
||||
| First number | Second number | Expected result |
|
||||
| 0 | 0 | 0 |
|
||||
| -1 | 10 | 9 |
|
||||
| 6 | 9 | 15 |
|
||||
@@ -0,0 +1,171 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by SpecFlow (https://www.specflow.org/).
|
||||
// SpecFlow Version:3.7.0.0
|
||||
// SpecFlow Generator Version:3.7.0.0
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
#region Designer generated code
|
||||
#pragma warning disable
|
||||
namespace Duplicati.Browser.Test.Features
|
||||
{
|
||||
using TechTalk.SpecFlow;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.7.0.0")]
|
||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[NUnit.Framework.TestFixtureAttribute()]
|
||||
[NUnit.Framework.DescriptionAttribute("Duplicati local backup")]
|
||||
[NUnit.Framework.CategoryAttribute("Duplicati")]
|
||||
public partial class DuplicatiLocalBackupFeature
|
||||
{
|
||||
|
||||
private TechTalk.SpecFlow.ITestRunner testRunner;
|
||||
|
||||
private string[] _featureTags = new string[] {
|
||||
"Duplicati"};
|
||||
|
||||
#line 1 "LocalBackup.feature"
|
||||
#line hidden
|
||||
|
||||
[NUnit.Framework.OneTimeSetUpAttribute()]
|
||||
public virtual void FeatureSetup()
|
||||
{
|
||||
testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner();
|
||||
TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Duplicati local backup", @"Link to a feature: [Calculator](Duplicati.Browser.Test/Features/LocalBackup.feature)
|
||||
***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)**", ProgrammingLanguage.CSharp, new string[] {
|
||||
"Duplicati"});
|
||||
testRunner.OnFeatureStart(featureInfo);
|
||||
}
|
||||
|
||||
[NUnit.Framework.OneTimeTearDownAttribute()]
|
||||
public virtual void FeatureTearDown()
|
||||
{
|
||||
testRunner.OnFeatureEnd();
|
||||
testRunner = null;
|
||||
}
|
||||
|
||||
[NUnit.Framework.SetUpAttribute()]
|
||||
public virtual void TestInitialize()
|
||||
{
|
||||
}
|
||||
|
||||
[NUnit.Framework.TearDownAttribute()]
|
||||
public virtual void TestTearDown()
|
||||
{
|
||||
testRunner.OnScenarioEnd();
|
||||
}
|
||||
|
||||
public virtual void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo)
|
||||
{
|
||||
testRunner.OnScenarioInitialize(scenarioInfo);
|
||||
testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs<NUnit.Framework.TestContext>(NUnit.Framework.TestContext.CurrentContext);
|
||||
}
|
||||
|
||||
public virtual void ScenarioStart()
|
||||
{
|
||||
testRunner.OnScenarioStart();
|
||||
}
|
||||
|
||||
public virtual void ScenarioCleanup()
|
||||
{
|
||||
testRunner.CollectScenarioErrors();
|
||||
}
|
||||
|
||||
[NUnit.Framework.TestAttribute()]
|
||||
[NUnit.Framework.DescriptionAttribute("Configure Backup")]
|
||||
public virtual void ConfigureBackup()
|
||||
{
|
||||
string[] tagsOfScenario = ((string[])(null));
|
||||
System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
|
||||
TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Configure Backup", null, tagsOfScenario, argumentsOfScenario, this._featureTags);
|
||||
#line 6
|
||||
this.ScenarioInitialize(scenarioInfo);
|
||||
#line hidden
|
||||
bool isScenarioIgnored = default(bool);
|
||||
bool isFeatureIgnored = default(bool);
|
||||
if ((tagsOfScenario != null))
|
||||
{
|
||||
isScenarioIgnored = tagsOfScenario.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
|
||||
}
|
||||
if ((this._featureTags != null))
|
||||
{
|
||||
isFeatureIgnored = this._featureTags.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
|
||||
}
|
||||
if ((isScenarioIgnored || isFeatureIgnored))
|
||||
{
|
||||
testRunner.SkipScenario();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ScenarioStart();
|
||||
#line 7
|
||||
testRunner.Given("there is local backup defined", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given ");
|
||||
#line hidden
|
||||
#line 8
|
||||
testRunner.When("you run the backup", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
|
||||
#line hidden
|
||||
#line 9
|
||||
testRunner.Then("the result should be 120", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
|
||||
#line hidden
|
||||
}
|
||||
this.ScenarioCleanup();
|
||||
}
|
||||
|
||||
[NUnit.Framework.TestAttribute()]
|
||||
[NUnit.Framework.DescriptionAttribute("Add two numbers permutations")]
|
||||
[NUnit.Framework.TestCaseAttribute("0", "0", "0", null)]
|
||||
[NUnit.Framework.TestCaseAttribute("-1", "10", "9", null)]
|
||||
[NUnit.Framework.TestCaseAttribute("6", "9", "15", null)]
|
||||
public virtual void AddTwoNumbersPermutations(string firstNumber, string secondNumber, string expectedResult, string[] exampleTags)
|
||||
{
|
||||
string[] tagsOfScenario = exampleTags;
|
||||
System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
|
||||
argumentsOfScenario.Add("First number", firstNumber);
|
||||
argumentsOfScenario.Add("Second number", secondNumber);
|
||||
argumentsOfScenario.Add("Expected result", expectedResult);
|
||||
TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Add two numbers permutations", null, tagsOfScenario, argumentsOfScenario, this._featureTags);
|
||||
#line 12
|
||||
this.ScenarioInitialize(scenarioInfo);
|
||||
#line hidden
|
||||
bool isScenarioIgnored = default(bool);
|
||||
bool isFeatureIgnored = default(bool);
|
||||
if ((tagsOfScenario != null))
|
||||
{
|
||||
isScenarioIgnored = tagsOfScenario.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
|
||||
}
|
||||
if ((this._featureTags != null))
|
||||
{
|
||||
isFeatureIgnored = this._featureTags.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
|
||||
}
|
||||
if ((isScenarioIgnored || isFeatureIgnored))
|
||||
{
|
||||
testRunner.SkipScenario();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ScenarioStart();
|
||||
#line 13
|
||||
testRunner.Given(string.Format("the first number is {0}", firstNumber), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given ");
|
||||
#line hidden
|
||||
#line 14
|
||||
testRunner.And(string.Format("the second number is {0}", secondNumber), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
|
||||
#line hidden
|
||||
#line 15
|
||||
testRunner.When("the two numbers are added", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
|
||||
#line hidden
|
||||
#line 16
|
||||
testRunner.Then(string.Format("the result should be {0}", expectedResult), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
|
||||
#line hidden
|
||||
}
|
||||
this.ScenarioCleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore
|
||||
#endregion
|
||||
@@ -0,0 +1,23 @@
|
||||
using Duplicati.Browser.Test.Drivers;
|
||||
using Duplicati.Browser.Test.PageObjects;
|
||||
using TechTalk.SpecFlow;
|
||||
|
||||
namespace Duplicati.Browser.Test.Hooks
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculator related hooks
|
||||
/// </summary>
|
||||
[Binding]
|
||||
public class DuplicatiHooks
|
||||
{
|
||||
///<summary>
|
||||
/// Reset the calculator before each scenario tagged with "Calculator"
|
||||
/// </summary>
|
||||
[BeforeScenario("Duplicati")]
|
||||
public static void BeforeScenario(BrowserDriver browserDriver)
|
||||
{
|
||||
var calculatorPageObject = new DuplicatiPageObject(browserDriver.Current);
|
||||
calculatorPageObject.Open();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using BoDi;
|
||||
using Duplicati.Browser.Test.Drivers;
|
||||
using TechTalk.SpecFlow;
|
||||
|
||||
namespace Duplicati.Browser.Test.Hooks
|
||||
{
|
||||
/// <summary>
|
||||
/// Share the same browser window for all scenarios
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This makes the sequential execution of scenarios faster (opening a new browser window each time would take more time)
|
||||
/// As a tradeoff:
|
||||
/// - we cannot run the tests in parallel
|
||||
/// - we have to "reset" the state of the browser before each scenario
|
||||
/// </remarks>
|
||||
[Binding]
|
||||
public class SharedBrowserHooks
|
||||
{
|
||||
[BeforeTestRun]
|
||||
public static void BeforeTestRun(ObjectContainer testThreadContainer)
|
||||
{
|
||||
//Initialize a shared BrowserDriver in the global container
|
||||
testThreadContainer.BaseContainer.Resolve<BrowserDriver>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using System.Threading;
|
||||
using Duplicati.Browser.Test.Drivers;
|
||||
using OpenQA.Selenium;
|
||||
|
||||
namespace Duplicati.Browser.Test.PageObjects;
|
||||
|
||||
public class AddBackupPage(IWebDriver webDriver)
|
||||
{
|
||||
private IWebElement NextButtonElement => webDriver.WaitForElement(By.ClassName("submit"));
|
||||
|
||||
public GeneralBackupSettingsPage ToGeneralSettings()
|
||||
{
|
||||
NextButtonElement.Click();
|
||||
|
||||
return new GeneralBackupSettingsPage(webDriver);
|
||||
}
|
||||
}
|
||||
|
||||
public class GeneralBackupSettingsPage(IWebDriver webDriver)
|
||||
{
|
||||
private IWebElement NameTextboxElement => webDriver.WaitForElement(By.Id("name"));
|
||||
private IWebElement NextButtonElement => webDriver.WaitForElement(By.ClassName("submit"));
|
||||
private IWebElement GeneratePasswordLink => webDriver.WaitForElement(By.PartialLinkText("Generuj"));
|
||||
|
||||
public GeneralBackupSettingsPage SetName(string name)
|
||||
{
|
||||
if (NameTextboxElement.WaitForElementBeing(e => e.GetAttribute("placeholder") is "Moje Zdjęcia" or "My Photos"))
|
||||
{
|
||||
NameTextboxElement.Clear();
|
||||
NameTextboxElement.SendKeysAndCheck(name);
|
||||
Thread.Sleep(1000);
|
||||
NameTextboxElement.SendKeysAndCheck(name);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public GeneralBackupSettingsPage GenerateRandomPassword()
|
||||
{
|
||||
GeneratePasswordLink.Click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public BackupTargetSettingsPage ToBackupTarget()
|
||||
{
|
||||
NextButtonElement.Click();
|
||||
|
||||
return new BackupTargetSettingsPage(webDriver);
|
||||
}
|
||||
}
|
||||
|
||||
public class BackupTargetSettingsPage(IWebDriver webDriver)
|
||||
{
|
||||
private IWebElement NextButtonElement => webDriver.WaitForElement(By.CssSelector("#nextStep2.submit"));
|
||||
private IWebElement ManualPathLink => webDriver.WaitForElement(By.LinkText("Podaj ścieżkę ręcznie"));
|
||||
private IWebElement FilePathTextbox => webDriver.WaitForElement(By.Id("file_path"));
|
||||
|
||||
public BackupTargetSettingsPage ChoosePathManually()
|
||||
{
|
||||
ManualPathLink.Click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public BackupTargetSettingsPage SetManualPath(string path)
|
||||
{
|
||||
if (FilePathTextbox.WaitForElementBeing(e => e.GetAttribute("placeholder") is "Wprowadź ścieżkę docelową"))
|
||||
{
|
||||
FilePathTextbox.Clear();
|
||||
FilePathTextbox.SendKeysAndCheck(path);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public BackupSourceSettingsPage ToBackupSource()
|
||||
{
|
||||
NextButtonElement.Click();
|
||||
|
||||
return new BackupSourceSettingsPage(webDriver);
|
||||
}
|
||||
}
|
||||
|
||||
public class BackupSourceSettingsPage(IWebDriver webDriver)
|
||||
{
|
||||
private IWebElement NextButtonElement => webDriver.WaitForElement(By.CssSelector("#nextStep3.submit"));
|
||||
private IWebElement FilePathTextbox => webDriver.WaitForElement(By.Id("sourcePath"));
|
||||
private IWebElement AddFilePathButton => webDriver.WaitForElement(By.Id("sourceFolderPathAdd"));
|
||||
private IWebElement ConfirmAddPathButton => webDriver.WaitForElement(By.PartialLinkText("Tak"));
|
||||
|
||||
public BackupSourceSettingsPage AddSourceManually(string path)
|
||||
{
|
||||
if (FilePathTextbox.WaitForElementBeing(e => e.GetAttribute("placeholder") is "Dodaj ścieżkę bezpośrednio"))
|
||||
{
|
||||
FilePathTextbox.Clear();
|
||||
FilePathTextbox.SendKeysAndCheck(path);
|
||||
}
|
||||
AddFilePathButton.Click();
|
||||
ConfirmAddPathButton.Click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ScheduleSettingsPage ToSchedule()
|
||||
{
|
||||
Thread.Sleep(20000);
|
||||
NextButtonElement.Click();
|
||||
|
||||
return new ScheduleSettingsPage(webDriver);
|
||||
}
|
||||
}
|
||||
|
||||
public class ScheduleSettingsPage(IWebDriver webDriver)
|
||||
{
|
||||
private IWebElement NextButtonElement => webDriver.WaitForElement(By.CssSelector("#nextStep4.submit"));
|
||||
private IWebElement AutoRunCheckbox => webDriver.WaitForElement(By.Id("useScheduleRun"));
|
||||
|
||||
public ScheduleSettingsPage DisableAutorun()
|
||||
{
|
||||
if (AutoRunCheckbox.Selected)
|
||||
{
|
||||
AutoRunCheckbox.Click();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToOptionsSettingsPage ToOptions()
|
||||
{
|
||||
NextButtonElement.Click();
|
||||
|
||||
return new ToOptionsSettingsPage(webDriver);
|
||||
}
|
||||
}
|
||||
|
||||
public class ToOptionsSettingsPage(IWebDriver webDriver)
|
||||
{
|
||||
private IWebElement SaveButtonElement => webDriver.WaitForElement(By.CssSelector("#save"));
|
||||
private IWebElement ConfirmationModal => webDriver.WaitForElement(By.CssSelector(".content.buttons"));
|
||||
|
||||
public ToOptionsSettingsPage Save()
|
||||
{
|
||||
SaveButtonElement.Click();
|
||||
ConfirmationModal.WaitForElement(By.CssSelector("ul>li:nth-child(2)>a")).Click();
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Duplicati.Browser.Test.Drivers;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
|
||||
namespace Duplicati.Browser.Test.PageObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculator Page Object
|
||||
/// </summary>
|
||||
public class DuplicatiPageObject(IWebDriver webDriver)
|
||||
{
|
||||
//The URL of the calculator to be opened in the browser
|
||||
private const string DuplicatiUrl = "http://localhost:8201";
|
||||
|
||||
//Finding elements by ID
|
||||
private IWebElement AddBackupElement => webDriver.WaitForElement(By.ClassName("add"));
|
||||
private IWebElement HomeButtonElement => webDriver.WaitForElement(By.ClassName("home"));
|
||||
private IWebElement BackupSection => webDriver.WaitForElement(By.ClassName("task"));
|
||||
|
||||
public AddBackupPage NavigateToBackupCreation()
|
||||
{
|
||||
AddBackupElement.Click();
|
||||
return new AddBackupPage(webDriver);
|
||||
}
|
||||
|
||||
public void StartBackupTask()
|
||||
{
|
||||
//Navigate to home just in case
|
||||
HomeButtonElement.Click();
|
||||
BackupSection.WaitForElement(By.ClassName("ng-binding")).Click();
|
||||
BackupSection.WaitForElement(By.XPath("/dl[1]/dd[1]/p[1]"));
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
//Open the calculator page in the browser if not opened yet
|
||||
if (webDriver.Url != DuplicatiUrl)
|
||||
{
|
||||
webDriver.Url = DuplicatiUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.IO;
|
||||
using Duplicati.Browser.Test.Drivers;
|
||||
using Duplicati.Browser.Test.PageObjects;
|
||||
using FluentAssertions;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using TechTalk.SpecFlow;
|
||||
|
||||
namespace Duplicati.Browser.Test.Steps
|
||||
{
|
||||
[Binding]
|
||||
public sealed class DuplicatiStepDefinitions(BrowserDriver browserDriver)
|
||||
{
|
||||
//Page Object for Calculator
|
||||
private readonly DuplicatiPageObject _duplicatiPageObject = new(browserDriver.Current);
|
||||
|
||||
[Given("there is local backup defined")]
|
||||
public void GivenLocalBackupDefined()
|
||||
{
|
||||
browserDriver.Current.TakeScreenshot();
|
||||
//delegate to Page Object
|
||||
_duplicatiPageObject.NavigateToBackupCreation()
|
||||
.ToGeneralSettings()
|
||||
.SetName("Local Backup")
|
||||
.GenerateRandomPassword()
|
||||
.ToBackupTarget()
|
||||
.ChoosePathManually()
|
||||
.SetManualPath(Path.Combine(Directory.GetCurrentDirectory(), "test-backup-target"))
|
||||
.ToBackupSource()
|
||||
.AddSourceManually(Path.Combine(Directory.GetCurrentDirectory(), "test-backup-source")+"/")
|
||||
.ToSchedule()
|
||||
.DisableAutorun()
|
||||
.ToOptions()
|
||||
.Save()
|
||||
;
|
||||
}
|
||||
|
||||
[When("you run the backup")]
|
||||
public void WhenBackupIsRun()
|
||||
{
|
||||
//delegate to Page Object
|
||||
_duplicatiPageObject.StartBackupTask();
|
||||
}
|
||||
|
||||
[Then("the result should be (.*)")]
|
||||
public void ThenTheResultShouldBe(int expectedResult)
|
||||
{
|
||||
//delegate to Page Object
|
||||
//var actualResult = _duplicatiPageObject.WaitForNonEmptyResult();
|
||||
|
||||
//actualResult.Should().Be(expectedResult.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
test
|
||||
@@ -25,7 +25,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Duplicati.GUI.TrayIcon
|
||||
{
|
||||
{
|
||||
/// <summary>
|
||||
/// We keep the hosted instance here to allow the TrayIcon executable to load without requiring the Server.exe
|
||||
/// </summary>
|
||||
@@ -37,19 +37,24 @@ namespace Duplicati.GUI.TrayIcon
|
||||
|
||||
public HostedInstanceKeeper(string[] args)
|
||||
{
|
||||
m_runner = new System.Threading.Thread((dummy_arg) => {
|
||||
m_runner = new System.Threading.Thread((dummy_arg) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
//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.Server.OPTION_PORT + "=", StringComparison.OrdinalIgnoreCase)))
|
||||
args = (args ?? new string[0]).Union(new string[] { "--" + Duplicati.Server.WebServer.Server.OPTION_PORT + "=8200,8300,8400,8500,8600,8700,8800,8900,8989" }).ToArray();
|
||||
|
||||
if (args == null || !args.Any(x => x.Trim().StartsWith("--" + Duplicati.Server.WebServerLoader.OPTION_PORT + "=", StringComparison.OrdinalIgnoreCase)))
|
||||
args = (args ?? new string[0]).Union(new string[] { "--" + Duplicati.Server.WebServerLoader.OPTION_PORT + "=8200,8300,8400,8500,8600,8700,8800,8900,8989" }).ToArray();
|
||||
|
||||
Duplicati.Server.Program.Main(args);
|
||||
} catch (Exception ex) {
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
m_runnerException = ex;
|
||||
Duplicati.Server.Program.ServerStartedEvent.Set();
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (InstanceShutdown != null)
|
||||
try { InstanceShutdown(); }
|
||||
catch (Exception shutex)
|
||||
@@ -62,7 +67,7 @@ namespace Duplicati.GUI.TrayIcon
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
m_runner.Start();
|
||||
|
||||
if (!Duplicati.Server.Program.ServerStartedEvent.WaitOne(TimeSpan.FromSeconds(100), true))
|
||||
|
||||
@@ -23,47 +23,89 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server.Serialization;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using Newtonsoft.Json;
|
||||
using Duplicati.WebserverCore;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Middlewares;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Duplicati.GUI.TrayIcon
|
||||
{
|
||||
public class HttpServerConnection : IDisposable
|
||||
{
|
||||
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<HttpServerConnection>();
|
||||
private const string LOGIN_SCRIPT = "login.cgi";
|
||||
private const string STATUS_WINDOW = "index.html";
|
||||
|
||||
private const string XSRF_COOKIE = "xsrf-token";
|
||||
private const string XSRF_HEADER = "X-XSRF-Token";
|
||||
private const string AUTH_COOKIE = "session-auth";
|
||||
|
||||
private const string TRAYICONPASSWORDSOURCE_HEADER = "X-TrayIcon-PasswordSource";
|
||||
|
||||
private class BackgroundRequest
|
||||
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<HttpServerConnection>();
|
||||
private const string LONGPOLL_TIMEOUT = "5m";
|
||||
private static readonly HttpClient HTTPCLIENT = new(new SocketsHttpHandler
|
||||
{
|
||||
public readonly string Method;
|
||||
public readonly string Endpoint;
|
||||
public readonly Dictionary<string, string> Query;
|
||||
|
||||
public BackgroundRequest(string method, string endpoint, Dictionary<string, string> query)
|
||||
{
|
||||
this.Method = method;
|
||||
this.Endpoint = endpoint;
|
||||
this.Query = query;
|
||||
PooledConnectionLifetime = TimeSpan.FromSeconds(10)
|
||||
})
|
||||
{
|
||||
// Max time a request can be pending, actual requests can set a lower limit
|
||||
Timeout = Library.Utility.Timeparser.ParseTimeSpan(LONGPOLL_TIMEOUT) + TimeSpan.FromSeconds(10),
|
||||
DefaultRequestHeaders = {
|
||||
UserAgent = { new ProductInfoHeaderValue("Duplicati-TrayIcon-Monitor", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()) }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private record ServerStatusImpl(
|
||||
Tuple<long, string> ActiveTask,
|
||||
LiveControlState ProgramState,
|
||||
IList<Tuple<long, string>> SchedulerQueueIds,
|
||||
bool HasWarning,
|
||||
bool HasError,
|
||||
SuggestedStatusIcon SuggestedStatusIcon,
|
||||
DateTime EstimatedPauseEnd,
|
||||
long LastEventID,
|
||||
long LastDataUpdateID,
|
||||
long LastNotificationUpdateID,
|
||||
string UpdatedVersion,
|
||||
string UpdateDownloadLink,
|
||||
UpdatePollerStates UpdaterState,
|
||||
double UpdateDownloadProgress
|
||||
) : IServerStatus;
|
||||
|
||||
private record NotificationImpl(
|
||||
long ID,
|
||||
Server.Serialization.NotificationType Type,
|
||||
string Title,
|
||||
string Message,
|
||||
string Exception,
|
||||
string BackupID,
|
||||
string Action,
|
||||
DateTime Timestamp,
|
||||
string LogEntryID,
|
||||
string MessageID,
|
||||
string MessageLogTag
|
||||
) : INotification;
|
||||
|
||||
private readonly JsonSerializerOptions serializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = null,
|
||||
Converters = {
|
||||
new JsonStringEnumConverter(),
|
||||
new DayOfWeekStringEnumConverter()
|
||||
}
|
||||
};
|
||||
|
||||
private const string STATUS_WINDOW = "index.html";
|
||||
private const string SIGNIN_WINDOW = "signin.html";
|
||||
|
||||
private record BackgroundRequest(string Method, string Endpoint, string Body, TimeSpan? Timeout = null);
|
||||
|
||||
private readonly string m_apiUri;
|
||||
private readonly string m_baseUri;
|
||||
private string m_password;
|
||||
private readonly bool m_saltedpassword;
|
||||
private string m_authtoken;
|
||||
private string m_xsrftoken;
|
||||
private static readonly System.Text.Encoding ENCODING = System.Text.Encoding.GetEncoding("utf-8");
|
||||
private string m_accesstoken;
|
||||
|
||||
public delegate void StatusUpdateDelegate(IServerStatus status);
|
||||
public event StatusUpdateDelegate OnStatusUpdated;
|
||||
@@ -73,27 +115,26 @@ namespace Duplicati.GUI.TrayIcon
|
||||
public delegate void NewNotificationDelegate(INotification notification);
|
||||
public event NewNotificationDelegate OnNotification;
|
||||
|
||||
private long m_lastEventId = 0;
|
||||
private long m_lastDataUpdateId = -1;
|
||||
private bool m_disableTrayIconLogin;
|
||||
|
||||
private volatile IServerStatus m_status;
|
||||
|
||||
private volatile bool m_shutdown = false;
|
||||
private volatile System.Threading.Thread m_requestThread;
|
||||
private volatile System.Threading.Thread m_pollThread;
|
||||
private readonly System.Threading.AutoResetEvent m_waitLock;
|
||||
private volatile Thread m_requestThread;
|
||||
private volatile Thread m_pollThread;
|
||||
private readonly AutoResetEvent m_waitLock;
|
||||
|
||||
private readonly Dictionary<string, string> m_updateRequest;
|
||||
private readonly Dictionary<string, string> m_options;
|
||||
private readonly Program.PasswordSource m_passwordSource;
|
||||
private string m_TrayIconHeaderValue => (m_passwordSource == Program.PasswordSource.Database) ? "database" : "user";
|
||||
|
||||
public IServerStatus Status { get { return m_status; } }
|
||||
|
||||
private readonly object m_lock = new object();
|
||||
private readonly Queue<BackgroundRequest> m_workQueue = new Queue<BackgroundRequest>();
|
||||
|
||||
public HttpServerConnection(Uri server, string password, bool saltedpassword, Program.PasswordSource passwordSource, bool disableTrayIconLogin, Dictionary<string, string> options)
|
||||
public HttpServerConnection(Uri server, string password, Program.PasswordSource passwordSource, bool disableTrayIconLogin, Dictionary<string, string> options)
|
||||
{
|
||||
m_baseUri = Util.AppendDirSeparator(server.ToString(), "/");
|
||||
|
||||
@@ -104,24 +145,16 @@ namespace Duplicati.GUI.TrayIcon
|
||||
m_firstNotificationTime = DateTime.Now;
|
||||
|
||||
m_password = password;
|
||||
m_saltedpassword = saltedpassword;
|
||||
m_options = options;
|
||||
m_passwordSource = passwordSource;
|
||||
|
||||
m_updateRequest = new Dictionary<string, string>();
|
||||
m_updateRequest["longpoll"] = "false";
|
||||
m_updateRequest["lasteventid"] = "0";
|
||||
// TODO: Not nice to do in constructor
|
||||
// Get a connection
|
||||
UpdateStatus(false);
|
||||
|
||||
UpdateStatus();
|
||||
|
||||
//We do the first request without long poll,
|
||||
// and all the rest with longpoll
|
||||
m_updateRequest["longpoll"] = "true";
|
||||
m_updateRequest["duration"] = "5m";
|
||||
|
||||
m_waitLock = new System.Threading.AutoResetEvent(false);
|
||||
m_requestThread = new System.Threading.Thread(ThreadRunner);
|
||||
m_pollThread = new System.Threading.Thread(LongPollRunner);
|
||||
m_waitLock = new AutoResetEvent(false);
|
||||
m_requestThread = new Thread(ThreadRunner);
|
||||
m_pollThread = new Thread(LongPollRunner);
|
||||
|
||||
m_requestThread.Name = "TrayIcon Request Thread";
|
||||
m_pollThread.Name = "TrayIcon Longpoll Thread";
|
||||
@@ -130,10 +163,12 @@ namespace Duplicati.GUI.TrayIcon
|
||||
m_pollThread.Start();
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
private void UpdateStatus(bool longpoll)
|
||||
{
|
||||
m_status = PerformRequest<IServerStatus>("GET", "/serverstate", m_updateRequest);
|
||||
m_updateRequest["lasteventid"] = m_status.LastEventID.ToString();
|
||||
var query = longpoll ? $"?longpoll=true&lastEventId={m_lastEventId}&duration={LONGPOLL_TIMEOUT}" : "";
|
||||
|
||||
m_status = PerformRequest<ServerStatusImpl>("GET", $"/serverstate{query}", null, longpoll ? Library.Utility.Timeparser.ParseTimeSpan(LONGPOLL_TIMEOUT) : null);
|
||||
m_lastEventId = m_status.LastEventID;
|
||||
|
||||
if (OnStatusUpdated != null)
|
||||
OnStatusUpdated(m_status);
|
||||
@@ -153,11 +188,10 @@ namespace Duplicati.GUI.TrayIcon
|
||||
|
||||
private void UpdateNotifications()
|
||||
{
|
||||
var req = new Dictionary<string, string>();
|
||||
var notifications = PerformRequest<INotification[]>("GET", "/notifications", req);
|
||||
var notifications = PerformRequest<NotificationImpl[]>("GET", "/notifications", null, null);
|
||||
if (notifications != null)
|
||||
{
|
||||
foreach(var n in notifications.Where(x => x.Timestamp > m_firstNotificationTime))
|
||||
foreach (var n in notifications.Where(x => x.Timestamp > m_firstNotificationTime))
|
||||
if (OnNotification != null)
|
||||
OnNotification(n);
|
||||
|
||||
@@ -168,24 +202,33 @@ namespace Duplicati.GUI.TrayIcon
|
||||
|
||||
private void UpdateApplicationSettings()
|
||||
{
|
||||
var req = new Dictionary<string, string>();
|
||||
var settings = PerformRequest<Dictionary<string, string>>("GET", "/serversettings", req);
|
||||
var settings = PerformRequest<Dictionary<string, string>>("GET", "/serversettings", null, null);
|
||||
if (settings != null && settings.TryGetValue("disable-tray-icon-login", out var str))
|
||||
m_disableTrayIconLogin = Library.Utility.Utility.ParseBool(str, false);
|
||||
}
|
||||
|
||||
private void LongPollRunner()
|
||||
{
|
||||
var started = DateTime.Now;
|
||||
var errorCount = 0;
|
||||
|
||||
while (!m_shutdown)
|
||||
{
|
||||
var waitTime = TimeSpan.FromSeconds(Math.Min(10, errorCount * 2)) - (DateTime.Now - started);
|
||||
if (waitTime.TotalSeconds > 0)
|
||||
Thread.Sleep(waitTime);
|
||||
|
||||
try
|
||||
{
|
||||
UpdateStatus();
|
||||
started = DateTime.Now;
|
||||
UpdateStatus(true);
|
||||
errorCount = 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorCount++;
|
||||
System.Diagnostics.Trace.WriteLine("Request error: " + ex.Message);
|
||||
Library.Logging.Log.WriteWarningMessage(LOGTAG, "TrayIconRequestError", ex, "Failed to get response");
|
||||
Library.Logging.Log.WriteWarningMessage(LOGTAG, "TrayIconRequestError", ex, "Failed to get response");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,18 +255,18 @@ namespace Duplicati.GUI.TrayIcon
|
||||
if (req != null)
|
||||
{
|
||||
any = true;
|
||||
PerformRequest<string>(req.Method, req.Endpoint, req.Query);
|
||||
PerformRequest<string>(req.Method, req.Endpoint, req.Body, req.Timeout);
|
||||
}
|
||||
|
||||
|
||||
} while (req != null);
|
||||
|
||||
|
||||
if (!(any || m_shutdown))
|
||||
m_waitLock.WaitOne(TimeSpan.FromMinutes(1), true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Trace.WriteLine("Request error: " + ex.Message);
|
||||
Library.Logging.Log.WriteWarningMessage(LOGTAG, "TrayIconRequestError", ex, "Failed to get response");
|
||||
Library.Logging.Log.WriteWarningMessage(LOGTAG, "TrayIconRequestError", ex, "Failed to get response");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,323 +284,131 @@ namespace Duplicati.GUI.TrayIcon
|
||||
}
|
||||
}
|
||||
|
||||
private static string EncodeQueryString(Dictionary<string, string> dict)
|
||||
private sealed record SigninResponse(string AccessToken);
|
||||
|
||||
private T PerformRequest<T>(string method, string urlfragment, string body, TimeSpan? timeout)
|
||||
{
|
||||
return string.Join("&", Array.ConvertAll(dict.Keys.ToArray(), key => string.Format("{0}={1}", Uri.EscapeUriString(key), Uri.EscapeUriString(dict[key]))));
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(m_accesstoken))
|
||||
ObtainAccessToken();
|
||||
|
||||
private class SaltAndNonce
|
||||
{
|
||||
// The JsonProperty attribute allows the Serializer.Deserialize method to
|
||||
// set these readonly fields.
|
||||
[JsonProperty]
|
||||
public readonly string Salt = null;
|
||||
|
||||
[JsonProperty]
|
||||
public readonly string Nonce = null;
|
||||
}
|
||||
|
||||
private SaltAndNonce GetSaltAndNonce()
|
||||
{
|
||||
var httpOptions = new Duplicati.Library.Modules.Builtin.HttpOptions();
|
||||
httpOptions.Configure(m_options);
|
||||
|
||||
using (httpOptions)
|
||||
{
|
||||
var req = (System.Net.HttpWebRequest) System.Net.WebRequest.Create(m_baseUri + LOGIN_SCRIPT);
|
||||
req.Method = "POST";
|
||||
req.UserAgent = "Duplicati TrayIcon Monitor, v" +
|
||||
System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
req.Headers.Add(TRAYICONPASSWORDSOURCE_HEADER, m_TrayIconHeaderValue);
|
||||
req.ContentType = "application/x-www-form-urlencoded";
|
||||
|
||||
Duplicati.Library.Utility.AsyncHttpRequest areq = new Library.Utility.AsyncHttpRequest(req);
|
||||
var body = System.Text.Encoding.ASCII.GetBytes("get-nonce=1");
|
||||
using (var f = areq.GetRequestStream(body.Length))
|
||||
f.Write(body, 0, body.Length);
|
||||
|
||||
using (var r = (System.Net.HttpWebResponse) areq.GetResponse())
|
||||
using (var s = areq.GetResponseStream())
|
||||
using (var sr = new System.IO.StreamReader(s, ENCODING, true))
|
||||
return Serializer.Deserialize<SaltAndNonce>(sr);
|
||||
}
|
||||
}
|
||||
|
||||
private string PerformLogin(string password, string nonce)
|
||||
{
|
||||
var httpOptions = new Duplicati.Library.Modules.Builtin.HttpOptions();
|
||||
httpOptions.Configure(m_options);
|
||||
|
||||
using (httpOptions)
|
||||
{
|
||||
System.Net.HttpWebRequest req =
|
||||
(System.Net.HttpWebRequest) System.Net.WebRequest.Create(m_baseUri + LOGIN_SCRIPT);
|
||||
req.Method = "POST";
|
||||
req.UserAgent = "Duplicati TrayIcon Monitor, v" +
|
||||
System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
req.Headers.Add(TRAYICONPASSWORDSOURCE_HEADER, m_TrayIconHeaderValue);
|
||||
req.ContentType = "application/x-www-form-urlencoded";
|
||||
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);
|
||||
var body = System.Text.Encoding.ASCII.GetBytes("password=" +
|
||||
Duplicati.Library.Utility.Uri.UrlEncode(password));
|
||||
using (var f = areq.GetRequestStream(body.Length))
|
||||
f.Write(body, 0, body.Length);
|
||||
|
||||
using (var r = (System.Net.HttpWebResponse) areq.GetResponse())
|
||||
if (r.StatusCode == System.Net.HttpStatusCode.OK)
|
||||
return (r.Cookies[AUTH_COOKIE] ?? r.Cookies[Library.Utility.Uri.UrlEncode(AUTH_COOKIE)]).Value;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetAuthToken()
|
||||
{
|
||||
var salt_nonce = GetSaltAndNonce();
|
||||
var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
var password = m_password;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(m_password))
|
||||
return "";
|
||||
|
||||
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 string GetXSRFToken()
|
||||
{
|
||||
var httpOptions = new Duplicati.Library.Modules.Builtin.HttpOptions();
|
||||
httpOptions.Configure(m_options);
|
||||
|
||||
using (httpOptions)
|
||||
{
|
||||
System.Net.HttpWebRequest req =
|
||||
(System.Net.HttpWebRequest) System.Net.WebRequest.Create(m_baseUri + STATUS_WINDOW);
|
||||
req.Method = "GET";
|
||||
req.UserAgent = "Duplicati TrayIcon Monitor, v" +
|
||||
System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
req.Headers.Add(TRAYICONPASSWORDSOURCE_HEADER, m_TrayIconHeaderValue);
|
||||
if (req.CookieContainer == null)
|
||||
req.CookieContainer = new System.Net.CookieContainer();
|
||||
|
||||
//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[XSRF_COOKIE] ?? r.Cookies[Library.Utility.Uri.UrlEncode(XSRF_COOKIE)]).Value;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private T PerformRequest<T>(string method, string urlfragment, Dictionary<string, string> queryparams)
|
||||
{
|
||||
var hasTriedXSRF = false;
|
||||
var hasTriedPassword = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
return PerformRequestInternal<T>(method, urlfragment, queryparams);
|
||||
return PerformRequestInternal<T>(method, urlfragment, body, timeout);
|
||||
}
|
||||
catch (System.Net.WebException wex)
|
||||
catch (AggregateException aex)
|
||||
{
|
||||
var httpex = wex.Response as HttpWebResponse;
|
||||
if (httpex == null)
|
||||
if (hasTriedPassword || !aex.InnerExceptions.Any(x => x is HttpRequestException hex && hex.StatusCode == HttpStatusCode.Unauthorized))
|
||||
throw;
|
||||
|
||||
if (
|
||||
!hasTriedXSRF &&
|
||||
wex.Status == System.Net.WebExceptionStatus.ProtocolError &&
|
||||
httpex.StatusCode == System.Net.HttpStatusCode.BadRequest &&
|
||||
httpex.StatusDescription.IndexOf("XSRF", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
hasTriedXSRF = true;
|
||||
var t = httpex.Cookies[XSRF_COOKIE]?.Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(t))
|
||||
t = GetXSRFToken();
|
||||
|
||||
m_xsrftoken = Duplicati.Library.Utility.Uri.UrlDecode(t);
|
||||
}
|
||||
else if (
|
||||
!hasTriedPassword &&
|
||||
wex.Status == System.Net.WebExceptionStatus.ProtocolError &&
|
||||
httpex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
//Can survive if server password is changed via web ui
|
||||
switch (m_passwordSource)
|
||||
{
|
||||
case Program.PasswordSource.Database:
|
||||
if (Program.databaseConnection != null)
|
||||
Program.databaseConnection.ApplicationSettings.ReloadSettings();
|
||||
|
||||
if (Program.databaseConnection != null && Program.databaseConnection.ApplicationSettings.WebserverPasswordTrayIcon != m_password)
|
||||
m_password = Program.databaseConnection.ApplicationSettings.WebserverPasswordTrayIcon;
|
||||
else
|
||||
hasTriedPassword = true;
|
||||
break;
|
||||
case Program.PasswordSource.HostedServer:
|
||||
if (Server.Program.DataConnection != null && Server.Program.DataConnection.ApplicationSettings.WebserverPassword != m_password)
|
||||
m_password = Server.Program.DataConnection.ApplicationSettings.WebserverPassword;
|
||||
else
|
||||
hasTriedPassword = true;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
m_authtoken = GetAuthToken();
|
||||
}
|
||||
else
|
||||
throw;
|
||||
// Only try once, and clear the token for the next try
|
||||
hasTriedPassword = true;
|
||||
m_accesstoken = null;
|
||||
ObtainAccessToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private T PerformRequestInternal<T>(string method, string endpoint, Dictionary<string, string> queryparams)
|
||||
private void ObtainAccessToken()
|
||||
{
|
||||
queryparams["format"] = "json";
|
||||
|
||||
string query = EncodeQueryString(queryparams);
|
||||
|
||||
// TODO: This can interfere with running backups,
|
||||
// as the System.Net.ServicePointManager is shared with
|
||||
// all connections doing ftp/http requests
|
||||
using (var httpOptions = new Duplicati.Library.Modules.Builtin.HttpOptions())
|
||||
{
|
||||
httpOptions.Configure(m_options);
|
||||
|
||||
var req =
|
||||
(System.Net.HttpWebRequest) System.Net.WebRequest.Create(
|
||||
new Uri(m_apiUri + endpoint + '?' + query));
|
||||
req.Method = method;
|
||||
req.Headers.Add("Accept-Charset", ENCODING.BodyName);
|
||||
if (m_xsrftoken != null)
|
||||
req.Headers.Add(XSRF_HEADER, m_xsrftoken);
|
||||
req.UserAgent = "Duplicati TrayIcon Monitor, v" +
|
||||
System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
req.Headers.Add(TRAYICONPASSWORDSOURCE_HEADER, m_TrayIconHeaderValue);
|
||||
if (req.CookieContainer == null)
|
||||
req.CookieContainer = new System.Net.CookieContainer();
|
||||
|
||||
if (m_authtoken != null)
|
||||
req.CookieContainer.Add(new System.Net.Cookie(AUTH_COOKIE, m_authtoken, "/", req.RequestUri.Host));
|
||||
if (m_xsrftoken != null)
|
||||
req.CookieContainer.Add(new System.Net.Cookie(XSRF_COOKIE, m_xsrftoken, "/", req.RequestUri.Host));
|
||||
|
||||
//Wrap it all in async stuff
|
||||
var areq = new Library.Utility.AsyncHttpRequest(req);
|
||||
req.AllowWriteStreamBuffering = true;
|
||||
|
||||
//Assign the timeout, and add a little processing time as well
|
||||
if (endpoint.Equals("/serverstate", StringComparison.OrdinalIgnoreCase) &&
|
||||
queryparams.ContainsKey("duration"))
|
||||
areq.Timeout = (int) (Duplicati.Library.Utility.Timeparser.ParseTimeSpan(queryparams["duration"]) +
|
||||
TimeSpan.FromSeconds(5)).TotalMilliseconds;
|
||||
|
||||
using (var r = (System.Net.HttpWebResponse) areq.GetResponse())
|
||||
using (var s = areq.GetResponseStream())
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
|
||||
{
|
||||
s.CopyTo(ms);
|
||||
return (T) (object) ENCODING.GetString(ms.ToArray());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(s, ENCODING, true))
|
||||
return Serializer.Deserialize<T>(sr);
|
||||
}
|
||||
}
|
||||
var token = ObtainSignInToken();
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
return;
|
||||
m_accesstoken = PerformRequestInternal<SigninResponse>("POST", "/auth/signin", JsonSerializer.Serialize(new { SigninToken = token }), null).AccessToken;
|
||||
}
|
||||
|
||||
private void ExecuteAndNotify(string method, string urifragment, Dictionary<string, string> req)
|
||||
private async Task<T> PerformRequestInternalAsync<T>(string method, string endpoint, string body, TimeSpan? timeout)
|
||||
{
|
||||
var request = new HttpRequestMessage(new HttpMethod(method), new Uri(m_apiUri + endpoint));
|
||||
if (!string.IsNullOrWhiteSpace(m_accesstoken))
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_accesstoken);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(body))
|
||||
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
|
||||
// Set up response timeout, use 100s which is the .NET default
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(timeout.HasValue
|
||||
? timeout.Value + TimeSpan.FromSeconds(5)
|
||||
: TimeSpan.FromSeconds(100));
|
||||
|
||||
var response = await HTTPCLIENT.SendAsync(request, cts.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
if (typeof(T) == typeof(string))
|
||||
return (T)(object)await response.Content.ReadAsStringAsync();
|
||||
|
||||
using (var stream = await response.Content.ReadAsStreamAsync())
|
||||
return await JsonSerializer.DeserializeAsync<T>(stream, serializerOptions);
|
||||
}
|
||||
|
||||
private T PerformRequestInternal<T>(string method, string endpoint, string body, TimeSpan? timeout)
|
||||
=> PerformRequestInternalAsync<T>(method, endpoint, body, timeout).Result;
|
||||
|
||||
private void ExecuteAndNotify(string method, string urifragment, string body)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
m_workQueue.Enqueue(new BackgroundRequest(method, urifragment, req));
|
||||
m_workQueue.Enqueue(new BackgroundRequest(method, urifragment, body, null));
|
||||
m_waitLock.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Pause(string duration = null)
|
||||
{
|
||||
var req = new Dictionary<string, string>();
|
||||
if (!string.IsNullOrWhiteSpace(duration))
|
||||
req.Add("duration", duration);
|
||||
|
||||
ExecuteAndNotify("POST", "/serverstate/pause", req);
|
||||
ExecuteAndNotify("POST", $"/serverstate/pause{(string.IsNullOrWhiteSpace(duration) ? "" : $"?duration={duration}")}", null);
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
var req = new Dictionary<string, string>();
|
||||
ExecuteAndNotify("POST", "/serverstate/resume", req);
|
||||
ExecuteAndNotify("POST", "/serverstate/resume", null);
|
||||
}
|
||||
|
||||
public void StopTask(long id)
|
||||
{
|
||||
var req = new Dictionary<string, string>();
|
||||
ExecuteAndNotify("POST", string.Format("/task/{0}/stop", Library.Utility.Uri.UrlPathEncode(id.ToString())), req);
|
||||
}
|
||||
|
||||
public void AbortTask(long id)
|
||||
{
|
||||
var req = new Dictionary<string, string>();
|
||||
ExecuteAndNotify("POST", string.Format("/task/{0}/abort", Library.Utility.Uri.UrlPathEncode(id.ToString())), req);
|
||||
}
|
||||
|
||||
public void RunBackup(long id, bool forcefull = false)
|
||||
{
|
||||
var req = new Dictionary<string, string>();
|
||||
if (forcefull)
|
||||
req.Add("full", "true");
|
||||
ExecuteAndNotify("POST", string.Format("/backup/{0}/start", Library.Utility.Uri.UrlPathEncode(id.ToString())), req);
|
||||
}
|
||||
|
||||
public void DismissNotification(long id)
|
||||
{
|
||||
var req = new Dictionary<string, string>();
|
||||
ExecuteAndNotify("DELETE", string.Format("/notification/{0}", Library.Utility.Uri.UrlPathEncode(id.ToString())), req);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
|
||||
private sealed record SigninTokenResponse(string Token);
|
||||
private string IssueSigninToken(string password)
|
||||
=> PerformRequest<SigninTokenResponse>("POST", "/auth/issuesignintoken", JsonSerializer.Serialize(new { Password = password }), null).Token;
|
||||
|
||||
private string ObtainSignInToken()
|
||||
{
|
||||
string signinjwt = null;
|
||||
|
||||
// If we host the server, issue the token from the service
|
||||
if (FIXMEGlobal.IsServerStarted || m_passwordSource == Program.PasswordSource.HostedServer)
|
||||
signinjwt = FIXMEGlobal.Provider.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("trayicon");
|
||||
|
||||
// If we have database access, grab the issuer key from the db and issue a token
|
||||
if (string.IsNullOrWhiteSpace(signinjwt) && m_passwordSource == Program.PasswordSource.Database)
|
||||
{
|
||||
Program.databaseConnection.ApplicationSettings.ReloadSettings();
|
||||
var cfg = Program.databaseConnection.ApplicationSettings.JWTConfig;
|
||||
if (!string.IsNullOrWhiteSpace(cfg))
|
||||
signinjwt = new JWTTokenProvider(JsonSerializer.Deserialize<JWTConfig>(cfg)).CreateSigninToken("trayicon");
|
||||
}
|
||||
|
||||
// If we know the password, issue a token from the API
|
||||
if (string.IsNullOrWhiteSpace(signinjwt) && !string.IsNullOrWhiteSpace(m_password))
|
||||
signinjwt = IssueSigninToken(m_password);
|
||||
|
||||
return signinjwt;
|
||||
}
|
||||
|
||||
public string StatusWindowURL
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_authtoken != null)
|
||||
return m_baseUri + STATUS_WINDOW + (m_disableTrayIconLogin ? string.Empty : "?auth-token=" + GetAuthToken());
|
||||
|
||||
return m_baseUri + STATUS_WINDOW;
|
||||
get
|
||||
{
|
||||
var signinjwt = m_disableTrayIconLogin ? null : ObtainSignInToken();
|
||||
return string.IsNullOrWhiteSpace(signinjwt)
|
||||
? m_baseUri + STATUS_WINDOW
|
||||
: m_baseUri + SIGNIN_WINDOW + $"?token={signinjwt}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using Duplicati.Library.Common;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Server;
|
||||
|
||||
namespace Duplicati.GUI.TrayIcon
|
||||
{
|
||||
@@ -36,7 +36,7 @@ namespace Duplicati.GUI.TrayIcon
|
||||
}
|
||||
|
||||
public static HttpServerConnection Connection;
|
||||
|
||||
|
||||
private const string HOSTURL_OPTION = "hosturl";
|
||||
private const string NOHOSTEDSERVER_OPTION = "no-hosted-server";
|
||||
private const string READCONFIGFROMDB_OPTION = "read-config-from-db";
|
||||
@@ -45,7 +45,7 @@ namespace Duplicati.GUI.TrayIcon
|
||||
private const string BROWSER_COMMAND_OPTION = "browser-command";
|
||||
|
||||
private const string DEFAULT_HOSTURL = "http://localhost:8200";
|
||||
|
||||
|
||||
private static string _browser_command = null;
|
||||
private static bool disableTrayIconLogin = false;
|
||||
private static bool openui = false;
|
||||
@@ -62,10 +62,10 @@ namespace Duplicati.GUI.TrayIcon
|
||||
public static int Main(string[] _args)
|
||||
{
|
||||
List<string> args = new List<string>(_args);
|
||||
Dictionary<string, string> options = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args);
|
||||
Dictionary<string, string> options = Library.Utility.CommandLineParser.ExtractOptions(args);
|
||||
|
||||
if (OperatingSystem.IsWindows() && !Duplicati.Library.Utility.Utility.ParseBoolOption(options, DETACHED_PROCESS))
|
||||
Duplicati.Library.Utility.Win32.AttachConsole(Duplicati.Library.Utility.Win32.ATTACH_PARENT_PROCESS);
|
||||
if (OperatingSystem.IsWindows() && !Library.Utility.Utility.ParseBoolOption(options, DETACHED_PROCESS))
|
||||
Library.Utility.Win32.AttachConsole(Library.Utility.Win32.ATTACH_PARENT_PROCESS);
|
||||
|
||||
foreach (string s in args)
|
||||
if (
|
||||
@@ -80,15 +80,13 @@ namespace Duplicati.GUI.TrayIcon
|
||||
Console.WriteLine("Supported commandline arguments:");
|
||||
Console.WriteLine();
|
||||
|
||||
foreach (Library.Interface.ICommandLineArgument arg in SupportedCommands)
|
||||
{
|
||||
foreach (ICommandLineArgument arg in SupportedCommands)
|
||||
Console.WriteLine("--{0}: {1}", arg.Name, arg.LongDescription);
|
||||
}
|
||||
|
||||
Console.WriteLine("Additionally, these server options are also supported:");
|
||||
Console.WriteLine();
|
||||
|
||||
foreach (Library.Interface.ICommandLineArgument arg in Duplicati.Server.Program.SupportedCommands)
|
||||
foreach (ICommandLineArgument arg in Server.Program.SupportedCommands)
|
||||
Console.WriteLine("--{0}: {1}", arg.Name, arg.LongDescription);
|
||||
|
||||
return 0;
|
||||
@@ -99,8 +97,6 @@ namespace Duplicati.GUI.TrayIcon
|
||||
HostedInstanceKeeper hosted = null;
|
||||
|
||||
string password = null;
|
||||
var saltedpassword = false;
|
||||
|
||||
if (!Library.Utility.Utility.ParseBoolOption(options, NOHOSTEDSERVER_OPTION))
|
||||
{
|
||||
try
|
||||
@@ -114,13 +110,11 @@ 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;
|
||||
openui = Server.Program.IsFirstRun || Server.Program.ServerPortChanged;
|
||||
// Tell the hosted server it was started by the TrayIcon
|
||||
Duplicati.Server.Program.Origin = "Tray icon";
|
||||
Server.Program.Origin = "Tray icon";
|
||||
|
||||
var cert = Duplicati.Server.Program.DataConnection.ApplicationSettings.ServerSSLCertificate;
|
||||
var cert = Server.Program.DataConnection.ApplicationSettings.ServerSSLCertificate;
|
||||
var scheme = "http";
|
||||
|
||||
if (cert != null && cert.HasPrivateKey)
|
||||
@@ -128,7 +122,7 @@ namespace Duplicati.GUI.TrayIcon
|
||||
|
||||
serverURL = (new UriBuilder(serverURL)
|
||||
{
|
||||
Port = Duplicati.Server.Program.ServerPort,
|
||||
Port = Server.Program.ServerPort,
|
||||
Scheme = scheme
|
||||
}).Uri;
|
||||
}
|
||||
@@ -139,8 +133,6 @@ namespace Duplicati.GUI.TrayIcon
|
||||
if (databaseConnection != null)
|
||||
{
|
||||
disableTrayIconLogin = databaseConnection.ApplicationSettings.DisableTrayIconLogin;
|
||||
password = databaseConnection.ApplicationSettings.WebserverPasswordTrayIcon;
|
||||
saltedpassword = false;
|
||||
|
||||
var cert = databaseConnection.ApplicationSettings.ServerSSLCertificate;
|
||||
var scheme = "http";
|
||||
@@ -148,33 +140,29 @@ namespace Duplicati.GUI.TrayIcon
|
||||
if (cert != null && cert.HasPrivateKey)
|
||||
scheme = "https";
|
||||
|
||||
serverURL = (new UriBuilder(serverURL)
|
||||
serverURL = new UriBuilder(serverURL)
|
||||
{
|
||||
Port = databaseConnection.ApplicationSettings.LastWebserverPort == -1 ? serverURL.Port : databaseConnection.ApplicationSettings.LastWebserverPort,
|
||||
Scheme = scheme
|
||||
}).Uri;
|
||||
}.Uri;
|
||||
}
|
||||
}
|
||||
|
||||
string pwd;
|
||||
|
||||
if (options.TryGetValue("webserver-password", out pwd))
|
||||
{
|
||||
// Legacy undocumented way to provide the password
|
||||
if (options.TryGetValue("webserver-password", out var pwd))
|
||||
password = pwd;
|
||||
if (options.TryGetValue(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, out pwd))
|
||||
password = pwd;
|
||||
saltedpassword = false;
|
||||
}
|
||||
|
||||
string url;
|
||||
|
||||
if (options.TryGetValue(HOSTURL_OPTION, out url))
|
||||
if (options.TryGetValue(HOSTURL_OPTION, out var url))
|
||||
serverURL = new Uri(url);
|
||||
|
||||
StartTray(_args, options, hosted, password, saltedpassword);
|
||||
StartTray(_args, options, hosted, password);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void StartTray(string[] _args, Dictionary<string, string> options, HostedInstanceKeeper hosted, string password, bool saltedpassword)
|
||||
private static void StartTray(string[] _args, Dictionary<string, string> options, HostedInstanceKeeper hosted, string password)
|
||||
{
|
||||
using (hosted)
|
||||
{
|
||||
@@ -185,7 +173,7 @@ namespace Duplicati.GUI.TrayIcon
|
||||
try
|
||||
{
|
||||
ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault;
|
||||
using (Connection = new HttpServerConnection(serverURL, password, saltedpassword, databaseConnection != null ? PasswordSource.Database : PasswordSource.HostedServer, disableTrayIconLogin, options))
|
||||
using (Connection = new HttpServerConnection(serverURL, password, databaseConnection != null ? PasswordSource.Database : PasswordSource.HostedServer, disableTrayIconLogin, options))
|
||||
{
|
||||
using (var tk = RunTrayIcon())
|
||||
{
|
||||
@@ -201,8 +189,8 @@ namespace Duplicati.GUI.TrayIcon
|
||||
{
|
||||
tk.ShowUrlInWindow(Connection.StatusWindowURL);
|
||||
|
||||
Duplicati.Server.Program.IsFirstRun = false;
|
||||
Duplicati.Server.Program.ServerPortChanged = false;
|
||||
Server.Program.IsFirstRun = false;
|
||||
Server.Program.ServerPortChanged = false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -253,22 +241,20 @@ namespace Duplicati.GUI.TrayIcon
|
||||
private static TrayIconBase RunTrayIcon()
|
||||
=> new AvaloniaRunner();
|
||||
|
||||
public static Duplicati.Library.Interface.ICommandLineArgument[] SupportedCommands
|
||||
public static ICommandLineArgument[] SupportedCommands
|
||||
{
|
||||
get
|
||||
{
|
||||
var args = new List<Duplicati.Library.Interface.ICommandLineArgument>()
|
||||
var args = new List<ICommandLineArgument>()
|
||||
{
|
||||
new Duplicati.Library.Interface.CommandLineArgument(HOSTURL_OPTION, CommandLineArgument.ArgumentType.String, "Selects the url to connect to", "Supply the url that the TrayIcon will connect to and show status for", DEFAULT_HOSTURL),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(NOHOSTEDSERVER_OPTION, CommandLineArgument.ArgumentType.String, "Disables local server", "Set this option to not spawn a local service, use if the TrayIcon should connect to a running service"),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(READCONFIGFROMDB_OPTION, CommandLineArgument.ArgumentType.String, "Read server connection info from DB", $"Set this option to read server connection info for running service from its database (only together with {NOHOSTEDSERVER_OPTION})"),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(BROWSER_COMMAND_OPTION, CommandLineArgument.ArgumentType.String, "Sets the browser command", "Set this option to override the default browser detection"),
|
||||
new CommandLineArgument(HOSTURL_OPTION, CommandLineArgument.ArgumentType.String, "Selects the url to connect to", "Supply the url that the TrayIcon will connect to and show status for", DEFAULT_HOSTURL),
|
||||
new CommandLineArgument(NOHOSTEDSERVER_OPTION, CommandLineArgument.ArgumentType.String, "Disables local server", "Set this option to not spawn a local service, use if the TrayIcon should connect to a running service"),
|
||||
new CommandLineArgument(READCONFIGFROMDB_OPTION, CommandLineArgument.ArgumentType.String, "Read server connection info from DB", $"Set this option to read server connection info for running service from its database (only together with {NOHOSTEDSERVER_OPTION})"),
|
||||
new CommandLineArgument(BROWSER_COMMAND_OPTION, CommandLineArgument.ArgumentType.String, "Sets the browser command", "Set this option to override the default browser detection"),
|
||||
};
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
args.Add(new Duplicati.Library.Interface.CommandLineArgument(DETACHED_PROCESS, CommandLineArgument.ArgumentType.String, "Runs the tray-icon detached", "This option runs the tray-icon in detached mode, meaning that the process will exit immediately and not send output to the console of the caller"));
|
||||
}
|
||||
args.Add(new CommandLineArgument(DETACHED_PROCESS, CommandLineArgument.ArgumentType.String, "Runs the tray-icon detached", "This option runs the tray-icon in detached mode, meaning that the process will exit immediately and not send output to the console of the caller"));
|
||||
|
||||
return args.ToArray();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Duplicati.Library.IO;
|
||||
|
||||
public interface ILiveControls
|
||||
{
|
||||
bool IsPaused { get; }
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
namespace Duplicati.Library.Common
|
||||
{
|
||||
public static class Platform
|
||||
{
|
||||
/// <value>
|
||||
/// Gets or sets a value indicating if the client is Linux/Unix based
|
||||
/// </value>
|
||||
[Obsolete("Use OperatingSystem.IsXXX instead")]
|
||||
public static readonly bool IsClientPosix;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating if the client is Windows based
|
||||
/// </summary>
|
||||
[Obsolete("Use OperatingSystem.IsXXX instead")]
|
||||
public static readonly bool IsClientWindows;
|
||||
|
||||
|
||||
/// <value>
|
||||
/// Gets or sets a value indicating if the client is running OSX
|
||||
/// </value>
|
||||
[Obsolete("Use OperatingSystem.IsXXX instead")]
|
||||
public static readonly bool IsClientOSX;
|
||||
|
||||
static Platform()
|
||||
{
|
||||
#pragma warning disable CS0618 // Obsolete
|
||||
IsClientPosix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX;
|
||||
IsClientWindows = !IsClientPosix;
|
||||
IsClientOSX = IsClientPosix && "Darwin".Equals(_RetrieveUname(false));
|
||||
#pragma warning restore CS0618 // Obsolete
|
||||
}
|
||||
|
||||
/// <value>
|
||||
/// Gets the output of "uname -a" on Linux, or null on Windows
|
||||
/// </value>
|
||||
public static string UnameAll
|
||||
{
|
||||
get
|
||||
{
|
||||
#pragma warning disable CS0618 // Obsolete
|
||||
if (!IsClientPosix)
|
||||
return null;
|
||||
#pragma warning restore CS0618 // Obsolete
|
||||
|
||||
return _RetrieveUname(true);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static string _RetrieveUname(bool showAll)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new System.Diagnostics.ProcessStartInfo("uname", showAll ? "-a" : null)
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = false,
|
||||
RedirectStandardInput = false,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
var pi = System.Diagnostics.Process.Start(psi);
|
||||
pi.WaitForExit(5000);
|
||||
if (pi.HasExited)
|
||||
return pi.StandardOutput.ReadToEnd().Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ namespace Duplicati.Library.Main.Database
|
||||
using (var cmd = m_connection.CreateCommand())
|
||||
using (var rd = cmd.ExecuteReader(@"SELECT ""ID"", ""Timestamp"" FROM ""Operation"" ORDER BY ""Timestamp"" DESC LIMIT 1"))
|
||||
{
|
||||
if(!rd.Read())
|
||||
if (!rd.Read())
|
||||
{
|
||||
throw new Exception("LocalDatabase does not contain a previous operation.");
|
||||
}
|
||||
@@ -263,13 +263,13 @@ namespace Duplicati.Library.Main.Database
|
||||
RemoveRemoteVolume(name, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<KeyValuePair<long, DateTime>> FilesetTimes
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var cmd = m_connection.CreateCommand())
|
||||
using(var rd = cmd.ExecuteReader(@"SELECT ""ID"", ""Timestamp"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC"))
|
||||
using (var rd = cmd.ExecuteReader(@"SELECT ""ID"", ""Timestamp"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC"))
|
||||
while (rd.Read())
|
||||
yield return new KeyValuePair<long, DateTime>(rd.GetInt64(0), ParseFromEpochSeconds(rd.GetInt64(1)).ToLocalTime());
|
||||
}
|
||||
@@ -865,9 +865,9 @@ ON
|
||||
anyError.Add(string.Format("Unexpected difference in fileset {0}, found {1} entries, but expected {2}", filesetname, expandedlist, storedlist));
|
||||
}
|
||||
}
|
||||
if (anyError.Any())
|
||||
if (anyError.Any())
|
||||
{
|
||||
throw new Interface.UserInformationException(string.Join("\n\r", anyError), "FilesetDifferences");
|
||||
throw new Interface.UserInformationException(string.Join("\n\r", anyError), "FilesetDifferences");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1210,6 +1210,16 @@ ORDER BY
|
||||
if (type != Library.Utility.FilterType.Regexp && !Library.Utility.Utility.IsFSCaseSensitive && filter.ToString().Any(x => x > 127))
|
||||
type = Library.Utility.FilterType.Regexp;
|
||||
|
||||
if (filter.Empty)
|
||||
{
|
||||
using (var cmd = m_connection.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = transaction;
|
||||
cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" AS SELECT DISTINCT ""Path"" FROM ""File"" ", Tablename));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == Library.Utility.FilterType.Regexp || type == Library.Utility.FilterType.Group)
|
||||
{
|
||||
using (var cmd = m_connection.CreateCommand())
|
||||
@@ -1324,7 +1334,7 @@ ORDER BY
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void AddIndexBlockLink(long indexVolumeID, long blockVolumeID, System.Data.IDbTransaction transaction)
|
||||
{
|
||||
m_insertIndexBlockLink.Transaction = transaction;
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace Duplicati.Library.Main.Operation
|
||||
yield return string.Format("Current Version folder {0}", System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location));
|
||||
|
||||
yield return string.Format("OS: {0}", Environment.OSVersion);
|
||||
yield return string.Format("Uname: {0}", Platform.UnameAll);
|
||||
yield return string.Format("OSType: {0}", OperatingSystem.IsWindows() ? "Windows" : OperatingSystem.IsLinux() ? "Linux" : OperatingSystem.IsMacOS() ? "MacOS" : "Unknown");
|
||||
|
||||
yield return string.Format("64bit: {0} ({1})", Environment.Is64BitOperatingSystem, Environment.Is64BitProcess);
|
||||
yield return string.Format("Machinename: {0}", Environment.MachineName);
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Duplicati.Library.Utility
|
||||
/// Size of buffers for copying stream
|
||||
/// </summary>
|
||||
public static long DEFAULT_BUFFER_SIZE => SystemContextSettings.Buffersize;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A cache of the FileSystemCaseSensitive property, which is computed upon the first access.
|
||||
/// </summary>
|
||||
@@ -93,15 +93,15 @@ namespace Duplicati.Library.Utility
|
||||
}
|
||||
|
||||
buf = buf ?? new byte[DEFAULT_BUFFER_SIZE];
|
||||
|
||||
|
||||
int read;
|
||||
long total = 0;
|
||||
while ((read = source.Read(buf, 0, buf.Length)) != 0)
|
||||
{
|
||||
target.Write(buf, 0, read);
|
||||
total += read;
|
||||
long total = 0;
|
||||
while ((read = source.Read(buf, 0, buf.Length)) != 0)
|
||||
{
|
||||
target.Write(buf, 0, read);
|
||||
total += read;
|
||||
}
|
||||
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace Duplicati.Library.Utility
|
||||
{
|
||||
if (tryRewindSource && source.CanSeek)
|
||||
try { source.Position = 0; }
|
||||
catch {}
|
||||
catch { }
|
||||
|
||||
buf = buf ?? new byte[DEFAULT_BUFFER_SIZE];
|
||||
|
||||
@@ -438,7 +438,7 @@ namespace Duplicati.Library.Utility
|
||||
public static bool IsPathBelowFolder(string fileOrFolderPath, string parentFolder)
|
||||
{
|
||||
var sanitizedParentFolder = Util.AppendDirSeparator(parentFolder);
|
||||
return fileOrFolderPath.StartsWith(sanitizedParentFolder, ClientFilenameStringComparison) &&
|
||||
return fileOrFolderPath.StartsWith(sanitizedParentFolder, ClientFilenameStringComparison) &&
|
||||
!fileOrFolderPath.Equals(sanitizedParentFolder, ClientFilenameStringComparison);
|
||||
}
|
||||
|
||||
@@ -459,7 +459,7 @@ namespace Duplicati.Library.Utility
|
||||
var last = path.LastIndexOf(Path.DirectorySeparatorChar, len);
|
||||
if (last == -1 || last == 0 && len == 0)
|
||||
return null;
|
||||
|
||||
|
||||
if (last == 0 && !OperatingSystem.IsWindows())
|
||||
return Util.DirectorySeparatorString;
|
||||
|
||||
@@ -474,7 +474,7 @@ namespace Duplicati.Library.Utility
|
||||
return parent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Given a collection of unique folders, returns only parent-most folders
|
||||
@@ -520,7 +520,7 @@ namespace Duplicati.Library.Utility
|
||||
|
||||
return result.Distinct();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Given a collection of file paths, return those NOT contained within specified collection of folders
|
||||
/// </summary>
|
||||
@@ -690,7 +690,7 @@ namespace Duplicati.Library.Utility
|
||||
else if (sizeAbs >= 1024)
|
||||
return Strings.Utility.FormatStringKB(size / 1024);
|
||||
else
|
||||
return Strings.Utility.FormatStringB((long) size); // safe to cast because lower than 1024 and thus well within range of long
|
||||
return Strings.Utility.FormatStringB((long)size); // safe to cast because lower than 1024 and thus well within range of long
|
||||
}
|
||||
|
||||
public static System.Threading.ThreadPriority ParsePriority(string value)
|
||||
@@ -831,9 +831,15 @@ namespace Duplicati.Library.Utility
|
||||
data[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
/// <summary>
|
||||
/// Invokes the "which" command to determine if a given application is available in the path
|
||||
/// </summary>
|
||||
/// <param name="appname">The name of the application to look for</param>
|
||||
public static bool Which(string appname)
|
||||
{
|
||||
if (!Platform.IsClientPosix)
|
||||
if (!(OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()))
|
||||
return false;
|
||||
|
||||
try
|
||||
@@ -919,7 +925,7 @@ namespace Duplicati.Library.Utility
|
||||
|
||||
ENVIRONMENT_VARIABLE_MATCHER_WINDOWS.Replace(str, m => Regex.Escape(lookup(m.Groups["name"].Value)));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a DateTime instance by converting to UTC and flooring to seconds.
|
||||
/// </summary>
|
||||
@@ -931,12 +937,12 @@ namespace Duplicati.Library.Utility
|
||||
ticks -= ticks % TimeSpan.TicksPerSecond;
|
||||
return new DateTime(ticks, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a DateTime instance, return the number of elapsed seconds since the Unix epoch
|
||||
/// </summary>
|
||||
/// <returns>The number of elapsed seconds since the Unix epoch</returns>
|
||||
/// <param name="input">The input time</param>
|
||||
|
||||
/// <summary>
|
||||
/// Given a DateTime instance, return the number of elapsed seconds since the Unix epoch
|
||||
/// </summary>
|
||||
/// <returns>The number of elapsed seconds since the Unix epoch</returns>
|
||||
/// <param name="input">The input time</param>
|
||||
public static long NormalizeDateTimeToEpochSeconds(DateTime input)
|
||||
{
|
||||
// Note that we cannot return (new DateTimeOffset(input)).ToUnixTimeSeconds() here.
|
||||
@@ -944,9 +950,9 @@ namespace Duplicati.Library.Utility
|
||||
// equivalent. However, if DateTime.MinValue is provided (for example, when creating
|
||||
// a new backup), this can result in values that fall outside the DateTimeOffset.MinValue
|
||||
// and DateTimeOffset.MaxValue bounds.
|
||||
return (long) Math.Floor((NormalizeDateTime(input) - EPOCH).TotalSeconds);
|
||||
return (long)Math.Floor((NormalizeDateTime(input) - EPOCH).TotalSeconds);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The format string for a DateTime
|
||||
/// </summary>
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
|
||||
// TODO: Delete this class.
|
||||
// It is essentially a queue that is processed by a worker thread, and can be implemented using a BlockingCollection or similar.
|
||||
|
||||
namespace Duplicati.Library.Utility
|
||||
{
|
||||
/// <summary>
|
||||
@@ -134,7 +138,7 @@ namespace Duplicati.Library.Utility
|
||||
{
|
||||
get
|
||||
{
|
||||
lock(m_lock)
|
||||
lock (m_lock)
|
||||
return new List<Tx>(m_tasks);
|
||||
}
|
||||
|
||||
@@ -174,7 +178,8 @@ namespace Duplicati.Library.Utility
|
||||
/// <param name="skipQueue">If set to <c>true</c> skip queue.</param>
|
||||
public void AddTask(Tx task, bool skipQueue)
|
||||
{
|
||||
if (!skipQueue) {
|
||||
if (!skipQueue)
|
||||
{
|
||||
// Fall back to default AddTask method
|
||||
AddTask(task);
|
||||
return;
|
||||
@@ -280,7 +285,7 @@ namespace Duplicati.Library.Utility
|
||||
{
|
||||
m_currentTask = null;
|
||||
|
||||
lock(m_lock)
|
||||
lock (m_lock)
|
||||
if (m_state == WorkerThread<Tx>.RunState.Run && m_tasks.Count > 0)
|
||||
m_currentTask = m_tasks.Dequeue();
|
||||
|
||||
@@ -311,7 +316,7 @@ namespace Duplicati.Library.Utility
|
||||
return;
|
||||
|
||||
if (m_currentTask == null && m_state == WorkerThread<Tx>.RunState.Run)
|
||||
lock(m_lock)
|
||||
lock (m_lock)
|
||||
if (m_tasks.Count > 0)
|
||||
m_currentTask = m_tasks.Dequeue();
|
||||
|
||||
@@ -348,7 +353,7 @@ namespace Duplicati.Library.Utility
|
||||
|
||||
if (CompletedWork != null)
|
||||
try { CompletedWork(this, task); }
|
||||
catch (Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { OnError(this, task, ex); }
|
||||
catch { }
|
||||
@@ -391,4 +396,4 @@ namespace Duplicati.Library.Utility
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,12 +72,12 @@
|
||||
<Content Include="..\..\thirdparty\uplink.NET\license.txt">
|
||||
<Link>licenses\uplink.NET\license.txt</Link>
|
||||
</Content>
|
||||
<Content Include="..\license.txt">
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<Link>licenses\license.txt</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="duplicati-url.txt">
|
||||
<Link>licenses\duplicat-url.txt</Link>
|
||||
<Link>licenses\duplicati-url.txt</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\thirdparty\SSH.NET\Homepage.txt">
|
||||
@@ -116,18 +116,6 @@
|
||||
<Link>licenses\AWSSDK\license.txt</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\thirdparty\HttpServer\licensedata.json">
|
||||
<Link>licenses\HttpServer\licensedata.json</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\thirdparty\HttpServer\Homepage.txt">
|
||||
<Link>licenses\HttpServer\Homepage.txt</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\thirdparty\HttpServer\license.txt">
|
||||
<Link>licenses\HttpServer\license.txt</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\thirdparty\Json.NET\licensedata.json">
|
||||
<Link>licenses\Json.NET\licensedata.json</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Utility;
|
||||
|
||||
using Duplicati.Library.Common.IO;
|
||||
|
||||
namespace Duplicati.License
|
||||
{
|
||||
/// <summary>
|
||||
@@ -52,14 +50,14 @@ namespace Duplicati.License
|
||||
/// <returns>A list of licenses</returns>
|
||||
public static List<LicenseEntry> ReadLicenses(string basefolder)
|
||||
{
|
||||
List<LicenseEntry> res = new List<LicenseEntry>();
|
||||
var res = new List<LicenseEntry>();
|
||||
|
||||
string[] folders = SystemIO.IO_OS.GetDirectories(basefolder);
|
||||
var folders = SystemIO.IO_OS.GetDirectories(basefolder);
|
||||
Array.Sort(folders);
|
||||
|
||||
foreach (string folder in folders)
|
||||
{
|
||||
string name = System.IO.Path.GetFileName(folder);
|
||||
var name = System.IO.Path.GetFileName(folder);
|
||||
string urlfile = null;
|
||||
string licensefile = null;
|
||||
string jsonfile = null;
|
||||
|
||||
@@ -1,55 +1,27 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Duplicati.Server.Serialization
|
||||
{
|
||||
public enum RunnerState
|
||||
{
|
||||
Started,
|
||||
Suspended,
|
||||
Running,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
public enum RunnerResult
|
||||
{
|
||||
OK,
|
||||
Partial,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
public enum CloseReason
|
||||
{
|
||||
None,
|
||||
ApplicationExitCall,
|
||||
TaskManagerClosing,
|
||||
WindowsShutDown,
|
||||
UserClosing
|
||||
}
|
||||
|
||||
public enum LiveControlState
|
||||
{
|
||||
Running,
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.Serialization.Interface
|
||||
@@ -53,27 +52,27 @@ namespace Duplicati.Server.Serialization.Interface
|
||||
/// The path to the local database
|
||||
/// </summary>
|
||||
string DBPath { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The backup source folders and files
|
||||
/// </summary>
|
||||
string[] Sources { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The backup settings
|
||||
/// </summary>
|
||||
ISetting[] Settings { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The filters applied to the source files
|
||||
/// </summary>
|
||||
IFilter[] Filters { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The backup metadata
|
||||
/// </summary>
|
||||
IDictionary<string, string> Metadata { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating if this instance is not persisted to the database
|
||||
/// </summary>
|
||||
|
||||
@@ -19,9 +19,6 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.Serialization.Interface
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Duplicati.Server.Serialization.Interface
|
||||
{
|
||||
/// <summary>
|
||||
@@ -32,12 +30,12 @@ namespace Duplicati.Server.Serialization.Interface
|
||||
/// The sort order
|
||||
/// </summary>
|
||||
long Order { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// True if the filter includes the items, false if it excludes
|
||||
/// </summary>
|
||||
bool Include { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The filter expression.
|
||||
/// If the filter is a regular expression, it starts and ends with hard brackets [ ]
|
||||
|
||||
@@ -31,48 +31,48 @@ namespace Duplicati.Server.Serialization.Interface
|
||||
/// <summary>
|
||||
/// The notification unique ID
|
||||
/// </summary>
|
||||
long ID { get; set; }
|
||||
long ID { get; }
|
||||
/// <summary>
|
||||
/// The notification type
|
||||
/// </summary>
|
||||
NotificationType Type { get; set; }
|
||||
NotificationType Type { get; }
|
||||
/// <summary>
|
||||
/// The notification title
|
||||
/// </summary>
|
||||
string Title { get; set; }
|
||||
string Title { get; }
|
||||
/// <summary>
|
||||
/// The notification message
|
||||
/// </summary>
|
||||
string Message { get; set; }
|
||||
string Message { get; }
|
||||
/// <summary>
|
||||
/// The serialized exception data, if any
|
||||
/// </summary>
|
||||
string Exception { get; set; }
|
||||
string Exception { get; }
|
||||
/// <summary>
|
||||
/// The ID of the backup that the notification belongs to
|
||||
/// </summary>
|
||||
string BackupID { get; set; }
|
||||
string BackupID { get; }
|
||||
/// <summary>
|
||||
/// The action for the notification
|
||||
/// </summary>
|
||||
string Action { get; set; }
|
||||
string Action { get; }
|
||||
/// <summary>
|
||||
/// When the notification was emitted
|
||||
/// </summary>
|
||||
DateTime Timestamp { get; set; }
|
||||
DateTime Timestamp { get; }
|
||||
/// <summary>
|
||||
/// The ID of the log entry that relates to this message, if any
|
||||
/// </summary>
|
||||
/// <value>The log entry identifier.</value>
|
||||
string LogEntryID { get; set; }
|
||||
string LogEntryID { get; }
|
||||
/// <summary>
|
||||
/// The ID of the event that triggered this notification
|
||||
/// </summary>
|
||||
string MessageID { get; set; }
|
||||
string MessageID { get; }
|
||||
/// <summary>
|
||||
/// The logtag of the error or event that triggered this notification
|
||||
/// </summary>
|
||||
string MessageLogTag { get; set; }
|
||||
string MessageLogTag { get; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
namespace Duplicati.Server.Serialization.Interface
|
||||
{
|
||||
@@ -29,19 +25,19 @@ namespace Duplicati.Server.Serialization.Interface
|
||||
{
|
||||
string BackupID { get; }
|
||||
long TaskID { get; }
|
||||
|
||||
string BackendAction { get; }
|
||||
|
||||
string BackendAction { get; }
|
||||
string BackendPath { get; }
|
||||
long BackendFileSize { get; }
|
||||
long BackendFileProgress { get; }
|
||||
long BackendSpeed { get; }
|
||||
bool BackendIsBlocking { get; }
|
||||
|
||||
|
||||
string CurrentFilename { get; }
|
||||
long CurrentFilesize { get; }
|
||||
long CurrentFileoffset { get; }
|
||||
bool CurrentFilecomplete { get; }
|
||||
|
||||
|
||||
string Phase { get; }
|
||||
float OverallProgress { get; }
|
||||
long ProcessedFileCount { get; }
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Duplicati.Server.Serialization.Interface
|
||||
{
|
||||
public interface IQueuedTask
|
||||
@@ -28,7 +26,7 @@ namespace Duplicati.Server.Serialization.Interface
|
||||
long TaskID { get; }
|
||||
string BackupID { get; }
|
||||
Duplicati.Server.Serialization.DuplicatiOperation Operation { get; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Duplicati.Server.Serialization.Interface
|
||||
{
|
||||
/// <summary>
|
||||
@@ -44,7 +42,7 @@ namespace Duplicati.Server.Serialization.Interface
|
||||
/// The generic modules known by the server
|
||||
/// </summary>
|
||||
IDynamicModule[] GenericModules { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The filters that are applied to all backups
|
||||
/// </summary>
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Duplicati.Server.Serialization.Interface
|
||||
{
|
||||
/// <summary>
|
||||
@@ -40,12 +38,12 @@ namespace Duplicati.Server.Serialization.Interface
|
||||
/// The setting value
|
||||
/// </summary>
|
||||
string Value { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The actual option arguments
|
||||
/// </summary>
|
||||
Duplicati.Library.Interface.ICommandLineArgument Argument { get; }
|
||||
|
||||
Library.Interface.ICommandLineArgument Argument { get; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -106,12 +103,12 @@ namespace Duplicati.Server.Serialization
|
||||
throw new JsonSerializationException(string.Format("Cannot convert null value to {0}", objectType));
|
||||
else if (reader.TokenType != JsonToken.String)
|
||||
throw new JsonSerializationException(string.Format("Cannot convert {0} value to {1}", reader.TokenType, objectType));
|
||||
|
||||
|
||||
var v = (string)reader.Value;
|
||||
DayOfWeek result;
|
||||
if (Enum.TryParse(v, out result))
|
||||
return result;
|
||||
|
||||
|
||||
switch (v.ToLowerInvariant())
|
||||
{
|
||||
case "mon":
|
||||
@@ -127,9 +124,9 @@ namespace Duplicati.Server.Serialization
|
||||
case "sat":
|
||||
return DayOfWeek.Saturday;
|
||||
case "sun":
|
||||
return DayOfWeek.Sunday;
|
||||
return DayOfWeek.Sunday;
|
||||
}
|
||||
|
||||
|
||||
throw new JsonSerializationException(string.Format("Cannot convert \"{0}\" to {1}", v, objectType));
|
||||
}
|
||||
|
||||
@@ -140,5 +137,5 @@ namespace Duplicati.Server.Serialization
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,56 +1,54 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace Duplicati.Server.Serialization
|
||||
{
|
||||
{
|
||||
public class Serializer
|
||||
{
|
||||
protected static readonly JsonSerializerSettings m_jsonSettings;
|
||||
public static JsonSerializerSettings JsonSettings { get; }
|
||||
protected static readonly Formatting m_jsonFormatting = Formatting.Indented;
|
||||
|
||||
static Serializer()
|
||||
{
|
||||
m_jsonSettings = new JsonSerializerSettings();
|
||||
m_jsonSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||
m_jsonSettings.Converters = new JsonConverter[] {
|
||||
new DayOfWeekConcerter(),
|
||||
new StringEnumConverter(),
|
||||
new SerializableStatusCreator(),
|
||||
new SettingsCreator(),
|
||||
new FilterCreator(),
|
||||
new NotificationCreator(),
|
||||
}.ToList();
|
||||
JsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
Converters = new JsonConverter[]
|
||||
{
|
||||
new DayOfWeekConcerter(),
|
||||
new StringEnumConverter(),
|
||||
new SerializableStatusCreator(),
|
||||
new SettingsCreator(),
|
||||
new FilterCreator(),
|
||||
new NotificationCreator(),
|
||||
}.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static void SerializeJson(System.IO.TextWriter sw, object o, bool preventDispose = false)
|
||||
{
|
||||
Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings);
|
||||
JsonSerializer jsonSerializer = JsonSerializer.Create(JsonSettings);
|
||||
var jsonWriter = new JsonTextWriter(sw);
|
||||
using (preventDispose ? null : jsonWriter)
|
||||
{
|
||||
@@ -61,26 +59,9 @@ namespace Duplicati.Server.Serialization
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SerializeJsonAsync(System.IO.TextWriter tw, object o, bool preventDispose = false)
|
||||
{
|
||||
Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
StringWriter sw = new StringWriter(sb);
|
||||
var jsonWriter = new JsonTextWriter(sw);
|
||||
using (preventDispose ? null : jsonWriter)
|
||||
{
|
||||
jsonWriter.Formatting = m_jsonFormatting;
|
||||
jsonSerializer.Serialize(jsonWriter, o);
|
||||
await jsonWriter.FlushAsync();
|
||||
}
|
||||
await sw.FlushAsync();
|
||||
await tw.WriteAsync(sb.ToString());
|
||||
await tw.FlushAsync();
|
||||
}
|
||||
|
||||
public static T Deserialize<T>(System.IO.TextReader sr)
|
||||
{
|
||||
Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings);
|
||||
JsonSerializer jsonSerializer = JsonSerializer.Create(JsonSettings);
|
||||
using (var jsonReader = new JsonTextReader(sr))
|
||||
{
|
||||
jsonReader.Culture = System.Globalization.CultureInfo.InvariantCulture;
|
||||
|
||||
+105
-117
@@ -22,10 +22,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Duplicati.Library.Common;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Main.Database;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.WebserverCore;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Duplicati.Server
|
||||
{
|
||||
@@ -50,11 +54,6 @@ namespace Duplicati.Server
|
||||
/// </summary>
|
||||
private static readonly string DATAFOLDER_ENV_NAME = Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName.ToUpper(CultureInfo.InvariantCulture) + "_HOME";
|
||||
|
||||
/// <summary>
|
||||
/// The environment variable that holds the database key used to encrypt the SQLite database
|
||||
/// </summary>
|
||||
private static readonly string DB_KEY_ENV_NAME = Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName.ToUpper(CultureInfo.InvariantCulture) + "_DB_KEY";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder where Duplicati data is stored
|
||||
/// </summary>
|
||||
@@ -78,12 +77,7 @@ namespace Duplicati.Server
|
||||
/// <summary>
|
||||
/// This is the scheduling thread
|
||||
/// </summary>
|
||||
public static Scheduler Scheduler { get => FIXMEGlobal.Scheduler; set => FIXMEGlobal.Scheduler = value; }
|
||||
|
||||
/// <summary>
|
||||
/// This is the working thread
|
||||
/// </summary>
|
||||
public static Duplicati.Library.Utility.WorkerThread<Runner.IRunnerData> WorkThread { get => FIXMEGlobal.WorkThread; set => FIXMEGlobal.WorkThread = value; }
|
||||
public static IScheduler Scheduler { get => FIXMEGlobal.Scheduler; }
|
||||
|
||||
/// <summary>
|
||||
/// List of completed task results
|
||||
@@ -108,7 +102,7 @@ namespace Duplicati.Server
|
||||
/// <summary>
|
||||
/// The controller interface for pause/resume and throttle options
|
||||
/// </summary>
|
||||
public static LiveControls LiveControl { get => FIXMEGlobal.LiveControl; set => FIXMEGlobal.LiveControl = value; }
|
||||
public static LiveControls LiveControl { get => DuplicatiWebserver.Provider.GetRequiredService<LiveControls>(); }
|
||||
|
||||
/// <summary>
|
||||
/// The application exit event
|
||||
@@ -116,19 +110,22 @@ namespace Duplicati.Server
|
||||
public static System.Threading.ManualResetEvent ApplicationExitEvent { get => FIXMEGlobal.ApplicationExitEvent; set => FIXMEGlobal.ApplicationExitEvent = value; }
|
||||
|
||||
/// <summary>
|
||||
/// The webserver instance
|
||||
/// Duplicati webserver instance
|
||||
/// </summary>
|
||||
private static WebServer.Server WebServer;
|
||||
public static DuplicatiWebserver DuplicatiWebserver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback to shutdown the modern webserver
|
||||
/// </summary>
|
||||
private static Action ShutdownModernWebserver;
|
||||
private static void ShutdownModernWebserver()
|
||||
{
|
||||
DuplicatiWebserver.Stop().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The update poll thread.
|
||||
/// </summary>
|
||||
public static UpdatePollThread UpdatePoller { get => FIXMEGlobal.UpdatePoller; set => FIXMEGlobal.UpdatePoller = value; }
|
||||
public static UpdatePollThread UpdatePoller => FIXMEGlobal.UpdatePoller;
|
||||
|
||||
/// <summary>
|
||||
/// An event that is set once the server is ready to respond to requests
|
||||
@@ -138,23 +135,13 @@ namespace Duplicati.Server
|
||||
/// <summary>
|
||||
/// The status event signaler, used to control long polling of status updates
|
||||
/// </summary>
|
||||
public static EventPollNotify StatusEventNotifyer { get => FIXMEGlobal.StatusEventNotifyer; }
|
||||
public static EventPollNotify StatusEventNotifyer => FIXMEGlobal.Provider.GetRequiredService<EventPollNotify>();
|
||||
|
||||
/// <summary>
|
||||
/// A delegate method for creating a copy of the current progress state
|
||||
/// </summary>
|
||||
public static Func<Duplicati.Server.Serialization.Interface.IProgressEventData> GenerateProgressState { get => FIXMEGlobal.GenerateProgressState; set => FIXMEGlobal.GenerateProgressState = value; }
|
||||
|
||||
/// <summary>
|
||||
/// An event ID that increases whenever the database is updated
|
||||
/// </summary>
|
||||
public static long LastDataUpdateID = 0;
|
||||
|
||||
/// <summary>
|
||||
/// An event ID that increases whenever a notification is updated
|
||||
/// </summary>
|
||||
public static long LastNotificationUpdateID = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The log redirect handler
|
||||
/// </summary>
|
||||
@@ -171,7 +158,7 @@ namespace Duplicati.Server
|
||||
{
|
||||
get
|
||||
{
|
||||
return WebServer.Port;
|
||||
return DuplicatiWebserver.Port;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,25 +180,10 @@ namespace Duplicati.Server
|
||||
set { DataConnection.ApplicationSettings.ServerPortChanged = value; }
|
||||
}
|
||||
|
||||
public static void IncrementLastDataUpdateID()
|
||||
{
|
||||
System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
|
||||
}
|
||||
|
||||
public static void IncrementLastNotificationUpdateID()
|
||||
{
|
||||
System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID);
|
||||
}
|
||||
|
||||
static Program()
|
||||
{
|
||||
FIXMEGlobal.IncrementLastDataUpdateID = Program.IncrementLastDataUpdateID;
|
||||
FIXMEGlobal.PeekLastDataUpdateID = () => Program.LastDataUpdateID;
|
||||
FIXMEGlobal.IncrementLastNotificationUpdateID = Program.IncrementLastNotificationUpdateID;
|
||||
FIXMEGlobal.PeekLastNotificationUpdateID = () => Program.LastNotificationUpdateID;
|
||||
FIXMEGlobal.GetDatabaseConnection = Program.GetDatabaseConnection;
|
||||
FIXMEGlobal.StartOrStopUsageReporter = Program.StartOrStopUsageReporter;
|
||||
FIXMEGlobal.UpdateThrottleSpeeds = Program.UpdateThrottleSpeeds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -256,12 +228,13 @@ namespace Duplicati.Server
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
DataConnection = GetDatabaseConnection(commandlineOptions);
|
||||
|
||||
if (!DataConnection.ApplicationSettings.FixedInvalidBackupId)
|
||||
DataConnection.FixInvalidBackupId();
|
||||
|
||||
DataConnection.ApplicationSettings.UpgradePasswordToKBDF();
|
||||
|
||||
CreateApplicationInstance(writeToConsole);
|
||||
|
||||
StartOrStopUsageReporter();
|
||||
@@ -270,12 +243,21 @@ namespace Duplicati.Server
|
||||
|
||||
ApplicationExitEvent = new System.Threading.ManualResetEvent(false);
|
||||
|
||||
Library.AutoUpdater.UpdaterManager.OnError += (Exception obj) =>
|
||||
Library.AutoUpdater.UpdaterManager.OnError += obj =>
|
||||
{
|
||||
DataConnection.LogError(null, "Error in updater", obj);
|
||||
};
|
||||
|
||||
UpdatePoller = new UpdatePollThread();
|
||||
DuplicatiWebserver = StartWebServer(commandlineOptions, DataConnection).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
if (FIXMEGlobal.Origin == "Server" && DataConnection.ApplicationSettings.AutogeneratedPassphrase)
|
||||
{
|
||||
var signinToken = DuplicatiWebserver.Provider.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("server-cli");
|
||||
Console.WriteLine($"Server is now running on port {DuplicatiWebserver.Port}");
|
||||
Console.WriteLine($"Initial signin url: http://localhost:{DuplicatiWebserver.Port}/signin.html?token={signinToken}");
|
||||
}
|
||||
|
||||
UpdatePoller.Init();
|
||||
|
||||
SetPurgeTempFilesTimer(commandlineOptions);
|
||||
|
||||
@@ -283,7 +265,6 @@ namespace Duplicati.Server
|
||||
|
||||
SetWorkerThread();
|
||||
|
||||
StartWebServer(commandlineOptions);
|
||||
|
||||
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, "ping-pong-keepalive"))
|
||||
{
|
||||
@@ -321,7 +302,7 @@ namespace Duplicati.Server
|
||||
ShutdownModernWebserver();
|
||||
UpdatePoller?.Terminate();
|
||||
Scheduler?.Terminate(true);
|
||||
WorkThread?.Terminate(true);
|
||||
FIXMEGlobal.WorkThread?.Terminate(true);
|
||||
ApplicationInstance?.Dispose();
|
||||
PurgeTempFilesTimer?.Dispose();
|
||||
|
||||
@@ -336,37 +317,58 @@ namespace Duplicati.Server
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void StartWebServer(Dictionary<string, string> commandlineOptions)
|
||||
private static async Task<DuplicatiWebserver> StartWebServer(IReadOnlyDictionary<string, string> options, Connection connection)
|
||||
{
|
||||
WebServer = new WebServer.Server(commandlineOptions);
|
||||
var server = await WebServerLoader.TryRunServer(options, connection, async parsedOptions =>
|
||||
{
|
||||
var mappedSettings = new DuplicatiWebserver.InitSettings(
|
||||
parsedOptions.WebRoot,
|
||||
parsedOptions.Port,
|
||||
parsedOptions.Interface,
|
||||
parsedOptions.Certificate,
|
||||
parsedOptions.Servername,
|
||||
parsedOptions.AllowedHostnames);
|
||||
|
||||
ServerPortChanged |= WebServer.Port != DataConnection.ApplicationSettings.LastWebserverPort;
|
||||
DataConnection.ApplicationSettings.LastWebserverPort = WebServer.Port;
|
||||
if (mappedSettings.AllowedHostnames == null || !mappedSettings.AllowedHostnames.Any())
|
||||
mappedSettings = mappedSettings with { AllowedHostnames = ["localhost", "127.0.0.1", "::1"] };
|
||||
|
||||
// var server = new DuplicatiWebserver();
|
||||
// ShutdownModernWebserver = server.Foo();
|
||||
var server = new DuplicatiWebserver();
|
||||
|
||||
server.InitWebServer(mappedSettings, connection);
|
||||
|
||||
// Start the server, but catch any configuration issues
|
||||
var task = server.Start(mappedSettings);
|
||||
await Task.WhenAny(task, Task.Delay(500));
|
||||
if (task.IsCompleted)
|
||||
await task;
|
||||
|
||||
return server;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
FIXMEGlobal.Provider = server.Provider;
|
||||
ServerPortChanged |= server.Port != DataConnection.ApplicationSettings.LastWebserverPort;
|
||||
DataConnection.ApplicationSettings.LastWebserverPort = server.Port;
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
private static void SetWorkerThread()
|
||||
{
|
||||
WorkThread = new Duplicati.Library.Utility.WorkerThread<Runner.IRunnerData>((x) => { Runner.Run(x, true); },
|
||||
LiveControl.State == LiveControls.LiveControlState.Paused);
|
||||
Scheduler = new Scheduler(WorkThread);
|
||||
|
||||
WorkThread.StartingWork += (worker, task) => { SignalNewEvent(null, null); };
|
||||
WorkThread.CompletedWork += (worker, task) => { SignalNewEvent(null, null); };
|
||||
WorkThread.WorkQueueChanged += (worker) => { SignalNewEvent(null, null); };
|
||||
Scheduler.NewSchedule += new EventHandler(SignalNewEvent);
|
||||
WorkThread.OnError += (worker, task, exception) =>
|
||||
FIXMEGlobal.WorkerThreadsManager.Spawn(x => { Runner.Run(x, true); });
|
||||
FIXMEGlobal.WorkThread.StartingWork += (worker, task) => { SignalNewEvent(null, null); };
|
||||
FIXMEGlobal.WorkThread.CompletedWork += (worker, task) => { SignalNewEvent(null, null); };
|
||||
FIXMEGlobal.WorkThread.WorkQueueChanged += (worker) => { SignalNewEvent(null, null); };
|
||||
FIXMEGlobal.Scheduler.SubScribeToNewSchedule(() => SignalNewEvent(null, null));
|
||||
FIXMEGlobal.WorkThread.OnError += (worker, task, exception) =>
|
||||
{
|
||||
Program.DataConnection.LogError(task?.BackupID, "Error in worker", exception);
|
||||
};
|
||||
|
||||
var lastScheduleId = LastDataUpdateID;
|
||||
var lastScheduleId = FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
|
||||
Program.StatusEventNotifyer.NewEvent += (sender, e) =>
|
||||
{
|
||||
if (lastScheduleId == LastDataUpdateID) return;
|
||||
lastScheduleId = LastDataUpdateID;
|
||||
if (lastScheduleId == FIXMEGlobal.NotificationUpdateService.LastDataUpdateId) return;
|
||||
lastScheduleId = FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
|
||||
Program.Scheduler.Reschedule();
|
||||
};
|
||||
|
||||
@@ -389,13 +391,12 @@ namespace Duplicati.Server
|
||||
}
|
||||
}
|
||||
|
||||
Program.WorkThread.CompletedWork += (worker, task) => { RegisterTaskResult(task.TaskID, null); };
|
||||
Program.WorkThread.OnError += (worker, task, exception) => { RegisterTaskResult(task.TaskID, exception); };
|
||||
FIXMEGlobal.WorkThread.CompletedWork += (worker, task) => { RegisterTaskResult(task.TaskID, null); };
|
||||
FIXMEGlobal.WorkThread.OnError += (worker, task, exception) => { RegisterTaskResult(task.TaskID, exception); };
|
||||
}
|
||||
|
||||
private static void SetLiveControls()
|
||||
{
|
||||
LiveControl = new LiveControls(DataConnection.ApplicationSettings);
|
||||
LiveControl.StateChanged += LiveControl_StateChanged;
|
||||
LiveControl.ThreadPriorityChanged += LiveControl_ThreadPriorityChanged;
|
||||
LiveControl.ThrottleSpeedChanged += LiveControl_ThrottleSpeedChanged;
|
||||
@@ -457,36 +458,33 @@ namespace Duplicati.Server
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
#if DEBUG
|
||||
PurgeTempFilesTimer =
|
||||
new System.Threading.Timer(purgeTempFilesCallback, null, TimeSpan.FromSeconds(10), TimeSpan.FromHours(1));
|
||||
PurgeTempFilesTimer =
|
||||
new System.Threading.Timer(purgeTempFilesCallback, null, TimeSpan.FromSeconds(10), TimeSpan.FromHours(1));
|
||||
#else
|
||||
PurgeTempFilesTimer =
|
||||
new System.Threading.Timer(purgeTempFilesCallback, null, TimeSpan.FromHours(1), TimeSpan.FromDays(1));
|
||||
PurgeTempFilesTimer =
|
||||
new System.Threading.Timer(purgeTempFilesCallback, null, TimeSpan.FromHours(1), TimeSpan.FromDays(1));
|
||||
#endif
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
//Bugfix for older Mono, slightly more resources used to avoid large values in the period field
|
||||
PurgeTempFilesTimer =
|
||||
new System.Threading.Timer(purgeTempFilesCallback, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AdjustApplicationSettings(Dictionary<string, string> commandlineOptions)
|
||||
{
|
||||
if (commandlineOptions.ContainsKey("webservice-password"))
|
||||
// This clears the JWT config, and a new will be generated, invalidating all existing tokens
|
||||
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_RESET_JWT_CONFIG))
|
||||
{
|
||||
DataConnection.ApplicationSettings.SetWebserverPassword(commandlineOptions["webservice-password"]);
|
||||
DataConnection.ApplicationSettings.JWTConfig = null;
|
||||
// Clean up stored tokens as they are now invalid
|
||||
DataConnection.ExecuteWithCommand((con) => con.ExecuteNonQuery("DELETE FROM TokenFamily"));
|
||||
}
|
||||
|
||||
DataConnection.ApplicationSettings.GenerateWebserverPasswordTrayIcon();
|
||||
|
||||
if (commandlineOptions.ContainsKey("webservice-allowed-hostnames"))
|
||||
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_PASSWORD))
|
||||
{
|
||||
DataConnection.ApplicationSettings.SetAllowedHostnames(commandlineOptions["webservice-allowed-hostnames"]);
|
||||
DataConnection.ApplicationSettings.SetWebserverPassword(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_PASSWORD]);
|
||||
}
|
||||
|
||||
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES))
|
||||
{
|
||||
DataConnection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +597,7 @@ namespace Duplicati.Server
|
||||
//
|
||||
|
||||
serverDataFolder = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Library.AutoUpdater.AutoUpdateSettings.AppName);
|
||||
if (Platform.IsClientWindows)
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var localappdata = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Library.AutoUpdater.AutoUpdateSettings.AppName);
|
||||
|
||||
@@ -668,22 +666,11 @@ namespace Duplicati.Server
|
||||
Library.UsageReporter.Reporter.SetReportLevel(reportLevel, disableUsageReporter);
|
||||
}
|
||||
|
||||
public static void UpdateThrottleSpeeds()
|
||||
{
|
||||
if (Program.WorkThread == null)
|
||||
return;
|
||||
|
||||
var cur = Program.WorkThread.CurrentTask;
|
||||
if (cur != null)
|
||||
cur.UpdateThrottleSpeed();
|
||||
}
|
||||
|
||||
private static void SignalNewEvent(object sender, EventArgs e)
|
||||
{
|
||||
StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handles a change in the LiveControl and updates the Runner
|
||||
/// </summary>
|
||||
@@ -707,24 +694,28 @@ namespace Duplicati.Server
|
||||
/// <summary>
|
||||
/// This event handler updates the trayicon menu with the current state of the runner.
|
||||
/// </summary>
|
||||
static void LiveControl_StateChanged(object sender, EventArgs e)
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
private static void LiveControl_StateChanged(object sender, EventArgs e)
|
||||
{
|
||||
var worker = FIXMEGlobal.WorkThread;
|
||||
switch (LiveControl.State)
|
||||
{
|
||||
case LiveControls.LiveControlState.Paused:
|
||||
{
|
||||
WorkThread.Pause();
|
||||
var t = WorkThread.CurrentTask;
|
||||
worker.Pause();
|
||||
var t = worker.CurrentTask;
|
||||
t?.Pause();
|
||||
break;
|
||||
}
|
||||
case LiveControls.LiveControlState.Running:
|
||||
{
|
||||
WorkThread.Resume();
|
||||
var t = WorkThread.CurrentTask;
|
||||
worker.Resume();
|
||||
var t = worker.CurrentTask;
|
||||
t?.Resume();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new InvalidOperationException($"State of {nameof(LiveControl)} was not recognized!");
|
||||
}
|
||||
|
||||
StatusEventNotifyer.SignalNewEvent();
|
||||
@@ -770,26 +761,23 @@ namespace Duplicati.Server
|
||||
new Duplicati.Library.Interface.CommandLineArgument("tempdir", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.TempdirShort, Strings.Program.TempdirLong, System.IO.Path.GetTempPath()),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("help", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.HelpCommandDescription, Strings.Program.HelpCommandDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("parameters-file", Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.ParametersFileOptionShort, Strings.Program.ParametersFileOptionLong2, "", new string[] {"parameter-file", "parameterfile"}),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("unencrypted-database", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.UnencrypteddatabaseCommandDescription, Strings.Program.UnencrypteddatabaseCommandDescription, "false", null, null, "Database encryption is no longer supported, this setting has no effect"),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("portable-mode", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.PortablemodeCommandDescription, Strings.Program.PortablemodeCommandDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("log-file", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.LogfileCommandDescription, Strings.Program.LogfileCommandDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("log-level", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Enumeration, Strings.Program.LoglevelCommandDescription, Strings.Program.LoglevelCommandDescription, "Warning", null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType))),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_WEBROOT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverWebrootDescription, Strings.Program.WebserverWebrootDescription, Duplicati.Server.WebServer.Server.DEFAULT_OPTION_WEBROOT),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_PORT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPortDescription, Strings.Program.WebserverPortDescription, Duplicati.Server.WebServer.Server.DEFAULT_OPTION_PORT.ToString()),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_SSLCERTIFICATEFILE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificateFileDescription, Strings.Program.WebserverCertificateFileDescription, Duplicati.Server.WebServer.Server.OPTION_SSLCERTIFICATEFILE),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_SSLCERTIFICATEFILEPASSWORD, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificatePasswordDescription, Strings.Program.WebserverCertificatePasswordDescription, Duplicati.Server.WebServer.Server.OPTION_SSLCERTIFICATEFILEPASSWORD),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_INTERFACE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverInterfaceDescription, Strings.Program.WebserverInterfaceDescription, Duplicati.Server.WebServer.Server.DEFAULT_OPTION_INTERFACE),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("webservice-password", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password, Strings.Program.WebserverPasswordDescription, Strings.Program.WebserverPasswordDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("webservice-allowed-hostnames", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverAllowedhostnamesDescription, Strings.Program.WebserverAllowedhostnamesDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBROOT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverWebrootDescription, Strings.Program.WebserverWebrootDescription, WebServerLoader.DEFAULT_OPTION_WEBROOT),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_PORT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPortDescription, Strings.Program.WebserverPortDescription, WebServerLoader.DEFAULT_OPTION_PORT.ToString()),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_SSLCERTIFICATEFILE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificateFileDescription, Strings.Program.WebserverCertificateFileDescription, WebServerLoader.OPTION_SSLCERTIFICATEFILE),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_SSLCERTIFICATEFILEPASSWORD, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificatePasswordDescription, Strings.Program.WebserverCertificatePasswordDescription, WebServerLoader.OPTION_SSLCERTIFICATEFILEPASSWORD),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_INTERFACE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverInterfaceDescription, Strings.Program.WebserverInterfaceDescription, WebServerLoader.DEFAULT_OPTION_INTERFACE),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password, Strings.Program.WebserverPasswordDescription, Strings.Program.WebserverPasswordDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverAllowedhostnamesDescription, Strings.Program.WebserverAllowedhostnamesDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_RESET_JWT_CONFIG, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverResetJwtConfigDescription, Strings.Program.WebserverResetJwtConfigDescription),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("ping-pong-keepalive", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.PingpongkeepaliveShort, Strings.Program.PingpongkeepaliveLong),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("log-retention", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan, Strings.Program.LogretentionShort, Strings.Program.LogretentionLong, DEFAULT_LOG_RETENTION),
|
||||
new Duplicati.Library.Interface.CommandLineArgument("server-datafolder", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.ServerdatafolderShort, Strings.Program.ServerdatafolderLong(DATAFOLDER_ENV_NAME), System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Library.AutoUpdater.AutoUpdateSettings.AppName)),
|
||||
|
||||
});
|
||||
|
||||
if (!(OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()))
|
||||
lst.Add(new Duplicati.Library.Interface.CommandLineArgument("server-encryption-key", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password, Strings.Program.ServerencryptionkeyShort, Strings.Program.ServerencryptionkeyLong(DB_KEY_ENV_NAME, "unencrypted-database"), Library.AutoUpdater.AutoUpdateSettings.AppName + "_Key_42", null, null, "Database encryption is no longer supported, this setting can only be used to decrypt the database"));
|
||||
|
||||
return lst.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Duplicati.Library.Common;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Utility;
|
||||
|
||||
namespace Duplicati.Server
|
||||
{
|
||||
@@ -138,7 +136,7 @@ namespace Duplicati.Server
|
||||
|
||||
m_lockfilename = System.IO.Path.Combine(m_controldir, CONTROL_FILE);
|
||||
m_file = null;
|
||||
|
||||
|
||||
System.IO.Stream temp_fs = null;
|
||||
|
||||
try
|
||||
@@ -147,7 +145,7 @@ namespace Duplicati.Server
|
||||
temp_fs = PosixFile.OpenExclusive(m_lockfilename, System.IO.FileAccess.Write);
|
||||
else
|
||||
temp_fs = System.IO.File.Open(m_lockfilename, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None);
|
||||
|
||||
|
||||
if (temp_fs != null)
|
||||
{
|
||||
System.IO.StreamWriter sw = new System.IO.StreamWriter(temp_fs);
|
||||
@@ -161,7 +159,7 @@ namespace Duplicati.Server
|
||||
{
|
||||
if (temp_fs != null)
|
||||
try { temp_fs.Dispose(); }
|
||||
catch {}
|
||||
catch { }
|
||||
}
|
||||
|
||||
//If we have write access
|
||||
@@ -174,7 +172,7 @@ namespace Duplicati.Server
|
||||
DateTime startup = System.IO.File.GetLastWriteTime(m_lockfilename);
|
||||
|
||||
//Clean up any files that were created before the app launched
|
||||
foreach(string s in SystemIO.IO_OS.GetFiles(m_controldir))
|
||||
foreach (string s in SystemIO.IO_OS.GetFiles(m_controldir))
|
||||
if (s != m_lockfilename && System.IO.File.GetCreationTime(s) < startup)
|
||||
try { System.IO.File.Delete(s); }
|
||||
catch { }
|
||||
@@ -313,7 +311,7 @@ namespace Duplicati.Server
|
||||
|
||||
//Finally inform this instance about the call
|
||||
if (SecondInstanceDetected != null)
|
||||
SecondInstanceDetected(commandline);
|
||||
SecondInstanceDetected(commandline);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Server.Database;
|
||||
|
||||
namespace Duplicati.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for starting the webserver
|
||||
/// </summary>
|
||||
public static class WebServerLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// The tag used for logging
|
||||
/// </summary>
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(typeof(WebServerLoader));
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webroot folder
|
||||
/// </summary>
|
||||
public const string OPTION_WEBROOT = "webservice-webroot";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen port
|
||||
/// </summary>
|
||||
public const string OPTION_PORT = "webservice-port";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen interface
|
||||
/// </summary>
|
||||
public const string OPTION_INTERFACE = "webservice-interface";
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice password
|
||||
/// </summary>
|
||||
public const string OPTION_WEBSERVICE_PASSWORD = "webservice-password";
|
||||
|
||||
/// <summary>
|
||||
/// Option for resetting the JWT configuration
|
||||
/// </summary>
|
||||
public const string OPTION_WEBSERVICE_RESET_JWT_CONFIG = "webservice-reset-jwt-config";
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice allowed hostnames
|
||||
/// </summary>
|
||||
public const string OPTION_WEBSERVICE_ALLOWEDHOSTNAMES = "webservice-allowedhostnames";
|
||||
|
||||
/// <summary>
|
||||
/// The default path to the web root
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_WEBROOT = "webroot";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening port
|
||||
/// </summary>
|
||||
public const int DEFAULT_OPTION_PORT = 8200;
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile";
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate key
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening interface
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_INTERFACE = "loopback";
|
||||
|
||||
/// <summary>
|
||||
/// The parsed settings for the webserver
|
||||
/// </summary>
|
||||
/// <param name="WebRoot">The root folder with static files</param>
|
||||
/// <param name="Port">The listining port</param>
|
||||
/// <param name="Interface">The listening interface</param>
|
||||
/// <param name="Certificate">The certificate, if any</param>
|
||||
/// <param name="Servername">The servername to report</param>
|
||||
/// <param name="AllowedHostnames">The allowed hostnames</param>
|
||||
public record ParsedWebserverSettings(
|
||||
string WebRoot,
|
||||
int Port,
|
||||
System.Net.IPAddress Interface,
|
||||
X509Certificate2? Certificate,
|
||||
string Servername,
|
||||
IEnumerable<string> AllowedHostnames
|
||||
);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the webserver and starts it
|
||||
/// </summary>
|
||||
/// <param name="options">A set of options</param>
|
||||
/// <param name="createServer">The method to start the server</param>
|
||||
public static async Task<TServer> TryRunServer<TServer>(IReadOnlyDictionary<string, string> options, Connection connection, Func<ParsedWebserverSettings, Task<TServer>> createServer)
|
||||
{
|
||||
var ports = Enumerable.Empty<int>();
|
||||
options.TryGetValue(OPTION_PORT, out var portstring);
|
||||
if (!string.IsNullOrEmpty(portstring))
|
||||
ports =
|
||||
from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
where int.TryParse(n, out _)
|
||||
select int.Parse(n);
|
||||
|
||||
if (ports == null || !ports.Any())
|
||||
ports = [DEFAULT_OPTION_PORT];
|
||||
|
||||
options.TryGetValue(OPTION_INTERFACE, out var interfacestring);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = connection.ApplicationSettings.ServerListenInterface;
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = DEFAULT_OPTION_INTERFACE;
|
||||
|
||||
var listenInterface = System.Net.IPAddress.Loopback;
|
||||
interfacestring = interfacestring.Trim();
|
||||
if (new[] { "*", "all", "any" }.Any(x => x.Equals(interfacestring, StringComparison.OrdinalIgnoreCase)))
|
||||
listenInterface = System.Net.IPAddress.Any;
|
||||
else if (interfacestring != "loopback")
|
||||
listenInterface = System.Net.IPAddress.Parse(interfacestring);
|
||||
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out var certificateFile);
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out var certificateFilePassword);
|
||||
certificateFilePassword = certificateFilePassword?.Trim() ?? "";
|
||||
|
||||
X509Certificate2? cert = null;
|
||||
if (certificateFile == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
cert = connection.ApplicationSettings.ServerSSLCertificate;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase);
|
||||
}
|
||||
}
|
||||
else if (certificateFile.Length == 0)
|
||||
{
|
||||
connection.ApplicationSettings.ServerSSLCertificate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
var webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;
|
||||
|
||||
#if DEBUG
|
||||
//For debug we go "../../../../../.." to get out of "Executables/net8/Duplicati.GUI.TrayIcon/bin/debug/net8.0"
|
||||
string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", "..", "..", ".."));
|
||||
tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Duplicati", "Server");
|
||||
if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot")))
|
||||
webroot = tmpwebroot;
|
||||
#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(Duplicati.Library.Common.IO.Util.AppendDirSeparator(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison))
|
||||
webroot = userroot;
|
||||
#endif
|
||||
}
|
||||
|
||||
var certValid = cert != null && cert.HasPrivateKey;
|
||||
var settings = new ParsedWebserverSettings(
|
||||
webroot,
|
||||
-1,
|
||||
listenInterface,
|
||||
cert,
|
||||
string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version),
|
||||
options.GetValueOrDefault(OPTION_WEBSERVICE_ALLOWEDHOSTNAMES, "").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
);
|
||||
|
||||
// If we are in hosted mode with no specified port,
|
||||
// then try different ports
|
||||
foreach (var p in ports)
|
||||
try
|
||||
{
|
||||
settings = settings with { Port = p };
|
||||
|
||||
var server = await createServer(settings);
|
||||
if (interfacestring != connection.ApplicationSettings.ServerListenInterface)
|
||||
connection.ApplicationSettings.ServerListenInterface = interfacestring;
|
||||
|
||||
if (certValid && cert != connection.ApplicationSettings.ServerSSLCertificate)
|
||||
connection.ApplicationSettings.ServerSSLCertificate = cert;
|
||||
|
||||
Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p));
|
||||
|
||||
return server;
|
||||
}
|
||||
catch (System.Net.Sockets.SocketException)
|
||||
{
|
||||
}
|
||||
|
||||
throw new Exception(Strings.Server.ServerStartFailure(ports));
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,44 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
|
||||
<title>Duplicati Login</title>
|
||||
|
||||
|
||||
<script type="text/javascript" src="login/jquery-2.0.3.min.js"></script>
|
||||
<script type="text/javascript" src="login/cryptojs.js"></script>
|
||||
<script type="text/javascript" src="login/login.js?v=2.0.0.7"></script>
|
||||
<link rel="stylesheet" type="text/css" href="login/login.css?v=2.0.0.7" />
|
||||
|
||||
<script type="text/javascript" src="oem/root/login/oem.js?v=2.0.0.7" ></script>
|
||||
<link rel="stylesheet" type="text/css" href="oem/root/login/oem.css?v=2.0.0.7" />
|
||||
</head>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="oem/root/login/oem.js?v=2.0.0.7"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="oem/root/login/oem.css?v=2.0.0.7"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<div id="login">
|
||||
<h2>Duplicati</h2>
|
||||
<form method="POST">
|
||||
<fieldset>
|
||||
<h2>Duplicati</h2>
|
||||
<form id="login-form" method="POST">
|
||||
<fieldset>
|
||||
<p><label for="login-password">Password</label></p>
|
||||
<p>
|
||||
<input
|
||||
type="password"
|
||||
id="login-password"
|
||||
placeholder="password"
|
||||
autofocus
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p><label for="login-password">Password</label></p>
|
||||
<p><input type="password" id="login-password" value="password" onBlur="if(this.value=='')this.value='password'" onFocus="if(this.value=='password')this.value=''" autofocus></p> <!-- JS because of IE support; better: placeholder="password" -->
|
||||
|
||||
<p><input type="submit" id="login-button" value="Sign In"></p>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
<p><input type="submit" id="login-button" value="Sign In" /></p>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
CryptoJS v3.1.2
|
||||
code.google.com/p/crypto-js
|
||||
(c) 2009-2013 by Jeff Mott. All rights reserved.
|
||||
code.google.com/p/crypto-js/wiki/License
|
||||
Combined: sha256 + core + enc-base64
|
||||
*/
|
||||
var CryptoJS=CryptoJS||function(h,r){var k={},l=k.lib={},n=function(){},f=l.Base={extend:function(a){n.prototype=this;var b=new n;a&&b.mixIn(a);b.hasOwnProperty("init")||(b.init=function(){b.$super.init.apply(this,arguments)});b.init.prototype=b;b.$super=this;return b},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
|
||||
j=l.WordArray=f.extend({init:function(a,b){a=this.words=a||[];this.sigBytes=b!=r?b:4*a.length},toString:function(a){return(a||s).stringify(this)},concat:function(a){var b=this.words,d=a.words,c=this.sigBytes;a=a.sigBytes;this.clamp();if(c%4)for(var e=0;e<a;e++)b[c+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((c+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)b[c+e>>>2]=d[e>>>2];else b.push.apply(b,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,b=this.sigBytes;a[b>>>2]&=4294967295<<
|
||||
32-8*(b%4);a.length=h.ceil(b/4)},clone:function(){var a=f.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var b=[],d=0;d<a;d+=4)b.push(4294967296*h.random()|0);return new j.init(b,a)}}),m=k.enc={},s=m.Hex={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++){var e=b[c>>>2]>>>24-8*(c%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c+=2)d[c>>>3]|=parseInt(a.substr(c,
|
||||
2),16)<<24-4*(c%8);return new j.init(d,b/2)}},p=m.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++)d.push(String.fromCharCode(b[c>>>2]>>>24-8*(c%4)&255));return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c++)d[c>>>2]|=(a.charCodeAt(c)&255)<<24-8*(c%4);return new j.init(d,b)}},t=m.Utf8={stringify:function(a){try{return decodeURIComponent(escape(p.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return p.parse(unescape(encodeURIComponent(a)))}},
|
||||
q=l.BufferedBlockAlgorithm=f.extend({reset:function(){this._data=new j.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=t.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,d=b.words,c=b.sigBytes,e=this.blockSize,f=c/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;c=h.min(4*a,c);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);b.sigBytes-=c}return new j.init(g,c)},clone:function(){var a=f.clone.call(this);
|
||||
a._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:f.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,d){return(new a.init(d)).finalize(b)}},_createHmacHelper:function(a){return function(b,d){return(new u.HMAC.init(a,
|
||||
d)).finalize(b)}}});var u=k.algo={};return k}(Math);
|
||||
var CryptoJS=CryptoJS||function(h,s){var f={},t=f.lib={},g=function(){},j=t.Base={extend:function(a){g.prototype=this;var c=new g;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
|
||||
q=t.WordArray=j.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||u).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
|
||||
32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=j.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new q.init(c,a)}}),v=f.enc={},u=v.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b,
|
||||
2),16)<<24-4*(b%8);return new q.init(d,c/2)}},k=v.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new q.init(d,c)}},l=v.Utf8={stringify:function(a){try{return decodeURIComponent(escape(k.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return k.parse(unescape(encodeURIComponent(a)))}},
|
||||
x=t.BufferedBlockAlgorithm=j.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=l.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var m=0;m<a;m+=e)this._doProcessBlock(d,m);m=d.splice(0,a);c.sigBytes-=b}return new q.init(m,b)},clone:function(){var a=j.clone.call(this);
|
||||
a._data=this._data.clone();return a},_minBufferSize:0});t.Hasher=x.extend({cfg:j.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){x.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new w.HMAC.init(a,
|
||||
d)).finalize(c)}}});var w=f.algo={};return f}(Math);
|
||||
(function(h){for(var s=CryptoJS,f=s.lib,t=f.WordArray,g=f.Hasher,f=s.algo,j=[],q=[],v=function(a){return 4294967296*(a-(a|0))|0},u=2,k=0;64>k;){var l;a:{l=u;for(var x=h.sqrt(l),w=2;w<=x;w++)if(!(l%w)){l=!1;break a}l=!0}l&&(8>k&&(j[k]=v(h.pow(u,0.5))),q[k]=v(h.pow(u,1/3)),k++);u++}var a=[],f=f.SHA256=g.extend({_doReset:function(){this._hash=new t.init(j.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],m=b[2],h=b[3],p=b[4],j=b[5],k=b[6],l=b[7],n=0;64>n;n++){if(16>n)a[n]=
|
||||
c[d+n]|0;else{var r=a[n-15],g=a[n-2];a[n]=((r<<25|r>>>7)^(r<<14|r>>>18)^r>>>3)+a[n-7]+((g<<15|g>>>17)^(g<<13|g>>>19)^g>>>10)+a[n-16]}r=l+((p<<26|p>>>6)^(p<<21|p>>>11)^(p<<7|p>>>25))+(p&j^~p&k)+q[n]+a[n];g=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&m^f&m);l=k;k=j;j=p;p=h+r|0;h=m;m=f;f=e;e=r+g|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+m|0;b[3]=b[3]+h|0;b[4]=b[4]+p|0;b[5]=b[5]+j|0;b[6]=b[6]+k|0;b[7]=b[7]+l|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes;
|
||||
d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=g.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=g._createHelper(f);s.HmacSHA256=g._createHmacHelper(f)})(Math);
|
||||
(function(){var h=CryptoJS,j=h.lib.WordArray;h.enc.Base64={stringify:function(b){var e=b.words,f=b.sigBytes,c=this._map;b.clamp();b=[];for(var a=0;a<f;a+=3)for(var d=(e[a>>>2]>>>24-8*(a%4)&255)<<16|(e[a+1>>>2]>>>24-8*((a+1)%4)&255)<<8|e[a+2>>>2]>>>24-8*((a+2)%4)&255,g=0;4>g&&a+0.75*g<f;g++)b.push(c.charAt(d>>>6*(3-g)&63));if(e=c.charAt(64))for(;b.length%4;)b.push(e);return b.join("")},parse:function(b){var e=b.length,f=this._map,c=f.charAt(64);c&&(c=b.indexOf(c),-1!=c&&(e=c));for(var c=[],a=0,d=0;d<
|
||||
e;d++)if(d%4){var g=f.indexOf(b.charAt(d-1))<<2*(d%4),h=f.indexOf(b.charAt(d))>>>6-2*(d%4);c[a>>>2]|=(g|h)<<24-8*(a%4);a++}return j.create(c,a)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();
|
||||
|
||||
@@ -1,55 +1,33 @@
|
||||
$(document).ready(function() {
|
||||
var processing = false;
|
||||
|
||||
$('#login-button').click(function() {
|
||||
$('#login-form').on('submit', function() {
|
||||
|
||||
console.log('login-form submit');
|
||||
if (processing)
|
||||
return;
|
||||
|
||||
processing = true;
|
||||
|
||||
// First we grab the nonce and salt
|
||||
$.ajax({
|
||||
url: './login.cgi',
|
||||
url: './api/v1/auth/login',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {'get-nonce': 1}
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({'Password': $('#login-password').val(), 'RememberMe': true })
|
||||
})
|
||||
.done(function(data) {
|
||||
var saltedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Utf8.parse($('#login-password').val()) + CryptoJS.enc.Base64.parse(data.Salt)));
|
||||
|
||||
var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse(data.Nonce) + saltedpwd)).toString(CryptoJS.enc.Base64);
|
||||
|
||||
$.ajax({
|
||||
url: './login.cgi',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {'password': noncedpwd }
|
||||
})
|
||||
.done(function(data) {
|
||||
window.location = './';
|
||||
})
|
||||
.fail(function(data) {
|
||||
var txt = data;
|
||||
if (txt && txt.statusText)
|
||||
txt = txt.statusText;
|
||||
alert('Login failed: ' + txt);
|
||||
processing = false;
|
||||
});
|
||||
window.location = './';
|
||||
})
|
||||
.fail(function(data) {
|
||||
var txt = data;
|
||||
if (txt && txt.statusText)
|
||||
if (txt && txt.responseJSON && txt.responseJSON.Error)
|
||||
txt = txt.responseJSON.Error;
|
||||
else if (txt && txt.statusText)
|
||||
txt = txt.statusText;
|
||||
|
||||
alert('Failed to get nonce: ' + txt);
|
||||
alert('Login failed: ' + txt);
|
||||
processing = false;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -29,8 +29,6 @@
|
||||
<link rel="stylesheet" type="text/css" href="styles/default.css" disabled="disabled">
|
||||
<link rel="stylesheet" type="text/css" href="styles/dark.css" disabled="disabled">
|
||||
<link rel="stylesheet" type="text/css" href="styles/{{active_theme}}.css">
|
||||
<link rel="stylesheet" type="text/css" href="../oem/ngax/styles/oem.css?v=2.0.0.7" />
|
||||
<link rel="stylesheet" type="text/css" href="../customized/customized.css?v=2.0.0.7" />
|
||||
|
||||
<script type="text/javascript" src="scripts/libs/oldbrowsersupport.js"></script>
|
||||
<script type="text/javascript" src="scripts/libs/cryptojs.js"></script>
|
||||
@@ -126,11 +124,6 @@
|
||||
<script type="text/javascript" src="scripts/directives/advancedOptionsEditor.js?v=2.0.0.7"></script>
|
||||
<script type="text/javascript" src="scripts/directives/timeFormatFixer.js?v=2.0.0.7"></script>
|
||||
<script type="text/javascript" src="scripts/directives/externalLink.js?v=2.0.0.7"></script>
|
||||
|
||||
<script type="text/javascript" src="../package/ngax/package.js?v=2.0.0.7"></script>
|
||||
<script type="text/javascript" src="../oem/ngax/scripts/oem.js?v=2.0.0.7"></script>
|
||||
<script type="text/javascript" src="../customized/customized.js?v=2.0.0.7"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -298,14 +291,20 @@
|
||||
<div id="connection-lost-dialog" ng-show="state.connectionState != 'connected'" class="connection-lost ng-cloak">
|
||||
<div class="info">
|
||||
<div class="title" translate>Connection lost</div>
|
||||
<div class="content" ng-show="state.connectionState !='connecting'" translate translate-params-time="(state.connectionAttemptTimer + 1000) | timeremaining">
|
||||
<div class="content" ng-show="state.connectionState == 'unauthorized'" translate>
|
||||
Connection to server was rejected due to invalid authentication. Reload browser window to reconnect to server properly.
|
||||
<br />
|
||||
If this problem persist open this page from the TrayIcon instead.
|
||||
</div>
|
||||
<div class="content" ng-show="state.connectionState != 'unauthorized' && state.connectionState != 'connecting'" translate translate-params-time="(state.connectionAttemptTimer + 1000) | timeremaining">
|
||||
The connection to the server is lost, attempting again in {{time}} ...
|
||||
</div>
|
||||
<div class="content" translate ng-hide="state.connectionState !='connecting'">
|
||||
<div class="content" translate ng-hide="state.connectionState != 'connecting'">
|
||||
Connecting to server …
|
||||
</div>
|
||||
<div class="content buttons">
|
||||
<a href ng-show="state.connectionState != 'connecting'" ng-click="doReconnect()" class="button" translate>Connect now</a>
|
||||
<a href ng-show="state.connectionState == 'unauthorized'" ng-click="reload()" class="button" translate>Reload</a>
|
||||
<a href ng-show="state.connectionState != 'unauthorized' && state.connectionState != 'connecting'" ng-click="doReconnect()" class="button" translate>Connect now</a>
|
||||
<a href ng-hide="state.connectionState != 'connecting'" class="button" style="cursor: default;" translate>Connecting...</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,10 @@ backupApp.controller('AppController', function($scope, $cookies, $location, AppS
|
||||
ServerStatus.reconnect();
|
||||
};
|
||||
|
||||
$scope.reload = function() {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
$scope.resume = function() {
|
||||
ServerStatus.resume().then(function() {}, AppUtils.connectionError);
|
||||
};
|
||||
@@ -165,21 +169,20 @@ backupApp.controller('AppController', function($scope, $cookies, $location, AppS
|
||||
var dt = data.data['max-download-speed'];
|
||||
$scope.throttle_active = (ut != null && ut.trim().length != 0) || (dt != null && dt.trim().length != 0);
|
||||
|
||||
var firstpw = data.data['has-asked-for-password-protection'];
|
||||
var haspw = data.data['server-passphrase'];
|
||||
if (!firstpw && haspw == '') {
|
||||
var has_asked = data.data['has-asked-for-password-change'];
|
||||
var autogen_passphrase = data.data['autogenerated-passphrase'];
|
||||
if (!has_asked && autogen_passphrase == "True") {
|
||||
DialogService.dialog(
|
||||
gettextCatalog.getString('First run setup'),
|
||||
gettextCatalog.getString('If your machine is in a multi-user environment (i.e. the machine has more than one account), you need to set a password to prevent other users from accessing data on your account.\nDo you want to set a password now?'),
|
||||
[gettextCatalog.getString('No, my machine has only a single account'), gettextCatalog.getString('Yes')],
|
||||
gettextCatalog.getString('Duplicati needs to be secured with a passphrase and a random passphrase has been generated for you.\nIf you open Duplicati from the tray icon, you do not need a passphrase, but if you plan to open it from another location you need to set a passphrase you know.\nDo you want to set a passphrase now?'),
|
||||
[gettextCatalog.getString('No'), gettextCatalog.getString('Yes')],
|
||||
function(btn) {
|
||||
AppService.patch('/serversettings', { 'has-asked-for-password-protection': 'true'}, {'headers': {'Content-Type': 'application/json'}});
|
||||
AppService.patch('/serversettings', { 'has-asked-for-password-change': 'true'}, {'headers': {'Content-Type': 'application/json'}});
|
||||
if (btn == 1) {
|
||||
$location.path('/settings');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}, AppUtils.connectionError);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user