diff --git a/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj index 44aba86..87f53ab 100644 --- a/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj +++ b/ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj @@ -1,48 +1,48 @@  - - net6.0 - disable - disable - True - false - false - Stephen Hodgson - ElevenLabs-DotNet-Proxy - A simple Proxy API gateway for ElevenLabs-DotNet to make authenticated requests from a front end application without exposing your API keys. - 2023 - https://github.com/RageAgainstThePixel/ElevenLabs-DotNet - https://github.com/RageAgainstThePixel/ElevenLabs-DotNet - ElevenLabs, AI, ML, API, api-proxy, proxy, gateway - ElevenLabs API Proxy - ElevenLabs-DotNet-Proxy - 1.0.1 - ElevenLabs.Proxy - Initial Release! - True - false - Readme.md - True - ElevenLabsIcon.png - LICENSE - - - - - - - - True - \ - - - True - \ - - - - - True - \ - - + + net6.0 + disable + disable + True + false + false + Stephen Hodgson + ElevenLabs-DotNet-Proxy + A simple Proxy API gateway for ElevenLabs-DotNet to make authenticated requests from a front end application without exposing your API keys. + 2024 + https://github.com/RageAgainstThePixel/ElevenLabs-DotNet + https://github.com/RageAgainstThePixel/ElevenLabs-DotNet + ElevenLabs, AI, ML, API, api-proxy, proxy, gateway + ElevenLabs API Proxy + ElevenLabs-DotNet-Proxy + 1.2.0 + ElevenLabs.Proxy + Initial Release! + True + false + Readme.md + True + ElevenLabsIcon.png + LICENSE + + + + + + + + True + \ + + + True + \ + + + + + True + \ + + diff --git a/ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs b/ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs index 6323cf3..37a4f12 100644 --- a/ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs +++ b/ElevenLabs-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs @@ -1,5 +1,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace ElevenLabs.Proxy @@ -9,5 +10,8 @@ public abstract class AbstractAuthenticationFilter : IAuthenticationFilter { /// public abstract void ValidateAuthentication(IHeaderDictionary request); + + /// + public abstract Task ValidateAuthenticationAsync(IHeaderDictionary request); } } diff --git a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs index af4f5a7..12a14e4 100644 --- a/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs +++ b/ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Net.Http.Headers; @@ -26,7 +27,7 @@ public class ElevenLabsProxyStartup private IAuthenticationFilter authenticationFilter; // Copied from https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83 - private static readonly HashSet ExcludedHeaders = new HashSet() + private static readonly HashSet excludedHeaders = new() { HeaderNames.Connection, HeaderNames.TransferEncoding, @@ -49,7 +50,12 @@ public class ElevenLabsProxyStartup #endif }; - public void ConfigureServices(IServiceCollection services) { } + /// + /// Configures the and services. + /// + /// + public void ConfigureServices(IServiceCollection services) + => SetupServices(services.BuildServiceProvider()); public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { @@ -58,8 +64,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } - elevenLabsClient = app.ApplicationServices.GetRequiredService(); - authenticationFilter = app.ApplicationServices.GetRequiredService(); + SetupServices(app.ApplicationServices); app.UseHttpsRedirection(); app.UseRouting(); @@ -77,25 +82,43 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) /// Startup args. /// with configured and . public static IHost CreateDefaultHost(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter - { - return Host.CreateDefaultBuilder(args) + => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - webBuilder.ConfigureKestrel(options => - { - options.AllowSynchronousIO = false; - options.Limits.MinRequestBodyDataRate = null; - options.Limits.MinResponseDataRate = null; - options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); - options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2); - }); + webBuilder.ConfigureKestrel(ConfigureKestrel); }) .ConfigureServices(services => { services.AddSingleton(elevenLabsClient); services.AddSingleton(); }).Build(); + + public static WebApplication CreateWebApplication(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter + { + var builder = WebApplication.CreateBuilder(args); + builder.WebHost.ConfigureKestrel(ConfigureKestrel); + builder.Services.AddSingleton(elevenLabsClient); + builder.Services.AddSingleton(); + var app = builder.Build(); + var startup = new ElevenLabsProxyStartup(); + startup.Configure(app, app.Environment); + return app; + } + + private static void ConfigureKestrel(KestrelServerOptions options) + { + options.AllowSynchronousIO = false; + options.Limits.MinRequestBodyDataRate = null; + options.Limits.MinResponseDataRate = null; + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); + options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2); + } + + private void SetupServices(IServiceProvider serviceProvider) + { + elevenLabsClient = serviceProvider.GetRequiredService(); + authenticationFilter = serviceProvider.GetRequiredService(); } private static async Task HealthEndpoint(HttpContext context) @@ -115,31 +138,34 @@ private async Task HandleRequest(HttpContext httpContext, string endpoint) { try { + // ReSharper disable once MethodHasAsyncOverload + // just in case either method is implemented we call it twice. authenticationFilter.ValidateAuthentication(httpContext.Request.Headers); + await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers); var method = new HttpMethod(httpContext.Request.Method); var uri = new Uri(string.Format(elevenLabsClient.ElevenLabsClientSettings.BaseRequestUrlFormat, $"{endpoint}{httpContext.Request.QueryString}")); - var elevenLabsRequest = new HttpRequestMessage(method, uri); + using var request = new HttpRequestMessage(method, uri); - elevenLabsRequest.Content = new StreamContent(httpContext.Request.Body); + request.Content = new StreamContent(httpContext.Request.Body); if (httpContext.Request.ContentType != null) { - elevenLabsRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType); } - var proxyResponse = await elevenLabsClient.Client.SendAsync(elevenLabsRequest, HttpCompletionOption.ResponseHeadersRead); + var proxyResponse = await elevenLabsClient.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); httpContext.Response.StatusCode = (int)proxyResponse.StatusCode; foreach (var (key, value) in proxyResponse.Headers) { - if (ExcludedHeaders.Contains(key)) { continue; } + if (excludedHeaders.Contains(key)) { continue; } httpContext.Response.Headers[key] = value.ToArray(); } foreach (var (key, value) in proxyResponse.Content.Headers) { - if (ExcludedHeaders.Contains(key)) { continue; } + if (excludedHeaders.Contains(key)) { continue; } httpContext.Response.Headers[key] = value.ToArray(); } diff --git a/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs b/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs index d568b07..2b692e8 100644 --- a/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs +++ b/ElevenLabs-DotNet-Proxy/Proxy/IAuthenticationFilter.cs @@ -1,7 +1,8 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Security.Authentication; using Microsoft.AspNetCore.Http; +using System.Security.Authentication; +using System.Threading.Tasks; namespace ElevenLabs.Proxy { @@ -17,5 +18,13 @@ public interface IAuthenticationFilter /// /// void ValidateAuthentication(IHeaderDictionary request); + + /// + /// Checks the headers for your user issued token. + /// If it's not valid, then throw . + /// + /// + /// + Task ValidateAuthenticationAsync(IHeaderDictionary request); } } diff --git a/ElevenLabs-DotNet/Authentication/ElevenLabsClientSettings.cs b/ElevenLabs-DotNet/Authentication/ElevenLabsClientSettings.cs index 715e202..c876e39 100644 --- a/ElevenLabs-DotNet/Authentication/ElevenLabsClientSettings.cs +++ b/ElevenLabs-DotNet/Authentication/ElevenLabsClientSettings.cs @@ -6,8 +6,9 @@ namespace ElevenLabs { public sealed class ElevenLabsClientSettings { - internal const string ElevenLabsDomain = "api.elevenlabs.io"; + internal const string Https = "https://"; internal const string DefaultApiVersion = "v1"; + internal const string ElevenLabsDomain = "api.elevenlabs.io"; /// /// Creates a new instance of for use with ElevenLabs API. @@ -17,7 +18,7 @@ public ElevenLabsClientSettings() Domain = ElevenLabsDomain; ApiVersion = "v1"; BaseRequest = $"/{ApiVersion}/"; - BaseRequestUrlFormat = $"https://{Domain}{BaseRequest}{{0}}"; + BaseRequestUrlFormat = $"{Https}{Domain}{BaseRequest}{{0}}"; } /// @@ -32,8 +33,8 @@ public ElevenLabsClientSettings(string domain, string apiVersion = DefaultApiVer domain = ElevenLabsDomain; } - if (!domain.Contains(".") && - !domain.Contains(":")) + if (!domain.Contains('.') && + !domain.Contains(':')) { throw new ArgumentException($"You're attempting to pass a \"resourceName\" parameter to \"{nameof(domain)}\". Please specify \"resourceName:\" for this parameter in constructor."); } @@ -43,10 +44,10 @@ public ElevenLabsClientSettings(string domain, string apiVersion = DefaultApiVer apiVersion = DefaultApiVersion; } - Domain = domain; + Domain = domain.Contains("http") ? domain : $"{Https}{domain}"; ApiVersion = apiVersion; BaseRequest = $"/{ApiVersion}/"; - BaseRequestUrlFormat = $"https://{Domain}{BaseRequest}{{0}}"; + BaseRequestUrlFormat = $"{Domain}{BaseRequest}{{0}}"; } public string Domain { get; } @@ -57,6 +58,6 @@ public ElevenLabsClientSettings(string domain, string apiVersion = DefaultApiVer public string BaseRequestUrlFormat { get; } - public static ElevenLabsClientSettings Default { get; } = new ElevenLabsClientSettings(); + public static ElevenLabsClientSettings Default { get; } = new(); } } diff --git a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj index 831b40f..d7a2c84 100644 --- a/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj +++ b/ElevenLabs-DotNet/ElevenLabs-DotNet.csproj @@ -12,10 +12,14 @@ Stephen Hodgson ElevenLabs-DotNet RageAgainstThePixel - 2023 + 2024 ElevenLabs, AI, ML, Voice, TTS - 2.1.1 - Version 2.1.1 + 2.2.0 + Version 2.2.0 +- Changed ElevenLabsClient to be IDisposable + - The ElevenLabsClient must now be disposed if you do not pass your own HttpClient +- Updated ElevenLabsClientSettings to accept custom domains +Version 2.1.1 - Added VoicesEndpoint.GetAllVoicesAsync overload that allows skipping downloading the voice settings Version 2.1.0 - Added ElevenLabsClient.EnableDebug option to enable and disable for all endpoints diff --git a/ElevenLabs-DotNet/ElevenLabsClient.cs b/ElevenLabs-DotNet/ElevenLabsClient.cs index aa33a62..a064398 100644 --- a/ElevenLabs-DotNet/ElevenLabsClient.cs +++ b/ElevenLabs-DotNet/ElevenLabsClient.cs @@ -6,6 +6,7 @@ using ElevenLabs.User; using ElevenLabs.VoiceGeneration; using ElevenLabs.Voices; +using System; using System.Net.Http; using System.Security.Authentication; using System.Text.Json; @@ -13,7 +14,7 @@ namespace ElevenLabs { - public sealed class ElevenLabsClient + public sealed class ElevenLabsClient : IDisposable { /// /// Creates a new client for the Eleven Labs API, handling auth and allowing for access to various API endpoints. @@ -27,6 +28,12 @@ public sealed class ElevenLabsClient /// /// Optional, . /// Raised when authentication details are missing or invalid. + /// implements to manage the lifecycle of the resources it uses, including . + /// + /// When you initialize , it will create an internal instance if one is not provided. + /// This internal HttpClient is disposed of when ElevenLabsClient is disposed of. + /// If you provide an external HttpClient instance to ElevenLabsClient, you are responsible for managing its disposal. + /// public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null, ElevenLabsClientSettings clientSettings = null, HttpClient httpClient = null) { ElevenLabsAuthentication = elevenLabsAuthentication ?? ElevenLabsAuthentication.Default; @@ -37,7 +44,19 @@ public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/RageAgainstThePixel/ElevenLabs-DotNet#authentication for details."); } - Client = httpClient ?? new HttpClient(); + if (httpClient == null) + { + httpClient = new HttpClient(new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15) + }); + } + else + { + isCustomClient = true; + } + + Client = httpClient; Client.DefaultRequestHeaders.Add("User-Agent", "ElevenLabs-DotNet"); Client.DefaultRequestHeaders.Add("xi-api-key", ElevenLabsAuthentication.ApiKey); @@ -49,6 +68,38 @@ public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null VoiceGenerationEndpoint = new VoiceGenerationEndpoint(this); } + ~ElevenLabsClient() + { + Dispose(false); + } + + #region IDisposable + + private bool isDisposed; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!isDisposed && disposing) + { + if (!isCustomClient) + { + Client?.Dispose(); + } + + isDisposed = true; + } + } + + #endregion IDisposable + + private bool isCustomClient; + /// /// to use when making calls to the API. /// @@ -57,7 +108,7 @@ public ElevenLabsClient(ElevenLabsAuthentication elevenLabsAuthentication = null /// /// The to use when making calls to the API. /// - internal static JsonSerializerOptions JsonSerializationOptions { get; } = new JsonSerializerOptions + internal static JsonSerializerOptions JsonSerializationOptions { get; } = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };