/**
 * @file ssltest.c
 * A program to grab a page from a web server running https
 * to demonstrate a problem in curl having to do with buffer
 * size and the size of the file being received..
 *
 * @author David Byron (dbyron@everdreamcorp.com)
 */

#include <assert.h>
#include <errno.h>
#include <openssl/ssl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>

#define ASSERT  assert

#define RETVAL_SUCCESS  0
#define RETVAL_ERROR    1

/**
 * This document causes problems for curl with the normal
 * buffer size it uses.
 */
#define TEST_HOST       "curl-http-ntlm.everdream.net"
#define TEST_GET        "/curltest-no-auth/WeirdBug.xml"
#define TEST_PORT       443

/**
 * This is the magic buffer size that causes problems
 * with the above document
 */
#define BUFFER_SIZE     16383

struct _ssl_info
{
    SSL_CTX     *ctx;
    SSL_METHOD  *meth;
    SSL         *ssl;
    int         sockfd;
};

static int read_response ( void );
static int send_request ( void );
static void ssl_cleanup ( void );
static int ssl_connect ( void );
static int ssl_init ( void );

static struct _ssl_info ssl_info;
static int              s_initialized = 0;

/**
 * Entry point of ssl test program
 *
 * @param argc number of invocation arguments
 * @param argv invocation arguments
 *
 * @retval RETVAL_SUCCESS success
 * @retval RETVAL_ERROR internal error
 */
int
main ( int      argc,
       char     **argv )
{
    if (!ssl_init())
    {
        ssl_cleanup();
        return RETVAL_ERROR;
    }

    /*
    ** Create the socket and connect to it
    */
    if (!ssl_connect())
    {
        ssl_cleanup();
        return RETVAL_ERROR;
    }

    /*
    ** Send our request
    */
    if (!send_request())
    {
        ssl_cleanup();
        return RETVAL_ERROR;
    }

    /*
    ** Read the response
    */
    if (!read_response())
    {
        ssl_cleanup();
        return RETVAL_ERROR;
    }

    ssl_cleanup();
    return RETVAL_SUCCESS;
}

/**
 * Read the response from the server
 *
 * @retval 0 error reading response
 * @retval 1 response successfully read
 */
static int
read_response ( void )
{
    static char buffer[BUFFER_SIZE];
    int         done;
    int         num_read;
    int         ssl_err;

    ASSERT(s_initialized);
    ASSERT(ssl_info.ssl);

    done = 0;
    while (!done)
    {
        memset(buffer,0,sizeof(buffer));
        num_read = SSL_read(ssl_info.ssl,buffer,sizeof(buffer));
        ssl_err = SSL_get_error(ssl_info.ssl,num_read);
        switch (ssl_err) {
        case SSL_ERROR_NONE:
            /*
            ** We read some bytes.  Dump them to the console.
            */
            fwrite(buffer,1,num_read,stdout);
            break;
        case SSL_ERROR_ZERO_RETURN:
            /*
            ** a normal closed socket
            */
            done = 1;
            break;
        case SSL_ERROR_WANT_READ:
            /*
            ** We need to read again
            */
            break;
        default:
            /*
            ** Something's wrong
            */
            fprintf(stderr,"%s: SSL_read failed(%d), %d bytes "
                    "read (%s)\n",__FUNCTION__,ssl_err,num_read,
                    ERR_error_string(ERR_get_error(),NULL));
            done = 1;
            break;
        }
    }

    return 1;
}

/**
 * Send a GET request just like curl does.
 *
 * @retval 0 error sending request
 * @retval 1 request sent successfully
 */
static int
send_request ( void )
{
    int         num_written;
    char        *request_buf;
    size_t      request_len;

    ASSERT(s_initialized);
    ASSERT(ssl_info.ssl);

    request_buf = NULL;
    if (asprintf(&request_buf,
                 "GET %s HTTP/1.1\r\n"
                 "User-Agent: curl/7.13.1-CVS (1686-pc-cygwin) libcurl/7.13.1-CVS OpenSSL/0.9.7e zlib/1.2.2\r\n"
                 "Host: %s\r\n"
                 "Pragma: no-cache\r\n"
                 "Accept: */*\r\n\r\n",TEST_GET,TEST_HOST) < 0)
    {
        fprintf(stderr,"%s: can't build GET request\n",
                __FUNCTION__);
        request_buf = NULL;
        return 0;
    }

    fprintf(stderr,"sending %s\n",request_buf);

    request_len = strlen(request_buf);
    num_written = SSL_write(ssl_info.ssl,request_buf,request_len);

    free(request_buf);
    request_buf = NULL;

    if (num_written <= 0)
    {
        fprintf(stderr,"%s: SSL_write failed: %s\n",__FUNCTION__,
               ERR_error_string(ERR_get_error(),NULL));
        return 0;
    }

    if (num_written < request_len)
    {
        fprintf(stderr,"%s: SSL_write incomplete\n",__FUNCTION__);
        return 0;
    }

    return 1;
}

/**
 * Clean up all of our resources
 */
static void
ssl_cleanup ( void )
{
    if (ssl_info.sockfd > 0)
    {
        close(ssl_info.sockfd);
        ssl_info.sockfd = 0;
    }

    ERR_free_strings();
    EVP_cleanup();
}

/**
 * Create a socket and tell SSL about it
 *
 * @retval 0 error creating the socket
 * @retval 1 socket created successfully
 */
static int
ssl_connect ( void )
{
    struct sockaddr_in  addr;
    struct hostent      *hp;

    ASSERT(s_initialized);
    ASSERT(ssl_info.ssl == NULL);
    ASSERT(ssl_info.ctx);

    /*
    ** Create the socket level connection
    */
    ssl_info.sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if (ssl_info.sockfd < 0)
    {
        fprintf(stderr,"%s: socket() failed\n",__FUNCTION__);
        return 0;
    }

    /*
    ** Deal with building the address structure
    */
    hp = gethostbyname(TEST_HOST);
    if (!hp)
    {
        fprintf(stderr,"%s: gethostbyname(%s) failed (%d)",
                __FUNCTION__,TEST_HOST,h_errno);
        return 0;
    }

    memset(&addr,0,sizeof(addr));
    addr.sin_family = AF_INET;
    ASSERT(hp->h_addr);
    addr.sin_addr = *(struct in_addr *)hp->h_addr;
    addr.sin_port = htons(TEST_PORT);

    /*
    ** And connect
    */
    if (connect(ssl_info.sockfd,(struct sockaddr *)&addr,
                sizeof(addr)) < 0)
    {
        fprintf(stderr,"%s: connect failed (%d)\n",__FUNCTION__,
                errno);
        return 0;
    }

    /*
    ** Now deal with SSL over the connection
    */
    ssl_info.ssl = SSL_new(ssl_info.ctx);
    if (!ssl_info.ssl)
    {
        fprintf(stderr,"%s: SSL_new failed\n",__FUNCTION__);
        return 0;
    }

    SSL_set_connect_state(ssl_info.ssl);

    if (!SSL_set_fd(ssl_info.ssl,ssl_info.sockfd))
    {
        fprintf(stderr,"%s: SSL_set_fd failed: %s",__FUNCTION__,
               ERR_error_string(ERR_get_error(),NULL));
        return 0;
    }

    if (SSL_connect(ssl_info.ssl) <= 0)
    {
        fprintf(stderr,"%s: SSL_connect failed: %s",__FUNCTION__,
               ERR_error_string(ERR_get_error(),NULL));
        return 0;
    }

    fprintf(stderr,"%s: SSL connection using %s\n",__FUNCTION__,
            SSL_get_cipher(ssl_info.ssl));

    return 1;
}

/**
 * Initialize SSL
 *
 * @retval 0 error initializing SSL
 * @retval 1 SSL initialized successfully
 */
static int
ssl_init ( void )
{
    /*
    ** If we're already initialized, don't do it again.
    */
    if (s_initialized == 1)
    {
        return 1;
    }

    memset(&ssl_info,0,sizeof(ssl_info));

    if (!SSL_library_init())
    {
        fprintf(stderr,"%s: SSL_Library_init failed\n",
                __FUNCTION__);
        return 0;
    }

    ssl_info.meth = SSLv23_client_method();
    if (!ssl_info.meth)
    {
        fprintf(stderr,"%s: SSLv23_client_method failed\n",
                __FUNCTION__);
        return 0;

    }

    ssl_info.ctx = SSL_CTX_new(ssl_info.meth);
    if (!ssl_info.ctx)
    {
        fprintf(stderr,"%s: SSL_CTX_new failed\n",__FUNCTION__);
        return 0;
    }

    /*
    ** Ignore the return value because we don't care what
    ** option bits are set
    */
    (void)SSL_CTX_set_options(ssl_info.ctx,SSL_OP_ALL);

    s_initialized = 1;

    return 1;
}
