File size: 4,309 Bytes
9038ca8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34e385a
9038ca8
 
34e385a
 
9038ca8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34e385a
9038ca8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { createReadStream } from 'node:fs'
import { stat } from 'node:fs/promises'
import { createServer } from 'node:http'
import { extname, join, normalize, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = fileURLToPath(new URL('.', import.meta.url))
const distDir = resolve(__dirname, 'dist')
const port = Number(process.env.PORT || 7860)
const host = process.env.HOST || '0.0.0.0'

const mimeTypes = new Map([
  ['.html', 'text/html; charset=utf-8'],
  ['.js', 'text/javascript; charset=utf-8'],
  ['.css', 'text/css; charset=utf-8'],
  ['.json', 'application/json; charset=utf-8'],
  ['.png', 'image/png'],
  ['.jpg', 'image/jpeg'],
  ['.jpeg', 'image/jpeg'],
  ['.ico', 'image/x-icon'],
  ['.svg', 'image/svg+xml'],
  ['.webp', 'image/webp'],
  ['.woff', 'font/woff'],
  ['.woff2', 'font/woff2']
])

const hopByHopHeaders = new Set([
  'accept-encoding',
  'connection',
  'content-length',
  'content-encoding',
  'content-md5',
  'host',
  'keep-alive',
  'proxy-authenticate',
  'proxy-authorization',
  'te',
  'trailer',
  'transfer-encoding',
  'upgrade'
])

const server = createServer(async (request, response) => {
  try {
    const requestUrl = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`)
    if (requestUrl.pathname === '/api/proxy') {
      await proxyApiRequest(request, response, requestUrl)
      return
    }
    await serveStatic(requestUrl.pathname, response)
  } catch (error) {
    console.error(error)
    sendJson(response, 500, { error: 'Internal server error.' })
  }
})

server.listen(port, host, () => {
  console.log(`PixAI web server listening on http://${host}:${port}`)
})

async function proxyApiRequest(request, response, requestUrl) {
  const targetValue = requestUrl.searchParams.get('target')
  if (!targetValue) {
    sendJson(response, 400, { error: 'Missing target URL.' })
    return
  }

  let targetUrl
  try {
    targetUrl = new URL(targetValue)
  } catch {
    sendJson(response, 400, { error: 'Invalid target URL.' })
    return
  }

  if (!['https:', 'http:'].includes(targetUrl.protocol)) {
    sendJson(response, 400, { error: 'Unsupported target protocol.' })
    return
  }

  const headers = new Headers()
  for (const [key, value] of Object.entries(request.headers)) {
    if (hopByHopHeaders.has(key.toLowerCase())) continue
    if (Array.isArray(value)) headers.set(key, value.join(', '))
    else if (value != null) headers.set(key, value)
  }
  headers.set('accept-encoding', 'identity')

  const upstream = await fetch(targetUrl, {
    method: request.method,
    headers,
    body: request.method === 'GET' || request.method === 'HEAD' ? undefined : request,
    duplex: 'half'
  })

  response.statusCode = upstream.status
  for (const [key, value] of upstream.headers) {
    if (hopByHopHeaders.has(key.toLowerCase())) continue
    response.setHeader(key, value)
  }

  if (!upstream.body) {
    response.end()
    return
  }

  await upstream.body.pipeTo(new WritableStream({
    write(chunk) {
      response.write(Buffer.from(chunk))
    },
    close() {
      response.end()
    },
    abort(reason) {
      response.destroy(reason)
    }
  }))
}

async function serveStatic(pathname, response) {
  const decodedPath = decodeURIComponent(pathname)
  const safePath = normalize(decodedPath).replace(/^(\.\.[/\\])+/, '')
  const requestedPath = resolve(join(distDir, safePath))
  const requestedStat = requestedPath.startsWith(distDir) ? await stat(requestedPath).catch(() => null) : null
  const filePath = requestedStat?.isFile()
    ? requestedPath
    : join(distDir, 'index.html')

  const fileStat = await stat(filePath).catch(() => null)
  if (!fileStat?.isFile()) {
    sendJson(response, 404, { error: 'Build output not found. Run pnpm build first.' })
    return
  }

  response.statusCode = 200
  response.setHeader('Content-Type', mimeTypes.get(extname(filePath)) || 'application/octet-stream')
  response.setHeader('Cache-Control', filePath.endsWith('index.html') ? 'no-cache' : 'public, max-age=31536000, immutable')
  createReadStream(filePath).pipe(response)
}

function sendJson(response, status, payload) {
  response.statusCode = status
  response.setHeader('Content-Type', 'application/json; charset=utf-8')
  response.end(JSON.stringify(payload))
}