Skip to content

Commit

Permalink
feat: add http auth and custom domain
Browse files Browse the repository at this point in the history
  • Loading branch information
VaalaCat committed Dec 22, 2024
1 parent ca74263 commit e116a5a
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 61 deletions.
26 changes: 14 additions & 12 deletions www/components/base/list-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
};
Expand Down
37 changes: 34 additions & 3 deletions www/components/base/visit-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,47 @@ 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 (
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-start p-2 text-sm font-mono text-nowrap">
<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">
Expand Down
102 changes: 58 additions & 44 deletions www/components/frpc/proxy_form.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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({
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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')}
Expand All @@ -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 })
Expand Down Expand Up @@ -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')}
Expand All @@ -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 })
Expand Down Expand Up @@ -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')}
Expand All @@ -472,21 +461,22 @@ 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: {
localIP: defaultConfig?.localIP,
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 })
Expand All @@ -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) {
Expand All @@ -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} />
<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="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>}
<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"} />
<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')}
Expand Down
8 changes: 7 additions & 1 deletion www/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion www/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "创建",
Expand Down
2 changes: 2 additions & 0 deletions www/lib/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down

0 comments on commit e116a5a

Please sign in to comment.