Skip to content

Commit

Permalink
Merge pull request #401 from wabbajack-tools/file-upload
Browse files Browse the repository at this point in the history
File upload and hosting for WJ
  • Loading branch information
halgari authored Jan 19, 2020
2 parents 2a31c85 + 141ff50 commit e30dcf2
Show file tree
Hide file tree
Showing 44 changed files with 811 additions and 52 deletions.
20 changes: 10 additions & 10 deletions Compression.BSA.Test/Compression.BSA.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
<WarningLevel>4</WarningLevel>
<NoWarn>CS1998</NoWarn>
<WarningsAsErrors>CS4014</WarningsAsErrors>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
Expand All @@ -45,8 +45,8 @@
<WarningLevel>4</WarningLevel>
<NoWarn>CS1998</NoWarn>
<WarningsAsErrors>CS4014</WarningsAsErrors>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
Expand All @@ -57,8 +57,8 @@
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
Expand All @@ -69,8 +69,8 @@
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
Expand Down Expand Up @@ -104,10 +104,10 @@
<Version>2.2.6</Version>
</PackageReference>
<PackageReference Include="MSTest.TestAdapter">
<Version>2.0.0</Version>
<Version>2.1.0-beta2</Version>
</PackageReference>
<PackageReference Include="MSTest.TestFramework">
<Version>2.0.0</Version>
<Version>2.1.0-beta2</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
Expand Down
87 changes: 87 additions & 0 deletions Wabbajack.BuildServer/ApiKeyAuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Wabbajack.BuildServer.Models;

namespace Wabbajack.BuildServer
{

public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "API Key";
public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme;
}

public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private const string ProblemDetailsContentType = "application/problem+json";
private readonly DBContext _db;
private const string ApiKeyHeaderName = "X-Api-Key";

public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
DBContext db) : base(options, logger, encoder, clock)
{
_db = db;
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues))
{
return AuthenticateResult.NoResult();
}

var providedApiKey = apiKeyHeaderValues.FirstOrDefault();

if (apiKeyHeaderValues.Count == 0 || string.IsNullOrWhiteSpace(providedApiKey))
{
return AuthenticateResult.NoResult();
}

var existingApiKey = await ApiKey.Get(_db, providedApiKey);

if (existingApiKey != null)
{
var claims = new List<Claim> {new Claim(ClaimTypes.Name, existingApiKey.Owner)};

claims.AddRange(existingApiKey.Roles.Select(role => new Claim(ClaimTypes.Role, role)));

var identity = new ClaimsIdentity(claims, Options.AuthenticationType);
var identities = new List<ClaimsIdentity> {identity};
var principal = new ClaimsPrincipal(identities);
var ticket = new AuthenticationTicket(principal, Options.Scheme);

return AuthenticateResult.Success(ticket);
}

return AuthenticateResult.Fail("Invalid API Key provided.");
}

protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
Response.ContentType = ProblemDetailsContentType;
await Response.WriteAsync("Unauthorized");
}

protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
Response.StatusCode = 403;
Response.ContentType = ProblemDetailsContentType;
await Response.WriteAsync("forbidden");
}
}
}
5 changes: 5 additions & 0 deletions Wabbajack.BuildServer/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,10 @@ public AppSettings(IConfiguration config)

public string DownloadDir { get; set; }
public string ArchiveDir { get; set; }

public bool MinimalMode { get; set; }

public bool RunFrontEndJobs { get; set; }
public bool RunBackEndJobs { get; set; }
}
}
11 changes: 10 additions & 1 deletion Wabbajack.BuildServer/Controllers/AControllerBase.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using GraphQL;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using Wabbajack.BuildServer.Models;
using Wabbajack.Common;

namespace Wabbajack.BuildServer.Controllers
{
Expand All @@ -15,5 +22,7 @@ protected AControllerBase(ILogger<T> logger, DBContext db)
Db = db;
Logger = logger;
}


}
}
9 changes: 9 additions & 0 deletions Wabbajack.BuildServer/Controllers/Heartbeat.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Wabbajack.BuildServer.Models;
Expand All @@ -27,5 +28,13 @@ public async Task<TimeSpan> GetHeartbeat()
{
return DateTime.Now - _startTime;
}

[HttpGet("only-authenticated")]
[Authorize]
public IActionResult OnlyAuthenticated()
{
var message = $"Hello from {nameof(OnlyAuthenticated)}";
return new ObjectResult(message);
}
}
}
2 changes: 2 additions & 0 deletions Wabbajack.BuildServer/Controllers/Jobs.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
Expand All @@ -10,6 +11,7 @@

namespace Wabbajack.BuildServer.Controllers
{
[Authorize]
[ApiController]
[Route("/jobs")]
public class Jobs : AControllerBase<Jobs>
Expand Down
122 changes: 122 additions & 0 deletions Wabbajack.BuildServer/Controllers/UploadedFiles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Nettle;
using Wabbajack.BuildServer.Models;
using Wabbajack.Common;
using Path = Alphaleonis.Win32.Filesystem.Path;

namespace Wabbajack.BuildServer.Controllers
{
public class UploadedFiles : AControllerBase<UploadedFiles>
{
public UploadedFiles(ILogger<UploadedFiles> logger, DBContext db) : base(logger, db)
{

}

[HttpPut]
[Route("upload_file/{Name}/start")]
public async Task<IActionResult> UploadFileStreaming(string Name)
{
var guid = Guid.NewGuid();
var key = Encoding.UTF8.GetBytes($"{Path.GetFileNameWithoutExtension(Name)}|{guid.ToString()}|{Path.GetExtension(Name)}").ToHex();
System.IO.File.Create(Path.Combine("public", "files", key)).Close();
Utils.Log($"Starting Ingest for {key}");
return Ok(key);
}

static private HashSet<char> HexChars = new HashSet<char>("abcdef1234567890");
[HttpPut]
[Route("upload_file/{Key}/data/{Offset}")]
public async Task<IActionResult> UploadFilePart(string Key, long Offset)
{
if (!Key.All(a => HexChars.Contains(a)))
return BadRequest("NOT A VALID FILENAME");
Utils.Log($"Writing at position {Offset} in ingest file {Key}");
await using (var file = System.IO.File.Open(Path.Combine("public", "files", Key), FileMode.Open, FileAccess.Write))
{
file.Position = Offset;
await Request.Body.CopyToAsync(file);
return Ok(file.Position.ToString());
}
}

[HttpPut]
[Route("upload_file/{Key}/finish")]
public async Task<IActionResult> UploadFileFinish(string Key)
{
var user = User.FindFirstValue(ClaimTypes.Name);
if (!Key.All(a => HexChars.Contains(a)))
return BadRequest("NOT A VALID FILENAME");
var parts = Encoding.UTF8.GetString(Key.FromHex()).Split('|');
var final_name = $"{parts[0]}-{parts[1]}{parts[2]}";
var original_name = $"{parts[0]}{parts[2]}";

var final_path = Path.Combine("public", "files", final_name);
System.IO.File.Move(Path.Combine("public", "files", Key), final_path);
var hash = await final_path.FileHashAsync();

var record = new UploadedFile
{
Id = parts[1],
Hash = hash,
Name = original_name,
Uploader = user,
Size = new FileInfo(final_path).Length
};
await Db.UploadedFiles.InsertOneAsync(record);
return Ok(record.Uri);
}


private static readonly Func<object, string> HandleGetListTemplate = NettleEngine.GetCompiler().Compile(@"
<html><body>
<table>
{{each $.files }}
<tr><td><a href='{{$.Link}}'>{{$.Name}}</a></td><td>{{$.Size}}</td><td>{{$.Date}}</td><td>{{$.Uploader}}</td></tr>
{{/each}}
</table>
</body></html>
");

[HttpGet]
[Route("uploaded_files")]
public async Task<ContentResult> UploadedFilesGet()
{
var files = await Db.UploadedFiles.AsQueryable().OrderByDescending(f => f.UploadDate).ToListAsync();
var response = HandleGetListTemplate(new
{
files = files.Select(file => new
{
Link = file.Uri,
Size = file.Size.ToFileSizeString(),
file.Name,
Date = file.UploadDate,
file.Uploader
})

});
return new ContentResult
{
ContentType = "text/html",
StatusCode = (int) HttpStatusCode.OK,
Content = response
};
}


}
}
58 changes: 58 additions & 0 deletions Wabbajack.BuildServer/Controllers/Users.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using Wabbajack.BuildServer.Models;
using Wabbajack.Common;

namespace Wabbajack.BuildServer.Controllers
{
[Authorize]
[Route("/users")]
public class Users : AControllerBase<Users>
{
public Users(ILogger<Users> logger, DBContext db) : base(logger, db)
{
}

[HttpGet]
[Route("add/{Name}")]
public async Task<string> AddUser(string Name)
{
var user = new ApiKey();
var arr = new byte[128];
new Random().NextBytes(arr);
user.Owner = Name;
user.Key = arr.ToHex();
user.Id = Guid.NewGuid().ToString();
user.Roles = new List<string>();
user.CanUploadLists = new List<string>();

await Db.ApiKeys.InsertOneAsync(user);

return user.Id;
}

[HttpGet]
[Route("export")]
public async Task<string> Export()
{
if (!Directory.Exists("exported_users"))
Directory.CreateDirectory("exported_users");

foreach (var user in await Db.ApiKeys.AsQueryable().ToListAsync())
{
Directory.CreateDirectory(Path.Combine("exported_users", user.Owner));
Alphaleonis.Win32.Filesystem.File.WriteAllText(Path.Combine("exported_users", user.Owner, "author-api-key.txt"), user.Key);
}

return "done";
}

}

}
Loading

0 comments on commit e30dcf2

Please sign in to comment.