improved robustness of client/server encoding functions
[rocksndiamonds.git] / src / libgame / http.c
1 // ============================================================================
2 // Artsoft Retro-Game Library
3 // ----------------------------------------------------------------------------
4 // (c) 1995-2021 by Artsoft Entertainment
5 //                  Holger Schemel
6 //                  info@artsoft.org
7 //                  https://www.artsoft.org/
8 // ----------------------------------------------------------------------------
9 // http.c
10 // ============================================================================
11
12 #include <sys/stat.h>
13
14 #include "platform.h"
15
16 #include "http.h"
17 #include "misc.h"
18
19
20 static char http_error[MAX_HTTP_ERROR_SIZE];
21
22 static void SetHttpError(char *format, ...)
23 {
24   va_list ap;
25
26   va_start(ap, format);
27   vsnprintf(http_error, MAX_HTTP_ERROR_SIZE, format, ap);
28   va_end(ap);
29 }
30
31 char *GetHttpError(void)
32 {
33   return http_error;
34 }
35
36 void ConvertHttpRequestBodyToServerEncoding(struct HttpRequest *request)
37 {
38   char *body_utf8 = getUTF8FromLatin1(request->body);
39
40   strncpy(request->body, body_utf8, MAX_HTTP_BODY_SIZE);
41   request->body[MAX_HTTP_BODY_SIZE] = '\0';
42
43   checked_free(body_utf8);
44 }
45
46 void ConvertHttpResponseBodyToClientEncoding(struct HttpResponse *response)
47 {
48   char *body_latin1 = getLatin1FromUTF8(response->body);
49
50   strncpy(response->body, body_latin1, MAX_HTTP_BODY_SIZE);
51   response->body[MAX_HTTP_BODY_SIZE] = '\0';
52
53   response->body_size = strlen(response->body);
54
55   checked_free(body_latin1);
56 }
57
58 static void SetHttpResponseToDefaults(struct HttpResponse *response)
59 {
60   response->head[0] = '\0';
61   response->body[0] = '\0';
62   response->body_size = 0;
63
64   response->status_code = 0;
65   response->status_text[0] = '\0';
66 }
67
68 static boolean SetHTTPResponseCode(struct HttpResponse *response, char *buffer)
69 {
70   char *prefix = "HTTP/1.1 ";
71   char *prefix_start = strstr(buffer, prefix);
72
73   if (prefix_start == NULL)
74     return FALSE;
75
76   char *status_code_start = prefix_start + strlen(prefix);
77   char *status_code_end = strstr(status_code_start, " ");
78
79   if (status_code_end == NULL)
80     return FALSE;
81
82   int status_code_size = status_code_end - status_code_start;
83
84   if (status_code_size != 3)    // status code must have three digits
85     return FALSE;
86
87   char status_code[status_code_size + 1];
88
89   strncpy(status_code, status_code_start, status_code_size);
90   status_code[status_code_size] = '\0';
91
92   response->status_code = atoi(status_code);
93
94   char *status_text_start = status_code_end + 1;
95   char *status_text_end = strstr(status_text_start, "\r\n");
96
97   if (status_text_end == NULL)
98     return FALSE;
99
100   int status_text_size = status_text_end - status_text_start;
101
102   if (status_text_size > MAX_HTTP_ERROR_SIZE)
103     return FALSE;
104
105   strncpy(response->status_text, status_text_start, status_text_size);
106   response->status_text[status_text_size] = '\0';
107
108   return TRUE;
109 }
110
111 static boolean SetHTTPResponseHead(struct HttpResponse *response, char *buffer)
112 {
113   char *separator = "\r\n\r\n";
114   char *separator_start = strstr(buffer, separator);
115
116   if (separator_start == NULL)
117     return FALSE;
118
119   int head_size = separator_start - buffer;
120
121   if (head_size > MAX_HTTP_HEAD_SIZE)
122     return FALSE;
123
124   strncpy(response->head, buffer, head_size);
125   response->head[head_size] = '\0';
126
127   return TRUE;
128 }
129
130 static boolean SetHTTPResponseBody(struct HttpResponse *response, char *buffer,
131                                    int buffer_size)
132 {
133   char *separator = "\r\n\r\n";
134   char *separator_start = strstr(buffer, separator);
135
136   if (separator_start == NULL)
137     return FALSE;
138
139   int separator_size = strlen(separator);
140   int full_head_size = separator_start + separator_size - buffer;
141   int body_size = buffer_size - full_head_size;
142
143   if (body_size > MAX_HTTP_BODY_SIZE)
144     return FALSE;
145
146   memcpy(response->body, buffer + full_head_size, body_size);
147   response->body[body_size] = '\0';
148   response->body_size = body_size;
149
150   return TRUE;
151 }
152
153 static boolean DoHttpRequestExt(struct HttpRequest *request,
154                                 struct HttpResponse *response,
155                                 char *send_buffer,
156                                 char *recv_buffer,
157                                 int max_http_buffer_size)
158 {
159   SDLNet_SocketSet socket_set;
160   TCPsocket socket;
161   IPaddress ip;
162   int server_host;
163
164   SetHttpResponseToDefaults(response);
165
166   socket_set = SDLNet_AllocSocketSet(1);
167
168   if (socket_set == NULL)
169   {
170     SetHttpError("cannot allocate socket set");
171
172     return FALSE;
173   }
174
175   SDLNet_ResolveHost(&ip, request->hostname, request->port);
176
177   if (ip.host == INADDR_NONE)
178   {
179     SetHttpError("cannot resolve hostname '%s'", request->hostname);
180
181     return FALSE;
182   }
183
184   server_host = SDLNet_Read32(&ip.host);
185
186   Debug("network:http", "trying to connect to server at %d.%d.%d.%d ...",
187         (server_host >> 24) & 0xff,
188         (server_host >> 16) & 0xff,
189         (server_host >>  8) & 0xff,
190         (server_host >>  0) & 0xff);
191
192   socket = SDLNet_TCP_Open(&ip);
193
194   if (socket == NULL)
195   {
196     SetHttpError("cannot connect to host '%s': %s", request->hostname,
197                  SDLNet_GetError());
198
199     return FALSE;
200   }
201
202   if (SDLNet_TCP_AddSocket(socket_set, socket) == -1)
203   {
204     SetHttpError("cannot add socket to socket set");
205
206     return FALSE;
207   }
208
209   Debug("network:http", "successfully connected to server");
210
211   snprintf(request->head, MAX_HTTP_HEAD_SIZE,
212            "%s %s HTTP/1.1\r\n"
213            "Host: %s\r\n"
214            "X-Requested-With: XMLHttpRequest\r\n"
215            "Content-Type: application/json\r\n"
216            "Connection: close\r\n"
217            "Content-Length: %d\r\n",
218            request->method,
219            request->uri,
220            request->hostname,
221            (int)strlen(request->body));
222
223   snprintf(send_buffer, max_http_buffer_size,
224            "%s\r\n%s", request->head, request->body);
225
226   Debug("network:http", "client request:\n--- snip ---\n%s\n--- snip ---",
227         send_buffer);
228
229   int send_bytes = SDLNet_TCP_Send(socket, send_buffer, strlen(send_buffer));
230
231   if (send_bytes != strlen(send_buffer))
232   {
233     SetHttpError("sending request to server failed");
234
235     return FALSE;
236   }
237
238   int recv_bytes = SDLNet_TCP_Recv(socket, recv_buffer, max_http_buffer_size);
239
240   if (recv_bytes <= 0)
241   {
242     SetHttpError("receiving response from server failed");
243
244     return FALSE;
245   }
246
247   recv_buffer[recv_bytes] = '\0';
248
249   Debug("network:http", "server response:\n--- snip ---\n%s\n--- snip ---",
250         recv_buffer);
251
252   if (!SetHTTPResponseCode(response, recv_buffer))
253   {
254     SetHttpError("malformed HTTP response");
255
256     return FALSE;
257   }
258
259   if (!SetHTTPResponseHead(response, recv_buffer))
260   {
261     SetHttpError("invalid HTTP response header");
262
263     return FALSE;
264   }
265
266   if (!SetHTTPResponseBody(response, recv_buffer, recv_bytes))
267   {
268     SetHttpError("invalid HTTP response body");
269
270     return FALSE;
271   }
272
273   SDLNet_TCP_DelSocket(socket_set, socket);
274   SDLNet_TCP_Close(socket);
275
276   Debug("network:http", "server response: %d %s",
277         response->status_code,
278         response->status_text);
279
280   return TRUE;
281 }
282
283 boolean DoHttpRequest(struct HttpRequest *request,
284                       struct HttpResponse *response)
285 {
286   int max_http_buffer_size = MAX_HTTP_HEAD_SIZE + MAX_HTTP_BODY_SIZE;
287   char *send_buffer = checked_malloc(max_http_buffer_size + 1);
288   char *recv_buffer = checked_malloc(max_http_buffer_size + 1);
289
290   boolean success = DoHttpRequestExt(request, response,
291                                      send_buffer, recv_buffer,
292                                      max_http_buffer_size);
293
294   checked_free(send_buffer);
295   checked_free(recv_buffer);
296
297   runtime.use_api_server = success;
298
299   return success;
300 }