feat: add http auth and custom domain

This commit is contained in:
VaalaCat
2024-12-22 12:37:09 +00:00
parent ca74263e74
commit e116a5ad17
6 changed files with 123 additions and 62 deletions

View File

@@ -51,18 +51,20 @@ const StringListInput: React.FC<StringListInputProps> = ({ value, onChange, plac
{t('input.list.add')}
</Button>
</div>
<div className="flex flex-wrap gap-2">
{value && value.map((item, index) => (
<Badge key={index} className='flex flex-row items-center justify-start'>{item}
<div
onClick={() => handleRemove(item)}
className="ml-1 h-4 w-4 text-center rounded-full hover:text-red-500 cursor-pointer"
>
×
</div>
</Badge>
))}
</div>
{
value && <div className="flex flex-wrap gap-2">
{value.map((item, index) => (
<Badge key={index} className='flex flex-row items-center justify-start'>{item}
<div
onClick={() => handleRemove(item)}
className="ml-1 h-4 w-4 text-center rounded-full hover:text-red-500 cursor-pointer"
>
×
</div>
</Badge>
))}
</div>
}
</div>
);
};

View File

@@ -20,7 +20,33 @@ export function VisitPreview({ server, typedProxyConfig }: { server: Server; typ
if (httpProxyConfig.locations.length == 1) {
return httpProxyConfig.locations[0];
}
return `[${httpProxyConfig.locations.join(",")}]`;
return `[${httpProxyConfig.locations.join(", ")}]`;
}
function getServerHost(httpProxyConfig: HTTPProxyConfig) {
let allHosts = []
if (httpProxyConfig.subdomain) {
allHosts.push(`${httpProxyConfig.subdomain}.${serverCfg.subDomainHost}`);
}
allHosts.push(...(httpProxyConfig.customDomains || []));
if (allHosts.length == 0) {
return serverAddress;
}
if (allHosts.length == 1) {
return allHosts[0];
}
return `[${allHosts.join(", ")}]`;
}
function getServerAuth(httpProxyConfig: HTTPProxyConfig) {
if (!httpProxyConfig.httpUser || !httpProxyConfig.httpPassword) {
return "";
}
return `${httpProxyConfig.httpUser}:${httpProxyConfig.httpPassword}@`
}
return (
@@ -28,8 +54,13 @@ export function VisitPreview({ server, typedProxyConfig }: { server: Server; typ
<div className="flex items-center mb-2 sm:mb-0">
<Globe className="w-4 h-4 text-blue-500 mr-2 flex-shrink-0" />
<span className="text-nowrap">{typedProxyConfig.type == "http" ? "http://" : ""}{
typedProxyConfig.type == "http" ? `${(typedProxyConfig as HTTPProxyConfig).subdomain}.${serverCfg.subDomainHost}` : serverAddress}:{
serverPort}{typedProxyConfig.type == "http" ? getServerPath(typedProxyConfig as HTTPProxyConfig) : ""}</span>
typedProxyConfig.type == "http" ? (
getServerAuth(typedProxyConfig as HTTPProxyConfig) + getServerHost(typedProxyConfig as HTTPProxyConfig)
) : serverAddress
}:{serverPort || "?"}{
typedProxyConfig.type == "http" ?
getServerPath(typedProxyConfig as HTTPProxyConfig) : ""
}</span>
</div>
<ArrowRight className="hidden sm:block w-4 h-4 text-gray-400 mx-2 flex-shrink-0" />
<div className="flex items-center mb-2 sm:mb-0">

View File

@@ -1,18 +1,19 @@
import { HTTPProxyConfig, TCPProxyConfig, TypedProxyConfig, UDPProxyConfig, STCPProxyConfig } from '@/types/proxy'
import * as z from 'zod'
import React from 'react'
import { ZodPortSchema, ZodStringSchema } from '@/lib/consts'
import { ZodPortSchema, ZodStringOptionalSchema, ZodStringSchema } from '@/lib/consts'
import { useEffect, useState } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { Control, useForm } from 'react-hook-form'
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 { Label } from '@/components/ui/label'
import { YesIcon } from '@/components/ui/icon'
import { useTranslation } from 'react-i18next'
import { useQuery } from '@tanstack/react-query'
import { getServer } from '@/api/server'
import { Switch } from "@/components/ui/switch"
import { VisitPreview } from '../base/visit-preview'
import StringListInput from '../base/list-input'
@@ -31,8 +32,11 @@ export const UDPConfigSchema = z.object({
export const HTTPConfigSchema = z.object({
localPort: ZodPortSchema,
localIP: ZodStringSchema.default('127.0.0.1'),
subdomain: ZodStringSchema,
subdomain: ZodStringOptionalSchema,
locations: z.array(ZodStringSchema).optional(),
customDomains: z.array(ZodStringSchema).optional(),
httpUser: ZodStringOptionalSchema,
httpPassword: ZodStringOptionalSchema,
})
export const STCPConfigSchema = z.object({
@@ -273,11 +277,6 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
}
})
useEffect(() => {
setTCPConfig(undefined)
form.reset({})
}, [])
const onSubmit = async (values: z.infer<typeof TCPConfigSchema>) => {
handleSave()
setTCPConfig({ type: 'tcp', ...values, name: proxyName })
@@ -319,13 +318,13 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
<div className="flex items-center space-x-2 flex-col justify-start w-full">
<Label className="text-sm font-medium text-start w-full">{t('proxy.form.access_method')}</Label>
<div className='w-full justify-start overflow-x-scroll'>
<VisitPreview server={server?.server} typedProxyConfig={defaultConfig} />
<VisitPreview server={server?.server} typedProxyConfig={defaultConfig} />
</div>
</div>
)}
<PortField name="localPort" control={form.control} label={t('proxy.form.local_port')} />
<HostField name="localIP" control={form.control} label={t('proxy.form.local_ip')} />
<PortField name="remotePort" control={form.control} label={t('proxy.form.remote_port')} placeholder='4321' />
<PortField name="localPort" control={form.control} label={t('proxy.form.local_port') + "*"} />
<HostField name="localIP" control={form.control} label={t('proxy.form.local_ip') + "*"} />
<PortField name="remotePort" control={form.control} label={t('proxy.form.remote_port') + "*"} placeholder='4321' />
<Button type="submit" disabled={isSaveDisabled} variant={'outline'} className='w-full'>
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
{t('proxy.form.save_changes')}
@@ -348,11 +347,6 @@ export const STCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
}
})
useEffect(() => {
setSTCPConfig(undefined)
form.reset({})
}, [])
const onSubmit = async (values: z.infer<typeof STCPConfigSchema>) => {
handleSave()
setSTCPConfig({ type: 'stcp', ...values, name: proxyName })
@@ -382,9 +376,9 @@ export const STCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 px-0.5">
<PortField name="localPort" control={form.control} label={t('proxy.form.local_port')} />
<HostField name="localIP" control={form.control} label={t('proxy.form.local_ip')} />
<SecretStringField name="secretKey" control={form.control} label={t('proxy.form.secret_key')} />
<PortField name="localPort" control={form.control} label={t('proxy.form.local_port') + "*"} />
<HostField name="localIP" control={form.control} label={t('proxy.form.local_ip') + "*"} />
<SecretStringField name="secretKey" control={form.control} label={t('proxy.form.secret_key') + "*"} />
<Button type="submit" disabled={isSaveDisabled} variant={'outline'} className='w-full'>
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
{t('proxy.form.save_changes')}
@@ -407,11 +401,6 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
}
})
useEffect(() => {
setUDPConfig(undefined)
form.reset({})
}, [])
const onSubmit = async (values: z.infer<typeof UDPConfigSchema>) => {
handleSave()
setUDPConfig({ type: 'udp', ...values, name: proxyName })
@@ -450,15 +439,15 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 px-0.5">
{server?.server?.ip && defaultConfig.remotePort && defaultConfig.localIP && defaultConfig.localPort && enablePreview && (
<div className="flex items-center space-x-2 flex-col justify-start w-full">
<Label className="text-sm font-medium text-start w-full">{t('proxy.form.access_method')}</Label>
<div className='w-full justify-start overflow-x-scroll'>
<VisitPreview server={server?.server} typedProxyConfig={defaultConfig} />
<Label className="text-sm font-medium text-start w-full">{t('proxy.form.access_method')}</Label>
<div className='w-full justify-start overflow-x-scroll'>
<VisitPreview server={server?.server} typedProxyConfig={defaultConfig} />
</div>
</div>
</div>
)}
<PortField name="localPort" control={form.control} label={t('proxy.form.local_port')} />
<HostField name="localIP" control={form.control} label={t('proxy.form.local_ip')} />
<PortField name="remotePort" control={form.control} label={t('proxy.form.remote_port')} />
<PortField name="localPort" control={form.control} label={t('proxy.form.local_port') + "*"} />
<HostField name="localIP" control={form.control} label={t('proxy.form.local_ip') + "*"} />
<PortField name="remotePort" control={form.control} label={t('proxy.form.remote_port') + "*"} />
<Button type="submit" disabled={isSaveDisabled} variant={'outline'} className='w-full'>
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
{t('proxy.form.save_changes')}
@@ -472,6 +461,9 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
const defaultConfig = defaultProxyConfig as HTTPProxyConfig
const [_, setHTTPConfig] = useState<HTTPProxyConfig | undefined>()
const [timeoutID, setTimeoutID] = useState<NodeJS.Timeout | undefined>()
const [moreSettings, setMoreSettings] = useState(false)
const [useAuth, setUseAuth] = useState(false)
const form = useForm<z.infer<typeof HTTPConfigSchema>>({
resolver: zodResolver(HTTPConfigSchema),
defaultValues: {
@@ -479,14 +471,12 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
localPort: defaultConfig?.localPort,
subdomain: defaultConfig?.subdomain,
locations: defaultConfig?.locations,
customDomains: defaultConfig?.customDomains,
httpPassword: defaultConfig?.httpPassword,
httpUser: defaultConfig?.httpUser
}
})
useEffect(() => {
setHTTPConfig(undefined)
form.reset({})
}, [])
const onSubmit = async (values: z.infer<typeof HTTPConfigSchema>) => {
handleSave()
setHTTPConfig({ ...values, type: 'http', name: proxyName })
@@ -501,6 +491,12 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
const [isSaveDisabled, setSaveDisabled] = useState(false)
useEffect(() => {
if (defaultConfig?.httpPassword || defaultConfig?.httpUser) {
setUseAuth(true)
}
}, [defaultConfig?.httpPassword, defaultConfig?.httpUser])
const handleSave = () => {
setSaveDisabled(true)
if (timeoutID) {
@@ -527,15 +523,33 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
defaultConfig.localIP && defaultConfig.localPort &&
defaultConfig.subdomain
&& enablePreview && <div className="flex items-center space-x-2 flex-col justify-start w-full">
<Label className="text-sm font-medium text-start w-full">{t('proxy.form.access_method')}</Label>
<div className='w-full justify-start overflow-x-scroll'>
<VisitPreview server={server?.server} typedProxyConfig={defaultConfig} />
</div>
</div>}
<PortField name="localPort" control={form.control} label={t('proxy.form.local_port')} />
<HostField name="localIP" control={form.control} label={t('proxy.form.local_ip')} />
<Label className="text-sm font-medium text-start w-full">{t('proxy.form.access_method')}</Label>
<div className='w-full justify-start overflow-x-scroll'>
<VisitPreview server={server?.server} typedProxyConfig={defaultConfig} />
</div>
</div>}
<PortField name="localPort" control={form.control} label={t('proxy.form.local_port') + "*"} />
<HostField name="localIP" control={form.control} label={t('proxy.form.local_ip') + "*"} />
<StringField name="subdomain" control={form.control} label={t('proxy.form.subdomain')} placeholder={"your_sub_domain"} />
<StringArrayField name="locations" control={form.control} label={t('proxy.form.route')} placeholder={"/path"} />
<StringArrayField name="customDomains" control={form.control} label={t('proxy.form.custom_domains')} placeholder={"your.example.com"} />
<FormDescription>
{t('proxy.form.domain_description')}
</FormDescription>
<div className="flex items-center space-x-2 justify-between">
<Label htmlFor="more-settings">{t('proxy.form.more_settings')}</Label>
<Switch id="more-settings" checked={moreSettings} onCheckedChange={setMoreSettings} />
</div>
{moreSettings && <div className='p-4 space-y-4 border rounded-md'>
<StringArrayField name="locations" control={form.control} label={t('proxy.form.route')} placeholder={"/path"} />
<div className="flex items-center space-x-2 justify-between">
<Label htmlFor="enable-http-auth">{t('proxy.form.enable_http_auth')}</Label>
<Switch id="enable-http-auth" checked={useAuth} onCheckedChange={setUseAuth} />
</div>
{useAuth && <div className='p-4 space-y-4 border rounded-md'>
<StringField name="httpUser" control={form.control} label={t('proxy.form.username')} placeholder={"username"} />
<StringField name="httpPassword" control={form.control} label={t('proxy.form.password')} placeholder={"password"} />
</div>}
</div>}
<Button type="submit" disabled={isSaveDisabled} variant={'outline'} className='w-full'>
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
{t('proxy.form.save_changes')}

View File

@@ -349,7 +349,13 @@
"ip_placeholder": "IP Address (eg. 127.0.0.1)",
"subdomain_placeholder": "Input Subdomain",
"secret_placeholder": "Input Secret Key",
"route": "Route"
"route": "Route",
"custom_domains": "Custom Domains",
"more_settings": "More Settings",
"domain_description": "Subdomain and custom domain can be configured at the same time, but at least one of them must be not empty. If frps is configured with domain suffix, the custom domain cannot be a subdomain or wildcard domain of the domain suffix.",
"enable_http_auth": "Enable HTTP Authentication",
"username": "Username",
"password": "Password"
},
"config": {
"create": "Create",

View File

@@ -349,7 +349,13 @@
"ip_placeholder": "请输入IP地址 (例如: 127.0.0.1)",
"subdomain_placeholder": "请输入子域名",
"secret_placeholder": "请输入密钥",
"route": "路由"
"route": "路由",
"custom_domains": "自定义域名",
"more_settings": "更多设置",
"domain_description": "「子域名」和「自定义域名」可以同时配置,但二者至少有一个不为空。如果 frps 配置了「域名后缀」,则「自定义域名」中不能是属于「域名后缀」的「子域名」或者「泛域名」",
"enable_http_auth": "启用 HTTP 认证",
"username": "用户名",
"password": "密码"
},
"config": {
"create": "创建",

View File

@@ -16,6 +16,8 @@ export const ZodIPSchema = z.string({ required_error: 'validation.required' })
.regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, { message: 'validation.ipAddress' })
export const ZodStringSchema = z.string({ required_error: 'validation.required' })
.min(1, { message: 'validation.required' })
export const ZodStringOptionalSchema = z.string().optional()
export const ZodEmailSchema = z.string({ required_error: 'validation.required' })
.min(1, { message: 'validation.required' })
.email({ message: 'auth.email.invalid' })