1 | /**
|
---|
2 | * @file
|
---|
3 | * HTTP client
|
---|
4 | */
|
---|
5 |
|
---|
6 | /*
|
---|
7 | * Copyright (c) 2018 Simon Goldschmidt <goldsimon@gmx.de>
|
---|
8 | * All rights reserved.
|
---|
9 | *
|
---|
10 | * Redistribution and use in source and binary forms, with or without modification,
|
---|
11 | * are permitted provided that the following conditions are met:
|
---|
12 | *
|
---|
13 | * 1. Redistributions of source code must retain the above copyright notice,
|
---|
14 | * this list of conditions and the following disclaimer.
|
---|
15 | * 2. Redistributions in binary form must reproduce the above copyright notice,
|
---|
16 | * this list of conditions and the following disclaimer in the documentation
|
---|
17 | * and/or other materials provided with the distribution.
|
---|
18 | * 3. The name of the author may not be used to endorse or promote products
|
---|
19 | * derived from this software without specific prior written permission.
|
---|
20 | *
|
---|
21 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
---|
22 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
---|
23 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
|
---|
24 | * SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
---|
25 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
---|
26 | * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
---|
27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
---|
28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
---|
29 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
|
---|
30 | * OF SUCH DAMAGE.
|
---|
31 | *
|
---|
32 | * This file is part of the lwIP TCP/IP stack.
|
---|
33 | *
|
---|
34 | * Author: Simon Goldschmidt <goldsimon@gmx.de>
|
---|
35 | */
|
---|
36 |
|
---|
37 | /**
|
---|
38 | * @defgroup httpc HTTP client
|
---|
39 | * @ingroup apps
|
---|
40 | * @todo:
|
---|
41 | * - persistent connections
|
---|
42 | * - select outgoing http version
|
---|
43 | * - optionally follow redirect
|
---|
44 | * - check request uri for invalid characters? (e.g. encode spaces)
|
---|
45 | * - IPv6 support
|
---|
46 | */
|
---|
47 |
|
---|
48 | #include "lwip/apps/http_client.h"
|
---|
49 |
|
---|
50 | #include "lwip/altcp_tcp.h"
|
---|
51 | #include "lwip/dns.h"
|
---|
52 | #include "lwip/debug.h"
|
---|
53 | #include "lwip/mem.h"
|
---|
54 | #include "lwip/altcp_tls.h"
|
---|
55 | #include "lwip/init.h"
|
---|
56 |
|
---|
57 | #include <stdio.h>
|
---|
58 | #include <stdlib.h>
|
---|
59 | #include <string.h>
|
---|
60 |
|
---|
61 | #if LWIP_TCP && LWIP_CALLBACK_API
|
---|
62 |
|
---|
63 | /**
|
---|
64 | * HTTPC_DEBUG: Enable debugging for HTTP client.
|
---|
65 | */
|
---|
66 | #ifndef HTTPC_DEBUG
|
---|
67 | #define HTTPC_DEBUG LWIP_DBG_OFF
|
---|
68 | #endif
|
---|
69 |
|
---|
70 | /** Set this to 1 to keep server name and uri in request state */
|
---|
71 | #ifndef HTTPC_DEBUG_REQUEST
|
---|
72 | #define HTTPC_DEBUG_REQUEST 0
|
---|
73 | #endif
|
---|
74 |
|
---|
75 | /** This string is passed in the HTTP header as "User-Agent: " */
|
---|
76 | #ifndef HTTPC_CLIENT_AGENT
|
---|
77 | #define HTTPC_CLIENT_AGENT "lwIP/" LWIP_VERSION_STRING " (http://savannah.nongnu.org/projects/lwip)"
|
---|
78 | #endif
|
---|
79 |
|
---|
80 | /* the various debug levels for this file */
|
---|
81 | #define HTTPC_DEBUG_TRACE (HTTPC_DEBUG | LWIP_DBG_TRACE)
|
---|
82 | #define HTTPC_DEBUG_STATE (HTTPC_DEBUG | LWIP_DBG_STATE)
|
---|
83 | #define HTTPC_DEBUG_WARN (HTTPC_DEBUG | LWIP_DBG_LEVEL_WARNING)
|
---|
84 | #define HTTPC_DEBUG_WARN_STATE (HTTPC_DEBUG | LWIP_DBG_LEVEL_WARNING | LWIP_DBG_STATE)
|
---|
85 | #define HTTPC_DEBUG_SERIOUS (HTTPC_DEBUG | LWIP_DBG_LEVEL_SERIOUS)
|
---|
86 |
|
---|
87 | #define HTTPC_POLL_INTERVAL 1
|
---|
88 | #define HTTPC_POLL_TIMEOUT 30 /* 15 seconds */
|
---|
89 |
|
---|
90 | #define HTTPC_CONTENT_LEN_INVALID 0xFFFFFFFF
|
---|
91 |
|
---|
92 | /* GET request basic */
|
---|
93 | #define HTTPC_REQ_11 "GET %s HTTP/1.1\r\n" /* URI */\
|
---|
94 | "User-Agent: %s\r\n" /* User-Agent */ \
|
---|
95 | "Accept: */*\r\n" \
|
---|
96 | "Connection: Close\r\n" /* we don't support persistent connections, yet */ \
|
---|
97 | "\r\n"
|
---|
98 | #define HTTPC_REQ_11_FORMAT(uri) HTTPC_REQ_11, uri, HTTPC_CLIENT_AGENT
|
---|
99 |
|
---|
100 | /* GET request with host */
|
---|
101 | #define HTTPC_REQ_11_HOST "GET %s HTTP/1.1\r\n" /* URI */\
|
---|
102 | "User-Agent: %s\r\n" /* User-Agent */ \
|
---|
103 | "Accept: */*\r\n" \
|
---|
104 | "Host: %s\r\n" /* server name */ \
|
---|
105 | "Connection: Close\r\n" /* we don't support persistent connections, yet */ \
|
---|
106 | "\r\n"
|
---|
107 | #define HTTPC_REQ_11_HOST_FORMAT(uri, srv_name) HTTPC_REQ_11_HOST, uri, HTTPC_CLIENT_AGENT, srv_name
|
---|
108 |
|
---|
109 | /* GET request with proxy */
|
---|
110 | #define HTTPC_REQ_11_PROXY "GET http://%s%s HTTP/1.1\r\n" /* HOST, URI */\
|
---|
111 | "User-Agent: %s\r\n" /* User-Agent */ \
|
---|
112 | "Accept: */*\r\n" \
|
---|
113 | "Host: %s\r\n" /* server name */ \
|
---|
114 | "Connection: Close\r\n" /* we don't support persistent connections, yet */ \
|
---|
115 | "\r\n"
|
---|
116 | #define HTTPC_REQ_11_PROXY_FORMAT(host, uri, srv_name) HTTPC_REQ_11_PROXY, host, uri, HTTPC_CLIENT_AGENT, srv_name
|
---|
117 |
|
---|
118 | /* GET request with proxy (non-default server port) */
|
---|
119 | #define HTTPC_REQ_11_PROXY_PORT "GET http://%s:%d%s HTTP/1.1\r\n" /* HOST, host-port, URI */\
|
---|
120 | "User-Agent: %s\r\n" /* User-Agent */ \
|
---|
121 | "Accept: */*\r\n" \
|
---|
122 | "Host: %s\r\n" /* server name */ \
|
---|
123 | "Connection: Close\r\n" /* we don't support persistent connections, yet */ \
|
---|
124 | "\r\n"
|
---|
125 | #define HTTPC_REQ_11_PROXY_PORT_FORMAT(host, host_port, uri, srv_name) HTTPC_REQ_11_PROXY_PORT, host, host_port, uri, HTTPC_CLIENT_AGENT, srv_name
|
---|
126 |
|
---|
127 | typedef enum ehttpc_parse_state {
|
---|
128 | HTTPC_PARSE_WAIT_FIRST_LINE = 0,
|
---|
129 | HTTPC_PARSE_WAIT_HEADERS,
|
---|
130 | HTTPC_PARSE_RX_DATA
|
---|
131 | } httpc_parse_state_t;
|
---|
132 |
|
---|
133 | typedef struct _httpc_state
|
---|
134 | {
|
---|
135 | struct altcp_pcb* pcb;
|
---|
136 | ip_addr_t remote_addr;
|
---|
137 | u16_t remote_port;
|
---|
138 | int timeout_ticks;
|
---|
139 | struct pbuf *request;
|
---|
140 | struct pbuf *rx_hdrs;
|
---|
141 | u16_t rx_http_version;
|
---|
142 | u16_t rx_status;
|
---|
143 | altcp_recv_fn recv_fn;
|
---|
144 | const httpc_connection_t *conn_settings;
|
---|
145 | void* callback_arg;
|
---|
146 | u32_t rx_content_len;
|
---|
147 | u32_t hdr_content_len;
|
---|
148 | httpc_parse_state_t parse_state;
|
---|
149 | #if HTTPC_DEBUG_REQUEST
|
---|
150 | char* server_name;
|
---|
151 | char* uri;
|
---|
152 | #endif
|
---|
153 | } httpc_state_t;
|
---|
154 |
|
---|
155 | /** Free http client state and deallocate all resources within */
|
---|
156 | static err_t
|
---|
157 | httpc_free_state(httpc_state_t* req)
|
---|
158 | {
|
---|
159 | struct altcp_pcb* tpcb;
|
---|
160 |
|
---|
161 | if (req->request != NULL) {
|
---|
162 | pbuf_free(req->request);
|
---|
163 | req->request = NULL;
|
---|
164 | }
|
---|
165 | if (req->rx_hdrs != NULL) {
|
---|
166 | pbuf_free(req->rx_hdrs);
|
---|
167 | req->rx_hdrs = NULL;
|
---|
168 | }
|
---|
169 |
|
---|
170 | tpcb = req->pcb;
|
---|
171 | mem_free(req);
|
---|
172 | req = NULL;
|
---|
173 |
|
---|
174 | if (tpcb != NULL) {
|
---|
175 | err_t r;
|
---|
176 | altcp_arg(tpcb, NULL);
|
---|
177 | altcp_recv(tpcb, NULL);
|
---|
178 | altcp_err(tpcb, NULL);
|
---|
179 | altcp_poll(tpcb, NULL, 0);
|
---|
180 | altcp_sent(tpcb, NULL);
|
---|
181 | r = altcp_close(tpcb);
|
---|
182 | if (r != ERR_OK) {
|
---|
183 | altcp_abort(tpcb);
|
---|
184 | return ERR_ABRT;
|
---|
185 | }
|
---|
186 | }
|
---|
187 | return ERR_OK;
|
---|
188 | }
|
---|
189 |
|
---|
190 | /** Close the connection: call finished callback and free the state */
|
---|
191 | static err_t
|
---|
192 | httpc_close(httpc_state_t* req, httpc_result_t result, u32_t server_response, err_t err)
|
---|
193 | {
|
---|
194 | if (req != NULL) {
|
---|
195 | if (req->conn_settings != NULL) {
|
---|
196 | if (req->conn_settings->result_fn != NULL) {
|
---|
197 | req->conn_settings->result_fn(req->callback_arg, result, req->rx_content_len, server_response, err);
|
---|
198 | }
|
---|
199 | }
|
---|
200 | return httpc_free_state(req);
|
---|
201 | }
|
---|
202 | return ERR_OK;
|
---|
203 | }
|
---|
204 |
|
---|
205 | /** Parse http header response line 1 */
|
---|
206 | static err_t
|
---|
207 | http_parse_response_status(struct pbuf *p, u16_t *http_version, u16_t *http_status, u16_t *http_status_str_offset)
|
---|
208 | {
|
---|
209 | u16_t end1 = pbuf_memfind(p, "\r\n", 2, 0);
|
---|
210 | if (end1 != 0xFFFF) {
|
---|
211 | /* get parts of first line */
|
---|
212 | u16_t space1, space2;
|
---|
213 | space1 = pbuf_memfind(p, " ", 1, 0);
|
---|
214 | if (space1 != 0xFFFF) {
|
---|
215 | if ((pbuf_memcmp(p, 0, "HTTP/", 5) == 0) && (pbuf_get_at(p, 6) == '.')) {
|
---|
216 | char status_num[10];
|
---|
217 | size_t status_num_len;
|
---|
218 | /* parse http version */
|
---|
219 | u16_t version = pbuf_get_at(p, 5) - '0';
|
---|
220 | version <<= 8;
|
---|
221 | version |= pbuf_get_at(p, 7) - '0';
|
---|
222 | *http_version = version;
|
---|
223 |
|
---|
224 | /* parse http status number */
|
---|
225 | space2 = pbuf_memfind(p, " ", 1, space1 + 1);
|
---|
226 | if (space2 != 0xFFFF) {
|
---|
227 | *http_status_str_offset = space2 + 1;
|
---|
228 | status_num_len = space2 - space1 - 1;
|
---|
229 | } else {
|
---|
230 | status_num_len = end1 - space1 - 1;
|
---|
231 | }
|
---|
232 | memset(status_num, 0, sizeof(status_num));
|
---|
233 | if (pbuf_copy_partial(p, status_num, (u16_t)status_num_len, space1 + 1) == status_num_len) {
|
---|
234 | int status = atoi(status_num);
|
---|
235 | if ((status > 0) && (status <= 0xFFFF)) {
|
---|
236 | *http_status = (u16_t)status;
|
---|
237 | return ERR_OK;
|
---|
238 | }
|
---|
239 | }
|
---|
240 | }
|
---|
241 | }
|
---|
242 | }
|
---|
243 | return ERR_VAL;
|
---|
244 | }
|
---|
245 |
|
---|
246 | /** Wait for all headers to be received, return its length and content-length (if available) */
|
---|
247 | static err_t
|
---|
248 | http_wait_headers(struct pbuf *p, u32_t *content_length, u16_t *total_header_len)
|
---|
249 | {
|
---|
250 | u16_t end1 = pbuf_memfind(p, "\r\n\r\n", 4, 0);
|
---|
251 | if (end1 < (0xFFFF - 2)) {
|
---|
252 | /* all headers received */
|
---|
253 | /* check if we have a content length (@todo: case insensitive?) */
|
---|
254 | u16_t content_len_hdr;
|
---|
255 | *content_length = HTTPC_CONTENT_LEN_INVALID;
|
---|
256 | *total_header_len = end1 + 4;
|
---|
257 |
|
---|
258 | content_len_hdr = pbuf_memfind(p, "Content-Length: ", 16, 0);
|
---|
259 | if (content_len_hdr != 0xFFFF) {
|
---|
260 | u16_t content_len_line_end = pbuf_memfind(p, "\r\n", 2, content_len_hdr);
|
---|
261 | if (content_len_line_end != 0xFFFF) {
|
---|
262 | char content_len_num[16];
|
---|
263 | u16_t content_len_num_len = (u16_t)(content_len_line_end - content_len_hdr - 16);
|
---|
264 | memset(content_len_num, 0, sizeof(content_len_num));
|
---|
265 | if (pbuf_copy_partial(p, content_len_num, content_len_num_len, content_len_hdr + 16) == content_len_num_len) {
|
---|
266 | int len = atoi(content_len_num);
|
---|
267 | if ((len >= 0) && ((u32_t)len < HTTPC_CONTENT_LEN_INVALID)) {
|
---|
268 | *content_length = (u32_t)len;
|
---|
269 | }
|
---|
270 | }
|
---|
271 | }
|
---|
272 | }
|
---|
273 | return ERR_OK;
|
---|
274 | }
|
---|
275 | return ERR_VAL;
|
---|
276 | }
|
---|
277 |
|
---|
278 | /** http client tcp recv callback */
|
---|
279 | static err_t
|
---|
280 | httpc_tcp_recv(void *arg, struct altcp_pcb *pcb, struct pbuf *p, err_t r)
|
---|
281 | {
|
---|
282 | httpc_state_t* req = (httpc_state_t*)arg;
|
---|
283 | LWIP_UNUSED_ARG(r);
|
---|
284 |
|
---|
285 | if (p == NULL) {
|
---|
286 | httpc_result_t result;
|
---|
287 | if (req->parse_state != HTTPC_PARSE_RX_DATA) {
|
---|
288 | /* did not get RX data yet */
|
---|
289 | result = HTTPC_RESULT_ERR_CLOSED;
|
---|
290 | } else if ((req->hdr_content_len != HTTPC_CONTENT_LEN_INVALID) &&
|
---|
291 | (req->hdr_content_len != req->rx_content_len)) {
|
---|
292 | /* header has been received with content length but not all data received */
|
---|
293 | result = HTTPC_RESULT_ERR_CONTENT_LEN;
|
---|
294 | } else {
|
---|
295 | /* receiving data and either all data received or no content length header */
|
---|
296 | result = HTTPC_RESULT_OK;
|
---|
297 | }
|
---|
298 | return httpc_close(req, result, req->rx_status, ERR_OK);
|
---|
299 | }
|
---|
300 | if (req->parse_state != HTTPC_PARSE_RX_DATA) {
|
---|
301 | if (req->rx_hdrs == NULL) {
|
---|
302 | req->rx_hdrs = p;
|
---|
303 | } else {
|
---|
304 | pbuf_cat(req->rx_hdrs, p);
|
---|
305 | }
|
---|
306 | if (req->parse_state == HTTPC_PARSE_WAIT_FIRST_LINE) {
|
---|
307 | u16_t status_str_off;
|
---|
308 | err_t err = http_parse_response_status(req->rx_hdrs, &req->rx_http_version, &req->rx_status, &status_str_off);
|
---|
309 | if (err == ERR_OK) {
|
---|
310 | /* don't care status string */
|
---|
311 | req->parse_state = HTTPC_PARSE_WAIT_HEADERS;
|
---|
312 | }
|
---|
313 | }
|
---|
314 | if (req->parse_state == HTTPC_PARSE_WAIT_HEADERS) {
|
---|
315 | u16_t total_header_len;
|
---|
316 | err_t err = http_wait_headers(req->rx_hdrs, &req->hdr_content_len, &total_header_len);
|
---|
317 | if (err == ERR_OK) {
|
---|
318 | struct pbuf *q;
|
---|
319 | /* full header received, send window update for header bytes and call into client callback */
|
---|
320 | altcp_recved(pcb, total_header_len);
|
---|
321 | if (req->conn_settings) {
|
---|
322 | if (req->conn_settings->headers_done_fn) {
|
---|
323 | err = req->conn_settings->headers_done_fn(req, req->callback_arg, req->rx_hdrs, total_header_len, req->hdr_content_len);
|
---|
324 | if (err != ERR_OK) {
|
---|
325 | return httpc_close(req, HTTPC_RESULT_LOCAL_ABORT, req->rx_status, err);
|
---|
326 | }
|
---|
327 | }
|
---|
328 | }
|
---|
329 | /* hide header bytes in pbuf */
|
---|
330 | q = pbuf_free_header(req->rx_hdrs, total_header_len);
|
---|
331 | p = q;
|
---|
332 | req->rx_hdrs = NULL;
|
---|
333 | /* go on with data */
|
---|
334 | req->parse_state = HTTPC_PARSE_RX_DATA;
|
---|
335 | }
|
---|
336 | }
|
---|
337 | }
|
---|
338 | if ((p != NULL) && (req->parse_state == HTTPC_PARSE_RX_DATA)) {
|
---|
339 | req->rx_content_len += p->tot_len;
|
---|
340 | if (req->recv_fn != NULL) {
|
---|
341 | /* directly return here: the connection migth already be aborted from the callback! */
|
---|
342 | return req->recv_fn(req->callback_arg, pcb, p, r);
|
---|
343 | } else {
|
---|
344 | altcp_recved(pcb, p->tot_len);
|
---|
345 | pbuf_free(p);
|
---|
346 | }
|
---|
347 | }
|
---|
348 | return ERR_OK;
|
---|
349 | }
|
---|
350 |
|
---|
351 | /** http client tcp err callback */
|
---|
352 | static void
|
---|
353 | httpc_tcp_err(void *arg, err_t err)
|
---|
354 | {
|
---|
355 | httpc_state_t* req = (httpc_state_t*)arg;
|
---|
356 | if (req != NULL) {
|
---|
357 | /* pcb has already been deallocated */
|
---|
358 | req->pcb = NULL;
|
---|
359 | httpc_close(req, HTTPC_RESULT_ERR_CLOSED, 0, err);
|
---|
360 | }
|
---|
361 | }
|
---|
362 |
|
---|
363 | /** http client tcp poll callback */
|
---|
364 | static err_t
|
---|
365 | httpc_tcp_poll(void *arg, struct altcp_pcb *pcb)
|
---|
366 | {
|
---|
367 | /* implement timeout */
|
---|
368 | httpc_state_t* req = (httpc_state_t*)arg;
|
---|
369 | LWIP_UNUSED_ARG(pcb);
|
---|
370 | if (req != NULL) {
|
---|
371 | if (req->timeout_ticks) {
|
---|
372 | req->timeout_ticks--;
|
---|
373 | }
|
---|
374 | if (!req->timeout_ticks) {
|
---|
375 | return httpc_close(req, HTTPC_RESULT_ERR_TIMEOUT, 0, ERR_OK);
|
---|
376 | }
|
---|
377 | }
|
---|
378 | return ERR_OK;
|
---|
379 | }
|
---|
380 |
|
---|
381 | /** http client tcp sent callback */
|
---|
382 | static err_t
|
---|
383 | httpc_tcp_sent(void *arg, struct altcp_pcb *pcb, u16_t len)
|
---|
384 | {
|
---|
385 | /* nothing to do here for now */
|
---|
386 | LWIP_UNUSED_ARG(arg);
|
---|
387 | LWIP_UNUSED_ARG(pcb);
|
---|
388 | LWIP_UNUSED_ARG(len);
|
---|
389 | return ERR_OK;
|
---|
390 | }
|
---|
391 |
|
---|
392 | /** http client tcp connected callback */
|
---|
393 | static err_t
|
---|
394 | httpc_tcp_connected(void *arg, struct altcp_pcb *pcb, err_t err)
|
---|
395 | {
|
---|
396 | err_t r;
|
---|
397 | httpc_state_t* req = (httpc_state_t*)arg;
|
---|
398 | LWIP_UNUSED_ARG(pcb);
|
---|
399 | LWIP_UNUSED_ARG(err);
|
---|
400 |
|
---|
401 | /* send request; last char is zero termination */
|
---|
402 | r = altcp_write(req->pcb, req->request->payload, req->request->len - 1, TCP_WRITE_FLAG_COPY);
|
---|
403 | if (r != ERR_OK) {
|
---|
404 | /* could not write the single small request -> fail, don't retry */
|
---|
405 | return httpc_close(req, HTTPC_RESULT_ERR_MEM, 0, r);
|
---|
406 | }
|
---|
407 | /* everything written, we can free the request */
|
---|
408 | pbuf_free(req->request);
|
---|
409 | req->request = NULL;
|
---|
410 |
|
---|
411 | altcp_output(req->pcb);
|
---|
412 | return ERR_OK;
|
---|
413 | }
|
---|
414 |
|
---|
415 | /** Start the http request when the server IP addr is known */
|
---|
416 | static err_t
|
---|
417 | httpc_get_internal_addr(httpc_state_t* req, const ip_addr_t *ipaddr)
|
---|
418 | {
|
---|
419 | err_t err;
|
---|
420 | LWIP_ASSERT("req != NULL", req != NULL);
|
---|
421 |
|
---|
422 | if (&req->remote_addr != ipaddr) {
|
---|
423 | /* fill in remote addr if called externally */
|
---|
424 | req->remote_addr = *ipaddr;
|
---|
425 | }
|
---|
426 |
|
---|
427 | err = altcp_connect(req->pcb, &req->remote_addr, req->remote_port, httpc_tcp_connected);
|
---|
428 | if (err == ERR_OK) {
|
---|
429 | return ERR_OK;
|
---|
430 | }
|
---|
431 | LWIP_DEBUGF(HTTPC_DEBUG_WARN_STATE, ("tcp_connect failed: %d\n", (int)err));
|
---|
432 | return err;
|
---|
433 | }
|
---|
434 |
|
---|
435 | #if LWIP_DNS
|
---|
436 | /** DNS callback
|
---|
437 | * If ipaddr is non-NULL, resolving succeeded and the request can be sent, otherwise it failed.
|
---|
438 | */
|
---|
439 | static void
|
---|
440 | httpc_dns_found(const char* hostname, const ip_addr_t *ipaddr, void *arg)
|
---|
441 | {
|
---|
442 | httpc_state_t* req = (httpc_state_t*)arg;
|
---|
443 | err_t err;
|
---|
444 | httpc_result_t result;
|
---|
445 |
|
---|
446 | LWIP_UNUSED_ARG(hostname);
|
---|
447 |
|
---|
448 | if (ipaddr != NULL) {
|
---|
449 | err = httpc_get_internal_addr(req, ipaddr);
|
---|
450 | if (err == ERR_OK) {
|
---|
451 | return;
|
---|
452 | }
|
---|
453 | result = HTTPC_RESULT_ERR_CONNECT;
|
---|
454 | } else {
|
---|
455 | LWIP_DEBUGF(HTTPC_DEBUG_WARN_STATE, ("httpc_dns_found: failed to resolve hostname: %s\n",
|
---|
456 | hostname));
|
---|
457 | result = HTTPC_RESULT_ERR_HOSTNAME;
|
---|
458 | err = ERR_ARG;
|
---|
459 | }
|
---|
460 | httpc_close(req, result, 0, err);
|
---|
461 | }
|
---|
462 | #endif /* LWIP_DNS */
|
---|
463 |
|
---|
464 | /** Start the http request after converting 'server_name' to ip address (DNS or address string) */
|
---|
465 | static err_t
|
---|
466 | httpc_get_internal_dns(httpc_state_t* req, const char* server_name)
|
---|
467 | {
|
---|
468 | err_t err;
|
---|
469 | LWIP_ASSERT("req != NULL", req != NULL);
|
---|
470 |
|
---|
471 | #if LWIP_DNS
|
---|
472 | err = dns_gethostbyname(server_name, &req->remote_addr, httpc_dns_found, req);
|
---|
473 | #else
|
---|
474 | err = ipaddr_aton(server_name, &req->remote_addr) ? ERR_OK : ERR_ARG;
|
---|
475 | #endif
|
---|
476 |
|
---|
477 | if (err == ERR_OK) {
|
---|
478 | /* cached or IP-string */
|
---|
479 | err = httpc_get_internal_addr(req, &req->remote_addr);
|
---|
480 | } else if (err == ERR_INPROGRESS) {
|
---|
481 | return ERR_OK;
|
---|
482 | }
|
---|
483 | return err;
|
---|
484 | }
|
---|
485 |
|
---|
486 | static int
|
---|
487 | httpc_create_request_string(const httpc_connection_t *settings, const char* server_name, int server_port, const char* uri,
|
---|
488 | int use_host, char *buffer, size_t buffer_size)
|
---|
489 | {
|
---|
490 | if (settings->use_proxy) {
|
---|
491 | LWIP_ASSERT("server_name != NULL", server_name != NULL);
|
---|
492 | if (server_port != HTTP_DEFAULT_PORT) {
|
---|
493 | return snprintf(buffer, buffer_size, HTTPC_REQ_11_PROXY_PORT_FORMAT(server_name, server_port, uri, server_name));
|
---|
494 | } else {
|
---|
495 | return snprintf(buffer, buffer_size, HTTPC_REQ_11_PROXY_FORMAT(server_name, uri, server_name));
|
---|
496 | }
|
---|
497 | } else if (use_host) {
|
---|
498 | LWIP_ASSERT("server_name != NULL", server_name != NULL);
|
---|
499 | return snprintf(buffer, buffer_size, HTTPC_REQ_11_HOST_FORMAT(uri, server_name));
|
---|
500 | } else {
|
---|
501 | return snprintf(buffer, buffer_size, HTTPC_REQ_11_FORMAT(uri));
|
---|
502 | }
|
---|
503 | }
|
---|
504 |
|
---|
505 | /** Initialize the connection struct */
|
---|
506 | static err_t
|
---|
507 | httpc_init_connection_common(httpc_state_t **connection, const httpc_connection_t *settings, const char* server_name,
|
---|
508 | u16_t server_port, const char* uri, altcp_recv_fn recv_fn, void* callback_arg, int use_host)
|
---|
509 | {
|
---|
510 | size_t alloc_len;
|
---|
511 | mem_size_t mem_alloc_len;
|
---|
512 | int req_len, req_len2;
|
---|
513 | httpc_state_t *req;
|
---|
514 | #if HTTPC_DEBUG_REQUEST
|
---|
515 | size_t server_name_len, uri_len;
|
---|
516 | #endif
|
---|
517 |
|
---|
518 | LWIP_ASSERT("uri != NULL", uri != NULL);
|
---|
519 |
|
---|
520 | /* get request len */
|
---|
521 | req_len = httpc_create_request_string(settings, server_name, server_port, uri, use_host, NULL, 0);
|
---|
522 | if ((req_len < 0) || (req_len > 0xFFFF)) {
|
---|
523 | return ERR_VAL;
|
---|
524 | }
|
---|
525 | /* alloc state and request in one block */
|
---|
526 | alloc_len = sizeof(httpc_state_t);
|
---|
527 | #if HTTPC_DEBUG_REQUEST
|
---|
528 | server_name_len = server_name ? strlen(server_name) : 0;
|
---|
529 | uri_len = strlen(uri);
|
---|
530 | alloc_len += server_name_len + 1 + uri_len + 1;
|
---|
531 | #endif
|
---|
532 | mem_alloc_len = (mem_size_t)alloc_len;
|
---|
533 | if ((mem_alloc_len < alloc_len) || (req_len + 1 > 0xFFFF)) {
|
---|
534 | return ERR_VAL;
|
---|
535 | }
|
---|
536 |
|
---|
537 | req = (httpc_state_t*)mem_malloc((mem_size_t)alloc_len);
|
---|
538 | if(req == NULL) {
|
---|
539 | return ERR_MEM;
|
---|
540 | }
|
---|
541 | memset(req, 0, sizeof(httpc_state_t));
|
---|
542 | req->timeout_ticks = HTTPC_POLL_TIMEOUT;
|
---|
543 | req->request = pbuf_alloc(PBUF_RAW, (u16_t)(req_len + 1), PBUF_RAM);
|
---|
544 | if (req->request == NULL) {
|
---|
545 | httpc_free_state(req);
|
---|
546 | return ERR_MEM;
|
---|
547 | }
|
---|
548 | if (req->request->next != NULL) {
|
---|
549 | /* need a pbuf in one piece */
|
---|
550 | httpc_free_state(req);
|
---|
551 | return ERR_MEM;
|
---|
552 | }
|
---|
553 | req->hdr_content_len = HTTPC_CONTENT_LEN_INVALID;
|
---|
554 | #if HTTPC_DEBUG_REQUEST
|
---|
555 | req->server_name = (char*)(req + 1);
|
---|
556 | if (server_name) {
|
---|
557 | memcpy(req->server_name, server_name, server_name_len + 1);
|
---|
558 | }
|
---|
559 | req->uri = req->server_name + server_name_len + 1;
|
---|
560 | memcpy(req->uri, uri, uri_len + 1);
|
---|
561 | #endif
|
---|
562 | req->pcb = altcp_new(settings->altcp_allocator);
|
---|
563 | if(req->pcb == NULL) {
|
---|
564 | httpc_free_state(req);
|
---|
565 | return ERR_MEM;
|
---|
566 | }
|
---|
567 | req->remote_port = settings->use_proxy ? settings->proxy_port : server_port;
|
---|
568 | altcp_arg(req->pcb, req);
|
---|
569 | altcp_recv(req->pcb, httpc_tcp_recv);
|
---|
570 | altcp_err(req->pcb, httpc_tcp_err);
|
---|
571 | altcp_poll(req->pcb, httpc_tcp_poll, HTTPC_POLL_INTERVAL);
|
---|
572 | altcp_sent(req->pcb, httpc_tcp_sent);
|
---|
573 |
|
---|
574 | /* set up request buffer */
|
---|
575 | req_len2 = httpc_create_request_string(settings, server_name, server_port, uri, use_host,
|
---|
576 | (char *)req->request->payload, req_len + 1);
|
---|
577 | if (req_len2 != req_len) {
|
---|
578 | httpc_free_state(req);
|
---|
579 | return ERR_VAL;
|
---|
580 | }
|
---|
581 |
|
---|
582 | req->recv_fn = recv_fn;
|
---|
583 | req->conn_settings = settings;
|
---|
584 | req->callback_arg = callback_arg;
|
---|
585 |
|
---|
586 | *connection = req;
|
---|
587 | return ERR_OK;
|
---|
588 | }
|
---|
589 |
|
---|
590 | /**
|
---|
591 | * Initialize the connection struct
|
---|
592 | */
|
---|
593 | static err_t
|
---|
594 | httpc_init_connection(httpc_state_t **connection, const httpc_connection_t *settings, const char* server_name,
|
---|
595 | u16_t server_port, const char* uri, altcp_recv_fn recv_fn, void* callback_arg)
|
---|
596 | {
|
---|
597 | return httpc_init_connection_common(connection, settings, server_name, server_port, uri, recv_fn, callback_arg, 1);
|
---|
598 | }
|
---|
599 |
|
---|
600 |
|
---|
601 | /**
|
---|
602 | * Initialize the connection struct (from IP address)
|
---|
603 | */
|
---|
604 | static err_t
|
---|
605 | httpc_init_connection_addr(httpc_state_t **connection, const httpc_connection_t *settings,
|
---|
606 | const ip_addr_t* server_addr, u16_t server_port, const char* uri,
|
---|
607 | altcp_recv_fn recv_fn, void* callback_arg)
|
---|
608 | {
|
---|
609 | char *server_addr_str = ipaddr_ntoa(server_addr);
|
---|
610 | if (server_addr_str == NULL) {
|
---|
611 | return ERR_VAL;
|
---|
612 | }
|
---|
613 | return httpc_init_connection_common(connection, settings, server_addr_str, server_port, uri,
|
---|
614 | recv_fn, callback_arg, 1);
|
---|
615 | }
|
---|
616 |
|
---|
617 | /**
|
---|
618 | * @ingroup httpc
|
---|
619 | * HTTP client API: get a file by passing server IP address
|
---|
620 | *
|
---|
621 | * @param server_addr IP address of the server to connect
|
---|
622 | * @param port tcp port of the server
|
---|
623 | * @param uri uri to get from the server, remember leading "/"!
|
---|
624 | * @param settings connection settings (callbacks, proxy, etc.)
|
---|
625 | * @param recv_fn the http body (not the headers) are passed to this callback
|
---|
626 | * @param callback_arg argument passed to all the callbacks
|
---|
627 | * @param connection retreives the connection handle (to match in callbacks)
|
---|
628 | * @return ERR_OK if starting the request succeeds (callback_fn will be called later)
|
---|
629 | * or an error code
|
---|
630 | */
|
---|
631 | err_t
|
---|
632 | httpc_get_file(const ip_addr_t* server_addr, u16_t port, const char* uri, const httpc_connection_t *settings,
|
---|
633 | altcp_recv_fn recv_fn, void* callback_arg, httpc_state_t **connection)
|
---|
634 | {
|
---|
635 | err_t err;
|
---|
636 | httpc_state_t* req;
|
---|
637 |
|
---|
638 | LWIP_ERROR("invalid parameters", (server_addr != NULL) && (uri != NULL) && (recv_fn != NULL), return ERR_ARG;);
|
---|
639 |
|
---|
640 | err = httpc_init_connection_addr(&req, settings, server_addr, port,
|
---|
641 | uri, recv_fn, callback_arg);
|
---|
642 | if (err != ERR_OK) {
|
---|
643 | return err;
|
---|
644 | }
|
---|
645 |
|
---|
646 | if (settings->use_proxy) {
|
---|
647 | err = httpc_get_internal_addr(req, &settings->proxy_addr);
|
---|
648 | } else {
|
---|
649 | err = httpc_get_internal_addr(req, server_addr);
|
---|
650 | }
|
---|
651 | if(err != ERR_OK) {
|
---|
652 | httpc_free_state(req);
|
---|
653 | return err;
|
---|
654 | }
|
---|
655 |
|
---|
656 | if (connection != NULL) {
|
---|
657 | *connection = req;
|
---|
658 | }
|
---|
659 | return ERR_OK;
|
---|
660 | }
|
---|
661 |
|
---|
662 | /**
|
---|
663 | * @ingroup httpc
|
---|
664 | * HTTP client API: get a file by passing server name as string (DNS name or IP address string)
|
---|
665 | *
|
---|
666 | * @param server_name server name as string (DNS name or IP address string)
|
---|
667 | * @param port tcp port of the server
|
---|
668 | * @param uri uri to get from the server, remember leading "/"!
|
---|
669 | * @param settings connection settings (callbacks, proxy, etc.)
|
---|
670 | * @param recv_fn the http body (not the headers) are passed to this callback
|
---|
671 | * @param callback_arg argument passed to all the callbacks
|
---|
672 | * @param connection retreives the connection handle (to match in callbacks)
|
---|
673 | * @return ERR_OK if starting the request succeeds (callback_fn will be called later)
|
---|
674 | * or an error code
|
---|
675 | */
|
---|
676 | err_t
|
---|
677 | httpc_get_file_dns(const char* server_name, u16_t port, const char* uri, const httpc_connection_t *settings,
|
---|
678 | altcp_recv_fn recv_fn, void* callback_arg, httpc_state_t **connection)
|
---|
679 | {
|
---|
680 | err_t err;
|
---|
681 | httpc_state_t* req;
|
---|
682 |
|
---|
683 | LWIP_ERROR("invalid parameters", (server_name != NULL) && (uri != NULL) && (recv_fn != NULL), return ERR_ARG;);
|
---|
684 |
|
---|
685 | err = httpc_init_connection(&req, settings, server_name, port, uri, recv_fn, callback_arg);
|
---|
686 | if (err != ERR_OK) {
|
---|
687 | return err;
|
---|
688 | }
|
---|
689 |
|
---|
690 | if (settings->use_proxy) {
|
---|
691 | err = httpc_get_internal_addr(req, &settings->proxy_addr);
|
---|
692 | } else {
|
---|
693 | err = httpc_get_internal_dns(req, server_name);
|
---|
694 | }
|
---|
695 | if(err != ERR_OK) {
|
---|
696 | httpc_free_state(req);
|
---|
697 | return err;
|
---|
698 | }
|
---|
699 |
|
---|
700 | if (connection != NULL) {
|
---|
701 | *connection = req;
|
---|
702 | }
|
---|
703 | return ERR_OK;
|
---|
704 | }
|
---|
705 |
|
---|
706 | #if LWIP_HTTPC_HAVE_FILE_IO
|
---|
707 | /* Implementation to disk via fopen/fwrite/fclose follows */
|
---|
708 |
|
---|
709 | typedef struct _httpc_filestate
|
---|
710 | {
|
---|
711 | const char* local_file_name;
|
---|
712 | FILE *file;
|
---|
713 | httpc_connection_t settings;
|
---|
714 | const httpc_connection_t *client_settings;
|
---|
715 | void *callback_arg;
|
---|
716 | } httpc_filestate_t;
|
---|
717 |
|
---|
718 | static void httpc_fs_result(void *arg, httpc_result_t httpc_result, u32_t rx_content_len,
|
---|
719 | u32_t srv_res, err_t err);
|
---|
720 |
|
---|
721 | /** Initalize http client state for download to file system */
|
---|
722 | static err_t
|
---|
723 | httpc_fs_init(httpc_filestate_t **filestate_out, const char* local_file_name,
|
---|
724 | const httpc_connection_t *settings, void* callback_arg)
|
---|
725 | {
|
---|
726 | httpc_filestate_t *filestate;
|
---|
727 | size_t file_len, alloc_len;
|
---|
728 | FILE *f;
|
---|
729 |
|
---|
730 | file_len = strlen(local_file_name);
|
---|
731 | alloc_len = sizeof(httpc_filestate_t) + file_len + 1;
|
---|
732 |
|
---|
733 | filestate = (httpc_filestate_t *)mem_malloc((mem_size_t)alloc_len);
|
---|
734 | if (filestate == NULL) {
|
---|
735 | return ERR_MEM;
|
---|
736 | }
|
---|
737 | memset(filestate, 0, sizeof(httpc_filestate_t));
|
---|
738 | filestate->local_file_name = (const char *)(filestate + 1);
|
---|
739 | memcpy((char *)(filestate + 1), local_file_name, file_len + 1);
|
---|
740 | filestate->file = NULL;
|
---|
741 | filestate->client_settings = settings;
|
---|
742 | filestate->callback_arg = callback_arg;
|
---|
743 | /* copy client settings but override result callback */
|
---|
744 | memcpy(&filestate->settings, settings, sizeof(httpc_connection_t));
|
---|
745 | filestate->settings.result_fn = httpc_fs_result;
|
---|
746 |
|
---|
747 | f = fopen(local_file_name, "wb");
|
---|
748 | if(f == NULL) {
|
---|
749 | /* could not open file */
|
---|
750 | mem_free(filestate);
|
---|
751 | return ERR_VAL;
|
---|
752 | }
|
---|
753 | filestate->file = f;
|
---|
754 | *filestate_out = filestate;
|
---|
755 | return ERR_OK;
|
---|
756 | }
|
---|
757 |
|
---|
758 | /** Free http client state for download to file system */
|
---|
759 | static void
|
---|
760 | httpc_fs_free(httpc_filestate_t *filestate)
|
---|
761 | {
|
---|
762 | if (filestate != NULL) {
|
---|
763 | if (filestate->file != NULL) {
|
---|
764 | fclose(filestate->file);
|
---|
765 | filestate->file = NULL;
|
---|
766 | }
|
---|
767 | mem_free(filestate);
|
---|
768 | }
|
---|
769 | }
|
---|
770 |
|
---|
771 | /** Connection closed (success or error) */
|
---|
772 | static void
|
---|
773 | httpc_fs_result(void *arg, httpc_result_t httpc_result, u32_t rx_content_len,
|
---|
774 | u32_t srv_res, err_t err)
|
---|
775 | {
|
---|
776 | httpc_filestate_t *filestate = (httpc_filestate_t *)arg;
|
---|
777 | if (filestate != NULL) {
|
---|
778 | if (filestate->client_settings->result_fn != NULL) {
|
---|
779 | filestate->client_settings->result_fn(filestate->callback_arg, httpc_result, rx_content_len,
|
---|
780 | srv_res, err);
|
---|
781 | }
|
---|
782 | httpc_fs_free(filestate);
|
---|
783 | }
|
---|
784 | }
|
---|
785 |
|
---|
786 | /** tcp recv callback */
|
---|
787 | static err_t
|
---|
788 | httpc_fs_tcp_recv(void *arg, struct altcp_pcb *pcb, struct pbuf *p, err_t err)
|
---|
789 | {
|
---|
790 | httpc_filestate_t *filestate = (httpc_filestate_t*)arg;
|
---|
791 | struct pbuf* q;
|
---|
792 | LWIP_UNUSED_ARG(err);
|
---|
793 |
|
---|
794 | LWIP_ASSERT("p != NULL", p != NULL);
|
---|
795 |
|
---|
796 | for (q = p; q != NULL; q = q->next) {
|
---|
797 | fwrite(q->payload, 1, q->len, filestate->file);
|
---|
798 | }
|
---|
799 | altcp_recved(pcb, p->tot_len);
|
---|
800 | pbuf_free(p);
|
---|
801 | return ERR_OK;
|
---|
802 | }
|
---|
803 |
|
---|
804 | /**
|
---|
805 | * @ingroup httpc
|
---|
806 | * HTTP client API: get a file to disk by passing server IP address
|
---|
807 | *
|
---|
808 | * @param server_addr IP address of the server to connect
|
---|
809 | * @param port tcp port of the server
|
---|
810 | * @param uri uri to get from the server, remember leading "/"!
|
---|
811 | * @param settings connection settings (callbacks, proxy, etc.)
|
---|
812 | * @param callback_arg argument passed to all the callbacks
|
---|
813 | * @param connection retreives the connection handle (to match in callbacks)
|
---|
814 | * @return ERR_OK if starting the request succeeds (callback_fn will be called later)
|
---|
815 | * or an error code
|
---|
816 | */
|
---|
817 | err_t
|
---|
818 | httpc_get_file_to_disk(const ip_addr_t* server_addr, u16_t port, const char* uri, const httpc_connection_t *settings,
|
---|
819 | void* callback_arg, const char* local_file_name, httpc_state_t **connection)
|
---|
820 | {
|
---|
821 | err_t err;
|
---|
822 | httpc_state_t* req;
|
---|
823 | httpc_filestate_t *filestate;
|
---|
824 |
|
---|
825 | LWIP_ERROR("invalid parameters", (server_addr != NULL) && (uri != NULL) && (local_file_name != NULL), return ERR_ARG;);
|
---|
826 |
|
---|
827 | err = httpc_fs_init(&filestate, local_file_name, settings, callback_arg);
|
---|
828 | if (err != ERR_OK) {
|
---|
829 | return err;
|
---|
830 | }
|
---|
831 |
|
---|
832 | err = httpc_init_connection_addr(&req, &filestate->settings, server_addr, port,
|
---|
833 | uri, httpc_fs_tcp_recv, filestate);
|
---|
834 | if (err != ERR_OK) {
|
---|
835 | httpc_fs_free(filestate);
|
---|
836 | return err;
|
---|
837 | }
|
---|
838 |
|
---|
839 | if (settings->use_proxy) {
|
---|
840 | err = httpc_get_internal_addr(req, &settings->proxy_addr);
|
---|
841 | } else {
|
---|
842 | err = httpc_get_internal_addr(req, server_addr);
|
---|
843 | }
|
---|
844 | if(err != ERR_OK) {
|
---|
845 | httpc_fs_free(filestate);
|
---|
846 | httpc_free_state(req);
|
---|
847 | return err;
|
---|
848 | }
|
---|
849 |
|
---|
850 | if (connection != NULL) {
|
---|
851 | *connection = req;
|
---|
852 | }
|
---|
853 | return ERR_OK;
|
---|
854 | }
|
---|
855 |
|
---|
856 | /**
|
---|
857 | * @ingroup httpc
|
---|
858 | * HTTP client API: get a file to disk by passing server name as string (DNS name or IP address string)
|
---|
859 | *
|
---|
860 | * @param server_name server name as string (DNS name or IP address string)
|
---|
861 | * @param port tcp port of the server
|
---|
862 | * @param uri uri to get from the server, remember leading "/"!
|
---|
863 | * @param settings connection settings (callbacks, proxy, etc.)
|
---|
864 | * @param callback_arg argument passed to all the callbacks
|
---|
865 | * @param connection retreives the connection handle (to match in callbacks)
|
---|
866 | * @return ERR_OK if starting the request succeeds (callback_fn will be called later)
|
---|
867 | * or an error code
|
---|
868 | */
|
---|
869 | err_t
|
---|
870 | httpc_get_file_dns_to_disk(const char* server_name, u16_t port, const char* uri, const httpc_connection_t *settings,
|
---|
871 | void* callback_arg, const char* local_file_name, httpc_state_t **connection)
|
---|
872 | {
|
---|
873 | err_t err;
|
---|
874 | httpc_state_t* req;
|
---|
875 | httpc_filestate_t *filestate;
|
---|
876 |
|
---|
877 | LWIP_ERROR("invalid parameters", (server_name != NULL) && (uri != NULL) && (local_file_name != NULL), return ERR_ARG;);
|
---|
878 |
|
---|
879 | err = httpc_fs_init(&filestate, local_file_name, settings, callback_arg);
|
---|
880 | if (err != ERR_OK) {
|
---|
881 | return err;
|
---|
882 | }
|
---|
883 |
|
---|
884 | err = httpc_init_connection(&req, &filestate->settings, server_name, port,
|
---|
885 | uri, httpc_fs_tcp_recv, filestate);
|
---|
886 | if (err != ERR_OK) {
|
---|
887 | httpc_fs_free(filestate);
|
---|
888 | return err;
|
---|
889 | }
|
---|
890 |
|
---|
891 | if (settings->use_proxy) {
|
---|
892 | err = httpc_get_internal_addr(req, &settings->proxy_addr);
|
---|
893 | } else {
|
---|
894 | err = httpc_get_internal_dns(req, server_name);
|
---|
895 | }
|
---|
896 | if(err != ERR_OK) {
|
---|
897 | httpc_fs_free(filestate);
|
---|
898 | httpc_free_state(req);
|
---|
899 | return err;
|
---|
900 | }
|
---|
901 |
|
---|
902 | if (connection != NULL) {
|
---|
903 | *connection = req;
|
---|
904 | }
|
---|
905 | return ERR_OK;
|
---|
906 | }
|
---|
907 | #endif /* LWIP_HTTPC_HAVE_FILE_IO */
|
---|
908 |
|
---|
909 | #endif /* LWIP_TCP && LWIP_CALLBACK_API */
|
---|