diff --git a/SaveHere/SaveHere.Tests/SaveHere.Tests.csproj b/SaveHere/SaveHere.Tests/SaveHere.Tests.csproj
index 1cfb178..8db7650 100644
--- a/SaveHere/SaveHere.Tests/SaveHere.Tests.csproj
+++ b/SaveHere/SaveHere.Tests/SaveHere.Tests.csproj
@@ -21,9 +21,8 @@
all
-
+
-
diff --git a/SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs b/SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs
new file mode 100644
index 0000000..73e3195
--- /dev/null
+++ b/SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs
@@ -0,0 +1,282 @@
+using Xunit;
+using SaveHere.Helpers;
+using SaveHere.Models;
+using System.Net.Http;
+using System.Text;
+
+namespace SaveHere.Tests.Services
+{
+ public class HttpRequestAuthenticatorTests
+ {
+ [Fact]
+ public void ApplyAuthentication_BasicAuth_SetsAuthorizationHeader()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.BasicAuth,
+ AuthUsername = "testuser",
+ AuthPassword = "testpass"
+ };
+
+ // Act
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+
+ // Assert
+ Assert.NotNull(request.Headers.Authorization);
+ Assert.Equal("Basic", request.Headers.Authorization.Scheme);
+
+ // Verify the credentials are correctly encoded
+ var expectedCredentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("testuser:testpass"));
+ Assert.Equal(expectedCredentials, request.Headers.Authorization.Parameter);
+ }
+
+ [Fact]
+ public void ApplyAuthentication_BasicAuth_WithEmptyPassword_SetsAuthorizationHeader()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.BasicAuth,
+ AuthUsername = "testuser",
+ AuthPassword = null
+ };
+
+ // Act
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+
+ // Assert
+ Assert.NotNull(request.Headers.Authorization);
+ Assert.Equal("Basic", request.Headers.Authorization.Scheme);
+
+ // Verify the credentials are correctly encoded with empty password
+ var expectedCredentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("testuser:"));
+ Assert.Equal(expectedCredentials, request.Headers.Authorization.Parameter);
+ }
+
+ [Fact]
+ public void ApplyAuthentication_BasicAuth_WithEmptyUsername_DoesNotSetHeader()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.BasicAuth,
+ AuthUsername = "",
+ AuthPassword = "testpass"
+ };
+
+ // Act
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+
+ // Assert
+ Assert.Null(request.Headers.Authorization);
+ }
+
+ [Fact]
+ public void ApplyAuthentication_BearerToken_SetsAuthorizationHeader()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.BearerToken,
+ AuthBearerToken = "my-jwt-token-12345"
+ };
+
+ // Act
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+
+ // Assert
+ Assert.NotNull(request.Headers.Authorization);
+ Assert.Equal("Bearer", request.Headers.Authorization.Scheme);
+ Assert.Equal("my-jwt-token-12345", request.Headers.Authorization.Parameter);
+ }
+
+ [Fact]
+ public void ApplyAuthentication_BearerToken_WithEmptyToken_DoesNotSetHeader()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.BearerToken,
+ AuthBearerToken = ""
+ };
+
+ // Act
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+
+ // Assert
+ Assert.Null(request.Headers.Authorization);
+ }
+
+ [Fact]
+ public void ApplyAuthentication_Cookie_SetsCookieHeader()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.Cookie,
+ AuthCookies = "session_id=abc123; user_token=xyz789"
+ };
+
+ // Act
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+
+ // Assert
+ Assert.True(request.Headers.Contains("Cookie"));
+ var cookieValues = request.Headers.GetValues("Cookie");
+ Assert.Contains("session_id=abc123; user_token=xyz789", cookieValues);
+ }
+
+ [Fact]
+ public void ApplyAuthentication_Cookie_WithEmptyCookies_DoesNotSetHeader()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.Cookie,
+ AuthCookies = ""
+ };
+
+ // Act
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+
+ // Assert
+ Assert.False(request.Headers.Contains("Cookie"));
+ }
+
+ [Fact]
+ public void ApplyAuthentication_CustomHeaders_SetsMultipleHeaders()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.CustomHeaders,
+ AuthCustomHeaders = "{\"X-Api-Key\": \"my-api-key\", \"X-Custom-Auth\": \"custom-value\"}"
+ };
+
+ // Act
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+
+ // Assert
+ Assert.True(request.Headers.Contains("X-Api-Key"));
+ Assert.True(request.Headers.Contains("X-Custom-Auth"));
+ Assert.Equal("my-api-key", request.Headers.GetValues("X-Api-Key").First());
+ Assert.Equal("custom-value", request.Headers.GetValues("X-Custom-Auth").First());
+ }
+
+ [Fact]
+ public void ApplyAuthentication_CustomHeaders_WithInvalidJson_DoesNotThrow()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.CustomHeaders,
+ AuthCustomHeaders = "not valid json"
+ };
+
+ // Act & Assert - should not throw
+ var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(request, queueItem));
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void ApplyAuthentication_CustomHeaders_WithEmptyHeaders_DoesNotThrow()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.CustomHeaders,
+ AuthCustomHeaders = ""
+ };
+
+ // Act & Assert - should not throw
+ var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(request, queueItem));
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void ApplyAuthentication_None_DoesNotModifyRequest()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.None
+ };
+
+ // Act
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+
+ // Assert
+ Assert.Null(request.Headers.Authorization);
+ Assert.False(request.Headers.Contains("Cookie"));
+ }
+
+ [Fact]
+ public void ApplyAuthentication_NullRequest_DoesNotThrow()
+ {
+ // Arrange
+ var queueItem = new FileDownloadQueueItem
+ {
+ InputUrl = "https://example.com/file.zip",
+ AuthType = AuthenticationType.BasicAuth,
+ AuthUsername = "user",
+ AuthPassword = "pass"
+ };
+
+ // Act & Assert - should not throw
+ var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(null!, queueItem));
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void ApplyAuthentication_NullQueueItem_DoesNotThrow()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip");
+
+ // Act & Assert - should not throw
+ var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(request, null!));
+ Assert.Null(exception);
+ }
+
+ [Theory]
+ [InlineData(AuthenticationType.BasicAuth, true)]
+ [InlineData(AuthenticationType.BearerToken, true)]
+ [InlineData(AuthenticationType.Cookie, true)]
+ [InlineData(AuthenticationType.CustomHeaders, true)]
+ [InlineData(AuthenticationType.None, false)]
+ public void HasAuthentication_ReturnsCorrectValue_ForAuthType(AuthenticationType authType, bool expected)
+ {
+ // Arrange
+ var queueItem = new FileDownloadQueueItem
+ {
+ AuthType = authType
+ };
+
+ // Assert
+ Assert.Equal(expected, queueItem.HasAuthentication);
+ }
+ }
+}
diff --git a/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor b/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor
index 99f281a..aeedef9 100644
--- a/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor
+++ b/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor
@@ -136,6 +136,68 @@
+
+
+
+ Authentication Settings
+
+
+
+ None
+ Basic Auth
+ Bearer Token
+ Cookies
+ Custom Headers
+
+
+
+
+
+
+ @if (_authType == AuthenticationType.BasicAuth)
+ {
+
+
+
+
+
+
+
+
+ }
+ else if (_authType == AuthenticationType.BearerToken)
+ {
+
+ }
+ else if (_authType == AuthenticationType.Cookie)
+ {
+
+ }
+ else if (_authType == AuthenticationType.CustomHeaders)
+ {
+
+ }
@@ -354,6 +416,14 @@
private bool _useHttp2 = true;
private bool _enableCompression = true;
+ // Authentication settings
+ private AuthenticationType _authType = AuthenticationType.None;
+ private string? _authUsername;
+ private string? _authPassword;
+ private string? _authBearerToken;
+ private string? _authCookies;
+ private string? _authCustomHeaders;
+
private readonly ChartOptions _chartOptions = new()
{
LineStrokeWidth = 3,
@@ -612,7 +682,13 @@
ParallelConnections = _parallelConnections,
BufferSizeKB = _bufferSizeKB,
UseHttp2 = _useHttp2,
- EnableCompression = _enableCompression
+ EnableCompression = _enableCompression,
+ AuthType = _authType,
+ AuthUsername = _authUsername,
+ AuthPassword = _authPassword,
+ AuthBearerToken = _authBearerToken,
+ AuthCookies = _authCookies,
+ AuthCustomHeaders = _authCustomHeaders
};
_context.FileDownloadQueueItems.Add(newFileDownload);
await _context.SaveChangesAsync();
diff --git a/SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs b/SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs
new file mode 100644
index 0000000..66032ba
--- /dev/null
+++ b/SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs
@@ -0,0 +1,76 @@
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using SaveHere.Models;
+
+namespace SaveHere.Helpers
+{
+ ///
+ /// Helper class to apply authentication settings to HTTP requests.
+ ///
+ public static class HttpRequestAuthenticator
+ {
+ ///
+ /// Applies the authentication settings from a queue item to an HTTP request.
+ ///
+ /// The HTTP request to modify.
+ /// The queue item containing authentication settings.
+ public static void ApplyAuthentication(HttpRequestMessage request, FileDownloadQueueItem queueItem)
+ {
+ if (request == null || queueItem == null)
+ return;
+
+ switch (queueItem.AuthType)
+ {
+ case AuthenticationType.BasicAuth:
+ if (!string.IsNullOrEmpty(queueItem.AuthUsername))
+ {
+ var credentials = Convert.ToBase64String(
+ Encoding.UTF8.GetBytes($"{queueItem.AuthUsername}:{queueItem.AuthPassword ?? string.Empty}"));
+ request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
+ }
+ break;
+
+ case AuthenticationType.BearerToken:
+ if (!string.IsNullOrEmpty(queueItem.AuthBearerToken))
+ {
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", queueItem.AuthBearerToken);
+ }
+ break;
+
+ case AuthenticationType.Cookie:
+ if (!string.IsNullOrEmpty(queueItem.AuthCookies))
+ {
+ request.Headers.TryAddWithoutValidation("Cookie", queueItem.AuthCookies);
+ }
+ break;
+
+ case AuthenticationType.CustomHeaders:
+ if (!string.IsNullOrEmpty(queueItem.AuthCustomHeaders))
+ {
+ try
+ {
+ var headers = JsonSerializer.Deserialize>(queueItem.AuthCustomHeaders);
+ if (headers != null)
+ {
+ foreach (var (key, value) in headers)
+ {
+ request.Headers.TryAddWithoutValidation(key, value);
+ }
+ }
+ }
+ catch (JsonException)
+ {
+ // Invalid JSON, silently ignore
+ }
+ }
+ break;
+
+ case AuthenticationType.None:
+ default:
+ // No authentication needed
+ break;
+ }
+ }
+ }
+}
diff --git a/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs
new file mode 100644
index 0000000..3ad21d9
--- /dev/null
+++ b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs
@@ -0,0 +1,414 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using SaveHere.Models.db;
+
+#nullable disable
+
+namespace SaveHere.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260118083444_AddDownloadAuthentication")]
+ partial class AddDownloadAuthentication
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.23");
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("RoleId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("SaveHere.Models.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("SaveHere.Models.FileDownloadQueueItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AuthBearerToken")
+ .HasColumnType("TEXT");
+
+ b.Property("AuthCookies")
+ .HasColumnType("TEXT");
+
+ b.Property("AuthCustomHeaders")
+ .HasColumnType("TEXT");
+
+ b.Property("AuthPassword")
+ .HasColumnType("TEXT");
+
+ b.Property("AuthType")
+ .HasColumnType("INTEGER");
+
+ b.Property("AuthUsername")
+ .HasColumnType("TEXT");
+
+ b.Property("AverageDownloadSpeed")
+ .HasColumnType("REAL");
+
+ b.Property("BufferSizeKB")
+ .HasColumnType("INTEGER");
+
+ b.Property("CurrentDownloadSpeed")
+ .HasColumnType("REAL");
+
+ b.Property("CustomFileName")
+ .HasColumnType("TEXT");
+
+ b.Property("DownloadFolder")
+ .HasColumnType("TEXT");
+
+ b.Property("EnableCompression")
+ .HasColumnType("INTEGER");
+
+ b.Property("InputUrl")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("MaxBytesPerSecond")
+ .HasColumnType("INTEGER");
+
+ b.Property("ParallelConnections")
+ .HasColumnType("INTEGER");
+
+ b.Property("ProgressPercentage")
+ .HasColumnType("INTEGER");
+
+ b.Property("SpeedHistory")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("SupportsRangeRequests")
+ .HasColumnType("INTEGER");
+
+ b.Property("UseHttp2")
+ .HasColumnType("INTEGER");
+
+ b.Property("bShouldGetFilenameFromHttpHeaders")
+ .HasColumnType("INTEGER");
+
+ b.Property("bShowMoreOptions")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("FileDownloadQueueItems");
+ });
+
+ modelBuilder.Entity("SaveHere.Models.RegistrationSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("IsRegistrationEnabled")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("RegistrationSettings");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ IsRegistrationEnabled = false
+ });
+ });
+
+ modelBuilder.Entity("SaveHere.Models.SaveHere.Models.YoutubeDownloadQueueItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CustomFileName")
+ .HasColumnType("TEXT");
+
+ b.Property("DownloadFolder")
+ .HasColumnType("TEXT");
+
+ b.Property("OutputLog")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("PersistedLog")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Proxy")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Quality")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("SubtitleLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("YoutubeDownloadQueueItems");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("SaveHere.Models.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("SaveHere.Models.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("SaveHere.Models.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("SaveHere.Models.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs
new file mode 100644
index 0000000..4eccb4d
--- /dev/null
+++ b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs
@@ -0,0 +1,164 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace SaveHere.Migrations
+{
+ ///
+ public partial class AddDownloadAuthentication : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(
+ name: "CustomFilename",
+ table: "YoutubeDownloadQueueItems",
+ newName: "CustomFileName");
+
+ migrationBuilder.AddColumn(
+ name: "SubtitleLanguage",
+ table: "YoutubeDownloadQueueItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "AuthBearerToken",
+ table: "FileDownloadQueueItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "AuthCookies",
+ table: "FileDownloadQueueItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "AuthCustomHeaders",
+ table: "FileDownloadQueueItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "AuthPassword",
+ table: "FileDownloadQueueItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "AuthType",
+ table: "FileDownloadQueueItems",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddColumn(
+ name: "AuthUsername",
+ table: "FileDownloadQueueItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "BufferSizeKB",
+ table: "FileDownloadQueueItems",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddColumn(
+ name: "CustomFileName",
+ table: "FileDownloadQueueItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "EnableCompression",
+ table: "FileDownloadQueueItems",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "ParallelConnections",
+ table: "FileDownloadQueueItems",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddColumn(
+ name: "SupportsRangeRequests",
+ table: "FileDownloadQueueItems",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "UseHttp2",
+ table: "FileDownloadQueueItems",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "SubtitleLanguage",
+ table: "YoutubeDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "AuthBearerToken",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "AuthCookies",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "AuthCustomHeaders",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "AuthPassword",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "AuthType",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "AuthUsername",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "BufferSizeKB",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "CustomFileName",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "EnableCompression",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "ParallelConnections",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "SupportsRangeRequests",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.DropColumn(
+ name: "UseHttp2",
+ table: "FileDownloadQueueItems");
+
+ migrationBuilder.RenameColumn(
+ name: "CustomFileName",
+ table: "YoutubeDownloadQueueItems",
+ newName: "CustomFilename");
+ }
+ }
+}
diff --git a/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs b/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs
index 560c02b..20a5048 100644
--- a/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs
+++ b/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs
@@ -15,7 +15,7 @@ partial class AppDbContextModelSnapshot : ModelSnapshot
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "8.0.14");
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.23");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
@@ -218,15 +218,42 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
+ b.Property("AuthBearerToken")
+ .HasColumnType("TEXT");
+
+ b.Property("AuthCookies")
+ .HasColumnType("TEXT");
+
+ b.Property("AuthCustomHeaders")
+ .HasColumnType("TEXT");
+
+ b.Property("AuthPassword")
+ .HasColumnType("TEXT");
+
+ b.Property("AuthType")
+ .HasColumnType("INTEGER");
+
+ b.Property("AuthUsername")
+ .HasColumnType("TEXT");
+
b.Property("AverageDownloadSpeed")
.HasColumnType("REAL");
+ b.Property("BufferSizeKB")
+ .HasColumnType("INTEGER");
+
b.Property("CurrentDownloadSpeed")
.HasColumnType("REAL");
+ b.Property("CustomFileName")
+ .HasColumnType("TEXT");
+
b.Property("DownloadFolder")
.HasColumnType("TEXT");
+ b.Property("EnableCompression")
+ .HasColumnType("INTEGER");
+
b.Property("InputUrl")
.IsRequired()
.HasColumnType("TEXT");
@@ -234,6 +261,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("MaxBytesPerSecond")
.HasColumnType("INTEGER");
+ b.Property("ParallelConnections")
+ .HasColumnType("INTEGER");
+
b.Property("ProgressPercentage")
.HasColumnType("INTEGER");
@@ -244,6 +274,12 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("Status")
.HasColumnType("INTEGER");
+ b.Property("SupportsRangeRequests")
+ .HasColumnType("INTEGER");
+
+ b.Property("UseHttp2")
+ .HasColumnType("INTEGER");
+
b.Property("bShouldGetFilenameFromHttpHeaders")
.HasColumnType("INTEGER");
@@ -282,6 +318,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
+ b.Property("CustomFileName")
+ .HasColumnType("TEXT");
+
b.Property("DownloadFolder")
.HasColumnType("TEXT");
@@ -304,6 +343,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("Status")
.HasColumnType("INTEGER");
+ b.Property("SubtitleLanguage")
+ .HasColumnType("TEXT");
+
b.Property("Url")
.IsRequired()
.HasColumnType("TEXT");
diff --git a/SaveHere/SaveHere/Models/AuthenticationType.cs b/SaveHere/SaveHere/Models/AuthenticationType.cs
new file mode 100644
index 0000000..854ce3b
--- /dev/null
+++ b/SaveHere/SaveHere/Models/AuthenticationType.cs
@@ -0,0 +1,33 @@
+namespace SaveHere.Models
+{
+ ///
+ /// Authentication types supported for HTTP downloads.
+ ///
+ public enum AuthenticationType
+ {
+ ///
+ /// No authentication required.
+ ///
+ None = 0,
+
+ ///
+ /// HTTP Basic Authentication (username/password).
+ ///
+ BasicAuth = 1,
+
+ ///
+ /// Bearer token authentication (OAuth, JWT, etc.).
+ ///
+ BearerToken = 2,
+
+ ///
+ /// Cookie-based authentication.
+ ///
+ Cookie = 3,
+
+ ///
+ /// Custom HTTP headers for authentication.
+ ///
+ CustomHeaders = 4
+ }
+}
diff --git a/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs b/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs
index 5c89df0..6261d7d 100644
--- a/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs
+++ b/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
namespace SaveHere.Models
{
@@ -30,5 +31,16 @@ public class FileDownloadQueueItem
public bool UseHttp2 { get; set; } = true;
public bool EnableCompression { get; set; } = true;
public bool SupportsRangeRequests { get; set; } = false;
+
+ // Authentication settings
+ public AuthenticationType AuthType { get; set; } = AuthenticationType.None;
+ public string? AuthUsername { get; set; }
+ public string? AuthPassword { get; set; }
+ public string? AuthBearerToken { get; set; }
+ public string? AuthCookies { get; set; }
+ public string? AuthCustomHeaders { get; set; }
+
+ [NotMapped]
+ public bool HasAuthentication => AuthType != AuthenticationType.None;
}
}
diff --git a/SaveHere/SaveHere/Services/DownloadQueueService.cs b/SaveHere/SaveHere/Services/DownloadQueueService.cs
index 5918564..7e47383 100644
--- a/SaveHere/SaveHere/Services/DownloadQueueService.cs
+++ b/SaveHere/SaveHere/Services/DownloadQueueService.cs
@@ -5,6 +5,7 @@
using System.Net;
using System.Web;
using System.Net.Http.Headers;
+using SaveHere.Helpers;
using SaveHere.Services;
namespace SaveHere.Services
@@ -185,7 +186,7 @@ public async Task DownloadFile(FileDownloadQueueItem queueItem, CancellationToke
// Check if server supports range requests for parallel downloads
if (queueItem.ParallelConnections > 1)
{
- queueItem.SupportsRangeRequests = await CheckRangeSupport(queueItem.InputUrl, cancellationToken);
+ queueItem.SupportsRangeRequests = await CheckRangeSupport(queueItem, cancellationToken);
if (queueItem.SupportsRangeRequests)
{
await DownloadFileParallel(queueItem, cancellationToken);
@@ -224,7 +225,9 @@ public async Task DownloadFile(FileDownloadQueueItem queueItem, CancellationToke
: queueItem.CustomFileName;
- var response = await httpClient.GetAsync(queueItem.InputUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ var initialRequest = new HttpRequestMessage(HttpMethod.Get, queueItem.InputUrl);
+ HttpRequestAuthenticator.ApplyAuthentication(initialRequest, queueItem);
+ var response = await httpClient.SendAsync(initialRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
if (string.IsNullOrWhiteSpace(queueItem.CustomFileName) && queueItem.bShouldGetFilenameFromHttpHeaders)
@@ -286,6 +289,7 @@ public async Task DownloadFile(FileDownloadQueueItem queueItem, CancellationToke
}
var requestMessage = new HttpRequestMessage(HttpMethod.Get, queueItem.InputUrl);
+ HttpRequestAuthenticator.ApplyAuthentication(requestMessage, queueItem);
if (totalBytesRead > 0)
{
@@ -546,9 +550,32 @@ public async Task CheckRangeSupport(string url, CancellationToken cancella
}
}
+ ///
+ /// Checks if the server supports range requests, with authentication support.
+ ///
+ public async Task CheckRangeSupport(FileDownloadQueueItem queueItem, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var request = new HttpRequestMessage(HttpMethod.Head, queueItem.InputUrl);
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
+ request.Headers.Range = new RangeHeaderValue(0, 0);
+
+ using var response = await _httpClient.SendAsync(request, cancellationToken);
+ return response.StatusCode == HttpStatusCode.PartialContent ||
+ response.Headers.AcceptRanges?.Contains("bytes") == true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
public async Task DownloadFileParallel(FileDownloadQueueItem queueItem, CancellationToken cancellationToken)
{
- var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, queueItem.InputUrl), cancellationToken);
+ var headRequest = new HttpRequestMessage(HttpMethod.Head, queueItem.InputUrl);
+ HttpRequestAuthenticator.ApplyAuthentication(headRequest, queueItem);
+ var response = await _httpClient.SendAsync(headRequest, cancellationToken);
var contentLength = response.Content.Headers.ContentLength;
if (!contentLength.HasValue)
@@ -631,6 +658,7 @@ public async Task DownloadFileParallel(FileDownloadQueueItem queueItem, Cancella
private async Task DownloadChunk(FileDownloadQueueItem queueItem, long start, long end, string chunkFile, int chunkIndex, CancellationToken cancellationToken, ParallelDownloadProgress progressTracker)
{
var request = new HttpRequestMessage(HttpMethod.Get, queueItem.InputUrl);
+ HttpRequestAuthenticator.ApplyAuthentication(request, queueItem);
request.Headers.Range = new RangeHeaderValue(start, end);
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);