mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
149 lines
4.9 KiB
TypeScript
149 lines
4.9 KiB
TypeScript
import 'server-only'
|
|
|
|
import { isIP } from 'node:net'
|
|
|
|
export type CampaignInviteOriginInput = {
|
|
configuredURL?: string
|
|
environment?: string
|
|
requestOrigin?: string | null
|
|
forwardedHost?: string | null
|
|
forwardedProto?: string | null
|
|
allowLocalTLS?: boolean
|
|
}
|
|
|
|
const LOCAL_AUTHORITY_PATTERN = /^(localhost|127\.0\.0\.1|\[::1\])(?::([0-9]{1,5}))?$/i
|
|
const DNS_HOSTNAME_PATTERN =
|
|
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i
|
|
|
|
const parsePort = (rawPort: string | undefined): boolean => {
|
|
if (rawPort === undefined) return true
|
|
const port = Number(rawPort)
|
|
return Number.isInteger(port) && port >= 1 && port <= 65535
|
|
}
|
|
|
|
const parseHTTPOrigin = (rawURL: string, source: string): URL => {
|
|
if (rawURL !== rawURL.trim()) {
|
|
throw new Error(`${source} precisa conter somente uma origem exata.`)
|
|
}
|
|
|
|
let url: URL
|
|
try {
|
|
url = new URL(rawURL)
|
|
} catch {
|
|
throw new Error(`${source} precisa conter uma URL válida e absoluta.`)
|
|
}
|
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
throw new Error(`${source} precisa usar HTTP ou HTTPS.`)
|
|
}
|
|
if (url.username || url.password) {
|
|
throw new Error(`${source} não pode conter credenciais.`)
|
|
}
|
|
if (url.pathname !== '/' || url.search || url.hash) {
|
|
throw new Error(
|
|
`${source} precisa conter somente a origem, sem caminho, consulta ou fragmento.`,
|
|
)
|
|
}
|
|
if (url.port && !parsePort(url.port)) {
|
|
throw new Error(`${source} precisa usar uma porta válida.`)
|
|
}
|
|
return url
|
|
}
|
|
|
|
const parseRawLocalAuthority = (rawAuthority: string): RegExpMatchArray | null => {
|
|
if (rawAuthority !== rawAuthority.trim()) return null
|
|
const match = rawAuthority.match(LOCAL_AUTHORITY_PATTERN)
|
|
return match && parsePort(match[2]) ? match : null
|
|
}
|
|
|
|
const parseLocalRequestOrigin = (
|
|
value: string | null | undefined,
|
|
allowLocalTLS: boolean,
|
|
): string | undefined => {
|
|
if (!value || value !== value.trim() || /[\u0000-\u001f\u007f,]/.test(value)) return undefined
|
|
|
|
const rawMatch = value.match(
|
|
/^(https?):\/\/(localhost|127\.0\.0\.1|\[::1\])(?::([0-9]{1,5}))?\/?$/i,
|
|
)
|
|
if (!rawMatch || !parsePort(rawMatch[3])) return undefined
|
|
|
|
const url = parseHTTPOrigin(value, 'A origem do convite')
|
|
if (url.protocol === 'https:' && !allowLocalTLS) url.protocol = 'http:'
|
|
return url.origin
|
|
}
|
|
|
|
const parseLocalForwardedOrigin = ({
|
|
forwardedHost,
|
|
forwardedProto,
|
|
allowLocalTLS,
|
|
}: Pick<CampaignInviteOriginInput, 'forwardedHost' | 'forwardedProto' | 'allowLocalTLS'>):
|
|
| string
|
|
| undefined => {
|
|
if (!forwardedHost || !parseRawLocalAuthority(forwardedHost)) return undefined
|
|
if (
|
|
forwardedProto &&
|
|
(forwardedProto !== forwardedProto.trim() ||
|
|
/[\u0000-\u001f\u007f,]/.test(forwardedProto) ||
|
|
!/^(?:http|https)$/i.test(forwardedProto))
|
|
) {
|
|
return undefined
|
|
}
|
|
|
|
const protocol = forwardedProto?.toLowerCase() || 'http'
|
|
return parseLocalRequestOrigin(`${protocol}://${forwardedHost}`, Boolean(allowLocalTLS))
|
|
}
|
|
|
|
const requireProductionDNSOrigin = (configuredURL: string): string => {
|
|
const url = parseHTTPOrigin(configuredURL, 'NEXT_PUBLIC_SITE_URL')
|
|
if (url.protocol !== 'https:') {
|
|
throw new Error('NEXT_PUBLIC_SITE_URL precisa usar HTTPS em produção.')
|
|
}
|
|
|
|
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
const forbiddenHostname =
|
|
hostname === 'localhost' ||
|
|
hostname.startsWith('localhost.') ||
|
|
/^(?:[0-9]{1,3}\.){4}/.test(hostname) ||
|
|
hostname.endsWith('.localhost') ||
|
|
hostname.endsWith('.local') ||
|
|
hostname.endsWith('.localdomain') ||
|
|
hostname.endsWith('.internal')
|
|
if (isIP(hostname) !== 0 || forbiddenHostname || !DNS_HOSTNAME_PATTERN.test(hostname)) {
|
|
throw new Error('NEXT_PUBLIC_SITE_URL precisa apontar para um nome DNS público em produção.')
|
|
}
|
|
return url.origin
|
|
}
|
|
|
|
export const getCampaignInviteBaseURL = (input: CampaignInviteOriginInput = {}): string => {
|
|
const environment = input.environment ?? process.env.NODE_ENV
|
|
const configuredURL = Object.hasOwn(input, 'configuredURL')
|
|
? input.configuredURL
|
|
: process.env.NEXT_PUBLIC_SITE_URL
|
|
|
|
if (environment === 'production') {
|
|
if (!configuredURL) {
|
|
throw new Error('NEXT_PUBLIC_SITE_URL precisa ser configurada em produção.')
|
|
}
|
|
return requireProductionDNSOrigin(configuredURL)
|
|
}
|
|
|
|
if (environment === 'development' || environment === 'test') {
|
|
const requestURL =
|
|
parseLocalRequestOrigin(input.requestOrigin, Boolean(input.allowLocalTLS)) ??
|
|
parseLocalForwardedOrigin(input)
|
|
if (requestURL) return requestURL
|
|
}
|
|
|
|
if (configuredURL) {
|
|
const url = parseHTTPOrigin(configuredURL, 'NEXT_PUBLIC_SITE_URL')
|
|
if (
|
|
(url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]') &&
|
|
url.protocol === 'https:' &&
|
|
!input.allowLocalTLS
|
|
) {
|
|
url.protocol = 'http:'
|
|
}
|
|
return url.origin
|
|
}
|
|
|
|
throw new Error('Não foi possível determinar uma origem segura para o convite.')
|
|
}
|