mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-29 16:11:05 +01:00
Merge pull request #1105 from tgstation/1103-SwaggerUI [APIDeploy][NugetDeploy]
Adds configuration option for swagger UI. Add DM Output display options to Discord embed. Add TokenFactory config settings. Plus other Discord buffs
This commit is contained in:
+2
-2
@@ -3,9 +3,9 @@
|
||||
<!-- This is the authorative version list -->
|
||||
<!-- Integration tests will ensure they match across the board -->
|
||||
<TgsCoreVersion>4.5.0</TgsCoreVersion>
|
||||
<TgsConfigVersion>2.0.0</TgsConfigVersion>
|
||||
<TgsConfigVersion>2.1.0</TgsConfigVersion>
|
||||
<TgsApiVersion>7.3.0</TgsApiVersion>
|
||||
<TgsClientVersion>8.2.0</TgsClientVersion>
|
||||
<TgsClientVersion>8.3.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>5.2.3</TgsDmapiVersion>
|
||||
<TgsControlPanelVersion>0.4.0</TgsControlPanelVersion>
|
||||
<TgsHostWatchdogVersion>1.1.0</TgsHostWatchdogVersion>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
@@ -12,11 +13,16 @@ namespace Tgstation.Server.Api.Models
|
||||
public override bool Valid => !String.IsNullOrEmpty(BotToken);
|
||||
|
||||
/// <summary>
|
||||
/// The Discord bot token
|
||||
/// The Discord bot token.
|
||||
/// </summary>
|
||||
/// <remarks>See https://discordapp.com/developers/docs/topics/oauth2#bots</remarks>
|
||||
public string? BotToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DiscordDMOutputDisplayType"/>.
|
||||
/// </summary>
|
||||
public DiscordDMOutputDisplayType DMOutputDisplay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="DiscordConnectionStringBuilder"/>
|
||||
/// </summary>
|
||||
@@ -28,10 +34,17 @@ namespace Tgstation.Server.Api.Models
|
||||
/// <param name="connectionString">The connection string</param>
|
||||
public DiscordConnectionStringBuilder(string connectionString)
|
||||
{
|
||||
BotToken = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
|
||||
if(connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
|
||||
var splits = connectionString.Split(';');
|
||||
BotToken = splits.First();
|
||||
if (splits.Length < 2 || !Enum.TryParse<DiscordDMOutputDisplayType>(splits[1], out var dMOutputDisplayType))
|
||||
dMOutputDisplayType = DiscordDMOutputDisplayType.Always;
|
||||
DMOutputDisplay = dMOutputDisplayType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => BotToken ?? "(null)";
|
||||
public override string ToString() => $"{BotToken};{(int)DMOutputDisplay}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// When the DM output section of Discord deployment embeds should be shown.
|
||||
/// </summary>
|
||||
public enum DiscordDMOutputDisplayType
|
||||
{
|
||||
/// <summary>
|
||||
/// Always show.
|
||||
/// </summary>
|
||||
Always,
|
||||
|
||||
/// <summary>
|
||||
/// Only show if DM failed.
|
||||
/// </summary>
|
||||
OnError,
|
||||
|
||||
/// <summary>
|
||||
/// Never show.
|
||||
/// </summary>
|
||||
Never
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,10 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
#pragma warning disable CA1506
|
||||
sealed class ChatManager : IChatManager, IRestartHandler
|
||||
{
|
||||
const string CommonMention = "!tgs";
|
||||
/// <summary>
|
||||
/// The common bot mention.
|
||||
/// </summary>
|
||||
public const string CommonMention = "!tgs";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IProviderFactory"/> for the <see cref="ChatManager"/>
|
||||
@@ -415,7 +418,11 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
// prune disconnected providers
|
||||
foreach (var I in messageTasks.Where(x => !x.Key.Disposed).ToList())
|
||||
{
|
||||
messageTasks.Remove(I.Key);
|
||||
if (I.Value.IsCompleted)
|
||||
(await I.Value.ConfigureAwait(false))?.Context?.Dispose();
|
||||
}
|
||||
|
||||
// add new ones
|
||||
Task updatedTask;
|
||||
@@ -439,6 +446,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
|
||||
{
|
||||
var message = await I.Value.ConfigureAwait(false);
|
||||
using var messageContext = message?.Context;
|
||||
var messageNumber = Interlocked.Increment(ref messagesProcessed);
|
||||
using (LogContext.PushProperty("ChatMessage", messageNumber))
|
||||
await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a message recieved by a <see cref="IProvider"/>
|
||||
@@ -14,5 +16,10 @@
|
||||
/// The <see cref="ChatUser"/> who sent the <see cref="Message"/>
|
||||
/// </summary>
|
||||
public ChatUser User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IDisposable"/> that should be <see cref="IDisposable.Dispose"/>d once the <see cref="Message"/> is processed.
|
||||
/// </summary>
|
||||
public IDisposable Context { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Discord bot token.
|
||||
/// </summary>
|
||||
string BotToken => ChatBot.ConnectionString;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="DiscordProvider"/>.
|
||||
/// </summary>
|
||||
@@ -52,6 +47,16 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// </summary>
|
||||
readonly List<ulong> mappedChannels;
|
||||
|
||||
/// <summary>
|
||||
/// The Discord bot token.
|
||||
/// </summary>
|
||||
readonly string botToken;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DiscordDMOutputDisplayType"/>.
|
||||
/// </summary>
|
||||
readonly DiscordDMOutputDisplayType outputDisplayType;
|
||||
|
||||
/// <summary>
|
||||
/// Normalize a discord mention string
|
||||
/// </summary>
|
||||
@@ -74,6 +79,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
: base(jobManager, logger, chatBot)
|
||||
{
|
||||
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
|
||||
var csb = new DiscordConnectionStringBuilder(chatBot.ConnectionString);
|
||||
botToken = csb.BotToken;
|
||||
outputDisplayType = csb.DMOutputDisplay;
|
||||
|
||||
client = new DiscordSocketClient();
|
||||
client.MessageReceived += Client_MessageReceived;
|
||||
mappedChannels = new List<ulong>();
|
||||
@@ -96,53 +106,76 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
if (e.Author.Id == client.CurrentUser.Id)
|
||||
return;
|
||||
|
||||
if (e.Content.Equals("Based on what?", StringComparison.OrdinalIgnoreCase))
|
||||
IDisposable typingState = null;
|
||||
void StartTyping() => typingState = e.Channel.EnterTypingState();
|
||||
try
|
||||
{
|
||||
// DCT: None available
|
||||
await SendMessage(
|
||||
e.Channel.Id,
|
||||
"https://youtu.be/LrNu-SuFF_o",
|
||||
default)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var pm = e.Channel is IPrivateChannel;
|
||||
|
||||
if (!pm && !mappedChannels.Contains(e.Channel.Id))
|
||||
{
|
||||
var mentionedUs = e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id);
|
||||
if (mentionedUs)
|
||||
if (e.Content.Equals("Based on what?", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username);
|
||||
StartTyping();
|
||||
|
||||
// DCT: None available
|
||||
await SendMessage(e.Channel.Id, "I do not respond to this channel!", default).ConfigureAwait(false);
|
||||
await SendMessage(
|
||||
e.Channel.Id,
|
||||
"https://youtu.be/LrNu-SuFF_o",
|
||||
default)
|
||||
.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
var pm = e.Channel is IPrivateChannel;
|
||||
var shouldNotAnswer = !pm;
|
||||
if (shouldNotAnswer)
|
||||
lock (mappedChannels)
|
||||
shouldNotAnswer = !mappedChannels.Contains(e.Channel.Id);
|
||||
|
||||
var result = new Message
|
||||
{
|
||||
Content = NormalizeMentions(e.Content),
|
||||
User = new ChatUser
|
||||
var content = NormalizeMentions(e.Content);
|
||||
var mentionedUs = e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id)
|
||||
|| (!shouldNotAnswer && content.Split(' ').First().Equals(ChatManager.CommonMention, StringComparison.OrdinalIgnoreCase));
|
||||
if (mentionedUs)
|
||||
StartTyping();
|
||||
|
||||
if (shouldNotAnswer)
|
||||
{
|
||||
RealId = e.Author.Id,
|
||||
Channel = new ChannelRepresentation
|
||||
if (mentionedUs)
|
||||
{
|
||||
RealId = e.Channel.Id,
|
||||
IsPrivateChannel = pm,
|
||||
ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
|
||||
FriendlyName = e.Channel.Name
|
||||
Logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username);
|
||||
|
||||
// isAdmin and Tag populated by manager
|
||||
},
|
||||
FriendlyName = e.Author.Username,
|
||||
Mention = NormalizeMentions(e.Author.Mention)
|
||||
// DCT: None available
|
||||
await SendMessage(e.Channel.Id, "I do not respond to this channel!", default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
EnqueueMessage(result);
|
||||
var result = new Message
|
||||
{
|
||||
Content = content,
|
||||
User = new ChatUser
|
||||
{
|
||||
RealId = e.Author.Id,
|
||||
Channel = new ChannelRepresentation
|
||||
{
|
||||
RealId = e.Channel.Id,
|
||||
IsPrivateChannel = pm,
|
||||
ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
|
||||
FriendlyName = e.Channel.Name
|
||||
|
||||
// isAdmin and Tag populated by manager
|
||||
},
|
||||
FriendlyName = e.Author.Username,
|
||||
Mention = NormalizeMentions(e.Author.Mention)
|
||||
},
|
||||
Context = typingState
|
||||
};
|
||||
|
||||
EnqueueMessage(result);
|
||||
typingState = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
typingState?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -150,7 +183,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.LoginAsync(TokenType.Bot, BotToken, true).ConfigureAwait(false);
|
||||
await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
|
||||
|
||||
Logger.LogTrace("Logged in.");
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -226,29 +259,47 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
|
||||
|
||||
var channelId = channelFromDB.DiscordChannelId.Value;
|
||||
var discordChannel = client.GetChannel(channelId);
|
||||
if (discordChannel is ITextChannel textChannel)
|
||||
ulong discordChannelId;
|
||||
string connectionName;
|
||||
string friendlyName;
|
||||
if (channelId == 0)
|
||||
{
|
||||
var channelModel = new ChannelRepresentation
|
||||
connectionName = client.CurrentUser.Username;
|
||||
friendlyName = "(Unmapped accessible channels)";
|
||||
discordChannelId = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var discordChannel = client.GetChannel(channelId);
|
||||
if (!(discordChannel is ITextChannel textChannel))
|
||||
{
|
||||
RealId = discordChannel.Id,
|
||||
IsAdminChannel = channelFromDB.IsAdminChannel == true,
|
||||
ConnectionName = textChannel.Guild.Name,
|
||||
FriendlyName = textChannel.Name,
|
||||
IsPrivateChannel = false,
|
||||
Tag = channelFromDB.Tag
|
||||
};
|
||||
Logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName);
|
||||
return channelModel;
|
||||
Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel?.GetType());
|
||||
return null;
|
||||
}
|
||||
|
||||
discordChannelId = textChannel.Id;
|
||||
connectionName = textChannel.Guild.Name;
|
||||
friendlyName = textChannel.Name;
|
||||
}
|
||||
|
||||
Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel?.GetType());
|
||||
return null;
|
||||
var channelModel = new ChannelRepresentation
|
||||
{
|
||||
RealId = discordChannelId,
|
||||
IsAdminChannel = channelFromDB.IsAdminChannel == true,
|
||||
ConnectionName = connectionName,
|
||||
FriendlyName = friendlyName,
|
||||
IsPrivateChannel = false,
|
||||
Tag = channelFromDB.Tag
|
||||
};
|
||||
Logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName);
|
||||
return channelModel;
|
||||
}
|
||||
|
||||
var enumerator = channels.Select(x => GetModelChannelFromDBChannel(x)).Where(x => x != null).ToList();
|
||||
var enumerator = channels
|
||||
.Select(x => GetModelChannelFromDBChannel(x))
|
||||
.Where(x => x != null).ToList();
|
||||
|
||||
lock (client)
|
||||
lock (mappedChannels)
|
||||
{
|
||||
mappedChannels.Clear();
|
||||
mappedChannels.AddRange(enumerator.Select(x => x.RealId));
|
||||
@@ -260,21 +311,51 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <inheritdoc />
|
||||
public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestOptions = new RequestOptions
|
||||
{
|
||||
CancelToken = cancellationToken,
|
||||
Timeout = 10000 // prevent stupid long hold ups from this
|
||||
};
|
||||
|
||||
Task SendToChannel(IMessageChannel channel) => channel.SendMessageAsync(
|
||||
message,
|
||||
false,
|
||||
null,
|
||||
requestOptions);
|
||||
|
||||
try
|
||||
{
|
||||
if (channelId == 0)
|
||||
{
|
||||
var unmappedTextChannels = client
|
||||
.Guilds
|
||||
.SelectMany(x => x.TextChannels);
|
||||
|
||||
lock (mappedChannels)
|
||||
unmappedTextChannels = unmappedTextChannels.Where(x => !mappedChannels.Contains(x.Id));
|
||||
|
||||
// discord API confirmed weak boned: https://stackoverflow.com/a/52462336
|
||||
var channelCount = 0UL;
|
||||
var tasks = unmappedTextChannels
|
||||
.Select(x =>
|
||||
{
|
||||
++channelCount;
|
||||
return SendToChannel(x);
|
||||
});
|
||||
|
||||
if (channelCount > 0)
|
||||
{
|
||||
Logger.LogTrace("Dispatched to {0} unmapped channels...", channelCount);
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(client.GetChannel(channelId) is IMessageChannel channel))
|
||||
return;
|
||||
|
||||
await channel.SendMessageAsync(
|
||||
message,
|
||||
false,
|
||||
null,
|
||||
new RequestOptions
|
||||
{
|
||||
CancelToken = cancellationToken,
|
||||
Timeout = 10000 // prevent stupid long hold ups from this
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
await SendToChannel(channel).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -379,7 +460,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
? "The deployment completed successfully and will be available at the next server reboot."
|
||||
: "The deployment failed.";
|
||||
|
||||
if (dreamMakerOutput != null)
|
||||
var showDMOutput = outputDisplayType switch
|
||||
{
|
||||
DiscordDMOutputDisplayType.Always => true,
|
||||
DiscordDMOutputDisplayType.Never => false,
|
||||
DiscordDMOutputDisplayType.OnError => errorMessage != null,
|
||||
_ => throw new InvalidOperationException($"Invalid DiscordDMOutputDisplayType: {outputDisplayType}"),
|
||||
};
|
||||
|
||||
if (showDMOutput && dreamMakerOutput != null)
|
||||
builder.AddField(new EmbedFieldBuilder
|
||||
{
|
||||
Name = "DreamMaker Output",
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
@@ -302,7 +303,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
Logger.LogTrace("Exiting listening task...");
|
||||
},
|
||||
cancellationToken,
|
||||
TaskCreationOptions.LongRunning,
|
||||
DefaultIOManager.BlockingTaskCreationOptions,
|
||||
TaskScheduler.Current);
|
||||
|
||||
await nickCheckCompleteTcs.Task.ConfigureAwait(false);
|
||||
@@ -430,7 +431,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
}
|
||||
},
|
||||
cancellationToken,
|
||||
TaskCreationOptions.LongRunning,
|
||||
DefaultIOManager.BlockingTaskCreationOptions,
|
||||
TaskScheduler.Current)
|
||||
.ConfigureAwait(false);
|
||||
await HardDisconnect(cancellationToken).ConfigureAwait(false);
|
||||
@@ -472,7 +473,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
}
|
||||
},
|
||||
cancellationToken,
|
||||
TaskCreationOptions.None,
|
||||
DefaultIOManager.BlockingTaskCreationOptions,
|
||||
TaskScheduler.Current);
|
||||
|
||||
await Task.WhenAny(
|
||||
@@ -545,7 +546,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
@@ -573,7 +574,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
Logger.LogWarning(e, "Unable to send to channel {0}!", channelName);
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<Func<string, string, Task>> SendUpdateMessage(
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Repository
|
||||
@@ -46,7 +47,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
return new LibGit2Sharp.Repository(path);
|
||||
},
|
||||
cancellationToken,
|
||||
TaskCreationOptions.LongRunning,
|
||||
DefaultIOManager.BlockingTaskCreationOptions,
|
||||
TaskScheduler.Current);
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
logger.LogTrace(ex, "Suppressing clone cancellation exception");
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public CredentialsHandler GenerateCredentialsHandler(string username, string password) => (a, b, supportedCredentialTypes) =>
|
||||
|
||||
@@ -375,7 +375,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
}
|
||||
|
||||
libGitRepo.RemoveUntrackedFiles();
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
|
||||
if (result.Status == MergeStatus.Conflicts)
|
||||
{
|
||||
@@ -389,7 +389,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
await Task.Factory.StartNew(() => libGitRepo.Commit(commitMessage, sig, sig, new CommitOptions
|
||||
{
|
||||
PrettifyMessage = true
|
||||
}), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}), cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await eventConsumer.HandleEvent(
|
||||
@@ -420,7 +420,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
libGitRepo.RemoveUntrackedFiles();
|
||||
RawCheckout(committish, progressReporter, cancellationToken);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -453,7 +453,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -492,7 +492,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
libGitRepo.Branches.Remove(branch);
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ResetToOrigin(Action<int> progressReporter, CancellationToken cancellationToken)
|
||||
@@ -530,7 +530,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
OnCheckoutProgress = CheckoutProgressHandler(progressReporter)
|
||||
});
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CopyTo(string path, CancellationToken cancellationToken)
|
||||
@@ -550,7 +550,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return libGitRepo.Head.TrackedBranch.Tip.Sha;
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool?> MergeOrigin(string committerName, string committerEmail, Action<int> progressReporter, CancellationToken cancellationToken)
|
||||
@@ -601,7 +601,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
}
|
||||
|
||||
libGitRepo.RemoveUntrackedFiles();
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
|
||||
if (result.Status == MergeStatus.Conflicts)
|
||||
{
|
||||
@@ -643,7 +643,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
libGitRepo.Config.Set("user.name", committerName);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
libGitRepo.Config.Set("user.email", committerEmail);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
@@ -668,7 +668,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
OnCheckoutProgress = CheckoutProgressHandler(progress => progressReporter(progress / 10))
|
||||
});
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
void FinalReporter(int progress) => progressReporter((int)(((float)progress) / 100 * 90));
|
||||
@@ -711,7 +711,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
logger.LogWarning(e, "Unable to make synchronization push!");
|
||||
return false;
|
||||
}
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -732,7 +732,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
if (libGitRepo.Lookup<Commit>(committish) != null)
|
||||
return true;
|
||||
return false;
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> ShaIsParent(string sha, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
@@ -770,6 +770,6 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
startSha);
|
||||
|
||||
return false;
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
await Task.Factory.StartNew(ReadImpl, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -418,7 +418,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
await Task.Factory.StartNew(WriteImpl, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -436,7 +436,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
await Task.Factory.StartNew(DoCreate, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current).ConfigureAwait(false);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System;
|
||||
@@ -24,7 +24,7 @@ namespace Tgstation.Server.Host.Configuration
|
||||
/// <summary>
|
||||
/// The current <see cref="ConfigVersion"/>.
|
||||
/// </summary>
|
||||
public static readonly Version CurrentConfigVersion = new Version(2, 0, 0);
|
||||
public static readonly Version CurrentConfigVersion = new Version(2, 1, 0);
|
||||
|
||||
/// <summary>
|
||||
/// The default value for <see cref="ServerInformation.MinimumPasswordLength"/>.
|
||||
@@ -87,6 +87,11 @@ namespace Tgstation.Server.Host.Configuration
|
||||
/// </summary>
|
||||
public bool UseBasicWatchdog { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the swagger UI should be made avaiable.
|
||||
/// </summary>
|
||||
public bool HostApiDocumemtation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GeneralConfiguration"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration options pertaining to user security
|
||||
/// </summary>
|
||||
sealed class SecurityConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="SecurityConfiguration"/> resides in
|
||||
/// </summary>
|
||||
public const string Section = "Security";
|
||||
|
||||
/// <summary>
|
||||
/// Default value of <see cref="TokenExpiryMinutes"/>.
|
||||
/// </summary>
|
||||
const uint DefaultTokenExpiryMinutes = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Default value of <see cref="TokenClockSkewMinutes"/>.
|
||||
/// </summary>
|
||||
const uint DefaultTokenClockSkewMinutes = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Default value of <see cref="TokenSigningKeyByteCount"/>.
|
||||
/// </summary>
|
||||
const uint DefaultTokenSigningKeyByteAmount = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of minutes until generated <see cref="Api.Models.Token"/>s expire.
|
||||
/// </summary>
|
||||
public uint TokenExpiryMinutes { get; set; } = DefaultTokenExpiryMinutes;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of minutes to skew the clock for <see cref="Api.Models.Token"/> validation.
|
||||
/// </summary>
|
||||
public uint TokenClockSkewMinutes { get; set; } = DefaultTokenClockSkewMinutes;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of bytes to use in the <see cref="Microsoft.IdentityModel.Tokens.TokenValidationParameters.IssuerSigningKey"/>.
|
||||
/// </summary>
|
||||
public uint TokenSigningKeyByteCount { get; set; } = DefaultTokenSigningKeyByteAmount;
|
||||
|
||||
/// <summary>
|
||||
/// A custom token signing key. Overrides <see cref="TokenSigningKeyByteCount"/>.
|
||||
/// </summary>
|
||||
public string CustomTokenSigningKeyBase64 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ namespace Tgstation.Server.Host.Core
|
||||
// configure configuration
|
||||
services.UseStandardConfig<UpdatesConfiguration>(Configuration);
|
||||
services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
|
||||
services.UseStandardConfig<SecurityConfiguration>(Configuration);
|
||||
|
||||
// enable options which give us config reloading
|
||||
services.AddOptions();
|
||||
@@ -394,7 +395,8 @@ namespace Tgstation.Server.Host.Core
|
||||
// suppress OperationCancelledExceptions, they are just aborted HTTP requests
|
||||
applicationBuilder.UseCancelledRequestSuppression();
|
||||
|
||||
if (hostingEnvironment.IsDevelopment())
|
||||
if (hostingEnvironment.IsDevelopment()
|
||||
|| generalConfiguration.HostApiDocumemtation)
|
||||
{
|
||||
applicationBuilder.UseSwagger();
|
||||
applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4"));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -61,7 +61,7 @@ namespace Tgstation.Server.Host.IO
|
||||
{
|
||||
CheckAvailable();
|
||||
global::System.Console.Read();
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
@@ -97,7 +97,7 @@ namespace Tgstation.Server.Host.IO
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
global::System.Console.WriteLine();
|
||||
return passwordBuilder.ToString();
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
@@ -113,6 +113,6 @@ namespace Tgstation.Server.Host.IO
|
||||
global::System.Console.WriteLine(text);
|
||||
else
|
||||
global::System.Console.Write(text);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
@@ -24,6 +24,11 @@ namespace Tgstation.Server.Host.IO
|
||||
/// </summary>
|
||||
public const int DefaultBufferSize = 4096;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="TaskCreationOptions"/> used to spawn <see cref="Task"/>s for potentially long running, blocking operations.
|
||||
/// </summary>
|
||||
public const TaskCreationOptions BlockingTaskCreationOptions = TaskCreationOptions.None;
|
||||
|
||||
/// <summary>
|
||||
/// Recursively empty a directory
|
||||
/// </summary>
|
||||
@@ -149,7 +154,7 @@ namespace Tgstation.Server.Host.IO
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.CreateDirectory(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.CreateDirectory(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteDirectory(string path, CancellationToken cancellationToken)
|
||||
@@ -162,18 +167,18 @@ namespace Tgstation.Server.Host.IO
|
||||
return Task.Factory.StartNew(
|
||||
() => NormalizeAndDelete(di, cancellationToken),
|
||||
cancellationToken,
|
||||
TaskCreationOptions.LongRunning,
|
||||
BlockingTaskCreationOptions,
|
||||
TaskScheduler.Current);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Delete(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Delete(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path)));
|
||||
@@ -201,7 +206,7 @@ namespace Tgstation.Server.Host.IO
|
||||
}
|
||||
|
||||
return results;
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task MoveFile(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
@@ -211,7 +216,7 @@ namespace Tgstation.Server.Host.IO
|
||||
source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
|
||||
destination = ResolvePath(destination);
|
||||
File.Move(source, destination);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task MoveDirectory(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
@@ -221,7 +226,7 @@ namespace Tgstation.Server.Host.IO
|
||||
source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
|
||||
destination = ResolvePath(destination);
|
||||
Directory.Move(source, destination);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
|
||||
@@ -261,7 +266,7 @@ namespace Tgstation.Server.Host.IO
|
||||
}
|
||||
|
||||
return (IReadOnlyList<string>)results;
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<string>> GetFiles(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
@@ -276,7 +281,7 @@ namespace Tgstation.Server.Host.IO
|
||||
}
|
||||
|
||||
return (IReadOnlyList<string>)results;
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> DownloadFile(Uri url, CancellationToken cancellationToken)
|
||||
@@ -312,7 +317,7 @@ namespace Tgstation.Server.Host.IO
|
||||
using var ms = new MemoryStream(zipFileBytes);
|
||||
using var archive = new ZipArchive(ms, ZipArchiveMode.Read);
|
||||
archive.ExtractToDirectory(path);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PathContainsParentAccess(string path) => path?.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Any(x => x == "..") ?? throw new ArgumentNullException(nameof(path));
|
||||
@@ -323,6 +328,6 @@ namespace Tgstation.Server.Host.IO
|
||||
path = ResolvePath(path ?? throw new ArgumentNullException(nameof(path)));
|
||||
var fileInfo = new FileInfo(path);
|
||||
return new DateTimeOffset(fileInfo.LastWriteTimeUtc);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Mono.Unix;
|
||||
using Mono.Unix;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
@@ -28,6 +28,6 @@ namespace Tgstation.Server.Host.IO
|
||||
fsInfo = new UnixDirectoryInfo(targetPath);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
fsInfo.CreateSymbolicLink(linkPath);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BetterWin32Errors;
|
||||
using BetterWin32Errors;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
@@ -37,6 +37,6 @@ namespace Tgstation.Server.Host.IO
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!NativeMethods.CreateSymbolicLink(linkPath, targetPath, flags))
|
||||
throw new Win32Exception();
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
@@ -31,12 +31,10 @@ namespace Tgstation.Server.Host.Security
|
||||
/// <inheritdoc />
|
||||
public byte[] GetSecureBytes(uint amount)
|
||||
{
|
||||
using (var rng = new RNGCryptoServiceProvider())
|
||||
{
|
||||
var byt = new byte[amount];
|
||||
rng.GetBytes(byt);
|
||||
return byt;
|
||||
}
|
||||
using var rng = new RNGCryptoServiceProvider();
|
||||
var byt = new byte[amount];
|
||||
rng.GetBytes(byt);
|
||||
return byt;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
@@ -6,6 +7,7 @@ using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
@@ -14,24 +16,14 @@ namespace Tgstation.Server.Host.Security
|
||||
/// <inheritdoc />
|
||||
sealed class TokenFactory : ITokenFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Amount of minutes until generated <see cref="Token"/>s expire
|
||||
/// </summary>
|
||||
const uint TokenExpiryMinutes = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of minutes to skew the clock for <see cref="Token"/> validation
|
||||
/// </summary>
|
||||
const uint TokenClockSkewMinutes = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of bytes to use in the <see cref="TokenValidationParameters.IssuerSigningKey"/>
|
||||
/// </summary>
|
||||
const uint TokenSigningKeyByteAmount = 256;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TokenValidationParameters ValidationParameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SecurityConfiguration"/> for the <see cref="TokenFactory"/>.
|
||||
/// </summary>
|
||||
readonly SecurityConfiguration securityConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAsyncDelayer"/> for the <see cref="TokenFactory"/>
|
||||
/// </summary>
|
||||
@@ -43,31 +35,44 @@ namespace Tgstation.Server.Host.Security
|
||||
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/></param>
|
||||
/// <param name="cryptographySuite">The <see cref="ICryptographySuite"/> used for generating the <see cref="ValidationParameters"/></param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> used to generate the issuer name.</param>
|
||||
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="securityConfiguration"/>.</param>
|
||||
public TokenFactory(
|
||||
IAsyncDelayer asyncDelayer,
|
||||
ICryptographySuite cryptographySuite,
|
||||
IAssemblyInformationProvider assemblyInformationProvider)
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions)
|
||||
{
|
||||
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
|
||||
|
||||
if (cryptographySuite == null)
|
||||
throw new ArgumentNullException(nameof(cryptographySuite));
|
||||
if (assemblyInformationProvider == null)
|
||||
throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
|
||||
securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
|
||||
|
||||
var signingKeyBytes = String.IsNullOrWhiteSpace(securityConfiguration.CustomTokenSigningKeyBase64)
|
||||
? cryptographySuite.GetSecureBytes(securityConfiguration.TokenSigningKeyByteCount)
|
||||
: Convert.FromBase64String(securityConfiguration.CustomTokenSigningKeyBase64);
|
||||
|
||||
ValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(cryptographySuite.GetSecureBytes(TokenSigningKeyByteAmount)),
|
||||
IssuerSigningKey = new SymmetricSecurityKey(signingKeyBytes),
|
||||
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = assemblyInformationProvider.Name.Name,
|
||||
ValidIssuer = assemblyInformationProvider.AssemblyName.Name,
|
||||
|
||||
ValidateLifetime = true,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = typeof(Token).Assembly.GetName().Name,
|
||||
|
||||
ClockSkew = TimeSpan.FromMinutes(TokenClockSkewMinutes),
|
||||
ClockSkew = TimeSpan.FromMinutes(securityConfiguration.TokenClockSkewMinutes),
|
||||
|
||||
RequireSignedTokens = true,
|
||||
|
||||
RequireExpirationTime = true
|
||||
};
|
||||
|
||||
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -88,7 +93,7 @@ namespace Tgstation.Server.Host.Security
|
||||
if (nowUnix == lpuUnix)
|
||||
await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var expiry = now.AddMinutes(TokenExpiryMinutes);
|
||||
var expiry = now.AddMinutes(securityConfiguration.TokenExpiryMinutes);
|
||||
var claims = new Claim[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.Value.ToString(CultureInfo.InvariantCulture)),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.DirectoryServices.AccountManagement;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
@@ -90,6 +91,6 @@ namespace Tgstation.Server.Host.Security
|
||||
if (identity == null)
|
||||
throw new InvalidOperationException("Impersonate using a UserPrincipal based WindowsSystemIdentity!");
|
||||
WindowsIdentity.RunImpersonated(identity.AccessToken, action);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.DirectoryServices.AccountManagement;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
@@ -88,7 +89,7 @@ namespace Tgstation.Server.Host.Security
|
||||
return null;
|
||||
return (ISystemIdentity)new WindowsSystemIdentity(principal);
|
||||
},
|
||||
cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ISystemIdentity> CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
@@ -114,6 +115,6 @@ namespace Tgstation.Server.Host.Security
|
||||
using var handle = new SafeAccessTokenHandle(token);
|
||||
return (ISystemIdentity)new WindowsSystemIdentity(
|
||||
new WindowsIdentity(handle.DangerousGetHandle())); // https://github.com/dotnet/corefx/blob/6ed61acebe3214fcf79b4274f2bb9b55c0604a4d/src/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L271
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -613,6 +613,8 @@ namespace Tgstation.Server.Host.Setup
|
||||
if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
|
||||
newGeneralConfiguration.GitHubAccessToken = null;
|
||||
|
||||
newGeneralConfiguration.HostApiDocumemtation = await PromptYesNo("Host API Documentation? (y/n): ", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return newGeneralConfiguration;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Tgstation.Server.Api;
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.System
|
||||
public Version Version { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public AssemblyName Name { get; }
|
||||
public AssemblyName AssemblyName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Path { get; }
|
||||
@@ -29,9 +29,9 @@ namespace Tgstation.Server.Host.System
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
Path = assembly.Location;
|
||||
Name = assembly.GetName();
|
||||
Version = Name.Version.Semver();
|
||||
VersionString = String.Concat(VersionPrefix, '-', Version);
|
||||
AssemblyName = assembly.GetName();
|
||||
Version = AssemblyName.Version.Semver();
|
||||
VersionString = String.Concat(VersionPrefix, "-v", Version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tgstation.Server.Host.System
|
||||
@@ -14,9 +14,9 @@ namespace Tgstation.Server.Host.System
|
||||
string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="AssemblyName"/>.
|
||||
/// Gets the <see cref="global::System.Reflection.AssemblyName"/>.
|
||||
/// </summary>
|
||||
AssemblyName Name { get; }
|
||||
AssemblyName AssemblyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Prefix to <see cref="VersionString"/>.
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
namespace Tgstation.Server.Host.System
|
||||
{
|
||||
@@ -97,7 +98,7 @@ namespace Tgstation.Server.Host.System
|
||||
}
|
||||
},
|
||||
default, // DCT: None available
|
||||
TaskCreationOptions.LongRunning,
|
||||
DefaultIOManager.BlockingTaskCreationOptions,
|
||||
TaskScheduler.Current);
|
||||
|
||||
logger.LogTrace("Created process ID: {0}", Id);
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Management;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
|
||||
namespace Tgstation.Server.Host.System
|
||||
@@ -119,7 +120,7 @@ namespace Tgstation.Server.Host.System
|
||||
throw new Win32Exception();
|
||||
},
|
||||
cancellationToken,
|
||||
TaskCreationOptions.LongRunning,
|
||||
DefaultIOManager.BlockingTaskCreationOptions,
|
||||
TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"General": {
|
||||
"MinimumPasswordLength": 15,
|
||||
"GitHubAccessToken": null,
|
||||
@@ -9,7 +9,8 @@
|
||||
"UseBasicWatchdog": false,
|
||||
"UserLimit": 100,
|
||||
"InstanceLimit": 10,
|
||||
"ValidInstancePaths": null
|
||||
"ValidInstancePaths": null,
|
||||
"HostApiDocumemtation": false
|
||||
},
|
||||
"FileLogging": {
|
||||
"Directory": null,
|
||||
@@ -48,5 +49,11 @@
|
||||
"ResetAdminPassword": false,
|
||||
"ServerVersion": null,
|
||||
"ConnectionString": "Data Source=(local);Initial Catalog=TGS;Integrated Security=True"
|
||||
},
|
||||
"Security": {
|
||||
"TokenExpiryMinutes": 15,
|
||||
"TokenClockSkewMinutes": 1,
|
||||
"TokenSigningKeyByteAmount": 256,
|
||||
"CustomTokenSigningKeyBase64": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
@@ -144,6 +144,7 @@ namespace Tgstation.Server.Host.Setup.Tests
|
||||
"-27",
|
||||
"5000",
|
||||
"fake token",
|
||||
"y",
|
||||
//logging config
|
||||
"no",
|
||||
//cp config
|
||||
@@ -166,6 +167,7 @@ namespace Tgstation.Server.Host.Setup.Tests
|
||||
String.Empty,
|
||||
String.Empty,
|
||||
"n",
|
||||
"n",
|
||||
//logging config
|
||||
"y",
|
||||
"not actually verified because lol mocks /../!@#$%^&*()/..///.",
|
||||
@@ -189,6 +191,7 @@ namespace Tgstation.Server.Host.Setup.Tests
|
||||
String.Empty,
|
||||
String.Empty,
|
||||
"y",
|
||||
"y",
|
||||
"will faile",
|
||||
String.Empty,
|
||||
String.Empty,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -93,7 +93,12 @@ namespace Tgstation.Server.Tests.Instance
|
||||
{
|
||||
var firstBot = new ChatBot
|
||||
{
|
||||
ConnectionString = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN"),
|
||||
ConnectionString =
|
||||
new DiscordConnectionStringBuilder
|
||||
{
|
||||
BotToken = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN"),
|
||||
DMOutputDisplay = DiscordDMOutputDisplayType.OnError
|
||||
}.ToString(),
|
||||
Enabled = false,
|
||||
Name = "r4407",
|
||||
Provider = ChatProvider.Discord,
|
||||
|
||||
Reference in New Issue
Block a user