Hi friends,
Please find the source code and use examples of functions curl_fopen()
and curl_fdopen() in attachement.
These allow associating an URL or a curl handle with a standard C FILE
pointer and use the standard C IO API on it.
There is a built-in CRLF conversion feature and a no-wait mode.
I wrote them for fun and as a proof of concept, so there is no doc. Look
at the prototypes and examples.
I think it can be useful for the community or as a base of a
non-callback API for our project, but I don't really know what to do it
with now that it mostly works! May be a curl doc example?
You will find use examples embedded within the source: simple download,
mail sending, copy a handle to another, mixing protocols and even using
an handle as the data source for another one.
Please feel free to use it at your convenience.
Now, the main drawbacks:
- It uses the GNU-specific function fopencookie(): there is no support
for other environment yet. An equivalent facility exists in BSD
(funopen), but I did not find one for other systems.
- Chunked output MUST be enabled, as we don't know the write data size
in advance.
- There is no way to get the CURL handle from the file outside the
package itself.
- A FILE-attached CURL handle is not reusable outside this context:
closing the FILE deletes it.
- Only limited seek support is available (of course!): rewind and flush
to request termination.
- By the non-callback nature of the C IO API, one must know at each time
if input or output is expected.
- Because file:// URLs do not support pausing, they are open as normal C
files.
- There's a lot of buffer copying, probably adding overhead (I did not
measure perfomance).
- As it is built outside libcurl, it has no access to the internal
structures.
- Redirection of upload/post request is not possible. This probably is
also the case for multi-pass authentication.
- FILE/curl tuned buffer size is important: better not change it!
Have fun!
Patrick
/*
* curl_fopen(), curl_fdopen(): open a curl handle as a FILE pointer.
* Patrick Monnerat, Dec 2022.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <curl/curl.h>
/* Wait timeout. */
#define WAIT_TIMEOUT_MS (60 * 60 * 1000) /* 1 hour in milliseconds. */
/* Cookie state flags. */
#define FLAG_READ (1 << 0) /* Opened for input. */
#define FLAG_WRITE (1 << 1) /* Opened for output. */
#define FLAG_BINARY (1 << 2) /* Transfered data is binary. */
#define FLAG_APPEND (1 << 3) /* FTP append data to remote file. */
#define FLAG_NOWAIT (1 << 4) /* Do not wait in curl. */
#define FLAG_CR (1 << 5) /* A CRLF is being processed. */
#define FLAG_PERFORM (1 << 6) /* Can call curl_multi_perform(). */
#define FLAG_WEOF (1 << 7) /* End of output. */
#define FLAG_DONE (1 << 8) /* Request completed. */
struct cookie {
CURL * easy; /* Request easy handle. */
CURLM * multi; /* Request multi handle. */
struct curl_slist *headers; /* Request headers. */
FILE * fp; /* The associated file structure. */
char * iptr; /* Caller's input buffer pointer. */
const char * iend; /* Input buffer end pointer. */
const char * optr; /* Caller's output data pointer. */
const char * oend; /* Output data end pointer. */
CURLcode result; /* Request completion status. */
unsigned short pausemask; /* Current request pause flags. */
unsigned short flags; /* Request state flags. */
char stdiobuf[CURL_MAX_WRITE_SIZE]; /* Standard IO buffer. */
};
static ssize_t cookie_read(void *cookie, char *buf, size_t size);
static ssize_t cookie_write(void *cookie, const char *buf, size_t size);
static int cookie_seek(void *cookie, off64_t *offset, int whence);
static int cookie_close(void *cookie);
static const cookie_io_functions_t io_funcs = {
cookie_read,
cookie_write,
cookie_seek,
cookie_close
};
static int seterror(struct cookie *p, int errcode)
{
errno = errcode;
p->fp->_flags |= _IO_ERR_SEEN;
return -1;
}
/*
* Perform a single curl step.
* Return 1 if still running, 0 if terminated, -1 if error.
*/
static int cookie_perform(struct cookie *p)
{
int nhandles;
int nqmsg;
CURLMcode result;
CURLMsg *m;
if(p->flags & FLAG_DONE)
return 0;
/* Perform one step. */
p->flags &= ~FLAG_PERFORM;
result = curl_multi_perform(p->multi, &nhandles);
switch(result) {
case CURLM_OK:
break;
case CURLM_CALL_MULTI_PERFORM:
p->flags |= FLAG_PERFORM;
break;
default: /* Error. */
return seterror(p, EIO);
}
if(nhandles)
return 1; /* Still running. */
/* Request complete. Get request result status code and check for error. */
m = curl_multi_info_read(p->multi, &nqmsg);
if(m) {
p->flags |= FLAG_DONE;
p->result = m->data.result;
}
if(p->result)
return seterror(p, EIO);
return 0; /* Request complete. */
}
/*
* Wait for a curl event.
* Return 0 if OK, else -1.
*/
static int cookie_wait(struct cookie *p)
{
CURLMcode result;
struct curl_waitfd fds;
result = curl_multi_wait(p->multi, &fds, 0, WAIT_TIMEOUT_MS, NULL);
if(result)
return seterror(p, EIO);
return 0;
}
/* Destroy cookie. */
static void destroy_cookie(struct cookie *p)
{
if(p->multi) {
if(p->easy)
curl_multi_remove_handle(p->multi, p->easy);
curl_multi_cleanup(p->multi);
}
if(p->easy)
curl_easy_cleanup(p->easy);
curl_slist_free_all(p->headers);
free(p);
}
/* Called by curl to read caller's data. */
static size_t cookie_read_callback(char *buffer, size_t size, size_t nitems,
void *userdata)
{
struct cookie *p = (struct cookie *) userdata;
size_t n = 0;
if((p->flags & (FLAG_WEOF | FLAG_WRITE)) != FLAG_WRITE)
return 0;
size *= nitems; /* Max byte count. */
if(p->optr >= p->oend || (p->pausemask & CURLPAUSE_SEND)) {
/* No input data available yet. Maybe there is still some more data to
come but this is unknown yet. Pause output. */
p->pausemask |= CURLPAUSE_SEND;
return CURL_READFUNC_PAUSE;
}
if(p->flags & FLAG_BINARY) {
/* Do not alter binary data, copy unchanged. */
n = p->oend - p->optr;
if(n > size)
n = size;
memcpy(buffer, p->optr, n);
p->optr += n;
}
else {
/* Newlines have to be mapped to CRLFs. */
while(n < size && p->optr < p->oend) {
char c = *p->optr;
if(!(p->flags & FLAG_CR) && c == '\n') {
c = '\r';
p->flags |= FLAG_CR; /* Mark CR has been inserted. */
}
else {
p->flags &= ~FLAG_CR; /* Ready for another CR insertion. */
p->optr++;
}
buffer[n++] = c;
}
}
return n;
}
/* Called by curl to write local data. */
static size_t cookie_write_callback(char *buffer, size_t size, size_t nitems,
void *userdata)
{
struct cookie *p = (struct cookie *) userdata;
size_t n = p->iend - p->iptr;
size *= nitems; /* Data byte count. */
if(!size || !(p->flags & FLAG_READ))
return size;
if(n < size)
p->pausemask |= CURLPAUSE_RECV;
if(p->pausemask & CURLPAUSE_RECV)
return CURL_WRITEFUNC_PAUSE;
if(p->flags & FLAG_BINARY) {
/* Copy binary data unchanged. */
n = size;
memcpy(p->iptr, buffer, n);
p->iptr += n;
}
else {
/* Copy mapping CRLFs to newlines. */
for(n = 0; n < size && p->iptr < p->iend;) {
char c = buffer[n++];
if(p->flags & FLAG_CR) { /* CR seen ? */
p->flags &= ~FLAG_CR;
/* If not CRLF, reinsert the deleted CR and reprocess same character. */
if(c != '\n') {
c = '\r';
n--;
}
}
else if(c == '\r') {
p->flags |= FLAG_CR;
continue;
}
*p->iptr++ = c;
}
}
return n;
}
/* Called from fread(). */
static ssize_t cookie_read(void *cookie, char *buf, size_t size)
{
struct cookie *p = (struct cookie *) cookie;
size_t n = 0;
int ret = 1;
if(p->flags & FLAG_DONE)
return 0;
/* Latch input buffer. */
p->iptr = buf;
p->iend = buf + size;
p->flags |= FLAG_WEOF; /* Output is over. */
if(p->pausemask) {
p->pausemask = 0;
curl_easy_pause(p->easy, p->pausemask);
}
/* Repeat stepping over curl until some data in input buffer. */
for(;;) {
ret = cookie_perform(p);
n = p->iptr - buf; /* Number of bytes read. */
if(ret <= 0 || n || (p->flags & FLAG_DONE))
break; /* Some data read, EOF or error. */
if(!(p->flags & FLAG_PERFORM)) {
/* Need to wait. Check if allowed. */
if(p->flags & FLAG_NOWAIT) {
ret = seterror(p, EWOULDBLOCK);
break;
}
cookie_wait(p); /* Wait for data from curl. */
}
}
/* Not ready to receive anymore. */
p->iend = p->iptr; /* Prevent getting anymore data. */
if(n || ret >= 0)
return n;
return -1;
}
/* Called from fwrite(). */
static ssize_t cookie_write(void *cookie, const char *buf, size_t size)
{
struct cookie *p = (struct cookie *) cookie;
size_t n = 0;
if((p->flags & (FLAG_WEOF | FLAG_WRITE | FLAG_DONE)) != FLAG_WRITE)
return seterror(p, EIO);
/* Latch output buffer. */
p->optr = buf;
p->oend = buf + size;
if(p->pausemask & CURLPAUSE_SEND) {
p->pausemask &= ~CURLPAUSE_SEND;
curl_easy_pause(p->easy, p->pausemask);
}
/* Loop stepping over curl to output buffered data. */
while(!ferror(p->fp)) {
cookie_perform(p);
if(p->optr >= p->oend || (p->flags & FLAG_DONE))
break; /* All data sent or request completed. */
if(!(p->flags & FLAG_PERFORM)) {
/* Need to wait. Check if allowed. */
if(p->flags & FLAG_NOWAIT) {
if(p->optr != buf) /* Some data sent? */
break;
return seterror(p, EWOULDBLOCK);
}
cookie_wait(p); /* Wait for a curl event. */
}
}
p->oend = p->optr; /* Do not send leftover. */
return p->optr - buf;
}
/* Called from fseek(). */
static int cookie_seek(void *cookie, off64_t *offset, int whence)
{
struct cookie *p = (struct cookie *) cookie;
int ret = 0;
if(*offset)
return seterror(p, EINVAL); /* No function supports a non 0 offset. */
switch(whence) {
case SEEK_SET: /* Rewind. */
/* Cancel and restart the request. */
curl_multi_remove_handle(p->multi, p->easy);
p->flags &= ~(FLAG_CR | FLAG_PERFORM | FLAG_WEOF | FLAG_DONE);
p->pausemask = 0;
p->iend = p->iptr;
p->oend = p->optr;
p->result = CURLE_OK;
curl_multi_add_handle(p->multi, p->easy);
break;
case SEEK_END: /* Terminate request discarding remaining input. */
p->flags |= FLAG_WEOF;
while(!(p->flags & FLAG_DONE)) {
p->iptr = p->stdiobuf; /* Not used at this time. */
p->iend = p->stdiobuf + sizeof(p->stdiobuf);
if(p->pausemask) {
p->pausemask = 0;
curl_easy_pause(p->easy, p->pausemask);
}
cookie_perform(p);
if(!(p->flags & FLAG_PERFORM)) {
if(p->flags & FLAG_NOWAIT) {
ret = seterror(p, EWOULDBLOCK);
break;
}
cookie_wait(p);
}
}
p->iend = p->iptr;
break;
default:
ret = seterror(p, EINVAL);
break;
}
return ret;
}
/*
* Called from fclose().
* Attempt to complete the request and close cookie.
* Always succeeds.
*/
static int cookie_close(void *cookie)
{
struct cookie *p = (struct cookie *) cookie;
/* Flush not needed: there's no pending write data and read data is
discarded. Aborts the request if still running. */
destroy_cookie(p);
return 0;
}
/* Associate a curl easy handle with a FILE pointer. Internal version. */
static FILE *cookie_fdopen(CURL *easy, const char *mode, struct cookie **cp)
{
struct cookie *p = NULL;
FILE *f = NULL;
char mymode[3];
if(cp)
*cp = NULL;
/* Check parameters. */
if(!easy || !mode) {
errno = EFAULT;
return NULL;
}
/* Create the cookie structure. */
p = (struct cookie *) calloc(1, sizeof(*p));
if(!p)
return NULL;
/* Decode requested mode. */
mymode[0] = *mode;
mymode[1] = mymode[2] = '\0';
switch(mode[0]) {
case 'a':
p->flags |= FLAG_WRITE | FLAG_APPEND;
break;
case 'r':
p->flags |= FLAG_READ;
break;
case 'w':
p->flags |= FLAG_WRITE;
break;
default:
destroy_cookie(p);
errno = EINVAL;
return NULL;
}
while(*++mode) {
switch(*mode) {
case 'b':
p->flags |= FLAG_BINARY;
break;
case '+':
p->flags |= FLAG_READ | FLAG_WRITE;
mymode[1] = '+';
break;
case 'i': /* No wait. */
p->flags |= FLAG_NOWAIT;
break;
default:
destroy_cookie(p);
errno = EINVAL;
return NULL;
}
}
/* Create the multi handle for our request. */
p->multi = curl_multi_init();
if(!p->multi) {
destroy_cookie(p);
errno = ENOMEM;
return NULL;
}
f = fopencookie(p, mymode, io_funcs);
if(!f) {
int sverrno = errno;
destroy_cookie(p);
errno = sverrno;
}
else {
p->fp = f;
/* Bind curl handle with cookie. */
p->easy = easy;
curl_multi_add_handle(p->multi, easy);
if(p->flags & FLAG_READ) {
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, cookie_write_callback);
curl_easy_setopt(easy, CURLOPT_WRITEDATA, p);
}
if(p->flags & FLAG_WRITE) {
curl_easy_setopt(easy, CURLOPT_READFUNCTION, cookie_read_callback);
curl_easy_setopt(easy, CURLOPT_READDATA, p);
}
curl_multi_add_handle(p->multi, p->easy);
setbuffer(f, p->stdiobuf, sizeof(p->stdiobuf));
if(cp)
*cp = p;
}
return f;
}
/* Associate a curl easy handle with a FILE pointer. */
FILE *curl_fdopen(CURL *easy, const char *mode)
{
return cookie_fdopen(easy, mode, NULL);
}
/* Open an URL as a FILE pointer. */
FILE *curl_fopen(const char *url, const char *mode)
{
FILE *f = NULL;
char *part;
struct cookie *p;
CURL *easy;
CURLU *u;
/* Check parameters. */
if(!url || !mode) {
errno = EFAULT;
return NULL;
}
/* If this is a file URL, use a normal fopen'ed file. */
u = curl_url();
if(!u) {
errno = ENOMEM;
return NULL;
}
if(curl_url_set(u, CURLUPART_URL, url,
CURLU_DEFAULT_SCHEME | CURLU_PATH_AS_IS)) {
curl_url_cleanup(u);
errno = EINVAL;
return NULL;
}
if(curl_url_get(u, CURLUPART_SCHEME, &part, 0)) {
curl_url_cleanup(u);
errno = ENOMEM;
return NULL;
}
if(curl_strequal(part, "file")) {
free(part);
if(curl_url_get(u, CURLUPART_PATH, &part, CURLU_URLDECODE)) {
curl_url_cleanup(u);
errno = ENOMEM;
return NULL;
}
f = fopen(part, mode);
free(part);
return f;
}
free(part);
/* Build the curl handle for the request. */
easy = curl_easy_init();
if(easy) {
curl_easy_setopt(easy, CURLOPT_URL, url);
f = cookie_fdopen(easy, mode, &p);
if(!f)
curl_easy_cleanup(easy);
else if(p->flags & FLAG_WRITE) {
p->headers = curl_slist_append(p->headers, "Transfer-Encoding: chunked");
p->headers = curl_slist_append(p->headers, "Expect:");
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, p->headers);
curl_easy_setopt(easy, CURLOPT_APPEND, p->flags & FLAG_APPEND? 1L: 0L);
if(p->flags & FLAG_READ)
curl_easy_setopt(easy, CURLOPT_POST, 1L);
else {
curl_easy_setopt(easy, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(easy, CURLOPT_NOBODY, 1L);
}
}
}
return f;
}
#ifndef NO_MAIN
/* Tune these for a real test. */
#define DOWNLOAD_FILE_URL "https://curl.se/libcurl/c/tinytest.c"
#define BIG_TEXT_URL \
"https://raw.githubusercontent.com/curl/curl/master/lib/http.c"
#define MTA_URL "smtp://localhost"
#define LDAP_URL "ldap://example.com"
#define MAIL_SENDER "Curl F. Open <curl_fo...@example.com>"
#define MAIL_RECIPIENT "Undisclosed list members <l...@example.com>"
#define WEB_SERVICE_URL "http://ws.example.com/service.php"
/* Simple curl_fopen() use: download a file and display it. */
void example1()
{
FILE *f = curl_fopen(DOWNLOAD_FILE_URL, "r");
int c;
puts("\n--- Example 1 ---");
while((c = getc(f)) != EOF)
putchar(c);
fclose(f);
}
static const char *mail_address(const char *addr)
{
if(addr || addr[strlen(addr) - 1] == '>') {
const char *a = strrchr(addr, '<');
if(a)
addr = a;
}
return addr;
}
/* Simple curl_fdopen() use: send a text e-mail. */
void example2()
{
CURL *easy = curl_easy_init();
struct curl_slist *recipients = NULL;
FILE *f;
int err;
puts("\n--- Example 2 ---");
curl_easy_setopt(easy, CURLOPT_URL, MTA_URL);
curl_easy_setopt(easy, CURLOPT_MAIL_FROM, mail_address(MAIL_SENDER));
recipients = curl_slist_append(recipients, mail_address(MAIL_RECIPIENT));
curl_easy_setopt(easy, CURLOPT_MAIL_RCPT, recipients);
curl_easy_setopt(easy, CURLOPT_UPLOAD, 1L);
f = curl_fdopen(easy, "w");
fputs("MIME-Version: 1.0\n", f);
fprintf(f, "From: %s\n", MAIL_SENDER);
fputs("Subject: This is a test message\n\n", f);
fprintf(f, "Greetings,\n\n%s\n", MAIL_SENDER);
fseek(f, 0L, SEEK_END); /* Terminate request. */
puts(ferror(f)? "Error: mail not sent": "Mail successfully sent");
fclose(f);
curl_slist_free_all(recipients);
}
/* Post data to a web service and display the response. */
void example3()
{
FILE *f = curl_fopen(WEB_SERVICE_URL, "w+");
int c;
puts("\n--- Example 3 ---");
fputs("ping data", f);
while((c = getc(f)) != EOF)
putchar(c);
fclose(f);
}
/* Send a remote file to a web service and display the service reply. */
void example4()
{
FILE *fi = curl_fopen(DOWNLOAD_FILE_URL, "r");
FILE *fo = curl_fopen(WEB_SERVICE_URL, "w+");
int c;
puts("\n--- Example 4 ---");
while((c = getc(fi)) != EOF)
putc(c, fo);
while((c = getc(fo)) != EOF)
putchar(c);
fclose(fi);
fclose(fo);
}
/* Use the output of a curl request as cascaded input to another. */
void example5()
{
FILE *fi = curl_fopen(LDAP_URL, "r");
CURL *easy = curl_easy_init();
struct curl_slist *sl = NULL;
FILE *fo;
int c;
FILE *f;
puts("\n--- Example 5 ---");
curl_easy_setopt(easy, CURLOPT_URL, WEB_SERVICE_URL);
curl_easy_setopt(easy, CURLOPT_POST, 1L);
sl = curl_slist_append(sl, "Transfer-Encoding: chunked");
sl = curl_slist_append(sl, "Expect:");
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, sl);
curl_easy_setopt(easy, CURLOPT_READFUNCTION, fread);
curl_easy_setopt(easy, CURLOPT_READDATA, fi);
curl_easy_setopt(easy, CURLOPT_SEEKFUNCTION, fseek);
curl_easy_setopt(easy, CURLOPT_SEEKDATA, fi);
fo = curl_fdopen(easy, "r");
while((c = getc(fo)) != EOF)
putchar(c);
fclose(fi);
fclose(fo);
curl_slist_free_all(sl);
}
/* Rewind example: display some lines around first occurrence of a string. */
void example6()
{
FILE *f = curl_fopen(BIG_TEXT_URL, "r");
int targetline = 0;
int c;
int line;
char buf[100];
puts("\n--- Example 6 ---");
for(line = 1; fgets(buf, sizeof(buf), f); line++) {
if(strstr(buf, "http_output_bearer")) {
targetline = line;
break;
}
}
if(!targetline)
puts("String not found");
else {
fseek(f, 0L, SEEK_SET); /* Rewind. */
for(line = 1; fgets(buf, sizeof(buf), f); line++)
if(line > targetline - 8) {
printf("%5d %s", line, buf);
if(line > targetline + 8)
break;
}
}
fclose(f);
}
int main(int argc, char **argv)
{
curl_global_init(CURL_GLOBAL_DEFAULT);
example1();
example2();
example3();
example4();
example5();
example6();
curl_global_cleanup();
return 0;
}
#endif
--
Unsubscribe: https://lists.haxx.se/listinfo/curl-library
Etiquette: https://curl.se/mail/etiquette.html