diff --git a/back/src/Kyoo.Core/Controllers/Base64RouteConstraint.cs b/back/src/Kyoo.Core/Controllers/Base64RouteConstraint.cs new file mode 100644 index 00000000..e69177a1 --- /dev/null +++ b/back/src/Kyoo.Core/Controllers/Base64RouteConstraint.cs @@ -0,0 +1,42 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Kyoo.Core.Controllers; + +public class Base64RouteConstraint : IRouteConstraint +{ + static Regex Base64Reg = new("^[-A-Za-z0-9+/]*={0,3}$"); + + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection + ) + { + return values.TryGetValue(routeKey, out object? val) + && val is string str + && Base64Reg.IsMatch(str); + } +} diff --git a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs index 452aed44..5dd49309 100644 --- a/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs +++ b/back/src/Kyoo.Core/Extensions/ServiceExtensions.cs @@ -76,6 +76,7 @@ public static class ServiceExtensions services.Configure(x => { x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint)); + x.ConstraintMap.Add("base64", typeof(Base64RouteConstraint)); }); services.AddResponseCompression(x => diff --git a/back/src/Kyoo.Core/Views/Content/ProxyApi.cs b/back/src/Kyoo.Core/Views/Content/ProxyApi.cs deleted file mode 100644 index 28b36253..00000000 --- a/back/src/Kyoo.Core/Views/Content/ProxyApi.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using AspNetCore.Proxy; -using AspNetCore.Proxy.Options; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models.Permissions; -using Kyoo.Abstractions.Models.Utils; -using Kyoo.Utils; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.Core.Api; - -/// -/// Proxy to other services -/// -[ApiController] -[Obsolete("Use /episode/id/master.m3u8 or routes like that")] -public class ProxyApi(ILibraryManager library) : Controller -{ - private Task _Proxy(string route, (string path, string route) info) - { - HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder - .Instance.WithBeforeSend( - (ctx, req) => - { - req.Headers.Add("X-Path", info.path); - req.Headers.Add("X-Route", info.route); - return Task.CompletedTask; - } - ) - .WithHandleFailure( - async (context, exception) => - { - context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - await context.Response.WriteAsJsonAsync( - new RequestError("Service unavailable") - ); - } - ) - .Build(); - return this.HttpProxyAsync($"{Transcoder.TranscoderUrl}{route}", proxyOptions); - } - - /// - /// Transcoder proxy - /// - /// - /// Simply proxy requests to the transcoder - /// - /// The path of the transcoder. - /// The return value of the transcoder. - [Route("video/{type}/{id:id}/{**rest}")] - [Permission("video", Kind.Read)] - [Obsolete("Use /episode/id/master.m3u8 or routes like that")] - public async Task Proxy( - string type, - Identifier id, - string rest, - [FromQuery] Dictionary query - ) - { - string path = await ( - type is "movie" or "movies" - ? id.Match( - async id => (await library.Movies.Get(id)).Path, - async slug => (await library.Movies.Get(slug)).Path - ) - : id.Match( - async id => (await library.Episodes.Get(id)).Path, - async slug => (await library.Episodes.Get(slug)).Path - ) - ); - await _Proxy(rest + query.ToQueryString(), (path, $"{type}/{id}")); - } -} diff --git a/back/src/Kyoo.Core/Views/Content/VideoApi.cs b/back/src/Kyoo.Core/Views/Content/VideoApi.cs new file mode 100644 index 00000000..3415986a --- /dev/null +++ b/back/src/Kyoo.Core/Views/Content/VideoApi.cs @@ -0,0 +1,122 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System; +using System.Threading.Tasks; +using AspNetCore.Proxy; +using AspNetCore.Proxy.Options; +using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Abstractions.Models.Permissions; +using Kyoo.Abstractions.Models.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using static Kyoo.Abstractions.Models.Utils.Constants; + +namespace Kyoo.Core.Api; + +/// +/// Private routes of the transcoder. +/// Url for these routes will be returned from /info or /master.m3u8 routes. +/// This should not be called manually +/// +[ApiController] +[Route("videos")] +[Route("video", Order = AlternativeRoute)] +[Permission("video", Kind.Read, Group = Group.Overall)] +[ApiDefinition("Video", Group = OtherGroup)] +public class VideoApi : Controller +{ + public static string TranscoderUrl = + Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666"; + + private Task _Proxy(string route) + { + HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder + .Instance.WithHandleFailure( + async (context, exception) => + { + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + await context.Response.WriteAsJsonAsync( + new RequestError("Service unavailable") + ); + } + ) + .Build(); + return this.HttpProxyAsync($"{TranscoderUrl}/{route}", proxyOptions); + } + + [HttpGet("{path:base64}/direct")] + [PartialPermission(Kind.Play)] + [ProducesResponseType(StatusCodes.Status206PartialContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetDirectStream(string path) + { + await _Proxy($"{path}/direct"); + } + + [HttpGet("{path:base64}/master.m3u8")] + [PartialPermission(Kind.Play)] + [ProducesResponseType(StatusCodes.Status206PartialContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetMaster(string path) + { + await _Proxy($"{path}/master.m3u8"); + } + + [HttpGet("{path:base64}/{quality}/index.m3u8")] + [PartialPermission(Kind.Play)] + public async Task GetVideoIndex(string path, string quality) + { + await _Proxy($"{path}/{quality}/index.m3u8"); + } + + [HttpGet("{path:base64}/{quality}/{segment}")] + [PartialPermission(Kind.Play)] + public async Task GetVideoSegment(string path, string quality, string segment) + { + await _Proxy($"{path}/{quality}/{segment}"); + } + + [HttpGet("{path:base64}/audio/{audio}/index.m3u8")] + [PartialPermission(Kind.Play)] + public async Task GetAudioIndex(string path, string audio) + { + await _Proxy($"{path}/audio/{audio}/index.m3u8"); + } + + [HttpGet("{path:base64}/audio/{audio}/{segment}")] + [PartialPermission(Kind.Play)] + public async Task GetAudioSegment(string path, string audio, string segment) + { + await _Proxy($"{path}/audio/{audio}/{segment}"); + } + + [HttpGet("{path:base64}/attachment/{name}")] + [PartialPermission(Kind.Play)] + public async Task GetAttachment(string path, string name) + { + await _Proxy($"{path}/attachment/{name}"); + } + + [HttpGet("{path:base64}/subtitle/{name}")] + [PartialPermission(Kind.Play)] + public async Task GetSubtitle(string path, string name) + { + await _Proxy($"{path}/subtitle/{name}"); + } +} diff --git a/back/src/Kyoo.Core/Views/Helper/Transcoder.cs b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs index 337f437b..43f25a8d 100644 --- a/back/src/Kyoo.Core/Views/Helper/Transcoder.cs +++ b/back/src/Kyoo.Core/Views/Helper/Transcoder.cs @@ -17,6 +17,7 @@ // along with Kyoo. If not, see . using System; +using System.Text; using System.Threading.Tasks; using AspNetCore.Proxy; using AspNetCore.Proxy.Options; @@ -29,27 +30,13 @@ using Microsoft.AspNetCore.Mvc; namespace Kyoo.Core.Api; -public static class Transcoder -{ - public static string TranscoderUrl = - Environment.GetEnvironmentVariable("TRANSCODER_URL") ?? "http://transcoder:7666"; -} - public abstract class TranscoderApi(IRepository repository) : CrudThumbsApi(repository) where T : class, IResource, IThumbnails, IQuery { - private Task _Proxy(string route, (string path, string route) info) + private Task _Proxy(string route) { HttpProxyOptions proxyOptions = HttpProxyOptionsBuilder - .Instance.WithBeforeSend( - (ctx, req) => - { - req.Headers.Add("X-Path", info.path); - req.Headers.Add("X-Route", info.route); - return Task.CompletedTask; - } - ) - .WithHandleFailure( + .Instance.WithHandleFailure( async (context, exception) => { context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; @@ -59,10 +46,16 @@ public abstract class TranscoderApi(IRepository repository) : CrudThumbsAp } ) .Build(); - return this.HttpProxyAsync($"{Transcoder.TranscoderUrl}{route}", proxyOptions); + return this.HttpProxyAsync($"{VideoApi.TranscoderUrl}/{route}", proxyOptions); } - protected abstract Task<(string path, string route)> GetPath(Identifier identifier); + protected abstract Task GetPath(Identifier identifier); + + private async Task _GetPath64(Identifier identifier) + { + string path = await GetPath(identifier); + return Convert.ToBase64String(Encoding.UTF8.GetBytes(path)); + } /// /// Direct stream @@ -76,11 +69,12 @@ public abstract class TranscoderApi(IRepository repository) : CrudThumbsAp /// No episode with the given ID or slug could be found. [HttpGet("{identifier:id}/direct")] [PartialPermission(Kind.Play)] - [ProducesResponseType(StatusCodes.Status206PartialContent)] + [ProducesResponseType(StatusCodes.Status302Found)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetDirectStream(Identifier identifier) + public async Task GetDirectStream(Identifier identifier) { - await _Proxy("/direct", await GetPath(identifier)); + // TODO: Remove the /api and use a proxy rewrite instead. + return Redirect($"/api/video/{await _GetPath64(identifier)}/direct"); } /// @@ -96,39 +90,12 @@ public abstract class TranscoderApi(IRepository repository) : CrudThumbsAp /// No episode with the given ID or slug could be found. [HttpGet("{identifier:id}/master.m3u8")] [PartialPermission(Kind.Play)] - [ProducesResponseType(StatusCodes.Status206PartialContent)] + [ProducesResponseType(StatusCodes.Status302Found)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetMaster(Identifier identifier) + public async Task GetMaster(Identifier identifier) { - await _Proxy("/master.m3u8", await GetPath(identifier)); - } - - [HttpGet("{identifier:id}/{quality}/index.m3u8")] - [PartialPermission(Kind.Play)] - public async Task GetVideoIndex(Identifier identifier, string quality) - { - await _Proxy($"/{quality}/index.m3u8", await GetPath(identifier)); - } - - [HttpGet("{identifier:id}/{quality}/{segment}")] - [PartialPermission(Kind.Play)] - public async Task GetVideoSegment(Identifier identifier, string quality, string segment) - { - await _Proxy($"/{quality}/{segment}", await GetPath(identifier)); - } - - [HttpGet("{identifier:id}/audio/{audio}/index.m3u8")] - [PartialPermission(Kind.Play)] - public async Task GetAudioIndex(Identifier identifier, string audio) - { - await _Proxy($"/audio/{audio}/index.m3u8", await GetPath(identifier)); - } - - [HttpGet("{identifier:id}/audio/{audio}/{segment}")] - [PartialPermission(Kind.Play)] - public async Task GetAudioSegment(Identifier identifier, string audio, string segment) - { - await _Proxy($"/audio/{audio}/{segment}", await GetPath(identifier)); + // TODO: Remove the /api and use a proxy rewrite instead. + return Redirect($"/api/video/{await _GetPath64(identifier)}/master.m3u8"); } /// @@ -144,7 +111,7 @@ public abstract class TranscoderApi(IRepository repository) : CrudThumbsAp [PartialPermission(Kind.Read)] public async Task GetInfo(Identifier identifier) { - await _Proxy("/info", await GetPath(identifier)); + await _Proxy($"{await _GetPath64(identifier)}/info"); } /// @@ -160,7 +127,7 @@ public abstract class TranscoderApi(IRepository repository) : CrudThumbsAp [PartialPermission(Kind.Read)] public async Task GetThumbnails(Identifier identifier) { - await _Proxy("/thumbnails.png", await GetPath(identifier)); + await _Proxy($"{await _GetPath64(identifier)}/thumbnails.png"); } /// @@ -177,6 +144,6 @@ public abstract class TranscoderApi(IRepository repository) : CrudThumbsAp [PartialPermission(Kind.Read)] public async Task GetThumbnailsVtt(Identifier identifier) { - await _Proxy("/thumbnails.vtt", await GetPath(identifier)); + await _Proxy($"{await _GetPath64(identifier)}/thumbnails.vtt"); } } diff --git a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs index 9fb268a0..b26c196c 100644 --- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs @@ -210,12 +210,12 @@ public class EpisodeApi(ILibraryManager libraryManager) await libraryManager.WatchStatus.DeleteEpisodeStatus(id, User.GetIdOrThrow()); } - protected override async Task<(string path, string route)> GetPath(Identifier identifier) + protected override async Task GetPath(Identifier identifier) { string path = await identifier.Match( async id => (await Repository.Get(id)).Path, async slug => (await Repository.Get(slug)).Path ); - return (path, $"/episodes/{identifier}"); + return path; } } diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs index 1d95ab6f..274b4e31 100644 --- a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs @@ -221,12 +221,12 @@ public class MovieApi(ILibraryManager libraryManager) : TranscoderApi(lib await libraryManager.WatchStatus.DeleteMovieStatus(id, User.GetIdOrThrow()); } - protected override async Task<(string path, string route)> GetPath(Identifier identifier) + protected override async Task GetPath(Identifier identifier) { string path = await identifier.Match( async id => (await Repository.Get(id)).Path, async slug => (await Repository.Get(slug)).Path ); - return (path, $"/movies/{identifier}"); + return path; } }