diff seobeo/s_http_client.c @ 226:3fa4bf481f42

[merge] Merge hg-web into default
author MrJuneJune <me@mrjunejune.com>
date Sun, 02 Aug 2026 14:42:01 -0700
parents ce7f4400c2de
children
line wrap: on
line diff
--- a/seobeo/s_http_client.c	Sun Aug 02 08:52:13 2026 -0700
+++ b/seobeo/s_http_client.c	Sun Aug 02 14:42:01 2026 -0700
@@ -1,5 +1,46 @@
 #include "seobeo/seobeo.h"
 #include <ctype.h>
+#include <time.h>
+
+static int64_t Seobeo_Client_Monotonic_Milliseconds(void)
+{
+  struct timespec now;
+  clock_gettime(CLOCK_MONOTONIC, &now);
+  return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
+}
+
+static boolean Seobeo_Client_Read_Timed_Out(int32 timeout_ms, int64_t last_progress_ms)
+{
+  return timeout_ms > 0 &&
+         Seobeo_Client_Monotonic_Milliseconds() - last_progress_ms >= timeout_ms;
+}
+
+static size_t Seobeo_Client_Find_Header_Length(const uint8 *buffer, size_t length)
+{
+  if (!buffer || length < 4)
+    return 0;
+  for (size_t i = 0; i + 3 < length; i++)
+  {
+    if (buffer[i] == '\r' && buffer[i + 1] == '\n' &&
+        buffer[i + 2] == '\r' && buffer[i + 3] == '\n')
+      return i + 4;
+  }
+  return 0;
+}
+
+static const char *Seobeo_Client_Header_Value(
+    Seobeo_Request_Entry *headers,
+    const char *name)
+{
+  if (!headers || !name)
+    return NULL;
+  for (size_t i = 0; i < Dowa_Array_Length(headers); i++)
+  {
+    if (headers[i].key && strcasecmp(headers[i].key, name) == 0)
+      return headers[i].value;
+  }
+  return NULL;
+}
 
 static void Seobeo_Client_Parse_Url(const char *url, char **p_host, 
     char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena)
@@ -87,6 +128,7 @@
 
   p_req->follow_redirects = FALSE;
   p_req->max_redirects = 10;
+  p_req->timeout_ms = 0;
 
   return p_req;
 }
@@ -149,6 +191,13 @@
   p_req->max_redirects = max_redirects > 0 ? max_redirects : 10;
 }
 
+void Seobeo_Client_Request_Set_Timeout_Milliseconds(Seobeo_Client_Request *p_req, int32 timeout_ms)
+{
+  if (!p_req)
+    return;
+  p_req->timeout_ms = timeout_ms > 0 ? timeout_ms : 0;
+}
+
 void Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path)
 {
   if (!p_req || !path)
@@ -223,7 +272,10 @@
   return offset;
 }
 
-static Seobeo_Client_Response *Seobeo_Client_Parse_Response(Seobeo_Handle *p_handle, const char *download_path)
+static Seobeo_Client_Response *Seobeo_Client_Parse_Response(
+    Seobeo_Handle *p_handle,
+    const char *download_path,
+    int32 timeout_ms)
 {
   Seobeo_Client_Response *p_resp = malloc(sizeof(Seobeo_Client_Response));
   if (!p_resp)
@@ -238,27 +290,49 @@
     return NULL;
   }
 
+  int64_t last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
   while (TRUE)
   {
     int r = Seobeo_Handle_Read(p_handle);
-    if (r < 0)
-      return p_resp;
     if (r == -2)
       break;
+    if (r < 0)
+    {
+      Seobeo_Client_Response_Destroy(p_resp);
+      return NULL;
+    }
+    if (r > 0)
+      last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
 
-    if (p_handle->read_buffer_len >= 4 && strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL)
+    if (Seobeo_Client_Find_Header_Length(
+            p_handle->read_buffer, p_handle->read_buffer_len) > 0)
       break;
 
     if (r == 0)
+    {
+      if (Seobeo_Client_Read_Timed_Out(timeout_ms, last_progress_ms))
+      {
+        Seobeo_Log(SEOBEO_ERROR, "HTTP response header timed out\n");
+        Seobeo_Client_Response_Destroy(p_resp);
+        return NULL;
+      }
+      usleep(1000);
       continue;
+    }
   }
 
-  char *buf = (char*)p_handle->read_buffer;
-  char *hdr_end = strstr(buf, "\r\n\r\n");
-  if (!hdr_end)
-    return p_resp;
+  size_t hdr_len = Seobeo_Client_Find_Header_Length(
+      p_handle->read_buffer, p_handle->read_buffer_len);
+  if (hdr_len == 0)
+  {
+    Seobeo_Client_Response_Destroy(p_resp);
+    return NULL;
+  }
 
-  size_t hdr_len = hdr_end - buf + 4;
+  char *buf = Dowa_Arena_Allocate(p_resp->p_arena, hdr_len + 1);
+  memcpy(buf, p_handle->read_buffer, hdr_len);
+  buf[hdr_len] = '\0';
+  char *hdr_end = buf + hdr_len - 4;
 
   char version[16];
   int status_code;
@@ -328,12 +402,12 @@
 
   Seobeo_Handle_Consume(p_handle, (uint32)hdr_len);
 
-  void *p_cl_kv = Dowa_HashMap_Get_Ptr(p_resp->headers, "Content-Length");
   size_t body_len = 0;
-  if (p_cl_kv)
+  const char *content_length = Seobeo_Client_Header_Value(
+      p_resp->headers, "Content-Length");
+  if (content_length)
   {
-    const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value;
-    body_len = atoi(content_length_str);
+    body_len = (size_t)strtoull(content_length, NULL, 10);
   }
 
   FILE *p_file = NULL;
@@ -366,15 +440,32 @@
 
         total_read += to_copy;
         Seobeo_Handle_Consume(p_handle, (uint32)to_copy);
+        last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
       }
 
       if (total_read < body_len)
       {
         int r = Seobeo_Handle_Read(p_handle);
-        if (r < 0 || r == -2)
-          break;
+        if (r == -2 || r < 0)
+        {
+          if (p_file) fclose(p_file);
+          Seobeo_Client_Response_Destroy(p_resp);
+          return NULL;
+        }
+        if (r > 0)
+          last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
         if (r == 0)
+        {
+          if (Seobeo_Client_Read_Timed_Out(timeout_ms, last_progress_ms))
+          {
+            Seobeo_Log(SEOBEO_ERROR, "HTTP response body timed out\n");
+            if (p_file) fclose(p_file);
+            Seobeo_Client_Response_Destroy(p_resp);
+            return NULL;
+          }
+          usleep(1000);
           continue;
+        }
       }
     }
 
@@ -397,9 +488,12 @@
 
     while (1)
     {
-      int n = Seobeo_Handle_Read(p_handle);
+      int n = p_handle->read_buffer_len > 0
+          ? (int)p_handle->read_buffer_len
+          : Seobeo_Handle_Read(p_handle);
       if (n > 0)
       {
+        last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
         if (download_path)
         {
           fwrite(p_handle->read_buffer, 1, p_handle->read_buffer_len, p_file);
@@ -420,9 +514,23 @@
       else if (n == -2)
         break;
       else if (n == 0)
+      {
+        if (Seobeo_Client_Read_Timed_Out(timeout_ms, last_progress_ms))
+        {
+          Seobeo_Log(SEOBEO_ERROR, "HTTP response body timed out\n");
+          if (p_file) fclose(p_file);
+          Seobeo_Client_Response_Destroy(p_resp);
+          return NULL;
+        }
+        usleep(1000);
         continue;
+      }
       else
-        break;
+      {
+        if (p_file) fclose(p_file);
+        Seobeo_Client_Response_Destroy(p_resp);
+        return NULL;
+      }
     }
 
     if (!download_path)
@@ -468,7 +576,8 @@
     return NULL;
   }
 
-  Seobeo_Client_Response *p_resp = Seobeo_Client_Parse_Response(p_handle, p_req->download_path);
+  Seobeo_Client_Response *p_resp =
+      Seobeo_Client_Parse_Response(p_handle, p_req->download_path, p_req->timeout_ms);
 
   Seobeo_Handle_Destroy(p_handle);