Initial Commit

This commit is contained in:
Lillith Fox 2024-11-11 14:15:51 -05:00
commit 22c2de9cc6
12 changed files with 594 additions and 0 deletions

View file

@ -0,0 +1,12 @@
using System.Net;
using VRChat.API.Client;
namespace VRCAuthProxy;
public class ApiClientWithCookies : ApiClient
{
public CookieContainer GetCookieContainer()
{
return CookieContainer;
}
}

62
VRCAuthProxy/Config.cs Normal file
View file

@ -0,0 +1,62 @@
using System.Text.Json;
namespace VRCAuthProxy;
public class ConfigAccount
{
public string username { get; set; }
public string password { get; set; }
public string? totpSecret { get; set; }
}
public class iConfig
{
public List<ConfigAccount> accounts { get; set; }
}
// Load config from appsettings.json
public class Config
{
private static Config? _instance;
public List<ConfigAccount> Accounts { get; set; }
public static Config Instance
{
get
{
if (_instance == null) _instance = Load();
return _instance;
}
}
public static Config Load()
{
var config = new Config();
var configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
if (File.Exists(configPath))
{
var json = File.ReadAllText(configPath);
// load iConfig
var iConfig = JsonSerializer.Deserialize<iConfig>(json);
if (iConfig == null) throw new Exception("Failed to load config");
config.Accounts = iConfig.accounts;
}
else
{
Console.WriteLine("No config found at " + configPath);
}
return config;
}
public void Save()
{
var configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
var iConfig = new iConfig
{
accounts = Accounts
};
var json = JsonSerializer.Serialize(iConfig, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(configPath, json);
}
}

21
VRCAuthProxy/Dockerfile Normal file
View file

@ -0,0 +1,21 @@
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
USER $APP_UID
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["VRCAuthProxy/VRCAuthProxy.csproj", "VRCAuthProxy/"]
RUN dotnet restore "VRCAuthProxy/VRCAuthProxy.csproj"
COPY . .
WORKDIR "/src/VRCAuthProxy"
RUN dotnet build "VRCAuthProxy.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "VRCAuthProxy.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "VRCAuthProxy.dll"]

184
VRCAuthProxy/Program.cs Normal file
View file

@ -0,0 +1,184 @@
// See https://aka.ms/new-console-template for more information
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using OtpNet;
using VRCAuthProxy;
using VRChat.API.Api;
using VRChat.API.Client;
using VRChat.API.Model;
using HttpMethod = System.Net.Http.HttpMethod;
var httpClient = new HttpClient();
var apiAccounts = new List<ApiClientWithCookies>();
// Push the first account to the end of the list
void RotateAccount()
{
var account = Config.Instance.Accounts.First();
Config.Instance.Accounts.Remove(account);
Config.Instance.Accounts.Add(account);
}
String UserAgent = "VRCAuthProxy V1.0.0 (https://github.com/PrideVRCommunity/VRCAuthProxy)";
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
Config.Instance.Accounts.ForEach(async account =>
{
try
{
Console.WriteLine($"Creating API for {account.username}");
// clone base config
var config = new Configuration();
config.UserAgent = UserAgent;
config.BasePath = "https://api.vrchat.cloud/api/1";
config.Username = account.username;
config.Password = account.password;
// create api client
var api = new ApiClientWithCookies();
var authApi = new AuthenticationApi(api, api, config);
var authResp = authApi.GetCurrentUserWithHttpInfo();
if (authResp.RawContent.Contains("totp"))
{
Console.WriteLine($"TOTP required for {account.username}");
if (account.totpSecret == null)
{
Console.WriteLine($"No TOTP secret found for {account.username}");
return;
}
// totp constructor needs a byte array decoded from the base32 secret
var totp = new Totp(Base32Encoding.ToBytes(account.totpSecret.Replace(" ", "")));
var code = totp.ComputeTotp();
if (code == null)
{
Console.WriteLine($"Failed to generate TOTP for {account.username}");
return;
}
var verifyRes = authApi.Verify2FA(new TwoFactorAuthCode(code));
if (verifyRes == null || verifyRes.Verified == false)
{
Console.WriteLine($"Failed to verify TOTP for {account.username}");
return;
}
}
var curUser = authApi.GetCurrentUser();
if (curUser == null) throw new Exception("Failed to get current user");
Console.WriteLine($"Logged in as {curUser.DisplayName}");
apiAccounts.Add(api);
} catch (ApiException e)
{
Console.WriteLine($"Failed to create API for {account.username}: {e.Message}, {e.ErrorCode}, {e}");
}
});
app.MapGet("/", () => $"Logged in with {apiAccounts.Count} accounts");
app.MapGet("/rotate", () =>
{
RotateAccount();
return "Rotated account";
});
// Proxy all requests starting with /api/1 to the VRChat API
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api/1"))
{
if (apiAccounts.Count == 0)
{
context.Response.StatusCode = 500;
await context.Response.WriteAsync("No accounts available");
return;
}
var account = apiAccounts.First();
var requestOpts = new RequestOptions();
requestOpts.Operation = context.Request.Method;
var path = context.Request.Path.ToString().Replace("/api/1", "") + context.Request.QueryString;
var message = new HttpRequestMessage
{
RequestUri = new Uri("https://api.vrchat.cloud/api/1" + path),
Method = new HttpMethod(context.Request.Method)
};
// Add common headers to request message
message.Headers.Add("User-Agent", UserAgent);
message.Headers.Add("Cookie", account.GetCookieContainer().GetCookieHeader(new Uri("https://api.vrchat.cloud")));
// Handle request body for methods that support content (POST, PUT, DELETE)
if (context.Request.ContentLength > 0 || context.Request.Headers.ContainsKey("Transfer-Encoding"))
{
message.Content = new StreamContent(context.Request.Body);
// Add content-specific headers to message.Content.Headers
foreach (var header in context.Request.Headers)
{
if (header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) ||
header.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) ||
header.Key.Equals("Content-Disposition", StringComparison.OrdinalIgnoreCase))
{
message.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
else if (!message.Headers.Contains(header.Key) && header.Key != "Host")
{
// Add non-content headers to message.Headers
message.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
}
}
else
{
// Add non-body headers to message.Headers for GET and other bodyless requests
foreach (var header in context.Request.Headers)
{
if (!message.Headers.Contains(header.Key) && header.Key != "Host")
{
message.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
}
}
// Send the request
var response = await httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead);
// Copy response status code and headers
context.Response.StatusCode = (int)response.StatusCode;
foreach (var header in response.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
foreach (var header in response.Content.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
context.Response.Headers.Remove("transfer-encoding");
// Copy response content to the response body
using (var responseStream = await response.Content.ReadAsStreamAsync())
using (var memoryStream = new MemoryStream())
{
await responseStream.CopyToAsync(memoryStream);
memoryStream.Position = 0;
await memoryStream.CopyToAsync(context.Response.Body);
}
}
else
{
await next();
}
});
app.Run();

View file

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.8"/>
<PackageReference Include="Otp.NET" Version="1.4.0"/>
<PackageReference Include="VRChat.API" Version="1.18.8"/>
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings.template.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,9 @@
{
"accounts": [
{
"username": "username",
"password": "password",
"totpSecret": "totp secret"
}
]
}