From fa2598084f612f1ab108f534aa8d336e77d1db4d Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 20 Apr 2024 16:54:58 +0200 Subject: [PATCH] Hard cutover https://github.com/C9Glax/tranga/pull/167#issuecomment-2067689986 --- Tranga/Server.cs | 807 ---------------------------------------- Tranga/Server/Server.cs | 196 ++++++++++ Tranga/ServerV2.cs | 44 --- Tranga/Tranga.cs | 4 +- 4 files changed, 198 insertions(+), 853 deletions(-) delete mode 100644 Tranga/Server.cs create mode 100644 Tranga/Server/Server.cs delete mode 100644 Tranga/ServerV2.cs diff --git a/Tranga/Server.cs b/Tranga/Server.cs deleted file mode 100644 index aa6aea2..0000000 --- a/Tranga/Server.cs +++ /dev/null @@ -1,807 +0,0 @@ -using System.Net; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Tranga.Jobs; -using Tranga.LibraryConnectors; -using Tranga.MangaConnectors; -using Tranga.NotificationConnectors; - -namespace Tranga; - -public partial class Server : GlobalBase -{ - private readonly HttpListener _listener = new(); - private readonly Tranga _parent; - - public Server(Tranga parent) : base(parent) - { - this._parent = parent; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - this._listener.Prefixes.Add($"http://*:{settings.apiPortNumber}/"); - else - this._listener.Prefixes.Add($"http://localhost:{settings.apiPortNumber}/"); - Thread listenThread = new(Listen); - listenThread.Start(); - Thread watchThread = new(WatchRunning); - watchThread.Start(); - } - - private void WatchRunning() - { - while (_parent.keepRunning) - Thread.Sleep(1000); - this._listener.Close(); - } - - private void Listen() - { - this._listener.Start(); - foreach (string prefix in this._listener.Prefixes) - Log($"Listening on {prefix}"); - while (this._listener.IsListening && _parent.keepRunning) - { - try - { - HttpListenerContext context = this._listener.GetContext(); - //Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}"); - Task t = new(() => - { - HandleRequest(context); - }); - t.Start(); - } - catch (HttpListenerException) - { - - } - } - } - - private void HandleRequest(HttpListenerContext context) - { - HttpListenerRequest request = context.Request; - HttpListenerResponse response = context.Response; - if (request.HttpMethod == "OPTIONS") - SendResponse(HttpStatusCode.OK, context.Response); - if (request.Url!.LocalPath.Contains("favicon")) - SendResponse(HttpStatusCode.NoContent, response); - - if (Regex.IsMatch(request.Url.LocalPath, "/v2(/.*)?")) - { - HandleRequestV2(context); - return; - } - - switch (request.HttpMethod) - { - case "GET": - HandleGet(request, response); - break; - case "POST": - HandlePost(request, response); - break; - case "DELETE": - HandleDelete(request, response); - break; - default: - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - } - - private Dictionary GetRequestVariables(string query) - { - Dictionary ret = new(); - Regex queryRex = new(@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*"); - if (!queryRex.IsMatch(query)) - return ret; - query = query.Substring(1); - foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3)) - { - string var = keyValuePair.Split('=')[0]; - string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " "); - val = Regex.Replace(val, "%[0-9]{2}", ""); - ret.Add(var, val); - } - return ret; - } - - private Dictionary GetRequestBody(HttpListenerRequest request) - { - if (!request.HasEntityBody) - { - Log("No request body"); - return new Dictionary(); - } - Stream body = request.InputStream; - Encoding encoding = request.ContentEncoding; - using StreamReader streamReader = new (body, encoding); - try - { - Dictionary requestBody = - JsonConvert.DeserializeObject>(streamReader.ReadToEnd()) - ?? new(); - return requestBody; - } - catch (JsonException e) - { - Log(e.Message); - } - return new Dictionary(); - } - - private void HandleGet(HttpListenerRequest request, HttpListenerResponse response) - { - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); - string? connectorName, jobId, internalId; - MangaConnector? connector; - Manga? manga; - string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; - switch (path) - { - case "Connectors": - SendResponse(HttpStatusCode.OK, response, _parent.GetConnectors().Select(con => con.name).ToArray()); - break; - case "Manga/Cover": - if (!requestVariables.TryGetValue("internalId", out internalId) || - !_parent.TryGetPublicationById(internalId, out manga)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - string filePath = settings.GetFullCoverPath((Manga)manga!); - if (File.Exists(filePath)) - { - FileStream coverStream = new(filePath, FileMode.Open); - SendResponse(HttpStatusCode.OK, response, coverStream); - } - else - { - SendResponse(HttpStatusCode.NotFound, response); - } - break; - case "Manga/FromConnector": - requestVariables.TryGetValue("title", out string? title); - requestVariables.TryGetValue("url", out string? url); - if (!requestVariables.TryGetValue("connector", out connectorName) || - !_parent.TryGetConnector(connectorName, out connector) || - (title is null && url is null)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (url is not null) - { - HashSet ret = new(); - manga = connector!.GetMangaFromUrl(url); - if (manga is not null) - ret.Add((Manga)manga); - SendResponse(HttpStatusCode.OK, response, ret); - }else - SendResponse(HttpStatusCode.OK, response, connector!.GetManga(title!)); - break; - case "Manga/Chapters": - if(!requestVariables.TryGetValue("connector", out connectorName) || - !requestVariables.TryGetValue("internalId", out internalId) || - !_parent.TryGetConnector(connectorName, out connector) || - !_parent.TryGetPublicationById(internalId, out manga)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - requestVariables.TryGetValue("translatedLanguage", out string? translatedLanguage); - SendResponse(HttpStatusCode.OK, response, connector!.GetChapters((Manga)manga!, translatedLanguage??"en")); - break; - case "Jobs": - if (!requestVariables.TryGetValue("jobId", out jobId)) - { - if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId)) - SendResponse(HttpStatusCode.BadRequest, response); - else - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId)); - break; - } - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs); - break; - case "Jobs/Progress": - if (requestVariables.TryGetValue("jobId", out jobId)) - { - if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId)) - SendResponse(HttpStatusCode.BadRequest, response); - else - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId).progressToken); - break; - } - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Select(jjob => jjob.progressToken)); - break; - case "Jobs/Running": - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Running)); - break; - case "Jobs/Waiting": - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Standby).OrderBy(jjob => jjob.nextExecution)); - break; - case "Jobs/MonitorJobs": - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob is DownloadNewChapters).OrderBy(jjob => ((DownloadNewChapters)jjob).manga.sortName)); - break; - case "Settings": - SendResponse(HttpStatusCode.OK, response, settings); - break; - case "Settings/userAgent": - SendResponse(HttpStatusCode.OK, response, settings.userAgent); - break; - case "Settings/customRequestLimit": - SendResponse(HttpStatusCode.OK, response, settings.requestLimits); - break; - case "Settings/AprilFoolsMode": - SendResponse(HttpStatusCode.OK, response, settings.aprilFoolsMode); - break; - case "NotificationConnectors": - SendResponse(HttpStatusCode.OK, response, notificationConnectors); - break; - case "NotificationConnectors/Types": - SendResponse(HttpStatusCode.OK, response, - Enum.GetValues().Select(nc => new KeyValuePair((byte)nc, Enum.GetName(nc)))); - break; - case "LibraryConnectors": - SendResponse(HttpStatusCode.OK, response, libraryConnectors); - break; - case "LibraryConnectors/Types": - SendResponse(HttpStatusCode.OK, response, - Enum.GetValues().Select(lc => new KeyValuePair((byte)lc, Enum.GetName(lc)))); - break; - case "Ping": - SendResponse(HttpStatusCode.OK, response, "Pong"); - break; - case "LogMessages": - if (logger is null || !File.Exists(logger?.logFilePath)) - { - SendResponse(HttpStatusCode.NotFound, response); - break; - } - - if (requestVariables.TryGetValue("count", out string? count)) - { - try - { - uint messageCount = uint.Parse(count); - SendResponse(HttpStatusCode.OK, response, logger.Tail(messageCount)); - } - catch (FormatException f) - { - SendResponse(HttpStatusCode.InternalServerError, response, f); - } - }else - SendResponse(HttpStatusCode.OK, response, logger.GetLog()); - break; - case "LogFile": - if (logger is null || !File.Exists(logger?.logFilePath)) - { - SendResponse(HttpStatusCode.NotFound, response); - break; - } - - string logDir = new FileInfo(logger.logFilePath).DirectoryName!; - string tmpFilePath = Path.Join(logDir, "Tranga.log"); - File.Copy(logger.logFilePath, tmpFilePath); - SendResponse(HttpStatusCode.OK, response, new FileStream(tmpFilePath, FileMode.Open)); - File.Delete(tmpFilePath); - break; - default: - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - } - - private void HandlePost(HttpListenerRequest request, HttpListenerResponse response) - { - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI - Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body - Dictionary requestParams = new(); //The actual variable used for the API - - //Concatenate the two dictionaries for compatibility with older versions of front-ends - requestParams = requestVariables.Concat(requestBody).ToDictionary(x => x.Key, x => x.Value); - - string? connectorName, internalId, jobId, chapterNumStr, customFolderName, translatedLanguage, notificationConnectorStr, libraryConnectorStr; - MangaConnector? connector; - Manga? tmpManga; - Manga manga; - Job? job; - NotificationConnector.NotificationConnectorType notificationConnectorType; - LibraryConnector.LibraryType libraryConnectorType; - string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; - switch (path) - { - case "Manga": - if(!requestParams.TryGetValue("internalId", out internalId) || - !_parent.TryGetPublicationById(internalId, out tmpManga)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - manga = (Manga)tmpManga!; - SendResponse(HttpStatusCode.OK, response, manga); - break; - case "Jobs/MonitorManga": - if(!requestParams.TryGetValue("connector", out connectorName) || - !requestParams.TryGetValue("internalId", out internalId) || - !requestParams.TryGetValue("interval", out string? intervalStr) || - !_parent.TryGetConnector(connectorName, out connector)|| - !_parent.TryGetPublicationById(internalId, out tmpManga) || - !TimeSpan.TryParse(intervalStr, out TimeSpan interval)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - manga = (Manga)tmpManga!; - - if (requestParams.TryGetValue("ignoreBelowChapterNum", out chapterNumStr)) - { - if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - manga.ignoreChaptersBelow = chapterNum; - } - - if (requestParams.TryGetValue("customFolderName", out customFolderName)) - manga.MovePublicationFolder(settings.downloadLocation, customFolderName); - requestParams.TryGetValue("translatedLanguage", out translatedLanguage); - - _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, true, interval, translatedLanguage: translatedLanguage??"en")); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Jobs/DownloadNewChapters": - if(!requestParams.TryGetValue("connector", out connectorName) || - !requestParams.TryGetValue("internalId", out internalId) || - !_parent.TryGetConnector(connectorName, out connector)|| - !_parent.TryGetPublicationById(internalId, out tmpManga)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - manga = (Manga)tmpManga!; - - if (requestParams.TryGetValue("ignoreBelowChapterNum", out chapterNumStr)) - { - if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - manga.ignoreChaptersBelow = chapterNum; - } - - if (requestParams.TryGetValue("customFolderName", out customFolderName)) - manga.MovePublicationFolder(settings.downloadLocation, customFolderName); - requestParams.TryGetValue("translatedLanguage", out translatedLanguage); - - _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, false, translatedLanguage: translatedLanguage??"en")); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Jobs/UpdateMetadata": - if (!requestParams.TryGetValue("internalId", out internalId)) - { - foreach (Job pJob in _parent.jobBoss.jobs.Where(possibleDncJob => - possibleDncJob.jobType is Job.JobType.DownloadNewChaptersJob).ToArray())//ToArray to avoid modyifying while adding new jobs - { - DownloadNewChapters dncJob = pJob as DownloadNewChapters ?? - throw new Exception("Has to be DownloadNewChapters Job"); - _parent.jobBoss.AddJob(new UpdateMetadata(this, dncJob.mangaConnector, dncJob.manga)); - } - SendResponse(HttpStatusCode.Accepted, response); - } - else - { - Job[] possibleDncJobs = _parent.jobBoss.GetJobsLike(internalId: internalId).ToArray(); - switch (possibleDncJobs.Length) - { - case <1: SendResponse(HttpStatusCode.BadRequest, response, "Could not find matching release"); break; - case >1: SendResponse(HttpStatusCode.BadRequest, response, "Multiple releases??"); break; - default: - DownloadNewChapters dncJob = possibleDncJobs[0] as DownloadNewChapters ?? - throw new Exception("Has to be DownloadNewChapters Job"); - _parent.jobBoss.AddJob(new UpdateMetadata(this, dncJob.mangaConnector, dncJob.manga)); - SendResponse(HttpStatusCode.Accepted, response); - break; - } - } - break; - case "Jobs/StartNow": - if (!requestParams.TryGetValue("jobId", out jobId) || - !_parent.jobBoss.TryGetJobById(jobId, out job)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - _parent.jobBoss.AddJobToQueue(job!); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Jobs/Cancel": - if (!requestParams.TryGetValue("jobId", out jobId) || - !_parent.jobBoss.TryGetJobById(jobId, out job)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - job!.Cancel(); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Settings/UpdateDownloadLocation": - if (!requestParams.TryGetValue("downloadLocation", out string? downloadLocation) || - !requestParams.TryGetValue("moveFiles", out string? moveFilesStr) || - !bool.TryParse(moveFilesStr, out bool moveFiles)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - settings.UpdateDownloadLocation(downloadLocation, moveFiles); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Settings/AprilFoolsMode": - if (!requestParams.TryGetValue("enabled", out string? aprilFoolsModeEnabledStr) || - bool.TryParse(aprilFoolsModeEnabledStr, out bool aprilFoolsModeEnabled)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - settings.UpdateAprilFoolsMode(aprilFoolsModeEnabled); - SendResponse(HttpStatusCode.Accepted, response); - break; - /*case "Settings/UpdateWorkingDirectory": - if (!requestParams.TryGetValue("workingDirectory", out string? workingDirectory)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - settings.UpdateWorkingDirectory(workingDirectory); - SendResponse(HttpStatusCode.Accepted, response); - break;*/ - case "Settings/userAgent": - if(!requestParams.TryGetValue("userAgent", out string? customUserAgent)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - settings.UpdateUserAgent(customUserAgent); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Settings/userAgent/Reset": - settings.UpdateUserAgent(null); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Settings/customRequestLimit": - if (!requestParams.TryGetValue("requestType", out string? requestTypeStr) || - !requestParams.TryGetValue("requestsPerMinute", out string? requestsPerMinuteStr) || - !Enum.TryParse(requestTypeStr, out RequestType requestType) || - !int.TryParse(requestsPerMinuteStr, out int requestsPerMinute)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (settings.requestLimits.ContainsKey(requestType)) - { - settings.requestLimits[requestType] = requestsPerMinute; - SendResponse(HttpStatusCode.Accepted, response); - }else - SendResponse(HttpStatusCode.BadRequest, response); - settings.ExportSettings(); - break; - case "Settings/customRequestLimit/Reset": - settings.requestLimits = TrangaSettings.DefaultRequestLimits; - settings.ExportSettings(); - break; - case "NotificationConnectors/Update": - if (!requestParams.TryGetValue("notificationConnector", out notificationConnectorStr) || - !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify) - { - if (!requestParams.TryGetValue("gotifyUrl", out string? gotifyUrl) || - !requestParams.TryGetValue("gotifyAppToken", out string? gotifyAppToken)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddNotificationConnector(new Gotify(this, gotifyUrl, gotifyAppToken)); - SendResponse(HttpStatusCode.Accepted, response); - }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea) - { - if (!requestParams.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddNotificationConnector(new LunaSea(this, lunaseaWebhook)); - SendResponse(HttpStatusCode.Accepted, response); - }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy) - { - if (!requestParams.TryGetValue("ntfyUrl", out string? ntfyUrl) || - !requestParams.TryGetValue("ntfyAuth", out string? ntfyAuth)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddNotificationConnector(new Ntfy(this, ntfyUrl, ntfyAuth)); - SendResponse(HttpStatusCode.Accepted, response); - } - else - { - SendResponse(HttpStatusCode.BadRequest, response); - } - break; - case "NotificationConnectors/Test": - NotificationConnector notificationConnector; - if (!requestParams.TryGetValue("notificationConnector", out notificationConnectorStr) || - !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify) - { - if (!requestParams.TryGetValue("gotifyUrl", out string? gotifyUrl) || - !requestParams.TryGetValue("gotifyAppToken", out string? gotifyAppToken)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - notificationConnector = new Gotify(this, gotifyUrl, gotifyAppToken); - }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea) - { - if (!requestParams.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - notificationConnector = new LunaSea(this, lunaseaWebhook); - }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy) - { - if (!requestParams.TryGetValue("ntfyUrl", out string? ntfyUrl) || - !requestParams.TryGetValue("ntfyAuth", out string? ntfyAuth)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - notificationConnector = new Ntfy(this, ntfyUrl, ntfyAuth); - } - else - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - notificationConnector.SendNotification("Tranga Test", "This is Test-Notification."); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "NotificationConnectors/Reset": - if (!requestParams.TryGetValue("notificationConnector", out notificationConnectorStr) || - !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - DeleteNotificationConnector(notificationConnectorType); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "LibraryConnectors/Update": - if (!requestParams.TryGetValue("libraryConnector", out libraryConnectorStr) || - !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (libraryConnectorType is LibraryConnector.LibraryType.Kavita) - { - if (!requestParams.TryGetValue("kavitaUrl", out string? kavitaUrl) || - !requestParams.TryGetValue("kavitaUsername", out string? kavitaUsername) || - !requestParams.TryGetValue("kavitaPassword", out string? kavitaPassword)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddLibraryConnector(new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword)); - SendResponse(HttpStatusCode.Accepted, response); - }else if (libraryConnectorType is LibraryConnector.LibraryType.Komga) - { - if (!requestParams.TryGetValue("komgaUrl", out string? komgaUrl) || - !requestParams.TryGetValue("komgaAuth", out string? komgaAuth)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddLibraryConnector(new Komga(this, komgaUrl, komgaAuth)); - SendResponse(HttpStatusCode.Accepted, response); - } - else - { - SendResponse(HttpStatusCode.BadRequest, response); - } - break; - case "LibraryConnectors/Test": - LibraryConnector libraryConnector; - if (!requestParams.TryGetValue("libraryConnector", out libraryConnectorStr) || - !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (libraryConnectorType is LibraryConnector.LibraryType.Kavita) - { - if (!requestParams.TryGetValue("kavitaUrl", out string? kavitaUrl) || - !requestParams.TryGetValue("kavitaUsername", out string? kavitaUsername) || - !requestParams.TryGetValue("kavitaPassword", out string? kavitaPassword)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - libraryConnector = new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword); - }else if (libraryConnectorType is LibraryConnector.LibraryType.Komga) - { - if (!requestParams.TryGetValue("komgaUrl", out string? komgaUrl) || - !requestParams.TryGetValue("komgaAuth", out string? komgaAuth)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - libraryConnector = new Komga(this, komgaUrl, komgaAuth); - } - else - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - libraryConnector.UpdateLibrary(); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "LibraryConnectors/Reset": - if (!requestParams.TryGetValue("libraryConnector", out libraryConnectorStr) || - !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - DeleteLibraryConnector(libraryConnectorType); - SendResponse(HttpStatusCode.Accepted, response); - break; - default: - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - } - - private void HandleDelete(HttpListenerRequest request, HttpListenerResponse response) - { - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI - Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body - Dictionary requestParams = new(); //The actual variable used for the API - - //Concatenate the two dictionaries for compatibility with older versions of front-ends - requestParams = requestVariables.Concat(requestBody).ToDictionary(x => x.Key, x => x.Value); - - string? connectorName, internalId; - MangaConnector connector; - Manga manga; - string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; - switch (path) - { - case "Jobs": - if (!requestParams.TryGetValue("jobId", out string? jobId) || - !_parent.jobBoss.TryGetJobById(jobId, out Job? job)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - _parent.jobBoss.RemoveJob(job!); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Jobs/DownloadNewChapters": - if(!requestParams.TryGetValue("connector", out connectorName) || - !requestParams.TryGetValue("internalId", out internalId) || - _parent.GetConnector(connectorName) is null || - _parent.GetPublicationById(internalId) is null) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - connector = _parent.GetConnector(connectorName)!; - manga = (Manga)_parent.GetPublicationById(internalId)!; - _parent.jobBoss.RemoveJobs(_parent.jobBoss.GetJobsLike(connector, manga)); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "NotificationConnectors": - if (!requestParams.TryGetValue("notificationConnector", out string? notificationConnectorStr) || - !Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - DeleteNotificationConnector(notificationConnectorType); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "LibraryConnectors": - if (!requestParams.TryGetValue("libraryConnector", out string? libraryConnectorStr) || - !Enum.TryParse(libraryConnectorStr, - out LibraryConnector.LibraryType libraryConnectoryType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - DeleteLibraryConnector(libraryConnectoryType); - SendResponse(HttpStatusCode.Accepted, response); - break; - default: - Log("Invalid Request:"); - Log(request.Url!.Query); - foreach (KeyValuePair kvp in requestParams) - { - Log("Request variable = {0}, Variable Value = {1}", kvp.Key, kvp.Value); - } - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - } - - private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null) - { - //Log($"Response: {statusCode} {content}"); - response.StatusCode = (int)statusCode; - response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); - response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE"); - response.AddHeader("Access-Control-Max-Age", "1728000"); - response.AppendHeader("Access-Control-Allow-Origin", "*"); - - if (content is not Stream) - { - response.ContentType = "application/json"; - try - { - response.OutputStream.Write(content is not null - ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)) - : Array.Empty()); - response.OutputStream.Close(); - } - catch (HttpListenerException e) - { - Log(e.ToString()); - } - } - else if(content is FileStream stream) - { - string contentType = stream.Name.Split('.')[^1]; - switch (contentType.ToLower()) - { - case "gif": - response.ContentType = "image/gif"; - break; - case "png": - response.ContentType = "image/png"; - break; - case "jpg": - case "jpeg": - response.ContentType = "image/jpeg"; - break; - case "log": - response.ContentType = "text/plain"; - break; - } - stream.CopyTo(response.OutputStream); - response.OutputStream.Close(); - stream.Close(); - } - } -} \ No newline at end of file diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs new file mode 100644 index 0000000..d090aff --- /dev/null +++ b/Tranga/Server/Server.cs @@ -0,0 +1,196 @@ +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace Tranga.Server; + +public class Server : GlobalBase, IDisposable +{ + private readonly HttpListener _listener = new(); + private readonly Tranga _parent; + private bool _running = true; + + public Server(Tranga parent) : base(parent) + { + this._parent = parent; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + this._listener.Prefixes.Add($"http://*:{settings.apiPortNumber}/"); + else + this._listener.Prefixes.Add($"http://localhost:{settings.apiPortNumber}/"); + Thread listenThread = new(Listen); + listenThread.Start(); + while(_parent.keepRunning && _running) + Thread.Sleep(100); + this.Dispose(); + } + + private void Listen() + { + this._listener.Start(); + foreach (string prefix in this._listener.Prefixes) + Log($"Listening on {prefix}"); + while (this._listener.IsListening && _parent.keepRunning) + { + try + { + HttpListenerContext context = this._listener.GetContext(); + //Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}"); + Task t = new(() => + { + HandleRequest(context); + }); + t.Start(); + } + catch (HttpListenerException) + { + + } + } + } + + private void HandleRequest(HttpListenerContext context) + { + HttpListenerRequest request = context.Request; + HttpListenerResponse response = context.Response; + if (request.HttpMethod == "OPTIONS") + SendResponse(HttpStatusCode.OK, context.Response); + if (request.Url!.LocalPath.Contains("favicon")) + SendResponse(HttpStatusCode.NoContent, response); + string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; + + if (!Regex.IsMatch(request.Url.LocalPath, "/v2(/.*)?")) + { + SendResponse(HttpStatusCode.NotFound, response); + return; + } + + Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI + Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body + Dictionary requestParams = requestVariables.UnionBy(requestBody, v => v.Key) + .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API + + ValueTuple responseMessage = request.HttpMethod switch + { + "GET" => HandleGet(path, requestParams), + "POST" => HandlePost(path, requestParams), + "DELETE" => HandleDelete(path, requestParams), + _ => new ValueTuple(HttpStatusCode.MethodNotAllowed, null) + }; + + SendResponse(responseMessage.Item1, response, responseMessage.Item2); + } + + private Dictionary GetRequestVariables(string query) + { + Dictionary ret = new(); + Regex queryRex = new(@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*"); + if (!queryRex.IsMatch(query)) + return ret; + query = query.Substring(1); + foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3)) + { + string var = keyValuePair.Split('=')[0]; + string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " "); + val = Regex.Replace(val, "%[0-9]{2}", ""); + ret.Add(var, val); + } + return ret; + } + + private Dictionary GetRequestBody(HttpListenerRequest request) + { + if (!request.HasEntityBody) + { + Log("No request body"); + return new Dictionary(); + } + Stream body = request.InputStream; + Encoding encoding = request.ContentEncoding; + using StreamReader streamReader = new (body, encoding); + try + { + Dictionary requestBody = + JsonConvert.DeserializeObject>(streamReader.ReadToEnd()) + ?? new(); + return requestBody; + } + catch (JsonException e) + { + Log(e.Message); + } + return new Dictionary(); + } + + private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null) + { + //Log($"Response: {statusCode} {content}"); + response.StatusCode = (int)statusCode; + response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); + response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE"); + response.AddHeader("Access-Control-Max-Age", "1728000"); + response.AppendHeader("Access-Control-Allow-Origin", "*"); + + if (content is not Stream) + { + response.ContentType = "application/json"; + try + { + response.OutputStream.Write(content is not null + ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)) + : Array.Empty()); + response.OutputStream.Close(); + } + catch (HttpListenerException e) + { + Log(e.ToString()); + } + } + else if(content is FileStream stream) + { + string contentType = stream.Name.Split('.')[^1]; + switch (contentType.ToLower()) + { + case "gif": + response.ContentType = "image/gif"; + break; + case "png": + response.ContentType = "image/png"; + break; + case "jpg": + case "jpeg": + response.ContentType = "image/jpeg"; + break; + case "log": + response.ContentType = "text/plain"; + break; + } + stream.CopyTo(response.OutputStream); + response.OutputStream.Close(); + stream.Close(); + } + } + + private ValueTuple HandleGet(string path, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); + } + + private ValueTuple HandlePost(string path, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); + } + + private ValueTuple HandleDelete(string path, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); + } + + + public void Dispose() + { + _running = false; + ((IDisposable)_listener).Dispose(); + } +} \ No newline at end of file diff --git a/Tranga/ServerV2.cs b/Tranga/ServerV2.cs deleted file mode 100644 index 092a8cb..0000000 --- a/Tranga/ServerV2.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Net; -using System.Text.RegularExpressions; - -namespace Tranga; - -public partial class Server -{ - private void HandleRequestV2(HttpListenerContext context) - { - HttpListenerRequest request = context.Request; - HttpListenerResponse response = context.Response; - string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; - - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI - Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body - Dictionary requestParams = requestVariables.UnionBy(requestBody, v => v.Key) - .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API - - ValueTuple responseMessage = request.HttpMethod switch - { - "GET" => HandleGetV2(path, requestParams), - "POST" => HandlePostV2(path, requestParams), - "DELETE" => HandleDeleteV2(path, requestParams), - _ => new ValueTuple(HttpStatusCode.MethodNotAllowed, null) - }; - - SendResponse(responseMessage.Item1, response, responseMessage.Item2); - } - - private ValueTuple HandleGetV2(string path, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); - } - - private ValueTuple HandlePostV2(string path, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); - } - - private ValueTuple HandleDeleteV2(string path, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); - } -} \ No newline at end of file diff --git a/Tranga/Tranga.cs b/Tranga/Tranga.cs index cd3b3bc..91b1f19 100644 --- a/Tranga/Tranga.cs +++ b/Tranga/Tranga.cs @@ -8,7 +8,7 @@ public partial class Tranga : GlobalBase { public bool keepRunning; public JobBoss jobBoss; - private Server _server; + private Server.Server _server; private HashSet _connectors; public Tranga(Logger? logger, TrangaSettings settings) : base(logger, settings) @@ -30,7 +30,7 @@ public partial class Tranga : GlobalBase dir.Delete(); jobBoss = new(this, this._connectors); StartJobBoss(); - this._server = new Server(this); + this._server = new Server.Server(this); string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"}; SendNotifications("Tranga Started", emojis[Random.Shared.Next(0,emojis.Length-1)]); }