]>
Commit | Line | Data |
---|---|---|
582311d7 DDO |
1 | /* |
2 | * Copyright 1995-2020 The OpenSSL Project Authors. All Rights Reserved. | |
3 | * | |
4 | * Licensed under the Apache License 2.0 (the "License"). You may not use | |
5 | * this file except in compliance with the License. You can obtain a copy | |
6 | * in the file LICENSE in the source distribution or at | |
7 | * https://www.openssl.org/source/license.html | |
8 | */ | |
9 | ||
10 | /* Very basic HTTP server */ | |
11 | ||
12 | #if !defined(_POSIX_C_SOURCE) && defined(OPENSSL_SYS_VMS) | |
13 | /* | |
14 | * On VMS, you need to define this to get the declaration of fileno(). The | |
15 | * value 2 is to make sure no function defined in POSIX-2 is left undefined. | |
16 | */ | |
17 | # define _POSIX_C_SOURCE 2 | |
18 | #endif | |
19 | ||
dd63f9bb | 20 | #include <string.h> |
582311d7 DDO |
21 | #include <ctype.h> |
22 | #include "http_server.h" | |
23 | #include "internal/sockets.h" | |
24 | #include <openssl/err.h> | |
25 | #include <openssl/rand.h> | |
a12da5da | 26 | #include "s_apps.h" |
582311d7 | 27 | |
08073700 RB |
28 | #if defined(__TANDEM) |
29 | # if defined(OPENSSL_TANDEM_FLOSS) | |
30 | # include <floss.h(floss_fork)> | |
31 | # endif | |
32 | #endif | |
33 | ||
cc1af4db | 34 | static int verbosity = LOG_INFO; |
19f97fe6 DDO |
35 | |
36 | #define HTTP_PREFIX "HTTP/" | |
37 | #define HTTP_VERSION_PATT "1." /* allow 1.x */ | |
38 | #define HTTP_PREFIX_VERSION HTTP_PREFIX""HTTP_VERSION_PATT | |
39 | #define HTTP_1_0 HTTP_PREFIX_VERSION"0" /* "HTTP/1.0" */ | |
40 | ||
582311d7 | 41 | #ifdef HTTP_DAEMON |
19f97fe6 | 42 | |
e109aaa9 | 43 | int multi = 0; /* run multiple responder processes */ |
582311d7 | 44 | int acfd = (int) INVALID_SOCKET; |
582311d7 | 45 | |
582311d7 DDO |
46 | static int print_syslog(const char *str, size_t len, void *levPtr) |
47 | { | |
48 | int level = *(int *)levPtr; | |
49 | int ilen = len > MAXERRLEN ? MAXERRLEN : len; | |
50 | ||
51 | syslog(level, "%.*s", ilen, str); | |
52 | ||
53 | return ilen; | |
54 | } | |
55 | #endif | |
56 | ||
57 | void log_message(const char *prog, int level, const char *fmt, ...) | |
58 | { | |
59 | va_list ap; | |
60 | ||
cc1af4db DDO |
61 | if (verbosity < level) |
62 | return; | |
63 | ||
582311d7 DDO |
64 | va_start(ap, fmt); |
65 | #ifdef HTTP_DAEMON | |
66 | if (multi) { | |
67 | char buf[1024]; | |
68 | ||
69 | if (vsnprintf(buf, sizeof(buf), fmt, ap) > 0) | |
70 | syslog(level, "%s", buf); | |
cc1af4db | 71 | if (level <= LOG_ERR) |
582311d7 | 72 | ERR_print_errors_cb(print_syslog, &level); |
e109aaa9 | 73 | } else |
582311d7 | 74 | #endif |
e109aaa9 | 75 | { |
582311d7 DDO |
76 | BIO_printf(bio_err, "%s: ", prog); |
77 | BIO_vprintf(bio_err, fmt, ap); | |
78 | BIO_printf(bio_err, "\n"); | |
cc1af4db | 79 | (void)BIO_flush(bio_err); |
582311d7 DDO |
80 | } |
81 | va_end(ap); | |
82 | } | |
83 | ||
84 | #ifdef HTTP_DAEMON | |
85 | void socket_timeout(int signum) | |
86 | { | |
87 | if (acfd != (int)INVALID_SOCKET) | |
88 | (void)shutdown(acfd, SHUT_RD); | |
89 | } | |
90 | ||
91 | static void killall(int ret, pid_t *kidpids) | |
92 | { | |
93 | int i; | |
94 | ||
95 | for (i = 0; i < multi; ++i) | |
96 | if (kidpids[i] != 0) | |
97 | (void)kill(kidpids[i], SIGTERM); | |
98 | OPENSSL_free(kidpids); | |
9be5f9a8 | 99 | ossl_sleep(1000); |
582311d7 DDO |
100 | exit(ret); |
101 | } | |
102 | ||
103 | static int termsig = 0; | |
104 | ||
105 | static void noteterm(int sig) | |
106 | { | |
107 | termsig = sig; | |
108 | } | |
109 | ||
110 | /* | |
111 | * Loop spawning up to `multi` child processes, only child processes return | |
112 | * from this function. The parent process loops until receiving a termination | |
113 | * signal, kills extant children and exits without returning. | |
114 | */ | |
115 | void spawn_loop(const char *prog) | |
116 | { | |
117 | pid_t *kidpids = NULL; | |
118 | int status; | |
119 | int procs = 0; | |
120 | int i; | |
121 | ||
122 | openlog(prog, LOG_PID, LOG_DAEMON); | |
123 | ||
124 | if (setpgid(0, 0)) { | |
125 | syslog(LOG_ERR, "fatal: error detaching from parent process group: %s", | |
126 | strerror(errno)); | |
127 | exit(1); | |
128 | } | |
129 | kidpids = app_malloc(multi * sizeof(*kidpids), "child PID array"); | |
130 | for (i = 0; i < multi; ++i) | |
131 | kidpids[i] = 0; | |
132 | ||
133 | signal(SIGINT, noteterm); | |
134 | signal(SIGTERM, noteterm); | |
135 | ||
136 | while (termsig == 0) { | |
137 | pid_t fpid; | |
138 | ||
139 | /* | |
140 | * Wait for a child to replace when we're at the limit. | |
141 | * Slow down if a child exited abnormally or waitpid() < 0 | |
142 | */ | |
143 | while (termsig == 0 && procs >= multi) { | |
144 | if ((fpid = waitpid(-1, &status, 0)) > 0) { | |
145 | for (i = 0; i < procs; ++i) { | |
146 | if (kidpids[i] == fpid) { | |
147 | kidpids[i] = 0; | |
148 | --procs; | |
149 | break; | |
150 | } | |
151 | } | |
152 | if (i >= multi) { | |
153 | syslog(LOG_ERR, "fatal: internal error: " | |
154 | "no matching child slot for pid: %ld", | |
155 | (long) fpid); | |
156 | killall(1, kidpids); | |
157 | } | |
158 | if (status != 0) { | |
159 | if (WIFEXITED(status)) | |
160 | syslog(LOG_WARNING, "child process: %ld, exit status: %d", | |
161 | (long)fpid, WEXITSTATUS(status)); | |
162 | else if (WIFSIGNALED(status)) | |
163 | syslog(LOG_WARNING, "child process: %ld, term signal %d%s", | |
164 | (long)fpid, WTERMSIG(status), | |
165 | # ifdef WCOREDUMP | |
166 | WCOREDUMP(status) ? " (core dumped)" : | |
167 | # endif | |
168 | ""); | |
9be5f9a8 | 169 | ossl_sleep(1000); |
582311d7 DDO |
170 | } |
171 | break; | |
172 | } else if (errno != EINTR) { | |
173 | syslog(LOG_ERR, "fatal: waitpid(): %s", strerror(errno)); | |
174 | killall(1, kidpids); | |
175 | } | |
176 | } | |
177 | if (termsig) | |
178 | break; | |
179 | ||
180 | switch (fpid = fork()) { | |
181 | case -1: /* error */ | |
182 | /* System critically low on memory, pause and try again later */ | |
9be5f9a8 | 183 | ossl_sleep(30000); |
582311d7 DDO |
184 | break; |
185 | case 0: /* child */ | |
186 | OPENSSL_free(kidpids); | |
187 | signal(SIGINT, SIG_DFL); | |
188 | signal(SIGTERM, SIG_DFL); | |
189 | if (termsig) | |
190 | _exit(0); | |
191 | if (RAND_poll() <= 0) { | |
192 | syslog(LOG_ERR, "fatal: RAND_poll() failed"); | |
193 | _exit(1); | |
194 | } | |
195 | return; | |
196 | default: /* parent */ | |
197 | for (i = 0; i < multi; ++i) { | |
198 | if (kidpids[i] == 0) { | |
199 | kidpids[i] = fpid; | |
200 | procs++; | |
201 | break; | |
202 | } | |
203 | } | |
204 | if (i >= multi) { | |
205 | syslog(LOG_ERR, "fatal: internal error: no free child slots"); | |
206 | killall(1, kidpids); | |
207 | } | |
208 | break; | |
209 | } | |
210 | } | |
211 | ||
212 | /* The loop above can only break on termsig */ | |
213 | syslog(LOG_INFO, "terminating on signal: %d", termsig); | |
214 | killall(0, kidpids); | |
215 | } | |
216 | #endif | |
217 | ||
218 | #ifndef OPENSSL_NO_SOCK | |
219 | BIO *http_server_init_bio(const char *prog, const char *port) | |
220 | { | |
221 | BIO *acbio = NULL, *bufbio; | |
a12da5da | 222 | int asock; |
582311d7 DDO |
223 | |
224 | bufbio = BIO_new(BIO_f_buffer()); | |
225 | if (bufbio == NULL) | |
226 | goto err; | |
227 | acbio = BIO_new(BIO_s_accept()); | |
228 | if (acbio == NULL | |
229 | || BIO_set_bind_mode(acbio, BIO_BIND_REUSEADDR) < 0 | |
230 | || BIO_set_accept_port(acbio, port) < 0) { | |
231 | log_message(prog, LOG_ERR, "Error setting up accept BIO"); | |
232 | goto err; | |
233 | } | |
234 | ||
235 | BIO_set_accept_bios(acbio, bufbio); | |
236 | bufbio = NULL; | |
237 | if (BIO_do_accept(acbio) <= 0) { | |
238 | log_message(prog, LOG_ERR, "Error starting accept"); | |
239 | goto err; | |
240 | } | |
241 | ||
a12da5da RL |
242 | /* Report back what address and port are used */ |
243 | BIO_get_fd(acbio, &asock); | |
244 | if (!report_server_accept(bio_out, asock, 1)) { | |
245 | log_message(prog, LOG_ERR, "Error printing ACCEPT string"); | |
246 | goto err; | |
247 | } | |
248 | ||
582311d7 DDO |
249 | return acbio; |
250 | ||
251 | err: | |
252 | BIO_free_all(acbio); | |
253 | BIO_free(bufbio); | |
254 | return NULL; | |
255 | } | |
256 | ||
257 | /* | |
258 | * Decode %xx URL-decoding in-place. Ignores malformed sequences. | |
259 | */ | |
260 | static int urldecode(char *p) | |
261 | { | |
262 | unsigned char *out = (unsigned char *)p; | |
263 | unsigned char *save = out; | |
264 | ||
265 | for (; *p; p++) { | |
266 | if (*p != '%') { | |
267 | *out++ = *p; | |
268 | } else if (isxdigit(_UC(p[1])) && isxdigit(_UC(p[2]))) { | |
269 | /* Don't check, can't fail because of ixdigit() call. */ | |
270 | *out++ = (OPENSSL_hexchar2int(p[1]) << 4) | |
271 | | OPENSSL_hexchar2int(p[2]); | |
272 | p += 2; | |
273 | } else { | |
274 | return -1; | |
275 | } | |
276 | } | |
277 | *out = '\0'; | |
278 | return (int)(out - save); | |
279 | } | |
280 | ||
19f97fe6 DDO |
281 | /* if *pcbio != NULL, continue given connected session, else accept new */ |
282 | /* if found_keep_alive != NULL, return this way connection persistence state */ | |
582311d7 | 283 | int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq, |
5a2ba207 | 284 | char **ppath, BIO **pcbio, BIO *acbio, |
19f97fe6 DDO |
285 | int *found_keep_alive, |
286 | const char *prog, const char *port, | |
287 | int accept_get, int timeout) | |
582311d7 | 288 | { |
19f97fe6 | 289 | BIO *cbio = *pcbio, *getbio = NULL, *b64 = NULL; |
582311d7 DDO |
290 | int len; |
291 | char reqbuf[2048], inbuf[2048]; | |
5a2ba207 | 292 | char *meth, *url, *end; |
582311d7 | 293 | ASN1_VALUE *req; |
cc1af4db | 294 | int ret = 0; |
582311d7 DDO |
295 | |
296 | *preq = NULL; | |
5a2ba207 DDO |
297 | if (ppath != NULL) |
298 | *ppath = NULL; | |
582311d7 | 299 | |
19f97fe6 DDO |
300 | if (cbio == NULL) { |
301 | log_message(prog, LOG_DEBUG, | |
302 | "Awaiting new connection on port %s...", port); | |
303 | if (BIO_do_accept(acbio) <= 0) | |
304 | /* Connection loss before accept() is routine, ignore silently */ | |
305 | return ret; | |
582311d7 | 306 | |
19f97fe6 DDO |
307 | *pcbio = cbio = BIO_pop(acbio); |
308 | } else { | |
309 | log_message(prog, LOG_DEBUG, "Awaiting next request..."); | |
310 | } | |
582311d7 | 311 | if (cbio == NULL) { |
5a2ba207 | 312 | /* Cannot call http_server_send_status(cbio, ...) */ |
582311d7 DDO |
313 | ret = -1; |
314 | goto out; | |
315 | } | |
316 | ||
317 | # ifdef HTTP_DAEMON | |
318 | if (timeout > 0) { | |
319 | (void)BIO_get_fd(cbio, &acfd); | |
320 | alarm(timeout); | |
321 | } | |
322 | # endif | |
323 | ||
324 | /* Read the request line. */ | |
325 | len = BIO_gets(cbio, reqbuf, sizeof(reqbuf)); | |
cc1af4db DDO |
326 | if (len == 0) |
327 | return ret; | |
328 | ret = 1; | |
329 | if (len < 0) { | |
330 | log_message(prog, LOG_WARNING, "Request line read error"); | |
5a2ba207 | 331 | (void)http_server_send_status(cbio, 400, "Bad Request"); |
582311d7 | 332 | goto out; |
5a2ba207 | 333 | } |
cc1af4db DDO |
334 | if ((end = strchr(reqbuf, '\r')) != NULL |
335 | || (end = strchr(reqbuf, '\n')) != NULL) | |
336 | *end = '\0'; | |
337 | log_message(prog, LOG_INFO, "Received request, 1st line: %s", reqbuf); | |
582311d7 | 338 | |
5a2ba207 DDO |
339 | meth = reqbuf; |
340 | url = meth + 3; | |
341 | if ((accept_get && strncmp(meth, "GET ", 4) == 0) | |
342 | || (url++, strncmp(meth, "POST ", 5) == 0)) { | |
19f97fe6 DDO |
343 | static const char http_version_str[] = " "HTTP_PREFIX_VERSION; |
344 | static const size_t http_version_str_len = sizeof(http_version_str) - 1; | |
345 | ||
5a2ba207 DDO |
346 | /* Expecting (GET|POST) {sp} /URL {sp} HTTP/1.x */ |
347 | *(url++) = '\0'; | |
348 | while (*url == ' ') | |
349 | url++; | |
582311d7 | 350 | if (*url != '/') { |
cc1af4db | 351 | log_message(prog, LOG_WARNING, |
5a2ba207 DDO |
352 | "Invalid %s -- URL does not begin with '/': %s", |
353 | meth, url); | |
354 | (void)http_server_send_status(cbio, 400, "Bad Request"); | |
582311d7 DDO |
355 | goto out; |
356 | } | |
357 | url++; | |
358 | ||
359 | /* Splice off the HTTP version identifier. */ | |
360 | for (end = url; *end != '\0'; end++) | |
361 | if (*end == ' ') | |
362 | break; | |
19f97fe6 | 363 | if (strncmp(end, http_version_str, http_version_str_len) != 0) { |
cc1af4db | 364 | log_message(prog, LOG_WARNING, |
5a2ba207 DDO |
365 | "Invalid %s -- bad HTTP/version string: %s", |
366 | meth, end + 1); | |
367 | (void)http_server_send_status(cbio, 400, "Bad Request"); | |
582311d7 DDO |
368 | goto out; |
369 | } | |
370 | *end = '\0'; | |
19f97fe6 DDO |
371 | /* above HTTP 1.0, connection persistence is the default */ |
372 | if (found_keep_alive != NULL) | |
373 | *found_keep_alive = end[http_version_str_len] > '0'; | |
582311d7 DDO |
374 | |
375 | /*- | |
376 | * Skip "GET / HTTP..." requests often used by load-balancers. | |
377 | * 'url' was incremented above to point to the first byte *after* | |
378 | * the leading slash, so in case 'GET / ' it is now an empty string. | |
379 | */ | |
5a2ba207 DDO |
380 | if (strlen(meth) == 3 && url[0] == '\0') { |
381 | (void)http_server_send_status(cbio, 200, "OK"); | |
582311d7 | 382 | goto out; |
5a2ba207 | 383 | } |
582311d7 DDO |
384 | |
385 | len = urldecode(url); | |
5a2ba207 | 386 | if (len < 0) { |
cc1af4db | 387 | log_message(prog, LOG_WARNING, |
5a2ba207 DDO |
388 | "Invalid %s request -- bad URL encoding: %s", |
389 | meth, url); | |
390 | (void)http_server_send_status(cbio, 400, "Bad Request"); | |
582311d7 DDO |
391 | goto out; |
392 | } | |
5a2ba207 DDO |
393 | if (strlen(meth) == 3) { /* GET */ |
394 | if ((getbio = BIO_new_mem_buf(url, len)) == NULL | |
395 | || (b64 = BIO_new(BIO_f_base64())) == NULL) { | |
396 | log_message(prog, LOG_ERR, | |
397 | "Could not allocate base64 bio with size = %d", | |
398 | len); | |
399 | goto fatal; | |
400 | } | |
401 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); | |
402 | getbio = BIO_push(b64, getbio); | |
582311d7 | 403 | } |
5a2ba207 | 404 | } else { |
cc1af4db | 405 | log_message(prog, LOG_WARNING, |
19f97fe6 DDO |
406 | "HTTP request does not begin with %sPOST: %s", |
407 | accept_get ? "GET or " : "", reqbuf); | |
582311d7 | 408 | /* TODO provide better diagnosis in case client tries TLS */ |
5a2ba207 | 409 | (void)http_server_send_status(cbio, 400, "Bad Request"); |
582311d7 DDO |
410 | goto out; |
411 | } | |
412 | ||
5a2ba207 DDO |
413 | /* chop any further/duplicate leading or trailing '/' */ |
414 | while (*url == '/') | |
415 | url++; | |
416 | while (end >= url + 2 && end[-2] == '/' && end[-1] == '/') | |
417 | end--; | |
418 | *end = '\0'; | |
419 | ||
582311d7 DDO |
420 | /* Read and skip past the headers. */ |
421 | for (;;) { | |
19f97fe6 DDO |
422 | char *key, *value, *line_end = NULL; |
423 | ||
582311d7 DDO |
424 | len = BIO_gets(cbio, inbuf, sizeof(inbuf)); |
425 | if (len <= 0) { | |
19f97fe6 | 426 | log_message(prog, LOG_WARNING, "Error reading HTTP header"); |
5a2ba207 | 427 | (void)http_server_send_status(cbio, 400, "Bad Request"); |
582311d7 DDO |
428 | goto out; |
429 | } | |
19f97fe6 DDO |
430 | |
431 | if (inbuf[0] == '\r' || inbuf[0] == '\n') | |
582311d7 | 432 | break; |
19f97fe6 DDO |
433 | |
434 | key = inbuf; | |
435 | value = strchr(key, ':'); | |
cef71ebb | 436 | if (value == NULL) { |
19f97fe6 DDO |
437 | log_message(prog, LOG_WARNING, |
438 | "Error parsing HTTP header: missing ':'"); | |
439 | (void)http_server_send_status(cbio, 400, "Bad Request"); | |
440 | goto out; | |
441 | } | |
cef71ebb P |
442 | *(value++) = '\0'; |
443 | while (*value == ' ') | |
444 | value++; | |
445 | line_end = strchr(value, '\r'); | |
446 | if (line_end == NULL) { | |
447 | line_end = strchr(value, '\n'); | |
448 | if (line_end == NULL) { | |
449 | log_message(prog, LOG_WARNING, | |
450 | "Error parsing HTTP header: missing end of line"); | |
451 | (void)http_server_send_status(cbio, 400, "Bad Request"); | |
452 | goto out; | |
19f97fe6 | 453 | } |
19f97fe6 | 454 | } |
cef71ebb P |
455 | *line_end = '\0'; |
456 | /* https://tools.ietf.org/html/rfc7230#section-6.3 Persistence */ | |
457 | if (found_keep_alive != NULL && strcasecmp(key, "Connection") == 0) { | |
458 | if (strcasecmp(value, "keep-alive") == 0) | |
459 | *found_keep_alive = 1; | |
a94d62ab | 460 | else if (strcasecmp(value, "close") == 0) |
cef71ebb P |
461 | *found_keep_alive = 0; |
462 | } | |
582311d7 DDO |
463 | } |
464 | ||
465 | # ifdef HTTP_DAEMON | |
466 | /* Clear alarm before we close the client socket */ | |
467 | alarm(0); | |
468 | timeout = 0; | |
469 | # endif | |
470 | ||
471 | /* Try to read and parse request */ | |
472 | req = ASN1_item_d2i_bio(it, getbio != NULL ? getbio : cbio, NULL); | |
5a2ba207 | 473 | if (req == NULL) { |
19f97fe6 DDO |
474 | log_message(prog, LOG_WARNING, |
475 | "Error parsing DER-encoded request content"); | |
cc1af4db | 476 | (void)http_server_send_status(cbio, 400, "Bad Request"); |
5a2ba207 DDO |
477 | } else if (ppath != NULL && (*ppath = OPENSSL_strdup(url)) == NULL) { |
478 | log_message(prog, LOG_ERR, | |
24b6261e | 479 | "Out of memory allocating %zu bytes", strlen(url) + 1); |
5a2ba207 DDO |
480 | ASN1_item_free(req, it); |
481 | goto fatal; | |
482 | } | |
582311d7 DDO |
483 | |
484 | *preq = req; | |
485 | ||
486 | out: | |
487 | BIO_free_all(getbio); | |
488 | # ifdef HTTP_DAEMON | |
489 | if (timeout > 0) | |
490 | alarm(0); | |
491 | acfd = (int)INVALID_SOCKET; | |
492 | # endif | |
493 | return ret; | |
5a2ba207 DDO |
494 | |
495 | fatal: | |
496 | (void)http_server_send_status(cbio, 500, "Internal Server Error"); | |
497 | if (ppath != NULL) { | |
498 | OPENSSL_free(*ppath); | |
499 | *ppath = NULL; | |
500 | } | |
501 | BIO_free_all(cbio); | |
502 | *pcbio = NULL; | |
503 | ret = -1; | |
504 | goto out; | |
582311d7 DDO |
505 | } |
506 | ||
507 | /* assumes that cbio does not do an encoding that changes the output length */ | |
19f97fe6 DDO |
508 | int http_server_send_asn1_resp(BIO *cbio, int keep_alive, |
509 | const char *content_type, | |
582311d7 DDO |
510 | const ASN1_ITEM *it, const ASN1_VALUE *resp) |
511 | { | |
19f97fe6 DDO |
512 | int ret = BIO_printf(cbio, HTTP_1_0" 200 OK\r\n%s" |
513 | "Content-type: %s\r\n" | |
514 | "Content-Length: %d\r\n\r\n", | |
515 | keep_alive ? "Connection: keep-alive\r\n" : "", | |
516 | content_type, | |
582311d7 DDO |
517 | ASN1_item_i2d(resp, NULL, it)) > 0 |
518 | && ASN1_item_i2d_bio(it, cbio, resp) > 0; | |
519 | ||
520 | (void)BIO_flush(cbio); | |
521 | return ret; | |
522 | } | |
5a2ba207 DDO |
523 | |
524 | int http_server_send_status(BIO *cbio, int status, const char *reason) | |
525 | { | |
19f97fe6 DDO |
526 | int ret = BIO_printf(cbio, HTTP_1_0" %d %s\r\n\r\n", |
527 | /* This implicitly cancels keep-alive */ | |
528 | status, reason) > 0; | |
5a2ba207 DDO |
529 | |
530 | (void)BIO_flush(cbio); | |
531 | return ret; | |
532 | } | |
582311d7 | 533 | #endif |