Просмотр исходного кода

fix: implement true HTTP/1.0 client using raw TCP/TLS for ShopRenter proxy #55

Claude 5 месяцев назад
Родитель
Сommit
520b8dcd95
1 измененных файлов с 136 добавлено и 32 удалено
  1. 136 32
      supabase/functions/shoprenter-proxy/index.ts

+ 136 - 32
supabase/functions/shoprenter-proxy/index.ts

@@ -23,12 +23,116 @@ const corsHeaders = {
  * Request and response bodies are proxied as-is.
  */
 
-// Create HTTP client that forces HTTP/1.1 only (no HTTP/2)
-// This is required because ShopRenter has issues with HTTP/2
-const httpClient = Deno.createHttpClient({
-  http1: true,  // Enable HTTP/1.1
-  http2: false, // Disable HTTP/2
-})
+/**
+ * Makes an HTTP/1.0 request using raw TCP/TLS connection
+ * This is necessary because ShopRenter API strictly requires HTTP/1.0
+ * and Deno's fetch API only supports HTTP/1.1 and HTTP/2
+ */
+async function makeHttp10Request(
+  hostname: string,
+  path: string,
+  method: string,
+  headers: Record<string, string>,
+  body?: string
+): Promise<{ status: number; statusText: string; headers: Record<string, string>; body: string }> {
+  // Open TLS connection
+  const conn = await Deno.connectTls({
+    hostname: hostname,
+    port: 443,
+  })
+
+  try {
+    // Build HTTP/1.0 request
+    const requestLines: string[] = [
+      `${method} ${path} HTTP/1.0`,
+      `Host: ${hostname}`,
+    ]
+
+    // Add all headers
+    for (const [key, value] of Object.entries(headers)) {
+      requestLines.push(`${key}: ${value}`)
+    }
+
+    // Add Connection: close for HTTP/1.0
+    requestLines.push('Connection: close')
+
+    // Empty line to end headers
+    requestLines.push('')
+
+    // Add body if present
+    if (body) {
+      requestLines.push(body)
+    }
+
+    const request = requestLines.join('\r\n')
+
+    console.log('[HTTP/1.0] Sending request:', request.split('\r\n').slice(0, 10).join('\n'))
+
+    // Send request
+    const encoder = new TextEncoder()
+    await conn.write(encoder.encode(request))
+
+    // Read response
+    const decoder = new TextDecoder()
+    const buffer = new Uint8Array(1024 * 1024) // 1MB buffer
+    let responseData = ''
+
+    while (true) {
+      const n = await conn.read(buffer)
+      if (n === null) break
+      responseData += decoder.decode(buffer.subarray(0, n))
+    }
+
+    console.log('[HTTP/1.0] Received response length:', responseData.length)
+
+    // Parse HTTP response
+    const headerEndIndex = responseData.indexOf('\r\n\r\n')
+    if (headerEndIndex === -1) {
+      throw new Error('Invalid HTTP response: no header/body separator found')
+    }
+
+    const headerSection = responseData.substring(0, headerEndIndex)
+    const bodySection = responseData.substring(headerEndIndex + 4)
+
+    // Parse status line
+    const lines = headerSection.split('\r\n')
+    const statusLine = lines[0]
+    const statusMatch = statusLine.match(/HTTP\/1\.[01]\s+(\d+)\s+(.*)/)
+
+    if (!statusMatch) {
+      throw new Error(`Invalid HTTP status line: ${statusLine}`)
+    }
+
+    const status = parseInt(statusMatch[1])
+    const statusText = statusMatch[2]
+
+    // Parse headers
+    const responseHeaders: Record<string, string> = {}
+    for (let i = 1; i < lines.length; i++) {
+      const colonIndex = lines[i].indexOf(':')
+      if (colonIndex > 0) {
+        const key = lines[i].substring(0, colonIndex).trim()
+        const value = lines[i].substring(colonIndex + 1).trim()
+        responseHeaders[key] = value
+      }
+    }
+
+    console.log('[HTTP/1.0] Response received:', status, statusText)
+
+    return {
+      status,
+      statusText,
+      headers: responseHeaders,
+      body: bodySection,
+    }
+  } finally {
+    try {
+      conn.close()
+    } catch (e) {
+      console.error('[HTTP/1.0] Error closing connection:', e)
+    }
+  }
+}
 
 serve(async (req) => {
   // Handle CORS preflight
@@ -86,13 +190,14 @@ serve(async (req) => {
 
     const apiPath = pathMatch[1]
 
-    // Construct ShopRenter API URL - use api2.myshoprenter.hu domain
-    const shopRenterUrl = `https://${shopName}.api2.myshoprenter.hu${apiPath}${url.search}`
+    // Construct ShopRenter API URL components
+    const hostname = `${shopName}.api2.myshoprenter.hu`
+    const fullPath = `${apiPath}${url.search}`
 
-    console.log(`[ShopRenter Proxy] Forwarding ${req.method} request to: ${shopRenterUrl}`)
+    console.log(`[ShopRenter Proxy] Forwarding ${req.method} request to: https://${hostname}${fullPath}`)
 
     // Prepare headers for ShopRenter API request
-    const shopRenterHeaders = new Headers()
+    const shopRenterHeaders: Record<string, string> = {}
 
     // Copy all headers from the original request except Host, Authorization, and custom headers
     for (const [key, value] of req.headers.entries()) {
@@ -103,45 +208,47 @@ serve(async (req) => {
         !lowerKey.startsWith('x-shoprenter-') &&
         !lowerKey.startsWith('x-client-info')
       ) {
-        shopRenterHeaders.set(key, value)
+        shopRenterHeaders[key] = value
       }
     }
 
     // Set Authorization header with ShopRenter token
-    shopRenterHeaders.set('Authorization', `Bearer ${shopRenterToken}`)
+    shopRenterHeaders['Authorization'] = `Bearer ${shopRenterToken}`
 
     // Ensure required headers are present
-    if (!shopRenterHeaders.has('Content-Type')) {
-      shopRenterHeaders.set('Content-Type', 'application/json')
+    if (!shopRenterHeaders['Content-Type']) {
+      shopRenterHeaders['Content-Type'] = 'application/json'
     }
-    if (!shopRenterHeaders.has('Accept')) {
-      shopRenterHeaders.set('Accept', 'application/json')
+    if (!shopRenterHeaders['Accept']) {
+      shopRenterHeaders['Accept'] = 'application/json'
     }
 
     // Read request body if present
-    let body = null
+    let body: string | undefined = undefined
     if (req.method !== 'GET' && req.method !== 'HEAD') {
       const contentType = req.headers.get('Content-Type') || ''
       if (contentType.includes('application/json')) {
         try {
           body = await req.text()
+          if (body) {
+            shopRenterHeaders['Content-Length'] = body.length.toString()
+          }
         } catch (e) {
           console.error('[ShopRenter Proxy] Error reading request body:', e)
         }
       }
     }
 
-    // Make request to ShopRenter API using HTTP/1.1 only (HTTP/2 disabled)
-    // ShopRenter API requires HTTP/1.x - higher versions cause auth errors
-    console.log(`[ShopRenter Proxy] Making request with HTTP/1.1 to: ${shopRenterUrl}`)
-    console.log(`[ShopRenter Proxy] Headers:`, Object.fromEntries(shopRenterHeaders.entries()))
+    console.log(`[ShopRenter Proxy] Making HTTP/1.0 request to: ${hostname}${fullPath}`)
 
-    const shopRenterResponse = await fetch(shopRenterUrl, {
-      method: req.method,
-      headers: shopRenterHeaders,
-      body: body,
-      client: httpClient, // Use HTTP/1.1-only client
-    })
+    // Make HTTP/1.0 request using raw TCP/TLS connection
+    const shopRenterResponse = await makeHttp10Request(
+      hostname,
+      fullPath,
+      req.method,
+      shopRenterHeaders,
+      body
+    )
 
     console.log(`[ShopRenter Proxy] ShopRenter API responded with status: ${shopRenterResponse.status}`)
 
@@ -149,7 +256,7 @@ serve(async (req) => {
     const responseHeaders = new Headers(corsHeaders)
 
     // Copy relevant headers from ShopRenter response
-    for (const [key, value] of shopRenterResponse.headers.entries()) {
+    for (const [key, value] of Object.entries(shopRenterResponse.headers)) {
       const lowerKey = key.toLowerCase()
       // Copy headers but skip transfer-encoding and connection as they're handled by the Edge Function
       if (lowerKey !== 'transfer-encoding' && lowerKey !== 'connection') {
@@ -157,12 +264,9 @@ serve(async (req) => {
       }
     }
 
-    // Get response body
-    const responseBody = await shopRenterResponse.text()
-
     // Return proxied response
     return new Response(
-      responseBody,
+      shopRenterResponse.body,
       {
         status: shopRenterResponse.status,
         statusText: shopRenterResponse.statusText,