// See https://aka.ms/new-console-template for more information using System.Diagnostics; using System.Net; using System.Net.Http.Json; using System.Text; using System.Web; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using OtpNet; using VRCAuthProxy; using VRCAuthProxy.types; using HttpMethod = System.Net.Http.HttpMethod; using User = VRCAuthProxy.types.User; var apiAccounts = new List(); // 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); } var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); Config.Instance.Accounts.ForEach(async account => { try { var cookieContainer = new CookieContainer(); var handler = new HttpClientHandler { CookieContainer = cookieContainer }; var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://api.vrchat.cloud/api/1") }; httpClient.DefaultRequestHeaders.Add("User-Agent", "VRCAuthProxy V1.0.0 (https://github.com/PrideVRCommunity/VRCAuthProxy)"); Console.WriteLine($"Creating API for {account.username}"); string encodedUsername = HttpUtility.UrlEncode(account.username); string encodedPassword = HttpUtility.UrlEncode(account.password); // Create Basic auth string string authString = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{encodedUsername}:{encodedPassword}")); // Add basic auth header var request = new HttpRequestMessage(HttpMethod.Get, "/api/1/auth/user"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authString); var authResp = await httpClient.SendAsync(request); if ((await authResp.Content.ReadAsStringAsync()).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 verifyReq = new HttpRequestMessage(HttpMethod.Post, "/api/1/auth/twofactorauth/totp/verify"); // set content type verifyReq.Content = new StringContent($"{{\"code\":\"{code}\"}}", Encoding.UTF8, "application/json"); var verifyResp = await httpClient.SendAsync(verifyReq); var verifyRes = await verifyResp.Content.ReadFromJsonAsync(); if (verifyRes.verified == false) { Console.WriteLine($"Failed to verify TOTP for {account.username}"); return; } } var curUserResp = await httpClient.GetAsync("/api/1/auth/user"); var curUser = await curUserResp.Content.ReadFromJsonAsync(); Console.WriteLine($"Logged in as {curUser.displayName}"); apiAccounts.Add(httpClient); } catch (HttpRequestException e) { Console.WriteLine($"Failed to create API for {account.username}: {e.Message}, {e.StatusCode}, {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 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) }; // 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 account.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();