More Fixes from Recent PRs (#1995)

* Added extra debugging for logout issue

* Fixed the null issue with ISBN

* Allow web links to be cleared out

* More logging on refresh token

* More key fallback when building Table of Contents

* Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced.

* Updated dependencies

* Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well.
This commit is contained in:
Joe Milazzo 2023-05-15 12:53:43 -05:00 committed by GitHub
parent 95df0a0825
commit 2ce4ddcaa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 104 additions and 31 deletions

View File

@ -10,8 +10,8 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.18.4" /> <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NSubstitute" Version="4.4.0" /> <PackageReference Include="NSubstitute" Version="4.4.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.22" /> <PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.26" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.22" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.26" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -102,7 +102,7 @@
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" /> <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.30.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.30.1" />
<PackageReference Include="System.IO.Abstractions" Version="19.2.22" /> <PackageReference Include="System.IO.Abstractions" Version="19.2.26" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" /> <PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" /> <PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
</ItemGroup> </ItemGroup>

View File

@ -443,6 +443,9 @@ public class AccountController : BaseApiController
if (!roleResult.Succeeded) return BadRequest(roleResult.Errors); if (!roleResult.Succeeded) return BadRequest(roleResult.Errors);
} }
// We might want to check if they had admin and no longer, if so:
// await _userManager.UpdateSecurityStampAsync(user); to force them to re-authenticate
var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
List<Library> libraries; List<Library> libraries;

View File

@ -165,16 +165,16 @@ public class Program
var env = hostingContext.HostingEnvironment; var env = hostingContext.HostingEnvironment;
config.AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: false) config.AddJsonFile("config/appsettings.json", optional: false, reloadOnChange: false)
.AddJsonFile($"config/appsettings.{env.EnvironmentName}.json", .AddJsonFile($"config/appsettings.{env.EnvironmentName}.json",
optional: true, reloadOnChange: false); optional: false, reloadOnChange: false);
}) })
.ConfigureWebHostDefaults(webBuilder => .ConfigureWebHostDefaults(webBuilder =>
{ {
webBuilder.UseKestrel((opts) => webBuilder.UseKestrel((opts) =>
{ {
var ipAddresses = Configuration.IpAddresses; var ipAddresses = Configuration.IpAddresses;
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker || string.IsNullOrEmpty(ipAddresses) || ipAddresses.Equals(Configuration.DefaultIpAddresses)) if (new OsInfo().IsDocker || string.IsNullOrEmpty(ipAddresses) || ipAddresses.Equals(Configuration.DefaultIpAddresses))
{ {
opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; }); opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; });
} }
@ -195,9 +195,6 @@ public class Program
} }
}); });
webBuilder.UseStartup<Startup>(); webBuilder.UseStartup<Startup>();
}); });
} }

View File

@ -435,7 +435,7 @@ public class BookService : IBookService
}; };
ComicInfo.CleanComicInfo(info); ComicInfo.CleanComicInfo(info);
foreach (var identifier in epubBook.Schema.Package.Metadata.Identifiers.Where(id => id.Scheme.Equals("ISBN"))) foreach (var identifier in epubBook.Schema.Package.Metadata.Identifiers.Where(id => !string.IsNullOrEmpty(id.Scheme) && id.Scheme.Equals("ISBN")))
{ {
if (string.IsNullOrEmpty(identifier.Identifier)) continue; if (string.IsNullOrEmpty(identifier.Identifier)) continue;
var isbn = identifier.Identifier.Replace("urn:isbn:", string.Empty).Replace("isbn:", string.Empty); var isbn = identifier.Identifier.Replace("urn:isbn:", string.Empty).Replace("isbn:", string.Empty);
@ -494,7 +494,7 @@ public class BookService : IBookService
break; break;
case "title-type": case "title-type":
break; break;
// This is currently not possible until VersOne update's to allow EPUB 3 Title to have attributes // This is currently not possible until VersOne update's to allow EPUB 3 Title to have attributes (3.3 update)
if (!metadataItem.Content.Equals("collection")) break; if (!metadataItem.Content.Equals("collection")) break;
var titleId = metadataItem.Refines.Replace("#", string.Empty); var titleId = metadataItem.Refines.Replace("#", string.Empty);
var readingListElem = epubBook.Schema.Package.Metadata.MetaItems.FirstOrDefault(item => var readingListElem = epubBook.Schema.Package.Metadata.MetaItems.FirstOrDefault(item =>
@ -855,7 +855,7 @@ public class BookService : IBookService
/// <param name="mappings"></param> /// <param name="mappings"></param>
/// <param name="key"></param> /// <param name="key"></param>
/// <returns></returns> /// <returns></returns>
private static string CoalesceKey(EpubBookRef book, IDictionary<string, int> mappings, string key) private static string CoalesceKey(EpubBookRef book, IReadOnlyDictionary<string, int> mappings, string key)
{ {
if (mappings.ContainsKey(CleanContentKeys(key))) return key; if (mappings.ContainsKey(CleanContentKeys(key))) return key;
@ -866,6 +866,19 @@ public class BookService : IBookService
key = correctedKey; key = correctedKey;
} }
var stepsBack = CountParentDirectory(book.Content.NavigationHtmlFile.FileName);
if (mappings.TryGetValue(key, out _))
{
return key;
}
var modifiedKey = RemovePathSegments(key, stepsBack);
if (mappings.TryGetValue(modifiedKey, out _))
{
return modifiedKey;
}
return key; return key;
} }
@ -904,7 +917,7 @@ public class BookService : IBookService
{ {
if (navigationItem.NestedItems.Count == 0) if (navigationItem.NestedItems.Count == 0)
{ {
CreateToCChapter(navigationItem, Array.Empty<BookChapterItem>(), chaptersList, mappings); CreateToCChapter(book, navigationItem, Array.Empty<BookChapterItem>(), chaptersList, mappings);
continue; continue;
} }
@ -913,19 +926,19 @@ public class BookService : IBookService
foreach (var nestedChapter in navigationItem.NestedItems.Where(n => n.Link != null)) foreach (var nestedChapter in navigationItem.NestedItems.Where(n => n.Link != null))
{ {
var key = CoalesceKey(book, mappings, nestedChapter.Link.ContentFileName); var key = CoalesceKey(book, mappings, nestedChapter.Link.ContentFileName);
if (mappings.ContainsKey(key)) if (mappings.TryGetValue(key, out var mapping))
{ {
nestedChapters.Add(new BookChapterItem nestedChapters.Add(new BookChapterItem
{ {
Title = nestedChapter.Title, Title = nestedChapter.Title,
Page = mappings[key], Page = mapping,
Part = nestedChapter.Link.Anchor ?? string.Empty, Part = nestedChapter.Link.Anchor ?? string.Empty,
Children = new List<BookChapterItem>() Children = new List<BookChapterItem>()
}); });
} }
} }
CreateToCChapter(navigationItem, nestedChapters, chaptersList, mappings); CreateToCChapter(book, navigationItem, nestedChapters, chaptersList, mappings);
} }
if (chaptersList.Count != 0) return chaptersList; if (chaptersList.Count != 0) return chaptersList;
@ -964,6 +977,38 @@ public class BookService : IBookService
return chaptersList; return chaptersList;
} }
private static int CountParentDirectory(string path)
{
const string pattern = @"\.\./";
var matches = Regex.Matches(path, pattern);
return matches.Count;
}
/// <summary>
/// Removes paths segments from the beginning of a path. Returns original path if any issues.
/// </summary>
/// <param name="path"></param>
/// <param name="segmentsToRemove"></param>
/// <returns></returns>
private static string RemovePathSegments(string path, int segmentsToRemove)
{
if (segmentsToRemove <= 0)
return path;
var startIndex = 0;
for (var i = 0; i < segmentsToRemove; i++)
{
var slashIndex = path.IndexOf('/', startIndex);
if (slashIndex == -1)
return path; // Not enough segments to remove
startIndex = slashIndex + 1;
}
return path.Substring(startIndex);
}
/// <summary> /// <summary>
/// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader, /// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader,
/// all css is scoped, etc. /// all css is scoped, etc.
@ -1028,7 +1073,7 @@ public class BookService : IBookService
throw new KavitaException("Could not find the appropriate html for that page"); throw new KavitaException("Could not find the appropriate html for that page");
} }
private static void CreateToCChapter(EpubNavigationItemRef navigationItem, IList<BookChapterItem> nestedChapters, private static void CreateToCChapter(EpubBookRef book, EpubNavigationItemRef navigationItem, IList<BookChapterItem> nestedChapters,
ICollection<BookChapterItem> chaptersList, IReadOnlyDictionary<string, int> mappings) ICollection<BookChapterItem> chaptersList, IReadOnlyDictionary<string, int> mappings)
{ {
if (navigationItem.Link == null) if (navigationItem.Link == null)
@ -1047,7 +1092,7 @@ public class BookService : IBookService
} }
else else
{ {
var groupKey = CleanContentKeys(navigationItem.Link.ContentFileName); var groupKey = CoalesceKey(book, mappings, navigationItem.Link.ContentFileName);
if (mappings.ContainsKey(groupKey)) if (mappings.ContainsKey(groupKey))
{ {
chaptersList.Add(new BookChapterItem chaptersList.Add(new BookChapterItem

View File

@ -98,10 +98,10 @@ public class SeriesService : ISeriesService
} }
// This shouldn't be needed post v0.5.3 release // This shouldn't be needed post v0.5.3 release
if (string.IsNullOrEmpty(series.Metadata.Summary)) // if (string.IsNullOrEmpty(series.Metadata.Summary))
{ // {
series.Metadata.Summary = string.Empty; // series.Metadata.Summary = string.Empty;
} // }
if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary)) if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary))
{ {
@ -120,7 +120,10 @@ public class SeriesService : ISeriesService
series.Metadata.LanguageLocked = true; series.Metadata.LanguageLocked = true;
} }
if (!string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata?.WebLinks)) if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata?.WebLinks))
{
series.Metadata.WebLinks = string.Empty;
} else
{ {
series.Metadata.WebLinks = string.Join(",", updateSeriesMetadataDto.SeriesMetadata?.WebLinks series.Metadata.WebLinks = string.Join(",", updateSeriesMetadataDto.SeriesMetadata?.WebLinks
.Split(",") .Split(",")

View File

@ -5,10 +5,12 @@ using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data;
using API.DTOs.Account; using API.DTOs.Account;
using API.Entities; using API.Entities;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using static System.Security.Claims.ClaimTypes; using static System.Security.Claims.ClaimTypes;
using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames;
@ -27,13 +29,17 @@ public interface ITokenService
public class TokenService : ITokenService public class TokenService : ITokenService
{ {
private readonly UserManager<AppUser> _userManager; private readonly UserManager<AppUser> _userManager;
private readonly ILogger<TokenService> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly SymmetricSecurityKey _key; private readonly SymmetricSecurityKey _key;
private const string RefreshTokenName = "RefreshToken"; private const string RefreshTokenName = "RefreshToken";
public TokenService(IConfiguration config, UserManager<AppUser> userManager) public TokenService(IConfiguration config, UserManager<AppUser> userManager, ILogger<TokenService> logger, IUnitOfWork unitOfWork)
{ {
_userManager = userManager; _userManager = userManager;
_logger = logger;
_unitOfWork = unitOfWork;
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"] ?? string.Empty)); _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"] ?? string.Empty));
} }
@ -78,12 +84,28 @@ public class TokenService : ITokenService
var tokenHandler = new JwtSecurityTokenHandler(); var tokenHandler = new JwtSecurityTokenHandler();
var tokenContent = tokenHandler.ReadJwtToken(request.Token); var tokenContent = tokenHandler.ReadJwtToken(request.Token);
var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.Name)?.Value; var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.Name)?.Value;
if (string.IsNullOrEmpty(username)) return null; if (string.IsNullOrEmpty(username))
{
_logger.LogDebug("[RefreshToken] failed to validate due to not finding user in RefreshToken");
return null;
}
var user = await _userManager.FindByNameAsync(username); var user = await _userManager.FindByNameAsync(username);
if (user == null) return null; // This forces a logout if (user == null)
{
_logger.LogDebug("[RefreshToken] failed to validate due to not finding user in DB");
return null;
}
var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken); var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken);
if (!validated) return null; if (!validated)
await _userManager.UpdateSecurityStampAsync(user); {
_logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token");
return null;
}
user.UpdateLastActive();
await _unitOfWork.CommitAsync();
return new TokenRequestDto() return new TokenRequestDto()
{ {
@ -93,11 +115,13 @@ public class TokenService : ITokenService
} catch (SecurityTokenExpiredException ex) } catch (SecurityTokenExpiredException ex)
{ {
// Handle expired token // Handle expired token
_logger.LogError(ex, "Failed to validate refresh token");
return null; return null;
} }
catch (Exception ex) catch (Exception ex)
{ {
// Handle other exceptions // Handle other exceptions
_logger.LogError(ex, "Failed to validate refresh token");
return null; return null;
} }
} }

View File

@ -10,6 +10,7 @@ public static class Configuration
{ {
public const string DefaultIpAddresses = "0.0.0.0,::"; public const string DefaultIpAddresses = "0.0.0.0,::";
public const string DefaultBaseUrl = "/"; public const string DefaultBaseUrl = "/";
public const int DefaultHttpPort = 5000;
public const string DefaultXFrameOptions = "SAMEORIGIN"; public const string DefaultXFrameOptions = "SAMEORIGIN";
private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
@ -307,7 +308,7 @@ public static class Configuration
// ReSharper disable once MemberHidesStaticFromOuterClass // ReSharper disable once MemberHidesStaticFromOuterClass
public int Port { get; set; } public int Port { get; set; }
// ReSharper disable once MemberHidesStaticFromOuterClass // ReSharper disable once MemberHidesStaticFromOuterClass
public string IpAddresses { get; set; } public string IpAddresses { get; set; } = string.Empty;
// ReSharper disable once MemberHidesStaticFromOuterClass // ReSharper disable once MemberHidesStaticFromOuterClass
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
} }

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.2.7" "version": "0.7.2.8"
}, },
"servers": [ "servers": [
{ {