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="Moq" Version="4.18.4" />
<PackageReference Include="NSubstitute" Version="4.4.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.22" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.22" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.26" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.26" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<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.Filters" Version="7.0.6" />
<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="VersOne.Epub" Version="3.3.0-alpha1" />
</ItemGroup>

View File

@ -443,6 +443,9 @@ public class AccountController : BaseApiController
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();
List<Library> libraries;

View File

@ -165,16 +165,16 @@ public class Program
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",
optional: true, reloadOnChange: false);
optional: false, reloadOnChange: false);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseKestrel((opts) =>
{
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; });
}
@ -197,7 +197,4 @@ public class Program
webBuilder.UseStartup<Startup>();
});
}

View File

@ -435,7 +435,7 @@ public class BookService : IBookService
};
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;
var isbn = identifier.Identifier.Replace("urn:isbn:", string.Empty).Replace("isbn:", string.Empty);
@ -494,7 +494,7 @@ public class BookService : IBookService
break;
case "title-type":
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;
var titleId = metadataItem.Refines.Replace("#", string.Empty);
var readingListElem = epubBook.Schema.Package.Metadata.MetaItems.FirstOrDefault(item =>
@ -855,7 +855,7 @@ public class BookService : IBookService
/// <param name="mappings"></param>
/// <param name="key"></param>
/// <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;
@ -866,6 +866,19 @@ public class BookService : IBookService
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;
}
@ -904,7 +917,7 @@ public class BookService : IBookService
{
if (navigationItem.NestedItems.Count == 0)
{
CreateToCChapter(navigationItem, Array.Empty<BookChapterItem>(), chaptersList, mappings);
CreateToCChapter(book, navigationItem, Array.Empty<BookChapterItem>(), chaptersList, mappings);
continue;
}
@ -913,19 +926,19 @@ public class BookService : IBookService
foreach (var nestedChapter in navigationItem.NestedItems.Where(n => n.Link != null))
{
var key = CoalesceKey(book, mappings, nestedChapter.Link.ContentFileName);
if (mappings.ContainsKey(key))
if (mappings.TryGetValue(key, out var mapping))
{
nestedChapters.Add(new BookChapterItem
{
Title = nestedChapter.Title,
Page = mappings[key],
Page = mapping,
Part = nestedChapter.Link.Anchor ?? string.Empty,
Children = new List<BookChapterItem>()
});
}
}
CreateToCChapter(navigationItem, nestedChapters, chaptersList, mappings);
CreateToCChapter(book, navigationItem, nestedChapters, chaptersList, mappings);
}
if (chaptersList.Count != 0) return chaptersList;
@ -964,6 +977,38 @@ public class BookService : IBookService
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>
/// 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.
@ -1028,7 +1073,7 @@ public class BookService : IBookService
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)
{
if (navigationItem.Link == null)
@ -1047,7 +1092,7 @@ public class BookService : IBookService
}
else
{
var groupKey = CleanContentKeys(navigationItem.Link.ContentFileName);
var groupKey = CoalesceKey(book, mappings, navigationItem.Link.ContentFileName);
if (mappings.ContainsKey(groupKey))
{
chaptersList.Add(new BookChapterItem

View File

@ -98,10 +98,10 @@ public class SeriesService : ISeriesService
}
// This shouldn't be needed post v0.5.3 release
if (string.IsNullOrEmpty(series.Metadata.Summary))
{
series.Metadata.Summary = string.Empty;
}
// if (string.IsNullOrEmpty(series.Metadata.Summary))
// {
// series.Metadata.Summary = string.Empty;
// }
if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary))
{
@ -120,7 +120,10 @@ public class SeriesService : ISeriesService
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
.Split(",")

View File

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

View File

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

View File

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