Compare commits
3 commits
v2
...
Applicatio
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2b85ffd93b | ||
![]() |
c0533d78fb | ||
![]() |
8dc1130d2a |
20 changed files with 754 additions and 2 deletions
|
@ -7,6 +7,7 @@
|
|||
<entry key="Common:buildConfiguration" value="Debug" />
|
||||
<entry key="Common:noBuild" value="false" />
|
||||
<entry key="Common:outputFolder" value="App/Database/Migrations" />
|
||||
<entry key="Common:useDefaultConnection" value="true" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
|
|
6
Moonlight/.idea/.gitignore
generated
vendored
Normal file
6
Moonlight/.idea/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
1
Moonlight/.idea/.name
generated
Normal file
1
Moonlight/.idea/.name
generated
Normal file
|
@ -0,0 +1 @@
|
|||
data.sqlite
|
6
Moonlight/.idea/vcs.xml
generated
Normal file
6
Moonlight/.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
10
Moonlight/App/Api/AbstractRequest.cs
Normal file
10
Moonlight/App/Api/AbstractRequest.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Moonlight.App.Api;
|
||||
|
||||
public abstract class AbstractRequest
|
||||
{
|
||||
public IServiceProvider ServiceProvider { get; set; }
|
||||
public ApiUserContext? Context { get; set; }
|
||||
public abstract void ReadData(RequestDataContext dataContext);
|
||||
public abstract Task ProcessRequest();
|
||||
public abstract ResponseDataBuilder CreateResponse(ResponseDataBuilder builder);
|
||||
}
|
63
Moonlight/App/Api/ApiManagementService.cs
Normal file
63
Moonlight/App/Api/ApiManagementService.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
using System.Net.WebSockets;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Moonlight.App.Api;
|
||||
|
||||
public class ApiManagementService
|
||||
{
|
||||
public Dictionary<int, Type> Requests;
|
||||
public List<ApiUserContext> Contexts;
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
|
||||
public ApiManagementService(IServiceProvider serviceProvider)
|
||||
{
|
||||
Requests = new Dictionary<int, Type>();
|
||||
Contexts = new List<ApiUserContext>();
|
||||
ServiceProvider = serviceProvider;
|
||||
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
var types = assembly.ExportedTypes;
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
var attribute = type.GetCustomAttribute<ApiRequestAttribute>();
|
||||
|
||||
if(attribute == null)
|
||||
continue;
|
||||
|
||||
var id = attribute.Id;
|
||||
Requests[id] = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AbstractRequest GetRequest(int id, ApiUserContext context)
|
||||
{
|
||||
var type = Requests[id];
|
||||
var obj = Activator.CreateInstance(type) as AbstractRequest;
|
||||
obj!.Context = context;
|
||||
obj!.ServiceProvider = ServiceProvider.CreateScope().ServiceProvider;
|
||||
|
||||
return obj!;
|
||||
}
|
||||
|
||||
public async Task HandleRequest(ApiUserContext context, byte[] data)
|
||||
{
|
||||
var rqd = new RequestDataContext(data);
|
||||
var id = rqd.ReadInt();
|
||||
var request = GetRequest(id, context);
|
||||
|
||||
request.ReadData(rqd);
|
||||
await request.ProcessRequest();
|
||||
|
||||
var rbd = new ResponseDataBuilder();
|
||||
rbd = request.CreateResponse(rbd);
|
||||
|
||||
CancellationToken t = new CancellationToken();
|
||||
var bytes = rbd.ToBytes();
|
||||
await context.WebSocket.SendAsync(bytes, WebSocketMessageType.Binary, true, t);
|
||||
}
|
||||
}
|
11
Moonlight/App/Api/ApiRequestAttribute.cs
Normal file
11
Moonlight/App/Api/ApiRequestAttribute.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace Moonlight.App.Api;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class ApiRequestAttribute : Attribute
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public ApiRequestAttribute(int id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
15
Moonlight/App/Api/ApiUserContext.cs
Normal file
15
Moonlight/App/Api/ApiUserContext.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System.Net.WebSockets;
|
||||
using Moonlight.App.Database.Entities;
|
||||
|
||||
namespace Moonlight.App.Api;
|
||||
|
||||
public class ApiUserContext
|
||||
{
|
||||
public ApiUserContext(WebSocket webSocket)
|
||||
{
|
||||
WebSocket = webSocket;
|
||||
}
|
||||
|
||||
public User? User { get; set; }
|
||||
public WebSocket WebSocket { get; }
|
||||
}
|
51
Moonlight/App/Api/RequestDataContext.cs
Normal file
51
Moonlight/App/Api/RequestDataContext.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace Moonlight.App.Api;
|
||||
|
||||
public class RequestDataContext
|
||||
{
|
||||
private List<byte> Data;
|
||||
|
||||
public RequestDataContext(byte[] data)
|
||||
{
|
||||
Data = data.ToList();
|
||||
}
|
||||
|
||||
public int ReadInt()
|
||||
{
|
||||
var bytes = Data.Take(4).ToList();
|
||||
Data.RemoveRange(0, 4);
|
||||
|
||||
if (BitConverter.IsLittleEndian) // because of java (the app needing the api is written in java/kotlin) we need to use big endian
|
||||
{
|
||||
bytes.Reverse();
|
||||
}
|
||||
|
||||
return BitConverter.ToInt32(bytes.ToArray());
|
||||
}
|
||||
|
||||
public byte ReadByte()
|
||||
{
|
||||
var b = Data[0];
|
||||
Data.RemoveAt(0);
|
||||
return b;
|
||||
}
|
||||
|
||||
public bool ReadBoolean()
|
||||
{
|
||||
var b = ReadByte();
|
||||
|
||||
return b == 255;
|
||||
}
|
||||
|
||||
public String ReadString()
|
||||
{
|
||||
var len = ReadInt();
|
||||
|
||||
var bytes = Data.Take(len).ToList();
|
||||
Data.RemoveRange(0, len);
|
||||
|
||||
return Encoding.UTF8.GetString(bytes.ToArray());
|
||||
}
|
||||
}
|
141
Moonlight/App/Api/Requests/Auth/CredentialBasedLoginRequest.cs
Normal file
141
Moonlight/App/Api/Requests/Auth/CredentialBasedLoginRequest.cs
Normal file
|
@ -0,0 +1,141 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Helpers;
|
||||
using Moonlight.App.Models.Abstractions;
|
||||
using Moonlight.App.Models.Enums;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services.Utils;
|
||||
using OtpNet;
|
||||
|
||||
namespace Moonlight.App.Api.Requests.Auth;
|
||||
|
||||
[ApiRequest(3)]
|
||||
public class CredentialBasedLoginRequest : AbstractRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Code { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
public bool RequireTotp { get; set; }
|
||||
/// <summary>
|
||||
/// 0: all fine
|
||||
/// 1: wrong credentials
|
||||
/// 2: Totp enabled
|
||||
/// 3: TotpKey missing
|
||||
/// 4: wrong totp code
|
||||
/// </summary>
|
||||
public int ErrorId { get; set; }
|
||||
public string Token { get; set; } = "";
|
||||
public override ResponseDataBuilder CreateResponse(ResponseDataBuilder builder)
|
||||
{
|
||||
builder.WriteBoolean(Success);
|
||||
builder.WriteBoolean(RequireTotp);
|
||||
builder.WriteInt(ErrorId);
|
||||
builder.WriteString(Token);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public override async Task ProcessRequest()
|
||||
{
|
||||
var userRepository = ServiceProvider.GetService<Repository<User>>();
|
||||
|
||||
var user = userRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Email == Email);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
Success = false;
|
||||
RequireTotp = false;
|
||||
ErrorId = 1;
|
||||
Token = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HashHelper.Verify(Password, user.Password))
|
||||
{
|
||||
Success = false;
|
||||
RequireTotp = false;
|
||||
ErrorId = 1;
|
||||
Token = "";
|
||||
return;
|
||||
}
|
||||
|
||||
var flags = new FlagStorage(user.Flags); // Construct FlagStorage to check for 2fa
|
||||
|
||||
if (!flags[UserFlag.TotpEnabled])
|
||||
{
|
||||
// No 2fa found on this user so were done here
|
||||
Success = true;
|
||||
RequireTotp = false;
|
||||
ErrorId = 0;
|
||||
Token = await GenerateToken(user);
|
||||
Context!.User = user;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we reach this point, 2fa is enabled so we need to continue validating
|
||||
|
||||
if (string.IsNullOrEmpty(Code))
|
||||
{
|
||||
// This will show an additional 2fa login field
|
||||
Success = false;
|
||||
RequireTotp = true;
|
||||
ErrorId = 2;
|
||||
Token = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.TotpKey == null)
|
||||
{
|
||||
// Hopefully we will never fulfill this check ;)
|
||||
Success = false;
|
||||
RequireTotp = false;
|
||||
ErrorId = 3;
|
||||
Token = "";
|
||||
return;
|
||||
throw new DisplayException("2FA key is missing. Please contact the support to fix your account");
|
||||
}
|
||||
|
||||
// Calculate server side code
|
||||
var totp = new Totp(Base32Encoding.ToBytes(user.TotpKey));
|
||||
var codeServerSide = totp.ComputeTotp();
|
||||
|
||||
if (codeServerSide == Code)
|
||||
{
|
||||
Success = true;
|
||||
RequireTotp = false;
|
||||
ErrorId = 0;
|
||||
Token = await GenerateToken(user);
|
||||
Context!.User = user;
|
||||
return;
|
||||
}
|
||||
|
||||
Success = false;
|
||||
RequireTotp = false;
|
||||
ErrorId = 4;
|
||||
Token = "";
|
||||
}
|
||||
|
||||
public async Task<string> GenerateToken(User user)
|
||||
{
|
||||
var jwtService = ServiceProvider.GetService<JwtService>();
|
||||
|
||||
var token = await jwtService.Create(data =>
|
||||
{
|
||||
data.Add("userId", user.Id.ToString());
|
||||
data.Add("issuedAt", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString());
|
||||
}, TimeSpan.FromDays(365));
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public override void ReadData(RequestDataContext dataContext)
|
||||
{
|
||||
Email = dataContext.ReadString();
|
||||
Password = dataContext.ReadString();
|
||||
Code = dataContext.ReadString();
|
||||
}
|
||||
}
|
50
Moonlight/App/Api/Requests/Auth/IsEmailVerifiedRequest.cs
Normal file
50
Moonlight/App/Api/Requests/Auth/IsEmailVerifiedRequest.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Models.Enums;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services;
|
||||
using Moonlight.App.Services.Users;
|
||||
|
||||
namespace Moonlight.App.Api.Requests.Auth;
|
||||
|
||||
[ApiRequest(5)]
|
||||
public class IsEmailVerifiedRequest : AbstractRequest
|
||||
{
|
||||
public bool SendMail { get; set; }
|
||||
public bool MailVerified { get; set; }
|
||||
|
||||
public override async Task ProcessRequest()
|
||||
{
|
||||
if(Context.User == null)
|
||||
return;
|
||||
|
||||
var userRepository = ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
Context.User = userRepository.Get().Where(x => x.Id == Context.User.Id).ToArray()[0];
|
||||
|
||||
if (SendMail && Context.User != null)
|
||||
{
|
||||
var userAuthService = ServiceProvider.GetRequiredService<UserAuthService>();
|
||||
await userAuthService.SendVerification(Context!.User);
|
||||
}
|
||||
|
||||
var configService = ServiceProvider.GetRequiredService<ConfigService>();
|
||||
var reqEmailVerify = configService.Get().Security.EnableEmailVerify;
|
||||
if (Context?.User?.Flags!.Contains(UserFlag.MailVerified.ToString()) ?? false)
|
||||
MailVerified = true;
|
||||
else
|
||||
MailVerified = !reqEmailVerify;
|
||||
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
public override ResponseDataBuilder CreateResponse(ResponseDataBuilder builder)
|
||||
{
|
||||
builder.WriteBoolean(MailVerified);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public override void ReadData(RequestDataContext dataContext)
|
||||
{
|
||||
SendMail = dataContext.ReadBoolean();
|
||||
}
|
||||
}
|
181
Moonlight/App/Api/Requests/Auth/RegisterRequest.cs
Normal file
181
Moonlight/App/Api/Requests/Auth/RegisterRequest.cs
Normal file
|
@ -0,0 +1,181 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Event;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Extensions;
|
||||
using Moonlight.App.Helpers;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services;
|
||||
using Moonlight.App.Services.Utils;
|
||||
|
||||
namespace Moonlight.App.Api.Requests.Auth;
|
||||
|
||||
[ApiRequest(4)]
|
||||
public class RegisterRequest: AbstractRequest
|
||||
{
|
||||
[Required(ErrorMessage = "9")]
|
||||
[EmailAddress(ErrorMessage = "10")]
|
||||
public string Email { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "8")]
|
||||
[MinLength(7, ErrorMessage = "7")]
|
||||
[MaxLength(20, ErrorMessage = "7")]
|
||||
[RegularExpression("^[a-z][a-z0-9]*$", ErrorMessage = "6")]
|
||||
public string Username { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "4")]
|
||||
[MinLength(8, ErrorMessage = "5")]
|
||||
[MaxLength(256, ErrorMessage = "5")]
|
||||
public string Password { get; set; } = "";
|
||||
public string PasswordConfirm { get; set; } = "";
|
||||
|
||||
public bool Success { get; set; }
|
||||
public bool RequireEmailVerify { get; set; }
|
||||
/// <summary>
|
||||
/// Error Codes:
|
||||
/// - 0 all successful
|
||||
/// - 1 email exists
|
||||
/// - 2 username exists
|
||||
/// - 3 passwords do not match
|
||||
/// - 4 password needs to be provided
|
||||
/// - 5 password needs to be between 8 and 256 characters
|
||||
/// - 6 Usernames can only contain lowercase characters and numbers
|
||||
/// - 7 username has to be between 7 and 20 chars
|
||||
/// - 8 username required
|
||||
/// - 9 email required
|
||||
/// - 10 email invalid
|
||||
/// </summary>
|
||||
public int ErrorCode { get; set; }
|
||||
public string Token { get; set; } = "";
|
||||
public override ResponseDataBuilder CreateResponse(ResponseDataBuilder builder)
|
||||
{
|
||||
builder.WriteBoolean(Success);
|
||||
builder.WriteBoolean(RequireEmailVerify);
|
||||
builder.WriteInt(ErrorCode);
|
||||
builder.WriteString(Token);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public override void ReadData(RequestDataContext dataContext)
|
||||
{
|
||||
Email = dataContext.ReadString();
|
||||
Username = dataContext.ReadString();
|
||||
Password = dataContext.ReadString();
|
||||
PasswordConfirm = dataContext.ReadString();
|
||||
}
|
||||
|
||||
public override async Task ProcessRequest()
|
||||
{
|
||||
var userRepository = ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
var configService = ServiceProvider.GetRequiredService<ConfigService>();
|
||||
|
||||
var reqEmailVerify = configService.Get().Security.EnableEmailVerify;
|
||||
// Event though we have form validation i want to
|
||||
// ensure that at least these basic formatting things are done
|
||||
Email = Email.ToLower().Trim();
|
||||
Username = Username.ToLower().Trim();
|
||||
|
||||
if (PasswordConfirm != Password)
|
||||
{
|
||||
Token = "";
|
||||
Success = false;
|
||||
RequireEmailVerify = false;
|
||||
ErrorCode = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsPropertyValid(this.GetProperty(x => x.Email)!, out var errorCd))
|
||||
{
|
||||
Token = "";
|
||||
Success = false;
|
||||
RequireEmailVerify = false;
|
||||
ErrorCode = errorCd;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsPropertyValid(this.GetProperty(x => x.Password)!, out var errorCd1))
|
||||
{
|
||||
Token = "";
|
||||
Success = false;
|
||||
RequireEmailVerify = false;
|
||||
ErrorCode = errorCd1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsPropertyValid(this.GetProperty(x => x.Username)!, out var errorCd2))
|
||||
{
|
||||
Token = "";
|
||||
Success = false;
|
||||
RequireEmailVerify = false;
|
||||
ErrorCode = errorCd2;
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplication or username and/or email
|
||||
if (userRepository.Get().Any(x => x.Email == Email))
|
||||
{
|
||||
Token = "";
|
||||
Success = false;
|
||||
RequireEmailVerify = false;
|
||||
ErrorCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (userRepository.Get().Any(x => x.Username == Username))
|
||||
{
|
||||
Token = "";
|
||||
Success = false;
|
||||
RequireEmailVerify = false;
|
||||
ErrorCode = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
var user = new User()
|
||||
{
|
||||
Username = Username,
|
||||
Email = Email,
|
||||
Password = HashHelper.HashToString(Password)
|
||||
};
|
||||
|
||||
var result = userRepository.Add(user);
|
||||
|
||||
await Events.OnUserRegistered.InvokeAsync(result);
|
||||
|
||||
Token = await GenerateToken(user);
|
||||
Success = true;
|
||||
RequireEmailVerify = reqEmailVerify;
|
||||
ErrorCode = 0;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateToken(User user)
|
||||
{
|
||||
var jwtService = ServiceProvider.GetService<JwtService>();
|
||||
|
||||
var token = await jwtService.Create(data =>
|
||||
{
|
||||
data.Add("userId", user.Id.ToString());
|
||||
data.Add("issuedAt", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString());
|
||||
}, TimeSpan.FromDays(365));
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private bool IsPropertyValid(PropertyInfo property, out int errorCode)
|
||||
{
|
||||
var attribs = property.GetCustomAttributes<ValidationAttribute>();
|
||||
|
||||
foreach (var a in attribs)
|
||||
{
|
||||
if (!a.IsValid(property.GetValue(this)))
|
||||
{
|
||||
errorCode = int.Parse(a.ErrorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
errorCode = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
65
Moonlight/App/Api/Requests/Auth/TokenBasedLoginRequest.cs
Normal file
65
Moonlight/App/Api/Requests/Auth/TokenBasedLoginRequest.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services.Utils;
|
||||
|
||||
namespace Moonlight.App.Api.Requests.Auth;
|
||||
|
||||
[ApiRequest(2)]
|
||||
public class TokenBasedLoginRequest: AbstractRequest
|
||||
{
|
||||
private String Token { get; set; }
|
||||
private bool Success { get; set; } = false;
|
||||
public override ResponseDataBuilder CreateResponse(ResponseDataBuilder builder)
|
||||
{
|
||||
builder.WriteBoolean(Success);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public override async Task ProcessRequest()
|
||||
{
|
||||
var jwtService = ServiceProvider.GetRequiredService<JwtService>();
|
||||
var userRepository = ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
if (string.IsNullOrEmpty(Token))
|
||||
return;
|
||||
|
||||
if (!await jwtService.Validate(Token))
|
||||
return;
|
||||
|
||||
var data = await jwtService.Decode(Token);
|
||||
|
||||
if (!data.ContainsKey("userId"))
|
||||
return;
|
||||
|
||||
var userId = int.Parse(data["userId"]);
|
||||
|
||||
var user = userRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Id == userId);
|
||||
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
if (!data.ContainsKey("issuedAt"))
|
||||
return;
|
||||
|
||||
var issuedAt = long.Parse(data["issuedAt"]);
|
||||
var issuedAtDateTime = DateTimeOffset.FromUnixTimeSeconds(issuedAt).DateTime;
|
||||
|
||||
// If the valid time is newer then when the token was issued, the token is not longer valid
|
||||
if (user.TokenValidTimestamp > issuedAtDateTime)
|
||||
return;
|
||||
|
||||
Context!.User = user;
|
||||
|
||||
if (Context.User == null) // If the current user is null, stop loading additional data
|
||||
return;
|
||||
|
||||
Success = true;
|
||||
}
|
||||
|
||||
public override void ReadData(RequestDataContext dataContext)
|
||||
{
|
||||
Token = dataContext.ReadString();
|
||||
}
|
||||
}
|
21
Moonlight/App/Api/Requests/PingRequest.cs
Normal file
21
Moonlight/App/Api/Requests/PingRequest.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace Moonlight.App.Api.Requests;
|
||||
|
||||
[ApiRequest(1)]
|
||||
public class PingRequest : AbstractRequest
|
||||
{
|
||||
public override void ReadData(RequestDataContext dataContext)
|
||||
{
|
||||
var chunk = dataContext.ReadInt();
|
||||
}
|
||||
|
||||
public override ResponseDataBuilder CreateResponse(ResponseDataBuilder builder)
|
||||
{
|
||||
builder.WriteInt(10324);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public override async Task ProcessRequest()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
49
Moonlight/App/Api/ResponseDataBuilder.cs
Normal file
49
Moonlight/App/Api/ResponseDataBuilder.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using System.Text;
|
||||
|
||||
namespace Moonlight.App.Api;
|
||||
|
||||
public class ResponseDataBuilder
|
||||
{
|
||||
private List<byte> Data;
|
||||
|
||||
public ResponseDataBuilder()
|
||||
{
|
||||
Data = new List<byte>();
|
||||
}
|
||||
|
||||
public void WriteInt(int data)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(data);
|
||||
|
||||
if (BitConverter.IsLittleEndian) // because of java (the app needing th api is written in java/kotlin) we need to use big endian
|
||||
{
|
||||
bytes = bytes.Reverse().ToArray();
|
||||
}
|
||||
|
||||
Data.AddRange(bytes);
|
||||
}
|
||||
|
||||
public void WriteByte(byte data)
|
||||
{
|
||||
Data.Add(data);
|
||||
}
|
||||
|
||||
public void WriteBoolean(bool data)
|
||||
{
|
||||
WriteByte(data ? (byte)255 : (byte)0);
|
||||
}
|
||||
|
||||
public void WriteString(String data)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(data);
|
||||
var len = bytes.Length;
|
||||
|
||||
WriteInt(len);
|
||||
Data.AddRange(bytes);
|
||||
}
|
||||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
return Data.ToArray();
|
||||
}
|
||||
}
|
17
Moonlight/App/Extensions/TypeExtension.cs
Normal file
17
Moonlight/App/Extensions/TypeExtension.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Moonlight.App.Extensions;
|
||||
|
||||
public static class TypeExtensions
|
||||
{
|
||||
public static PropertyInfo? GetProperty<T, TValue>(this T type, Expression<Func<T, TValue>> selector)
|
||||
where T : class
|
||||
{
|
||||
Expression expression = selector.Body;
|
||||
|
||||
return expression.NodeType == ExpressionType.MemberAccess
|
||||
? (PropertyInfo) ((MemberExpression) expression).Member
|
||||
: null;
|
||||
}
|
||||
}
|
59
Moonlight/App/Http/Controllers/Api/WebsocketController.cs
Normal file
59
Moonlight/App/Http/Controllers/Api/WebsocketController.cs
Normal file
|
@ -0,0 +1,59 @@
|
|||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.App.Api;
|
||||
|
||||
namespace Moonlight.App.Http.Controllers.Api;
|
||||
|
||||
public class WebsocketController : Controller
|
||||
{
|
||||
private readonly ApiManagementService ApiManagementService;
|
||||
|
||||
public WebsocketController(ApiManagementService apiManagementService)
|
||||
{
|
||||
ApiManagementService = apiManagementService;
|
||||
}
|
||||
|
||||
[Route("/api/ws")]
|
||||
public async Task Get()
|
||||
{
|
||||
if (HttpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
using (var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync())
|
||||
{
|
||||
await Echo(webSocket);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Echo(WebSocket webSocket)
|
||||
{
|
||||
var context = new ApiUserContext(webSocket);
|
||||
ApiManagementService.Contexts.Add(context);
|
||||
|
||||
await webSocket.SendAsync(Encoding.UTF8.GetBytes("Hello World"), WebSocketMessageType.Text,
|
||||
true, CancellationToken.None);
|
||||
|
||||
try
|
||||
{
|
||||
while (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
var buffer = new byte[1024 * 10];
|
||||
var data = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
buffer = buffer[..data.Count];
|
||||
|
||||
await ApiManagementService.HandleRequest(context, buffer);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
ApiManagementService.Contexts.Remove(context);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using BlazorTable;
|
||||
using Moonlight.App.Actions.Dummy;
|
||||
using Moonlight.App.Api;
|
||||
using Moonlight.App.Database;
|
||||
using Moonlight.App.Database.Enums;
|
||||
using Moonlight.App.Extensions;
|
||||
|
@ -81,6 +82,7 @@ builder.Services.AddSingleton<ConfigService>();
|
|||
builder.Services.AddSingleton<SessionService>();
|
||||
builder.Services.AddSingleton<BucketService>();
|
||||
builder.Services.AddSingleton<MailService>();
|
||||
builder.Services.AddSingleton<ApiManagementService>();
|
||||
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddServerSideBlazor();
|
||||
|
@ -100,6 +102,7 @@ var app = builder.Build();
|
|||
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.UseWebSockets();
|
||||
|
||||
app.MapBlazorHub();
|
||||
app.MapFallbackToPage("/_Host");
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5132",
|
||||
"applicationUrl": "http://192.168.178.32:5132",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
@echo off
|
||||
sass style.scss ../wwwroot/css/theme.css
|
||||
sass style.scss ../wwwroot/css/theme.css
|
||||
|
||||
pause
|
Loading…
Add table
Reference in a new issue