chat: add hello controller
This commit is contained in:
parent
6f6e19bbb5
commit
7b4cbd4fb7
12 changed files with 114 additions and 53 deletions
45
Foxchat.Chat/Controllers/HelloController.cs
Normal file
45
Foxchat.Chat/Controllers/HelloController.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using Foxchat.Chat.Database;
|
||||
using Foxchat.Chat.Database.Models;
|
||||
using Foxchat.Chat.Middleware;
|
||||
using Foxchat.Core.Extensions;
|
||||
using Foxchat.Core.Federation;
|
||||
using Foxchat.Core.Models.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using ApiError = Foxchat.Core.ApiError;
|
||||
|
||||
namespace Foxchat.Chat.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Unauthenticated]
|
||||
[Route("/_fox/chat/hello")]
|
||||
public class HelloController(
|
||||
ILogger logger,
|
||||
ChatContext db,
|
||||
InstanceConfig config,
|
||||
RequestSigningService requestSigningService)
|
||||
: ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Hello([FromBody] Hello.HelloRequest req)
|
||||
{
|
||||
var node = await requestSigningService.RequestAsync<Hello.NodeInfo>(HttpMethod.Get, req.Host,
|
||||
"/_fox/ident/node");
|
||||
|
||||
if (!HttpContext.ExtractRequestData(out var signature, out var domain, out var signatureData))
|
||||
throw new ApiError.IncomingFederationError("This endpoint requires signed requests.");
|
||||
|
||||
if (!requestSigningService.VerifySignature(node.PublicKey, signature, signatureData))
|
||||
throw new ApiError.IncomingFederationError("Signature is not valid.");
|
||||
|
||||
var instance = await db.GetInstanceAsync();
|
||||
db.IdentityInstances.Add(new IdentityInstance
|
||||
{
|
||||
Domain = req.Host,
|
||||
BaseUrl = $"https://{req.Host}",
|
||||
PublicKey = node.PublicKey
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new Hello.HelloResponse(instance.PublicKey, config.Domain));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
using Foxchat.Chat.Database;
|
||||
using Foxchat.Chat.Database.Models;
|
||||
using Foxchat.Core;
|
||||
using Foxchat.Core.Extensions;
|
||||
using Foxchat.Core.Federation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
@ -20,7 +21,7 @@ public class AuthenticationMiddleware(ILogger logger, ChatContext db, RequestSig
|
|||
return;
|
||||
}
|
||||
|
||||
if (!ExtractRequestData(ctx, out var signature, out var domain, out var signatureData))
|
||||
if (!ctx.ExtractRequestData(out var signature, out var domain, out var signatureData))
|
||||
throw new ApiError.IncomingFederationError("This endpoint requires signed requests.");
|
||||
|
||||
var instance = await GetInstanceAsync(domain);
|
||||
|
@ -38,37 +39,6 @@ public class AuthenticationMiddleware(ILogger logger, ChatContext db, RequestSig
|
|||
return await db.IdentityInstances.FirstOrDefaultAsync(i => i.Domain == domain)
|
||||
?? throw new ApiError.IncomingFederationError("Remote instance is not known.");
|
||||
}
|
||||
|
||||
private bool ExtractRequestData(HttpContext ctx, out string signature, out string domain, out SignatureData data)
|
||||
{
|
||||
signature = string.Empty;
|
||||
domain = string.Empty;
|
||||
data = SignatureData.Empty;
|
||||
|
||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SIGNATURE_HEADER, out var encodedSignature))
|
||||
return false;
|
||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.DATE_HEADER, out var date))
|
||||
return false;
|
||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SERVER_HEADER, out var server))
|
||||
return false;
|
||||
var time = RequestSigningService.ParseTime(date.ToString());
|
||||
string? userId = null;
|
||||
if (ctx.Request.Headers.TryGetValue(RequestSigningService.USER_HEADER, out var userIdHeader))
|
||||
userId = userIdHeader;
|
||||
var host = ctx.Request.Headers.Host.ToString();
|
||||
|
||||
signature = encodedSignature.ToString();
|
||||
domain = server.ToString();
|
||||
data = new SignatureData(
|
||||
time,
|
||||
host,
|
||||
ctx.Request.Path,
|
||||
(int?)ctx.Request.Headers.ContentLength,
|
||||
userId
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
|
|
|
@ -3,6 +3,7 @@ using Serilog;
|
|||
using Foxchat.Core;
|
||||
using Foxchat.Chat;
|
||||
using Foxchat.Chat.Database;
|
||||
using Foxchat.Chat.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
@ -33,6 +34,7 @@ builder.Services
|
|||
|
||||
builder.Services
|
||||
.AddCoreServices<ChatContext>()
|
||||
.AddCustomMiddleware()
|
||||
.AddEndpointsApiExplorer()
|
||||
.AddSwaggerGen();
|
||||
|
||||
|
@ -43,8 +45,7 @@ app.UseRouting();
|
|||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.UseCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseCustomMiddleware();
|
||||
app.MapControllers();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
|
|
|
@ -16,11 +16,11 @@ public abstract class IDatabaseContext : DbContext
|
|||
var publicKey = rsa.ExportRSAPublicKeyPem();
|
||||
var privateKey = rsa.ExportRSAPrivateKeyPem();
|
||||
|
||||
await Instance.AddAsync(new Instance
|
||||
Instance.Add(new Instance
|
||||
{
|
||||
PublicKey = publicKey!,
|
||||
PrivateKey = privateKey!,
|
||||
}, ct);
|
||||
});
|
||||
|
||||
await SaveChangesAsync(ct);
|
||||
return true;
|
||||
|
|
38
Foxchat.Core/Extensions/HttpContextExtensions.cs
Normal file
38
Foxchat.Core/Extensions/HttpContextExtensions.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using Foxchat.Core.Federation;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Foxchat.Core.Extensions;
|
||||
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
public static bool ExtractRequestData(this HttpContext ctx, out string signature, out string domain, out SignatureData data)
|
||||
{
|
||||
signature = string.Empty;
|
||||
domain = string.Empty;
|
||||
data = SignatureData.Empty;
|
||||
|
||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SIGNATURE_HEADER, out var encodedSignature))
|
||||
return false;
|
||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.DATE_HEADER, out var date))
|
||||
return false;
|
||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SERVER_HEADER, out var server))
|
||||
return false;
|
||||
var time = RequestSigningService.ParseTime(date.ToString());
|
||||
string? userId = null;
|
||||
if (ctx.Request.Headers.TryGetValue(RequestSigningService.USER_HEADER, out var userIdHeader))
|
||||
userId = userIdHeader;
|
||||
var host = ctx.Request.Headers.Host.ToString();
|
||||
|
||||
signature = encodedSignature.ToString();
|
||||
domain = server.ToString();
|
||||
data = new SignatureData(
|
||||
time,
|
||||
host,
|
||||
ctx.Request.Path,
|
||||
(int?)ctx.Request.Headers.ContentLength,
|
||||
userId
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,6 @@ public static class Hello
|
|||
{
|
||||
public record HelloRequest(string Host);
|
||||
public record HelloResponse(string PublicKey, string Host);
|
||||
public record NodeInfo(string Software, string PublicKey);
|
||||
public record NodeInfo(NodeSoftware Software, string PublicKey);
|
||||
public record NodeSoftware(string Name, string? Version);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using Foxchat.Core;
|
||||
using Foxchat.Core.Models.Http;
|
||||
using Foxchat.Identity.Database;
|
||||
using Foxchat.Identity.Services;
|
||||
|
@ -7,15 +8,16 @@ namespace Foxchat.Identity.Controllers;
|
|||
|
||||
[ApiController]
|
||||
[Route("/_fox/ident/node")]
|
||||
public class NodeController(IdentityContext db, ChatInstanceResolverService chatInstanceResolverService) : ControllerBase
|
||||
public class NodeController(IdentityContext db, ChatInstanceResolverService chatInstanceResolverService)
|
||||
: ControllerBase
|
||||
{
|
||||
public const string SOFTWARE_NAME = "Foxchat.NET.Identity";
|
||||
private const string SoftwareName = "Foxchat.NET.Identity";
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetNode()
|
||||
{
|
||||
var instance = await db.GetInstanceAsync();
|
||||
return Ok(new Hello.NodeInfo(SOFTWARE_NAME, instance.PublicKey));
|
||||
return Ok(new Hello.NodeInfo(new Hello.NodeSoftware(SoftwareName, BuildInfo.Version), instance.PublicKey));
|
||||
}
|
||||
|
||||
[HttpGet("{domain}")]
|
||||
|
|
|
@ -17,7 +17,7 @@ public class AppsController(ILogger logger, IdentityContext db) : ControllerBase
|
|||
public async Task<IActionResult> CreateApplication([FromBody] Apps.CreateRequest req)
|
||||
{
|
||||
var app = Application.Create(req.Name, req.Scopes, req.RedirectUris);
|
||||
await db.AddAsync(app);
|
||||
db.Add(app);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
logger.Information("Created new application {Name} with ID {Id} and client ID {ClientId}", app.Name, app.Id, app.ClientId);
|
||||
|
|
|
@ -21,10 +21,13 @@ public class PasswordAuthController(ILogger logger, IdentityContext db, IClock c
|
|||
public async Task<IActionResult> Register([FromBody] RegisterRequest req)
|
||||
{
|
||||
var app = HttpContext.GetApplicationOrThrow();
|
||||
var appToken = HttpContext.GetToken() ?? throw new UnreachableException(); // GetApplicationOrThrow already gets the token and throws if it's null
|
||||
var appToken =
|
||||
HttpContext.GetToken() ??
|
||||
throw new UnreachableException(); // GetApplicationOrThrow already gets the token and throws if it's null
|
||||
|
||||
if (req.Scopes.Except(appToken.Scopes).Any())
|
||||
throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", req.Scopes.Except(appToken.Scopes));
|
||||
throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token",
|
||||
req.Scopes.Except(appToken.Scopes));
|
||||
|
||||
var acct = new Account
|
||||
{
|
||||
|
@ -33,12 +36,12 @@ public class PasswordAuthController(ILogger logger, IdentityContext db, IClock c
|
|||
Role = Account.AccountRole.User
|
||||
};
|
||||
|
||||
await db.AddAsync(acct);
|
||||
db.Add(acct);
|
||||
var hashedPassword = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password));
|
||||
acct.Password = hashedPassword;
|
||||
// TODO: make token expiry configurable
|
||||
var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365));
|
||||
await db.AddAsync(token);
|
||||
db.Add(token);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr));
|
||||
|
@ -51,10 +54,11 @@ public class PasswordAuthController(ILogger logger, IdentityContext db, IClock c
|
|||
var appToken = HttpContext.GetToken() ?? throw new UnreachableException();
|
||||
|
||||
if (req.Scopes.Except(appToken.Scopes).Any())
|
||||
throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", req.Scopes.Except(appToken.Scopes));
|
||||
throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token",
|
||||
req.Scopes.Except(appToken.Scopes));
|
||||
|
||||
var acct = await db.Accounts.FirstOrDefaultAsync(a => a.Email == req.Email)
|
||||
?? throw new ApiError.NotFound("No user with that email found, or password is incorrect");
|
||||
?? throw new ApiError.NotFound("No user with that email found, or password is incorrect");
|
||||
|
||||
var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(acct, acct.Password, req.Password));
|
||||
if (pwResult == PasswordVerificationResult.Failed)
|
||||
|
@ -62,15 +66,16 @@ public class PasswordAuthController(ILogger logger, IdentityContext db, IClock c
|
|||
if (pwResult == PasswordVerificationResult.SuccessRehashNeeded)
|
||||
acct.Password = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password));
|
||||
|
||||
|
||||
var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365));
|
||||
await db.AddAsync(token);
|
||||
db.Add(token);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr));
|
||||
}
|
||||
|
||||
public record RegisterRequest(string Username, string Password, string Email, string[] Scopes);
|
||||
|
||||
public record LoginRequest(string Email, string Password, string[] Scopes);
|
||||
|
||||
public record AuthResponse(Ulid Id, string Username, string Email, string Token);
|
||||
}
|
|
@ -41,7 +41,7 @@ public class TokenController(ILogger logger, IdentityContext db, IClock clock) :
|
|||
var expiry = clock.GetCurrentInstant() + Duration.FromDays(365);
|
||||
var (token, tokenObj) = Token.Create(null, app, scopes, expiry);
|
||||
|
||||
await db.AddAsync(tokenObj);
|
||||
db.Add(tokenObj);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
logger.Debug("Created token with scopes {Scopes} for application {ApplicationId}", scopes, app.Id);
|
||||
|
|
|
@ -31,7 +31,7 @@ public class ChatInstanceResolverService(ILogger logger, RequestSigningService r
|
|||
PublicKey = resp.PublicKey,
|
||||
Status = ChatInstance.InstanceStatus.Active,
|
||||
};
|
||||
await db.AddAsync(instance);
|
||||
db.Add(instance);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return instance;
|
||||
|
|
Loading…
Reference in a new issue