Add the client script while index.html is served

Writing the tag into index.html on disk fails on most fresh installs
(linuxserver image, distro packages, Docker as a normal user) because
the web files belong to root. A middleware now adds the tag to the
response instead, so the ShareLink action and the guest lockdown work
right after install. The on-disk edit stays as a best-effort extra, and
the tag uses a relative src so it also works under a base URL.

Bump to 1.0.8.0.
Cette révision appartient à :
Francois CB
2026-09-18 16:31:45 +02:00
Parent 21ca14b780
révision 1c7acfe185
6 fichiers modifiés avec 155 ajouts et 10 suppressions
+120
Voir le fichier
@@ -0,0 +1,120 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Plugin.ShareLinks.Web;
/// <summary>
/// Adds the plugin's client script tag to the web client's index.html while
/// Jellyfin serves it, so nothing on disk has to be writable.
/// </summary>
public sealed class IndexHtmlScriptMiddleware
{
private const string ScriptMarker = "ShareLinks/ClientScript";
private const string Snippet = "\n<!-- ShareLinks:begin -->\n<script src=\"../ShareLinks/ClientScript\" defer></script>\n<!-- ShareLinks:end -->\n";
private readonly RequestDelegate _next;
private readonly ILogger<IndexHtmlScriptMiddleware> _logger;
private int _logged;
/// <summary>Initializes a new instance of the <see cref="IndexHtmlScriptMiddleware"/> class.</summary>
public IndexHtmlScriptMiddleware(RequestDelegate next, ILogger<IndexHtmlScriptMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>Invokes the middleware for one request.</summary>
/// <param name="context">The current request's HTTP context.</param>
public async Task InvokeAsync(HttpContext context)
{
var request = context.Request;
if (!HttpMethods.IsGet(request.Method) || !IsIndexHtmlRequest(request.Path.Value))
{
await _next(context).ConfigureAwait(false);
return;
}
// Ask the inner pipeline for the full, uncompressed file: a cached 304
// or a compressed body would leave nothing usable to rewrite.
request.Headers.Remove(HeaderNames.AcceptEncoding);
request.Headers.Remove(HeaderNames.IfNoneMatch);
request.Headers.Remove(HeaderNames.IfModifiedSince);
request.Headers.Remove(HeaderNames.IfRange);
request.Headers.Remove(HeaderNames.Range);
var originalBody = context.Response.Body;
using var buffer = new MemoryStream();
context.Response.Body = buffer;
try
{
await _next(context).ConfigureAwait(false);
}
finally
{
context.Response.Body = originalBody;
}
var bytes = buffer.ToArray();
if (context.Response.StatusCode == StatusCodes.Status200OK
&& context.Response.ContentType is not null
&& context.Response.ContentType.StartsWith("text/html", StringComparison.OrdinalIgnoreCase))
{
bytes = AddScriptTag(context, bytes);
}
if (bytes.Length == 0)
{
return;
}
await originalBody.WriteAsync(bytes, context.RequestAborted).ConfigureAwait(false);
}
private static bool IsIndexHtmlRequest(string? path)
{
return !string.IsNullOrEmpty(path)
&& (path.EndsWith("/web/", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith("/web/index.html", StringComparison.OrdinalIgnoreCase));
}
private byte[] AddScriptTag(HttpContext context, byte[] original)
{
try
{
var html = Encoding.UTF8.GetString(original);
if (html.Contains(ScriptMarker, StringComparison.OrdinalIgnoreCase))
{
// Already tagged, whether by this middleware, an older on-disk
// injection, or by hand. Leave it alone.
return original;
}
var bodyIndex = html.LastIndexOf("</body>", StringComparison.OrdinalIgnoreCase);
html = bodyIndex >= 0 ? html.Insert(bodyIndex, Snippet) : html + Snippet;
var updated = Encoding.UTF8.GetBytes(html);
context.Response.Headers.Remove(HeaderNames.ETag);
context.Response.Headers.Remove(HeaderNames.LastModified);
context.Response.ContentLength = updated.Length;
if (Interlocked.Exchange(ref _logged, 1) == 0)
{
_logger.LogInformation("ShareLinks: adding the client script to index.html as it is served.");
}
return updated;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "ShareLinks: could not add the client script to index.html.");
return original;
}
}
}
+21
Voir le fichier
@@ -0,0 +1,21 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace Jellyfin.Plugin.ShareLinks.Web;
/// <summary>
/// Puts <see cref="IndexHtmlScriptMiddleware"/> in front of Jellyfin's own pipeline.
/// </summary>
public sealed class IndexHtmlScriptStartupFilter : IStartupFilter
{
/// <inheritdoc />
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.UseMiddleware<IndexHtmlScriptMiddleware>();
next(app);
};
}
}
+7 -5
Voir le fichier
@@ -9,9 +9,11 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.ShareLinks.Web;
/// <summary>
/// Injects the ShareLinks client script into Jellyfin Web's index.html using
/// explicit markers so the edit can be applied and removed repeatedly without
/// drift.
/// Best-effort extra: injects the ShareLinks client script into Jellyfin Web's
/// index.html on disk at startup, using explicit markers so the edit can be
/// applied and removed repeatedly without drift. <see cref="IndexHtmlScriptMiddleware"/>
/// adds the same tag while Jellyfin serves the page, which is what makes the
/// guest flow work even when this cannot write to disk.
/// </summary>
public sealed class WebInjectionHostedService : IHostedService
{
@@ -39,7 +41,7 @@ public sealed class WebInjectionHostedService : IHostedService
}
catch (Exception ex)
{
_logger.LogWarning(ex, "ShareLinks: could not inject client script into web index.html.");
_logger.LogDebug(ex, "ShareLinks: could not write the script tag into index.html, it is added when the page is served instead.");
}
return Task.CompletedTask;
@@ -72,7 +74,7 @@ public sealed class WebInjectionHostedService : IHostedService
TryBackup(path, path + ".sharelinks.bak");
var snippet = "\n" + Begin + "\n<script src=\"/ShareLinks/ClientScript\" defer></script>\n" + End + "\n";
var snippet = "\n" + Begin + "\n<script src=\"../ShareLinks/ClientScript\" defer></script>\n" + End + "\n";
var bodyIndex = html.LastIndexOf("</body>", StringComparison.OrdinalIgnoreCase);
html = bodyIndex >= 0 ? html.Insert(bodyIndex, snippet) : html + snippet;