mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
fix: Proxy does not support formats from proxy providers #307
This commit is contained in:
@@ -32,7 +32,7 @@ describe('Proxy Form Schema', () => {
|
||||
if (!result.success) {
|
||||
expect(
|
||||
result.error.issues.some((i) =>
|
||||
i.message?.includes('http:// or socks5://')
|
||||
i.message?.includes('Invalid format')
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ describe('Proxy Form Schema', () => {
|
||||
if (!result.success) {
|
||||
expect(
|
||||
result.error.issues.some((i) =>
|
||||
i.message?.includes('Invalid URL format')
|
||||
i.message?.includes('Invalid format')
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
@@ -170,4 +170,27 @@ describe('Proxy Form Schema', () => {
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('url field - non-standard format (host:port:user:pass)', () => {
|
||||
it('accepts non-standard format with auth', () => {
|
||||
const result = proxyFormSchema.safeParse({
|
||||
url: 'socks5://server.nodeprovider.com:8080:nodeprovider_a1234_alias_com-country-us-region-california-sid-b123123123-filter-medium:passwordhere',
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts simple non-standard format', () => {
|
||||
const result = proxyFormSchema.safeParse({
|
||||
url: 'socks5://proxy.example.com:1080:myuser:mypassword',
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects non-standard format without password', () => {
|
||||
const result = proxyFormSchema.safeParse({
|
||||
url: 'socks5://proxy.example.com:1080:myuser',
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ export const getColumns = (t: (key: string) => string): ColumnDef<Proxy>[] => [
|
||||
cell: ({ row }) => {
|
||||
return <LongText>{row.original.url}</LongText>
|
||||
},
|
||||
meta: { className: 'w-60' },
|
||||
meta: { className: 'max-w-60' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
|
||||
@@ -1,63 +1,175 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// Parse a proxy URL into components. Supports two formats:
|
||||
// Standard: socks5://[user:pass@]host:port
|
||||
// Non-standard: socks5://host:port:user:pass (some proxy providers)
|
||||
function parseProxyUrl(value: string): {
|
||||
scheme: string
|
||||
host: string
|
||||
port: number
|
||||
username?: string
|
||||
password?: string
|
||||
} | null {
|
||||
// Strip scheme
|
||||
let stripped: string
|
||||
let scheme: string
|
||||
const lower = value.toLowerCase()
|
||||
if (lower.startsWith('socks5://')) {
|
||||
scheme = 'socks5'
|
||||
stripped = value.slice('socks5://'.length)
|
||||
} else if (lower.startsWith('http://')) {
|
||||
scheme = 'http'
|
||||
stripped = value.slice('http://'.length)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!stripped) return null
|
||||
|
||||
// Standard format: user:pass@host:port
|
||||
const atIdx = stripped.lastIndexOf('@')
|
||||
if (atIdx >= 0) {
|
||||
const userinfo = stripped.slice(0, atIdx)
|
||||
const hostport = stripped.slice(atIdx + 1)
|
||||
|
||||
// Parse userinfo
|
||||
let username: string | undefined
|
||||
let password: string | undefined
|
||||
if (userinfo) {
|
||||
const colonIdx = userinfo.indexOf(':')
|
||||
if (colonIdx >= 0) {
|
||||
username = userinfo.slice(0, colonIdx)
|
||||
password = userinfo.slice(colonIdx + 1)
|
||||
} else {
|
||||
username = userinfo
|
||||
}
|
||||
}
|
||||
|
||||
// Parse host:port
|
||||
const { host, port } = splitHostPort(hostport)
|
||||
if (!host || !port) return null
|
||||
|
||||
return { scheme, host, port, username, password }
|
||||
}
|
||||
|
||||
// Non-standard format: host:port[:user[:pass]]
|
||||
const parts = stripped.split(':')
|
||||
if (parts.length === 1) {
|
||||
// host only, default port to 1080
|
||||
const host = parts[0]
|
||||
if (!host) return null
|
||||
return { scheme, host, port: 1080 }
|
||||
}
|
||||
if (parts.length === 2) {
|
||||
// host:port, no auth
|
||||
const host = parts[0]
|
||||
const port = parseInt(parts[1], 10)
|
||||
if (!host || isNaN(port)) return null
|
||||
return { scheme, host, port }
|
||||
}
|
||||
if (parts.length >= 4) {
|
||||
// host:port:username:password (and possibly more colons in user/pass)
|
||||
// Last part = password, second-to-last = username, rest = host:port
|
||||
const password = parts[parts.length - 1]
|
||||
const username = parts[parts.length - 2]
|
||||
const hostport = parts.slice(0, parts.length - 2).join(':')
|
||||
const { host, port } = splitHostPort(hostport)
|
||||
if (!host || !port || !username || !password) return null
|
||||
return { scheme, host, port, username, password }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function splitHostPort(hostport: string): { host: string; port: number | null } {
|
||||
if (!hostport) return { host: '', port: null }
|
||||
|
||||
// IPv6: [::1]:1080 or [::1]
|
||||
if (hostport.startsWith('[')) {
|
||||
const close = hostport.indexOf(']')
|
||||
if (close < 0) return { host: '', port: null }
|
||||
const host = hostport.slice(1, close)
|
||||
const after = hostport.slice(close + 1)
|
||||
if (!after.startsWith(':')) {
|
||||
// No port specified, default to 1080
|
||||
return { host, port: 1080 }
|
||||
}
|
||||
const port = parseInt(after.slice(1), 10)
|
||||
return { host, port: isNaN(port) ? null : port }
|
||||
}
|
||||
|
||||
const lastColon = hostport.lastIndexOf(':')
|
||||
if (lastColon < 0) {
|
||||
// No port specified, default to 1080
|
||||
return { host: hostport, port: 1080 }
|
||||
}
|
||||
const host = hostport.slice(0, lastColon)
|
||||
const port = parseInt(hostport.slice(lastColon + 1), 10)
|
||||
return { host, port: isNaN(port) ? null : port }
|
||||
}
|
||||
|
||||
export const proxyFormSchema = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.min(1, 'Proxy address cannot be empty')
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.length === 0) {
|
||||
return
|
||||
}
|
||||
if (value.length === 0) return
|
||||
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(value)
|
||||
} catch (_e) {
|
||||
// Try our custom parser first (handles both standard and non-standard)
|
||||
const parsed = parseProxyUrl(value)
|
||||
|
||||
if (!parsed) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid URL format',
|
||||
message: 'Invalid format. Expected socks5://[user:pass@]host:port or socks5://host:port:user:pass',
|
||||
path: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (url.protocol !== 'socks5:' && url.protocol !== 'http:') {
|
||||
if (parsed.scheme !== 'socks5' && parsed.scheme !== 'http') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'URL must start with http:// or socks5://',
|
||||
path: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9\-\.]+$/.test(url.hostname)) {
|
||||
if (!/^[a-zA-Z0-9\-\.]+$/.test(parsed.host)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Hostname contains invalid characters',
|
||||
path: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const port = parseInt(url.port || '1080')
|
||||
if (port <= 0 || port > 65535) {
|
||||
if (parsed.port <= 0 || parsed.port > 65535) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Port must be between 1-65535',
|
||||
path: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (url.username && !url.password) {
|
||||
if (parsed.username && !parsed.password) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Password cannot be empty when username is provided',
|
||||
path: [],
|
||||
})
|
||||
} else if (url.password && url.password.length < 8) {
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.password && parsed.password.length < 8) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Password must be at least 8 characters',
|
||||
path: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user