gengxin
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
export function dependency(name) {
|
||||
const root = process.env.TANG_COS_TOOLS_DIR
|
||||
return createRequire(root ? path.join(path.resolve(root), 'package.json') : import.meta.url)(name)
|
||||
}
|
||||
|
||||
// Fail closed rather than evaluating PHP or guessing deployment settings.
|
||||
export function configuredDatabase(serverDirectory, environment = process.env) {
|
||||
const filename = path.join(serverDirectory, 'config/database.php')
|
||||
const source = fs.readFileSync(filename, 'utf8')
|
||||
if (fs.existsSync(path.join(serverDirectory, '.env'))) {
|
||||
throw new Error('SERVER_ENV_REQUIRES_NATIVE_RUNTIME')
|
||||
}
|
||||
function value(key) {
|
||||
const envKey = `DATABASE_${key.toUpperCase()}`
|
||||
if (environment[envKey] !== undefined) return environment[envKey]
|
||||
if (environment[`PHP_${envKey}`] !== undefined) return environment[`PHP_${envKey}`]
|
||||
const match = source.match(new RegExp(`env\\('database\\.${key}',\\s*'((?:\\\\.|[^'\\\\])*)'\\)`))
|
||||
if (!match) throw new Error('UNSUPPORTED_DATABASE_CONFIG')
|
||||
return match[1].replace(/\\([\\'])/g, '$1')
|
||||
}
|
||||
const prefix = value('prefix')
|
||||
const port = Number(value('hostport'))
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(prefix) || !Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error('INVALID_DATABASE_CONFIG')
|
||||
}
|
||||
const options = {
|
||||
host: value('hostname'), port, database: value('database'), user: value('username'), password: value('password'),
|
||||
connectTimeout: 8000, multipleStatements: false,
|
||||
ssl: { rejectUnauthorized: true, verifyIdentity: true },
|
||||
}
|
||||
// Supply only a trusted CA obtained from the server administrator; never fetch and trust a peer certificate.
|
||||
if (environment.TANG_DB_CA_FILE) options.ssl.ca = fs.readFileSync(environment.TANG_DB_CA_FILE, 'utf8')
|
||||
return { options, prefix }
|
||||
}
|
||||
|
||||
export function validateCosConfig(config, driver) {
|
||||
if (driver !== 'qcloud') throw new Error('CONFIGURED_DRIVER_IS_NOT_COS')
|
||||
if (!/^[a-z0-9][a-z0-9-]*-\d+$/.test(config.bucket || '') || !/^[a-z][a-z0-9-]+$/.test(config.region || '')) {
|
||||
throw new Error('INVALID_COS_DESTINATION')
|
||||
}
|
||||
if (typeof config.access_key !== 'string' || !config.access_key || typeof config.secret_key !== 'string' || !config.secret_key) {
|
||||
throw new Error('COS_CREDENTIALS_MISSING')
|
||||
}
|
||||
const rawDomain = String(config.domain || '').trim()
|
||||
const base = new URL(rawDomain ? (/^https?:\/\//i.test(rawDomain) ? rawDomain : `https://${rawDomain}`)
|
||||
: `https://${config.bucket}.cos.${config.region}.myqcloud.com`)
|
||||
if (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash) {
|
||||
throw new Error('COS_REQUIRES_UNSIGNED_HTTPS_BASE_URL')
|
||||
}
|
||||
return { ...config, baseUrl: base.href.replace(/\/+$/, '') }
|
||||
}
|
||||
|
||||
export async function withDeadline(operation, milliseconds, abort, code) {
|
||||
let timer
|
||||
try {
|
||||
return await Promise.race([operation, new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
try { abort() } catch {}
|
||||
reject(new Error(code))
|
||||
}, milliseconds)
|
||||
})])
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
export async function readCosConfig(serverDirectory) {
|
||||
const mysql = dependency('mysql2')
|
||||
const { options, prefix } = configuredDatabase(serverDirectory)
|
||||
let connection
|
||||
let destroyed = false
|
||||
const abort = () => { destroyed = true; try { connection?.destroy() } catch {} }
|
||||
try {
|
||||
connection = mysql.createConnection(options).promise()
|
||||
await withDeadline(connection.connect(), 8000, abort, 'DATABASE_CONNECT_TIMEOUT')
|
||||
const [rows] = await withDeadline(connection.execute(`SELECT name,value FROM \`${prefix}config\` WHERE type=? AND name IN (?,?)`,
|
||||
['storage', 'default', 'qcloud']), 8000, abort, 'DATABASE_QUERY_TIMEOUT')
|
||||
const config = JSON.parse(rows.find(row => row.name === 'qcloud')?.value || '{}')
|
||||
return validateCosConfig(config, rows.find(row => row.name === 'default')?.value)
|
||||
} finally {
|
||||
if (connection && !destroyed) {
|
||||
try { await withDeadline(connection.end(), 2000, abort, 'DATABASE_CLOSE_TIMEOUT') } catch { abort() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Never print raw MySQL/COS errors: they may contain connection values or signed request URLs.
|
||||
export function safeError(error) {
|
||||
const code = String(error?.code || error?.message || '')
|
||||
return /^[A-Z][A-Z0-9_]{2,80}$/.test(code) ? code : 'REDACTED_OPERATION_ERROR'
|
||||
}
|
||||
Reference in New Issue
Block a user