Alpha Commit
commit
824bdd034b
|
@ -0,0 +1,5 @@
|
|||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
|
@ -0,0 +1,13 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/modules.xml
|
||||
/projectSettingsUpdater.xml
|
||||
/.idea.ln.ai.assistant.iml
|
||||
/contentModel.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
|
@ -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>
|
|
@ -0,0 +1,81 @@
|
|||
|
||||
using ln.ai.assistant.auth;
|
||||
|
||||
namespace ln.ai.assistant.cli;
|
||||
|
||||
public class AiAssistantCli
|
||||
{
|
||||
public static void Main(string[] arguments)
|
||||
{
|
||||
if (arguments.Length < 2) {
|
||||
Console.Error.WriteLine("usage: AiAssistantCli <username> <password>");
|
||||
return;
|
||||
}
|
||||
|
||||
IdentityService identityService = new IdentityService(arguments[0]);
|
||||
|
||||
if (!identityService.TryGetLoginIdentity(arguments[1], out LoginIdentity loginIdentity)) {
|
||||
if (!identityService.CreateLoginIdentity(arguments[1], out loginIdentity)) {
|
||||
Console.Error.WriteLine("could not create identity {0}", arguments[1]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (arguments.Length > 2) {
|
||||
loginIdentity.SetPassword(arguments[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
string newPassword = ReadPassword("Enter new password for user {0} or <ENTER> to quit: ", arguments[1]);
|
||||
if (!string.Empty.Equals(newPassword))
|
||||
{
|
||||
string repeatPassword = ReadPassword("repeat new password for user {0} : ", arguments[1]);
|
||||
if (newPassword.Equals(repeatPassword))
|
||||
{
|
||||
loginIdentity.SetPassword(newPassword);
|
||||
identityService.UpdateLoginIdentity(loginIdentity);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Passwords do not match!");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static string ReadPassword(string prompt, params object[] arguments)
|
||||
{
|
||||
Console.Write(prompt, arguments);
|
||||
|
||||
string password = "";
|
||||
while (true)
|
||||
{
|
||||
ConsoleKeyInfo key = Console.ReadKey(true);
|
||||
|
||||
if (key.Key == ConsoleKey.Enter)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (key.Key == ConsoleKey.Backspace)
|
||||
{
|
||||
if (password.Length > 0)
|
||||
{
|
||||
password = password.Substring(0, password.Length - 1);
|
||||
Console.Write("\b \b");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
password += key.KeyChar;
|
||||
Console.Write("*");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("");
|
||||
return password;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ln.ai.assistant\ln.ai.assistant.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.ai.assistant", "ln.ai.assistant\ln.ai.assistant.csproj", "{B232F124-4775-408D-A684-06D5A1E1E6C0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http", "..\dotnet\ln.http\ln.http\ln.http.csproj", "{25931030-FE87-422A-9210-175432F26D3B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.ai.assistant.cli", "ln.ai.assistant.cli\ln.ai.assistant.cli.csproj", "{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{B232F124-4775-408D-A684-06D5A1E1E6C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B232F124-4775-408D-A684-06D5A1E1E6C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B232F124-4775-408D-A684-06D5A1E1E6C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B232F124-4775-408D-A684-06D5A1E1E6C0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{25931030-FE87-422A-9210-175432F26D3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{25931030-FE87-422A-9210-175432F26D3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{25931030-FE87-422A-9210-175432F26D3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{25931030-FE87-422A-9210-175432F26D3B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1,4 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||
<Assembly Path="/home/haraldwolff/.nuget/packages/ln.http/0.9.8/lib/net7.0/ln.http.dll" />
|
||||
</AssemblyExplorer></s:String></wpf:ResourceDictionary>
|
|
@ -0,0 +1,41 @@
|
|||
|
||||
using System.Net;
|
||||
using ln.ai.assistant.auth;
|
||||
using ln.http;
|
||||
using ln.mime;
|
||||
using HttpMethod = ln.http.HttpMethod;
|
||||
|
||||
namespace ln.ai.assistant
|
||||
{
|
||||
|
||||
public class AiAssistantServer : HttpRouter
|
||||
{
|
||||
private FileSystemRouter FileSystemRouter { get; }
|
||||
public string RootPath { get; }
|
||||
|
||||
|
||||
private IdentityService _identityService;
|
||||
|
||||
public AiAssistantServer(string path, IdentityService identityService)
|
||||
{
|
||||
_identityService = identityService;
|
||||
RootPath = path;
|
||||
FileSystemRouter = new FileSystemRouter(this, "/", new MimeTypes(), path);
|
||||
}
|
||||
|
||||
public override bool RouteRequest(HttpRequestContext httpRequestContext, string routePath)
|
||||
{
|
||||
bool success = base.RouteRequest(httpRequestContext, routePath);
|
||||
if (success)
|
||||
Console.Error.WriteLine("{0} {1} {2} {3} {4}",
|
||||
httpRequestContext.Request.Method.ToString(),
|
||||
httpRequestContext.Request.RequestUri.ToString(),
|
||||
httpRequestContext.Response.HttpStatusCode.ToString(),
|
||||
httpRequestContext.Response.HttpContent?.Length.ToString() ?? "-",
|
||||
httpRequestContext.Response.Headers.Get("content-type",
|
||||
httpRequestContext.Response.HttpContent?.ContentType ?? "-")
|
||||
);
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using ln.type;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public class CertificateAuthority
|
||||
{
|
||||
public string StoragePath { get; }
|
||||
public string Name { get; }
|
||||
|
||||
private X509Certificate2 _privateCertificate;
|
||||
|
||||
public CertificateAuthority(string storagePath, string caName)
|
||||
{
|
||||
StoragePath = storagePath;
|
||||
Name = caName;
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
string caCertFileName = Path.Combine(StoragePath, ".ca.pem");
|
||||
string caKeyFileName = Path.Combine(StoragePath, ".ca.key");
|
||||
|
||||
if (File.Exists(caCertFileName) && File.Exists(caKeyFileName))
|
||||
{
|
||||
_privateCertificate = X509Certificate2.CreateFromPemFile(caCertFileName, caKeyFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
using (RSA rsa = RSA.Create(4096))
|
||||
{
|
||||
var request = new CertificateRequest($"CN={Name}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign, false));
|
||||
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, false));
|
||||
_privateCertificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now + TimeSpan.FromDays(3650));
|
||||
|
||||
WritePemFile(caCertFileName, _privateCertificate);
|
||||
WritePemFile(caKeyFileName, rsa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetOrCreateCertificate(string distinguishedName, out X509Certificate2? certificate)
|
||||
{
|
||||
return TryLoadCertificate(distinguishedName, out certificate) ||
|
||||
CreateSignedCertificate(distinguishedName, out certificate);
|
||||
}
|
||||
|
||||
private bool CreateSignedCertificate(string distinguishedName, out X509Certificate2? certificate)
|
||||
{
|
||||
string certFileName = Path.Combine(StoragePath, $"{distinguishedName}.cert.pem");
|
||||
string keyFileName = Path.Combine(StoragePath, $"{distinguishedName}.key.pem");
|
||||
|
||||
using (RSA rsa = RSA.Create(4096))
|
||||
{
|
||||
var request = new CertificateRequest($"CN={distinguishedName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment |
|
||||
X509KeyUsageFlags.DigitalSignature, false));
|
||||
request.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
|
||||
|
||||
var san = new SubjectAlternativeNameBuilder();
|
||||
san.AddDnsName(distinguishedName);
|
||||
request.CertificateExtensions.Add(san.Build());
|
||||
|
||||
certificate = request.Create(_privateCertificate, DateTimeOffset.Now,
|
||||
DateTimeOffset.Now + TimeSpan.FromDays(365), BitConverter.GetBytes(DateTime.Now.ToUnixTimeSeconds()));
|
||||
|
||||
WritePemFile(certFileName, certificate);
|
||||
WritePemFile(keyFileName, rsa);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
certificate = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private bool TryLoadCertificate(string distinguishedName, out X509Certificate2? certificate)
|
||||
{
|
||||
string certFileName = Path.Combine(StoragePath, $"{distinguishedName}.cert.pem");
|
||||
string keyFileName = Path.Combine(StoragePath, $"{distinguishedName}.key.pem");
|
||||
|
||||
if (File.Exists(certFileName) && File.Exists(keyFileName))
|
||||
{
|
||||
// certificate = new X509Certificate2(X509Certificate2.CreateFromPemFile(certFileName, keyFileName).Export(X509ContentType.Pfx, "alpha"), "alpha");
|
||||
certificate = X509Certificate2.CreateFromPemFile(certFileName, keyFileName);
|
||||
return true;
|
||||
}
|
||||
|
||||
certificate = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void WritePemFile(string pemFileName, X509Certificate2 certificate2, RSA rsa)
|
||||
{
|
||||
using (var pemFileStream =
|
||||
new StreamWriter(new FileStream(pemFileName, FileMode.CreateNew)))
|
||||
{
|
||||
WritePemFile("CERTIFICATE", certificate2.RawData, pemFileStream);
|
||||
WritePemFile("RSA PRIVATE KEY", rsa.ExportRSAPrivateKey(), pemFileStream);
|
||||
}
|
||||
}
|
||||
private void WritePemFile(string pemFileName, X509Certificate2 certificate2)
|
||||
{
|
||||
using (var pemFileStream =
|
||||
new StreamWriter(new FileStream(pemFileName, FileMode.CreateNew)))
|
||||
{
|
||||
WritePemFile("CERTIFICATE", certificate2.RawData, pemFileStream);
|
||||
}
|
||||
}
|
||||
private void WritePemFile(string pemFileName, RSA rsa)
|
||||
{
|
||||
using (var pemFileStream =
|
||||
new StreamWriter(new FileStream(pemFileName, FileMode.CreateNew)))
|
||||
{
|
||||
WritePemFile("RSA PRIVATE KEY", rsa.ExportRSAPrivateKey(), pemFileStream);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void WritePemFile(string type, byte[] data, TextWriter outputFile)
|
||||
{
|
||||
outputFile.WriteLine($"-----BEGIN {type}-----");
|
||||
outputFile.WriteLine(Convert.ToBase64String(data, Base64FormattingOptions.InsertLineBreaks));
|
||||
outputFile.WriteLine($"-----END {type}-----");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using ln.http;
|
||||
using ln.http.client;
|
||||
using HttpClient = ln.http.client.HttpClient;
|
||||
using HttpMethod = ln.http.HttpMethod;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public class ChatCompletionProxy : HttpEndpointController
|
||||
{
|
||||
public Uri BackendUri { get; }
|
||||
private HttpClient _httpClient = new HttpClient();
|
||||
|
||||
private string apiKey;
|
||||
|
||||
public ChatCompletionProxy(HttpRouter parentRouter, Uri backendUri):this(parentRouter, backendUri, null){}
|
||||
|
||||
public ChatCompletionProxy(HttpRouter parentRouter, Uri backendUri, string apiKey)
|
||||
:base(parentRouter)
|
||||
{
|
||||
BackendUri = backendUri;
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
[Map(HttpMethod.POST, "/v1/chat/completion")]
|
||||
[Requires]
|
||||
public HttpResponse ChatCompletion(HttpRequest request)
|
||||
{
|
||||
Uri uri = new Uri(BackendUri, request.RequestUri.PathAndQuery);
|
||||
HeaderContainer backendHeaders = new HeaderContainer();
|
||||
if (apiKey != null)
|
||||
backendHeaders.Add("Authorization", $"Bearer { apiKey }");
|
||||
|
||||
var o = _httpClient.Request(request.Method, uri, backendHeaders, request.ContentStream);
|
||||
if (o && o.Value is HttpClientResponse response)
|
||||
{
|
||||
HttpResponse httpResponse = new HttpResponse(response.StatusCode);
|
||||
httpResponse.Content(response.ContentStream);
|
||||
httpResponse.SetHeader("Content-Type", response.Headers.Get("Content-Type"));
|
||||
return httpResponse;
|
||||
}
|
||||
return HttpResponse.BadGateway();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
using LiteDB;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public class ChatRole
|
||||
{
|
||||
private static List<ChatRole> _roles = new List<ChatRole>();
|
||||
|
||||
private string _role;
|
||||
|
||||
private ChatRole(string role)
|
||||
{
|
||||
_role = role;
|
||||
}
|
||||
|
||||
public readonly static ChatRole System = new ChatRole("system");
|
||||
public readonly static ChatRole User = new ChatRole("user");
|
||||
public readonly static ChatRole Assistant = new ChatRole("assistant");
|
||||
public readonly static ChatRole Tool = new ChatRole("tool");
|
||||
|
||||
public override string ToString() => _role;
|
||||
|
||||
public static ChatRole from(string role)
|
||||
{
|
||||
foreach (var chatRole in _roles)
|
||||
{
|
||||
if (chatRole._role.Equals(role))
|
||||
return chatRole;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static ChatRole()
|
||||
{
|
||||
BsonMapper.Global.RegisterType<ChatRole>
|
||||
(
|
||||
serialize: (chatRole) => chatRole.ToString(),
|
||||
deserialize: (bson) => ChatRole.from(bson.AsString)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChatMessage
|
||||
{
|
||||
public ChatRole role = ChatRole.User;
|
||||
public string content = "";
|
||||
}
|
||||
|
||||
public class Chat
|
||||
{
|
||||
public Guid id;
|
||||
public string owner;
|
||||
public List<ChatMessage> messages = new List<ChatMessage>();
|
||||
}
|
||||
|
||||
public class ChatStorage
|
||||
{
|
||||
private LiteDatabase _liteDatabase;
|
||||
private ILiteCollection<Chat> _chats;
|
||||
public ChatStorage(string storagePath)
|
||||
{
|
||||
_liteDatabase = new LiteDatabase(storagePath);
|
||||
_chats = _liteDatabase.GetCollection<Chat>();
|
||||
_chats.EnsureIndex(c => c.owner);
|
||||
}
|
||||
|
||||
public IEnumerable<Chat> GetUserChats(string username) => _chats.Find(c => c.owner.Equals(username));
|
||||
|
||||
public Chat GetUserChat(string username, Guid id) =>
|
||||
_chats.FindOne(c => c.owner.Equals(username) && c.id.Equals(id));
|
||||
|
||||
public void AddChat(Chat chat) => _chats.Insert(chat);
|
||||
public bool UpdateChat(Chat chat) => _chats.Update(chat);
|
||||
public bool RemoveChat(Chat chat) => _chats.Delete(chat.id);
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
using ln.http;
|
||||
using ln.json;
|
||||
using ln.json.mapping;
|
||||
using HttpMethod = ln.http.HttpMethod;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public class ChatStorageApi : HttpEndpointController
|
||||
{
|
||||
public ChatStorage _chatStorage;
|
||||
|
||||
public ChatStorageApi(ChatStorage chatStorage)
|
||||
{
|
||||
_chatStorage = chatStorage;
|
||||
}
|
||||
|
||||
[Map(HttpMethod.GET,"/v1/chats")]
|
||||
[Requires]
|
||||
public HttpResponse GetUserChats(HttpPrincipal principal)
|
||||
{
|
||||
JSONValue jsonChats = JSONMapper.DefaultMapper.ToJson(
|
||||
_chatStorage.GetUserChats(principal.UniqueId)
|
||||
);
|
||||
return HttpResponse.OK().Content(jsonChats);
|
||||
}
|
||||
|
||||
[Map(HttpMethod.POST, "/v1/chats")]
|
||||
[Requires]
|
||||
public HttpResponse PostUserChat(HttpPrincipal principal, HttpRequest request,
|
||||
[HttpArgumentSource(HttpArgumentSource.CONTENT)] JSONObject jsonChat)
|
||||
{
|
||||
Chat newChat = JSONMapper.DefaultMapper.FromJson<Chat>(jsonChat);
|
||||
newChat.id = Guid.NewGuid();
|
||||
newChat.owner = principal.UniqueId;
|
||||
_chatStorage.AddChat(newChat);
|
||||
return HttpResponse.SeeOther($"{ request.BaseUri }/v1/chats/{ newChat.id.ToString() }");
|
||||
}
|
||||
|
||||
[Map(HttpMethod.GET, "/v1/chats/:id")]
|
||||
[Requires]
|
||||
public HttpResponse GetUserChat(HttpPrincipal principal, Guid id)
|
||||
{
|
||||
Chat chat = _chatStorage.GetUserChat(principal.UniqueId, id);
|
||||
JSONValue jsonChat = JSONMapper.DefaultMapper.ToJson(chat);
|
||||
return HttpResponse.OK().Content(jsonChat);
|
||||
}
|
||||
|
||||
[Map(HttpMethod.POST, "/v1/chats/:id")]
|
||||
[Requires]
|
||||
public HttpResponse AddNewUserMessage(HttpPrincipal principal, Guid id,
|
||||
[HttpArgumentSource(HttpArgumentSource.CONTENT)] ChatMessage chatMessage)
|
||||
{
|
||||
Chat chat = _chatStorage.GetUserChat(principal.UniqueId, id);
|
||||
chat.messages.Add(chatMessage);
|
||||
_chatStorage.UpdateChat(chat);
|
||||
|
||||
JSONValue jsonChat = JSONMapper.DefaultMapper.ToJson(chat);
|
||||
return HttpResponse.OK().Content(jsonChat);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
using LiteDB;
|
||||
using ln.ai.assistant.api.openai;
|
||||
using ln.ai.assistant.persist;
|
||||
using ln.http;
|
||||
using ln.json.mapping;
|
||||
using HttpMethod = ln.http.HttpMethod;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public class CompletionApi : HttpEndpointController
|
||||
{
|
||||
private CompletionProvider _completionProvider;
|
||||
|
||||
public CompletionApi(HttpRouter parentRouter, CompletionProvider completionProvider) :base(parentRouter)
|
||||
{
|
||||
_completionProvider = completionProvider;
|
||||
}
|
||||
|
||||
[Requires]
|
||||
[Map(HttpMethod.POST, "/v1/chat/completions")]
|
||||
public HttpResponse PostChatCompletion(HttpPrincipal principal,
|
||||
[JsonBody] api.openai.ChatCompletionRequest chatCompletionRequest)
|
||||
{
|
||||
var completionTask = new CompletionTask<ChatCompletionResponse>(principal.UniqueId, () =>
|
||||
{
|
||||
var result = _completionProvider.ChatCompletion(chatCompletionRequest);
|
||||
if (result)
|
||||
return result.Value;
|
||||
return result.Exception;
|
||||
});
|
||||
_cache.Add(completionTask);
|
||||
return HttpResponse.SeeOther($"/v1/chat/completions/{completionTask.Id}");
|
||||
}
|
||||
|
||||
[Requires]
|
||||
[Map(HttpMethod.GET, "/v1/chat/completions/(?<completionTaskId>\\w+)$")]
|
||||
public HttpResponse GetChatCompletion(
|
||||
HttpPrincipal principal,
|
||||
[Route] string completionTaskId,
|
||||
[Query] bool wait = false)
|
||||
{
|
||||
if (_cache.TryGet(completionTaskId, out CompletionTask _completionTask) &&
|
||||
_completionTask.Owner.Equals(principal.UniqueId))
|
||||
{
|
||||
if (!(_completionTask is CompletionTask<ChatCompletionResponse> completionTask))
|
||||
return HttpResponse.InternalServerError().Content("_completionTask is not CompletionTask<ChatCompletionResponse>");
|
||||
|
||||
if ((completionTask.Result is null) && wait)
|
||||
completionTask.Wait(TimeSpan.FromMinutes(1));
|
||||
|
||||
if (completionTask.Result is not null)
|
||||
{
|
||||
_cache.Remove(completionTaskId);
|
||||
|
||||
if (completionTask.Result)
|
||||
return HttpResponse.OK().Json(completionTask.Result.Value);
|
||||
else
|
||||
return HttpResponse.InternalServerError().Content(completionTask.Result.Exception);
|
||||
}
|
||||
|
||||
return HttpResponse.NoContent();
|
||||
}
|
||||
|
||||
return HttpResponse.NotFound();
|
||||
}
|
||||
|
||||
[Requires]
|
||||
[Map(HttpMethod.POST, "/v1/completions")]
|
||||
public HttpResponse PostCompletion(HttpPrincipal principal,
|
||||
[JsonBody] api.openai.CompletionRequest completionRequest)
|
||||
{
|
||||
CompletionTask completionTask = new CompletionTask<CompletionResponse>(principal.UniqueId,
|
||||
() => _completionProvider.Completion(completionRequest));
|
||||
|
||||
_cache.Add(completionTask);
|
||||
return HttpResponse.SeeOther($"/v1/completions/{completionTask.Id}");
|
||||
}
|
||||
|
||||
[Requires]
|
||||
[Map(HttpMethod.GET, "/v1/completions/(?<completionTaskId>\\w+)$")]
|
||||
public HttpResponse GetCompletion(
|
||||
HttpPrincipal principal,
|
||||
[Route] string completionTaskId,
|
||||
[Query] bool wait = false)
|
||||
{
|
||||
if (_cache.TryGet(completionTaskId, out CompletionTask _completionTask) &&
|
||||
_completionTask.Owner.Equals(principal.UniqueId))
|
||||
{
|
||||
if (!(_completionTask is CompletionTask<CompletionResponse> completionTask))
|
||||
return HttpResponse.InternalServerError().Content("_completionTask is not CompletionTask<CompletionResponse>");
|
||||
|
||||
if ((completionTask.Result is null) && wait)
|
||||
completionTask.Wait(TimeSpan.FromMinutes(1));
|
||||
|
||||
if (completionTask.Result is not null)
|
||||
{
|
||||
if (completionTask.Result)
|
||||
return HttpResponse.OK().Json(completionTask.Result.Value);
|
||||
else
|
||||
return HttpResponse.InternalServerError().Content(completionTask.Result.Exception);
|
||||
}
|
||||
|
||||
return HttpResponse.NoContent();
|
||||
}
|
||||
|
||||
return HttpResponse.NotFound();
|
||||
}
|
||||
|
||||
private TaskCache _cache = new TaskCache();
|
||||
class TaskCache : Cache<string, CompletionTask>
|
||||
{
|
||||
public void Add(CompletionTask task) => Add(task.Id, task);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
using System.Reflection;
|
||||
using ln.ai.assistant.api.openai;
|
||||
using ln.patterns;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public abstract class CompletionProvider
|
||||
{
|
||||
public CompletionProvider(){}
|
||||
public abstract Optional<ChatCompletionResponse> ChatCompletion(ChatCompletionRequest chatCompletionRequest);
|
||||
public abstract Optional<CompletionResponse> Completion(CompletionRequest chatCompletionRequest);
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
using ln.json;
|
||||
using ln.patterns;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public class CompletionTask
|
||||
{
|
||||
public string Id { get; protected set; }
|
||||
public string Owner { get; protected set; }
|
||||
}
|
||||
|
||||
public class CompletionTask<T> : CompletionTask
|
||||
{
|
||||
public Optional<T>? _result;
|
||||
public Optional<T>? Result => _result;
|
||||
|
||||
public CompletionTask(string owner, Func<Optional<T>> taskDelegate)
|
||||
{
|
||||
Id = GetRandomId();
|
||||
Owner = owner;
|
||||
|
||||
ThreadPool.QueueUserWorkItem((o) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_result = taskDelegate();
|
||||
if (_result is null)
|
||||
_result = new NullReferenceException("taskDelegate returned null");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_result = e;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (this)
|
||||
Monitor.PulseAll(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public bool Wait(TimeSpan timeout)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_result is null)
|
||||
return Monitor.Wait(this, timeout);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
static string GetRandomId()
|
||||
{
|
||||
byte[] b = new byte[32];
|
||||
Random.Shared.NextBytes(b);
|
||||
return Convert.ToHexString(b);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
using ln.ai.assistant.auth;
|
||||
using ln.ai.assistant.persist;
|
||||
using ln.http;
|
||||
using ln.json;
|
||||
using HttpMethod = ln.http.HttpMethod;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public class IdentityApi : HttpEndpointController
|
||||
{
|
||||
private IdentityService _identityService;
|
||||
private Cache<string, byte[]> _challenges = new Cache<string, byte[]>();
|
||||
|
||||
public IdentityApi(HttpRouter httpRouter, IdentityService identityService)
|
||||
:base(httpRouter)
|
||||
{
|
||||
_identityService = identityService;
|
||||
httpRouter.HttpFilters += httpFilter;
|
||||
}
|
||||
|
||||
private void httpFilter(HttpRequestContext httprequestcontext)
|
||||
{
|
||||
var request = httprequestcontext.Request;
|
||||
if (request.Headers.TryGetValue("Authorization", out string authorizationValue))
|
||||
{
|
||||
string[] authSplit = authorizationValue.Split(new char[]{ }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if ((authSplit.Length == 2) && authSplit[0].Equals("Token"))
|
||||
{
|
||||
byte[] token = Convert.FromBase64String(authSplit[1]);
|
||||
if (_identityService.TryGetAuthenticationTokenIdentity(token, out LoginIdentity loginIdentity))
|
||||
{
|
||||
httprequestcontext.AuthenticatedPrincipal = new HttpPrincipal(
|
||||
loginIdentity.Username,
|
||||
loginIdentity.Username,
|
||||
loginIdentity.Roles
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Map(HttpMethod.GET, "/v1/login")]
|
||||
public HttpResponse Login([Query] string username = null)
|
||||
{
|
||||
if (username is null)
|
||||
return HttpResponse.BadRequest();
|
||||
|
||||
byte[] salt = LoginIdentity.GenerateRandomBytes(128);
|
||||
byte[] challenge = LoginIdentity.GenerateRandomBytes(128);
|
||||
|
||||
if (_identityService.TryGetLoginIdentity(username, out LoginIdentity loginIdentity))
|
||||
salt = loginIdentity.Salt;
|
||||
|
||||
_challenges.Set(username, challenge);
|
||||
JSONObject jsonResponse = new JSONObject()
|
||||
{
|
||||
["salt"] = Convert.ToBase64String(salt),
|
||||
["challenge"] = Convert.ToBase64String(challenge),
|
||||
["username"] = username
|
||||
};
|
||||
|
||||
return HttpResponse.OK().Content(jsonResponse);
|
||||
}
|
||||
|
||||
[Map(HttpMethod.GET, "/v1/authenticate")]
|
||||
public HttpResponse Authenticate(
|
||||
[Query] string username,
|
||||
[Query] byte[] response)
|
||||
{
|
||||
if (_challenges.TryGet(username, out byte[] challenge))
|
||||
{
|
||||
if (_identityService.TryGetLoginIdentity(username, out LoginIdentity loginIdentity))
|
||||
{
|
||||
if (loginIdentity.ValidateResponse(loginIdentity.Salt, challenge, response))
|
||||
{
|
||||
var authenticationToken = _identityService.CreateAuthenticationToken(loginIdentity);
|
||||
JSONObject jsonResult = new JSONObject()
|
||||
{
|
||||
["token"] = Convert.ToBase64String(authenticationToken.TokenValue),
|
||||
["username"] = authenticationToken.LoginIdentity.Username
|
||||
};
|
||||
|
||||
return HttpResponse.OK().Content(jsonResult);
|
||||
}
|
||||
}
|
||||
return HttpResponse.Unauthorized();
|
||||
}
|
||||
else
|
||||
return HttpResponse.NoContent();
|
||||
}
|
||||
|
||||
[Requires]
|
||||
[Map(HttpMethod.GET, "/v1/checkToken")]
|
||||
public HttpResponse CheckToken() => HttpResponse.OK().Content("Token Valid");
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
using System.Reflection;
|
||||
using System.Text;
|
||||
using ln.ai.assistant.api.openai;
|
||||
using ln.http;
|
||||
using ln.patterns;
|
||||
using Newtonsoft.Json;
|
||||
using HttpClient = ln.http.client.HttpClient;
|
||||
using HttpMethod = ln.http.HttpMethod;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public class OpenAiApiCompletionProvider : CompletionProvider
|
||||
{
|
||||
private Uri _baseUri;
|
||||
private string? _apiKey;
|
||||
private string? _model;
|
||||
|
||||
private HttpClient _httpClient = new HttpClient();
|
||||
|
||||
public OpenAiApiCompletionProvider(Uri baseUri, string? apiKey = null, string? model = null)
|
||||
{
|
||||
_baseUri = baseUri;
|
||||
_apiKey = apiKey;
|
||||
_model = model;
|
||||
}
|
||||
|
||||
public override Optional<ChatCompletionResponse> ChatCompletion(ChatCompletionRequest chatCompletionRequest)
|
||||
{
|
||||
if (_model is string)
|
||||
chatCompletionRequest.Model = _model;
|
||||
|
||||
Uri uri = new Uri(_baseUri, "/v1/chat/completions");
|
||||
var r = _httpClient.Post<ChatCompletionRequest>(
|
||||
uri,
|
||||
headers: (_apiKey != null)
|
||||
? new Header[] { new Header("Authorization", $"Bearer {_apiKey}") }
|
||||
: Array.Empty<Header>(),
|
||||
body: chatCompletionRequest
|
||||
);
|
||||
if (r)
|
||||
{
|
||||
return r.Value.Json<ChatCompletionResponse>();
|
||||
}
|
||||
return new IOException();
|
||||
}
|
||||
|
||||
public override Optional<CompletionResponse> Completion(CompletionRequest completionRequest)
|
||||
{
|
||||
if (_model is string)
|
||||
completionRequest.Model = _model;
|
||||
|
||||
Uri uri = new Uri(_baseUri, "/v1/completions");
|
||||
var r = _httpClient.Post<CompletionRequest>(
|
||||
uri,
|
||||
headers: (_apiKey != null)
|
||||
? new Header[] { new Header("Authorization", $"Bearer {_apiKey}") }
|
||||
: Array.Empty<Header>(),
|
||||
body: completionRequest
|
||||
);
|
||||
if (r)
|
||||
{
|
||||
return r.Value.Json<CompletionResponse>();
|
||||
}
|
||||
return new IOException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using System.Resources;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using ln.http;
|
||||
|
||||
namespace ln.ai.assistant;
|
||||
|
||||
public class SelfSigningCertificateStore : CertificateStore
|
||||
{
|
||||
public DirectoryInfo StorageDirectory { get; }
|
||||
private CertificateAuthority _certificateAuthority;
|
||||
|
||||
public SelfSigningCertificateStore(string path)
|
||||
{
|
||||
_certificateAuthority = new CertificateAuthority(path, "ca.local");
|
||||
|
||||
StorageDirectory = new DirectoryInfo(path);
|
||||
if (!StorageDirectory.Exists)
|
||||
StorageDirectory.Create();
|
||||
}
|
||||
|
||||
public override bool TryGetCertificate(string hostname, out X509Certificate certificate)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (hostname.Equals(String.Empty))
|
||||
hostname = "localhost";
|
||||
|
||||
if (!base.TryGetCertificate(hostname, out certificate))
|
||||
{
|
||||
if (!_certificateAuthority.GetOrCreateCertificate(hostname, out X509Certificate2? cert))
|
||||
return false;
|
||||
|
||||
certificate = cert;
|
||||
AddCertificate(cert);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace ln.ai.assistant.api.openai
|
||||
{
|
||||
public struct ChatCompletionRequest
|
||||
{
|
||||
[JsonProperty("model",NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Model;
|
||||
|
||||
[JsonProperty("messages")]
|
||||
public List<Message> Messages;
|
||||
|
||||
[JsonProperty("max_tokens",NullValueHandling = NullValueHandling.Ignore)]
|
||||
public int? MaxTokens;
|
||||
|
||||
[JsonProperty("temperature",NullValueHandling = NullValueHandling.Ignore)]
|
||||
public float? Temperature;
|
||||
|
||||
[JsonProperty("top_p",NullValueHandling = NullValueHandling.Ignore)]
|
||||
public float? TopP;
|
||||
|
||||
[JsonProperty("n",NullValueHandling = NullValueHandling.Ignore)]
|
||||
public int? N;
|
||||
|
||||
[JsonProperty("stream",NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? Stream;
|
||||
|
||||
[JsonProperty("stop",NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<string> Stop;
|
||||
}
|
||||
|
||||
public struct ChatCompletionResponse
|
||||
{
|
||||
[JsonProperty("id")] public string Id;
|
||||
[JsonProperty("object")] public string Object;
|
||||
[JsonProperty("created")] public int Created;
|
||||
[JsonProperty("model")] public string Model;
|
||||
[JsonProperty("choices")] public List<ChatCompletionChoice> Choices;
|
||||
[JsonProperty("usage")] public Usage Usage;
|
||||
}
|
||||
|
||||
public struct ChatCompletionChoice
|
||||
{
|
||||
[JsonProperty("message")] public Message Message;
|
||||
[JsonProperty("finish_reason")] public string FinishReason;
|
||||
[JsonProperty("index")] public int Index;
|
||||
}
|
||||
|
||||
public struct Message
|
||||
{
|
||||
[JsonProperty("role")] public string Role;
|
||||
[JsonProperty("content")] public string Content;
|
||||
}
|
||||
|
||||
public struct Usage
|
||||
{
|
||||
[JsonProperty("prompt_tokens")] public int PromptTokens;
|
||||
[JsonProperty("completion_tokens")] public int CompletionTokens;
|
||||
[JsonProperty("total_tokens")] public int TotalTokens;
|
||||
}
|
||||
|
||||
public struct CompletionRequest
|
||||
{
|
||||
[JsonProperty("model")]
|
||||
public string Model;
|
||||
|
||||
[JsonProperty("prompt")]
|
||||
public string Prompt;
|
||||
|
||||
[JsonProperty("max_tokens")]
|
||||
public int MaxTokens;
|
||||
|
||||
[JsonProperty("temperature")]
|
||||
public float Temperature;
|
||||
|
||||
[JsonProperty("top_p")]
|
||||
public float TopP;
|
||||
|
||||
[JsonProperty("n")]
|
||||
public int N;
|
||||
|
||||
[JsonProperty("stream")]
|
||||
public bool Stream;
|
||||
|
||||
[JsonProperty("logprobs")]
|
||||
public int? Logprobs;
|
||||
|
||||
[JsonProperty("stop")]
|
||||
public List<string> Stop;
|
||||
}
|
||||
|
||||
public struct CompletionResponse
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string Id;
|
||||
|
||||
[JsonProperty("object")]
|
||||
public string Object;
|
||||
|
||||
[JsonProperty("created")]
|
||||
public int Created;
|
||||
|
||||
[JsonProperty("model")]
|
||||
public string Model;
|
||||
|
||||
[JsonProperty("choices")]
|
||||
public List<CompletionChoice> Choices;
|
||||
|
||||
[JsonProperty("usage")]
|
||||
public Usage Usage;
|
||||
}
|
||||
|
||||
public struct CompletionChoice
|
||||
{
|
||||
[JsonProperty("text")]
|
||||
public string Text;
|
||||
|
||||
[JsonProperty("index")]
|
||||
public int Index;
|
||||
|
||||
[JsonProperty("logprobs")]
|
||||
public Dictionary<string, float> Logprobs;
|
||||
|
||||
[JsonProperty("finish_reason")]
|
||||
public string FinishReason;
|
||||
}
|
||||
|
||||
public struct TranscriptionRequest
|
||||
{
|
||||
[JsonProperty("model")]
|
||||
private string Model;
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
using LiteDB;
|
||||
using ln.ai.assistant.persist;
|
||||
|
||||
namespace ln.ai.assistant.auth;
|
||||
|
||||
public class AuthenticationToken
|
||||
{
|
||||
public LoginIdentity LoginIdentity { get; }
|
||||
public byte[] TokenValue { get; }
|
||||
public DateTime ValidUntil { get; }
|
||||
|
||||
public AuthenticationToken(LoginIdentity loginIdentity)
|
||||
{
|
||||
LoginIdentity = loginIdentity;
|
||||
TokenValue = LoginIdentity.GenerateRandomBytes(32);
|
||||
ValidUntil = DateTime.Now + TimeSpan.FromMinutes(10);
|
||||
}
|
||||
}
|
||||
|
||||
public class IdentityService
|
||||
{
|
||||
private LiteDatabase _liteDatabase;
|
||||
|
||||
private ILiteCollection<LoginIdentity> _identitiesCollection;
|
||||
|
||||
public IdentityService(string storagePath)
|
||||
{
|
||||
_liteDatabase = new LiteDatabase(storagePath);
|
||||
_identitiesCollection = _liteDatabase.GetCollection<LoginIdentity>();
|
||||
_identitiesCollection.EnsureIndex(identity => identity.Username);
|
||||
}
|
||||
|
||||
public bool TryGetLoginIdentity(string username, out LoginIdentity loginIdentity)
|
||||
{
|
||||
loginIdentity = _identitiesCollection.FindById(username);
|
||||
return loginIdentity is LoginIdentity;
|
||||
}
|
||||
public bool CreateLoginIdentity(string username) =>
|
||||
CreateLoginIdentity(username, out LoginIdentity loginIdentity);
|
||||
public bool CreateLoginIdentity(string username, out LoginIdentity loginIdentity)
|
||||
{
|
||||
loginIdentity = new LoginIdentity(username);
|
||||
return _identitiesCollection.Insert(loginIdentity) is BsonValue;
|
||||
}
|
||||
public bool RemoveLoginIdentity(string username) => _identitiesCollection.Delete(username);
|
||||
public bool UpdateLoginIdentity(LoginIdentity loginIdentity) => _identitiesCollection.Update(loginIdentity);
|
||||
public IEnumerable<LoginIdentity> GetAllLoginIdentities() => _identitiesCollection.FindAll();
|
||||
|
||||
// AuthTokens
|
||||
private Cache<string, AuthenticationToken> _authenticationTokens = new Cache<string, AuthenticationToken>(TimeSpan.FromMinutes(10));
|
||||
public AuthenticationToken CreateAuthenticationToken(LoginIdentity loginIdentity)
|
||||
{
|
||||
AuthenticationToken authenticationToken = new AuthenticationToken(loginIdentity);
|
||||
_authenticationTokens.Add(Convert.ToBase64String(authenticationToken.TokenValue), authenticationToken);
|
||||
return authenticationToken;
|
||||
}
|
||||
|
||||
public void RemoveAuthenticationToken(byte[] token) => _authenticationTokens.Remove(Convert.ToBase64String(token));
|
||||
public bool TryGetAuthenticationTokenIdentity(byte[] token, out LoginIdentity loginIdentity)
|
||||
{
|
||||
if (_authenticationTokens.TryGet(Convert.ToBase64String(token), out AuthenticationToken authenticationToken))
|
||||
{
|
||||
loginIdentity = authenticationToken.LoginIdentity;
|
||||
return true;
|
||||
}
|
||||
|
||||
loginIdentity = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using LiteDB;
|
||||
|
||||
namespace ln.ai.assistant.auth;
|
||||
|
||||
public class LoginIdentity
|
||||
{
|
||||
[BsonId]
|
||||
public string Username { get; set; }
|
||||
public byte[] HashedPassword { get; set; }
|
||||
public byte[] Salt { get; set; }
|
||||
|
||||
public string[] Roles { get; set; } = Array.Empty<string>();
|
||||
|
||||
private LoginIdentity(){}
|
||||
|
||||
public LoginIdentity(string username, string password)
|
||||
{
|
||||
Username = username;
|
||||
CreatePasswordHash(password);
|
||||
}
|
||||
public LoginIdentity(string username)
|
||||
{
|
||||
Username = username;
|
||||
}
|
||||
public LoginIdentity(string username, byte[] salt, byte[] hashedPassword)
|
||||
{
|
||||
Username = username;
|
||||
Salt = salt;
|
||||
HashedPassword = hashedPassword;
|
||||
}
|
||||
|
||||
public void SetPassword(string password) => CreatePasswordHash(password);
|
||||
|
||||
private void CreatePasswordHash(string password)
|
||||
{
|
||||
using (var hmac = new HMACSHA512())
|
||||
{
|
||||
Salt = hmac.Key;
|
||||
HashedPassword = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
|
||||
}
|
||||
}
|
||||
|
||||
public bool VerifyPassword(string password)
|
||||
{
|
||||
using (var hmac = new HMACSHA512(Salt))
|
||||
{
|
||||
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
|
||||
for (int i = 0; i < computedHash.Length; i++)
|
||||
{
|
||||
if (computedHash[i] != HashedPassword[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ValidateResponse(byte[] salt, byte[] challenge, byte[] response)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(salt, nameof(salt));
|
||||
ArgumentNullException.ThrowIfNull(challenge, nameof(challenge));
|
||||
ArgumentNullException.ThrowIfNull(response, nameof(response));
|
||||
|
||||
var combined = new byte[challenge.Length * 2 + HashedPassword.Length];
|
||||
Buffer.BlockCopy(challenge, 0, combined, 0, challenge.Length);
|
||||
Buffer.BlockCopy(HashedPassword, 0, combined,challenge.Length, HashedPassword.Length);
|
||||
Buffer.BlockCopy(challenge, 0, combined, challenge.Length + HashedPassword.Length, challenge.Length);
|
||||
|
||||
using (var hmac = new HMACSHA512(salt))
|
||||
{
|
||||
var computedHash = hmac.ComputeHash(combined);
|
||||
|
||||
if (response.Length != computedHash.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < computedHash.Length; i++)
|
||||
{
|
||||
if (computedHash[i] != response[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] GenerateChallenge() => Encoding.UTF8.GetBytes(Guid.NewGuid().ToString());
|
||||
|
||||
private static Random _random = new Random();
|
||||
public static byte[] GenerateRandomBytes(int length)
|
||||
{
|
||||
byte[] randomBytes = new byte[length];
|
||||
_random.NextBytes(randomBytes);
|
||||
return randomBytes;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"ln.ai.assistant.auth.IdentityService": {
|
||||
"set": {
|
||||
"storagePath": "identities.db"
|
||||
}
|
||||
},
|
||||
"ln.ai.assistant.AiAssistantServer": {
|
||||
"set": {
|
||||
"path": "/home/haraldwolff/src/www/ai.chat"
|
||||
},
|
||||
"select": {
|
||||
"parentRouter": "ln.ai.assistant.AiAssistantServer"
|
||||
}
|
||||
},
|
||||
"ln.ai.assistant.OpenAiApiCompletionProvider": {
|
||||
"set": {
|
||||
"baseUri": "http://10.96.2.20:8080"
|
||||
},
|
||||
"select": {
|
||||
"parentRouter": "ln.ai.assistant.AiAssistantServer"
|
||||
}
|
||||
},
|
||||
"ln.ai.assistant.IdentityApi":{
|
||||
"select": {
|
||||
"parentRouter": "ln.ai.assistant.AiAssistantServer"
|
||||
}
|
||||
},
|
||||
"ln.ai.assistant.CompletionApi":{
|
||||
"select": {
|
||||
"parentRouter": "ln.ai.assistant.AiAssistantServer"
|
||||
}
|
||||
},
|
||||
"ln.ai.assistant.SelfSigningCertificateStore": {
|
||||
"set": {
|
||||
"path": "certs"
|
||||
}
|
||||
},
|
||||
"ln.http.TlsListener": {
|
||||
"set": {
|
||||
"port": 8080,
|
||||
"bind": "::1"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LiteDB" Version="5.0.19" />
|
||||
<PackageReference Include="ln.bootstrap" Version="1.5.0-preview1" />
|
||||
<PackageReference Include="ln.http" Version="0.9.10-preview0" />
|
||||
<PackageReference Include="ln.json" Version="1.3.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="bootstrap.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\dotnet\ln.http\ln.http\ln.http.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,17 @@
|
|||
using ln.bootstrap;
|
||||
using ln.http;
|
||||
|
||||
namespace ln.ai.assistant
|
||||
{
|
||||
|
||||
public static class Program
|
||||
{
|
||||
static void Main(string[] arguments)
|
||||
{
|
||||
Bootstrap.Debug = true;
|
||||
Bootstrap
|
||||
.DefaultInstance
|
||||
.Start(arguments);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
namespace ln.ai.assistant.persist;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
public class Cache<TKey, TValue>
|
||||
{
|
||||
private readonly int _maxSize;
|
||||
private readonly Dictionary<TKey, CachedItem> _cache;
|
||||
|
||||
private Timer _timer;
|
||||
private TimeSpan _expirationTime;
|
||||
|
||||
public Cache() :this(TimeSpan.Zero, 0) { }
|
||||
public Cache(int maxSize) :this(TimeSpan.Zero, maxSize) { }
|
||||
public Cache(TimeSpan expirationTime) :this(expirationTime, 0) { }
|
||||
|
||||
public Cache(TimeSpan expirationTime, int maxSize)
|
||||
{
|
||||
_maxSize = maxSize;
|
||||
_cache = new Dictionary<TKey, CachedItem>();
|
||||
|
||||
ExpirationTime = expirationTime;
|
||||
}
|
||||
|
||||
public void Add(TKey key, TValue value)
|
||||
{
|
||||
lock (_cache)
|
||||
{
|
||||
if ((_maxSize > 0) && (_cache.Count >= _maxSize))
|
||||
RemoveOldestItem();
|
||||
|
||||
_cache.Add(key, new CachedItem() { Value = value });
|
||||
}
|
||||
}
|
||||
|
||||
public void Set(TKey key, TValue value)
|
||||
{
|
||||
lock (_cache)
|
||||
{
|
||||
if ((_maxSize > 0) && (_cache.Count >= _maxSize))
|
||||
RemoveOldestItem();
|
||||
|
||||
_cache[key] = new CachedItem() { Value = value };
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(TKey key)
|
||||
{
|
||||
lock (_cache)
|
||||
_cache.Remove(key);
|
||||
}
|
||||
|
||||
public TValue Get(TKey key)
|
||||
{
|
||||
lock (_cache)
|
||||
{
|
||||
if (_cache.TryGetValue(key, out var item))
|
||||
{
|
||||
item.LastAccessed = DateTime.Now;
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
return default(TValue);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGet(TKey key, out TValue value)
|
||||
{
|
||||
lock (_cache)
|
||||
{
|
||||
if (_cache.TryGetValue(key, out var item))
|
||||
{
|
||||
item.LastAccessed = DateTime.Now;
|
||||
value = item.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default(TValue);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CleanUp(object state)
|
||||
{
|
||||
lock (_cache)
|
||||
{
|
||||
var expiredItems = _cache.Where(item => DateTime.Now - item.Value.LastAccessed > _expirationTime).ToList();
|
||||
foreach (var item in expiredItems)
|
||||
{
|
||||
_cache.Remove(item.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveOldestItem()
|
||||
{
|
||||
var oldestItem = _cache.OrderBy(item => item.Value.LastAccessed).First();
|
||||
_cache.Remove(oldestItem.Key);
|
||||
}
|
||||
|
||||
private class CachedItem
|
||||
{
|
||||
public TValue Value { get; set; }
|
||||
public DateTime LastAccessed { get; set; } = DateTime.Now;
|
||||
}
|
||||
|
||||
public TimeSpan ExpirationTime
|
||||
{
|
||||
get => _expirationTime;
|
||||
set
|
||||
{
|
||||
if (_timer is Timer)
|
||||
{
|
||||
if (value == TimeSpan.Zero)
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_timer.Change(value, value);
|
||||
}
|
||||
} else if (value != TimeSpan.Zero)
|
||||
{
|
||||
_timer = new Timer(CleanUp, null, value, value);
|
||||
}
|
||||
|
||||
_expirationTime = value;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue