diff --git a/docs/API.dox b/docs/API.dox
index ddc5028bd4..49c19ce3ee 100644
--- a/docs/API.dox
+++ b/docs/API.dox
@@ -418,11 +418,19 @@ To create, write, and delete files use the following request
I POST "/Config" @ref Tgstation.Server.Api.Models.ConfigurationFile => @ref Tgstation.Server.Api.Models.ConfigurationFile
-When creating a file, only @ref Tgstation.Server.Api.Models.ConfigurationFile.Path and @ref Tgstation.Server.Api.Models.ConfigurationFile.Content should be specified. Any necessary preceeding directories will be created if possible.
+When creating a file, only @ref Tgstation.Server.Api.Models.ConfigurationFile.Path and @ref Tgstation.Server.Api.Models.ConfigurationFile.Content should be specified
If the file already exists, the @ref Tgstation.Server.Api.Models.ConfigurationFile.LastReadHash field must also be present with the last version recieved from the server for that file. If this does not match at the time of the request, 409 will be returned, indicating the file has changed since it was last viewed by the client.
-To delete a file set @ref Tgstation.Server.Api.Models.ConfigurationFile.Content to null in the request. If deleting a file leaves a directory empty, they too will be deleted.
+To delete a file set @ref Tgstation.Server.Api.Models.ConfigurationFile.Content to null in the request.
+
+
+To delete an empty directory use the following request
+
+I DELETE "/Config" @ref Tgstation.Server.Api.Models.ConfigurationFile => OK
+
+Where the request @ref Tgstation.Server.Api.Models.ConfigurationFile.Path is set to the directory to delete
+
@subsection api_dog Watchdog
diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs
index 8405ac5517..e20a143c47 100644
--- a/src/Tgstation.Server.Client/ApiClient.cs
+++ b/src/Tgstation.Server.Client/ApiClient.cs
@@ -197,6 +197,9 @@ namespace Tgstation.Server.Client
///
public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Delete, instanceId, cancellationToken);
+ ///
+ public Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest(route, body, HttpMethod.Delete, instanceId, cancellationToken);
+
///
public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Delete, instanceId, cancellationToken);
diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs
index 4aa4d82330..d3f1a5bccf 100644
--- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs
+++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs
@@ -19,6 +19,20 @@ namespace Tgstation.Server.Client.Components
///
readonly Instance instance;
+ ///
+ /// Sanitize a path for use in a GET
+ ///
+ /// The path to sanitize
+ /// The sanitized path
+ static string SanitizeGetPath(string path)
+ {
+ if (path == null)
+ path = String.Empty;
+ if (path.Length == 0 || path[0] != '/')
+ path = '/' + path;
+ return path;
+ }
+
///
/// Construct a
///
@@ -31,22 +45,17 @@ namespace Tgstation.Server.Client.Components
}
///
- public Task DeleteEmptyDirectory(string directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration + directory ?? throw new ArgumentNullException(nameof(directory)), instance.Id, cancellationToken);
+ public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken);
///
- public Task> List(string directory, CancellationToken cancellationToken)
- {
- if (directory == null)
- directory = String.Empty;
- return apiClient.Read>(Routes.List(Routes.Configuration) + directory, instance.Id, cancellationToken);
- }
+ public Task> List(string directory, CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken);
///
public Task Read(ConfigurationFile file, CancellationToken cancellationToken)
{
if (file == null)
throw new ArgumentNullException(nameof(file));
- return apiClient.Read(Routes.Configuration + file.Path, instance.Id, cancellationToken);
+ return apiClient.Read(Routes.Configuration + SanitizeGetPath(file.Path), instance.Id, cancellationToken);
}
///
diff --git a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs
index ec8a5c9396..2d2337ff50 100644
--- a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs
+++ b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs
@@ -37,9 +37,9 @@ namespace Tgstation.Server.Client.Components
///
/// Delete an empty
///
- /// The path to directory to delete
+ /// The representing the directory to delete
/// The for the operation
/// A representing the running operation
- Task DeleteEmptyDirectory(string directory, CancellationToken cancellationToken);
+ Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Client/IApiClient.cs b/src/Tgstation.Server.Client/IApiClient.cs
index 8359972f02..82b1298122 100644
--- a/src/Tgstation.Server.Client/IApiClient.cs
+++ b/src/Tgstation.Server.Client/IApiClient.cs
@@ -35,6 +35,7 @@ namespace Tgstation.Server.Client
Task Read(string route, long instanceId, CancellationToken cancellationToken);
Task Update(string route, TBody body, long instanceId, CancellationToken cancellationToken);
Task Delete(string route, long instanceId, CancellationToken cancellationToken);
+ Task Delete (string route, TBody body, long instanceId, CancellationToken cancellationToken);
Task Delete(string route, long instanceId, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
index 1e0a42c7bc..69846570a3 100644
--- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
+++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
@@ -144,6 +144,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles
var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
if (nullOrEmptyCheck)
configurationRelativePath = ".";
+ if (configurationRelativePath[0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
+ configurationRelativePath = '.' + configurationRelativePath;
var resolved = ioManager.ResolvePath(configurationRelativePath);
var local = !nullOrEmptyCheck ? ioManager.ResolvePath(".") : null;
if (!nullOrEmptyCheck && resolved.Length < local.Length) //.. fuccbois
diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
index f1ad7587a1..8e36f841c7 100644
--- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
@@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using System;
using System.IO;
+using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
@@ -9,6 +10,7 @@ using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
+using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
@@ -25,29 +27,39 @@ namespace Tgstation.Server.Host.Controllers
///
readonly IInstanceManager instanceManager;
+ ///
+ /// The for the
+ ///
+ readonly IIOManager ioManager;
+
///
/// Construct a
///
/// The for the
/// The for the
/// The value of
+ /// The value of
/// The for the
- public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true)
+ public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
+ this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
}
///
- /// If a should be returned from actions due to conflicts with one or both of the or the
+ /// If a should be returned from actions due to conflicts with one or both of the or the or a given tries to access parent directories
///
+ /// The path to validate if any
/// if a should be returned, otherwise
- bool ForbidDueToModeConflicts() => Instance.ConfigurationType == ConfigurationType.Disallowed || (Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite && AuthenticationContext.SystemIdentity == null);
+ bool ForbidDueToModeConflicts(string path) => Instance.ConfigurationType == ConfigurationType.Disallowed || (Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite && AuthenticationContext.SystemIdentity == null) || (path != null && ioManager.PathContainsParentAccess(path));
///
[TgsAuthorize(ConfigurationRights.Write)]
public override async Task Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
{
- if (ForbidDueToModeConflicts())
+ if (model == null)
+ throw new ArgumentNullException(nameof(model));
+ if (ForbidDueToModeConflicts(model.Path))
return Forbid();
var config = instanceManager.GetInstance(Instance).Configuration;
@@ -88,7 +100,7 @@ namespace Tgstation.Server.Host.Controllers
[TgsAuthorize(ConfigurationRights.Read)]
public async Task File(string filePath, CancellationToken cancellationToken)
{
- if (ForbidDueToModeConflicts())
+ if (ForbidDueToModeConflicts(filePath))
return Forbid();
try
@@ -123,7 +135,7 @@ namespace Tgstation.Server.Host.Controllers
[TgsAuthorize(ConfigurationRights.List)]
public async Task Directory(string directoryPath, CancellationToken cancellationToken)
{
- if (ForbidDueToModeConflicts())
+ if (ForbidDueToModeConflicts(directoryPath))
return Forbid();
try
@@ -152,7 +164,10 @@ namespace Tgstation.Server.Host.Controllers
[TgsAuthorize(ConfigurationRights.Write)]
public override async Task Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
{
- if (ForbidDueToModeConflicts())
+ if (model == null)
+ throw new ArgumentNullException(nameof(model));
+
+ if (ForbidDueToModeConflicts(model.Path))
return Forbid();
try
@@ -169,16 +184,19 @@ namespace Tgstation.Server.Host.Controllers
}
}
- [HttpDelete("{*directoryPath}")]
+ [HttpDelete]
[TgsAuthorize(ConfigurationRights.Delete)]
- public async Task Delete(string directoryPath, CancellationToken cancellationToken)
+ public async Task Delete([FromBody] ConfigurationFile directory, CancellationToken cancellationToken)
{
- if (ForbidDueToModeConflicts())
+ if (directory == null)
+ throw new ArgumentNullException(nameof(directory));
+
+ if (ForbidDueToModeConflicts(directory.Path))
return Forbid();
try
{
- return await instanceManager.GetInstance(Instance).Configuration.DeleteDirectory(directoryPath, AuthenticationContext.SystemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Ok() : Conflict(new ErrorMessage
+ return await instanceManager.GetInstance(Instance).Configuration.DeleteDirectory(directory.Path, AuthenticationContext.SystemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Ok() : Conflict(new ErrorMessage
{
Message = "Directory not empty!"
});
diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs
index 15fd161b0d..dee0d3eecf 100644
--- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs
+++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs
@@ -310,5 +310,8 @@ namespace Tgstation.Server.Host.IO
using (var archive = new ZipArchive(ms, ZipArchiveMode.Read))
archive.ExtractToDirectory(path);
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
+
+ ///
+ public bool PathContainsParentAccess(string path) => path?.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Any(x => x == "..") ?? throw new ArgumentNullException(nameof(path));
}
}
diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs
index da63c6d117..4ce21a7c97 100644
--- a/src/Tgstation.Server.Host/IO/IIOManager.cs
+++ b/src/Tgstation.Server.Host/IO/IIOManager.cs
@@ -31,6 +31,13 @@ namespace Tgstation.Server.Host.IO
/// The file name portion of
string GetFileNameWithoutExtension(string path);
+ ///
+ /// Check if a contains the '..' parent directory accessor
+ ///
+ /// The path to check
+ /// if contains a '..' accessor, otherwise
+ bool PathContainsParentAccess(string path);
+
///
/// Copies a directory from to
///
diff --git a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs
index fa394a644c..d31e652e5d 100644
--- a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs
+++ b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs
@@ -104,7 +104,7 @@ namespace Tgstation.Server.Host.IO
using (var sha1 = new SHA1Managed())
#pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1.
{
- string GetSha1(byte[] dataToHash) => dataToHash.Length != 0 ? String.Join("", sha1.ComputeHash(dataToHash).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))) : null;
+ string GetSha1(byte[] dataToHash) => dataToHash != null && dataToHash.Length != 0 ? String.Join("", sha1.ComputeHash(dataToHash).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))) : null;
var originalSha1 = GetSha1(originalBytes);
if (originalSha1 != sha1InOut)
{