/* This test case is based on several cURL example programs, adapted to show
   the "no timeout" issue by using the curl_multi_socket_action() interface.
   It's intended to be as simple as possible to show the issue, not to be a
   robust or efficient implementation.


   This program expects to connect to the following CGI script running under
   Apache 2.2:

   
#!/usr/bin/perl -t
#
# Iff $QUERY_STRING is of the form "timeout=N", where N is a decimal integer,
# then when an Authorization headers exists in the request, this will sleep
# N seconds before returning the 200 OK response.
#
use strict;
use warnings;
use MIME::Base64;

# Do we have an Authorization header?
my $header = $ENV{HTTP_AUTHORIZATION} || '';
if($header eq '') {
  # No, so issue a challenge.
  print("Status: 401\r\n");
  print("WWW-Authenticate: Digest ",
        "realm=\"test\", ",
        "qop=\"auth\", ",
        "nonce=\"thisISaNONCE\"\r\n");
  print("X-This-Is-The-401: yes\r\n");
  print("\r\n");
  print("This is the 401.\n");
  exit(0);
}

# Yes, they have passed the AUTHORIZATION header.
#
# If there's a timeout, wait for it.
my $request = $ENV{QUERY_STRING} || '';
if($request =~ /^timeout=(\d+)$/) {
  sleep($1);
}

# Echo their header back to them.
print("Content-Type: text/plain\r\n\r\n$header");
exit(0);


   The following Apache configuration snippet allows the CGI to run and access
   to the Authorization header:

<Directory /var/www/>
    AllowOverride None
    Options ExecCGI -MultiViews +SymLinksIfOwnerMatch
    Order allow,deny
    Allow from all

    # Run all of the files as CGIs.
    <Files "*">
        SetHandler cgi-script
    </Files>

    # Expose the Authorization header to the CGIs.
    RewriteEngine on
    RewriteCond %{HTTP:Authorization} ^(.*)
    RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
</Directory>

*/   

#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <assert.h>
#include <errno.h>

#include <curl/curl.h>

/* The URL of the test CGI script, minus query arguments. */
#define TEST_URL "http://server/test.cgi"

/* Enough room for some test fds */
struct pollfd fds[100];

/* The number of fds we are using in the array. */
int fd_count = 0;

/* Called when cURL changes the fd list */
static int sock_cb(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp)
{
  int i;

  /* Convert events */
  short events = 0;
  switch(what) {
  case CURL_POLL_NONE: events = 0; break;
  case CURL_POLL_IN: events = POLLIN; break;
  case CURL_POLL_OUT: events = POLLOUT; break;
  case CURL_POLL_INOUT: events = POLLIN | POLLOUT; break;
  case CURL_POLL_REMOVE: /* We'll remove */ break;
  }

  /* Find */
  for(i = 0; i < fd_count; ++i) {
    if(fds[i].fd == s) {
      /* Remove? */
      if(what == CURL_POLL_REMOVE) {
        printf("Remove fd=%d\n", s);
        memcpy(fds + i + 1, fds + i, sizeof(fds[0]) * fd_count - i - 1);
        --fd_count;
        return 0;
      }
      /* Change */
      printf("Change fd=%d events=%x\n", s, events);
      fds[i].events = events;
      return 0;
    }
  }
  /* Add */
  assert(i < sizeof(fds) / sizeof(fds[0]));
  printf("Add fd=%d events=%x\n", s, events);
  fds[i].fd = s;
  fds[i].events = events;
  ++fd_count;
  return 0;
}

/* poll() timeout */
long timeout = -1;

/* Called when cURL changes the timeout */
static int multi_timer_cb(CURLM *multi, long timeout_ms, void *ignored)
{
  timeout = timeout_ms;
  printf("Timeout changed to %ld ms\n", timeout);
  return 0;
}

/*
 * Demonstrate the issue.
 */
int main(void)
{
  CURL *http_handle;
  CURLM *multi_handle;
  int remaining;

  curl_global_init(CURL_GLOBAL_DEFAULT);

  http_handle = curl_easy_init();

  /* Create the test request for digest auth.  The server will respond with a
     401 and cURL will retry with the credentials but then the server will not
     respond to that second request (for 1000 seconds) to demonstrate that
     cURL doesn't detect the timeout. */
  curl_easy_setopt(http_handle, CURLOPT_URL, TEST_URL "?timeout=1000");
  curl_easy_setopt(http_handle, CURLOPT_VERBOSE, 1L);
  curl_easy_setopt(http_handle, CURLOPT_USERNAME, "user");
  curl_easy_setopt(http_handle, CURLOPT_PASSWORD, "password");
  curl_easy_setopt(http_handle, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);

  /* Set a short (but not too short) timeout to demonstrate the error. */
  curl_easy_setopt(http_handle, CURLOPT_TIMEOUT_MS, 5000L);
  
  /* Create the multi */
  multi_handle = curl_multi_init();

  /* Set up for curl_multi_socket_action() use */
  curl_multi_setopt(multi_handle, CURLMOPT_SOCKETFUNCTION, sock_cb);
  curl_multi_setopt(multi_handle, CURLMOPT_TIMERFUNCTION, multi_timer_cb);

  /* add the individual transfers */
  curl_multi_add_handle(multi_handle, http_handle);

  /* loop */
  do {
    int i;
    const int actual_fd_count = fd_count;
    struct pollfd actual[100];

    /* To keep the example logic simple, just copy everything before we call
       poll() so the callbacks can change the array however they want. */
    memcpy(actual, fds, sizeof(fds[0]) * fd_count);
    
    printf("poll() on %d fds for %ld ms\n", actual_fd_count, timeout);
    const int result = poll(actual, actual_fd_count, timeout);
    if(result == -1 && errno != EINTR) {
      printf("poll() fails: %s\n", strerror(errno));
      return 1;
    }

    /* dispatch timeout */
    if(result == 0) {
      printf("call curl_multi_socket_action() for timeout\n");
      const CURLMcode result =
        curl_multi_socket_action(multi_handle, CURL_SOCKET_TIMEOUT, 0,
                                 &remaining);
      if(result != CURLM_OK) {
        printf("curl_multi_socket_action() timeout failed: %d\n", result);
        return 2;
      }
    }
    /* dispatch events */
    else {
      for(i = 0; i < actual_fd_count; ++i) {
        if(actual[i].revents) {
          const int fd = actual[i].fd;
          int mask = 0;
          if(actual[i].revents & POLLIN) {
            mask |= CURL_CSELECT_IN;
          }
          if(actual[i].revents & POLLOUT) {
            mask |= CURL_CSELECT_OUT;
          }
          if(actual[i].revents & (POLLHUP | POLLERR)) {
            mask |= CURL_CSELECT_ERR;
          }
          printf("call curl_multi_socket_action() fd=%d revents=%x\n", fd,
                 actual[i].revents);
          const CURLMcode result =
            curl_multi_socket_action(multi_handle, fd, mask, &remaining);
          if(result != CURLM_OK) {
            printf("curl_multi_socket_action() fd=%d failed: %d\n", fd, result);
            return 3;
          }
        }
      }
    }
  } while(remaining);
  
  curl_multi_remove_handle(multi_handle, http_handle);

  curl_easy_cleanup(http_handle);

  curl_multi_cleanup(multi_handle);

  curl_global_cleanup();

  return 0;
}

