diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000000..5c3351535e1
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+version: 2
+updates:
+ - package-ecosystem: "nuget"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ labels:
+ - "Dependencies"
+ open-pull-requests-limit: 100
diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml
index 9a7b7471377..2e04ae0b1ec 100644
--- a/.github/workflows/ci-pipeline.yml
+++ b/.github/workflows/ci-pipeline.yml
@@ -55,34 +55,40 @@ jobs:
runs-on: ubuntu-latest
permissions:
pull-requests: write
- if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id && github.event.pull_request.state == 'open'
+ if: github.event_name == 'pull_request_target' && (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id || github.event.pull_request.user.id == 49699333) && github.event.pull_request.state == 'open'
steps:
- name: Comment on new Fork PR
- if: github.event.action == 'opened' && !contains(github.event.pull_request.labels.*.name, 'CI Cleared')
+ if: github.event.action == 'opened' && !contains(github.event.pull_request.labels.*.name, 'CI Cleared') && github.event.pull_request.user.id != 49699333
uses: thollander/actions-comment-pull-request@1d3973dc4b8e1399c0620d3f2b1aa5e795465308
with:
message: Thank you for contributing to ${{ github.event.pull_request.base.repo.name }}! The workflow '${{ github.workflow }}' requires repository secrets and will not run without approval. Maintainers can add the `CI Cleared` label to allow it to run. Please note that any changes to the workflow file will not be reflected in the run.
+ - name: Comment on dependabot PR
+ if: github.event.action == 'opened' && !contains(github.event.pull_request.labels.*.name, 'CI Cleared') && github.event.pull_request.user.id == 49699333
+ uses: thollander/actions-comment-pull-request@1d3973dc4b8e1399c0620d3f2b1aa5e795465308
+ with:
+ message: Set the milestone to the next minor version, check for supply chain attacks, and then add the `CI Cleared` label to allow CI to run.
+
- name: "Remove Stale 'CI Cleared' Label"
if: github.event.action == 'synchronize' || github.event.action == 'reopened'
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0
with:
labels: CI Cleared
- - name: "Add 'CI Approval Required' Label"
- if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
- uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8
+ - name: "Remove 'CI Approval Required' Label"
+ if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
+ uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0
with:
labels: CI Approval Required
- github_token: ${{ github.token }}
- - name: "Remove 'CI Approval Required' Label"
+ - name: "Add 'CI Approval Required' Label"
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
- uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0
+ uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8
with:
labels: CI Approval Required
+ github_token: ${{ github.token }}
- - name: Fail Clearance Check if PR has Unlabeled new Commits from Fork
+ - name: Fail Clearance Check if PR has Unlabeled new Commits from User
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
run: exit 1
@@ -90,7 +96,7 @@ jobs:
name: CI Start Gate
needs: security-checkpoint
runs-on: ubuntu-latest
- if: (!(cancelled() || failure()) && (needs.security-checkpoint.result == 'success' || (needs.security-checkpoint.result == 'skipped' && (github.event_name == 'push' || github.event_name == 'schedule' || (github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id && github.event_name != 'pull_request_target')))))
+ if: (!(cancelled() || failure()) && (needs.security-checkpoint.result == 'success' || (needs.security-checkpoint.result == 'skipped' && (github.event_name == 'push' || github.event_name == 'schedule' || ((github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id && github.event.pull_request.user.id != 49699333) && github.event_name != 'pull_request_target')))))
steps:
- name: GitHub Requires at Least One Step for a Job
run: exit 0
diff --git a/README.md b/README.md
index 5f9246c5ab9..794543dc854 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
+
+
+
+
# tgstation-server
![CI Pipeline](https://github.com/tgstation/tgstation-server/workflows/CI%20Pipeline/badge.svg) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server)
diff --git a/build/Version.props b/build/Version.props
index 80aeac2c88d..0099619aac5 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -3,12 +3,12 @@
- 6.7.0
+ 6.8.0
5.1.0
- 10.5.0
+ 10.6.0
7.0.0
- 13.5.0
- 15.5.0
+ 13.6.0
+ 15.6.0
7.1.3
5.9.0
1.4.1
diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj
index 85cbe7e02f2..bf40086eea5 100644
--- a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj
+++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs
index 7dc134f9053..9e1fea2fe96 100644
--- a/src/Tgstation.Server.Api/Models/ErrorCode.cs
+++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs
@@ -651,5 +651,11 @@ public enum ErrorCode : uint
///
[Description("Could not create dump as dotnet diagnostics threw an exception!")]
DotnetDiagnosticsFailure,
+
+ ///
+ /// The configured .dme could not be found.
+ ///
+ [Description("Could not load configured .dme due to it being outside the deployment directory! This should be a relative path.")]
+ DeploymentWrongDme,
}
}
diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
index b55dff5e5f4..0a2e4679bd7 100644
--- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
+++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
@@ -28,7 +28,7 @@
-
+
diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
index 6890dc25e54..e923f773051 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
@@ -607,6 +607,9 @@ await eventConsumer.HandleEvent(
else
{
var targetDme = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension));
+ if (!await ioManager.PathIsChildOf(outputDirectory, targetDme, cancellationToken))
+ throw new JobException(ErrorCode.DeploymentWrongDme);
+
var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken);
if (!targetDmeExists)
throw new JobException(ErrorCode.DeploymentMissingDme);
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
index 85f37f0f842..b56d5a9a7ad 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
@@ -103,8 +103,8 @@ public InstanceController(
IInstanceManager instanceManager,
IJobManager jobManager,
IIOManager ioManager,
- IPortAllocator portAllocator,
IPlatformIdentifier platformIdentifier,
+ IPortAllocator portAllocator,
IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
IOptions generalConfigurationOptions,
IOptions swarmConfigurationOptions,
@@ -150,77 +150,52 @@ public async ValueTask Create([FromBody] InstanceCreateRequest mo
if (earlyOut != null)
return earlyOut;
- var unNormalizedPath = model.Path;
- var targetInstancePath = NormalizePath(unNormalizedPath);
+ var targetInstancePath = NormalizePath(model.Path!);
model.Path = targetInstancePath;
- var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory);
-
- bool InstanceIsChildOf(string otherPath)
- {
- if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
- return false;
-
- bool sameLength = targetInstancePath.Length == otherPath.Length;
- char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
- return sameLength
- || dirSeparatorChar == Path.DirectorySeparatorChar
- || dirSeparatorChar == Path.AltDirectorySeparatorChar;
- }
-
- if (InstanceIsChildOf(installationDirectoryPath))
+ var installationDirectoryPath = DefaultIOManager.CurrentDirectory;
+ if (await ioManager.PathIsChildOf(installationDirectoryPath, targetInstancePath, cancellationToken))
return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath));
// Validate it's not a child of any other instance
- ulong countOfOtherInstances = 0;
- using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
- {
- var newCancellationToken = cts.Token;
- try
- {
- await DatabaseContext
- .Instances
- .AsQueryable()
- .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier)
- .Select(x => new Models.Instance
- {
- Path = x.Path,
- })
- .ForEachAsync(
- otherInstance =>
- {
- if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
- earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceLimitReached));
- else if (InstanceIsChildOf(otherInstance.Path!))
- earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath));
-
- if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
- cts.Cancel();
- },
- newCancellationToken);
- }
- catch (OperationCanceledException)
+ var instancePaths = await DatabaseContext
+ .Instances
+ .AsQueryable()
+ .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier)
+ .Select(x => new Models.Instance
{
- cancellationToken.ThrowIfCancellationRequested();
- }
- }
+ Path = x.Path,
+ })
+ .ToListAsync(cancellationToken);
- if (earlyOut != null)
- return earlyOut;
+ if ((instancePaths.Count + 1) >= generalConfiguration.InstanceLimit)
+ return Conflict(new ErrorMessageResponse(ErrorCode.InstanceLimitReached));
+
+ var instancePathChecks = instancePaths
+ .Select(otherInstance => ioManager.PathIsChildOf(otherInstance.Path!, targetInstancePath, cancellationToken))
+ .ToArray();
+
+ await Task.WhenAll(instancePathChecks);
+
+ if (instancePathChecks.Any(task => task.Result))
+ return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath));
// Last test, ensure it's in the list of valid paths
- if (!(generalConfiguration.ValidInstancePaths?
- .Select(path => NormalizePath(path))
- .Any(path => InstanceIsChildOf(path)) ?? true))
+ var pathChecks = generalConfiguration.ValidInstancePaths?
+ .Select(path => ioManager.PathIsChildOf(path, targetInstancePath, cancellationToken))
+ .ToArray()
+ ?? Enumerable.Empty>();
+ await Task.WhenAll(pathChecks);
+ if (!pathChecks.All(task => task.Result))
return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceNotAtWhitelistedPath));
async ValueTask DirExistsAndIsNotEmpty()
{
- if (!await ioManager.DirectoryExists(model.Path, cancellationToken))
+ if (!await ioManager.DirectoryExists(targetInstancePath, cancellationToken))
return false;
- var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
- var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken);
+ var filesTask = ioManager.GetFiles(targetInstancePath, cancellationToken);
+ var dirsTask = ioManager.GetDirectories(targetInstancePath, cancellationToken);
var files = await filesTask;
var dirs = await dirsTask;
@@ -230,8 +205,8 @@ async ValueTask DirExistsAndIsNotEmpty()
var dirExistsTask = DirExistsAndIsNotEmpty();
bool attached = false;
- if (await ioManager.FileExists(model.Path, cancellationToken) || await dirExistsTask)
- if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken))
+ if (await ioManager.FileExists(targetInstancePath, cancellationToken) || await dirExistsTask)
+ if (!await ioManager.FileExists(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken))
return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtExistingPath));
else
attached = true;
@@ -248,7 +223,7 @@ async ValueTask DirExistsAndIsNotEmpty()
try
{
// actually reserve it now
- await ioManager.CreateDirectory(unNormalizedPath, cancellationToken);
+ await ioManager.CreateDirectory(targetInstancePath, cancellationToken);
await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken);
}
catch
@@ -397,13 +372,13 @@ bool CheckModified(Expression> expression, Insta
}
string? originalModelPath = null;
- string? rawPath = null;
+ string? normalizedPath = null;
var originalOnline = originalModel.Online!.Value;
if (model.Path != null)
{
- rawPath = NormalizePath(model.Path);
+ normalizedPath = NormalizePath(model.Path);
- if (rawPath != originalModel.Path)
+ if (normalizedPath != originalModel.Path)
{
if (!userRights.HasFlag(InstanceManagerRights.Relocate))
return Forbid();
@@ -415,7 +390,7 @@ bool CheckModified(Expression> expression, Insta
return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtExistingPath));
originalModelPath = originalModel.Path;
- originalModel.Path = rawPath;
+ originalModel.Path = normalizedPath;
}
}
@@ -505,7 +480,7 @@ await WithComponentInstanceNullable(
var moving = originalModelPath != null;
if (moving)
{
- var description = $"Move instance ID {originalModel.Id} from {originalModelPath} to {rawPath}";
+ var description = $"Move instance ID {originalModel.Id} from {originalModelPath} to {normalizedPath}";
var job = Job.Create(JobCode.Move, AuthenticationContext.User, originalModel, InstanceManagerRights.Relocate);
job.Description = description;
@@ -823,8 +798,7 @@ InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet? permissi
return null;
path = ioManager.ResolvePath(path);
- if (platformIdentifier.IsWindows)
- path = path.ToUpperInvariant().Replace('\\', '/');
+ path = platformIdentifier.NormalizePath(path);
return path;
}
diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs
index c88c7a5e0d3..694fff0c9c0 100644
--- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs
+++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs
@@ -51,6 +51,11 @@ sealed class DatabaseSeeder : IDatabaseSeeder
///
readonly DatabaseConfiguration databaseConfiguration;
+ ///
+ /// The for the .
+ ///
+ readonly SwarmConfiguration swarmConfiguration;
+
///
/// Add a default system to a given .
///
@@ -83,6 +88,7 @@ static User SeedSystemUser(IDatabaseContext databaseContext, User? tgsUser = nul
/// The value of .
/// The containing the value of .
/// The containing the value of .
+ /// The containing the value of .
/// The value of .
/// The value of .
public DatabaseSeeder(
@@ -90,6 +96,7 @@ public DatabaseSeeder(
IPlatformIdentifier platformIdentifier,
IOptions generalConfigurationOptions,
IOptions databaseConfigurationOptions,
+ IOptions swarmConfigurationOptions,
ILogger databaseLogger,
ILogger logger)
{
@@ -97,6 +104,7 @@ public DatabaseSeeder(
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
+ swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
this.databaseLogger = databaseLogger ?? throw new ArgumentNullException(nameof(databaseLogger));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -223,16 +231,14 @@ async ValueTask SanitizeDatabase(IDatabaseContext databaseContext, CancellationT
}
}
- if (platformIdentifier.IsWindows)
- {
- // normalize backslashes to forward slashes
- var allInstances = await databaseContext
- .Instances
- .AsQueryable()
- .ToListAsync(cancellationToken);
- foreach (var instance in allInstances)
- instance.Path = instance.Path!.Replace('\\', '/');
- }
+ // normalize backslashes to forward slashes
+ var allInstances = await databaseContext
+ .Instances
+ .AsQueryable()
+ .Where(instance => instance.SwarmIdentifer == swarmConfiguration.Identifier)
+ .ToListAsync(cancellationToken);
+ foreach (var instance in allInstances)
+ instance.Path = platformIdentifier.NormalizePath(instance.Path!.Replace('\\', '/'));
if (generalConfiguration.ByondTopicTimeout != 0)
{
diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs
index 0ce1d50f45d..b392bb76543 100644
--- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs
+++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs
@@ -358,6 +358,33 @@ public Task GetLastModified(string path, CancellationToken cance
DefaultBufferSize,
true);
+ ///
+ public Task PathIsChildOf(string parentPath, string childPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
+ () =>
+ {
+ parentPath = ResolvePath(parentPath);
+ childPath = ResolvePath(childPath);
+
+ if (parentPath == childPath)
+ return true;
+
+ // https://stackoverflow.com/questions/5617320/given-full-path-check-if-path-is-subdirectory-of-some-other-path-or-otherwise?lq=1
+ var di1 = new DirectoryInfo(parentPath);
+ var di2 = new DirectoryInfo(childPath);
+ while (di2.Parent != null)
+ {
+ if (di2.Parent.FullName == di1.FullName)
+ return true;
+
+ di2 = di2.Parent;
+ }
+
+ return false;
+ },
+ cancellationToken,
+ BlockingTaskCreationOptions,
+ TaskScheduler.Current);
+
///
/// Copies a directory from to .
///
diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs
index e14f15cf292..13a29310e1e 100644
--- a/src/Tgstation.Server.Host/IO/IIOManager.cs
+++ b/src/Tgstation.Server.Host/IO/IIOManager.cs
@@ -45,6 +45,15 @@ public interface IIOManager
/// if contains a '..' accessor, otherwise.
bool PathContainsParentAccess(string path);
+ ///
+ /// Check if a given is a parent of a given .
+ ///
+ /// The parent path.
+ /// The child path.
+ /// The for the operation.
+ /// A resulting in if is a child of or they are equivalent.
+ Task PathIsChildOf(string parentPath, string childPath, CancellationToken cancellationToken);
+
///
/// Copies a directory from to .
///
@@ -68,7 +77,7 @@ ValueTask CopyDirectory(
///
/// The file to check for existence.
/// The for the operation.
- /// A resulting in if the file at exists, otherwise.
+ /// A resulting in if the file at exists, otherwise.
Task FileExists(string path, CancellationToken cancellationToken);
///
diff --git a/src/Tgstation.Server.Host/System/IPlatformIdentifier.cs b/src/Tgstation.Server.Host/System/IPlatformIdentifier.cs
index 805b93b97d4..b22173d2c63 100644
--- a/src/Tgstation.Server.Host/System/IPlatformIdentifier.cs
+++ b/src/Tgstation.Server.Host/System/IPlatformIdentifier.cs
@@ -17,5 +17,12 @@ public interface IPlatformIdentifier
/// The extension of executable script files for the system.
///
string ScriptFileExtension { get; }
+
+ ///
+ /// Normalize a path for consistency.
+ ///
+ /// The path to normalize.
+ /// The normalized path.
+ string NormalizePath(string path);
}
}
diff --git a/src/Tgstation.Server.Host/System/PlatformIdentifier.cs b/src/Tgstation.Server.Host/System/PlatformIdentifier.cs
index 22931b97ce1..afa9d607e14 100644
--- a/src/Tgstation.Server.Host/System/PlatformIdentifier.cs
+++ b/src/Tgstation.Server.Host/System/PlatformIdentifier.cs
@@ -1,4 +1,5 @@
-using System.Runtime.InteropServices;
+using System;
+using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Tgstation.Server.Host.System
@@ -21,5 +22,15 @@ public PlatformIdentifier()
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
ScriptFileExtension = IsWindows ? "bat" : "sh";
}
+
+ ///
+ public string NormalizePath(string path)
+ {
+ ArgumentNullException.ThrowIfNull(path);
+ if (IsWindows)
+ path = path.Replace('\\', '/');
+
+ return path;
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
index e29c37faa7e..197583214f0 100644
--- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
+++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
@@ -125,7 +125,7 @@
-
+
diff --git a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj
index 326b6f38b24..5e2a7f71e96 100644
--- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj
+++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs
index fe32e9e1969..1b20cd560c3 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs
@@ -1,4 +1,6 @@
using System;
+using System.IO;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -200,6 +202,27 @@ await dreamMakerClient.Update(new DreamMakerRequest
deployJob = await dreamMakerClient.Compile(cancellationToken);
await WaitForJob(deployJob, 40, true, ErrorCode.DeploymentMissingDme, cancellationToken);
+ // set to an absolute path that does exist
+ var tempFile = Path.GetTempFileName().Replace('\\', '/');
+ try
+ {
+ // for testing purposes, assume same drive for windows
+ var relativePath = $"../../{String.Join("/", instanceClient.Metadata.Path.Replace('\\', '/').Where(pathChar => pathChar == '/').Select(x => ".."))}{tempFile.Substring(tempFile.IndexOf('/'))}";
+ var dmePath = $"{tempFile}.dme";
+ File.Move(tempFile, dmePath);
+ tempFile = dmePath;
+ await dreamMakerClient.Update(new DreamMakerRequest
+ {
+ ProjectName = relativePath
+ }, cancellationToken);
+ deployJob = await dreamMakerClient.Compile(cancellationToken);
+ await WaitForJob(deployJob, 40, true, ErrorCode.DeploymentWrongDme, cancellationToken);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+
// check that we can change the visibility
await vpTest;