[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[Libevent-users] evhttp: persistent connections over ssl with chunked transfer encoding bug?



Hi,

While working on a comet http server, I have run into a problem where
the second request on a reused (persistent) TCP connection is not
processed by evhttp. Has anyone experienced behavior like this?

I created a minimal test case [1] which demonstrates that all of the
following must be true for the bug to be triggered:
  * HTTP persistent connection
  * HTTPS/SSL enabled
  * HTTP chunked encoding (on the first request over the TCP connection)

When this situation occurs, the second request eventually times out
after HTTP_CONNECT_TIMEOUT. I have observed this behavior against the
most recent code in git at levent.git.sourceforge.net. Does anyone
have experience with this issue or any recommendations of where in the
code to begin looking for a fix?

Thanks,
-Andy

[1] main.cc is an example evhttp server that provides who handlers
[/no_chunks and /] over both SSL and clear text

http.py is an example HTTP client that creates a persistent HTTP
connection and sends two requests to each of the above handlers over a
shared TCP connection
#!/usr/bin/env python

import socket
import ssl

debug = False

configs = [
  { 'host': 'www.google.com', 'port': 80, 'url': '/', 'ssl': False, },
  { 'host': 'www.google.com', 'port': 443, 'url': '/', 'ssl': True, },
  { 'host': '127.0.0.1', 'port': 8888, 'url': '/no_chunks', 'ssl': False, },
  { 'host': '127.0.0.1', 'port': 8889, 'url': '/no_chunks', 'ssl': True, },
  { 'host': '127.0.0.1', 'port': 8888, 'url': '/', 'ssl': False, },
  { 'host': '127.0.0.1', 'port': 8889, 'url': '/', 'ssl': True, },
]

def read_until(s, until):
  rtn = ''
  while 1:
    data = s.recv(1)
    if not data:
      raise Exception('malformed req')
    rtn += data
    if rtn.endswith(until):
      break
  return rtn

def read_bytes(s, bytes):
  rtn = ''
  while 1:
    data = s.recv(1)
    if not data:
      raise Exception('malformed req')
    rtn += data
    if len(rtn) == bytes:
      break
  return rtn

def process_req(s, config):
  req = """GET %(url)s HTTP/1.1\r
Host: %(host)s:%(port)s\r
\r
""" % config
  if debug: print 'req: %s' % req
  s.sendall(req)
  chunked = None
  try:
    status = read_until(s, '\r\n')
    if debug: print 'status: %s' % status.strip()
    headers = read_until(s, '\r\n\r\n')
    if debug: print 'headers: %s' % headers.strip()
    header_list = headers.strip().split('\r\n')
    content_length = None
    for header in header_list:
      (key, value) = header.split(":", 1)
      if key.lower() == "content-length":
        content_length = int(value)
      elif key.lower() == "transfer-encoding":
        chunked = True
    if debug: print 'chunked: %s' % chunked
    if debug: print 'content_length: %s' % content_length
    if chunked:
      while True:
        chunk_size = read_until(s, '\r\n')
        if debug: print 'chunk_size: %s' % chunk_size.strip()
        chunk_size = int(chunk_size.strip(), 16)
        if chunk_size == 0:
          break
        chunk = read_bytes(s, chunk_size)
        if debug: print 'chunk: %s' % chunk
        burn = read_until(s, '\r\n')
      burn = read_until(s, '\r\n')
    else:
      message = read_bytes(s, content_length)
  except Exception as e:
    if debug: print 'REQ FAILED: %s' % e
    return (chunked, False)
  if debug: print 'REQ DONE'
  return (chunked, True)

socket.setdefaulttimeout(10)
for config in configs:
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  if config['ssl']:
    s = ssl.wrap_socket(s)
  s.connect((config['host'], config['port']))
  for i in range(2):
    (chunked, success) = process_req(s, config)
    if not success:
      break
  s.close()
  config['chunked'] = chunked
  print 'host=%(host)s ssl=%(ssl)s chunked=%(chunked)s: ' % config,
  if success:
    print 'success'
  else:
    print 'failure'
#include <event2/buffer.h>
#include <event2/bufferevent_ssl.h>
#include <event2/event.h>
#include <event2/http.h>
#include <openssl/err.h>
#include <openssl/ssl.h>

#include <string>
#include <vector>
#include <iostream>

const std::string crt_asn1_str = "XXX";
const std::string intermediate2_crt_asn1_str = "XXX";
const std::string intermediate1_crt_asn1_str = "XXX";
const std::string root_crt_asn1_str = "XXX";
const std::string key_asn1_str = "XXX";

void NonChunkHandler(struct evhttp_request *req, void */* arg */) {
  struct evbuffer *buf = evbuffer_new();
  if (!buf) {
    evhttp_send_error(req, HTTP_INTERNAL, 0);
    return;
  }
  const std::string payload = "12345";
  if (evbuffer_add(buf, payload.c_str(), payload.length()) != 0) {
    evbuffer_free(buf);
    return;
  }
  evhttp_send_reply(req, HTTP_OK, "OK", buf);
  evbuffer_free(buf);
}

void ChunkHandler(struct evhttp_request *req, void */* arg */) {
  evhttp_send_reply_start(req, HTTP_OK, "OK");
  struct evbuffer *buf = evbuffer_new();
  if (!buf) {
    evhttp_send_error(req, HTTP_INTERNAL, 0);
    return;
  }
  const std::string payload = "12345";
  if (evbuffer_add(buf, payload.c_str(), payload.length()) != 0) {
    evbuffer_free(buf);
    return;
  }
  evhttp_send_reply_chunk(req, buf);
  evbuffer_free(buf);
  evhttp_send_reply_end(req);
}

struct bufferevent *BufferEventCallback(struct event_base *base, void *arg) {
  SSL_CTX *sctx = static_cast<SSL_CTX *>(arg);
  return bufferevent_openssl_socket_new(base,
                                        -1,
                                        SSL_new(sctx),
                                        BUFFEREVENT_SSL_ACCEPTING,
                                        BEV_OPT_CLOSE_ON_FREE);
}

int main(int /* argc */, char */* argv */[]) {
  struct event_base *base_;
  struct evhttp *http_, *https_;
  struct evhttp_bound_socket *handle_, *handles_;
  SSL_CTX *sctx_;

  base_ = event_base_new();
  assert(base_ != NULL);

  http_ = evhttp_new(base_);
  assert(http_ != NULL);
  https_ = evhttp_new(base_);
  assert(https_ != NULL);

  assert(evhttp_set_cb(http_, "/no_chunks", NonChunkHandler, NULL) == 0);
  assert(evhttp_set_cb(https_, "/no_chunks", NonChunkHandler, NULL) == 0);
  evhttp_set_gencb(http_, ChunkHandler, NULL);
  evhttp_set_gencb(https_, ChunkHandler, NULL);

  SSL_load_error_strings();
  SSL_library_init();
  OpenSSL_add_all_algorithms();
  sctx_ = SSL_CTX_new(SSLv23_server_method());
  assert(sctx_ != NULL);
  if (SSL_CTX_use_certificate_ASN1(sctx_, crt_asn1_str.length(),
                                   reinterpret_cast<const unsigned char*> (
                                       crt_asn1_str.c_str())) != 1) {
    std::cerr << "SSL_CTX_use_certificate_ASN1: "
              << ERR_reason_error_string(ERR_get_error());
    exit(1);
  }
  std::vector<std::string> chain;
  chain.push_back(intermediate2_crt_asn1_str);
  chain.push_back(intermediate1_crt_asn1_str);
  chain.push_back(root_crt_asn1_str);
  for (std::vector<std::string>::const_iterator i = chain.begin();
       i != chain.end(); ++i) {
    const unsigned char *crt = reinterpret_cast<const unsigned char*> (
        i->c_str());
    X509 *x509 = d2i_X509(NULL, &crt, i->length());
    assert(x509 != NULL);
    if (SSL_CTX_add_extra_chain_cert(sctx_, x509) != 1) {
      std::cerr << "SSL_CTX_add_extra_chain_cert: "
                << ERR_reason_error_string(ERR_get_error());
      exit(2);
    }
    break;
  }
  if (SSL_CTX_use_PrivateKey_ASN1(
          EVP_PKEY_RSA,
          sctx_,
          reinterpret_cast<const unsigned char*> (key_asn1_str.c_str()),
          key_asn1_str.length()) != 1) {
    std::cerr << "SSL_CTX_use_PrivateKey_ASN1: "
              << ERR_reason_error_string(ERR_get_error());
    exit(3);
  }
  SSL_CTX_set_verify(sctx_, SSL_VERIFY_NONE, NULL);
  evhttp_set_bevcb(https_, BufferEventCallback, sctx_);

  handle_ = evhttp_bind_socket_with_handle(http_, "0.0.0.0", 8888);
  assert(handle_ != NULL);
  handles_ = evhttp_bind_socket_with_handle(https_, "0.0.0.0", 8889);
  assert(handles_ != NULL);

  event_base_dispatch(base_);

  return 0;
}