open_toontown_panda3d/panda/src/downloader/httpChannel.cxx

3502 lines
112 KiB
C++

// Filename: httpChannel.cxx
// Created by: drose (24Sep02)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved
//
// All use of this software is subject to the terms of the Panda 3d
// Software license. You should have received a copy of this license
// along with this source code; you will also find a current copy of
// the license at http://etc.cmu.edu/panda3d/docs/license/ .
//
// To contact the maintainers of this program write to
// panda3d-general@lists.sourceforge.net .
//
////////////////////////////////////////////////////////////////////
#include "httpChannel.h"
#include "httpClient.h"
#include "httpCookie.h"
#include "bioStream.h"
#include "ssl_utils.h"
#include "chunkedStream.h"
#include "identityStream.h"
#include "config_downloader.h"
#include "clockObject.h"
#include "ramfile.h"
#ifdef HAVE_OPENSSL
#include "openssl/x509.h"
#ifdef WIN32_VC
#include <windows.h> // for select()
#undef X509_NAME
#endif // WIN32_VC
TypeHandle HTTPChannel::_type_handle;
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::Constructor
// Access: Private
// Description:
////////////////////////////////////////////////////////////////////
HTTPChannel::
HTTPChannel(HTTPClient *client) :
_client(client)
{
_proxy_next_index = 0;
_persistent_connection = false;
_allow_proxy = true;
_proxy_tunnel = http_proxy_tunnel;
_connect_timeout = http_connect_timeout;
_http_timeout = http_timeout;
_blocking_connect = false;
_download_throttle = false;
_max_bytes_per_second = downloader_byte_rate;
_seconds_per_update = downloader_frequency;
_max_updates_per_second = 1.0f / _seconds_per_update;
_bytes_per_update = int(_max_bytes_per_second * _seconds_per_update);
_nonblocking = false;
_want_ssl = false;
_proxy_serves_document = false;
_proxy_tunnel_now = false;
_first_byte_requested = 0;
_last_byte_requested = 0;
_first_byte_delivered = 0;
_last_byte_delivered = 0;
_read_index = 0;
_expected_file_size = 0;
_file_size = 0;
_transfer_file_size = 0;
_got_expected_file_size = false;
_got_file_size = false;
_got_transfer_file_size = false;
_bytes_downloaded = 0;
_bytes_requested = 0;
_status_entry = StatusEntry();
_response_type = RT_none;
_http_version = _client->get_http_version();
_http_version_string = _client->get_http_version_string();
_state = S_new;
_done_state = S_new;
_started_download = false;
_sent_so_far = 0;
_body_stream = NULL;
_sbio = NULL;
_last_status_code = 0;
_last_run_time = 0.0f;
_download_to_ramfile = NULL;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::Destructor
// Access: Public, Virtual
// Description:
////////////////////////////////////////////////////////////////////
HTTPChannel::
~HTTPChannel() {
close_connection();
reset_download_to();
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::get_file_system
// Access: Published, Virtual
// Description: Returns the VirtualFileSystem this file is associated
// with.
////////////////////////////////////////////////////////////////////
VirtualFileSystem *HTTPChannel::
get_file_system() const {
return NULL;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::get_filename
// Access: Published, Virtual
// Description: Returns the full pathname to this file within the
// virtual file system.
////////////////////////////////////////////////////////////////////
Filename HTTPChannel::
get_filename() const {
return Filename();
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::is_regular_file
// Access: Published, Virtual
// Description: Returns true if this file represents a regular file
// (and read_file() may be called), false otherwise.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
is_regular_file() const {
return is_valid();
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::will_close_connection
// Access: Public
// Description: Returns true if the server has indicated it will
// close the connection after this document has been
// read, or false if it will remain open (and future
// documents may be requested on the same connection).
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
will_close_connection() const {
if (get_http_version() < HTTPEnum::HV_11) {
// pre-HTTP 1.1 always closes.
return true;
}
string connection = get_header_value("Connection");
if (downcase(connection) == "close") {
// The server says it will close.
return true;
}
// Assume the server will keep it open.
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::open_read_file
// Access: Public, Virtual
// Description: Opens the document for reading. Returns a newly
// allocated istream on success (which you should
// pass to close_read_file() when you are done reading).
// Returns NULL on failure. This may only be called
// once for a particular HTTPChannel.
////////////////////////////////////////////////////////////////////
istream *HTTPChannel::
open_read_file() const {
return ((HTTPChannel *)this)->read_body();
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::close_read_file
// Access: Public
// Description: Closes a file opened by a previous call to
// open_read_file(). This really just deletes the
// istream pointer, but it is recommended to use this
// interface instead of deleting it explicitly, to help
// work around compiler issues.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
close_read_file(istream *stream) const {
if (stream != (istream *)NULL) {
// For some reason--compiler bug in gcc 3.2?--explicitly deleting
// the stream pointer does not call the appropriate global delete
// function; instead apparently calling the system delete
// function. So we call the delete function by hand instead.
#ifndef NDEBUG
stream->~istream();
(*global_operator_delete)(stream);
#else
delete stream;
#endif
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::get_status_string
// Access: Published
// Description: Returns the string as returned by the server
// describing the status code for humans. This may or
// may not be meaningful.
////////////////////////////////////////////////////////////////////
string HTTPChannel::
get_status_string() const {
switch (_status_entry._status_code) {
case SC_incomplete:
return "Connection in progress";
case SC_internal_error:
return "Internal error";
case SC_no_connection:
return "No connection";
case SC_timeout:
return "Timeout on connection";
case SC_lost_connection:
return "Lost connection";
case SC_non_http_response:
return "Non-HTTP response";
case SC_invalid_http:
return "Could not understand HTTP response";
case SC_socks_invalid_version:
return "Unsupported SOCKS version";
case SC_socks_no_acceptable_login_method:
return "No acceptable SOCKS login method";
case SC_socks_refused:
return "SOCKS proxy refused connection";
case SC_socks_no_connection:
return "SOCKS proxy unable to connect";
case SC_ssl_internal_failure:
return "SSL internal failure";
case SC_ssl_no_handshake:
return "No SSL handshake";
case SC_http_error_watermark:
// This shouldn't be triggered.
return "Internal error";
case SC_ssl_invalid_server_certificate:
return "SSL invalid server certificate";
case SC_ssl_unexpected_server:
return "Unexpected SSL server";
case SC_download_write_error:
return "Error writing to disk";
}
return _status_entry._status_string;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::get_header_value
// Access: Published
// Description: Returns the HTML header value associated with the
// indicated key, or empty string if the key was not
// defined in the message returned by the server.
////////////////////////////////////////////////////////////////////
string HTTPChannel::
get_header_value(const string &key) const {
Headers::const_iterator hi = _headers.find(downcase(key));
if (hi != _headers.end()) {
return (*hi).second;
}
return string();
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::get_file_size
// Access: Published
// Description: Returns the size of the file, if it is known.
// Returns the value set by set_expected_file_size() if
// the file size is not known, or 0 if this value was
// not set.
//
// If the file is dynamically generated, the size may
// not be available until a read has started
// (e.g. open_read_file() has been called); and even
// then it may increase as more of the file is read due
// to the nature of HTTP/1.1 requests which can change
// their minds midstream about how much data they're
// sending you.
////////////////////////////////////////////////////////////////////
size_t HTTPChannel::
get_file_size() const {
if (_got_file_size) {
return _file_size;
} else if (_got_transfer_file_size) {
return _transfer_file_size;
} else if (_got_expected_file_size) {
return _expected_file_size;
} else {
return 0;
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::write_headers
// Access: Published
// Description: Outputs a list of all headers defined by the server
// to the indicated output stream.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
write_headers(ostream &out) const {
Headers::const_iterator hi;
for (hi = _headers.begin(); hi != _headers.end(); ++hi) {
out << (*hi).first << ": " << (*hi).second << "\n";
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run
// Access: Published
// Description: This must be called from time to time when
// non-blocking I/O is in use. It checks for data
// coming in on the socket and writes data out to the
// socket when possible, and does whatever processing is
// required towards completing the current task.
//
// The return value is true if the task is still pending
// (and run() will need to be called again in the
// future), or false if the current task is complete.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run() {
if (_state == _done_state || _state == S_failure) {
clear_extra_headers();
if (!reached_done_state()) {
return false;
}
}
if (_started_download) {
if (_nonblocking && _download_throttle) {
double now = ClockObject::get_global_clock()->get_real_time();
double elapsed = now - _last_run_time;
if (elapsed < _seconds_per_update) {
// Come back later.
return true;
}
int num_potential_updates = (int)(elapsed / _seconds_per_update);
_last_run_time = now;
_bytes_requested += _bytes_per_update * num_potential_updates;
if (downloader_cat.is_spam()) {
downloader_cat.spam()
<< "elapsed = " << elapsed << " num_potential_updates = "
<< num_potential_updates << " bytes_requested = "
<< _bytes_requested << "\n";
}
}
switch (_download_dest) {
case DD_none:
return false; // We're done.
case DD_file:
return run_download_to_file();
case DD_ram:
return run_download_to_ram();
}
}
if (downloader_cat.is_spam()) {
downloader_cat.spam()
<< "begin run(), _state = " << _state << ", _done_state = "
<< _done_state << "\n";
}
if (_state == _done_state) {
return reached_done_state();
}
bool repeat_later;
do {
// If we're in a state that expects to have a connection already
// (that is, any state other that S_try_next_proxy), then
// reestablish the connection if it has been dropped.
if (_bio.is_null() && _state != S_try_next_proxy) {
if (_connect_count > http_max_connect_count) {
// Too many connection attempts, just give up. We should
// never trigger this failsafe, since the code in each
// individual case has similar logic to prevent more than two
// consecutive lost connections.
downloader_cat.warning()
<< "Too many lost connections, giving up.\n";
_status_entry._status_code = SC_lost_connection;
_state = S_failure;
return false;
}
// No connection. Attempt to establish one.
if (_proxy.empty()) {
_bio = new BioPtr(_request.get_url());
} else {
_bio = new BioPtr(_proxy);
}
_source = new BioStreamPtr(new BioStream(_bio));
if (_nonblocking) {
BIO_set_nbio(*_bio, 1);
}
if (_connect_count > 0) {
downloader_cat.info()
<< "Reconnecting to " << _bio->get_server_name() << ":"
<< _bio->get_port() << "\n";
} else {
downloader_cat.info()
<< "Connecting to " << _bio->get_server_name() << ":"
<< _bio->get_port() << "\n";
}
_state = S_connecting;
_started_connecting_time =
ClockObject::get_global_clock()->get_real_time();
_connect_count++;
}
if (downloader_cat.is_spam()) {
downloader_cat.spam()
<< "continue run(), _state = " << _state << "\n";
}
switch (_state) {
case S_try_next_proxy:
repeat_later = run_try_next_proxy();
break;
case S_connecting:
repeat_later = run_connecting();
break;
case S_connecting_wait:
repeat_later = run_connecting_wait();
break;
case S_http_proxy_ready:
repeat_later = run_http_proxy_ready();
break;
case S_http_proxy_request_sent:
repeat_later = run_http_proxy_request_sent();
break;
case S_http_proxy_reading_header:
repeat_later = run_http_proxy_reading_header();
break;
case S_socks_proxy_greet:
repeat_later = run_socks_proxy_greet();
break;
case S_socks_proxy_greet_reply:
repeat_later = run_socks_proxy_greet_reply();
break;
case S_socks_proxy_connect:
repeat_later = run_socks_proxy_connect();
break;
case S_socks_proxy_connect_reply:
repeat_later = run_socks_proxy_connect_reply();
break;
case S_setup_ssl:
repeat_later = run_setup_ssl();
break;
case S_ssl_handshake:
repeat_later = run_ssl_handshake();
break;
case S_ready:
repeat_later = run_ready();
break;
case S_request_sent:
repeat_later = run_request_sent();
break;
case S_reading_header:
repeat_later = run_reading_header();
break;
case S_read_header:
repeat_later = run_read_header();
break;
case S_begin_body:
repeat_later = run_begin_body();
break;
case S_reading_body:
repeat_later = run_reading_body();
break;
case S_read_body:
repeat_later = run_read_body();
break;
case S_read_trailer:
repeat_later = run_read_trailer();
break;
default:
downloader_cat.warning()
<< "Unhandled state " << _state << "\n";
return false;
}
if (_state == _done_state || _state == S_failure) {
clear_extra_headers();
// We've reached our terminal state.
return reached_done_state();
}
} while (!repeat_later || _bio.is_null());
if (downloader_cat.is_spam()) {
downloader_cat.spam()
<< "later run(), _state = " << _state
<< ", _done_state = " << _done_state << "\n";
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::read_body
// Access: Published
// Description: Returns a newly-allocated istream suitable for
// reading the body of the document. This may only be
// called immediately after a call to get_document() or
// post_form(), or after a call to run() has returned
// false.
//
// The user is responsible for deleting the returned
// istream later.
////////////////////////////////////////////////////////////////////
ISocketStream *HTTPChannel::
read_body() {
if ((_state != S_read_header && _state != S_begin_body) || _source.is_null()) {
return NULL;
}
string transfer_coding = downcase(get_header_value("Transfer-Encoding"));
ISocketStream *result;
if (transfer_coding == "chunked") {
// "chunked" transfer encoding. This means we will have to decode
// the length of the file as we read it in chunks. The
// IChunkedStream does this.
_state = S_reading_body;
_read_index++;
result = new IChunkedStream(_source, this);
} else {
// If the transfer encoding is anything else, assume "identity".
// This is just the literal characters following the header, up
// until _file_size bytes have been read (if content-length was
// specified), or till end of file otherwise.
_state = S_reading_body;
_read_index++;
result = new IIdentityStream(_source, this, _got_file_size, _file_size);
}
return result;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::download_to_file
// Access: Published
// Description: Specifies the name of a file to download the
// resulting document to. This should be called
// immediately after get_document() or
// begin_get_document() or related functions.
//
// In the case of the blocking I/O methods like
// get_document(), this function will download the
// entire document to the file and return true if it was
// successfully downloaded, false otherwise.
//
// In the case of non-blocking I/O methods like
// begin_get_document(), this function simply indicates an
// intention to download to the indicated file. It
// returns true if the file can be opened for writing,
// false otherwise, but the contents will not be
// completely downloaded until run() has returned false.
// At this time, it is possible that a communications
// error will have left a partial file, so
// is_download_complete() may be called to test this.
//
// If subdocument_resumes is true and the document in
// question was previously requested as a subdocument
// (i.e. get_subdocument() with a first_byte value
// greater than zero), this will automatically seek to
// the appropriate byte within the file for writing the
// output. In this case, the file must already exist
// and must have at least first_byte bytes in it. If
// subdocument_resumes is false, a subdocument will
// always be downloaded beginning at the first byte of
// the file.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
download_to_file(const Filename &filename, bool subdocument_resumes) {
reset_download_to();
_download_to_filename = filename;
_download_to_filename.set_binary();
_download_to_file.close();
_download_to_file.clear();
_subdocument_resumes = (subdocument_resumes && _first_byte_requested != 0);
if (!_download_to_filename.open_write(_download_to_file, !_subdocument_resumes)) {
downloader_cat.info()
<< "Could not open " << _download_to_filename << " for writing.\n";
return false;
}
_download_dest = DD_file;
if (!reset_download_position(_first_byte_requested)) {
reset_download_to();
return false;
}
if (_nonblocking) {
// In nonblocking mode, we can't start the download yet; that will
// be done later as run() is called.
return true;
}
// In normal, blocking mode, go ahead and do the download.
run();
return is_download_complete();
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::download_to_ram
// Access: Published
// Description: Specifies a Ramfile object to download the
// resulting document to. This should be called
// immediately after get_document() or
// begin_get_document() or related functions.
//
// In the case of the blocking I/O methods like
// get_document(), this function will download the
// entire document to the Ramfile and return true if it
// was successfully downloaded, false otherwise.
//
// In the case of non-blocking I/O methods like
// begin_get_document(), this function simply indicates an
// intention to download to the indicated Ramfile. It
// returns true if the file can be opened for writing,
// false otherwise, but the contents will not be
// completely downloaded until run() has returned false.
// At this time, it is possible that a communications
// error will have left a partial file, so
// is_download_complete() may be called to test this.
//
// If subdocument_resumes is true and the document in
// question was previously requested as a subdocument
// (i.e. get_subdocument() with a first_byte value
// greater than zero), this will automatically seek to
// the appropriate byte within the Ramfile for writing
// the output. In this case, the Ramfile must already
// have at least first_byte bytes in it.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
download_to_ram(Ramfile *ramfile, bool subdocument_resumes) {
nassertr(ramfile != (Ramfile *)NULL, false);
reset_download_to();
ramfile->_pos = 0;
_download_to_ramfile = ramfile;
_download_dest = DD_ram;
_subdocument_resumes = (subdocument_resumes && _first_byte_requested != 0);
if (!reset_download_position(_first_byte_requested)) {
reset_download_to();
return false;
}
if (_nonblocking) {
// In nonblocking mode, we can't start the download yet; that will
// be done later as run() is called.
return true;
}
// In normal, blocking mode, go ahead and do the download.
run();
return is_download_complete();
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::get_connection
// Access: Published
// Description: Returns the connection that was established via a
// previous call to connect_to() or begin_connect_to(),
// or NULL if the connection attempt failed or if those
// methods have not recently been called.
//
// This stream has been allocated from the free store.
// It is the user's responsibility to delete this
// pointer when finished with it.
////////////////////////////////////////////////////////////////////
SocketStream *HTTPChannel::
get_connection() {
if (!is_connection_ready()) {
return NULL;
}
BioStream *stream = _source->get_stream();
_source->set_stream(NULL);
// We're now passing ownership of the connection to the user.
reset_to_new();
return stream;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::downcase
// Access: Public, Static
// Description: Returns the input string with all uppercase letters
// converted to lowercase.
////////////////////////////////////////////////////////////////////
string HTTPChannel::
downcase(const string &s) {
string result;
result.reserve(s.size());
string::const_iterator p;
for (p = s.begin(); p != s.end(); ++p) {
result += tolower(*p);
}
return result;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::reached_done_state
// Access: Private
// Description: Called by run() after it reaches the done state, this
// simply checks to see if a download was requested, and
// begins the download if it has been.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
reached_done_state() {
if (downloader_cat.is_spam()) {
downloader_cat.spam()
<< "terminating run(), _state = " << _state
<< ", _done_state = " << _done_state << "\n";
}
if (_state == S_failure) {
// We had to give up. Each proxy we tried, in sequence, failed.
// But maybe the last attempt didn't give us the most informative
// response; go back and find the best one.
if (!_status_list.empty()) {
_status_list.push_back(_status_entry);
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Reexamining failure responses.\n";
}
size_t best_i = 0;
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< " " << 0 << ". " << _status_list[0]._status_code << " "
<< _status_list[0]._status_string << "\n";
}
for (size_t i = 1; i < _status_list.size(); i++) {
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< " " << i << ". " << _status_list[i]._status_code << " "
<< _status_list[i]._status_string << "\n";
}
if (more_useful_status_code(_status_list[i]._status_code,
_status_list[best_i]._status_code)) {
best_i = i;
}
}
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "chose index " << best_i << ", above.\n";
}
_status_entry = _status_list[best_i];
_status_list.clear();
}
return false;
}
// We don't need the list of previous failures any more--we've connected.
_status_list.clear();
if (_download_dest == DD_none) {
// All done.
return false;
} else {
// Oops, we have to download the body now.
// We shouldn't already be in the middle of reading some other
// body when we come here.
if (_body_stream != NULL) {
delete _body_stream;
_body_stream = (ISocketStream *)NULL;
}
_body_stream = read_body();
if (_body_stream == (ISocketStream *)NULL) {
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Unable to download body.\n";
}
return false;
} else {
if (_state != S_reading_body) {
_body_stream = NULL;
}
_started_download = true;
_last_run_time = ClockObject::get_global_clock()->get_real_time();
return true;
}
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_try_next_proxy
// Access: Private
// Description: This state is reached when a previous connection
// attempt fails. If we have multiple proxies in line
// to try, it sets us up for the next proxy and tries to
// connect again; otherwise, it sets the state to
// S_failure.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_try_next_proxy() {
if (_proxy_next_index < _proxies.size()) {
// Record the previous proxy's status entry, so we can come back
// to it later if we get nonsense from the remaining proxies.
_status_list.push_back(_status_entry);
_status_entry = StatusEntry();
// Now try the next proxy in sequence.
_proxy = _proxies[_proxy_next_index];
_proxy_auth = (HTTPAuthorization *)NULL;
_proxy_next_index++;
close_connection();
reconsider_proxy();
_state = S_connecting;
return false;
}
// No more proxies to try, or we're not using a proxy.
_state = S_failure;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_connecting
// Access: Private
// Description: In this state, we have not yet established a
// network connection to the server (or proxy).
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_connecting() {
_status_entry = StatusEntry();
if (BIO_do_connect(*_bio) <= 0) {
if (BIO_should_retry(*_bio)) {
_state = S_connecting_wait;
return false;
}
downloader_cat.info()
<< "Could not connect to " << _bio->get_server_name() << ":"
<< _bio->get_port() << "\n";
notify_ssl_errors();
_status_entry._status_code = SC_no_connection;
_state = S_try_next_proxy;
return false;
}
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Connected to " << _bio->get_server_name() << ":"
<< _bio->get_port() << "\n";
}
if (_proxy_tunnel_now) {
if (_proxy.get_scheme() == "socks") {
_state = S_socks_proxy_greet;
} else {
_state = S_http_proxy_ready;
}
} else {
_state = _want_ssl ? S_setup_ssl : S_ready;
}
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_connecting_wait
// Access: Private
// Description: Here we have begun to establish a nonblocking
// connection, but we got a come-back-later message, so
// we are waiting for the socket to finish connecting.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_connecting_wait() {
int fd = -1;
BIO_get_fd(*_bio, &fd);
if (fd < 0) {
downloader_cat.warning()
<< "nonblocking socket BIO has no file descriptor.\n";
// This shouldn't be possible.
_status_entry._status_code = SC_internal_error;
_state = S_try_next_proxy;
return false;
}
if (downloader_cat.is_spam()) {
downloader_cat.spam()
<< "waiting to connect to " << _request.get_url().get_server_and_port() << ".\n";
}
fd_set wset;
FD_ZERO(&wset);
FD_SET(fd, &wset);
struct timeval tv;
if (get_blocking_connect()) {
// Since we'll be blocking on this connect, fill in the timeout
// into the structure.
tv.tv_sec = (int)_connect_timeout;
tv.tv_usec = (int)((_connect_timeout - tv.tv_sec) * 1000000.0);
} else {
// We won't block on this connect, so select() for 0 time.
tv.tv_sec = 0;
tv.tv_usec = 0;
}
int errcode = select(fd + 1, NULL, &wset, NULL, &tv);
if (errcode < 0) {
downloader_cat.warning()
<< "Error in select.\n";
// This shouldn't be possible.
_status_entry._status_code = SC_internal_error;
_state = S_try_next_proxy;
return false;
}
if (errcode == 0) {
// Nothing's happened so far; come back later.
if (get_blocking_connect() ||
(ClockObject::get_global_clock()->get_real_time() -
_started_connecting_time > get_connect_timeout())) {
// Time to give up.
downloader_cat.info()
<< "Timeout connecting to "
<< _request.get_url().get_server_and_port() << ".\n";
_status_entry._status_code = SC_timeout;
_state = S_try_next_proxy;
return false;
}
return true;
}
// The socket is now ready for writing.
_state = S_connecting;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_http_proxy_ready
// Access: Private
// Description: This state is reached only after first establishing a
// connection to the proxy, if a proxy is in use and we
// are tunneling through it via a CONNECT command.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_http_proxy_ready() {
// If there's a request to be sent to the proxy, send it now.
nassertr(!_proxy_request_text.empty(), false);
if (!server_send(_proxy_request_text, false)) {
return true;
}
// All done sending request.
_state = S_http_proxy_request_sent;
_sent_request_time = ClockObject::get_global_clock()->get_real_time();
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_http_proxy_request_sent
// Access: Private
// Description: This state is reached only after we have sent a
// special message to the proxy and we are waiting for
// the proxy's response. It is not used in the normal
// http-over-proxy case, which does not require a
// special message to the proxy.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_http_proxy_request_sent() {
// Wait for the first line to come back from the server.
string line;
if (!server_getline_failsafe(line)) {
return true;
}
if (!parse_http_response(line)) {
return false;
}
_state = S_http_proxy_reading_header;
_current_field_name = string();
_current_field_value = string();
_headers.clear();
_got_file_size = false;
_got_transfer_file_size = false;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_http_proxy_reading_header
// Access: Private
// Description: In this state we are reading the header lines from
// the proxy's response to our special message.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_http_proxy_reading_header() {
if (parse_http_header()) {
return true;
}
_redirect = get_header_value("Location");
// We can take the proxy's word for it that this is the actual URL
// for the redirect.
_server_response_has_no_body =
(get_status_code() / 100 == 1 ||
get_status_code() == 204 ||
get_status_code() == 304);
int last_status = _last_status_code;
_last_status_code = get_status_code();
if (get_status_code() == 407 && last_status != 407 && !_proxy.empty()) {
// 407: not authorized to proxy. Try to get the authorization.
string authenticate_request = get_header_value("Proxy-Authenticate");
_proxy_auth = _client->generate_auth(_proxy, true, authenticate_request);
if (_proxy_auth != (HTTPAuthorization *)NULL) {
_proxy_realm = _proxy_auth->get_realm();
_proxy_username = _client->select_username(_proxy, true, _proxy_realm);
if (!_proxy_username.empty()) {
make_proxy_request_text();
// Roll the state forward to force a new request.
_state = S_begin_body;
return false;
}
}
}
if (!is_valid()) {
// Proxy wouldn't open connection.
// Change some of the status codes a proxy might return to
// differentiate them from similar status codes the destination
// server might have returned.
if (get_status_code() != 407) {
_status_entry._status_code += 1000;
}
_state = S_try_next_proxy;
return false;
}
// Now we have a tunnel opened through the proxy.
make_request_text();
_state = _want_ssl ? S_setup_ssl : S_ready;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_socks_proxy_greet
// Access: Private
// Description: This state is reached only after first establishing a
// connection to a SOCKS proxy, with which we now have
// to negotiate a connection.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_socks_proxy_greet() {
static const char socks_greeting[] = {
0x05, // Socks version 5
0x01, // Number of supported login methods
0x00, // Login method 0: no authentication
/*
0x01, // Login method 1: GSSAPI
0x02 // Login method 2: username/password
*/
};
static const int socks_greeting_len = sizeof(socks_greeting);
if (!server_send(string(socks_greeting, socks_greeting_len), true)) {
return true;
}
_sent_request_time = ClockObject::get_global_clock()->get_real_time();
// All done sending request.
_state = S_socks_proxy_greet_reply;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_socks_proxy_greet_reply
// Access: Private
// Description: We are waiting for the SOCKS proxy to respond to our
// greeting.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_socks_proxy_greet_reply() {
string reply;
// Get the two-byte reply from the SOCKS server.
if (!server_get_failsafe(reply, 2)) {
return true;
}
if (reply[0] != 0x05) {
// We only speak Socks5.
downloader_cat.info()
<< "Rejecting Socks version " << (int)reply[0] << "\n";
_status_entry._status_code = SC_socks_invalid_version;
_state = S_try_next_proxy;
return false;
}
if (reply[1] == (char)0xff) {
downloader_cat.info()
<< "Socks server does not accept our available login methods.\n";
_status_entry._status_code = SC_socks_no_acceptable_login_method;
_state = S_try_next_proxy;
return false;
}
if (reply[1] == 0x00) {
// No login method required. Proceed directly to the connect
// message.
_state = S_socks_proxy_connect;
return false;
}
// The server accepted a login method we didn't offer!
downloader_cat.info()
<< "Socks server accepted unrequested login method "
<< (int)reply[1] << "\n";
_status_entry._status_code = SC_socks_no_acceptable_login_method;
_state = S_try_next_proxy;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_socks_proxy_connect
// Access: Private
// Description: The SOCKS proxy has accepted us, and now we may issue
// the connect request.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_socks_proxy_connect() {
static const char socks_connect[] = {
0x05, // Socks version 5
0x01, // Command 1: connect
0x00, // reserved
0x03, // DNS name
};
static const int socks_connect_len = sizeof(socks_connect);
string hostname = _request.get_url().get_server();
int port = _request.get_url().get_port();
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Requesting SOCKS5 connection to "
<< _request.get_url().get_server_and_port() << "\n";
}
string connect =
string(socks_connect, socks_connect_len) +
string(1, (char)hostname.length()) +
hostname +
string(1, (char)((port >> 8) & 0xff)) +
string(1, (char)(port & 0xff));
if (!server_send(connect, true)) {
return true;
}
_sent_request_time = ClockObject::get_global_clock()->get_real_time();
_state = S_socks_proxy_connect_reply;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_socks_proxy_connect_reply
// Access: Private
// Description: We are waiting for the SOCKS proxy to honor our
// connect request.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_socks_proxy_connect_reply() {
string reply;
// Get the first two bytes of the connect reply.
if (!server_get_failsafe(reply, 2)) {
return true;
}
if (reply[0] != 0x05) {
// We only speak Socks5.
downloader_cat.info()
<< "Rejecting Socks version " << (int)reply[0] << "\n";
close_connection(); // connection is now bad.
_status_entry._status_code = SC_socks_invalid_version;
_state = S_try_next_proxy;
return false;
}
if (reply[1] != 0x00) {
downloader_cat.info()
<< "Connection refused, SOCKS code " << (int)reply[1] << "\n";
/*
Socks error codes (from RFC1928):
o X'00' succeeded
o X'01' general SOCKS server failure
o X'02' connection not allowed by ruleset
o X'03' Network unreachable
o X'04' Host unreachable
o X'05' Connection refused
o X'06' TTL expired
o X'07' Command not supported
o X'08' Address type not supported
o X'09' to X'FF' unassigned
*/
switch (reply[1]) {
case 0x03:
case 0x04:
case 0x05:
case 0x06:
// These generally mean the same thing: the SOCKS proxy tried,
// but couldn't reach the host.
_status_entry._status_code = SC_socks_no_connection;
break;
default:
_status_entry._status_code = SC_socks_refused;
}
close_connection(); // connection is now bad.
_state = S_try_next_proxy;
return false;
}
// Now put those bytes back, and get five bytes of the reply.
_working_get = reply;
if (!server_get_failsafe(reply, 5)) {
return true;
}
// Figure out how many bytes total we will expect for the reply.
int total_bytes = 6;
switch (reply[3]) {
case 0x01: // IPv4
total_bytes += 4;
break;
case 0x03: // DNS
total_bytes += (unsigned int)reply[4];
break;
default:
downloader_cat.info()
<< "Unsupported SOCKS address type: " << (int)reply[3] << "\n";
_status_entry._status_code = SC_socks_invalid_version;
_state = S_try_next_proxy;
return false;
}
// Now put back the bytes we've read so far, and get the rest of
// them.
_working_get = reply;
if (!server_get_failsafe(reply, total_bytes)) {
return true;
}
if (downloader_cat.is_debug()) {
// Finally, we can decode the whole thing.
string connect_host;
switch (reply[3]) {
case 0x01: // IPv4
{
ostringstream strm;
strm << (unsigned int)(unsigned char)reply[4] << "."
<< (unsigned int)(unsigned char)reply[5] << "."
<< (unsigned int)(unsigned char)reply[6] << "."
<< (unsigned int)(unsigned char)reply[7];
connect_host = strm.str();
}
break;
case 0x03: // DNS
connect_host = string(&reply[5], (unsigned int)reply[4]);
break;
}
int connect_port =
(((unsigned int)(unsigned char)reply[total_bytes - 2]) << 8) |
((unsigned int)(unsigned char)reply[total_bytes - 1]);
downloader_cat.debug()
<< _proxy << " directed us to " << connect_host << ":"
<< connect_port << "\n";
}
_state = _want_ssl ? S_setup_ssl : S_ready;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_setup_ssl
// Access: Private
// Description: This state begins elevating our existing, unsecure
// connection to a secure, SSL connection.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_setup_ssl() {
_sbio = BIO_new_ssl(_client->get_ssl_ctx(), true);
BIO_push(_sbio, *_bio);
SSL *ssl = NULL;
BIO_get_ssl(_sbio, &ssl);
nassertr(ssl != (SSL *)NULL, false);
string cipher_list = _client->get_cipher_list();
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Setting ssl-cipher-list '" << cipher_list << "'\n";
}
int result = SSL_set_cipher_list(ssl, cipher_list.c_str());
if (result == 0) {
downloader_cat.error()
<< "Invalid cipher list: '" << cipher_list << "'\n";
notify_ssl_errors();
_status_entry._status_code = SC_ssl_internal_failure;
_state = S_failure;
return false;
}
// It would be nice to use something like SSL_set_client_cert_cb()
// here to set a callback to provide the certificate should it be
// requested, or even to potentially provide any of a number of
// certificates according to the server's CA presented, but that
// interface as provided by OpenSSL is broken since there's no way
// to pass additional data to the callback function (and hence no
// way to tie it back to the HTTPChannel object, other than by
// building a messy mapping of SSL pointers back to HTTPChannel
// pointers).
if (_client->load_client_certificate()) {
SSL_use_certificate(ssl, _client->_client_certificate_pub);
SSL_use_PrivateKey(ssl, _client->_client_certificate_priv);
if (!SSL_check_private_key(ssl)) {
downloader_cat.warning()
<< "Client private key does not match public key!\n";
}
}
if (downloader_cat.is_spam()) {
downloader_cat.spam()
<< "SSL Ciphers available:\n";
const char *name;
int pri = 0;
name = SSL_get_cipher_list(ssl, pri);
while (name != NULL) {
downloader_cat.spam()
<< " " << pri + 1 << ". " << name << "\n";
pri++;
name = SSL_get_cipher_list(ssl, pri);
}
}
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "performing SSL handshake\n";
}
_state = S_ssl_handshake;
// We start the connect timer over again when we reach the SSL
// handshake.
_started_connecting_time =
ClockObject::get_global_clock()->get_real_time();
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_ssl_handshake
// Access: Private
// Description: This state performs the SSL handshake with the
// server, and also verifies the server's identity when
// the handshake has successfully completed.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_ssl_handshake() {
if (BIO_do_handshake(_sbio) <= 0) {
if (BIO_should_retry(_sbio)) {
double elapsed =
ClockObject::get_global_clock()->get_real_time() -
_started_connecting_time;
if (elapsed <= get_connect_timeout()) {
// Keep trying.
return true;
}
// Time to give up on the handshake.
}
downloader_cat.info()
<< "Could not establish SSL handshake with "
<< _request.get_url().get_server_and_port() << "\n";
notify_ssl_errors();
// It seems to be an error to free sbio at this point; perhaps
// it's already been freed?
_status_entry._status_code = SC_ssl_no_handshake;
_state = S_failure;
return false;
}
SSL *ssl = NULL;
BIO_get_ssl(_sbio, &ssl);
nassertr(ssl != (SSL *)NULL, false);
if (!_nonblocking) {
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
}
SSL_CIPHER *cipher = SSL_get_current_cipher(ssl);
if (cipher == (SSL_CIPHER *)NULL) {
downloader_cat.warning()
<< "No current cipher on SSL connection.\n";
} else {
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Using cipher " << SSL_CIPHER_get_name(cipher) << "\n";
}
}
// Now that we've made an SSL handshake, we can use the SSL bio to
// do all of our communication henceforth.
_bio->set_bio(_sbio);
_sbio = NULL;
// Now verify the server is who we expect it to be.
long verify_result = SSL_get_verify_result(ssl);
if (verify_result == X509_V_ERR_CERT_HAS_EXPIRED) {
downloader_cat.info()
<< "Expired certificate from " << _request.get_url().get_server_and_port() << "\n";
if (_client->get_verify_ssl() == HTTPClient::VS_normal) {
_status_entry._status_code = SC_ssl_invalid_server_certificate;
_state = S_failure;
return false;
}
} else if (verify_result == X509_V_ERR_CERT_NOT_YET_VALID) {
downloader_cat.info()
<< "Premature certificate from " << _request.get_url().get_server_and_port() << "\n";
if (_client->get_verify_ssl() == HTTPClient::VS_normal) {
_status_entry._status_code = SC_ssl_invalid_server_certificate;
_state = S_failure;
return false;
}
} else if (verify_result != X509_V_OK) {
downloader_cat.info()
<< "Unable to verify identity of " << _request.get_url().get_server_and_port()
<< ", verify error code " << verify_result << "\n";
if (_client->get_verify_ssl() != HTTPClient::VS_no_verify) {
_status_entry._status_code = SC_ssl_invalid_server_certificate;
_state = S_failure;
return false;
}
}
X509 *cert = SSL_get_peer_certificate(ssl);
if (cert == (X509 *)NULL) {
downloader_cat.info()
<< "No certificate was presented by server.\n";
// This shouldn't be possible, per the SSL specs.
if (_client->get_verify_ssl() != HTTPClient::VS_no_verify ||
!_client->_expected_servers.empty()) {
_status_entry._status_code = SC_ssl_invalid_server_certificate;
_state = S_failure;
return false;
}
} else {
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Received certificate from server:\n" << flush;
X509_print_fp(stderr, cert);
fflush(stderr);
}
X509_NAME *subject = X509_get_subject_name(cert);
if (downloader_cat.is_debug()) {
string org_name = get_x509_name_component(subject, NID_organizationName);
string org_unit_name = get_x509_name_component(subject, NID_organizationalUnitName);
string common_name = get_x509_name_component(subject, NID_commonName);
downloader_cat.debug()
<< "Server is " << common_name << " from " << org_unit_name
<< " / " << org_name << "\n";
}
if (_client->get_verify_ssl() != HTTPClient::VS_no_verify) {
// Check that the server is someone we expected to be talking
// to.
if (!verify_server(subject)) {
downloader_cat.info()
<< "Server does not match any expected server.\n";
_status_entry._status_code = SC_ssl_unexpected_server;
_state = S_failure;
return false;
}
}
X509_free(cert);
}
_state = S_ready;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_ready
// Access: Private
// Description: This is the main "ready" state. In this state, we
// have established a (possibly secure) connection to
// the server (or proxy), and the server (or proxy) is
// idle and waiting for us to send a request.
//
// If persistent_connection is true, we will generally
// come back to this state after finishing each request
// on a given connection.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_ready() {
// If there's a request to be sent upstream, send it now.
if (!_request_text.empty()) {
if (!server_send(_request_text, false)) {
return true;
}
}
// All done sending request.
_state = S_request_sent;
_sent_request_time = ClockObject::get_global_clock()->get_real_time();
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_request_sent
// Access: Private
// Description: In this state we have sent our request to the server
// (or proxy) and we are waiting for a response.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_request_sent() {
// Wait for the first line to come back from the server.
string line;
if (!server_getline_failsafe(line)) {
return true;
}
if (!parse_http_response(line)) {
// Not an HTTP response. _state is already set appropriately.
return false;
}
_state = S_reading_header;
_current_field_name = string();
_current_field_value = string();
_headers.clear();
_got_file_size = false;
_got_transfer_file_size = false;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_reading_header
// Access: Private
// Description: In this state we have received the first response to
// our request from the server (or proxy) and we are
// reading the set of header lines preceding the
// requested document.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_reading_header() {
if (parse_http_header()) {
if (_bio.is_null()) {
downloader_cat.info()
<< "Connection lost while reading HTTP response.\n";
if (_response_type == RT_http_hangup) {
// This was our second hangup in a row. Give up.
_status_entry._status_code = SC_lost_connection;
_state = S_try_next_proxy;
} else {
// Try again, once.
_response_type = RT_http_hangup;
}
} else {
double elapsed =
ClockObject::get_global_clock()->get_real_time() -
_sent_request_time;
if (elapsed > get_http_timeout()) {
// Time to give up.
downloader_cat.info()
<< "Timeout waiting for "
<< _request.get_url().get_server_and_port()
<< " in run_reading_header (" << elapsed
<< " seconds elapsed).\n";
_status_entry._status_code = SC_timeout;
_state = S_try_next_proxy;
}
}
return true;
}
_response_type = RT_http_complete;
// Ok, we've established an HTTP connection to the server. Our
// extra send headers have done their job; clear them for next time.
clear_extra_headers();
_server_response_has_no_body =
(get_status_code() / 100 == 1 ||
get_status_code() == 204 ||
get_status_code() == 304 ||
_method == HTTPEnum::M_head);
// Look for key properties in the header fields.
if (get_status_code() == 206) {
string content_range = get_header_value("Content-Range");
if (content_range.empty()) {
downloader_cat.warning()
<< "Got 206 response without Content-Range header!\n";
_status_entry._status_code = SC_invalid_http;
_state = S_failure;
return false;
} else {
if (!parse_content_range(content_range)) {
downloader_cat.warning()
<< "Couldn't parse Content-Range: " << content_range << "\n";
_status_entry._status_code = SC_invalid_http;
_state = S_failure;
return false;
}
}
} else {
_first_byte_delivered = 0;
_last_byte_delivered = 0;
}
// Set the _document_spec to reflect what we just retrieved.
_document_spec = DocumentSpec(_request.get_url());
string tag = get_header_value("ETag");
if (!tag.empty()) {
_document_spec.set_tag(HTTPEntityTag(tag));
}
string date = get_header_value("Last-Modified");
if (!date.empty()) {
_document_spec.set_date(HTTPDate(date));
}
// In case we've got a download in effect, reset the download
// position to match our starting byte.
if (!reset_download_position(_first_byte_delivered)) {
_status_entry._status_code = SC_invalid_http;
_state = S_failure;
return false;
}
_got_expected_file_size = false;
_got_file_size = false;
_got_transfer_file_size = false;
string content_length = get_header_value("Content-Length");
if (!content_length.empty()) {
_file_size = atoi(content_length.c_str());
_got_file_size = true;
} else if (get_status_code() == 206) {
// Well, we didn't get a content-length from the server, but we
// can infer the number of bytes based on the range we're given.
_file_size = _last_byte_delivered - _first_byte_delivered + 1;
_got_file_size = true;
}
_redirect = get_header_value("Location");
// The server might have given us just a filename for the redirect.
// In that case, it's relative to the same server.
if (_redirect.has_path() && !_redirect.has_authority()) {
_redirect.set_scheme(_document_spec.get_url().get_scheme());
_redirect.set_authority(_document_spec.get_url().get_authority());
}
_state = S_read_header;
if (_server_response_has_no_body && will_close_connection()) {
// If the server said it will close the connection, we should
// close it too.
close_connection();
}
// Handle automatic retries and redirects.
int last_status = _last_status_code;
_last_status_code = get_status_code();
if (get_status_code() == 407 && last_status != 407 && !_proxy.empty()) {
// 407: not authorized to proxy. Try to get the authorization.
string authenticate_request = get_header_value("Proxy-Authenticate");
_proxy_auth =
_client->generate_auth(_proxy, true, authenticate_request);
if (_proxy_auth != (HTTPAuthorization *)NULL) {
_proxy_realm = _proxy_auth->get_realm();
_proxy_username = _client->select_username(_proxy, true, _proxy_realm);
if (!_proxy_username.empty()) {
make_request_text();
// Roll the state forward to force a new request.
_state = S_begin_body;
return false;
}
}
}
if (get_status_code() == 401 && last_status != 401) {
// 401: not authorized to remote server. Try to get the authorization.
string authenticate_request = get_header_value("WWW-Authenticate");
_www_auth = _client->generate_auth(_request.get_url(), false, authenticate_request);
if (_www_auth != (HTTPAuthorization *)NULL) {
_www_realm = _www_auth->get_realm();
_www_username = _client->select_username(_request.get_url(), false, _www_realm);
if (!_www_username.empty()) {
make_request_text();
// Roll the state forward to force a new request.
_state = S_begin_body;
return false;
}
}
}
if ((get_status_code() / 100) == 3 && get_status_code() != 305 &&
!get_redirect().empty()) {
// Redirect. Should we handle it automatically?
// According to the letter of RFC 2616, 301 and 302 responses to
// POST requests must not be automatically redirected without
// confirmation by the user. In reality, browsers do allow
// automatic redirection of these responses, changing the POST to
// a GET, and we reproduce this behavior here.
if (_method == HTTPEnum::M_post) {
_method = HTTPEnum::M_get;
_body = string();
}
if (_method == HTTPEnum::M_get || _method == HTTPEnum::M_head) {
// Sure!
URLSpec new_url = get_redirect();
if (find(_redirect_trail.begin(), _redirect_trail.end(),
new_url) != _redirect_trail.end()) {
downloader_cat.warning()
<< "cycle detected in redirect to " << new_url << "\n";
} else {
_redirect_trail.push_back(new_url);
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "following redirect to " << new_url << "\n";
}
if (_request.get_url().has_username()) {
new_url.set_username(_request.get_url().get_username());
}
reset_url(_request.get_url(), new_url);
_request.set_url(new_url);
make_header();
make_request_text();
// Roll the state forward to force a new request.
_state = S_begin_body;
return false;
}
}
}
if (_state == S_read_header &&
((get_status_code() / 100) == 4 || (get_status_code() / 100) == 5) &&
_proxy_serves_document && _proxy_next_index < _proxies.size()) {
// If we were using a proxy (but not tunneling through the proxy)
// and we got some kind of a server error, try the next proxy in
// sequence (if we have one). This handles the case of a working
// proxy that cannot see the host (and so returns 504 or something
// along those lines). Some proxies are so broken they return a
// 404 in this case, so we have to consider that along the same
// lines.
_state = S_try_next_proxy;
return false;
}
// Otherwise, we're good to go.
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_read_header
// Access: Private
// Description: In this state we have completely read the header
// lines returned by the server (or proxy) in response
// to our request. This state represents the normal
// stopping point of a call to get_document(), etc.;
// further reads will return the body of the request,
// the requested document.
//
// Normally run_read_header() is not called unless the
// user has elected not to read the returned document
// himself. In fact, the state itself only exists so we
// can make a distinction between S_read_header and
// S_begin_body, where S_read_header is safe to return
// to the user and S_begin_body means we need to start
// skipping the document.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_read_header() {
_state = S_begin_body;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_begin_body
// Access: Private
// Description: This state begins to skip over the body in
// preparation for making a new request.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_begin_body() {
if (will_close_connection()) {
// If the socket will close anyway, no point in skipping past the
// previous body; just reset.
reset_to_new();
return false;
}
if (_server_response_has_no_body) {
// We have already "read" the nonexistent body.
_state = S_read_trailer;
} else if (get_file_size() > 8192) {
// If we know the size of the body we are about to skip and it's
// too large (and here we arbitrarily say 8KB is too large), then
// don't bother skipping it--just drop the connection and get a
// new one.
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Dropping connection rather than skipping past "
<< get_file_size() << " bytes.\n";
}
reset_to_new();
} else {
// We shouldn't already be in the middle of reading some other
// body when we come here.
if (_body_stream != NULL) {
delete _body_stream;
_body_stream = (ISocketStream *)NULL;
}
_body_stream = read_body();
if (_body_stream == (ISocketStream *)NULL) {
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Unable to skip body.\n";
}
reset_to_new();
} else {
if (_state != S_reading_body) {
_body_stream = NULL;
}
}
}
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_reading_body
// Access: Private
// Description: In this state we are in the process of reading the
// response's body. We will only come to this function
// if the user did not choose to read the entire body
// himself (by calling read_body(), for instance, or
// open_read_file()).
//
// In this case we should skip past the body to reset
// the connection for making a new request.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_reading_body() {
if (will_close_connection()) {
// If the socket will close anyway, no point in skipping past the
// previous body; just reset.
reset_to_new();
return false;
}
// Skip the body we've already started.
if (_body_stream == NULL) {
// Whoops, we're not in skip-body mode. Better reset.
reset_to_new();
return false;
}
string line;
getline(*_body_stream, line);
while (!_body_stream->fail() && !_body_stream->eof()) {
if (downloader_cat.is_spam()) {
downloader_cat.spam() << "skip: " << line << "\n";
}
getline(*_body_stream, line);
}
if (!_body_stream->is_closed()) {
// There's more to come later.
return true;
}
delete _body_stream;
_body_stream = NULL;
// This should have been set by the _body_stream finishing.
nassertr(_state != S_reading_body, false);
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_read_body
// Access: Private
// Description: In this state we have completely read (or skipped
// over) the body of the response. We should continue
// skipping past the trailer following the body.
//
// Not all bodies come with trailers; in particular, the
// "identity" transfer encoding does not include a
// trailer. It is therefore the responsibility of the
// IdentityStreamBuf or ChunkedStreamBuf to set the
// state appropriately to either S_read_body or
// S_read_trailer following the completion of the body.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_read_body() {
if (will_close_connection()) {
// If the socket will close anyway, no point in skipping past the
// previous body; just reset.
reset_to_new();
return false;
}
// Skip the trailer following the recently-read body.
string line;
if (!server_getline(line)) {
return true;
}
while (!line.empty()) {
if (!server_getline(line)) {
return true;
}
}
_state = S_read_trailer;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_read_trailer
// Access: Private
// Description: In this state we have completely read the body and
// the trailer. This state is simply a pass-through
// back to S_ready.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_read_trailer() {
if (will_close_connection()) {
// If the socket will close anyway, no point in skipping past the
// previous body; just reset.
reset_to_new();
return false;
}
_state = S_ready;
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_download_to_file
// Access: Private
// Description: After the headers, etc. have been read, this streams
// the download to the named file.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_download_to_file() {
nassertr(_body_stream != (ISocketStream *)NULL, false);
bool do_throttle = _nonblocking && _download_throttle;
int count = 0;
int ch = _body_stream->get();
nassertr(_body_stream != (ISocketStream *)NULL, false);
while (!_body_stream->eof() && !_body_stream->fail()) {
_download_to_file.put(ch);
_bytes_downloaded++;
if (do_throttle && (++count >= _bytes_per_update)) {
// That's enough for now.
return true;
}
ch = _body_stream->get();
nassertr(_body_stream != (ISocketStream *)NULL, false);
}
if (_download_to_file.fail()) {
downloader_cat.warning()
<< "Error writing to " << _download_to_filename << "\n";
_status_entry._status_code = SC_download_write_error;
_state = S_failure;
_download_to_file.close();
return false;
}
if (_body_stream->is_closed()) {
// Done.
_download_to_file.close();
return false;
} else {
// More to come.
return true;
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::run_download_to_ram
// Access: Private
// Description: After the headers, etc. have been read, this streams
// the download to the specified Ramfile object.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
run_download_to_ram() {
nassertr(_body_stream != (ISocketStream *)NULL, false);
nassertr(_download_to_ramfile != (Ramfile *)NULL, false);
bool do_throttle = _nonblocking && _download_throttle;
int count = 0;
int ch = _body_stream->get();
nassertr(_body_stream != (ISocketStream *)NULL, false);
while (!_body_stream->eof() && !_body_stream->fail()) {
_download_to_ramfile->_data += (char)ch;
_bytes_downloaded++;
if (do_throttle && (++count >= _bytes_per_update)) {
// That's enough for now.
return true;
}
ch = _body_stream->get();
nassertr(_body_stream != (ISocketStream *)NULL, false);
}
if (_body_stream->is_closed()) {
// Done.
return false;
} else {
// More to come.
return true;
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::begin_request
// Access: Private
// Description: Begins a new document request to the server, throwing
// away whatever request was currently pending if
// necessary.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
begin_request(HTTPEnum::Method method, const DocumentSpec &url,
const string &body, bool nonblocking,
size_t first_byte, size_t last_byte) {
reset_for_new_request();
// Get the set of proxies that are appropriate for this URL.
_proxies.clear();
_proxy_next_index = 0;
if (get_allow_proxy()) {
_client->get_proxies_for_url(url.get_url(), _proxies);
}
// If we still have a live connection to a proxy that is on the
// list, that proxy should be moved immediately to the front of the
// list (to minimize restarting connections unnecessarily).
if (!_bio.is_null() && !_proxies.empty() && !_proxy.empty()) {
Proxies::iterator pi = find(_proxies.begin(), _proxies.end(), _proxy);
if (pi != _proxies.end()) {
_proxies.erase(pi);
_proxies.insert(_proxies.begin(), _proxy);
}
}
URLSpec new_proxy;
if (_proxy_next_index < _proxies.size()) {
new_proxy = _proxies[_proxy_next_index];
_proxy_next_index++;
}
// Changing the proxy is grounds for dropping the old connection, if
// any.
if (_proxy != new_proxy) {
_proxy = new_proxy;
_proxy_auth = (HTTPAuthorization *)NULL;
reset_to_new();
}
// Ditto with changing the nonblocking state.
if (_nonblocking != nonblocking) {
_nonblocking = nonblocking;
reset_to_new();
}
reset_url(_request.get_url(), url.get_url());
_request = url;
_document_spec = DocumentSpec();
_method = method;
_body = body;
// An https-style request means we'll need to establish an SSL
// connection.
_want_ssl = _request.get_url().is_ssl();
_first_byte_requested = first_byte;
_last_byte_requested = last_byte;
_connect_count = 0;
reconsider_proxy();
// Also, reset from whatever previous request might still be pending.
if (_state == S_failure || (_state < S_read_header && _state != S_ready)) {
reset_to_new();
} else if (_state == S_read_header) {
// Roll one step forwards to start skipping past the previous
// body.
_state = S_begin_body;
}
_done_state = (_method == HTTPEnum::M_connect) ? S_ready : S_read_header;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::reconsider_proxy
// Access: Private
// Description: Reevaluates the flags and strings that are computed
// based on the particular proxy we are attempting to
// connect to. This should be called when we initiate a
// request, and also whenever we change proxies while
// processing a request.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
reconsider_proxy() {
_proxy_tunnel_now = false;
_proxy_serves_document = false;
if (!_proxy.empty()) {
// If the user insists we always tunnel through a proxy, or if
// we're opening an SSL connection, or the user has explicitly
// asked for a direct connection of some kind, or if we have a
// SOCKS-style proxy; each of these demands a tunnel through the
// proxy to speak directly to the http server.
_proxy_tunnel_now =
(get_proxy_tunnel() || _want_ssl ||
_method == HTTPEnum::M_connect || _proxy.get_scheme() == "socks");
// Otherwise (but we still have a proxy), then we ask the proxy to
// hand us the document.
_proxy_serves_document = !_proxy_tunnel_now;
}
make_header();
make_request_text();
if (_proxy_tunnel_now) {
// Maybe we need to tunnel through the proxy to connect to the
// server directly.
ostringstream request;
request
<< "CONNECT " << _request.get_url().get_server_and_port()
<< " " << _client->get_http_version_string() << "\r\n";
if (_client->get_http_version() >= HTTPEnum::HV_11) {
request
<< "Host: " << _request.get_url().get_server_and_port() << "\r\n";
}
_proxy_header = request.str();
make_proxy_request_text();
} else {
_proxy_header = string();
_proxy_request_text = string();
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::reset_for_new_request
// Access: Private
// Description: Resets the internal state variables in preparation
// for beginning a new request.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
reset_for_new_request() {
reset_download_to();
_last_status_code = 0;
_status_entry = StatusEntry();
_response_type = RT_none;
_redirect_trail.clear();
_bytes_downloaded = 0;
_bytes_requested = 0;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::finished_body
// Access: Private
// Description: This is called by the body reading
// classes--ChunkedStreamBuf and IdentityStreamBuf--when
// they have finished reading the body. It advances the
// state appropriately.
//
// has_trailer should be set true if the body type has
// an associated trailer which should be read or
// skipped, or false if there is no trailer.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
finished_body(bool has_trailer) {
if (will_close_connection() && _download_dest == DD_none) {
reset_to_new();
} else {
if (has_trailer) {
_state = HTTPChannel::S_read_body;
} else {
_state = HTTPChannel::S_read_trailer;
}
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::reset_download_position
// Access: Private
// Description: If a download has been requested, seeks within the
// download file to the appropriate first_byte position,
// so that downloaded bytes will be written to the
// appropriate point within the file. Returns true if
// the starting position is valid, false otherwise.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
reset_download_position(size_t first_byte) {
if (_subdocument_resumes) {
if (_download_dest == DD_file) {
// Windows doesn't complain if you try to seek past the end of
// file--it happily appends enough zero bytes to make the
// difference. Blecch. That means we need to get the file size
// first to check it ourselves.
_download_to_file.seekp(0, ios::end);
if (first_byte > (size_t)_download_to_file.tellp()) {
downloader_cat.info()
<< "Invalid starting position of byte " << first_byte
<< " within " << _download_to_filename << " (which has "
<< _download_to_file.tellp() << " bytes)\n";
_download_to_file.close();
return false;
}
_download_to_file.seekp(first_byte);
} else if (_download_dest == DD_ram) {
if (first_byte > _download_to_ramfile->_data.length()) {
downloader_cat.info()
<< "Invalid starting position of byte " << first_byte
<< " within Ramfile (which has "
<< _download_to_ramfile->_data.length() << " bytes)\n";
return false;
}
if (first_byte == 0) {
_download_to_ramfile->_data = string();
} else {
_download_to_ramfile->_data =
_download_to_ramfile->_data.substr(0, first_byte);
}
}
} else {
// If _subdocument_resumes is false, we should be sure to reset to
// the beginning of the file, regardless of the value of
// first_byte.
if (_download_dest == DD_file) {
_download_to_file.seekp(0);
} else if (_download_dest == DD_ram) {
_download_to_ramfile->_data = string();
}
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::server_getline
// Access: Private
// Description: Reads a single line from the server's reply. Returns
// true if the line is successfully retrieved, or false
// if a complete line has not yet been received or if
// the connection has been closed.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
server_getline(string &str) {
nassertr(!_source.is_null(), false);
int ch = (*_source)->get();
while (!(*_source)->eof() && !(*_source)->fail()) {
switch (ch) {
case '\n':
// end-of-line character, we're done.
str = _working_get;
_working_get = string();
{
// Trim trailing whitespace. We're not required to do this per the
// HTTP spec, but let's be generous.
size_t p = str.length();
while (p > 0 && isspace(str[p - 1])) {
--p;
}
str = str.substr(0, p);
}
if (downloader_cat.is_spam()) {
downloader_cat.spam() << "recv: " << str << "\n";
}
return true;
case '\r':
// Ignore CR characters.
break;
default:
_working_get += (char)ch;
}
ch = (*_source)->get();
}
check_socket();
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::server_getline_failsafe
// Access: Private
// Description: Reads a line from the server's reply. If the server
// disconnects or times out before sending a reply,
// moves on to the next proxy server (or sets failure
// mode) and returns false; otherwise, returns true.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
server_getline_failsafe(string &str) {
if (!server_getline(str)) {
if (_bio.is_null()) {
// Huh, the server hung up on us as soon as we tried to connect.
if (_response_type == RT_hangup) {
// This was our second immediate hangup in a row. Give up.
_status_entry._status_code = SC_lost_connection;
_state = S_try_next_proxy;
} else {
// Try again, once.
_response_type = RT_hangup;
}
} else {
double elapsed =
ClockObject::get_global_clock()->get_real_time() -
_sent_request_time;
if (elapsed > get_http_timeout()) {
// Time to give up.
downloader_cat.info()
<< "Timeout waiting for "
<< _request.get_url().get_server_and_port()
<< " in server_getline_failsafe (" << elapsed
<< " seconds elapsed).\n";
_status_entry._status_code = SC_timeout;
_state = S_try_next_proxy;
}
}
return false;
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::server_get
// Access: Private
// Description: Reads a fixed number of bytes from the server's
// reply. Returns true if the indicated number of bytes
// are successfully retrieved, or false if the complete
// set has not yet been received or if the connection
// has been closed.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
server_get(string &str, size_t num_bytes) {
nassertr(!_source.is_null(), false);
int ch = (*_source)->get();
while (!(*_source)->eof() && !(*_source)->fail()) {
_working_get += (char)ch;
if (_working_get.length() >= num_bytes) {
str = _working_get;
_working_get = string();
return true;
}
ch = (*_source)->get();
}
check_socket();
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::server_get_failsafe
// Access: Private
// Description: Reads a fixed number of bytes from the server. If
// the server disconnects or times out before sending a
// reply, moves on to the next proxy server (or sets
// failure mode) and returns false; otherwise, returns
// true.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
server_get_failsafe(string &str, size_t num_bytes) {
if (!server_get(str, num_bytes)) {
if (_bio.is_null()) {
// Huh, the server hung up on us as soon as we tried to connect.
if (_response_type == RT_hangup) {
// This was our second immediate hangup in a row. Give up.
_status_entry._status_code = SC_lost_connection;
_state = S_try_next_proxy;
} else {
// Try again, once.
_response_type = RT_hangup;
}
} else {
double elapsed =
ClockObject::get_global_clock()->get_real_time() -
_sent_request_time;
if (elapsed > get_http_timeout()) {
// Time to give up.
downloader_cat.info()
<< "Timeout waiting for "
<< _request.get_url().get_server_and_port()
<< " in server_get_failsafe (" << elapsed
<< " seconds elapsed).\n";
_status_entry._status_code = SC_timeout;
_state = S_try_next_proxy;
}
}
return false;
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::server_send
// Access: Private
// Description: Sends a series of lines to the server. Returns true
// if the buffer is fully sent, or false if some of it
// remains. If this returns false, the function must be
// called again later, passing in the exact same string,
// until the return value is true.
//
// If the secret flag is true, the data is not echoed to
// the log (even in spam mode). This may be desirable
// if the data may contain binary data, or if it may
// contain passwords etc.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
server_send(const string &str, bool secret) {
nassertr(str.length() > _sent_so_far, true);
// Use the underlying BIO to write to the server, instead of the
// BIOStream, which would insist on blocking (and might furthermore
// delay the send due to collect-tcp mode being enabled).
size_t bytes_to_send = str.length() - _sent_so_far;
int write_count =
BIO_write(*_bio, str.data() + _sent_so_far, bytes_to_send);
if (write_count <= 0) {
if (BIO_should_retry(*_bio)) {
// Temporary failure: the pipe is full. Wait till later.
return false;
}
// Oops, the connection has been closed!
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Lost connection to server unexpectedly during write.\n";
}
reset_to_new();
return false;
}
#ifndef NDEBUG
if (!secret && downloader_cat.is_spam()) {
show_send(str.substr(0, write_count));
}
#endif
if (write_count < (int)bytes_to_send) {
_sent_so_far += write_count;
return false;
}
// Buffer completely sent.
_sent_so_far = 0;
return true;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::parse_http_response
// Access: Private
// Description: Parses the first line sent back from an HTTP server
// or proxy and stores the result in _status_code and
// _http_version, etc. Returns true on success, false
// on invalid response.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
parse_http_response(const string &line) {
// The first line back should include the HTTP version and the
// result code.
if (line.length() < 5 || line.substr(0, 5) != string("HTTP/")) {
// Not an HTTP response.
_status_entry._status_code = SC_non_http_response;
if (_response_type == RT_non_http) {
// This was our second non-HTTP response in a row. Give up.
_state = S_try_next_proxy;
} else {
// Maybe we were just in some bad state. Drop the connection
// and try again, once.
reset_to_new();
_response_type = RT_non_http;
}
return false;
}
// Split out the first line into its three components.
size_t p = 5;
while (p < line.length() && !isspace(line[p])) {
p++;
}
_http_version_string = line.substr(0, p);
_http_version = HTTPClient::parse_http_version_string(_http_version_string);
while (p < line.length() && isspace(line[p])) {
p++;
}
size_t q = p;
while (q < line.length() && !isspace(line[q])) {
q++;
}
string status_code = line.substr(p, q - p);
_status_entry._status_code = atoi(status_code.c_str());
while (q < line.length() && isspace(line[q])) {
q++;
}
_status_entry._status_string = line.substr(q, line.length() - q);
return true;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::parse_http_header
// Access: Private
// Description: Reads the series of header lines from the server and
// stores them in _headers. Returns true if there is
// more to read, false when done.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
parse_http_header() {
string line;
if (!server_getline(line)) {
return true;
}
while (!line.empty()) {
if (isspace(line[0])) {
// If the line begins with a space, that continues the previous
// field.
size_t p = 0;
while (p < line.length() && isspace(line[p])) {
p++;
}
_current_field_value += line.substr(p - 1);
} else {
// If the line does not begin with a space, that defines a new
// field.
if (!_current_field_name.empty()) {
store_header_field(_current_field_name, _current_field_value);
_current_field_value = string();
}
size_t colon = line.find(':');
if (colon != string::npos) {
_current_field_name = downcase(line.substr(0, colon));
size_t p = colon + 1;
while (p < line.length() && isspace(line[p])) {
p++;
}
_current_field_value = line.substr(p);
}
}
if (!server_getline(line)) {
return true;
}
}
// After reading an empty line, we're done with the headers.
if (!_current_field_name.empty()) {
store_header_field(_current_field_name, _current_field_value);
_current_field_value = string();
}
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::parse_content_range
// Access: Private
// Description: Interprets the "Content-Range" header in the reply,
// and fills in _first_byte_delivered and
// _last_byte_delivered appropriately if the header
// response can be understood.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
parse_content_range(const string &content_range) {
// First, get the units indication.
size_t p = 0;
while (p < content_range.length() && !isspace(content_range[p])) {
p++;
}
string units = content_range.substr(0, p);
while (p < content_range.length() && isspace(content_range[p])) {
p++;
}
if (units == "bytes") {
const char *c_str = content_range.c_str();
char *endptr;
if (p < content_range.length() && isdigit(content_range[p])) {
long first_byte = strtol(c_str + p, &endptr, 10);
p = endptr - c_str;
if (p < content_range.length() && content_range[p] == '-') {
p++;
if (p < content_range.length() && isdigit(content_range[p])) {
long last_byte = strtol(c_str + p, &endptr, 10);
p = endptr - c_str;
if (last_byte >= first_byte) {
_first_byte_delivered = first_byte;
_last_byte_delivered = last_byte;
return true;
}
}
}
}
}
// Invalid or unhandled response.
return false;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::check_socket
// Access: Private
// Description: Checks whether the connection to the server has been
// closed after a failed read. If it has, issues a
// warning and calls reset_to_new().
////////////////////////////////////////////////////////////////////
void HTTPChannel::
check_socket() {
nassertv(!_source.is_null());
if ((*_source)->is_closed()) {
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Lost connection to server unexpectedly during read.\n";
}
reset_to_new();
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::verify_server
// Access: Private
// Description: Returns true if the indicated server matches one of
// our expected servers (or the list of expected servers
// is empty), or false if it does not match any of our
// expected servers.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
verify_server(X509_NAME *subject) const {
if (_client->_expected_servers.empty()) {
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "No expected servers on list; allowing any https connection.\n";
}
return true;
}
if (downloader_cat.is_debug()) {
downloader_cat.debug() << "checking server: " << flush;
X509_NAME_print_ex_fp(stderr, subject, 0, 0);
fflush(stderr);
downloader_cat.debug(false) << "\n";
}
HTTPClient::ExpectedServers::const_iterator ei;
for (ei = _client->_expected_servers.begin();
ei != _client->_expected_servers.end();
++ei) {
X509_NAME *expected_name = (*ei);
if (x509_name_subset(expected_name, subject)) {
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "Match found!\n";
}
return true;
}
}
// None of the lines matched.
if (downloader_cat.is_debug()) {
downloader_cat.debug()
<< "No match found against any of the following expected servers:\n";
for (ei = _client->_expected_servers.begin();
ei != _client->_expected_servers.end();
++ei) {
X509_NAME *expected_name = (*ei);
X509_NAME_print_ex_fp(stderr, expected_name, 0, 0);
fputs("\n", stderr);
}
fflush(stderr);
}
return false;
}
/*
Certificate verify error codes:
0 X509_V_OK: ok
the operation was successful.
2 X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: unable to get issuer certificate
the issuer certificate could not be found: this occurs if the
issuer certificate of an untrusted certificate cannot be found.
3 X509_V_ERR_UNABLE_TO_GET_CRL unable to get certificate CRL
the CRL of a certificate could not be found. Unused.
4 X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE: unable to decrypt
certificate's signature
the certificate signature could not be decrypted. This means that
the actual signature value could not be determined rather than it
not matching the expected value, this is only meaningful for RSA
keys.
5 X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE: unable to decrypt CRL's signature
the CRL signature could not be decrypted: this means that the
actual signature value could not be determined rather than it not
matching the expected value. Unused.
6 X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY: unable to decode
issuer public key
the public key in the certificate SubjectPublicKeyInfo could not
be read.
7 X509_V_ERR_CERT_SIGNATURE_FAILURE: certificate signature failure
the signature of the certificate is invalid.
8 X509_V_ERR_CRL_SIGNATURE_FAILURE: CRL signature failure
the signature of the certificate is invalid. Unused.
9 X509_V_ERR_CERT_NOT_YET_VALID: certificate is not yet valid
the certificate is not yet valid: the notBefore date is after the
current time.
10 X509_V_ERR_CERT_HAS_EXPIRED: certificate has expired
the certificate has expired: that is the notAfter date is before
the current time.
11 X509_V_ERR_CRL_NOT_YET_VALID: CRL is not yet valid
the CRL is not yet valid. Unused.
12 X509_V_ERR_CRL_HAS_EXPIRED: CRL has expired
the CRL has expired. Unused.
13 X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD: format error in
certificate's notBefore field
the certificate notBefore field contains an invalid time.
14 X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD: format error in
certificate's notAfter field
the certificate notAfter field contains an invalid time.
15 X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD: format error in CRL's
lastUpdate field
the CRL lastUpdate field contains an invalid time. Unused.
16 X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD: format error in CRL's
nextUpdate field
the CRL nextUpdate field contains an invalid time. Unused.
17 X509_V_ERR_OUT_OF_MEM: out of memory
an error occurred trying to allocate memory. This should never
happen.
18 X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: self signed certificate
the passed certificate is self signed and the same certificate
cannot be found in the list of trusted certificates.
19 X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN: self signed certificate in
certificate chain
the certificate chain could be built up using the untrusted
certificates but the root could not be found locally.
20 X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: unable to get local
issuer certificate
the issuer certificate of a locally looked up certificate could
not be found. This normally means the list of trusted certificates
is not complete.
21 X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE: unable to verify the
first certificate
no signatures could be verified because the chain contains only
one certificate and it is not self signed.
22 X509_V_ERR_CERT_CHAIN_TOO_LONG: certificate chain too long
the certificate chain length is greater than the supplied maximum
depth. Unused.
23 X509_V_ERR_CERT_REVOKED: certificate revoked
the certificate has been revoked. Unused.
24 X509_V_ERR_INVALID_CA: invalid CA certificate
a CA certificate is invalid. Either it is not a CA or its
extensions are not consistent with the supplied purpose.
25 X509_V_ERR_PATH_LENGTH_EXCEEDED: path length constraint exceeded
the basicConstraints pathlength parameter has been exceeded.
26 X509_V_ERR_INVALID_PURPOSE: unsupported certificate purpose
the supplied certificate cannot be used for the specified purpose.
27 X509_V_ERR_CERT_UNTRUSTED: certificate not trusted
the root CA is not marked as trusted for the specified purpose.
28 X509_V_ERR_CERT_REJECTED: certificate rejected
the root CA is marked to reject the specified purpose.
29 X509_V_ERR_SUBJECT_ISSUER_MISMATCH: subject issuer mismatch
the current candidate issuer certificate was rejected because its
subject name did not match the issuer name of the current
certificate. Only displayed when the -issuer_checks option is set.
30 X509_V_ERR_AKID_SKID_MISMATCH: authority and subject key identifier
mismatch
the current candidate issuer certificate was rejected because its
subject key identifier was present and did not match the authority
key identifier current certificate. Only displayed when the
-issuer_checks option is set.
31 X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH: authority and issuer serial
number mismatch
the current candidate issuer certificate was rejected because its
issuer name and serial number was present and did not match the
authority key identifier of the current certificate. Only
displayed when the -issuer_checks option is set.
32 X509_V_ERR_KEYUSAGE_NO_CERTSIGN:key usage does not include
certificate signing
the current candidate issuer certificate was rejected because its
keyUsage extension does not permit certificate signing.
50 X509_V_ERR_APPLICATION_VERIFICATION: application verification failure
an application specific error. Unused.
*/
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::get_x509_name_component
// Access: Private, Static
// Description: Returns the indicated component of the X509 name as a
// string, if defined, or empty string if it is not.
////////////////////////////////////////////////////////////////////
string HTTPChannel::
get_x509_name_component(X509_NAME *name, int nid) {
ASN1_OBJECT *obj = OBJ_nid2obj(nid);
if (obj == NULL) {
// Unknown nid. See openssl/objects.h.
return string();
}
int i = X509_NAME_get_index_by_OBJ(name, obj, -1);
if (i < 0) {
return string();
}
ASN1_STRING *data = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(name, i));
return string((char *)data->data, data->length);
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::x509_name_subset
// Access: Private, Static
// Description: Returns true if name_a is a subset of name_b: each
// property of name_a is defined in name_b, and the
// defined value is equivalent to that of name_a.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
x509_name_subset(X509_NAME *name_a, X509_NAME *name_b) {
int count_a = X509_NAME_entry_count(name_a);
for (int ai = 0; ai < count_a; ai++) {
X509_NAME_ENTRY *na = X509_NAME_get_entry(name_a, ai);
int bi = X509_NAME_get_index_by_OBJ(name_b, na->object, -1);
if (bi < 0) {
// This entry in name_a is not defined in name_b.
return false;
}
X509_NAME_ENTRY *nb = X509_NAME_get_entry(name_b, bi);
if (na->value->length != nb->value->length ||
memcmp(na->value->data, nb->value->data, na->value->length) != 0) {
// This entry in name_a doesn't match that of name_b.
return false;
}
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::make_header
// Access: Private
// Description: Formats the appropriate GET or POST (or whatever)
// request to send to the server, based on the current
// _method, _document_spec, _body, and _proxy settings.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
make_header() {
_proxy_auth = _client->select_auth(_proxy, true, _proxy_realm);
_proxy_username = string();
if (_proxy_auth != (HTTPAuthorization *)NULL) {
_proxy_realm = _proxy_auth->get_realm();
_proxy_username = _client->select_username(_proxy, true, _proxy_realm);
}
if (_method == HTTPEnum::M_connect) {
// This method doesn't require an HTTP header at all; we'll just
// open a plain connection. (Except when we're using a proxy; but
// in that case, it's the proxy_header we'll need, not the regular
// HTTP header.)
_header = string();
return;
}
_www_auth = _client->select_auth(_request.get_url(), false, _www_realm);
_www_username = string();
if (_www_auth != (HTTPAuthorization *)NULL) {
_www_realm = _www_auth->get_realm();
_www_username = _client->select_username(_request.get_url(), false, _www_realm);
}
string request_path;
if (_proxy_serves_document) {
// If we'll be asking the proxy for the document, we need its full
// URL--but we omit the username, which is information just for us.
URLSpec url_no_username = _request.get_url();
url_no_username.set_username(string());
request_path = url_no_username.get_url();
} else {
// If we'll be asking the server directly for the document, we
// just want its path relative to the server.
request_path = _request.get_url().get_path_and_query();
}
// HTTP syntax always requires something in the request path. If it
// is empty, put in a star as a placeholder (OPTIONS, for instance,
// uses this).
if (request_path.empty()) {
request_path = "*";
}
ostringstream stream;
stream
<< _method << " " << request_path << " "
<< _client->get_http_version_string() << "\r\n";
if (_client->get_http_version() >= HTTPEnum::HV_11) {
stream
<< "Host: " << _request.get_url().get_server_and_port() << "\r\n";
if (!get_persistent_connection()) {
stream
<< "Connection: close\r\n";
}
}
if (_last_byte_requested != 0) {
stream
<< "Range: bytes=" << _first_byte_requested << "-"
<< _last_byte_requested << "\r\n";
} else if (_first_byte_requested != 0) {
stream
<< "Range: bytes=" << _first_byte_requested << "-\r\n";
}
switch (_request.get_request_mode()) {
case DocumentSpec::RM_any:
// No particular request; give us any document that matches the
// URL.
if (_first_byte_requested != 0) {
// Unless we're requesting a subrange, in which case if the
// exact document matches, retrieve the subrange indicated;
// otherwise, retrieve the entire document.
if (_request.has_tag()) {
stream
<< "If-Range: " << _request.get_tag().get_string() << "\r\n";
} else if (_request.has_date()) {
stream
<< "If-Range: " << _request.get_date().get_string() << "\r\n";
}
}
break;
case DocumentSpec::RM_equal:
// Give us only this particular version of the document, or
// nothing.
if (_request.has_tag()) {
stream
<< "If-Match: " << _request.get_tag().get_string() << "\r\n";
}
if (_request.has_date()) {
stream
<< "If-Unmodified-Since: " << _request.get_date().get_string()
<< "\r\n";
}
break;
case DocumentSpec::RM_newer:
// Give us anything newer than this document, or nothing.
if (_request.has_tag()) {
stream
<< "If-None-Match: " << _request.get_tag().get_string() << "\r\n";
}
if (_request.has_date()) {
stream
<< "If-Modified-Since: " << _request.get_date().get_string()
<< "\r\n";
}
break;
case DocumentSpec::RM_equal_or_newer:
// Just don't give us anything older.
if (_request.has_date()) {
// This is a little unreliable: we ask for any document that's
// been modified since one second before our last-modified-date.
// Who knows whether the server will honor this properly.
stream
<< "If-Modified-Since: " << (_request.get_date() - 1).get_string()
<< "\r\n";
}
break;
}
switch (_request.get_cache_control()) {
case DocumentSpec::CC_allow_cache:
// Normal, caching behavior.
break;
case DocumentSpec::CC_revalidate:
// Request the server to revalidate its cache before returning it.
stream
<< "Cache-Control: max-age=0\r\n";
break;
case DocumentSpec::CC_no_cache:
// Request the server to get a fresh copy regardless of its cache.
stream
<< "Cache-Control: no-cache\r\n"
<< "Pragma: no-cache\r\n";
break;
}
_client->send_cookies(stream, _request.get_url());
if (!_body.empty()) {
stream
<< "Content-Type: application/x-www-form-urlencoded\r\n"
<< "Content-Length: " << _body.length() << "\r\n";
}
_header = stream.str();
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::make_proxy_request_text
// Access: Private
// Description: Builds the _proxy_request_text string. This is a
// special request that will be sent directly to the
// proxy prior to the request tailored for the server.
// Generally this is used to open a tunnelling
// connection for https-over-proxy.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
make_proxy_request_text() {
_proxy_request_text = _proxy_header;
if (_proxy_auth != (HTTPAuthorization *)NULL && !_proxy_username.empty()) {
_proxy_request_text += "Proxy-Authorization: ";
_proxy_request_text +=
_proxy_auth->generate(HTTPEnum::M_connect, _request.get_url().get_server_and_port(),
_proxy_username, _body);
_proxy_request_text += "\r\n";
}
_proxy_request_text += "\r\n";
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::make_request_text
// Access: Private
// Description: Builds the _request_text string. This is the
// specific request that will be sent to the server this
// pass, based on the current header and body.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
make_request_text() {
_request_text = _header;
if (_proxy_serves_document &&
_proxy_auth != (HTTPAuthorization *)NULL && !_proxy_username.empty()) {
_request_text += "Proxy-Authorization: ";
_request_text +=
_proxy_auth->generate(_method, _request.get_url().get_url(), _proxy_username, _body);
_request_text += "\r\n";
}
if (_www_auth != (HTTPAuthorization *)NULL && !_www_username.empty()) {
string authorization =
_request_text += "Authorization: ";
_request_text +=
_www_auth->generate(_method, _request.get_url().get_path_and_query(), _www_username, _body);
_request_text += "\r\n";
}
_request_text += _send_extra_headers;
_request_text += "\r\n";
_request_text += _body;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::reset_url
// Access: Private
// Description: Redirects the next connection to the indicated URL
// (from the previous URL). This resets the socket if
// necessary when we are about to switch servers.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
reset_url(const URLSpec &old_url, const URLSpec &new_url) {
// If we change between http and https, we have to reset the
// connection regardless of proxy. Otherwise, we have to drop the
// connection if the server or port changes, unless we're
// communicating through a proxy.
if (new_url.get_scheme() != old_url.get_scheme() ||
(_proxy.empty() && (new_url.get_server() != old_url.get_server() ||
new_url.get_port() != old_url.get_port()))) {
reset_to_new();
}
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::store_header_field
// Access: Private
// Description: Stores a single name: value pair in the header list,
// or appends the value to the end of the existing
// value, if the header has been repeated.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
store_header_field(const string &field_name, const string &field_value) {
pair<Headers::iterator, bool> insert_result =
_headers.insert(Headers::value_type(field_name, field_value));
if (!insert_result.second) {
// It didn't insert; thus, the field already existed. Append the
// new value.
Headers::iterator hi = insert_result.first;
(*hi).second += ", ";
(*hi).second += field_value;
}
if (field_name == "set-cookie") {
_client->set_cookie(HTTPCookie(field_value, _request.get_url()));
}
}
#ifndef NDEBUG
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::show_send
// Access: Private, Static
// Description: Writes the outgoing message, one line at a time, to
// the debugging log.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
show_send(const string &message) {
size_t start = 0;
size_t newline = message.find('\n', start);
while (newline != string::npos) {
// Assume every \n is preceded by a \r.
downloader_cat.spam()
<< "send: " << message.substr(start, newline - start - 1) << "\n";
start = newline + 1;
newline = message.find('\n', start);
}
if (start < message.length()) {
downloader_cat.spam()
<< "send: " << message.substr(start) << " (no newline)\n";
}
}
#endif // NDEBUG
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::reset_download_to
// Access: Private
// Description: Resets the indication of how the document will be
// downloaded. This must be re-specified after each
// get_document() (or related) call.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
reset_download_to() {
_started_download = false;
_download_to_file.close();
_download_to_ramfile = (Ramfile *)NULL;
_download_dest = DD_none;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::reset_to_new
// Access: Private
// Description: Closes the connection and resets the state to S_new.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
reset_to_new() {
close_connection();
_state = S_new;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::close_connection
// Access: Private
// Description: Closes the connection but leaves the _state
// unchanged.
////////////////////////////////////////////////////////////////////
void HTTPChannel::
close_connection() {
if (_body_stream != (ISocketStream *)NULL) {
delete _body_stream;
_body_stream = (ISocketStream *)NULL;
}
_source.clear();
_bio.clear();
_working_get = string();
_sent_so_far = 0;
_read_index++;
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::more_useful_status_code
// Access: Private, Static
// Description: Returns true if status code a is a more useful value
// (that is, it represents a more-nearly successfully
// connection attempt, or contains more information)
// than b, or false otherwise.
////////////////////////////////////////////////////////////////////
bool HTTPChannel::
more_useful_status_code(int a, int b) {
if (a >= 100 && b >= 100) {
// Both represent HTTP responses. Responses from a server (<
// 1000) are better than those from a proxy; we take advantage of
// the fact that we have already added 1000 to proxy responses.
// Except for 407, so let's fix that now.
if (a == 407) {
a += 1000;
}
if (b == 407) {
b += 1000;
}
// Now just check the series.
int series_a = (a / 100);
int series_b = (b / 100);
// In general, a lower series is a closer success.
return (series_a < series_b);
}
if (a < 100 && b < 100) {
// Both represent non-HTTP responses. Here a larger number is
// better.
return (a > b);
}
if (a < 100) {
// a is a non-HTTP response, while b is an HTTP response. HTTP is
// generally, better, unless we exceeded SC_http_error_watermark.
return (a > SC_http_error_watermark);
}
// Exactly the opposite case as above.
return (b < SC_http_error_watermark);
}
////////////////////////////////////////////////////////////////////
// Function: HTTPChannel::State output operator
// Description:
////////////////////////////////////////////////////////////////////
ostream &
operator << (ostream &out, HTTPChannel::State state) {
#ifdef NDEBUG
return out << (int)state;
#else
switch (state) {
case HTTPChannel::S_new:
return out << "new";
case HTTPChannel::S_try_next_proxy:
return out << "try_next_proxy";
case HTTPChannel::S_connecting:
return out << "connecting";
case HTTPChannel::S_connecting_wait:
return out << "connecting_wait";
case HTTPChannel::S_http_proxy_ready:
return out << "http_proxy_ready";
case HTTPChannel::S_http_proxy_request_sent:
return out << "http_proxy_request_sent";
case HTTPChannel::S_http_proxy_reading_header:
return out << "http_proxy_reading_header";
case HTTPChannel::S_socks_proxy_greet:
return out << "socks_proxy_greet";
case HTTPChannel::S_socks_proxy_greet_reply:
return out << "socks_proxy_greet_reply";
case HTTPChannel::S_socks_proxy_connect:
return out << "socks_proxy_connect";
case HTTPChannel::S_socks_proxy_connect_reply:
return out << "socks_proxy_connect_reply";
case HTTPChannel::S_setup_ssl:
return out << "setup_ssl";
case HTTPChannel::S_ssl_handshake:
return out << "ssl_handshake";
case HTTPChannel::S_ready:
return out << "ready";
case HTTPChannel::S_request_sent:
return out << "request_sent";
case HTTPChannel::S_reading_header:
return out << "reading_header";
case HTTPChannel::S_read_header:
return out << "read_header";
case HTTPChannel::S_begin_body:
return out << "begin_body";
case HTTPChannel::S_reading_body:
return out << "reading_body";
case HTTPChannel::S_read_body:
return out << "read_body";
case HTTPChannel::S_read_trailer:
return out << "read_trailer";
case HTTPChannel::S_failure:
return out << "failure";
}
return out << "invalid state(" << (int)state << ")";
#endif // NDEBUG
}
#endif // HAVE_OPENSSL