mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge branch 'main' into dependabot/nuget/Orc.FileAssociation-5.0.0
This commit is contained in:
commit
a6e6758333
16
.github/workflows/tests.yaml
vendored
16
.github/workflows/tests.yaml
vendored
@ -7,7 +7,7 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VERSION: 3.5.0.2
|
VERSION: 3.6.0.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@ -38,18 +38,6 @@ jobs:
|
|||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
include-prerelease: true
|
include-prerelease: true
|
||||||
|
|
||||||
- name: Setup .NET Core SDK 7.0.x
|
|
||||||
uses: actions/setup-dotnet@v1
|
|
||||||
with:
|
|
||||||
dotnet-version: '7.0.x'
|
|
||||||
include-prerelease: true
|
|
||||||
|
|
||||||
- name: Setup .NET Core SDK 6.0.x
|
|
||||||
uses: actions/setup-dotnet@v1
|
|
||||||
with:
|
|
||||||
dotnet-version: '6.0.x'
|
|
||||||
include-prerelease: true
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test /p:EnableWindowsTargeting=true --filter "Category!=FlakeyNetwork"
|
run: dotnet test /p:EnableWindowsTargeting=true --filter "Category!=FlakeyNetwork"
|
||||||
|
|
||||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,5 +1,22 @@
|
|||||||
### Changelog
|
### Changelog
|
||||||
|
|
||||||
|
#### Version - 3.6.1.1 - TBD
|
||||||
|
* Fixed `set-nexus-api-key` CLI command
|
||||||
|
|
||||||
|
#### Version - 3.6.1.0 - 5/26/2024
|
||||||
|
* Fixed a race condition on renewing Nexus Mods OAuth2 tokens
|
||||||
|
* Added `set-nexus-api-key` CLI command
|
||||||
|
* Added Starfield meta data
|
||||||
|
* Added Fallout New Vegas Epic Games metadata
|
||||||
|
|
||||||
|
#### Version - 3.6.0.0 - 5/25/2024
|
||||||
|
* Wabbajack now uses OAuth2 for Nexus Mods logins
|
||||||
|
* Support for DirectURL use with LL files
|
||||||
|
|
||||||
|
#### Version - 3.5.0.2 - 5/21/2024
|
||||||
|
* *HOTFIX* - change how we log into Nexus Mods. We still need to rewrite this on
|
||||||
|
Oauth2, but this should fix the current issues we have, and get people back up and running
|
||||||
|
|
||||||
#### Version - 3.5.0.1 - 1/15/2024
|
#### Version - 3.5.0.1 - 1/15/2024
|
||||||
* *HOTFIX* - change the cache file names so files will be auto-rehashed
|
* *HOTFIX* - change the cache file names so files will be auto-rehashed
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ namespace Wabbajack.LoginManagers;
|
|||||||
public class NexusLoginManager : ViewModel, ILoginFor<NexusDownloader>
|
public class NexusLoginManager : ViewModel, ILoginFor<NexusDownloader>
|
||||||
{
|
{
|
||||||
private readonly ILogger<NexusLoginManager> _logger;
|
private readonly ILogger<NexusLoginManager> _logger;
|
||||||
private readonly ITokenProvider<NexusApiState> _token;
|
private readonly ITokenProvider<NexusOAuthState> _token;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
public string SiteName { get; } = "Nexus Mods";
|
public string SiteName { get; } = "Nexus Mods";
|
||||||
@ -35,7 +35,7 @@ public class NexusLoginManager : ViewModel, ILoginFor<NexusDownloader>
|
|||||||
[Reactive]
|
[Reactive]
|
||||||
public bool HaveLogin { get; set; }
|
public bool HaveLogin { get; set; }
|
||||||
|
|
||||||
public NexusLoginManager(ILogger<NexusLoginManager> logger, ITokenProvider<NexusApiState> token, IServiceProvider serviceProvider)
|
public NexusLoginManager(ILogger<NexusLoginManager> logger, ITokenProvider<NexusOAuthState> token, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_token = token;
|
_token = token;
|
||||||
|
@ -1,92 +1,143 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
using Fizzler.Systems.HtmlAgilityPack;
|
using Fizzler.Systems.HtmlAgilityPack;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
|
using Wabbajack.DTOs.OAuth;
|
||||||
using Wabbajack.Messages;
|
using Wabbajack.Messages;
|
||||||
using Wabbajack.Models;
|
using Wabbajack.Models;
|
||||||
using Wabbajack.Networking.Http.Interfaces;
|
using Wabbajack.Networking.Http.Interfaces;
|
||||||
using Wabbajack.Services.OSIntegrated;
|
using Wabbajack.Services.OSIntegrated;
|
||||||
|
using Cookie = Wabbajack.DTOs.Logins.Cookie;
|
||||||
|
|
||||||
namespace Wabbajack.UserIntervention;
|
namespace Wabbajack.UserIntervention;
|
||||||
|
|
||||||
public class NexusLoginHandler : BrowserWindowViewModel
|
public class NexusLoginHandler : BrowserWindowViewModel
|
||||||
{
|
{
|
||||||
private readonly EncryptedJsonTokenProvider<NexusApiState> _tokenProvider;
|
private static Uri OAuthUrl = new Uri("https://users.nexusmods.com/oauth");
|
||||||
|
private static string OAuthRedirectUrl = "https://127.0.0.1:1234";
|
||||||
|
private static string OAuthClientId = "wabbajack";
|
||||||
|
|
||||||
|
private readonly EncryptedJsonTokenProvider<NexusOAuthState> _tokenProvider;
|
||||||
|
private readonly ILogger<NexusLoginHandler> _logger;
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
|
||||||
public NexusLoginHandler(EncryptedJsonTokenProvider<NexusApiState> tokenProvider)
|
public NexusLoginHandler(ILogger<NexusLoginHandler> logger, HttpClient client, EncryptedJsonTokenProvider<NexusOAuthState> tokenProvider)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_client = client;
|
||||||
HeaderText = "Nexus Login";
|
HeaderText = "Nexus Login";
|
||||||
_tokenProvider = tokenProvider;
|
_tokenProvider = tokenProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string Base64Id()
|
||||||
|
{
|
||||||
|
var bytes = new byte[32];
|
||||||
|
using var rng = RandomNumberGenerator.Create();
|
||||||
|
rng.GetBytes(bytes);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task Run(CancellationToken token)
|
protected override async Task Run(CancellationToken token)
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// see https://www.rfc-editor.org/rfc/rfc7636#section-4.1
|
||||||
|
var codeVerifier = Guid.NewGuid().ToString("N").ToBase64();
|
||||||
|
|
||||||
|
// see https://www.rfc-editor.org/rfc/rfc7636#section-4.2
|
||||||
|
var codeChallengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
|
||||||
|
var codeChallenge = StringBase64Extensions.Base64UrlEncode(codeChallengeBytes);
|
||||||
|
|
||||||
|
|
||||||
Instructions = "Please log into the Nexus";
|
Instructions = "Please log into the Nexus";
|
||||||
|
|
||||||
await NavigateTo(new Uri(
|
var state = Guid.NewGuid().ToString();
|
||||||
"https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=//www.nexusmods.com"));
|
|
||||||
|
await NavigateTo(new Uri("https://nexusmods.com"));
|
||||||
|
var codeCompletionSource = new TaskCompletionSource<Dictionary<string, StringValues>>();
|
||||||
Cookie[] cookies = { };
|
|
||||||
while (true)
|
Browser!.Browser.CoreWebView2.NewWindowRequested += (sender, args) =>
|
||||||
{
|
{
|
||||||
cookies = await GetCookies("nexusmods.com", token);
|
var uri = new Uri(args.Uri);
|
||||||
if (cookies.Any(c => c.Name == "member_id"))
|
_logger.LogInformation("New Window Requested {Uri}", args.Uri);
|
||||||
break;
|
if (uri.Host != "127.0.0.1") return;
|
||||||
|
|
||||||
|
codeCompletionSource.TrySetResult(QueryHelpers.ParseQuery(uri.Query));
|
||||||
|
args.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
var uri = GenerateAuthorizeUrl(codeChallenge, state);
|
||||||
await Task.Delay(500, token);
|
await NavigateTo(uri);
|
||||||
|
|
||||||
|
var ctx = await codeCompletionSource.Task;
|
||||||
|
|
||||||
|
if (ctx["state"].FirstOrDefault() != state)
|
||||||
|
{
|
||||||
|
throw new Exception("State mismatch");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var code = ctx["code"].FirstOrDefault();
|
||||||
|
|
||||||
Instructions = "Getting API Key...";
|
var result = await AuthorizeToken(codeVerifier, code, token);
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
result.ReceivedAt = DateTime.UtcNow.ToFileTimeUtc();
|
||||||
|
|
||||||
await NavigateTo(new Uri("https://next.nexusmods.com/settings/api-keys"));
|
await _tokenProvider.SetToken(new NexusOAuthState()
|
||||||
|
|
||||||
var key = "";
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
try
|
OAuth = result!
|
||||||
{
|
|
||||||
key = (await GetDom(token)).DocumentNode.QuerySelectorAll("img[alt='Wabbajack']").SelectMany(p => p.ParentNode.ParentNode.QuerySelectorAll("input[aria-label='api key']")).Select(node => node.Attributes["value"]).FirstOrDefault()?.Value;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(key))
|
|
||||||
break;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await EvaluateJavaScript(
|
|
||||||
"var found = document.querySelector(\"img[alt='Wabbajack']\").parentElement.parentElement.querySelector(\"button[aria-label='Request Api Key']\");" +
|
|
||||||
"found.onclick= function() {return true;};" +
|
|
||||||
"found.class = \" \"; " +
|
|
||||||
"found.click();"
|
|
||||||
);
|
|
||||||
Instructions = "Generating API Key, Please Wait...";
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
await Task.Delay(500, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
Instructions = "Success, saving information...";
|
|
||||||
await _tokenProvider.SetToken(new NexusApiState
|
|
||||||
{
|
|
||||||
Cookies = cookies,
|
|
||||||
ApiKey = key
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<JwtTokenReply?> AuthorizeToken(string verifier, string code, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var request = new Dictionary<string, string> {
|
||||||
|
{ "grant_type", "authorization_code" },
|
||||||
|
{ "client_id", OAuthClientId },
|
||||||
|
{ "redirect_uri", OAuthRedirectUrl },
|
||||||
|
{ "code", code },
|
||||||
|
{ "code_verifier", verifier },
|
||||||
|
};
|
||||||
|
|
||||||
|
var content = new FormUrlEncodedContent(request);
|
||||||
|
|
||||||
|
var response = await _client.PostAsync($"{OAuthUrl}/token", content, cancel);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogCritical("Failed to get token {code} - {message}", response.StatusCode,
|
||||||
|
response.ReasonPhrase);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var responseString = await response.Content.ReadAsStringAsync(cancel);
|
||||||
|
return JsonSerializer.Deserialize<JwtTokenReply>(responseString);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Uri GenerateAuthorizeUrl(string challenge, string state)
|
||||||
|
{
|
||||||
|
var request = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "response_type", "code" },
|
||||||
|
{ "scope", "public openid profile" },
|
||||||
|
{ "code_challenge_method", "S256" },
|
||||||
|
{ "client_id", OAuthClientId },
|
||||||
|
{ "redirect_uri", OAuthRedirectUrl },
|
||||||
|
{ "code_challenge", challenge },
|
||||||
|
{ "state", state },
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Uri(QueryHelpers.AddQueryString($"{OAuthUrl}/authorize", request));
|
||||||
|
}
|
||||||
}
|
}
|
@ -6,6 +6,7 @@ using System.Net.Http.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
@ -103,4 +104,5 @@ public abstract class OAuth2LoginHandler<TLoginType> : BrowserWindowViewModel
|
|||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
126
Wabbajack.App.Wpf/UserIntervention/StringExtensions.cs
Normal file
126
Wabbajack.App.Wpf/UserIntervention/StringExtensions.cs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Wabbajack.UserIntervention;
|
||||||
|
|
||||||
|
public static class StringBase64Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Convert string to base 64 encoding
|
||||||
|
/// </summary>
|
||||||
|
public static string ToBase64(this string input)
|
||||||
|
{
|
||||||
|
return ToBase64(Encoding.UTF8.GetBytes(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert byte array to base 64 encoding
|
||||||
|
/// </summary>
|
||||||
|
public static string ToBase64(this byte[] input)
|
||||||
|
{
|
||||||
|
return Convert.ToBase64String(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes <paramref name="input"/> using base64url encoding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The binary input to encode.</param>
|
||||||
|
/// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
|
||||||
|
[SkipLocalsInit]
|
||||||
|
public static string Base64UrlEncode(ReadOnlySpan<byte> input)
|
||||||
|
{
|
||||||
|
// TODO: use Microsoft.AspNetCore.WebUtilities when .NET 8 is available
|
||||||
|
// Source: https://github.com/dotnet/aspnetcore/blob/main/src/Shared/WebEncoders/WebEncoders.cs
|
||||||
|
// The MIT License (MIT)
|
||||||
|
//
|
||||||
|
// Copyright (c) .NET Foundation and Contributors
|
||||||
|
//
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
const int stackAllocThreshold = 128;
|
||||||
|
|
||||||
|
if (input.IsEmpty)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bufferSize = GetArraySizeRequiredToEncode(input.Length);
|
||||||
|
|
||||||
|
char[]? bufferToReturnToPool = null;
|
||||||
|
var buffer = bufferSize <= stackAllocThreshold
|
||||||
|
? stackalloc char[stackAllocThreshold]
|
||||||
|
: bufferToReturnToPool = ArrayPool<char>.Shared.Rent(bufferSize);
|
||||||
|
|
||||||
|
var numBase64Chars = Base64UrlEncode(input, buffer);
|
||||||
|
var base64Url = new string(buffer[..numBase64Chars]);
|
||||||
|
|
||||||
|
if (bufferToReturnToPool != null)
|
||||||
|
{
|
||||||
|
ArrayPool<char>.Shared.Return(bufferToReturnToPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int Base64UrlEncode(ReadOnlySpan<byte> input, Span<char> output)
|
||||||
|
{
|
||||||
|
Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length));
|
||||||
|
|
||||||
|
if (input.IsEmpty)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use base64url encoding with no padding characters. See RFC 4648, Sec. 5.
|
||||||
|
|
||||||
|
Convert.TryToBase64Chars(input, output, out int charsWritten);
|
||||||
|
|
||||||
|
// Fix up '+' -> '-' and '/' -> '_'. Drop padding characters.
|
||||||
|
for (var i = 0; i < charsWritten; i++)
|
||||||
|
{
|
||||||
|
var ch = output[i];
|
||||||
|
switch (ch)
|
||||||
|
{
|
||||||
|
case '+':
|
||||||
|
output[i] = '-';
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
output[i] = '_';
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
// We've reached a padding character; truncate the remainder.
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return charsWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetArraySizeRequiredToEncode(int count)
|
||||||
|
{
|
||||||
|
var numWholeOrPartialInputBlocks = checked(count + 2) / 3;
|
||||||
|
return checked(numWholeOrPartialInputBlocks * 4);
|
||||||
|
}
|
||||||
|
}
|
@ -159,7 +159,6 @@ namespace Wabbajack
|
|||||||
Filters.Connect().QueryWhenChanged(),
|
Filters.Connect().QueryWhenChanged(),
|
||||||
resultSelector: (target, type, checkOption, query) =>
|
resultSelector: (target, type, checkOption, query) =>
|
||||||
{
|
{
|
||||||
Console.WriteLine("fff");
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case PathTypeOptions.Either:
|
case PathTypeOptions.Either:
|
||||||
|
@ -90,8 +90,9 @@
|
|||||||
<PackageReference Include="MahApps.Metro" Version="2.4.10" />
|
<PackageReference Include="MahApps.Metro" Version="2.4.10" />
|
||||||
<PackageReference Include="MahApps.Metro.IconPacks" Version="4.11.0" />
|
<PackageReference Include="MahApps.Metro.IconPacks" Version="4.11.0" />
|
||||||
<PackageReference Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.5" />
|
<PackageReference Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2151.40" />
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2478.35" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.5" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.5" />
|
||||||
<PackageReference Include="Orc.FileAssociation" Version="5.0.0" />
|
<PackageReference Include="Orc.FileAssociation" Version="5.0.0" />
|
||||||
<PackageReference Include="PInvoke.User32" Version="0.7.124" />
|
<PackageReference Include="PInvoke.User32" Version="0.7.124" />
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||||
|
-->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>Any CPU</Platform>
|
||||||
|
<PublishDir>bin\Release\net8.0\publish\win-x64\</PublishDir>
|
||||||
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSingleFile>false</PublishSingleFile>
|
||||||
|
<PublishReadyToRun>false</PublishReadyToRun>
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
@ -43,6 +43,8 @@ CommandLineBuilder.RegisterCommand<MirrorFile>(MirrorFile.Definition, c => ((Mir
|
|||||||
services.AddSingleton<MirrorFile>();
|
services.AddSingleton<MirrorFile>();
|
||||||
CommandLineBuilder.RegisterCommand<ModlistReport>(ModlistReport.Definition, c => ((ModlistReport)c).Run);
|
CommandLineBuilder.RegisterCommand<ModlistReport>(ModlistReport.Definition, c => ((ModlistReport)c).Run);
|
||||||
services.AddSingleton<ModlistReport>();
|
services.AddSingleton<ModlistReport>();
|
||||||
|
CommandLineBuilder.RegisterCommand<SetNexusApiKey>(SetNexusApiKey.Definition, c => ((SetNexusApiKey)c).Run);
|
||||||
|
services.AddSingleton<SetNexusApiKey>();
|
||||||
CommandLineBuilder.RegisterCommand<SteamDownloadFile>(SteamDownloadFile.Definition, c => ((SteamDownloadFile)c).Run);
|
CommandLineBuilder.RegisterCommand<SteamDownloadFile>(SteamDownloadFile.Definition, c => ((SteamDownloadFile)c).Run);
|
||||||
services.AddSingleton<SteamDownloadFile>();
|
services.AddSingleton<SteamDownloadFile>();
|
||||||
CommandLineBuilder.RegisterCommand<SteamDumpAppInfo>(SteamDumpAppInfo.Definition, c => ((SteamDumpAppInfo)c).Run);
|
CommandLineBuilder.RegisterCommand<SteamDumpAppInfo>(SteamDumpAppInfo.Definition, c => ((SteamDumpAppInfo)c).Run);
|
||||||
|
40
Wabbajack.CLI/Verbs/SetNexusApiKey.cs
Normal file
40
Wabbajack.CLI/Verbs/SetNexusApiKey.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Wabbajack.CLI.Builder;
|
||||||
|
using Wabbajack.DTOs.Logins;
|
||||||
|
using Wabbajack.Services.OSIntegrated;
|
||||||
|
|
||||||
|
namespace Wabbajack.CLI.Verbs;
|
||||||
|
|
||||||
|
public class SetNexusApiKey
|
||||||
|
{
|
||||||
|
private readonly EncryptedJsonTokenProvider<NexusOAuthState> _tokenProvider;
|
||||||
|
private readonly ILogger<SetNexusApiKey> _logger;
|
||||||
|
|
||||||
|
public SetNexusApiKey(EncryptedJsonTokenProvider<NexusOAuthState> tokenProvider, ILogger<SetNexusApiKey> logger)
|
||||||
|
{
|
||||||
|
_tokenProvider = tokenProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VerbDefinition Definition = new("set-nexus-api-key",
|
||||||
|
"Sets the Nexus API key to the specified value",
|
||||||
|
[
|
||||||
|
new OptionDefinition(typeof(string), "k", "key", "The Nexus API key")
|
||||||
|
]);
|
||||||
|
|
||||||
|
public async Task<int> Run(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Not setting Nexus API key, that looks like an empty string to me.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _tokenProvider.SetToken(new() { ApiKey = key });
|
||||||
|
_logger.LogInformation("Set Nexus API Key to {key}", key);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -611,7 +611,9 @@ public class ValidateLists
|
|||||||
return (ArchiveStatus.InValid, archive);
|
return (ArchiveStatus.InValid, archive);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (archive.State is Http http && http.Url.Host.EndsWith("github.com"))
|
if (archive.State is Http http && (http.Url.Host.EndsWith("github.com")
|
||||||
|
//TODO: Find a better solution for the list validation of LoversLab files.
|
||||||
|
|| http.Url.Host.EndsWith("loverslab.com")))
|
||||||
return (ArchiveStatus.Valid, archive);
|
return (ArchiveStatus.Valid, archive);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -8,12 +8,13 @@
|
|||||||
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
|
||||||
<Version>$(VERSION)</Version>
|
<Version>$(VERSION)</Version>
|
||||||
<AssemblyName>wabbajack-cli</AssemblyName>
|
<AssemblyName>wabbajack-cli</AssemblyName>
|
||||||
<PublishTrimmed>true</PublishTrimmed>
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
<TimeMode>linked</TimeMode>
|
<TimeMode>linked</TimeMode>
|
||||||
<NoWarn>CS8600</NoWarn>
|
<NoWarn>CS8600</NoWarn>
|
||||||
<NoWarn>CS8601</NoWarn>
|
<NoWarn>CS8601</NoWarn>
|
||||||
<NoWarn>CS8618</NoWarn>
|
<NoWarn>CS8618</NoWarn>
|
||||||
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
|
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -24,7 +25,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="NLog" Version="5.2.5" />
|
<PackageReference Include="NLog" Version="5.2.5" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.5" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.5" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.2" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
<PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />
|
<PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -54,5 +54,6 @@ public enum Game
|
|||||||
[Description("Modding Tools")] ModdingTools,
|
[Description("Modding Tools")] ModdingTools,
|
||||||
|
|
||||||
[Description("Final Fantasy VII Remake")] FinalFantasy7Remake,
|
[Description("Final Fantasy VII Remake")] FinalFantasy7Remake,
|
||||||
[Description("Baldur's Gate 3")] BaldursGate3
|
[Description("Baldur's Gate 3")] BaldursGate3,
|
||||||
|
[Description("Starfield")] Starfield
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ public static class GameRegistry
|
|||||||
MO2ArchiveName = "falloutnv",
|
MO2ArchiveName = "falloutnv",
|
||||||
SteamIDs = new[] {22380, 22490}, // normal and RU version
|
SteamIDs = new[] {22380, 22490}, // normal and RU version
|
||||||
GOGIDs = new long[] {1454587428},
|
GOGIDs = new long[] {1454587428},
|
||||||
|
EpicGameStoreIDs = new[] {"dabb52e328834da7bbe99691e374cb84"},
|
||||||
RequiredFiles = new[]
|
RequiredFiles = new[]
|
||||||
{
|
{
|
||||||
"FalloutNV.exe".ToRelativePath()
|
"FalloutNV.exe".ToRelativePath()
|
||||||
@ -602,6 +603,22 @@ public static class GameRegistry
|
|||||||
MainExecutable = @"bin/bg3.exe".ToRelativePath()
|
MainExecutable = @"bin/bg3.exe".ToRelativePath()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Game.Starfield, new GameMetaData
|
||||||
|
{
|
||||||
|
Game = Game.Starfield,
|
||||||
|
NexusName = "starfield",
|
||||||
|
NexusGameId = 4187,
|
||||||
|
MO2Name = "Starfield",
|
||||||
|
MO2ArchiveName = "Starfield",
|
||||||
|
SteamIDs = [1716740],
|
||||||
|
RequiredFiles = new []
|
||||||
|
{
|
||||||
|
@"Starfield.exe".ToRelativePath()
|
||||||
|
},
|
||||||
|
MainExecutable = @"Starfield.exe".ToRelativePath()
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Game.ModdingTools, new GameMetaData
|
Game.ModdingTools, new GameMetaData
|
||||||
{
|
{
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Wabbajack.DTOs.Logins;
|
|
||||||
|
|
||||||
public class NexusApiState
|
|
||||||
{
|
|
||||||
[JsonPropertyName("api-key")] public string ApiKey { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("cookies")] public Cookie[] Cookies { get; set; }
|
|
||||||
}
|
|
13
Wabbajack.DTOs/Logins/NexusOAuthState.cs
Normal file
13
Wabbajack.DTOs/Logins/NexusOAuthState.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Wabbajack.DTOs.OAuth;
|
||||||
|
|
||||||
|
namespace Wabbajack.DTOs.Logins;
|
||||||
|
|
||||||
|
public class NexusOAuthState
|
||||||
|
{
|
||||||
|
[JsonPropertyName("oauth")]
|
||||||
|
public JwtTokenReply? OAuth { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("api_key")]
|
||||||
|
public string ApiKey { get; set; } = string.Empty;
|
||||||
|
}
|
52
Wabbajack.DTOs/OAuth/JWTTokenReply.cs
Normal file
52
Wabbajack.DTOs/OAuth/JWTTokenReply.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Wabbajack.DTOs.OAuth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JWT Token info as provided by the OAuth server
|
||||||
|
/// </summary>
|
||||||
|
public class JwtTokenReply
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// the token to use for authentication
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("access_token")]
|
||||||
|
public string? AccessToken { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("_received_at")]
|
||||||
|
public long ReceivedAt { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public bool IsExpired => DateTime.FromFileTimeUtc(ReceivedAt) + TimeSpan.FromSeconds(ExpiresIn) - TimeSpan.FromMinutes(5) <= DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// token type, e.g. "Bearer"
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("token_type")]
|
||||||
|
public string? Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// when the access token expires in seconds
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("expires_in")]
|
||||||
|
public ulong ExpiresIn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// token to use to refresh once this one has expired
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("refresh_token")]
|
||||||
|
public string? RefreshToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// space separated list of scopes. defined by the server, currently always "public"?
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("scope")]
|
||||||
|
public string? Scope { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// unix timestamp (seconds resolution) of when the token was created
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("created_at")]
|
||||||
|
public long CreatedAt { get; set; }
|
||||||
|
}
|
@ -48,7 +48,7 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
|
|||||||
|
|
||||||
public override Task<bool> Prepare()
|
public override Task<bool> Prepare()
|
||||||
{
|
{
|
||||||
return Task.FromResult(_api.ApiKey.HaveToken());
|
return Task.FromResult(_api.AuthInfo.HaveToken());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
<PackageReference Include="Shipwreck.Phash" Version="0.5.0" />
|
<PackageReference Include="Shipwreck.Phash" Version="0.5.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.2" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||||
<PackageReference Include="xunit" Version="2.6.1" />
|
<PackageReference Include="xunit" Version="2.6.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Wabbajack.DTOs.Logins;
|
|
||||||
using Wabbajack.Networking.Http.Interfaces;
|
|
||||||
using Wabbajack.Paths;
|
|
||||||
using Wabbajack.Paths.IO;
|
|
||||||
|
|
||||||
namespace Wabbajack.Launcher.Models;
|
|
||||||
|
|
||||||
public class LegacyNexusApiKey : ITokenProvider<NexusApiState>
|
|
||||||
{
|
|
||||||
private AbsolutePath TokenPath => KnownFolders.WabbajackAppLocal.Combine("nexusapikey");
|
|
||||||
public async ValueTask<NexusApiState?> Get()
|
|
||||||
{
|
|
||||||
var data = await TokenPath.ReadAllBytesAsync();
|
|
||||||
var decoded = ProtectedData.Unprotect(data, Encoding.UTF8.GetBytes("nexusapikey"), DataProtectionScope.LocalMachine);
|
|
||||||
var apiKey = JsonSerializer.Deserialize<string>(decoded)!;
|
|
||||||
return new NexusApiState()
|
|
||||||
{
|
|
||||||
ApiKey = apiKey
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask SetToken(NexusApiState val)
|
|
||||||
{
|
|
||||||
throw new System.NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask<bool> Delete()
|
|
||||||
{
|
|
||||||
throw new System.NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HaveToken()
|
|
||||||
{
|
|
||||||
return TokenPath.FileExists();
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,6 @@ using Wabbajack.Downloaders.Http;
|
|||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
using Wabbajack.DTOs.JsonConverters;
|
using Wabbajack.DTOs.JsonConverters;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Launcher.Models;
|
|
||||||
using Wabbajack.Launcher.ViewModels;
|
using Wabbajack.Launcher.ViewModels;
|
||||||
using Wabbajack.Networking.Http;
|
using Wabbajack.Networking.Http;
|
||||||
using Wabbajack.Networking.Http.Interfaces;
|
using Wabbajack.Networking.Http.Interfaces;
|
||||||
@ -57,7 +56,7 @@ internal class Program
|
|||||||
|
|
||||||
services.AddSingleton<MainWindowViewModel>();
|
services.AddSingleton<MainWindowViewModel>();
|
||||||
services.AddSingleton<HttpClient>();
|
services.AddSingleton<HttpClient>();
|
||||||
services.AddSingleton<ITokenProvider<NexusApiState>, NexusApiTokenProvider>();
|
services.AddSingleton<ITokenProvider<NexusOAuthState>, NexusApiTokenProvider>();
|
||||||
services.AddSingleton<HttpDownloader>();
|
services.AddSingleton<HttpDownloader>();
|
||||||
services.AddAllSingleton<IResource, IResource<HttpClient>>(s => new Resource<HttpClient>("Web Requests", 4));
|
services.AddAllSingleton<IResource, IResource<HttpClient>>(s => new Resource<HttpClient>("Web Requests", 4));
|
||||||
services.AddAllSingleton<IHttpDownloader, SingleThreadedDownloader>();
|
services.AddAllSingleton<IHttpDownloader, SingleThreadedDownloader>();
|
||||||
|
@ -32,9 +32,9 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
public Uri GITHUB_REPO = new("https://api.github.com/repos/wabbajack-tools/wabbajack/releases");
|
public Uri GITHUB_REPO = new("https://api.github.com/repos/wabbajack-tools/wabbajack/releases");
|
||||||
private readonly NexusApi _nexusApi;
|
private readonly NexusApi _nexusApi;
|
||||||
private readonly HttpDownloader _downloader;
|
private readonly HttpDownloader _downloader;
|
||||||
private readonly ITokenProvider<NexusApiState> _tokenProvider;
|
private readonly ITokenProvider<NexusOAuthState> _tokenProvider;
|
||||||
|
|
||||||
public MainWindowViewModel(NexusApi nexusApi, HttpDownloader downloader, ITokenProvider<NexusApiState> tokenProvider)
|
public MainWindowViewModel(NexusApi nexusApi, HttpDownloader downloader, ITokenProvider<NexusOAuthState> tokenProvider)
|
||||||
{
|
{
|
||||||
_nexusApi = nexusApi;
|
_nexusApi = nexusApi;
|
||||||
Status = "Checking for new versions";
|
Status = "Checking for new versions";
|
||||||
|
@ -40,4 +40,4 @@ public class NexusApiTests
|
|||||||
var (links, _) = await _api.DownloadLink("skyrimspecialedition", 12604, file.FileId);
|
var (links, _) = await _api.DownloadLink("skyrimspecialedition", 12604, file.FileId);
|
||||||
Assert.True(links.Length > 0);
|
Assert.True(links.Length > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,4 +17,4 @@ public class Startup
|
|||||||
{
|
{
|
||||||
loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(accessor, delegate { return true; }));
|
loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(accessor, delegate { return true; }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@ using Wabbajack.Networking.Http.Interfaces;
|
|||||||
|
|
||||||
namespace Wabbajack.Networking.NexusApi;
|
namespace Wabbajack.Networking.NexusApi;
|
||||||
|
|
||||||
public interface ApiKey : ITokenProvider<NexusApiState>
|
public interface IAuthInfo : ITokenProvider<NexusOAuthState>
|
||||||
{
|
{
|
||||||
}
|
}
|
33
Wabbajack.Networking.NexusApi/DTOs/OAuthUserInfo.cs
Normal file
33
Wabbajack.Networking.NexusApi/DTOs/OAuthUserInfo.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
using Octokit;
|
||||||
|
|
||||||
|
namespace Wabbajack.Networking.NexusApi.DTOs;
|
||||||
|
|
||||||
|
public record OAuthUserInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the User ID.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("sub")]
|
||||||
|
public string Sub { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the User Name.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the avatar url.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("avatar")]
|
||||||
|
public Uri? Avatar { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an array of membership roles.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("membership_roles")]
|
||||||
|
public string[] MembershipRoles { get; set; } = [];
|
||||||
|
}
|
@ -8,4 +8,6 @@ public static class Endpoints
|
|||||||
public const string ModFile = "v1/games/{0}/mods/{1}/files/{2}.json";
|
public const string ModFile = "v1/games/{0}/mods/{1}/files/{2}.json";
|
||||||
public const string DownloadLink = "v1/games/{0}/mods/{1}/files/{2}/download_link.json";
|
public const string DownloadLink = "v1/games/{0}/mods/{1}/files/{2}/download_link.json";
|
||||||
public const string Updates = "v1/games/{0}/mods/updated.json?period={1}";
|
public const string Updates = "v1/games/{0}/mods/updated.json?period={1}";
|
||||||
|
|
||||||
|
public const string OAuthValidate = "https://users.nexusmods.com/oauth/userinfo";
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@ -12,6 +13,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
|
using Wabbajack.DTOs.OAuth;
|
||||||
using Wabbajack.Networking.Http;
|
using Wabbajack.Networking.Http;
|
||||||
using Wabbajack.Networking.Http.Interfaces;
|
using Wabbajack.Networking.Http.Interfaces;
|
||||||
using Wabbajack.Networking.NexusApi.DTOs;
|
using Wabbajack.Networking.NexusApi.DTOs;
|
||||||
@ -28,14 +30,15 @@ public class NexusApi
|
|||||||
private readonly JsonSerializerOptions _jsonOptions;
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
private readonly IResource<HttpClient> _limiter;
|
private readonly IResource<HttpClient> _limiter;
|
||||||
private readonly ILogger<NexusApi> _logger;
|
private readonly ILogger<NexusApi> _logger;
|
||||||
public readonly ITokenProvider<NexusApiState> ApiKey;
|
public readonly ITokenProvider<NexusOAuthState> AuthInfo;
|
||||||
private DateTime _lastValidated;
|
private DateTime _lastValidated;
|
||||||
private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
|
private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
|
||||||
|
private AsyncLock _authLock = new();
|
||||||
|
|
||||||
public NexusApi(ITokenProvider<NexusApiState> apiKey, ILogger<NexusApi> logger, HttpClient client,
|
public NexusApi(ITokenProvider<NexusOAuthState> authInfo, ILogger<NexusApi> logger, HttpClient client,
|
||||||
IResource<HttpClient> limiter, ApplicationInfo appInfo, JsonSerializerOptions jsonOptions)
|
IResource<HttpClient> limiter, ApplicationInfo appInfo, JsonSerializerOptions jsonOptions)
|
||||||
{
|
{
|
||||||
ApiKey = apiKey;
|
AuthInfo = authInfo;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_client = client;
|
_client = client;
|
||||||
_appInfo = appInfo;
|
_appInfo = appInfo;
|
||||||
@ -48,8 +51,27 @@ public class NexusApi
|
|||||||
public virtual async Task<(ValidateInfo info, ResponseMetadata header)> Validate(
|
public virtual async Task<(ValidateInfo info, ResponseMetadata header)> Validate(
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var msg = await GenerateMessage(HttpMethod.Get, Endpoints.Validate);
|
var (isApi, code) = await GetAuthInfo();
|
||||||
return await Send<ValidateInfo>(msg, token);
|
|
||||||
|
if (isApi)
|
||||||
|
{
|
||||||
|
var msg = await GenerateMessage(HttpMethod.Get, Endpoints.Validate);
|
||||||
|
_lastValidatedInfo = await Send<ValidateInfo>(msg, token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var msg = await GenerateMessage(HttpMethod.Get, Endpoints.OAuthValidate);
|
||||||
|
var (data, header) = await Send<OAuthUserInfo>(msg, token);
|
||||||
|
var validateInfo = new ValidateInfo
|
||||||
|
{
|
||||||
|
IsPremium = data.MembershipRoles.Contains("premium"),
|
||||||
|
Name = data.Name,
|
||||||
|
};
|
||||||
|
_lastValidatedInfo = (validateInfo, header);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastValidated = DateTime.Now;
|
||||||
|
return _lastValidatedInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(ValidateInfo info, ResponseMetadata header)> ValidateCached(
|
public async Task<(ValidateInfo info, ResponseMetadata header)> ValidateCached(
|
||||||
@ -60,8 +82,8 @@ public class NexusApi
|
|||||||
return _lastValidatedInfo;
|
return _lastValidatedInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg = await GenerateMessage(HttpMethod.Get, Endpoints.Validate);
|
await Validate(token);
|
||||||
_lastValidatedInfo = await Send<ValidateInfo>(msg, token);
|
|
||||||
return _lastValidatedInfo;
|
return _lastValidatedInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,25 +188,95 @@ public class NexusApi
|
|||||||
protected virtual async ValueTask<HttpRequestMessage> GenerateMessage(HttpMethod method, string uri,
|
protected virtual async ValueTask<HttpRequestMessage> GenerateMessage(HttpMethod method, string uri,
|
||||||
params object?[] parameters)
|
params object?[] parameters)
|
||||||
{
|
{
|
||||||
|
using var _ = await _authLock.WaitAsync();
|
||||||
var msg = new HttpRequestMessage();
|
var msg = new HttpRequestMessage();
|
||||||
msg.Method = method;
|
msg.Method = method;
|
||||||
|
|
||||||
var userAgent =
|
var userAgent =
|
||||||
$"{_appInfo.ApplicationSlug}/{_appInfo.Version} ({_appInfo.OSVersion}; {_appInfo.Platform})";
|
$"{_appInfo.ApplicationSlug}/{_appInfo.Version} ({_appInfo.OSVersion}; {_appInfo.Platform})";
|
||||||
|
|
||||||
if (!ApiKey.HaveToken())
|
await AddAuthHeaders(msg);
|
||||||
throw new Exception("Please log into the Nexus before attempting to use Wabbajack");
|
|
||||||
|
|
||||||
var token = (await ApiKey.Get())!;
|
if (uri.StartsWith("http"))
|
||||||
|
{
|
||||||
|
msg.RequestUri = new Uri($"{string.Format(uri, parameters)}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
msg.RequestUri = new Uri($"https://api.nexusmods.com/{string.Format(uri, parameters)}");
|
||||||
|
}
|
||||||
|
|
||||||
msg.RequestUri = new Uri($"https://api.nexusmods.com/{string.Format(uri, parameters)}");
|
|
||||||
msg.Headers.Add("User-Agent", userAgent);
|
msg.Headers.Add("User-Agent", userAgent);
|
||||||
msg.Headers.Add("Application-Name", _appInfo.ApplicationSlug);
|
msg.Headers.Add("Application-Name", _appInfo.ApplicationSlug);
|
||||||
msg.Headers.Add("Application-Version", _appInfo.Version);
|
msg.Headers.Add("Application-Version", _appInfo.Version);
|
||||||
msg.Headers.Add("apikey", token.ApiKey);
|
|
||||||
|
|
||||||
msg.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
msg.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ValueTask AddAuthHeaders(HttpRequestMessage msg)
|
||||||
|
{
|
||||||
|
var (isApi, code) = await GetAuthInfo();
|
||||||
|
if (string.IsNullOrWhiteSpace(code))
|
||||||
|
throw new Exception("No API Key or OAuth Token found for NexusMods");
|
||||||
|
|
||||||
|
if (isApi)
|
||||||
|
msg.Headers.Add("apikey", code);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
msg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<(bool IsApiKey, string code)> GetAuthInfo()
|
||||||
|
{
|
||||||
|
if (AuthInfo.HaveToken())
|
||||||
|
{
|
||||||
|
var info = await AuthInfo.Get();
|
||||||
|
if (info!.OAuth != null)
|
||||||
|
{
|
||||||
|
if (info!.OAuth.IsExpired)
|
||||||
|
info = await RefreshToken(info, CancellationToken.None);
|
||||||
|
return (false, info.OAuth!.AccessToken!);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(info.ApiKey))
|
||||||
|
{
|
||||||
|
return (true, info.ApiKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (Environment.GetEnvironmentVariable("NEXUS_API_KEY") is { } apiKey)
|
||||||
|
{
|
||||||
|
return (true, apiKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<NexusOAuthState> RefreshToken(NexusOAuthState state, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Refreshing OAuth Token");
|
||||||
|
var request = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "grant_type", "refresh_token" },
|
||||||
|
{ "client_id", "wabbajack" },
|
||||||
|
{ "refresh_token", state.OAuth!.RefreshToken },
|
||||||
|
};
|
||||||
|
|
||||||
|
var content = new FormUrlEncodedContent(request);
|
||||||
|
|
||||||
|
var response = await _client.PostAsync($"https://users.nexusmods.com/oauth/token", content, cancel);
|
||||||
|
var responseString = await response.Content.ReadAsStringAsync(cancel);
|
||||||
|
var newJwt = JsonSerializer.Deserialize<JwtTokenReply>(responseString);
|
||||||
|
|
||||||
|
state.OAuth = newJwt;
|
||||||
|
await AuthInfo.SetToken(state);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<(UpdateEntry[], ResponseMetadata headers)> GetUpdates(Game game, CancellationToken token)
|
public async Task<(UpdateEntry[], ResponseMetadata headers)> GetUpdates(Game game, CancellationToken token)
|
||||||
{
|
{
|
||||||
@ -274,7 +366,7 @@ public class NexusApi
|
|||||||
private async Task CheckAccess()
|
private async Task CheckAccess()
|
||||||
{
|
{
|
||||||
var msg = new HttpRequestMessage(HttpMethod.Get, "https://www.nexusmods.com/users/myaccount");
|
var msg = new HttpRequestMessage(HttpMethod.Get, "https://www.nexusmods.com/users/myaccount");
|
||||||
msg.AddCookies((await ApiKey.Get())!.Cookies);
|
throw new NotSupportedException("Uploading to NexusMods is currently disabled");
|
||||||
using var response = await _client.SendAsync(msg);
|
using var response = await _client.SendAsync(msg);
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
@ -292,7 +384,7 @@ public class NexusApi
|
|||||||
new Uri(
|
new Uri(
|
||||||
$"https://www.nexusmods.com/{d.Game.MetaData().NexusName}/mods/edit/?id={d.ModId}&game_id={d.GameId}&step=files");
|
$"https://www.nexusmods.com/{d.Game.MetaData().NexusName}/mods/edit/?id={d.ModId}&game_id={d.GameId}&step=files");
|
||||||
|
|
||||||
msg.AddCookies((await ApiKey.Get())!.Cookies);
|
throw new NotSupportedException("Uploading to NexusMods is currently disabled");
|
||||||
var form = new MultipartFormDataContent();
|
var form = new MultipartFormDataContent();
|
||||||
form.Add(new StringContent(d.GameId.ToString()), "game_id");
|
form.Add(new StringContent(d.GameId.ToString()), "game_id");
|
||||||
form.Add(new StringContent(d.Name), "name");
|
form.Add(new StringContent(d.Name), "name");
|
||||||
@ -330,6 +422,7 @@ public class NexusApi
|
|||||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<bool> IsPremium(CancellationToken token)
|
public async Task<bool> IsPremium(CancellationToken token)
|
||||||
{
|
{
|
||||||
|
@ -25,7 +25,7 @@ public class ProxiedNexusApi : NexusApi
|
|||||||
Endpoints.ModFile
|
Endpoints.ModFile
|
||||||
};
|
};
|
||||||
|
|
||||||
public ProxiedNexusApi(ITokenProvider<NexusApiState> apiKey, ILogger<ProxiedNexusApi> logger, HttpClient client,
|
public ProxiedNexusApi(ITokenProvider<NexusOAuthState> apiKey, ILogger<ProxiedNexusApi> logger, HttpClient client,
|
||||||
IResource<HttpClient> limiter,
|
IResource<HttpClient> limiter,
|
||||||
ApplicationInfo appInfo, JsonSerializerOptions jsonOptions, ITokenProvider<WabbajackApiState> apiState,
|
ApplicationInfo appInfo, JsonSerializerOptions jsonOptions, ITokenProvider<WabbajackApiState> apiState,
|
||||||
ClientConfiguration wabbajackClientConfiguration)
|
ClientConfiguration wabbajackClientConfiguration)
|
||||||
|
@ -166,7 +166,7 @@ public static class ServiceExtensions
|
|||||||
service.AddBethesdaNet();
|
service.AddBethesdaNet();
|
||||||
|
|
||||||
// Token Providers
|
// Token Providers
|
||||||
service.AddAllSingleton<ITokenProvider<NexusApiState>, EncryptedJsonTokenProvider<NexusApiState>, NexusApiTokenProvider>();
|
service.AddAllSingleton<ITokenProvider<NexusOAuthState>, EncryptedJsonTokenProvider<NexusOAuthState>, NexusApiTokenProvider>();
|
||||||
service.AddAllSingleton<ITokenProvider<MegaToken>, EncryptedJsonTokenProvider<MegaToken>, MegaTokenProvider>();
|
service.AddAllSingleton<ITokenProvider<MegaToken>, EncryptedJsonTokenProvider<MegaToken>, MegaTokenProvider>();
|
||||||
|
|
||||||
service.AddAllSingleton<ITokenProvider<BethesdaNetLoginState>, EncryptedJsonTokenProvider<BethesdaNetLoginState>, BethesdaNetTokenProvider>();
|
service.AddAllSingleton<ITokenProvider<BethesdaNetLoginState>, EncryptedJsonTokenProvider<BethesdaNetLoginState>, BethesdaNetTokenProvider>();
|
||||||
|
@ -5,10 +5,10 @@ using Wabbajack.Networking.NexusApi;
|
|||||||
|
|
||||||
namespace Wabbajack.Services.OSIntegrated.TokenProviders;
|
namespace Wabbajack.Services.OSIntegrated.TokenProviders;
|
||||||
|
|
||||||
public class NexusApiTokenProvider : EncryptedJsonTokenProvider<NexusApiState>, ApiKey
|
public class NexusApiTokenProvider : EncryptedJsonTokenProvider<NexusOAuthState>, IAuthInfo
|
||||||
{
|
{
|
||||||
public NexusApiTokenProvider(ILogger<NexusApiTokenProvider> logger, DTOSerializer dtos) : base(logger, dtos,
|
public NexusApiTokenProvider(ILogger<NexusApiTokenProvider> logger, DTOSerializer dtos) : base(logger, dtos,
|
||||||
"nexus-login")
|
"nexus-oauth-info")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.118" />
|
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.118" PrivateAssets="None" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
Loading…
Reference in New Issue
Block a user