Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions core/providers/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package utils

import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -103,7 +105,6 @@ func ConfigureProxy(client *fasthttp.Client, proxyConfig *schemas.ProxyConfig, l
}

var dialFunc fasthttp.DialFunc

// Create the appropriate proxy based on type
switch proxyConfig.Type {
case schemas.NoProxy:
Expand All @@ -113,7 +114,18 @@ func ConfigureProxy(client *fasthttp.Client, proxyConfig *schemas.ProxyConfig, l
logger.Warn("Warning: HTTP proxy URL is required for setting up proxy")
return client
}
dialFunc = fasthttpproxy.FasthttpHTTPDialer(proxyConfig.URL)
proxyURL := proxyConfig.URL
if proxyConfig.Username != "" && proxyConfig.Password != "" {
parsedURL, err := url.Parse(proxyConfig.URL)
if err != nil {
logger.Warn("Invalid proxy configuration: invalid HTTP proxy URL")
return client
}
// Set user and password in the parsed URL
parsedURL.User = url.UserPassword(proxyConfig.Username, proxyConfig.Password)
proxyURL = parsedURL.String()
}
dialFunc = fasthttpproxy.FasthttpHTTPDialer(proxyURL)
case schemas.Socks5Proxy:
if proxyConfig.URL == "" {
logger.Warn("Warning: SOCKS5 proxy URL is required for setting up proxy")
Expand Down Expand Up @@ -144,9 +156,40 @@ func ConfigureProxy(client *fasthttp.Client, proxyConfig *schemas.ProxyConfig, l
client.Dial = dialFunc
}

// Configure custom CA certificate if provided
if proxyConfig.CACertPEM != "" {
tlsConfig, err := createTLSConfigWithCA(proxyConfig.CACertPEM)
if err != nil {
logger.Warn(fmt.Sprintf("Failed to configure custom CA certificate: %v", err))
} else {
client.TLSConfig = tlsConfig
}
}

return client
}

// createTLSConfigWithCA creates a TLS configuration with a custom CA certificate
// appended to the system root CA pool.
func createTLSConfigWithCA(caCertPEM string) (*tls.Config, error) {
// Get the system root CA pool
rootCAs, err := x509.SystemCertPool()
if err != nil {
// If we can't get system certs, create a new pool
rootCAs = x509.NewCertPool()
}

// Append the custom CA certificate
if !rootCAs.AppendCertsFromPEM([]byte(caCertPEM)) {
return nil, fmt.Errorf("failed to parse CA certificate PEM")
}

return &tls.Config{
RootCAs: rootCAs,
MinVersion: tls.VersionTLS12,
}, nil
}

// hopByHopHeaders are HTTP/1.1 headers that must not be forwarded by proxies.
var hopByHopHeaders = map[string]bool{
"connection": true,
Expand Down
9 changes: 5 additions & 4 deletions core/schemas/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,11 @@ const (

// ProxyConfig holds the configuration for proxy settings.
type ProxyConfig struct {
Type ProxyType `json:"type"` // Type of proxy to use
URL string `json:"url"` // URL of the proxy server
Username string `json:"username"` // Username for proxy authentication
Password string `json:"password"` // Password for proxy authentication
Type ProxyType `json:"type"` // Type of proxy to use
URL string `json:"url"` // URL of the proxy server
Username string `json:"username"` // Username for proxy authentication
Password string `json:"password"` // Password for proxy authentication
CACertPEM string `json:"ca_cert_pem"` // PEM-encoded CA certificate to trust for TLS connections through the proxy
}

// AllowedRequests controls which operations are permitted.
Expand Down
32 changes: 19 additions & 13 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"dark": "/media/bifrost-logo-dark.png",
"light": "/media/bifrost-logo.png"
},
"banner": {
"content": "Try Bifrost Enterprise free for 14 days. [Explore now](https://www.getmaxim.ai/bifrost/enterprise)",
"dismissible": true
},
"theme": "mint",
"colors": {
"primary": "#0C3B43",
Expand All @@ -21,19 +25,21 @@
"name": "Dashboard",
"url": "https://www.getbifrost.ai"
},
"anchors": [
{
"name": "Community",
"icon": "discord",
"url": "https://getmax.im/bifrost-discord"
},
{
"name": "Blog",
"icon": "newspaper",
"url": "https://getmaxim.ai/blog"
}
],
"navigation": {
"global": {
"anchors": [
{
"anchor": "Discord",
"icon": "discord",
"href": "https://getmax.im/bifrost-discord"
},
{
"anchor": "Try Enterprise",
"icon": "building",
"href": "https://www.getmaxim.ai/bifrost/enterprise"
}
]
},
"tabs": [
{
"tab": "Documentation",
Expand Down Expand Up @@ -380,4 +386,4 @@
"linkedin": "https://linkedin.com/company/maxim-ai"
}
}
}
}
4 changes: 4 additions & 0 deletions transports/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1842,6 +1842,10 @@
"password": {
"type": "string",
"description": "Password for proxy authentication"
},
"ca_cert_pem": {
"type": "string",
"description": "PEM-encoded CA certificate to trust for TLS connections through the proxy (for SSL-intercepting proxies)"
}
},
"required": [
Expand Down
25 changes: 25 additions & 0 deletions ui/app/workspace/config/views/proxyView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,31 @@ export default function ProxyView() {
)}
/>

{/* CA Certificate */}
<FormField
control={form.control}
name="ca_cert_pem"
render={({ field }) => (
<FormItem>
<FormLabel>CA Certificate (PEM) (Optional)</FormLabel>
<FormControl>
<Textarea
placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
className="font-mono text-xs"
rows={6}
disabled={!watchedEnabled}
{...field}
value={field.value || ""}
/>
</FormControl>
<FormDescription>
PEM-encoded CA certificate to trust for TLS connections through SSL-intercepting proxies.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>

{/* Skip TLS Verify */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
Expand Down
28 changes: 27 additions & 1 deletion ui/app/workspace/providers/fragments/proxyFormFragment.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { getErrorMessage, setProviderFormDirtyState, useAppDispatch } from "@/lib/store";
import { useUpdateProviderMutation } from "@/lib/store/apis/providersApi";
import { ModelProvider } from "@/lib/types/config";
Expand Down Expand Up @@ -33,6 +34,7 @@ export function ProxyFormFragment({ provider }: ProxyFormFragmentProps) {
url: provider.proxy_config?.url || "",
username: provider.proxy_config?.username || "",
password: provider.proxy_config?.password || "",
ca_cert_pem: provider.proxy_config?.ca_cert_pem || "",
},
},
});
Expand All @@ -48,6 +50,7 @@ export function ProxyFormFragment({ provider }: ProxyFormFragmentProps) {
url: provider.proxy_config?.url || "",
username: provider.proxy_config?.username || "",
password: provider.proxy_config?.password || "",
ca_cert_pem: provider.proxy_config?.ca_cert_pem || "",
},
});
}, [form, provider.name, provider.proxy_config]);
Expand All @@ -62,6 +65,7 @@ export function ProxyFormFragment({ provider }: ProxyFormFragmentProps) {
url: data.proxy_config?.url || undefined,
username: data.proxy_config?.username || undefined,
password: data.proxy_config?.password || undefined,
ca_cert_pem: data.proxy_config?.ca_cert_pem || undefined,
},
})
.unwrap()
Expand Down Expand Up @@ -152,6 +156,28 @@ export function ProxyFormFragment({ provider }: ProxyFormFragmentProps) {
)}
/>
</div>
<FormField
control={form.control}
name="proxy_config.ca_cert_pem"
render={({ field }) => (
<FormItem>
<FormLabel>CA Certificate (PEM) (Optional)</FormLabel>
<FormControl>
<Textarea
placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
className="font-mono text-xs"
rows={6}
{...field}
value={field.value || ""}
/>
</FormControl>
<FormDescription>
PEM-encoded CA certificate to trust for TLS connections through SSL-intercepting proxies
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions ui/lib/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export interface ProxyConfig {
url?: string;
username?: string;
password?: string;
ca_cert_pem?: string;
}

// Request types matching Go's schemas.RequestType
Expand Down Expand Up @@ -272,6 +273,7 @@ export interface GlobalProxyConfig {
url: string;
username?: string;
password?: string;
ca_cert_pem?: string;
no_proxy?: string;
timeout?: number;
skip_tls_verify?: boolean;
Expand Down
3 changes: 3 additions & 0 deletions ui/lib/types/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ export const proxyConfigSchema = z
url: z.url("Must be a valid URL"),
username: z.string().optional(),
password: z.string().optional(),
ca_cert_pem: z.string().optional(),
})
.refine((data) => !(data.type === "http" || data.type === "socks5") || (data.url && data.url.trim().length > 0), {
message: "Proxy URL is required when using HTTP or SOCKS5 proxy",
Expand Down Expand Up @@ -282,6 +283,7 @@ export const proxyFormConfigSchema = z
url: z.string().optional(),
username: z.string().optional(),
password: z.string().optional(),
ca_cert_pem: z.string().optional(),
})
.refine(
(data) => {
Expand Down Expand Up @@ -629,6 +631,7 @@ export const globalProxyConfigSchema = z
url: z.string(),
username: z.string().optional(),
password: z.string().optional(),
ca_cert_pem: z.string().optional(),
no_proxy: z.string().optional(),
timeout: z.number().min(0).optional(),
skip_tls_verify: z.boolean().optional(),
Expand Down