Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ npm run build # Production bundle

- **Report Bugs:** [GitHub Issues](https://github.com/Feramance/Torrentarr/issues)
- **Discussions:** [GitHub Discussions](https://github.com/Feramance/Torrentarr/discussions)
- **Security:** [SECURITY.md](SECURITY.md) (supported versions, vulnerability reporting)

## Contributing

Expand Down
31 changes: 31 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Security policy

## Supported versions

Security fixes are applied only to the **latest released version** of Torrentarr. Older releases are not maintained with backported security patches. If you report an issue affecting an older version, we may ask you to upgrade to the current release and confirm whether the problem still exists.

## Reporting a vulnerability

Please report security vulnerabilities responsibly so we can address them before public disclosure.

**Preferred:** Use [GitHub Security Advisories](https://github.com/Feramance/Torrentarr/security/advisories) for this repository (Report a vulnerability).

**Alternative:** Open a private discussion with maintainers if GitHub Advisories is not available, or contact the maintainers through the channels listed in the [README](README.md).

Include:

- A description of the issue and its impact
- Steps to reproduce or proof-of-concept, if safe to share
- Affected component (WebUI, Host, config, etc.) and version

We aim to acknowledge reports in a timely manner. Please avoid testing against production systems you do not own.

## Scope

This policy applies to the Torrentarr application and its official distribution artifacts (for example, release binaries published on GitHub). Third-party services you configure (qBittorrent, Radarr, Sonarr, Lidarr, reverse proxies, identity providers) follow their own security practices and are outside this project’s control.

## Deployment reminders

- When **`AuthDisabled`** is true, `/web/*` is not behind the login screen—restrict network access (firewall, bind address, or reverse proxy with authentication) if needed. **`/api/*`** still requires `WebUI.Token` (Bearer). Prefer keeping authentication enabled or using network controls when exposing Torrentarr beyond a trusted LAN.
- Prefer HTTPS in production (e.g. terminate TLS at a reverse proxy).
- See the project documentation for [WebUI authentication](docs/configuration/webui-authentication.md) and [API usage](docs/webui/api.md).
10 changes: 10 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ Theme = "Dark"
# View density (Comfortable or Compact)
ViewDensity = "Comfortable"

# Optional: restrict browser CORS to these origins (omit or set [] to allow any origin). See docs/configuration/webui-authentication.md
# CorsAllowedOrigins = [ "https://torrentarr.example.com" ]

[qBit]
# Disable qBittorrent integration (headless search-only mode)
Disabled = false
Expand All @@ -123,6 +126,13 @@ ManagedCategories = []
# Global tracker configurations (inherited by all Arr instances)
Trackers = []

# Example per-tracker override:
# [[qBit.Trackers]]
# Name = "Private Tracker"
# URI = "https://tracker.example.com/announce"
# Priority = 10
# SortTorrents = false

[qBit.CategorySeeding]
# Download rate limit per torrent in KB/s (-1 = unlimited)
DownloadRateLimitPerTorrent = -1
Expand Down
19 changes: 18 additions & 1 deletion docs/configuration/seeding.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Seeding Configuration
# Seeding Configuration

Configure intelligent seeding management with per-torrent and per-tracker rules for ratio limits, seeding time, and automatic cleanup.

Expand Down Expand Up @@ -437,6 +437,23 @@ MaxUploadRatio = 2.0

---

#### SortTorrents

```toml
SortTorrents = false
```

**Type:** Boolean
**Default:** `false`

When enabled for a tracker, torrents matching that tracker are reordered in the qBittorrent queue by tracker `Priority` during processing.

- Higher `Priority` trackers are pushed toward the top of the queue.
- This setting only affects torrents whose effective tracker configuration has `SortTorrents = true`.
- qBittorrent **Torrent Queuing** must be enabled for queue ordering effects to be visible.

---

#### URI

```toml
Expand Down
3 changes: 2 additions & 1 deletion docs/configuration/webui-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ RequireHttpsMetadata = true

## Deployment and security

- **CORS:** The WebUI server may allow cross-origin requests (e.g. `AllowAnyOrigin` in development). In sensitive deployments, restrict CORS to trusted origins via your reverse proxy or application configuration so that only your intended UI origin can call the API.
- **CORS:** When **`CorsAllowedOrigins`** in `[WebUI]` is **empty** (default), the server allows any origin (`AllowAnyOrigin`), which is convenient for local development and simple LAN setups. When you set **`CorsAllowedOrigins`** to one or more origins (e.g. `["https://torrentarr.example.com"]`), only those origins may call the API from the browser, with credentials allowed; restart after changing this value. You can also restrict cross-origin access entirely at your reverse proxy.
- **Auth disabled:** When **`AuthDisabled = true`**, `/web/*` routes do not require a login; **`/api/*`** still requires **`WebUI.Token`**. The WebUI shows a confirmation when you turn off authentication. If the service is exposed to the internet or untrusted networks, use a reverse proxy with authentication, VPN, or firewall rules—or keep authentication enabled.
- **HTTPS:** In production, serve Torrentarr over HTTPS (e.g. behind a reverse proxy with TLS). Session cookies use `SecurePolicy=SameAsRequest`, so they are only sent over HTTPS when the request is HTTPS.

---
Expand Down
15 changes: 14 additions & 1 deletion docs/configuration/webui.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ curl -H "Authorization: Bearer my-secure-token-12345" \

## Authentication

When **AuthDisabled** = `true` (default for existing configs), there is no login screen; the WebUI and API are protected only by the Token (or are public if Token was empty and has not yet been auto-generated). When **AuthDisabled** = `false`, browser users must either log in (local username/password and/or OIDC) or present the Bearer token. At least one of **LocalAuthEnabled** or **OIDCEnabled** should be true so the login page can offer a sign-in method.
When **AuthDisabled** = `true` (default for existing configs), there is no login screen; **`/api/*`** is protected by the **Token** (Bearer), while **`/web/*`** is open to the network path—use firewall, reverse proxy auth, or keep **AuthDisabled** = `false` if exposing to untrusted networks. The WebUI warns you when disabling authentication. When **AuthDisabled** = `false`, browser users must either log in (local username/password and/or OIDC) or present the Bearer token. At least one of **LocalAuthEnabled** or **OIDCEnabled** should be true so the login page can offer a sign-in method.

**New installs:** If Torrentarr creates the config file on first run (it did not exist before), the generated config has **AuthDisabled = false** and **LocalAuthEnabled = true**. Users see a welcome screen to set an admin username and password before accessing the rest of the WebUI. Existing configs are unchanged unless you edit auth settings.

Expand Down Expand Up @@ -337,6 +337,19 @@ Default color theme for the WebUI.

---

## CorsAllowedOrigins

```toml
# CorsAllowedOrigins = [ "https://torrentarr.example.com" ]
```

**Type:** Array of strings (optional)
**Default:** omitted or empty — any origin is allowed for cross-origin browser requests (legacy behavior).

When set to one or more origins, the server restricts CORS to those origins and allows credentials. Restart after changing this value. See [WebUI authentication — Deployment and security](webui-authentication.md#deployment-and-security).

---

## Complete Configuration Examples

### Example 1: Default (Public Access)
Expand Down
10 changes: 5 additions & 5 deletions docs/webui/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ Torrentarr provides dual endpoint patterns for flexibility:
| Pattern | Purpose | Authentication | Use Case |
|---------|---------|----------------|----------|
| `/api/*` | API-first endpoints | **Required** (Bearer token) | External clients, scripts, automation |
| `/web/*` | First-party endpoints | **Optional** (no token required) | WebUI, reverse proxies with auth bypass |
| `/web/*` | First-party endpoints | **Optional** when `AuthDisabled` is true (no login); login/OIDC or Bearer when auth is required | WebUI, reverse proxies with auth bypass |

Both patterns return identical responses. Choose based on your authentication requirements.
Both patterns return identical responses. Choose based on your authentication requirements. If you disable authentication in config, confirm the warning in the WebUI: untrusted network access should use a reverse proxy, VPN, or firewall—or keep authentication enabled.

### Interactive API (Swagger)

When Torrentarr is running, interactive API documentation is available at **`/swagger`** (for example, `http://localhost:6969/swagger`). Swagger UI lists all endpoints and lets you try them from the browser.

When `WebUI.Token` is set, use the **Authorize** button in Swagger UI, enter your Bearer token (or paste the value from `GET /web/token`), then click Authorize. Requests to `/api/*` endpoints will then include the token. `/web/*` endpoints do not require authorization.
When `WebUI.Token` is set, use the **Authorize** button in Swagger UI, enter your Bearer token (or paste the value from `GET /web/token`), then click Authorize. Requests to `/api/*` endpoints will then include the token. `/web/*` endpoints do not require the Bearer token when `AuthDisabled` is true (see [WebUI authentication](../configuration/webui-authentication.md) for login when auth is enabled).

---

Expand Down Expand Up @@ -63,7 +63,7 @@ The following endpoints are **always public** (no authentication):
- `GET /ui` - WebUI entry point
- `GET /sw.js` - Service worker
- `GET /static/*` - Static assets
- `GET /web/*` - All first-party endpoints
- `GET /web/*` - All first-party endpoints when `AuthDisabled` is true; when authentication is required, see [WebUI authentication](../configuration/webui-authentication.md)

### Token Authentication

Expand Down Expand Up @@ -1292,7 +1292,7 @@ curl "http://localhost:6969/web/radarr/radarr-4k/movies?has_file=false&year_min=

## Best Practices

1. **Use `/web/*` endpoints** for WebUI to avoid token management
1. **Use `/web/*` endpoints** for WebUI when `AuthDisabled` is true to avoid token management for browser traffic
2. **Use `/api/*` endpoints** for external clients with Bearer token
3. **Cache `/api/meta` responses** for 1 hour to reduce GitHub API load
4. **Poll `/api/processes`** every 5-10 seconds (not faster to avoid overhead)
Expand Down
4 changes: 3 additions & 1 deletion docs/webui/config-editor.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Configuration Editor
# Configuration Editor

The Configuration Editor provides a user-friendly interface for managing Torrentarr's configuration through the WebUI. All changes are saved to the `config.toml` file and can trigger live reloads of affected components without requiring a full application restart.

Expand Down Expand Up @@ -445,6 +445,7 @@ Define custom per-tracker seeding policies and tagging rules.
- **Name**: Tracker name (for display purposes)
- **URI**: Tracker URL (used for matching)
- **Priority**: Tracker priority (higher = preferred)
- **Sort Torrents**: Reorder matching torrents in qBittorrent queue by tracker priority
- **Maximum ETA (s)**: Override global ETA limit for this tracker
- **Download Rate Limit**: Override global download limit
- **Upload Rate Limit**: Override global upload limit
Expand All @@ -461,6 +462,7 @@ Define custom per-tracker seeding policies and tagging rules.
Name = "Premium Tracker"
URI = "https://premium.tracker.com/announce"
Priority = 10
SortTorrents = false
MaximumETA = 86400
DownloadRateLimit = -1
UploadRateLimit = -1
Expand Down
37 changes: 37 additions & 0 deletions src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,10 @@ private static bool ValidateAndFillConfig(TomlTable root)
}

// --- HnR defaults on CategorySeeding and Tracker sections ---
var trackerDefaults = new Dictionary<string, object>
{
["SortTorrents"] = false
};
var hnrDefaults = new Dictionary<string, object>
{
["HitAndRunMode"] = "disabled",
Expand Down Expand Up @@ -823,6 +827,14 @@ private static bool ValidateAndFillConfig(TomlTable root)
{
foreach (var trackerTable in GetTrackerTables(trObj))
{
foreach (var (field, defaultVal) in trackerDefaults)
{
if (!trackerTable.ContainsKey(field))
{
trackerTable[field] = defaultVal;
changed = true;
}
}
foreach (var (field, defaultVal) in hnrDefaults)
{
if (!trackerTable.ContainsKey(field))
Expand All @@ -840,6 +852,14 @@ private static bool ValidateAndFillConfig(TomlTable root)
{
foreach (var trackerTable in GetTrackerTables(atrObj))
{
foreach (var (field, defaultVal) in trackerDefaults)
{
if (!trackerTable.ContainsKey(field))
{
trackerTable[field] = defaultVal;
changed = true;
}
}
foreach (var (field, defaultVal) in hnrDefaults)
{
if (!trackerTable.ContainsKey(field))
Expand Down Expand Up @@ -977,6 +997,9 @@ private QBitConfig ParseQBit(TomlTable table)
if (table.TryGetValue("Priority", out var priority))
tracker.Priority = Convert.ToInt32(priority);

if (table.TryGetValue("SortTorrents", out var sortTorrents))
tracker.SortTorrents = Convert.ToBoolean(sortTorrents);

if (table.TryGetValue("MaxUploadRatio", out var maxRatio))
tracker.MaxUploadRatio = Convert.ToDouble(maxRatio);

Expand Down Expand Up @@ -1166,6 +1189,16 @@ private WebUIConfig ParseWebUI(TomlTable table)
if (table.TryGetValue("ViewDensity", out var viewDensity))
webui.ViewDensity = viewDensity?.ToString() ?? "Comfortable";

if (table.TryGetValue("CorsAllowedOrigins", out var corsVal) && corsVal is TomlArray corsArr)
{
foreach (var item in corsArr)
{
var s = item?.ToString()?.Trim();
if (!string.IsNullOrEmpty(s))
webui.CorsAllowedOrigins.Add(s);
}
}

if (table.TryGetValue("OIDC", out var oidcObj) && oidcObj is TomlTable oidcTable)
webui.OIDC = ParseOIDC(oidcTable);

Expand Down Expand Up @@ -1661,6 +1694,9 @@ private string GenerateTomlContent(TorrentarrConfig config)
sb.AppendLine($"GroupLidarr = {config.WebUI.GroupLidarr.ToString().ToLower()}");
sb.AppendLine($"Theme = \"{config.WebUI.Theme}\"");
sb.AppendLine($"ViewDensity = \"{config.WebUI.ViewDensity}\"");
if (config.WebUI.CorsAllowedOrigins.Count > 0)
sb.AppendLine(
$"CorsAllowedOrigins = [{string.Join(", ", config.WebUI.CorsAllowedOrigins.Select(o => $"\"{EscapeTomlString(o)}\""))}]");
if (config.WebUI.OIDC != null)
{
var o = config.WebUI.OIDC;
Expand Down Expand Up @@ -1773,6 +1809,7 @@ private string GenerateTomlContent(TorrentarrConfig config)
sb.AppendLine($"Name = \"{EscapeTomlString(tracker.Name)}\"");
sb.AppendLine($"URI = \"{tracker.Uri}\"");
sb.AppendLine($"Priority = {tracker.Priority}");
sb.AppendLine($"SortTorrents = {tracker.SortTorrents.ToString().ToLower()}");
sb.AppendLine($"MaximumETA = {tracker.MaxETA ?? -1}");
sb.AppendLine($"DownloadRateLimit = {tracker.DownloadRateLimit ?? -1}");
sb.AppendLine($"UploadRateLimit = {tracker.UploadRateLimit ?? -1}");
Expand Down
5 changes: 5 additions & 0 deletions src/Torrentarr.Core/Configuration/TorrentarrConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public class TrackerConfig
public string? Name { get; set; } // Human-readable tracker name
public string Uri { get; set; } = "";
public int Priority { get; set; } = 0;
public bool SortTorrents { get; set; } = false;
public double? MaxUploadRatio { get; set; }
public int? MaxSeedingTime { get; set; }
public int? RemoveTorrent { get; set; }
Expand Down Expand Up @@ -149,6 +150,10 @@ public class WebUIConfig
public bool GroupLidarr { get; set; } = true;
public string Theme { get; set; } = "Dark";
public string ViewDensity { get; set; } = "Comfortable";
/// <summary>
/// When non-empty, CORS allows only these origins (with credentials). When empty, any origin is allowed (legacy behavior).
/// </summary>
public List<string> CorsAllowedOrigins { get; set; } = new();
/// <summary>OIDC settings when OIDCEnabled is true. Optional.</summary>
public OIDCConfig? OIDC { get; set; }
}
Expand Down
14 changes: 13 additions & 1 deletion src/Torrentarr.Host/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,19 @@
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
{
if (config.WebUI.CorsAllowedOrigins.Count > 0)
{
policy.WithOrigins(config.WebUI.CorsAllowedOrigins.ToArray())
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}
else
{
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
}
});
});

// Database - paths already defined at top of file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,23 @@ public async Task<bool> SetFilePriorityAsync(string hash, int[] fileIds, int pri
return response.IsSuccessful;
}

/// <summary>
/// Move torrents to the top of the queue priority list.
/// POST /api/v2/torrents/topPrio
/// </summary>
public async Task<bool> TopPriorityAsync(List<string> hashes, CancellationToken ct = default)
{
if (hashes == null || hashes.Count == 0)
return true;

var request = new RestRequest("api/v2/torrents/topPrio", Method.Post);
AddAuthCookie(request);
request.AddParameter("hashes", string.Join("|", hashes));

var response = await _client.ExecuteAsync(request, ct);
return response.IsSuccessful;
}

/// <summary>
/// Create a new category
/// </summary>
Expand Down
Loading
Loading