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}")] | ||||
|  | @ -24,4 +26,4 @@ public class NodeController(IdentityContext db, ChatInstanceResolverService chat | |||
|         var instance = await chatInstanceResolverService.ResolveChatInstanceAsync(domain); | ||||
|         return Ok(instance); | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | @ -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,26 +54,28 @@ 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) | ||||
|             throw new ApiError.NotFound("No user with that email found, or password is incorrect"); | ||||
|         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…
	
	Add table
		Add a link
		
	
		Reference in a new issue