Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User input validation for worker names and usernames #11

Merged
merged 7 commits into from
Jan 20, 2025
25 changes: 21 additions & 4 deletions Trell.Engine/Utility/IO/TrellPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ public class TrellPath {
// | RegexOptions.Compiled,
// TimeSpan.FromMilliseconds(100));

readonly static HashSet<char> AllowedPathCharacter = new HashSet<char>();
readonly static HashSet<char> AllowedPathCharacter;
readonly static HashSet<char> ValidFolderNameCharacters = [];
const int MAX_PATH_LENGTH = 4 * 1024;

static TrellPath() {
for (int i = 'a'; i <= 'z'; i++) {
AllowedPathCharacter.Add((char)i);
ValidFolderNameCharacters.Add((char)i);
}

for (int i = '0'; i <= '9'; i++) {
AllowedPathCharacter.Add((char)i);
ValidFolderNameCharacters.Add((char)i);
}

AllowedPathCharacter.Add('_');
ValidFolderNameCharacters.Add('_');

AllowedPathCharacter = new(ValidFolderNameCharacters);
AllowedPathCharacter.Add('/');
AllowedPathCharacter.Add('.');
}
Expand Down Expand Up @@ -94,4 +97,18 @@ public static bool TryParseRelative(string path, [NotNullWhen(true)] out TrellPa

return true;
}

public static bool IsValidNameForFolder(string folderName) {
if (string.IsNullOrWhiteSpace(folderName)) {
return false;
}

for (int i = 0; i < folderName.Length; i++) {
if (!ValidFolderNameCharacters.Contains(folderName[i])) {
return false;
}
}

return true;
}
}
85 changes: 57 additions & 28 deletions Trell/CliCommands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
using Serilog;
using Spectre.Console;
using Spectre.Console.Cli;
using Tomlyn.Syntax;
using Trell.Engine.Extensibility;
using Trell.Engine.Utility.IO;
using static Trell.DirectoryHelper;

namespace Trell.CliCommands;
Expand All @@ -36,10 +38,6 @@ public class InitCommandSettings : CommandSettings {

class InitCommand : AsyncCommand<InitCommandSettings> {
public async override Task<int> ExecuteAsync(CommandContext context, InitCommandSettings settings) {
// TODO: validate user input from all the following prompts, and possibly expand TrellPath's valid characters.
// We use TrellPath.TryParseRelative to navigate to some of these folders, so if the names given contain
// any characters like 'ø' it can fail TrellPath's validation for paths.

var configDir = settings.ConfigDirectory ?? AnsiConsole.Prompt(
new TextPrompt<string>("Please provide a path for where to store configuration data")
.DefaultValue(Directory.GetCurrentDirectory())
Expand Down Expand Up @@ -71,22 +69,42 @@ public async override Task<int> ExecuteAsync(CommandContext context, InitCommand
);
userDataRootDirectory = Path.GetFullPath(userDataRootDirectory);

if (!Directory.Exists(configDir)) {
Directory.CreateDirectory(configDir);
AnsiConsole.WriteLine($"Created {configDir}");
}

if (!Directory.Exists(userDataRootDirectory)) {
Directory.CreateDirectory(userDataRootDirectory);
AnsiConsole.WriteLine($"Created {userDataRootDirectory}");
}

await File.WriteAllTextAsync(configFilePath, $$"""
socket = "server.sock"

var config = TrellConfig.LoadExample();
config.Storage.Path = userDataRootDirectory;
if (configAlreadyExists) {
File.Delete(configFilePath);
}
using var configFs = File.Open(configFilePath, FileMode.CreateNew, FileAccess.ReadWrite);
using var configSw = new StreamWriter(configFs);
if (!config.TryConvertToToml(out var configAsText)) {
throw new TrellException("Unable to convert config to TOML");
}
await configSw.WriteLineAsync(configAsText);
[logger]
type = "Trell.ConsoleLogger"

[storage]
path = {{new StringValueSyntax(userDataRootDirectory)}}

[worker.pool]
size = 10

[worker.limits]
max_startup_duration = "1s"
max_execution_duration = "15m"
grace_period = "10s"

[[Serilog.WriteTo]]
Name = "Console"
Args = {"OutputTemplate" = "[{Timestamp:HH:mm:ss} {ProcessId} {Level:u3}] {Message:lj}{NewLine}{Exception}"}

[Serilog.MinimumLevel]
Default = "Debug"
Override = {"Microsoft" = "Warning"}
"""
);

AnsiConsole.WriteLine(configAlreadyExists ? $"Overwrote {configFilePath}" : $"Created {configFilePath}");

Expand All @@ -99,17 +117,30 @@ public async override Task<int> ExecuteAsync(CommandContext context, InitCommand
);

if (!shouldSkipExample) {
var username = settings.Username ?? AnsiConsole.Prompt(
new TextPrompt<string>("Please provide a username")
.DefaultValue("new_user")
.ShowDefaultValue()
);
const string INVALID_INPUT_MSG = "Name may contain only lowercase letters (a-z), numbers (0-9), and underscore (_)";

var workerName = settings.WorkerName ?? AnsiConsole.Prompt(
new TextPrompt<string>("Please provide a name for a new worker")
.DefaultValue("new_worker")
.ShowDefaultValue()
);
var usernameIsValid = !string.IsNullOrWhiteSpace(settings.Username) && TrellPath.IsValidNameForFolder(settings.Username);
var workerNameIsValid = !string.IsNullOrWhiteSpace(settings.WorkerName) && TrellPath.IsValidNameForFolder(settings.WorkerName);

if (!usernameIsValid || !workerNameIsValid) {
AnsiConsole.WriteLine("Use only lowercase letters (a-z), numbers (0-9), and underscore (_) for the following.");
}
var username = usernameIsValid
? settings.Username!
: AnsiConsole.Prompt(
new TextPrompt<string>("Please provide a username")
.DefaultValue("new_user")
.ShowDefaultValue()
.Validate(TrellPath.IsValidNameForFolder, INVALID_INPUT_MSG)
);
var workerName = workerNameIsValid
? settings.WorkerName!
: AnsiConsole.Prompt(
new TextPrompt<string>("Please provide a name for a new worker")
.DefaultValue("new_worker")
.ShowDefaultValue()
.Validate(TrellPath.IsValidNameForFolder, INVALID_INPUT_MSG)
);

var workerFilePath = Path.GetFullPath("worker.js", GetWorkerSrcPath(userDataRootDirectory, username, workerName));
var workerAlreadyExists = File.Exists(workerFilePath);
Expand All @@ -134,9 +165,7 @@ public async override Task<int> ExecuteAsync(CommandContext context, InitCommand
if (workerAlreadyExists) {
File.Delete(workerFilePath);
}
using var workerFs = File.Open(workerFilePath, FileMode.CreateNew, FileAccess.ReadWrite);
using var workerSw = new StreamWriter(workerFs);
await workerSw.WriteLineAsync("""
await File.WriteAllTextAsync(workerFilePath, """
async function scheduled(event, env, ctx) {
console.log("running scheduled")
}
Expand Down
3 changes: 2 additions & 1 deletion Trell/DirectoryHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
namespace Trell;

static class DirectoryHelper {
static readonly Lazy<string> DEFAULT_USER_DATA_ROOT_DIR_LAZY = new(() => Path.GetFullPath("/Temp/TrellUserData"));
static readonly Lazy<string> DEFAULT_USER_DATA_ROOT_DIR_LAZY = new(
() => Path.GetFullPath("TrellData", Directory.GetCurrentDirectory()));
internal static string DEFAULT_USER_DATA_ROOT_DIR => DEFAULT_USER_DATA_ROOT_DIR_LAZY.Value;

const string USERS_DIR = "users";
Expand Down
1 change: 0 additions & 1 deletion Trell/Trell.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@
<ItemGroup>
<None Include="../LICENSE" Pack="true" PackagePath="/" />
<None Include="../README.md" Pack="true" PackagePath="/" />
<EmbeddedResource Include="Trell.example.toml" />
</ItemGroup>

</Project>
7 changes: 0 additions & 7 deletions Trell/TrellConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ public static TrellConfig LoadToml(string sourcePath) {
return ParseToml(text, sourcePath);
}

public static TrellConfig LoadExample() {
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Trell.Trell.example.toml")!;
using var sr = new StreamReader(stream);
var text = sr.ReadToEnd();
return ParseToml(text);
}

static TrellConfig ParseToml(string rawText, string? sourcePath = null) {
var syntax = Tomlyn.Toml.Parse(rawText, sourcePath);
var config = Tomlyn.Toml.ToModel<TrellConfig>(syntax, options: TOML_OPTIONS);
Expand Down
Loading