refuse an over-the-ceiling viewer without destroying the link
The ceiling shipped in 1.0.3.0 did the opposite of its job. Jellyfin throws MediaBrowser.Controller.Net.SecurityException when a user is at their session limit, and the catch that was meant to handle it named System.Security's type of the same name, so it never matched. The exception fell through to the failure path, which marks the record failed and runs cleanup, which deletes the guest account. Live result with the ceiling at 3: the fourth viewer got a dead-link page, the three already watching were kicked out, and the link was gone. Rather than swap one exception type for another and trust it, the over-the-ceiling viewer is now turned away before anything is written at all: no tag work, no status change, the guest account untouched. The check mirrors what Jellyfin does when it creates a session, counting sessions for the guest against the ceiling. The typed catch stays as a backstop for the race between the check and the call, and it now names the right type. A link that has never been redeemed has no guest yet, so it can never be at a ceiling, and 0 still means no limit.
Cette révision appartient à :
@@ -6,9 +6,9 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<RootNamespace>Jellyfin.Plugin.ShareLinks</RootNamespace>
|
||||
<AssemblyName>Jellyfin.Plugin.ShareLinks</AssemblyName>
|
||||
<Version>1.0.3.0</Version>
|
||||
<AssemblyVersion>1.0.3.0</AssemblyVersion>
|
||||
<FileVersion>1.0.3.0</FileVersion>
|
||||
<Version>1.0.3.1</Version>
|
||||
<AssemblyVersion>1.0.3.1</AssemblyVersion>
|
||||
<FileVersion>1.0.3.1</FileVersion>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -112,6 +112,19 @@ public sealed class ShareLinkRedemptionService
|
||||
return new ShareLinkRedemptionResult();
|
||||
}
|
||||
|
||||
// Turn an over-the-ceiling viewer away before anything is written: no tag
|
||||
// work, no status change, no guest account touched. Jellyfin enforces the
|
||||
// same limit itself when the session is created, but it does so by throwing,
|
||||
// and a throw here would land in the failure path below and tear the whole
|
||||
// share down on the people already watching.
|
||||
if (IsAtViewerCeiling(record))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"ShareLinks: record {RecordId} is at its viewer ceiling; turning a viewer away.",
|
||||
record.Id);
|
||||
return new ShareLinkRedemptionResult { AtCapacity = true };
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(record.ItemId, out var itemId))
|
||||
{
|
||||
await HandleFailureAsync(record, "Shared item snapshot is invalid.", cancellationToken).ConfigureAwait(false);
|
||||
@@ -176,15 +189,17 @@ public sealed class ShareLinkRedemptionService
|
||||
record.CleanupError = null;
|
||||
await _store.UpdateAsync(record, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SecurityException ex)
|
||||
catch (MediaBrowser.Controller.Net.SecurityException ex)
|
||||
{
|
||||
// The link is fine, the guest account has simply reached its viewer
|
||||
// ceiling. Leave the record and the guest alone: marking this failed
|
||||
// would tear down the account and throw out everyone already watching.
|
||||
// Backstop for the race between the ceiling check above and this call.
|
||||
// Jellyfin's own type, NOT System.Security.SecurityException. The link is
|
||||
// fine, so leave the record and the guest exactly as they were: marking
|
||||
// this failed would tear the account down and throw out everyone already
|
||||
// watching.
|
||||
_logger.LogInformation(ex, "ShareLinks: record {RecordId} hit its viewer ceiling while creating the session.", record.Id);
|
||||
record.Status = record.RedeemedAtUtc.HasValue ? ShareLinkStatus.Redeemed : ShareLinkStatus.Active;
|
||||
record.CleanupError = null;
|
||||
await _store.UpdateAsync(record, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation(ex, "ShareLinks: record {RecordId} is at its viewer ceiling; turning a viewer away.", record.Id);
|
||||
return new ShareLinkRedemptionResult { AtCapacity = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -200,6 +215,32 @@ public sealed class ShareLinkRedemptionService
|
||||
return new ShareLinkRedemptionResult { Html = BuildBootstrapHtml(request, authResult, itemId) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the share already has as many viewers watching as it is
|
||||
/// allowed. Mirrors the limit Jellyfin applies when it creates a session, so we
|
||||
/// can refuse politely instead of letting it throw.
|
||||
/// </summary>
|
||||
private bool IsAtViewerCeiling(ShareLinkRecord record)
|
||||
{
|
||||
if (!record.GuestUserId.HasValue)
|
||||
{
|
||||
// Nobody has redeemed this link yet, so nothing can be at a ceiling.
|
||||
return false;
|
||||
}
|
||||
|
||||
var ceiling = record.OneUse
|
||||
? 1
|
||||
: Math.Max(Plugin.Instance?.Configuration.MaxConcurrentViewers ?? 0, 0);
|
||||
if (ceiling < 1)
|
||||
{
|
||||
// 0 means no limit, the same way Jellyfin reads it.
|
||||
return false;
|
||||
}
|
||||
|
||||
var guestUserId = record.GuestUserId.Value;
|
||||
return _sessionManager.Sessions.Count(session => session.UserId.Equals(guestUserId)) >= ceiling;
|
||||
}
|
||||
|
||||
private async Task HandleTerminalRecordAsync(ShareLinkRecord record, ShareLinkStatus terminalStatus, string reason, CancellationToken cancellationToken)
|
||||
{
|
||||
record.Status = terminalStatus;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"guid": "68540b76-ee74-436d-85ff-2abc884bbea6",
|
||||
"name": "ShareLinks",
|
||||
"version": "1.0.3.0",
|
||||
"version": "1.0.3.1",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"framework": "net9.0",
|
||||
"owner": "Franciskid",
|
||||
"overview": "Secure expiring guest-share links for Jellyfin items.",
|
||||
"description": "Adds secure, expiring share links for Jellyfin items with JSON-backed storage, token hashing, and cleanup scaffolding.",
|
||||
"category": "General",
|
||||
"timestamp": "2026-07-26T19:30:00.0000000Z"
|
||||
"timestamp": "2026-07-26T20:00:00.0000000Z"
|
||||
}
|
||||
|
||||
Référencer dans un nouveau ticket
Bloquer un utilisateur