diff --git a/tools-webrtc/sanitizers/lsan_suppressions_webrtc.cc b/tools-webrtc/sanitizers/lsan_suppressions_webrtc.cc index 8affacae71..49dee1b562 100644 --- a/tools-webrtc/sanitizers/lsan_suppressions_webrtc.cc +++ b/tools-webrtc/sanitizers/lsan_suppressions_webrtc.cc @@ -49,6 +49,12 @@ char kLSanDefaultSuppressions[] = // pre-existing leaks. // rtc_unittest +// https://code.google.com/p/webrtc/issues/detail?id=3827 for details. +"leak:rtc::unstarted_task_test_DoNotDeleteTask2_Test::TestBody\n" +"leak:rtc::HttpServer::HandleConnection\n" +"leak:rtc::HttpServer::Connection::onHttpHeaderComplete\n" +"leak:rtc::HttpResponseData::set_success\n" +"leak:rtc::HttpData::changeHeader\n" // https://code.google.com/p/webrtc/issues/detail?id=4149 for details. "leak:StartDNSLookup\n" // https://code.google.com/p/webrtc/issues/detail?id=2527 diff --git a/tools-webrtc/valgrind/memcheck/suppressions.txt b/tools-webrtc/valgrind/memcheck/suppressions.txt index f1eff06edf..6182f56d70 100644 --- a/tools-webrtc/valgrind/memcheck/suppressions.txt +++ b/tools-webrtc/valgrind/memcheck/suppressions.txt @@ -31,6 +31,14 @@ #----------------------------------------------------------------------- # webrtc stuff +{ + bug_6784 + Memcheck:Leak + fun:_Znw* + fun:_ZN3rtc10HttpServer16HandleConnectionEPNS_15StreamInterfaceE + fun:_ZN3rtc12_GLOBAL__N_122CreateClientConnectionERNS_10HttpServerERNS0_17HttpServerMonitorEb + fun:_ZN3rtc47HttpServer_SignalsCloseAfterForcedCloseAll_Test8TestBodyEv +} { bug_6773_3 Memcheck:Uninitialized @@ -154,6 +162,13 @@ fun:BIO_new_mem_buf fun:_ZN9rtc15OpenSSLIdentity14FromPEMStringsERKSsS2_ } +{ + SignalsCloseAfterForcedCloseAll + Memcheck:Leak + fun:_Znw* + fun:_ZN3rtc10HttpServer10Connection12BeginProcessEPNS_15StreamInterfaceE + ... +} { SignalsCloseAfterForcedCloseAll2 Memcheck:Leak @@ -468,6 +483,21 @@ fun:_Znw* fun:_ZN6webrtc51AudioEncoderCopyRedDeathTest_NullSpeechEncoder_Test8TestBodyEv } +{ + bug_5988 + Memcheck:Leak + fun:_Znw* + fun:_ZNSt8_Rb_treeISsSt4pairIKSsSsESt10_Select1stIS2_EN3rtc5ilessESaIS2_EE15_M_insert_equalIS2_EESt17_Rb_tree_iteratorIS2_EOT_ + fun:_ZN3rtc8HttpData12changeHeaderERKSsS2_NS0_13HeaderCombineE + fun:_ZN3rtc16HttpResponseData9set_errorEj + fun:_ZN3rtc12_GLOBAL__N_117HttpServerMonitor9OnRequestEPNS_10HttpServerEPNS_21HttpServerTransactionE + fun:_ZN3rtc10HttpServer10Connection14onHttpCompleteENS_8HttpModeENS_9HttpErrorE + fun:_ZN3rtc8HttpBase10OnCompleteENS_9HttpErrorE + fun:_ZN3rtc8HttpBase17OnHttpStreamEventEPNS_15StreamInterfaceEii + fun:_ZN7testing12StreamSource11QueueStringEPKc + fun:_ZN3rtc12_GLOBAL__N_122CreateClientConnectionERNS_10HttpServerERNS0_17HttpServerMonitorEb + fun:_ZN3rtc47HttpServer_SignalsCloseAfterForcedCloseAll_Test8TestBodyEv +} { bug_5989 Memcheck:Unaddressable @@ -511,4 +541,4 @@ fun:_ZN7testing8internal25UntypedFunctionMockerBase17UntypedInvokeWithEPKv fun:_ZNK6webrtc11MockRtpRtcp11FlexfecSsrcEv ... -} +} \ No newline at end of file diff --git a/webrtc/base/BUILD.gn b/webrtc/base/BUILD.gn index bf6d989027..83d9ebc3da 100644 --- a/webrtc/base/BUILD.gn +++ b/webrtc/base/BUILD.gn @@ -380,6 +380,8 @@ rtc_static_library("rtc_base") { "asyncudpsocket.h", "crc32.cc", "crc32.h", + "cryptstring.cc", + "cryptstring.h", "filerotatingstream.cc", "filerotatingstream.h", "fileutils.cc", @@ -387,6 +389,11 @@ rtc_static_library("rtc_base") { "gunit_prod.h", "helpers.cc", "helpers.h", + "httpbase.cc", + "httpbase.h", + "httpcommon-inl.h", + "httpcommon.cc", + "httpcommon.h", "ipaddress.cc", "ipaddress.h", "messagedigest.cc", @@ -414,6 +421,7 @@ rtc_static_library("rtc_base") { "opensslstreamadapter.h", "physicalsocketserver.cc", "physicalsocketserver.h", + "proxyinfo.cc", "proxyinfo.h", "ratelimiter.cc", "ratelimiter.h", @@ -675,6 +683,8 @@ if (rtc_include_tests) { "firewallsocketserver.cc", "firewallsocketserver.h", "gunit.h", + "httpserver.cc", + "httpserver.h", "memory_usage.cc", "memory_usage.h", "natserver.cc", @@ -828,6 +838,9 @@ if (rtc_include_tests) { "crc32_unittest.cc", "fileutils_unittest.cc", "helpers_unittest.cc", + "httpbase_unittest.cc", + "httpcommon_unittest.cc", + "httpserver_unittest.cc", "ipaddress_unittest.cc", "memory_usage_unittest.cc", "messagedigest_unittest.cc", @@ -835,6 +848,7 @@ if (rtc_include_tests) { "nat_unittest.cc", "network_unittest.cc", "optionsfile_unittest.cc", + "proxy_unittest.cc", "ratelimiter_unittest.cc", "rollingaccumulator_unittest.cc", "rtccertificate_unittest.cc", diff --git a/webrtc/base/cryptstring.cc b/webrtc/base/cryptstring.cc new file mode 100644 index 0000000000..4b2a83fca9 --- /dev/null +++ b/webrtc/base/cryptstring.cc @@ -0,0 +1,75 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "webrtc/base/cryptstring.h" + +namespace rtc { + +size_t EmptyCryptStringImpl::GetLength() const { + return 0; +} + +void EmptyCryptStringImpl::CopyTo(char* dest, bool nullterminate) const { + if (nullterminate) { + *dest = '\0'; + } +} + +std::string EmptyCryptStringImpl::UrlEncode() const { + return ""; +} + +CryptStringImpl* EmptyCryptStringImpl::Copy() const { + return new EmptyCryptStringImpl(); +} + +void EmptyCryptStringImpl::CopyRawTo(std::vector* dest) const { + dest->clear(); +} + +CryptString::CryptString() : impl_(new EmptyCryptStringImpl()) { +} + +CryptString::CryptString(const CryptString& other) + : impl_(other.impl_->Copy()) { +} + +CryptString::CryptString(const CryptStringImpl& impl) : impl_(impl.Copy()) { +} + +CryptString::~CryptString() = default; + +size_t InsecureCryptStringImpl::GetLength() const { + return password_.size(); +} + +void InsecureCryptStringImpl::CopyTo(char* dest, bool nullterminate) const { + memcpy(dest, password_.data(), password_.size()); + if (nullterminate) + dest[password_.size()] = 0; +} + +std::string InsecureCryptStringImpl::UrlEncode() const { + return password_; +} + +CryptStringImpl* InsecureCryptStringImpl::Copy() const { + InsecureCryptStringImpl* copy = new InsecureCryptStringImpl; + copy->password() = password_; + return copy; +} + +void InsecureCryptStringImpl::CopyRawTo( + std::vector* dest) const { + dest->resize(password_.size()); + memcpy(&dest->front(), password_.data(), password_.size()); +} + +}; // namespace rtc diff --git a/webrtc/base/cryptstring.h b/webrtc/base/cryptstring.h new file mode 100644 index 0000000000..e1ee309f65 --- /dev/null +++ b/webrtc/base/cryptstring.h @@ -0,0 +1,167 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef _WEBRTC_BASE_CRYPTSTRING_H_ +#define _WEBRTC_BASE_CRYPTSTRING_H_ + +#include + +#include +#include +#include + +namespace rtc { + +class CryptStringImpl { +public: + virtual ~CryptStringImpl() {} + virtual size_t GetLength() const = 0; + virtual void CopyTo(char * dest, bool nullterminate) const = 0; + virtual std::string UrlEncode() const = 0; + virtual CryptStringImpl * Copy() const = 0; + virtual void CopyRawTo(std::vector * dest) const = 0; +}; + +class EmptyCryptStringImpl : public CryptStringImpl { +public: + ~EmptyCryptStringImpl() override {} + size_t GetLength() const override; + void CopyTo(char* dest, bool nullterminate) const override; + std::string UrlEncode() const override; + CryptStringImpl* Copy() const override; + void CopyRawTo(std::vector* dest) const override; +}; + +class CryptString { + public: + CryptString(); + size_t GetLength() const { return impl_->GetLength(); } + void CopyTo(char * dest, bool nullterminate) const { impl_->CopyTo(dest, nullterminate); } + CryptString(const CryptString& other); + explicit CryptString(const CryptStringImpl& impl); + ~CryptString(); + CryptString & operator=(const CryptString & other) { + if (this != &other) { + impl_.reset(other.impl_->Copy()); + } + return *this; + } + void Clear() { impl_.reset(new EmptyCryptStringImpl()); } + std::string UrlEncode() const { return impl_->UrlEncode(); } + void CopyRawTo(std::vector * dest) const { + return impl_->CopyRawTo(dest); + } + + private: + std::unique_ptr impl_; +}; + + +// Used for constructing strings where a password is involved and we +// need to ensure that we zero memory afterwards +class FormatCryptString { +public: + FormatCryptString() { + storage_ = new char[32]; + capacity_ = 32; + length_ = 0; + storage_[0] = 0; + } + + void Append(const std::string & text) { + Append(text.data(), text.length()); + } + + void Append(const char * data, size_t length) { + EnsureStorage(length_ + length + 1); + memcpy(storage_ + length_, data, length); + length_ += length; + storage_[length_] = '\0'; + } + + void Append(const CryptString * password) { + size_t len = password->GetLength(); + EnsureStorage(length_ + len + 1); + password->CopyTo(storage_ + length_, true); + length_ += len; + } + + size_t GetLength() { + return length_; + } + + const char * GetData() { + return storage_; + } + + + // Ensures storage of at least n bytes + void EnsureStorage(size_t n) { + if (capacity_ >= n) { + return; + } + + size_t old_capacity = capacity_; + char * old_storage = storage_; + + for (;;) { + capacity_ *= 2; + if (capacity_ >= n) + break; + } + + storage_ = new char[capacity_]; + + if (old_capacity) { + memcpy(storage_, old_storage, length_); + + // zero memory in a way that an optimizer won't optimize it out + old_storage[0] = 0; + for (size_t i = 1; i < old_capacity; i++) { + old_storage[i] = old_storage[i - 1]; + } + delete[] old_storage; + } + } + + ~FormatCryptString() { + if (capacity_) { + storage_[0] = 0; + for (size_t i = 1; i < capacity_; i++) { + storage_[i] = storage_[i - 1]; + } + } + delete[] storage_; + } +private: + char * storage_; + size_t capacity_; + size_t length_; +}; + +class InsecureCryptStringImpl : public CryptStringImpl { + public: + std::string& password() { return password_; } + const std::string& password() const { return password_; } + + ~InsecureCryptStringImpl() override = default; + size_t GetLength() const override; + void CopyTo(char* dest, bool nullterminate) const override; + std::string UrlEncode() const override; + CryptStringImpl* Copy() const override; + void CopyRawTo(std::vector* dest) const override; + + private: + std::string password_; +}; + +} + +#endif // _WEBRTC_BASE_CRYPTSTRING_H_ diff --git a/webrtc/base/httpbase.cc b/webrtc/base/httpbase.cc new file mode 100644 index 0000000000..bc8ac64c71 --- /dev/null +++ b/webrtc/base/httpbase.cc @@ -0,0 +1,886 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include + +#if defined(WEBRTC_WIN) +#include "webrtc/base/win32.h" +#else // !WEBRTC_WIN +#define SEC_E_CERT_EXPIRED (-2146893016) +#endif // !WEBRTC_WIN + +#include "webrtc/base/checks.h" +#include "webrtc/base/httpbase.h" +#include "webrtc/base/logging.h" +#include "webrtc/base/socket.h" +#include "webrtc/base/stringutils.h" +#include "webrtc/base/thread.h" + +namespace rtc { + +////////////////////////////////////////////////////////////////////// +// Helpers +////////////////////////////////////////////////////////////////////// + +bool MatchHeader(const char* str, size_t len, HttpHeader header) { + const char* const header_str = ToString(header); + const size_t header_len = strlen(header_str); + return (len == header_len) && (_strnicmp(str, header_str, header_len) == 0); +} + +enum { + MSG_READ +}; + +////////////////////////////////////////////////////////////////////// +// HttpParser +////////////////////////////////////////////////////////////////////// + +HttpParser::HttpParser() { + reset(); +} + +HttpParser::~HttpParser() { +} + +void +HttpParser::reset() { + state_ = ST_LEADER; + chunked_ = false; + data_size_ = SIZE_UNKNOWN; +} + +HttpParser::ProcessResult +HttpParser::Process(const char* buffer, size_t len, size_t* processed, + HttpError* error) { + *processed = 0; + *error = HE_NONE; + + if (state_ >= ST_COMPLETE) { + RTC_NOTREACHED(); + return PR_COMPLETE; + } + + while (true) { + if (state_ < ST_DATA) { + size_t pos = *processed; + while ((pos < len) && (buffer[pos] != '\n')) { + pos += 1; + } + if (pos >= len) { + break; // don't have a full header + } + const char* line = buffer + *processed; + size_t len = (pos - *processed); + *processed = pos + 1; + while ((len > 0) && isspace(static_cast(line[len-1]))) { + len -= 1; + } + ProcessResult result = ProcessLine(line, len, error); + LOG(LS_VERBOSE) << "Processed line, result=" << result; + + if (PR_CONTINUE != result) { + return result; + } + } else if (data_size_ == 0) { + if (chunked_) { + state_ = ST_CHUNKTERM; + } else { + return PR_COMPLETE; + } + } else { + size_t available = len - *processed; + if (available <= 0) { + break; // no more data + } + if ((data_size_ != SIZE_UNKNOWN) && (available > data_size_)) { + available = data_size_; + } + size_t read = 0; + ProcessResult result = ProcessData(buffer + *processed, available, read, + error); + LOG(LS_VERBOSE) << "Processed data, result: " << result << " read: " + << read << " err: " << error; + + if (PR_CONTINUE != result) { + return result; + } + *processed += read; + if (data_size_ != SIZE_UNKNOWN) { + data_size_ -= read; + } + } + } + + return PR_CONTINUE; +} + +HttpParser::ProcessResult +HttpParser::ProcessLine(const char* line, size_t len, HttpError* error) { + LOG_F(LS_VERBOSE) << " state: " << state_ << " line: " + << std::string(line, len) << " len: " << len << " err: " + << error; + + switch (state_) { + case ST_LEADER: + state_ = ST_HEADERS; + return ProcessLeader(line, len, error); + + case ST_HEADERS: + if (len > 0) { + const char* value = strchrn(line, len, ':'); + if (!value) { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + size_t nlen = (value - line); + const char* eol = line + len; + do { + value += 1; + } while ((value < eol) && isspace(static_cast(*value))); + size_t vlen = eol - value; + if (MatchHeader(line, nlen, HH_CONTENT_LENGTH)) { + // sscanf isn't safe with strings that aren't null-terminated, and there + // is no guarantee that |value| is. + // Create a local copy that is null-terminated. + std::string value_str(value, vlen); + unsigned int temp_size; + if (sscanf(value_str.c_str(), "%u", &temp_size) != 1) { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + data_size_ = static_cast(temp_size); + } else if (MatchHeader(line, nlen, HH_TRANSFER_ENCODING)) { + if ((vlen == 7) && (_strnicmp(value, "chunked", 7) == 0)) { + chunked_ = true; + } else if ((vlen == 8) && (_strnicmp(value, "identity", 8) == 0)) { + chunked_ = false; + } else { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + } + return ProcessHeader(line, nlen, value, vlen, error); + } else { + state_ = chunked_ ? ST_CHUNKSIZE : ST_DATA; + return ProcessHeaderComplete(chunked_, data_size_, error); + } + break; + + case ST_CHUNKSIZE: + if (len > 0) { + char* ptr = nullptr; + data_size_ = strtoul(line, &ptr, 16); + if (ptr != line + len) { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + state_ = (data_size_ == 0) ? ST_TRAILERS : ST_DATA; + } else { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } + break; + + case ST_CHUNKTERM: + if (len > 0) { + *error = HE_PROTOCOL; + return PR_COMPLETE; + } else { + state_ = chunked_ ? ST_CHUNKSIZE : ST_DATA; + } + break; + + case ST_TRAILERS: + if (len == 0) { + return PR_COMPLETE; + } + // *error = onHttpRecvTrailer(); + break; + + default: + RTC_NOTREACHED(); + break; + } + + return PR_CONTINUE; +} + +bool +HttpParser::is_valid_end_of_input() const { + return (state_ == ST_DATA) && (data_size_ == SIZE_UNKNOWN); +} + +void +HttpParser::complete(HttpError error) { + if (state_ < ST_COMPLETE) { + state_ = ST_COMPLETE; + OnComplete(error); + } +} + +////////////////////////////////////////////////////////////////////// +// HttpBase::DocumentStream +////////////////////////////////////////////////////////////////////// + +class BlockingMemoryStream : public ExternalMemoryStream { +public: + BlockingMemoryStream(char* buffer, size_t size) + : ExternalMemoryStream(buffer, size) { } + + StreamResult DoReserve(size_t size, int* error) override { + return (buffer_length_ >= size) ? SR_SUCCESS : SR_BLOCK; + } +}; + +class HttpBase::DocumentStream : public StreamInterface { +public: + DocumentStream(HttpBase* base) : base_(base), error_(HE_DEFAULT) { } + + StreamState GetState() const override { + if (nullptr == base_) + return SS_CLOSED; + if (HM_RECV == base_->mode_) + return SS_OPEN; + return SS_OPENING; + } + + StreamResult Read(void* buffer, + size_t buffer_len, + size_t* read, + int* error) override { + if (!base_) { + if (error) *error = error_; + return (HE_NONE == error_) ? SR_EOS : SR_ERROR; + } + + if (HM_RECV != base_->mode_) { + return SR_BLOCK; + } + + // DoReceiveLoop writes http document data to the StreamInterface* document + // member of HttpData. In this case, we want this data to be written + // directly to our buffer. To accomplish this, we wrap our buffer with a + // StreamInterface, and replace the existing document with our wrapper. + // When the method returns, we restore the old document. Ideally, we would + // pass our StreamInterface* to DoReceiveLoop, but due to the callbacks + // of HttpParser, we would still need to store the pointer temporarily. + std::unique_ptr stream( + new BlockingMemoryStream(reinterpret_cast(buffer), buffer_len)); + + // Replace the existing document with our wrapped buffer. + base_->data_->document.swap(stream); + + // Pump the I/O loop. DoReceiveLoop is guaranteed not to attempt to + // complete the I/O process, which means that our wrapper is not in danger + // of being deleted. To ensure this, DoReceiveLoop returns true when it + // wants complete to be called. We make sure to uninstall our wrapper + // before calling complete(). + HttpError http_error; + bool complete = base_->DoReceiveLoop(&http_error); + + // Reinstall the original output document. + base_->data_->document.swap(stream); + + // If we reach the end of the receive stream, we disconnect our stream + // adapter from the HttpBase, and further calls to read will either return + // EOS or ERROR, appropriately. Finally, we call complete(). + StreamResult result = SR_BLOCK; + if (complete) { + HttpBase* base = Disconnect(http_error); + if (error) *error = error_; + result = (HE_NONE == error_) ? SR_EOS : SR_ERROR; + base->complete(http_error); + } + + // Even if we are complete, if some data was read we must return SUCCESS. + // Future Reads will return EOS or ERROR based on the error_ variable. + size_t position; + stream->GetPosition(&position); + if (position > 0) { + if (read) *read = position; + result = SR_SUCCESS; + } + return result; + } + + StreamResult Write(const void* data, + size_t data_len, + size_t* written, + int* error) override { + if (error) *error = -1; + return SR_ERROR; + } + + void Close() override { + if (base_) { + HttpBase* base = Disconnect(HE_NONE); + if (HM_RECV == base->mode_ && base->http_stream_) { + // Read I/O could have been stalled on the user of this DocumentStream, + // so restart the I/O process now that we've removed ourselves. + base->http_stream_->PostEvent(SE_READ, 0); + } + } + } + + bool GetAvailable(size_t* size) const override { + if (!base_ || HM_RECV != base_->mode_) + return false; + size_t data_size = base_->GetDataRemaining(); + if (SIZE_UNKNOWN == data_size) + return false; + if (size) + *size = data_size; + return true; + } + + HttpBase* Disconnect(HttpError error) { + RTC_DCHECK(nullptr != base_); + RTC_DCHECK(nullptr != base_->doc_stream_); + HttpBase* base = base_; + base_->doc_stream_ = nullptr; + base_ = nullptr; + error_ = error; + return base; + } + +private: + HttpBase* base_; + HttpError error_; +}; + +////////////////////////////////////////////////////////////////////// +// HttpBase +////////////////////////////////////////////////////////////////////// + +HttpBase::HttpBase() + : mode_(HM_NONE), + data_(nullptr), + notify_(nullptr), + http_stream_(nullptr), + doc_stream_(nullptr) {} + +HttpBase::~HttpBase() { + RTC_DCHECK(HM_NONE == mode_); +} + +bool +HttpBase::isConnected() const { + return (http_stream_ != nullptr) && (http_stream_->GetState() == SS_OPEN); +} + +bool +HttpBase::attach(StreamInterface* stream) { + if ((mode_ != HM_NONE) || (http_stream_ != nullptr) || (stream == nullptr)) { + RTC_NOTREACHED(); + return false; + } + http_stream_ = stream; + http_stream_->SignalEvent.connect(this, &HttpBase::OnHttpStreamEvent); + mode_ = (http_stream_->GetState() == SS_OPENING) ? HM_CONNECT : HM_NONE; + return true; +} + +StreamInterface* +HttpBase::detach() { + RTC_DCHECK(HM_NONE == mode_); + if (mode_ != HM_NONE) { + return nullptr; + } + StreamInterface* stream = http_stream_; + http_stream_ = nullptr; + if (stream) { + stream->SignalEvent.disconnect(this); + } + return stream; +} + +void +HttpBase::send(HttpData* data) { + RTC_DCHECK(HM_NONE == mode_); + if (mode_ != HM_NONE) { + return; + } else if (!isConnected()) { + OnHttpStreamEvent(http_stream_, SE_CLOSE, HE_DISCONNECTED); + return; + } + + mode_ = HM_SEND; + data_ = data; + len_ = 0; + ignore_data_ = chunk_data_ = false; + + if (data_->document) { + data_->document->SignalEvent.connect(this, &HttpBase::OnDocumentEvent); + } + + std::string encoding; + if (data_->hasHeader(HH_TRANSFER_ENCODING, &encoding) + && (encoding == "chunked")) { + chunk_data_ = true; + } + + len_ = data_->formatLeader(buffer_, sizeof(buffer_)); + len_ += strcpyn(buffer_ + len_, sizeof(buffer_) - len_, "\r\n"); + + header_ = data_->begin(); + if (header_ == data_->end()) { + // We must call this at least once, in the case where there are no headers. + queue_headers(); + } + + flush_data(); +} + +void +HttpBase::recv(HttpData* data) { + RTC_DCHECK(HM_NONE == mode_); + if (mode_ != HM_NONE) { + return; + } else if (!isConnected()) { + OnHttpStreamEvent(http_stream_, SE_CLOSE, HE_DISCONNECTED); + return; + } + + mode_ = HM_RECV; + data_ = data; + len_ = 0; + ignore_data_ = chunk_data_ = false; + + reset(); + if (doc_stream_) { + doc_stream_->SignalEvent(doc_stream_, SE_OPEN | SE_READ, 0); + } else { + read_and_process_data(); + } +} + +void +HttpBase::abort(HttpError err) { + if (mode_ != HM_NONE) { + if (http_stream_ != nullptr) { + http_stream_->Close(); + } + do_complete(err); + } +} + +StreamInterface* HttpBase::GetDocumentStream() { + if (doc_stream_) + return nullptr; + doc_stream_ = new DocumentStream(this); + return doc_stream_; +} + +HttpError HttpBase::HandleStreamClose(int error) { + if (http_stream_ != nullptr) { + http_stream_->Close(); + } + if (error == 0) { + if ((mode_ == HM_RECV) && is_valid_end_of_input()) { + return HE_NONE; + } else { + return HE_DISCONNECTED; + } + } else if (error == SOCKET_EACCES) { + return HE_AUTH; + } else if (error == SEC_E_CERT_EXPIRED) { + return HE_CERTIFICATE_EXPIRED; + } + LOG_F(LS_ERROR) << "(" << error << ")"; + return (HM_CONNECT == mode_) ? HE_CONNECT_FAILED : HE_SOCKET_ERROR; +} + +bool HttpBase::DoReceiveLoop(HttpError* error) { + RTC_DCHECK(HM_RECV == mode_); + RTC_DCHECK(nullptr != error); + + // Do to the latency between receiving read notifications from + // pseudotcpchannel, we rely on repeated calls to read in order to acheive + // ideal throughput. The number of reads is limited to prevent starving + // the caller. + + size_t loop_count = 0; + const size_t kMaxReadCount = 20; + bool process_requires_more_data = false; + do { + // The most frequent use of this function is response to new data available + // on http_stream_. Therefore, we optimize by attempting to read from the + // network first (as opposed to processing existing data first). + + if (len_ < sizeof(buffer_)) { + // Attempt to buffer more data. + size_t read; + int read_error; + StreamResult read_result = http_stream_->Read(buffer_ + len_, + sizeof(buffer_) - len_, + &read, &read_error); + switch (read_result) { + case SR_SUCCESS: + RTC_DCHECK(len_ + read <= sizeof(buffer_)); + len_ += read; + break; + case SR_BLOCK: + if (process_requires_more_data) { + // We're can't make progress until more data is available. + return false; + } + // Attempt to process the data already in our buffer. + break; + case SR_EOS: + // Clean close, with no error. + read_error = 0; + FALLTHROUGH(); // Fall through to HandleStreamClose. + case SR_ERROR: + *error = HandleStreamClose(read_error); + return true; + } + } else if (process_requires_more_data) { + // We have too much unprocessed data in our buffer. This should only + // occur when a single HTTP header is longer than the buffer size (32K). + // Anything longer than that is almost certainly an error. + *error = HE_OVERFLOW; + return true; + } + + // Process data in our buffer. Process is not guaranteed to process all + // the buffered data. In particular, it will wait until a complete + // protocol element (such as http header, or chunk size) is available, + // before processing it in its entirety. Also, it is valid and sometimes + // necessary to call Process with an empty buffer, since the state machine + // may have interrupted state transitions to complete. + size_t processed; + ProcessResult process_result = Process(buffer_, len_, &processed, + error); + RTC_DCHECK(processed <= len_); + len_ -= processed; + memmove(buffer_, buffer_ + processed, len_); + switch (process_result) { + case PR_CONTINUE: + // We need more data to make progress. + process_requires_more_data = true; + break; + case PR_BLOCK: + // We're stalled on writing the processed data. + return false; + case PR_COMPLETE: + // *error already contains the correct code. + return true; + } + } while (++loop_count <= kMaxReadCount); + + LOG_F(LS_WARNING) << "danger of starvation"; + return false; +} + +void +HttpBase::read_and_process_data() { + HttpError error; + if (DoReceiveLoop(&error)) { + complete(error); + } +} + +void +HttpBase::flush_data() { + RTC_DCHECK(HM_SEND == mode_); + + // When send_required is true, no more buffering can occur without a network + // write. + bool send_required = (len_ >= sizeof(buffer_)); + + while (true) { + RTC_DCHECK(len_ <= sizeof(buffer_)); + + // HTTP is inherently sensitive to round trip latency, since a frequent use + // case is for small requests and responses to be sent back and forth, and + // the lack of pipelining forces a single request to take a minimum of the + // round trip time. As a result, it is to our benefit to pack as much data + // into each packet as possible. Thus, we defer network writes until we've + // buffered as much data as possible. + + if (!send_required && (header_ != data_->end())) { + // First, attempt to queue more header data. + send_required = queue_headers(); + } + + if (!send_required && data_->document) { + // Next, attempt to queue document data. + + const size_t kChunkDigits = 8; + size_t offset, reserve; + if (chunk_data_) { + // Reserve characters at the start for X-byte hex value and \r\n + offset = len_ + kChunkDigits + 2; + // ... and 2 characters at the end for \r\n + reserve = offset + 2; + } else { + offset = len_; + reserve = offset; + } + + if (reserve >= sizeof(buffer_)) { + send_required = true; + } else { + size_t read; + int error; + StreamResult result = data_->document->Read(buffer_ + offset, + sizeof(buffer_) - reserve, + &read, &error); + if (result == SR_SUCCESS) { + RTC_DCHECK(reserve + read <= sizeof(buffer_)); + if (chunk_data_) { + // Prepend the chunk length in hex. + // Note: sprintfn appends a null terminator, which is why we can't + // combine it with the line terminator. + sprintfn(buffer_ + len_, kChunkDigits + 1, "%.*x", + kChunkDigits, read); + // Add line terminator to the chunk length. + memcpy(buffer_ + len_ + kChunkDigits, "\r\n", 2); + // Add line terminator to the end of the chunk. + memcpy(buffer_ + offset + read, "\r\n", 2); + } + len_ = reserve + read; + } else if (result == SR_BLOCK) { + // Nothing to do but flush data to the network. + send_required = true; + } else if (result == SR_EOS) { + if (chunk_data_) { + // Append the empty chunk and empty trailers, then turn off + // chunking. + RTC_DCHECK(len_ + 5 <= sizeof(buffer_)); + memcpy(buffer_ + len_, "0\r\n\r\n", 5); + len_ += 5; + chunk_data_ = false; + } else if (0 == len_) { + // No more data to read, and no more data to write. + do_complete(); + return; + } + // Although we are done reading data, there is still data which needs + // to be flushed to the network. + send_required = true; + } else { + LOG_F(LS_ERROR) << "Read error: " << error; + do_complete(HE_STREAM); + return; + } + } + } + + if (0 == len_) { + // No data currently available to send. + if (!data_->document) { + // If there is no source document, that means we're done. + do_complete(); + } + return; + } + + size_t written; + int error; + StreamResult result = http_stream_->Write(buffer_, len_, &written, &error); + if (result == SR_SUCCESS) { + RTC_DCHECK(written <= len_); + len_ -= written; + memmove(buffer_, buffer_ + written, len_); + send_required = false; + } else if (result == SR_BLOCK) { + if (send_required) { + // Nothing more we can do until network is writeable. + return; + } + } else { + RTC_DCHECK(result == SR_ERROR); + LOG_F(LS_ERROR) << "error"; + OnHttpStreamEvent(http_stream_, SE_CLOSE, error); + return; + } + } + + RTC_NOTREACHED(); +} + +bool +HttpBase::queue_headers() { + RTC_DCHECK(HM_SEND == mode_); + while (header_ != data_->end()) { + size_t len = sprintfn(buffer_ + len_, sizeof(buffer_) - len_, + "%.*s: %.*s\r\n", + header_->first.size(), header_->first.data(), + header_->second.size(), header_->second.data()); + if (len_ + len < sizeof(buffer_) - 3) { + len_ += len; + ++header_; + } else if (len_ == 0) { + LOG(WARNING) << "discarding header that is too long: " << header_->first; + ++header_; + } else { + // Not enough room for the next header, write to network first. + return true; + } + } + // End of headers + len_ += strcpyn(buffer_ + len_, sizeof(buffer_) - len_, "\r\n"); + return false; +} + +void +HttpBase::do_complete(HttpError err) { + RTC_DCHECK(mode_ != HM_NONE); + HttpMode mode = mode_; + mode_ = HM_NONE; + if (data_ && data_->document) { + data_->document->SignalEvent.disconnect(this); + } + data_ = nullptr; + if ((HM_RECV == mode) && doc_stream_) { + RTC_DCHECK(HE_NONE != + err); // We should have Disconnected doc_stream_ already. + DocumentStream* ds = doc_stream_; + ds->Disconnect(err); + ds->SignalEvent(ds, SE_CLOSE, err); + } + if (notify_) { + notify_->onHttpComplete(mode, err); + } +} + +// +// Stream Signals +// + +void +HttpBase::OnHttpStreamEvent(StreamInterface* stream, int events, int error) { + RTC_DCHECK(stream == http_stream_); + if ((events & SE_OPEN) && (mode_ == HM_CONNECT)) { + do_complete(); + return; + } + + if ((events & SE_WRITE) && (mode_ == HM_SEND)) { + flush_data(); + return; + } + + if ((events & SE_READ) && (mode_ == HM_RECV)) { + if (doc_stream_) { + doc_stream_->SignalEvent(doc_stream_, SE_READ, 0); + } else { + read_and_process_data(); + } + return; + } + + if ((events & SE_CLOSE) == 0) + return; + + HttpError http_error = HandleStreamClose(error); + if (mode_ == HM_RECV) { + complete(http_error); + } else if (mode_ != HM_NONE) { + do_complete(http_error); + } else if (notify_) { + notify_->onHttpClosed(http_error); + } +} + +void +HttpBase::OnDocumentEvent(StreamInterface* stream, int events, int error) { + RTC_DCHECK(stream == data_->document.get()); + if ((events & SE_WRITE) && (mode_ == HM_RECV)) { + read_and_process_data(); + return; + } + + if ((events & SE_READ) && (mode_ == HM_SEND)) { + flush_data(); + return; + } + + if (events & SE_CLOSE) { + LOG_F(LS_ERROR) << "Read error: " << error; + do_complete(HE_STREAM); + return; + } +} + +// +// HttpParser Implementation +// + +HttpParser::ProcessResult +HttpBase::ProcessLeader(const char* line, size_t len, HttpError* error) { + *error = data_->parseLeader(line, len); + return (HE_NONE == *error) ? PR_CONTINUE : PR_COMPLETE; +} + +HttpParser::ProcessResult +HttpBase::ProcessHeader(const char* name, size_t nlen, const char* value, + size_t vlen, HttpError* error) { + std::string sname(name, nlen), svalue(value, vlen); + data_->addHeader(sname, svalue); + return PR_CONTINUE; +} + +HttpParser::ProcessResult +HttpBase::ProcessHeaderComplete(bool chunked, size_t& data_size, + HttpError* error) { + StreamInterface* old_docstream = doc_stream_; + if (notify_) { + *error = notify_->onHttpHeaderComplete(chunked, data_size); + // The request must not be aborted as a result of this callback. + RTC_DCHECK(nullptr != data_); + } + if ((HE_NONE == *error) && data_->document) { + data_->document->SignalEvent.connect(this, &HttpBase::OnDocumentEvent); + } + if (HE_NONE != *error) { + return PR_COMPLETE; + } + if (old_docstream != doc_stream_) { + // Break out of Process loop, since our I/O model just changed. + return PR_BLOCK; + } + return PR_CONTINUE; +} + +HttpParser::ProcessResult +HttpBase::ProcessData(const char* data, size_t len, size_t& read, + HttpError* error) { + if (ignore_data_ || !data_->document) { + read = len; + return PR_CONTINUE; + } + int write_error = 0; + switch (data_->document->Write(data, len, &read, &write_error)) { + case SR_SUCCESS: + return PR_CONTINUE; + case SR_BLOCK: + return PR_BLOCK; + case SR_EOS: + LOG_F(LS_ERROR) << "Unexpected EOS"; + *error = HE_STREAM; + return PR_COMPLETE; + case SR_ERROR: + default: + LOG_F(LS_ERROR) << "Write error: " << write_error; + *error = HE_STREAM; + return PR_COMPLETE; + } +} + +void +HttpBase::OnComplete(HttpError err) { + LOG_F(LS_VERBOSE); + do_complete(err); +} + +} // namespace rtc diff --git a/webrtc/base/httpbase.h b/webrtc/base/httpbase.h new file mode 100644 index 0000000000..4b834a4e5b --- /dev/null +++ b/webrtc/base/httpbase.h @@ -0,0 +1,187 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + + +#ifndef WEBRTC_BASE_HTTPBASE_H__ +#define WEBRTC_BASE_HTTPBASE_H__ + +#include "webrtc/base/httpcommon.h" + +namespace rtc { + +class StreamInterface; + +/////////////////////////////////////////////////////////////////////////////// +// HttpParser - Parses an HTTP stream provided via Process and end_of_input, and +// generates events for: +// Structural Elements: Leader, Headers, Document Data +// Events: End of Headers, End of Document, Errors +/////////////////////////////////////////////////////////////////////////////// + +class HttpParser { +public: + enum ProcessResult { PR_CONTINUE, PR_BLOCK, PR_COMPLETE }; + HttpParser(); + virtual ~HttpParser(); + + void reset(); + ProcessResult Process(const char* buffer, size_t len, size_t* processed, + HttpError* error); + bool is_valid_end_of_input() const; + void complete(HttpError err); + + size_t GetDataRemaining() const { return data_size_; } + +protected: + ProcessResult ProcessLine(const char* line, size_t len, HttpError* error); + + // HttpParser Interface + virtual ProcessResult ProcessLeader(const char* line, size_t len, + HttpError* error) = 0; + virtual ProcessResult ProcessHeader(const char* name, size_t nlen, + const char* value, size_t vlen, + HttpError* error) = 0; + virtual ProcessResult ProcessHeaderComplete(bool chunked, size_t& data_size, + HttpError* error) = 0; + virtual ProcessResult ProcessData(const char* data, size_t len, size_t& read, + HttpError* error) = 0; + virtual void OnComplete(HttpError err) = 0; + +private: + enum State { + ST_LEADER, ST_HEADERS, + ST_CHUNKSIZE, ST_CHUNKTERM, ST_TRAILERS, + ST_DATA, ST_COMPLETE + } state_; + bool chunked_; + size_t data_size_; +}; + +/////////////////////////////////////////////////////////////////////////////// +// IHttpNotify +/////////////////////////////////////////////////////////////////////////////// + +enum HttpMode { HM_NONE, HM_CONNECT, HM_RECV, HM_SEND }; + +class IHttpNotify { +public: + virtual ~IHttpNotify() {} + virtual HttpError onHttpHeaderComplete(bool chunked, size_t& data_size) = 0; + virtual void onHttpComplete(HttpMode mode, HttpError err) = 0; + virtual void onHttpClosed(HttpError err) = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// +// HttpBase - Provides a state machine for implementing HTTP-based components. +// Attach HttpBase to a StreamInterface which represents a bidirectional HTTP +// stream, and then call send() or recv() to initiate sending or receiving one +// side of an HTTP transaction. By default, HttpBase operates as an I/O pump, +// moving data from the HTTP stream to the HttpData object and vice versa. +// However, it can also operate in stream mode, in which case the user of the +// stream interface drives I/O via calls to Read(). +/////////////////////////////////////////////////////////////////////////////// + +class HttpBase +: private HttpParser, + public sigslot::has_slots<> +{ +public: + HttpBase(); + ~HttpBase() override; + + void notify(IHttpNotify* notify) { notify_ = notify; } + bool attach(StreamInterface* stream); + StreamInterface* stream() { return http_stream_; } + StreamInterface* detach(); + bool isConnected() const; + + void send(HttpData* data); + void recv(HttpData* data); + void abort(HttpError err); + + HttpMode mode() const { return mode_; } + + void set_ignore_data(bool ignore) { ignore_data_ = ignore; } + bool ignore_data() const { return ignore_data_; } + + // Obtaining this stream puts HttpBase into stream mode until the stream + // is closed. HttpBase can only expose one open stream interface at a time. + // Further calls will return null. + StreamInterface* GetDocumentStream(); + +protected: + // Do cleanup when the http stream closes (error may be 0 for a clean + // shutdown), and return the error code to signal. + HttpError HandleStreamClose(int error); + + // DoReceiveLoop acts as a data pump, pulling data from the http stream, + // pushing it through the HttpParser, and then populating the HttpData object + // based on the callbacks from the parser. One of the most interesting + // callbacks is ProcessData, which provides the actual http document body. + // This data is then written to the HttpData::document. As a result, data + // flows from the network to the document, with some incidental protocol + // parsing in between. + // Ideally, we would pass in the document* to DoReceiveLoop, to more easily + // support GetDocumentStream(). However, since the HttpParser is callback + // driven, we are forced to store the pointer somewhere until the callback + // is triggered. + // Returns true if the received document has finished, and + // HttpParser::complete should be called. + bool DoReceiveLoop(HttpError* err); + + void read_and_process_data(); + void flush_data(); + bool queue_headers(); + void do_complete(HttpError err = HE_NONE); + + void OnHttpStreamEvent(StreamInterface* stream, int events, int error); + void OnDocumentEvent(StreamInterface* stream, int events, int error); + + // HttpParser Interface + ProcessResult ProcessLeader(const char* line, + size_t len, + HttpError* error) override; + ProcessResult ProcessHeader(const char* name, + size_t nlen, + const char* value, + size_t vlen, + HttpError* error) override; + ProcessResult ProcessHeaderComplete(bool chunked, + size_t& data_size, + HttpError* error) override; + ProcessResult ProcessData(const char* data, + size_t len, + size_t& read, + HttpError* error) override; + void OnComplete(HttpError err) override; + +private: + class DocumentStream; + friend class DocumentStream; + + enum { kBufferSize = 32 * 1024 }; + + HttpMode mode_; + HttpData* data_; + IHttpNotify* notify_; + StreamInterface* http_stream_; + DocumentStream* doc_stream_; + char buffer_[kBufferSize]; + size_t len_; + + bool ignore_data_, chunk_data_; + HttpData::const_iterator header_; +}; + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace rtc + +#endif // WEBRTC_BASE_HTTPBASE_H__ diff --git a/webrtc/base/httpbase_unittest.cc b/webrtc/base/httpbase_unittest.cc new file mode 100644 index 0000000000..52c1c53fb1 --- /dev/null +++ b/webrtc/base/httpbase_unittest.cc @@ -0,0 +1,526 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include + +#include "webrtc/base/gunit.h" +#include "webrtc/base/httpbase.h" +#include "webrtc/base/testutils.h" + +namespace rtc { + +const char* const kHttpResponse = + "HTTP/1.1 200\r\n" + "Connection: Keep-Alive\r\n" + "Content-Type: text/plain\r\n" + "Proxy-Authorization: 42\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "00000008\r\n" + "Goodbye!\r\n" + "0\r\n\r\n"; + +const char* const kHttpEmptyResponse = + "HTTP/1.1 200\r\n" + "Connection: Keep-Alive\r\n" + "Content-Length: 0\r\n" + "Proxy-Authorization: 42\r\n" + "\r\n"; + +const char* const kHttpResponsePrefix = + "HTTP/1.1 200\r\n" + "Connection: Keep-Alive\r\n" + "Content-Type: text/plain\r\n" + "Proxy-Authorization: 42\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "8\r\n" + "Goodbye!\r\n"; + +class HttpBaseTest : public testing::Test, public IHttpNotify { +public: + enum EventType { E_HEADER_COMPLETE, E_COMPLETE, E_CLOSED }; + struct Event { + EventType event; + bool chunked; + size_t data_size; + HttpMode mode; + HttpError err; + }; + HttpBaseTest() : mem(nullptr), obtain_stream(false), http_stream(nullptr) {} + + virtual void SetUp() { } + virtual void TearDown() { + delete http_stream; + // Avoid an ASSERT, in case a test doesn't clean up properly + base.abort(HE_NONE); + } + + virtual HttpError onHttpHeaderComplete(bool chunked, size_t& data_size) { + LOG_F(LS_VERBOSE) << "chunked: " << chunked << " size: " << data_size; + Event e = { E_HEADER_COMPLETE, chunked, data_size, HM_NONE, HE_NONE}; + events.push_back(e); + if (obtain_stream) { + ObtainDocumentStream(); + } + return HE_NONE; + } + virtual void onHttpComplete(HttpMode mode, HttpError err) { + LOG_F(LS_VERBOSE) << "mode: " << mode << " err: " << err; + Event e = { E_COMPLETE, false, 0, mode, err }; + events.push_back(e); + } + virtual void onHttpClosed(HttpError err) { + LOG_F(LS_VERBOSE) << "err: " << err; + Event e = { E_CLOSED, false, 0, HM_NONE, err }; + events.push_back(e); + } + + void SetupSource(const char* response); + + void VerifyHeaderComplete(size_t event_count, bool empty_doc); + void VerifyDocumentContents(const char* expected_data, + size_t expected_length = SIZE_UNKNOWN); + + void ObtainDocumentStream(); + void VerifyDocumentStreamIsOpening(); + void VerifyDocumentStreamOpenEvent(); + void ReadDocumentStreamData(const char* expected_data); + void VerifyDocumentStreamIsEOS(); + + void SetupDocument(const char* response); + void VerifySourceContents(const char* expected_data, + size_t expected_length = SIZE_UNKNOWN); + + void VerifyTransferComplete(HttpMode mode, HttpError error); + + HttpBase base; + MemoryStream* mem; + HttpResponseData data; + + // The source of http data, and source events + testing::StreamSource src; + std::vector events; + + // Document stream, and stream events + bool obtain_stream; + StreamInterface* http_stream; + testing::StreamSink sink; +}; + +void HttpBaseTest::SetupSource(const char* http_data) { + LOG_F(LS_VERBOSE) << "Enter"; + + src.SetState(SS_OPENING); + src.QueueString(http_data); + + base.notify(this); + base.attach(&src); + EXPECT_TRUE(events.empty()); + + src.SetState(SS_OPEN); + ASSERT_EQ(1U, events.size()); + EXPECT_EQ(E_COMPLETE, events[0].event); + EXPECT_EQ(HM_CONNECT, events[0].mode); + EXPECT_EQ(HE_NONE, events[0].err); + events.clear(); + + mem = new MemoryStream; + data.document.reset(mem); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyHeaderComplete(size_t event_count, bool empty_doc) { + LOG_F(LS_VERBOSE) << "Enter"; + + ASSERT_EQ(event_count, events.size()); + EXPECT_EQ(E_HEADER_COMPLETE, events[0].event); + + std::string header; + EXPECT_EQ(HVER_1_1, data.version); + EXPECT_EQ(static_cast(HC_OK), data.scode); + EXPECT_TRUE(data.hasHeader(HH_PROXY_AUTHORIZATION, &header)); + EXPECT_EQ("42", header); + EXPECT_TRUE(data.hasHeader(HH_CONNECTION, &header)); + EXPECT_EQ("Keep-Alive", header); + + if (empty_doc) { + EXPECT_FALSE(events[0].chunked); + EXPECT_EQ(0U, events[0].data_size); + + EXPECT_TRUE(data.hasHeader(HH_CONTENT_LENGTH, &header)); + EXPECT_EQ("0", header); + } else { + EXPECT_TRUE(events[0].chunked); + EXPECT_EQ(SIZE_UNKNOWN, events[0].data_size); + + EXPECT_TRUE(data.hasHeader(HH_CONTENT_TYPE, &header)); + EXPECT_EQ("text/plain", header); + EXPECT_TRUE(data.hasHeader(HH_TRANSFER_ENCODING, &header)); + EXPECT_EQ("chunked", header); + } + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyDocumentContents(const char* expected_data, + size_t expected_length) { + LOG_F(LS_VERBOSE) << "Enter"; + + if (SIZE_UNKNOWN == expected_length) { + expected_length = strlen(expected_data); + } + EXPECT_EQ(mem, data.document.get()); + + size_t length; + mem->GetSize(&length); + EXPECT_EQ(expected_length, length); + EXPECT_TRUE(0 == memcmp(expected_data, mem->GetBuffer(), length)); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::ObtainDocumentStream() { + LOG_F(LS_VERBOSE) << "Enter"; + EXPECT_FALSE(http_stream); + http_stream = base.GetDocumentStream(); + ASSERT_TRUE(nullptr != http_stream); + sink.Monitor(http_stream); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyDocumentStreamIsOpening() { + LOG_F(LS_VERBOSE) << "Enter"; + ASSERT_TRUE(nullptr != http_stream); + EXPECT_EQ(0, sink.Events(http_stream)); + EXPECT_EQ(SS_OPENING, http_stream->GetState()); + + size_t read = 0; + char buffer[5] = { 0 }; + EXPECT_EQ(SR_BLOCK, + http_stream->Read(buffer, sizeof(buffer), &read, nullptr)); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyDocumentStreamOpenEvent() { + LOG_F(LS_VERBOSE) << "Enter"; + + ASSERT_TRUE(nullptr != http_stream); + EXPECT_EQ(SE_OPEN | SE_READ, sink.Events(http_stream)); + EXPECT_EQ(SS_OPEN, http_stream->GetState()); + + // HTTP headers haven't arrived yet + EXPECT_EQ(0U, events.size()); + EXPECT_EQ(static_cast(HC_INTERNAL_SERVER_ERROR), data.scode); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::ReadDocumentStreamData(const char* expected_data) { + LOG_F(LS_VERBOSE) << "Enter"; + + ASSERT_TRUE(nullptr != http_stream); + EXPECT_EQ(SS_OPEN, http_stream->GetState()); + + // Pump the HTTP I/O using Read, and verify the results. + size_t verified_length = 0; + const size_t expected_length = strlen(expected_data); + while (verified_length < expected_length) { + size_t read = 0; + char buffer[5] = { 0 }; + size_t amt_to_read = + std::min(expected_length - verified_length, sizeof(buffer)); + EXPECT_EQ(SR_SUCCESS, + http_stream->Read(buffer, amt_to_read, &read, nullptr)); + EXPECT_EQ(amt_to_read, read); + EXPECT_TRUE(0 == memcmp(expected_data + verified_length, buffer, read)); + verified_length += read; + } + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyDocumentStreamIsEOS() { + LOG_F(LS_VERBOSE) << "Enter"; + + ASSERT_TRUE(nullptr != http_stream); + size_t read = 0; + char buffer[5] = { 0 }; + EXPECT_EQ(SR_EOS, http_stream->Read(buffer, sizeof(buffer), &read, nullptr)); + EXPECT_EQ(SS_CLOSED, http_stream->GetState()); + + // When EOS is caused by Read, we don't expect SE_CLOSE + EXPECT_EQ(0, sink.Events(http_stream)); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::SetupDocument(const char* document_data) { + LOG_F(LS_VERBOSE) << "Enter"; + src.SetState(SS_OPEN); + + base.notify(this); + base.attach(&src); + EXPECT_TRUE(events.empty()); + + if (document_data) { + // Note: we could just call data.set_success("text/plain", mem), but that + // won't allow us to use the chunked transfer encoding. + mem = new MemoryStream(document_data); + data.document.reset(mem); + data.setHeader(HH_CONTENT_TYPE, "text/plain"); + data.setHeader(HH_TRANSFER_ENCODING, "chunked"); + } else { + data.setHeader(HH_CONTENT_LENGTH, "0"); + } + data.scode = HC_OK; + data.setHeader(HH_PROXY_AUTHORIZATION, "42"); + data.setHeader(HH_CONNECTION, "Keep-Alive"); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifySourceContents(const char* expected_data, + size_t expected_length) { + LOG_F(LS_VERBOSE) << "Enter"; + if (SIZE_UNKNOWN == expected_length) { + expected_length = strlen(expected_data); + } + std::string contents = src.ReadData(); + EXPECT_EQ(expected_length, contents.length()); + EXPECT_TRUE(0 == memcmp(expected_data, contents.data(), expected_length)); + LOG_F(LS_VERBOSE) << "Exit"; +} + +void HttpBaseTest::VerifyTransferComplete(HttpMode mode, HttpError error) { + LOG_F(LS_VERBOSE) << "Enter"; + // Verify that http operation has completed + ASSERT_TRUE(events.size() > 0); + size_t last_event = events.size() - 1; + EXPECT_EQ(E_COMPLETE, events[last_event].event); + EXPECT_EQ(mode, events[last_event].mode); + EXPECT_EQ(error, events[last_event].err); + LOG_F(LS_VERBOSE) << "Exit"; +} + +// +// Tests +// + +TEST_F(HttpBaseTest, SupportsSend) { + // Queue response document + SetupDocument("Goodbye!"); + + // Begin send + base.send(&data); + + // Send completed successfully + VerifyTransferComplete(HM_SEND, HE_NONE); + VerifySourceContents(kHttpResponse); +} + +TEST_F(HttpBaseTest, SupportsSendNoDocument) { + // Queue response document + SetupDocument(nullptr); + + // Begin send + base.send(&data); + + // Send completed successfully + VerifyTransferComplete(HM_SEND, HE_NONE); + VerifySourceContents(kHttpEmptyResponse); +} + +TEST_F(HttpBaseTest, SignalsCompleteOnInterruptedSend) { + // This test is attempting to expose a bug that occurs when a particular + // base objects is used for receiving, and then used for sending. In + // particular, the HttpParser state is different after receiving. Simulate + // that here. + SetupSource(kHttpResponse); + base.recv(&data); + VerifyTransferComplete(HM_RECV, HE_NONE); + + src.Clear(); + data.clear(true); + events.clear(); + base.detach(); + + // Queue response document + SetupDocument("Goodbye!"); + + // Prevent entire response from being sent + const size_t kInterruptedLength = strlen(kHttpResponse) - 1; + src.SetWriteBlock(kInterruptedLength); + + // Begin send + base.send(&data); + + // Document is mostly complete, but no completion signal yet. + EXPECT_TRUE(events.empty()); + VerifySourceContents(kHttpResponse, kInterruptedLength); + + src.SetState(SS_CLOSED); + + // Send completed with disconnect error, and no additional data. + VerifyTransferComplete(HM_SEND, HE_DISCONNECTED); + EXPECT_TRUE(src.ReadData().empty()); +} + +TEST_F(HttpBaseTest, SupportsReceiveViaDocumentPush) { + // Queue response document + SetupSource(kHttpResponse); + + // Begin receive + base.recv(&data); + + // Document completed successfully + VerifyHeaderComplete(2, false); + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents("Goodbye!"); +} + +TEST_F(HttpBaseTest, SupportsReceiveViaStreamPull) { + // Switch to pull mode + ObtainDocumentStream(); + VerifyDocumentStreamIsOpening(); + + // Queue response document + SetupSource(kHttpResponse); + VerifyDocumentStreamIsOpening(); + + // Begin receive + base.recv(&data); + + // Pull document data + VerifyDocumentStreamOpenEvent(); + ReadDocumentStreamData("Goodbye!"); + VerifyDocumentStreamIsEOS(); + + // Document completed successfully + VerifyHeaderComplete(2, false); + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents(""); +} + +TEST_F(HttpBaseTest, DISABLED_AllowsCloseStreamBeforeDocumentIsComplete) { + + // TODO: Remove extra logging once test failure is understood + LoggingSeverity old_sev = rtc::LogMessage::GetLogToDebug(); + rtc::LogMessage::LogToDebug(LS_VERBOSE); + + + // Switch to pull mode + ObtainDocumentStream(); + VerifyDocumentStreamIsOpening(); + + // Queue response document + SetupSource(kHttpResponse); + VerifyDocumentStreamIsOpening(); + + // Begin receive + base.recv(&data); + + // Pull some of the data + VerifyDocumentStreamOpenEvent(); + ReadDocumentStreamData("Goodb"); + + // We've seen the header by now + VerifyHeaderComplete(1, false); + + // Close the pull stream, this will transition back to push I/O. + http_stream->Close(); + Thread::Current()->ProcessMessages(0); + + // Remainder of document completed successfully + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents("ye!"); + + rtc::LogMessage::LogToDebug(old_sev); +} + +TEST_F(HttpBaseTest, AllowsGetDocumentStreamInResponseToHttpHeader) { + // Queue response document + SetupSource(kHttpResponse); + + // Switch to pull mode in response to header arrival + obtain_stream = true; + + // Begin receive + base.recv(&data); + + // We've already seen the header, but not data has arrived + VerifyHeaderComplete(1, false); + VerifyDocumentContents(""); + + // Pull the document data + ReadDocumentStreamData("Goodbye!"); + VerifyDocumentStreamIsEOS(); + + // Document completed successfully + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents(""); +} + +TEST_F(HttpBaseTest, AllowsGetDocumentStreamWithEmptyDocumentBody) { + // Queue empty response document + SetupSource(kHttpEmptyResponse); + + // Switch to pull mode in response to header arrival + obtain_stream = true; + + // Begin receive + base.recv(&data); + + // We've already seen the header, but not data has arrived + VerifyHeaderComplete(1, true); + VerifyDocumentContents(""); + + // The document is still open, until we attempt to read + ASSERT_TRUE(nullptr != http_stream); + EXPECT_EQ(SS_OPEN, http_stream->GetState()); + + // Attempt to read data, and discover EOS + VerifyDocumentStreamIsEOS(); + + // Document completed successfully + VerifyTransferComplete(HM_RECV, HE_NONE); + VerifyDocumentContents(""); +} + +TEST_F(HttpBaseTest, SignalsDocumentStreamCloseOnUnexpectedClose) { + // Switch to pull mode + ObtainDocumentStream(); + VerifyDocumentStreamIsOpening(); + + // Queue response document + SetupSource(kHttpResponsePrefix); + VerifyDocumentStreamIsOpening(); + + // Begin receive + base.recv(&data); + + // Pull document data + VerifyDocumentStreamOpenEvent(); + ReadDocumentStreamData("Goodbye!"); + + // Simulate unexpected close + src.SetState(SS_CLOSED); + + // Observe error event on document stream + EXPECT_EQ(testing::SSE_ERROR, sink.Events(http_stream)); + + // Future reads give an error + int error = 0; + char buffer[5] = { 0 }; + EXPECT_EQ(SR_ERROR, + http_stream->Read(buffer, sizeof(buffer), nullptr, &error)); + EXPECT_EQ(HE_DISCONNECTED, error); + + // Document completed with error + VerifyHeaderComplete(2, false); + VerifyTransferComplete(HM_RECV, HE_DISCONNECTED); + VerifyDocumentContents(""); +} + +} // namespace rtc diff --git a/webrtc/base/httpcommon-inl.h b/webrtc/base/httpcommon-inl.h new file mode 100644 index 0000000000..f29f075998 --- /dev/null +++ b/webrtc/base/httpcommon-inl.h @@ -0,0 +1,132 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_BASE_HTTPCOMMON_INL_H__ +#define WEBRTC_BASE_HTTPCOMMON_INL_H__ + +#include "webrtc/base/arraysize.h" +#include "webrtc/base/checks.h" +#include "webrtc/base/httpcommon.h" + +namespace rtc { + +/////////////////////////////////////////////////////////////////////////////// +// Url +/////////////////////////////////////////////////////////////////////////////// + +template +void Url::do_set_url(const CTYPE* val, size_t len) { + if (ascnicmp(val, "http://", 7) == 0) { + val += 7; len -= 7; + secure_ = false; + } else if (ascnicmp(val, "https://", 8) == 0) { + val += 8; len -= 8; + secure_ = true; + } else { + clear(); + return; + } + const CTYPE* path = strchrn(val, len, static_cast('/')); + if (!path) { + path = val + len; + } + size_t address_length = (path - val); + do_set_address(val, address_length); + do_set_full_path(path, len - address_length); +} + +template +void Url::do_set_address(const CTYPE* val, size_t len) { + if (const CTYPE* at = strchrn(val, len, static_cast('@'))) { + // Everything before the @ is a user:password combo, so skip it. + len -= at - val + 1; + val = at + 1; + } + if (const CTYPE* colon = strchrn(val, len, static_cast(':'))) { + host_.assign(val, colon - val); + // Note: In every case, we're guaranteed that colon is followed by a null, + // or non-numeric character. + port_ = static_cast(::strtoul(colon + 1, nullptr, 10)); + // TODO: Consider checking for invalid data following port number. + } else { + host_.assign(val, len); + port_ = HttpDefaultPort(secure_); + } +} + +template +void Url::do_set_full_path(const CTYPE* val, size_t len) { + const CTYPE* query = strchrn(val, len, static_cast('?')); + if (!query) { + query = val + len; + } + size_t path_length = (query - val); + if (0 == path_length) { + // TODO: consider failing in this case. + path_.assign(1, static_cast('/')); + } else { + RTC_DCHECK(val[0] == static_cast('/')); + path_.assign(val, path_length); + } + query_.assign(query, len - path_length); +} + +template +void Url::do_get_url(string* val) const { + CTYPE protocol[9]; + asccpyn(protocol, arraysize(protocol), secure_ ? "https://" : "http://"); + val->append(protocol); + do_get_address(val); + do_get_full_path(val); +} + +template +void Url::do_get_address(string* val) const { + val->append(host_); + if (port_ != HttpDefaultPort(secure_)) { + CTYPE format[5], port[32]; + asccpyn(format, arraysize(format), ":%hu"); + sprintfn(port, arraysize(port), format, port_); + val->append(port); + } +} + +template +void Url::do_get_full_path(string* val) const { + val->append(path_); + val->append(query_); +} + +template +bool Url::get_attribute(const string& name, string* value) const { + if (query_.empty()) + return false; + + std::string::size_type pos = query_.find(name, 1); + if (std::string::npos == pos) + return false; + + pos += name.length() + 1; + if ((pos > query_.length()) || (static_cast('=') != query_[pos-1])) + return false; + + std::string::size_type end = query_.find(static_cast('&'), pos); + if (std::string::npos == end) { + end = query_.length(); + } + value->assign(query_.substr(pos, end - pos)); + return true; +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace rtc + +#endif // WEBRTC_BASE_HTTPCOMMON_INL_H__ diff --git a/webrtc/base/httpcommon.cc b/webrtc/base/httpcommon.cc new file mode 100644 index 0000000000..031505b7f9 --- /dev/null +++ b/webrtc/base/httpcommon.cc @@ -0,0 +1,1009 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include + +#if defined(WEBRTC_WIN) +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#define SECURITY_WIN32 +#include +#endif + +#include + +#include "webrtc/base/arraysize.h" +#include "webrtc/base/base64.h" +#include "webrtc/base/checks.h" +#include "webrtc/base/cryptstring.h" +#include "webrtc/base/httpcommon-inl.h" +#include "webrtc/base/httpcommon.h" +#include "webrtc/base/messagedigest.h" +#include "webrtc/base/socketaddress.h" +#include "webrtc/base/stringencode.h" +#include "webrtc/base/stringutils.h" + +namespace rtc { + +#if defined(WEBRTC_WIN) +extern const ConstantLabel SECURITY_ERRORS[]; +#endif + +////////////////////////////////////////////////////////////////////// +// Enum - TODO: expose globally later? +////////////////////////////////////////////////////////////////////// + +bool find_string(size_t& index, const std::string& needle, + const char* const haystack[], size_t max_index) { + for (index=0; index +struct Enum { + static const char** Names; + static size_t Size; + + static inline const char* Name(E val) { return Names[val]; } + static inline bool Parse(E& val, const std::string& name) { + size_t index; + if (!find_string(index, name, Names, Size)) + return false; + val = static_cast(index); + return true; + } + + E val; + + inline operator E&() { return val; } + inline Enum& operator=(E rhs) { val = rhs; return *this; } + + inline const char* name() const { return Name(val); } + inline bool assign(const std::string& name) { return Parse(val, name); } + inline Enum& operator=(const std::string& rhs) { assign(rhs); return *this; } +}; + +#define ENUM(e,n) \ + template<> const char** Enum::Names = n; \ + template<> size_t Enum::Size = sizeof(n)/sizeof(n[0]) + +////////////////////////////////////////////////////////////////////// +// HttpCommon +////////////////////////////////////////////////////////////////////// + +static const char* kHttpVersions[HVER_LAST+1] = { + "1.0", "1.1", "Unknown" +}; +ENUM(HttpVersion, kHttpVersions); + +static const char* kHttpVerbs[HV_LAST+1] = { + "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD" +}; +ENUM(HttpVerb, kHttpVerbs); + +static const char* kHttpHeaders[HH_LAST+1] = { + "Age", + "Cache-Control", + "Connection", + "Content-Disposition", + "Content-Length", + "Content-Range", + "Content-Type", + "Cookie", + "Date", + "ETag", + "Expires", + "Host", + "If-Modified-Since", + "If-None-Match", + "Keep-Alive", + "Last-Modified", + "Location", + "Proxy-Authenticate", + "Proxy-Authorization", + "Proxy-Connection", + "Range", + "Set-Cookie", + "TE", + "Trailers", + "Transfer-Encoding", + "Upgrade", + "User-Agent", + "WWW-Authenticate", +}; +ENUM(HttpHeader, kHttpHeaders); + +const char* ToString(HttpVersion version) { + return Enum::Name(version); +} + +bool FromString(HttpVersion& version, const std::string& str) { + return Enum::Parse(version, str); +} + +const char* ToString(HttpVerb verb) { + return Enum::Name(verb); +} + +bool FromString(HttpVerb& verb, const std::string& str) { + return Enum::Parse(verb, str); +} + +const char* ToString(HttpHeader header) { + return Enum::Name(header); +} + +bool FromString(HttpHeader& header, const std::string& str) { + return Enum::Parse(header, str); +} + +bool HttpCodeHasBody(uint32_t code) { + return !HttpCodeIsInformational(code) + && (code != HC_NO_CONTENT) && (code != HC_NOT_MODIFIED); +} + +bool HttpCodeIsCacheable(uint32_t code) { + switch (code) { + case HC_OK: + case HC_NON_AUTHORITATIVE: + case HC_PARTIAL_CONTENT: + case HC_MULTIPLE_CHOICES: + case HC_MOVED_PERMANENTLY: + case HC_GONE: + return true; + default: + return false; + } +} + +bool HttpHeaderIsEndToEnd(HttpHeader header) { + switch (header) { + case HH_CONNECTION: + case HH_KEEP_ALIVE: + case HH_PROXY_AUTHENTICATE: + case HH_PROXY_AUTHORIZATION: + case HH_PROXY_CONNECTION: // Note part of RFC... this is non-standard header + case HH_TE: + case HH_TRAILERS: + case HH_TRANSFER_ENCODING: + case HH_UPGRADE: + return false; + default: + return true; + } +} + +bool HttpHeaderIsCollapsible(HttpHeader header) { + switch (header) { + case HH_SET_COOKIE: + case HH_PROXY_AUTHENTICATE: + case HH_WWW_AUTHENTICATE: + return false; + default: + return true; + } +} + +bool HttpShouldKeepAlive(const HttpData& data) { + std::string connection; + if ((data.hasHeader(HH_PROXY_CONNECTION, &connection) + || data.hasHeader(HH_CONNECTION, &connection))) { + return (_stricmp(connection.c_str(), "Keep-Alive") == 0); + } + return (data.version >= HVER_1_1); +} + +namespace { + +inline bool IsEndOfAttributeName(size_t pos, size_t len, const char * data) { + if (pos >= len) + return true; + if (isspace(static_cast(data[pos]))) + return true; + // The reason for this complexity is that some attributes may contain trailing + // equal signs (like base64 tokens in Negotiate auth headers) + if ((pos+1 < len) && (data[pos] == '=') && + !isspace(static_cast(data[pos+1])) && + (data[pos+1] != '=')) { + return true; + } + return false; +} + +// TODO: unittest for EscapeAttribute and HttpComposeAttributes. + +std::string EscapeAttribute(const std::string& attribute) { + const size_t kMaxLength = attribute.length() * 2 + 1; + char* buffer = STACK_ARRAY(char, kMaxLength); + size_t len = escape(buffer, kMaxLength, attribute.data(), attribute.length(), + "\"", '\\'); + return std::string(buffer, len); +} + +} // anonymous namespace + +void HttpComposeAttributes(const HttpAttributeList& attributes, char separator, + std::string* composed) { + std::stringstream ss; + for (size_t i=0; i 0) { + ss << separator << " "; + } + ss << attributes[i].first; + if (!attributes[i].second.empty()) { + ss << "=\"" << EscapeAttribute(attributes[i].second) << "\""; + } + } + *composed = ss.str(); +} + +void HttpParseAttributes(const char * data, size_t len, + HttpAttributeList& attributes) { + size_t pos = 0; + while (true) { + // Skip leading whitespace + while ((pos < len) && isspace(static_cast(data[pos]))) { + ++pos; + } + + // End of attributes? + if (pos >= len) + return; + + // Find end of attribute name + size_t start = pos; + while (!IsEndOfAttributeName(pos, len, data)) { + ++pos; + } + + HttpAttribute attribute; + attribute.first.assign(data + start, data + pos); + + // Attribute has value? + if ((pos < len) && (data[pos] == '=')) { + ++pos; // Skip '=' + // Check if quoted value + if ((pos < len) && (data[pos] == '"')) { + while (++pos < len) { + if (data[pos] == '"') { + ++pos; + break; + } + if ((data[pos] == '\\') && (pos + 1 < len)) + ++pos; + attribute.second.append(1, data[pos]); + } + } else { + while ((pos < len) && + !isspace(static_cast(data[pos])) && + (data[pos] != ',')) { + attribute.second.append(1, data[pos++]); + } + } + } + + attributes.push_back(attribute); + if ((pos < len) && (data[pos] == ',')) ++pos; // Skip ',' + } +} + +bool HttpHasAttribute(const HttpAttributeList& attributes, + const std::string& name, + std::string* value) { + for (HttpAttributeList::const_iterator it = attributes.begin(); + it != attributes.end(); ++it) { + if (it->first == name) { + if (value) { + *value = it->second; + } + return true; + } + } + return false; +} + +bool HttpHasNthAttribute(HttpAttributeList& attributes, + size_t index, + std::string* name, + std::string* value) { + if (index >= attributes.size()) + return false; + + if (name) + *name = attributes[index].first; + if (value) + *value = attributes[index].second; + return true; +} + +bool HttpDateToSeconds(const std::string& date, time_t* seconds) { + const char* const kTimeZones[] = { + "UT", "GMT", "EST", "EDT", "CST", "CDT", "MST", "MDT", "PST", "PDT", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "K", "L", "M", + "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y" + }; + const int kTimeZoneOffsets[] = { + 0, 0, -5, -4, -6, -5, -7, -6, -8, -7, + -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 + }; + + RTC_DCHECK(nullptr != seconds); + struct tm tval; + memset(&tval, 0, sizeof(tval)); + char month[4], zone[6]; + memset(month, 0, sizeof(month)); + memset(zone, 0, sizeof(zone)); + + if (7 != sscanf(date.c_str(), "%*3s, %d %3s %d %d:%d:%d %5c", + &tval.tm_mday, month, &tval.tm_year, + &tval.tm_hour, &tval.tm_min, &tval.tm_sec, zone)) { + return false; + } + switch (toupper(month[2])) { + case 'N': tval.tm_mon = (month[1] == 'A') ? 0 : 5; break; + case 'B': tval.tm_mon = 1; break; + case 'R': tval.tm_mon = (month[0] == 'M') ? 2 : 3; break; + case 'Y': tval.tm_mon = 4; break; + case 'L': tval.tm_mon = 6; break; + case 'G': tval.tm_mon = 7; break; + case 'P': tval.tm_mon = 8; break; + case 'T': tval.tm_mon = 9; break; + case 'V': tval.tm_mon = 10; break; + case 'C': tval.tm_mon = 11; break; + } + tval.tm_year -= 1900; + time_t gmt, non_gmt = mktime(&tval); + if ((zone[0] == '+') || (zone[0] == '-')) { + if (!isdigit(zone[1]) || !isdigit(zone[2]) + || !isdigit(zone[3]) || !isdigit(zone[4])) { + return false; + } + int hours = (zone[1] - '0') * 10 + (zone[2] - '0'); + int minutes = (zone[3] - '0') * 10 + (zone[4] - '0'); + int offset = (hours * 60 + minutes) * 60; + gmt = non_gmt + ((zone[0] == '+') ? offset : -offset); + } else { + size_t zindex; + if (!find_string(zindex, zone, kTimeZones, arraysize(kTimeZones))) { + return false; + } + gmt = non_gmt + kTimeZoneOffsets[zindex] * 60 * 60; + } + // TODO: Android should support timezone, see b/2441195 +#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS) || defined(WEBRTC_ANDROID) || defined(BSD) + tm *tm_for_timezone = localtime(&gmt); + *seconds = gmt + tm_for_timezone->tm_gmtoff; +#else +#if defined(_MSC_VER) && _MSC_VER >= 1900 + long timezone = 0; + _get_timezone(&timezone); +#endif + *seconds = gmt - timezone; +#endif + return true; +} + +std::string HttpAddress(const SocketAddress& address, bool secure) { + return (address.port() == HttpDefaultPort(secure)) + ? address.hostname() : address.ToString(); +} + +////////////////////////////////////////////////////////////////////// +// HttpData +////////////////////////////////////////////////////////////////////// + +HttpData::HttpData() : version(HVER_1_1) { +} + +HttpData::~HttpData() = default; + +void +HttpData::clear(bool release_document) { + // Clear headers first, since releasing a document may have far-reaching + // effects. + headers_.clear(); + if (release_document) { + document.reset(); + } +} + +void +HttpData::copy(const HttpData& src) { + headers_ = src.headers_; +} + +void +HttpData::changeHeader(const std::string& name, const std::string& value, + HeaderCombine combine) { + if (combine == HC_AUTO) { + HttpHeader header; + // Unrecognized headers are collapsible + combine = !FromString(header, name) || HttpHeaderIsCollapsible(header) + ? HC_YES : HC_NO; + } else if (combine == HC_REPLACE) { + headers_.erase(name); + combine = HC_NO; + } + // At this point, combine is one of (YES, NO, NEW) + if (combine != HC_NO) { + HeaderMap::iterator it = headers_.find(name); + if (it != headers_.end()) { + if (combine == HC_YES) { + it->second.append(","); + it->second.append(value); + } + return; + } + } + headers_.insert(HeaderMap::value_type(name, value)); +} + +size_t HttpData::clearHeader(const std::string& name) { + return headers_.erase(name); +} + +HttpData::iterator HttpData::clearHeader(iterator header) { + iterator deprecated = header++; + headers_.erase(deprecated); + return header; +} + +bool +HttpData::hasHeader(const std::string& name, std::string* value) const { + HeaderMap::const_iterator it = headers_.find(name); + if (it == headers_.end()) { + return false; + } else if (value) { + *value = it->second; + } + return true; +} + +void HttpData::setContent(const std::string& content_type, + StreamInterface* document) { + setHeader(HH_CONTENT_TYPE, content_type); + setDocumentAndLength(document); +} + +void HttpData::setDocumentAndLength(StreamInterface* document) { + // TODO: Consider calling Rewind() here? + RTC_DCHECK(!hasHeader(HH_CONTENT_LENGTH, nullptr)); + RTC_DCHECK(!hasHeader(HH_TRANSFER_ENCODING, nullptr)); + RTC_DCHECK(document != nullptr); + this->document.reset(document); + size_t content_length = 0; + if (this->document->GetAvailable(&content_length)) { + char buffer[32]; + sprintfn(buffer, sizeof(buffer), "%d", content_length); + setHeader(HH_CONTENT_LENGTH, buffer); + } else { + setHeader(HH_TRANSFER_ENCODING, "chunked"); + } +} + +// +// HttpRequestData +// + +void +HttpRequestData::clear(bool release_document) { + verb = HV_GET; + path.clear(); + HttpData::clear(release_document); +} + +void +HttpRequestData::copy(const HttpRequestData& src) { + verb = src.verb; + path = src.path; + HttpData::copy(src); +} + +size_t +HttpRequestData::formatLeader(char* buffer, size_t size) const { + RTC_DCHECK(path.find(' ') == std::string::npos); + return sprintfn(buffer, size, "%s %.*s HTTP/%s", ToString(verb), path.size(), + path.data(), ToString(version)); +} + +HttpError +HttpRequestData::parseLeader(const char* line, size_t len) { + unsigned int vmajor, vminor; + int vend, dstart, dend; + // sscanf isn't safe with strings that aren't null-terminated, and there is + // no guarantee that |line| is. Create a local copy that is null-terminated. + std::string line_str(line, len); + line = line_str.c_str(); + if ((sscanf(line, "%*s%n %n%*s%n HTTP/%u.%u", + &vend, &dstart, &dend, &vmajor, &vminor) != 2) + || (vmajor != 1)) { + return HE_PROTOCOL; + } + if (vminor == 0) { + version = HVER_1_0; + } else if (vminor == 1) { + version = HVER_1_1; + } else { + return HE_PROTOCOL; + } + std::string sverb(line, vend); + if (!FromString(verb, sverb.c_str())) { + return HE_PROTOCOL; // !?! HC_METHOD_NOT_SUPPORTED? + } + path.assign(line + dstart, line + dend); + return HE_NONE; +} + +bool HttpRequestData::getAbsoluteUri(std::string* uri) const { + if (HV_CONNECT == verb) + return false; + Url url(path); + if (url.valid()) { + uri->assign(path); + return true; + } + std::string host; + if (!hasHeader(HH_HOST, &host)) + return false; + url.set_address(host); + url.set_full_path(path); + uri->assign(url.url()); + return url.valid(); +} + +bool HttpRequestData::getRelativeUri(std::string* host, + std::string* path) const +{ + if (HV_CONNECT == verb) + return false; + Url url(this->path); + if (url.valid()) { + host->assign(url.address()); + path->assign(url.full_path()); + return true; + } + if (!hasHeader(HH_HOST, host)) + return false; + path->assign(this->path); + return true; +} + +// +// HttpResponseData +// + +void +HttpResponseData::clear(bool release_document) { + scode = HC_INTERNAL_SERVER_ERROR; + message.clear(); + HttpData::clear(release_document); +} + +void +HttpResponseData::copy(const HttpResponseData& src) { + scode = src.scode; + message = src.message; + HttpData::copy(src); +} + +void HttpResponseData::set_success(uint32_t scode) { + this->scode = scode; + message.clear(); + setHeader(HH_CONTENT_LENGTH, "0", false); +} + +void HttpResponseData::set_success(const std::string& content_type, + StreamInterface* document, + uint32_t scode) { + this->scode = scode; + message.erase(message.begin(), message.end()); + setContent(content_type, document); +} + +void HttpResponseData::set_redirect(const std::string& location, + uint32_t scode) { + this->scode = scode; + message.clear(); + setHeader(HH_LOCATION, location); + setHeader(HH_CONTENT_LENGTH, "0", false); +} + +void HttpResponseData::set_error(uint32_t scode) { + this->scode = scode; + message.clear(); + setHeader(HH_CONTENT_LENGTH, "0", false); +} + +size_t +HttpResponseData::formatLeader(char* buffer, size_t size) const { + size_t len = sprintfn(buffer, size, "HTTP/%s %lu", ToString(version), scode); + if (!message.empty()) { + len += sprintfn(buffer + len, size - len, " %.*s", + message.size(), message.data()); + } + return len; +} + +HttpError +HttpResponseData::parseLeader(const char* line, size_t len) { + size_t pos = 0; + unsigned int vmajor, vminor, temp_scode; + int temp_pos; + // sscanf isn't safe with strings that aren't null-terminated, and there is + // no guarantee that |line| is. Create a local copy that is null-terminated. + std::string line_str(line, len); + line = line_str.c_str(); + if (sscanf(line, "HTTP %u%n", + &temp_scode, &temp_pos) == 1) { + // This server's response has no version. :( NOTE: This happens for every + // response to requests made from Chrome plugins, regardless of the server's + // behaviour. + LOG(LS_VERBOSE) << "HTTP version missing from response"; + version = HVER_UNKNOWN; + } else if ((sscanf(line, "HTTP/%u.%u %u%n", + &vmajor, &vminor, &temp_scode, &temp_pos) == 3) + && (vmajor == 1)) { + // This server's response does have a version. + if (vminor == 0) { + version = HVER_1_0; + } else if (vminor == 1) { + version = HVER_1_1; + } else { + return HE_PROTOCOL; + } + } else { + return HE_PROTOCOL; + } + scode = temp_scode; + pos = static_cast(temp_pos); + while ((pos < len) && isspace(static_cast(line[pos]))) ++pos; + message.assign(line + pos, len - pos); + return HE_NONE; +} + +////////////////////////////////////////////////////////////////////// +// Http Authentication +////////////////////////////////////////////////////////////////////// + +std::string quote(const std::string& str) { + std::string result; + result.push_back('"'); + for (size_t i=0; iauth_method != auth_method)) + return HAR_IGNORE; + + // BASIC + if (_stricmp(auth_method.c_str(), "basic") == 0) { + if (context) + return HAR_CREDENTIALS; // Bad credentials + if (username.empty()) + return HAR_CREDENTIALS; // Missing credentials + + context = new HttpAuthContext(auth_method); + + // TODO: convert sensitive to a secure buffer that gets securely deleted + //std::string decoded = username + ":" + password; + size_t len = username.size() + password.GetLength() + 2; + char * sensitive = new char[len]; + size_t pos = strcpyn(sensitive, len, username.data(), username.size()); + pos += strcpyn(sensitive + pos, len - pos, ":"); + password.CopyTo(sensitive + pos, true); + + response = auth_method; + response.append(" "); + // TODO: create a sensitive-source version of Base64::encode + response.append(Base64::Encode(sensitive)); + memset(sensitive, 0, len); + delete [] sensitive; + return HAR_RESPONSE; + } + + // DIGEST + if (_stricmp(auth_method.c_str(), "digest") == 0) { + if (context) + return HAR_CREDENTIALS; // Bad credentials + if (username.empty()) + return HAR_CREDENTIALS; // Missing credentials + + context = new HttpAuthContext(auth_method); + + std::string cnonce, ncount; + char buffer[256]; + sprintf(buffer, "%d", static_cast(time(0))); + cnonce = MD5(buffer); + ncount = "00000001"; + + std::string realm, nonce, qop, opaque; + HttpHasAttribute(args, "realm", &realm); + HttpHasAttribute(args, "nonce", &nonce); + bool has_qop = HttpHasAttribute(args, "qop", &qop); + bool has_opaque = HttpHasAttribute(args, "opaque", &opaque); + + // TODO: convert sensitive to be secure buffer + //std::string A1 = username + ":" + realm + ":" + password; + size_t len = username.size() + realm.size() + password.GetLength() + 3; + char * sensitive = new char[len]; // A1 + size_t pos = strcpyn(sensitive, len, username.data(), username.size()); + pos += strcpyn(sensitive + pos, len - pos, ":"); + pos += strcpyn(sensitive + pos, len - pos, realm.c_str()); + pos += strcpyn(sensitive + pos, len - pos, ":"); + password.CopyTo(sensitive + pos, true); + + std::string A2 = method + ":" + uri; + std::string middle; + if (has_qop) { + qop = "auth"; + middle = nonce + ":" + ncount + ":" + cnonce + ":" + qop; + } else { + middle = nonce; + } + std::string HA1 = MD5(sensitive); + memset(sensitive, 0, len); + delete [] sensitive; + std::string HA2 = MD5(A2); + std::string dig_response = MD5(HA1 + ":" + middle + ":" + HA2); + + std::stringstream ss; + ss << auth_method; + ss << " username=" << quote(username); + ss << ", realm=" << quote(realm); + ss << ", nonce=" << quote(nonce); + ss << ", uri=" << quote(uri); + if (has_qop) { + ss << ", qop=" << qop; + ss << ", nc=" << ncount; + ss << ", cnonce=" << quote(cnonce); + } + ss << ", response=\"" << dig_response << "\""; + if (has_opaque) { + ss << ", opaque=" << quote(opaque); + } + response = ss.str(); + return HAR_RESPONSE; + } + +#if defined(WEBRTC_WIN) +#if 1 + bool want_negotiate = (_stricmp(auth_method.c_str(), "negotiate") == 0); + bool want_ntlm = (_stricmp(auth_method.c_str(), "ntlm") == 0); + // SPNEGO & NTLM + if (want_negotiate || want_ntlm) { + const size_t MAX_MESSAGE = 12000, MAX_SPN = 256; + char out_buf[MAX_MESSAGE], spn[MAX_SPN]; + +#if 0 // Requires funky windows versions + DWORD len = MAX_SPN; + if (DsMakeSpn("HTTP", server.HostAsURIString().c_str(), nullptr, + server.port(), + 0, &len, spn) != ERROR_SUCCESS) { + LOG_F(WARNING) << "(Negotiate) - DsMakeSpn failed"; + return HAR_IGNORE; + } +#else + sprintfn(spn, MAX_SPN, "HTTP/%s", server.ToString().c_str()); +#endif + + SecBuffer out_sec; + out_sec.pvBuffer = out_buf; + out_sec.cbBuffer = sizeof(out_buf); + out_sec.BufferType = SECBUFFER_TOKEN; + + SecBufferDesc out_buf_desc; + out_buf_desc.ulVersion = 0; + out_buf_desc.cBuffers = 1; + out_buf_desc.pBuffers = &out_sec; + + const ULONG NEG_FLAGS_DEFAULT = + //ISC_REQ_ALLOCATE_MEMORY + ISC_REQ_CONFIDENTIALITY + //| ISC_REQ_EXTENDED_ERROR + //| ISC_REQ_INTEGRITY + | ISC_REQ_REPLAY_DETECT + | ISC_REQ_SEQUENCE_DETECT + //| ISC_REQ_STREAM + //| ISC_REQ_USE_SUPPLIED_CREDS + ; + + ::TimeStamp lifetime; + SECURITY_STATUS ret = S_OK; + ULONG ret_flags = 0, flags = NEG_FLAGS_DEFAULT; + + bool specify_credentials = !username.empty(); + size_t steps = 0; + + // uint32_t now = Time(); + + NegotiateAuthContext * neg = static_cast(context); + if (neg) { + const size_t max_steps = 10; + if (++neg->steps >= max_steps) { + LOG(WARNING) << "AsyncHttpsProxySocket::Authenticate(Negotiate) too many retries"; + return HAR_ERROR; + } + steps = neg->steps; + + std::string challenge, decoded_challenge; + if (HttpHasNthAttribute(args, 1, &challenge, nullptr) && + Base64::Decode(challenge, Base64::DO_STRICT, &decoded_challenge, + nullptr)) { + SecBuffer in_sec; + in_sec.pvBuffer = const_cast(decoded_challenge.data()); + in_sec.cbBuffer = static_cast(decoded_challenge.size()); + in_sec.BufferType = SECBUFFER_TOKEN; + + SecBufferDesc in_buf_desc; + in_buf_desc.ulVersion = 0; + in_buf_desc.cBuffers = 1; + in_buf_desc.pBuffers = &in_sec; + + ret = InitializeSecurityContextA(&neg->cred, &neg->ctx, spn, flags, 0, SECURITY_NATIVE_DREP, &in_buf_desc, 0, &neg->ctx, &out_buf_desc, &ret_flags, &lifetime); + //LOG(INFO) << "$$$ InitializeSecurityContext @ " << TimeSince(now); + if (FAILED(ret)) { + LOG(LS_ERROR) << "InitializeSecurityContext returned: " + << ErrorName(ret, SECURITY_ERRORS); + return HAR_ERROR; + } + } else if (neg->specified_credentials) { + // Try again with default credentials + specify_credentials = false; + delete context; + context = neg = 0; + } else { + return HAR_CREDENTIALS; + } + } + + if (!neg) { + unsigned char userbuf[256], passbuf[256], domainbuf[16]; + SEC_WINNT_AUTH_IDENTITY_A auth_id, * pauth_id = 0; + if (specify_credentials) { + memset(&auth_id, 0, sizeof(auth_id)); + size_t len = password.GetLength()+1; + char * sensitive = new char[len]; + password.CopyTo(sensitive, true); + std::string::size_type pos = username.find('\\'); + if (pos == std::string::npos) { + auth_id.UserLength = static_cast( + std::min(sizeof(userbuf) - 1, username.size())); + memcpy(userbuf, username.c_str(), auth_id.UserLength); + userbuf[auth_id.UserLength] = 0; + auth_id.DomainLength = 0; + domainbuf[auth_id.DomainLength] = 0; + auth_id.PasswordLength = static_cast( + std::min(sizeof(passbuf) - 1, password.GetLength())); + memcpy(passbuf, sensitive, auth_id.PasswordLength); + passbuf[auth_id.PasswordLength] = 0; + } else { + auth_id.UserLength = static_cast( + std::min(sizeof(userbuf) - 1, username.size() - pos - 1)); + memcpy(userbuf, username.c_str() + pos + 1, auth_id.UserLength); + userbuf[auth_id.UserLength] = 0; + auth_id.DomainLength = + static_cast(std::min(sizeof(domainbuf) - 1, pos)); + memcpy(domainbuf, username.c_str(), auth_id.DomainLength); + domainbuf[auth_id.DomainLength] = 0; + auth_id.PasswordLength = static_cast( + std::min(sizeof(passbuf) - 1, password.GetLength())); + memcpy(passbuf, sensitive, auth_id.PasswordLength); + passbuf[auth_id.PasswordLength] = 0; + } + memset(sensitive, 0, len); + delete [] sensitive; + auth_id.User = userbuf; + auth_id.Domain = domainbuf; + auth_id.Password = passbuf; + auth_id.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI; + pauth_id = &auth_id; + LOG(LS_VERBOSE) << "Negotiate protocol: Using specified credentials"; + } else { + LOG(LS_VERBOSE) << "Negotiate protocol: Using default credentials"; + } + + CredHandle cred; + ret = AcquireCredentialsHandleA( + 0, const_cast(want_negotiate ? NEGOSSP_NAME_A : NTLMSP_NAME_A), + SECPKG_CRED_OUTBOUND, 0, pauth_id, 0, 0, &cred, &lifetime); + //LOG(INFO) << "$$$ AcquireCredentialsHandle @ " << TimeSince(now); + if (ret != SEC_E_OK) { + LOG(LS_ERROR) << "AcquireCredentialsHandle error: " + << ErrorName(ret, SECURITY_ERRORS); + return HAR_IGNORE; + } + + //CSecBufferBundle<5, CSecBufferBase::FreeSSPI> sb_out; + + CtxtHandle ctx; + ret = InitializeSecurityContextA(&cred, 0, spn, flags, 0, SECURITY_NATIVE_DREP, 0, 0, &ctx, &out_buf_desc, &ret_flags, &lifetime); + //LOG(INFO) << "$$$ InitializeSecurityContext @ " << TimeSince(now); + if (FAILED(ret)) { + LOG(LS_ERROR) << "InitializeSecurityContext returned: " + << ErrorName(ret, SECURITY_ERRORS); + FreeCredentialsHandle(&cred); + return HAR_IGNORE; + } + + RTC_DCHECK(!context); + context = neg = new NegotiateAuthContext(auth_method, cred, ctx); + neg->specified_credentials = specify_credentials; + neg->steps = steps; + } + + if ((ret == SEC_I_COMPLETE_NEEDED) || (ret == SEC_I_COMPLETE_AND_CONTINUE)) { + ret = CompleteAuthToken(&neg->ctx, &out_buf_desc); + //LOG(INFO) << "$$$ CompleteAuthToken @ " << TimeSince(now); + LOG(LS_VERBOSE) << "CompleteAuthToken returned: " + << ErrorName(ret, SECURITY_ERRORS); + if (FAILED(ret)) { + return HAR_ERROR; + } + } + + //LOG(INFO) << "$$$ NEGOTIATE took " << TimeSince(now) << "ms"; + + std::string decoded(out_buf, out_buf + out_sec.cbBuffer); + response = auth_method; + response.append(" "); + response.append(Base64::Encode(decoded)); + return HAR_RESPONSE; + } +#endif +#endif // WEBRTC_WIN + + return HAR_IGNORE; +} + +////////////////////////////////////////////////////////////////////// + +} // namespace rtc diff --git a/webrtc/base/httpcommon.h b/webrtc/base/httpcommon.h new file mode 100644 index 0000000000..7182aa2f85 --- /dev/null +++ b/webrtc/base/httpcommon.h @@ -0,0 +1,458 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_BASE_HTTPCOMMON_H__ +#define WEBRTC_BASE_HTTPCOMMON_H__ + +#include +#include +#include +#include +#include "webrtc/base/basictypes.h" +#include "webrtc/base/checks.h" +#include "webrtc/base/stringutils.h" +#include "webrtc/base/stream.h" + +namespace rtc { + +class CryptString; +class SocketAddress; + +////////////////////////////////////////////////////////////////////// +// Constants +////////////////////////////////////////////////////////////////////// + +enum HttpCode { + HC_OK = 200, + HC_NON_AUTHORITATIVE = 203, + HC_NO_CONTENT = 204, + HC_PARTIAL_CONTENT = 206, + + HC_MULTIPLE_CHOICES = 300, + HC_MOVED_PERMANENTLY = 301, + HC_FOUND = 302, + HC_SEE_OTHER = 303, + HC_NOT_MODIFIED = 304, + HC_MOVED_TEMPORARILY = 307, + + HC_BAD_REQUEST = 400, + HC_UNAUTHORIZED = 401, + HC_FORBIDDEN = 403, + HC_NOT_FOUND = 404, + HC_PROXY_AUTHENTICATION_REQUIRED = 407, + HC_GONE = 410, + + HC_INTERNAL_SERVER_ERROR = 500, + HC_NOT_IMPLEMENTED = 501, + HC_SERVICE_UNAVAILABLE = 503, +}; + +enum HttpVersion { + HVER_1_0, HVER_1_1, HVER_UNKNOWN, + HVER_LAST = HVER_UNKNOWN +}; + +enum HttpVerb { + HV_GET, HV_POST, HV_PUT, HV_DELETE, HV_CONNECT, HV_HEAD, + HV_LAST = HV_HEAD +}; + +enum HttpError { + HE_NONE, + HE_PROTOCOL, // Received non-valid HTTP data + HE_DISCONNECTED, // Connection closed unexpectedly + HE_OVERFLOW, // Received too much data for internal buffers + HE_CONNECT_FAILED, // The socket failed to connect. + HE_SOCKET_ERROR, // An error occurred on a connected socket + HE_SHUTDOWN, // Http object is being destroyed + HE_OPERATION_CANCELLED, // Connection aborted locally + HE_AUTH, // Proxy Authentication Required + HE_CERTIFICATE_EXPIRED, // During SSL negotiation + HE_STREAM, // Problem reading or writing to the document + HE_CACHE, // Problem reading from cache + HE_DEFAULT +}; + +enum HttpHeader { + HH_AGE, + HH_CACHE_CONTROL, + HH_CONNECTION, + HH_CONTENT_DISPOSITION, + HH_CONTENT_LENGTH, + HH_CONTENT_RANGE, + HH_CONTENT_TYPE, + HH_COOKIE, + HH_DATE, + HH_ETAG, + HH_EXPIRES, + HH_HOST, + HH_IF_MODIFIED_SINCE, + HH_IF_NONE_MATCH, + HH_KEEP_ALIVE, + HH_LAST_MODIFIED, + HH_LOCATION, + HH_PROXY_AUTHENTICATE, + HH_PROXY_AUTHORIZATION, + HH_PROXY_CONNECTION, + HH_RANGE, + HH_SET_COOKIE, + HH_TE, + HH_TRAILERS, + HH_TRANSFER_ENCODING, + HH_UPGRADE, + HH_USER_AGENT, + HH_WWW_AUTHENTICATE, + HH_LAST = HH_WWW_AUTHENTICATE +}; + +const uint16_t HTTP_DEFAULT_PORT = 80; +const uint16_t HTTP_SECURE_PORT = 443; + +////////////////////////////////////////////////////////////////////// +// Utility Functions +////////////////////////////////////////////////////////////////////// + +inline HttpError mkerr(HttpError err, HttpError def_err = HE_DEFAULT) { + return (err != HE_NONE) ? err : def_err; +} + +const char* ToString(HttpVersion version); +bool FromString(HttpVersion& version, const std::string& str); + +const char* ToString(HttpVerb verb); +bool FromString(HttpVerb& verb, const std::string& str); + +const char* ToString(HttpHeader header); +bool FromString(HttpHeader& header, const std::string& str); + +inline bool HttpCodeIsInformational(uint32_t code) { + return ((code / 100) == 1); +} +inline bool HttpCodeIsSuccessful(uint32_t code) { + return ((code / 100) == 2); +} +inline bool HttpCodeIsRedirection(uint32_t code) { + return ((code / 100) == 3); +} +inline bool HttpCodeIsClientError(uint32_t code) { + return ((code / 100) == 4); +} +inline bool HttpCodeIsServerError(uint32_t code) { + return ((code / 100) == 5); +} + +bool HttpCodeHasBody(uint32_t code); +bool HttpCodeIsCacheable(uint32_t code); +bool HttpHeaderIsEndToEnd(HttpHeader header); +bool HttpHeaderIsCollapsible(HttpHeader header); + +struct HttpData; +bool HttpShouldKeepAlive(const HttpData& data); + +typedef std::pair HttpAttribute; +typedef std::vector HttpAttributeList; +void HttpComposeAttributes(const HttpAttributeList& attributes, char separator, + std::string* composed); +void HttpParseAttributes(const char * data, size_t len, + HttpAttributeList& attributes); +bool HttpHasAttribute(const HttpAttributeList& attributes, + const std::string& name, + std::string* value); +bool HttpHasNthAttribute(HttpAttributeList& attributes, + size_t index, + std::string* name, + std::string* value); + +// Convert RFC1123 date (DoW, DD Mon YYYY HH:MM:SS TZ) to unix timestamp +bool HttpDateToSeconds(const std::string& date, time_t* seconds); + +inline uint16_t HttpDefaultPort(bool secure) { + return secure ? HTTP_SECURE_PORT : HTTP_DEFAULT_PORT; +} + +// Returns the http server notation for a given address +std::string HttpAddress(const SocketAddress& address, bool secure); + +// functional for insensitive std::string compare +struct iless { + bool operator()(const std::string& lhs, const std::string& rhs) const { + return (::_stricmp(lhs.c_str(), rhs.c_str()) < 0); + } +}; + +// put quotes around a string and escape any quotes inside it +std::string quote(const std::string& str); + +////////////////////////////////////////////////////////////////////// +// Url +////////////////////////////////////////////////////////////////////// + +template +class Url { +public: + typedef typename Traits::string string; + + // TODO: Implement Encode/Decode + static int Encode(const CTYPE* source, CTYPE* destination, size_t len); + static int Encode(const string& source, string& destination); + static int Decode(const CTYPE* source, CTYPE* destination, size_t len); + static int Decode(const string& source, string& destination); + + Url(const string& url) { do_set_url(url.c_str(), url.size()); } + Url(const string& path, const string& host, uint16_t port = HTTP_DEFAULT_PORT) + : host_(host), port_(port), secure_(HTTP_SECURE_PORT == port) { + set_full_path(path); + } + + bool valid() const { return !host_.empty(); } + void clear() { + host_.clear(); + port_ = HTTP_DEFAULT_PORT; + secure_ = false; + path_.assign(1, static_cast('/')); + query_.clear(); + } + + void set_url(const string& val) { + do_set_url(val.c_str(), val.size()); + } + string url() const { + string val; do_get_url(&val); return val; + } + + void set_address(const string& val) { + do_set_address(val.c_str(), val.size()); + } + string address() const { + string val; do_get_address(&val); return val; + } + + void set_full_path(const string& val) { + do_set_full_path(val.c_str(), val.size()); + } + string full_path() const { + string val; do_get_full_path(&val); return val; + } + + void set_host(const string& val) { host_ = val; } + const string& host() const { return host_; } + + void set_port(uint16_t val) { port_ = val; } + uint16_t port() const { return port_; } + + void set_secure(bool val) { secure_ = val; } + bool secure() const { return secure_; } + + void set_path(const string& val) { + if (val.empty()) { + path_.assign(1, static_cast('/')); + } else { + RTC_DCHECK(val[0] == static_cast('/')); + path_ = val; + } + } + const string& path() const { return path_; } + + void set_query(const string& val) { + RTC_DCHECK(val.empty() || (val[0] == static_cast('?'))); + query_ = val; + } + const string& query() const { return query_; } + + bool get_attribute(const string& name, string* value) const; + +private: + void do_set_url(const CTYPE* val, size_t len); + void do_set_address(const CTYPE* val, size_t len); + void do_set_full_path(const CTYPE* val, size_t len); + + void do_get_url(string* val) const; + void do_get_address(string* val) const; + void do_get_full_path(string* val) const; + + string host_, path_, query_; + uint16_t port_; + bool secure_; +}; + +////////////////////////////////////////////////////////////////////// +// HttpData +////////////////////////////////////////////////////////////////////// + +struct HttpData { + typedef std::multimap HeaderMap; + typedef HeaderMap::const_iterator const_iterator; + typedef HeaderMap::iterator iterator; + + HttpVersion version; + std::unique_ptr document; + + HttpData(); + + enum HeaderCombine { HC_YES, HC_NO, HC_AUTO, HC_REPLACE, HC_NEW }; + void changeHeader(const std::string& name, const std::string& value, + HeaderCombine combine); + inline void addHeader(const std::string& name, const std::string& value, + bool append = true) { + changeHeader(name, value, append ? HC_AUTO : HC_NO); + } + inline void setHeader(const std::string& name, const std::string& value, + bool overwrite = true) { + changeHeader(name, value, overwrite ? HC_REPLACE : HC_NEW); + } + // Returns count of erased headers + size_t clearHeader(const std::string& name); + // Returns iterator to next header + iterator clearHeader(iterator header); + + // keep in mind, this may not do what you want in the face of multiple headers + bool hasHeader(const std::string& name, std::string* value) const; + + inline const_iterator begin() const { + return headers_.begin(); + } + inline const_iterator end() const { + return headers_.end(); + } + inline iterator begin() { + return headers_.begin(); + } + inline iterator end() { + return headers_.end(); + } + inline const_iterator begin(const std::string& name) const { + return headers_.lower_bound(name); + } + inline const_iterator end(const std::string& name) const { + return headers_.upper_bound(name); + } + inline iterator begin(const std::string& name) { + return headers_.lower_bound(name); + } + inline iterator end(const std::string& name) { + return headers_.upper_bound(name); + } + + // Convenience methods using HttpHeader + inline void changeHeader(HttpHeader header, const std::string& value, + HeaderCombine combine) { + changeHeader(ToString(header), value, combine); + } + inline void addHeader(HttpHeader header, const std::string& value, + bool append = true) { + addHeader(ToString(header), value, append); + } + inline void setHeader(HttpHeader header, const std::string& value, + bool overwrite = true) { + setHeader(ToString(header), value, overwrite); + } + inline void clearHeader(HttpHeader header) { + clearHeader(ToString(header)); + } + inline bool hasHeader(HttpHeader header, std::string* value) const { + return hasHeader(ToString(header), value); + } + inline const_iterator begin(HttpHeader header) const { + return headers_.lower_bound(ToString(header)); + } + inline const_iterator end(HttpHeader header) const { + return headers_.upper_bound(ToString(header)); + } + inline iterator begin(HttpHeader header) { + return headers_.lower_bound(ToString(header)); + } + inline iterator end(HttpHeader header) { + return headers_.upper_bound(ToString(header)); + } + + void setContent(const std::string& content_type, StreamInterface* document); + void setDocumentAndLength(StreamInterface* document); + + virtual size_t formatLeader(char* buffer, size_t size) const = 0; + virtual HttpError parseLeader(const char* line, size_t len) = 0; + +protected: + virtual ~HttpData(); + void clear(bool release_document); + void copy(const HttpData& src); + +private: + HeaderMap headers_; +}; + +struct HttpRequestData : public HttpData { + HttpVerb verb; + std::string path; + + HttpRequestData() : verb(HV_GET) { } + + void clear(bool release_document); + void copy(const HttpRequestData& src); + + size_t formatLeader(char* buffer, size_t size) const override; + HttpError parseLeader(const char* line, size_t len) override; + + bool getAbsoluteUri(std::string* uri) const; + bool getRelativeUri(std::string* host, std::string* path) const; +}; + +struct HttpResponseData : public HttpData { + uint32_t scode; + std::string message; + + HttpResponseData() : scode(HC_INTERNAL_SERVER_ERROR) { } + void clear(bool release_document); + void copy(const HttpResponseData& src); + + // Convenience methods + void set_success(uint32_t scode = HC_OK); + void set_success(const std::string& content_type, + StreamInterface* document, + uint32_t scode = HC_OK); + void set_redirect(const std::string& location, + uint32_t scode = HC_MOVED_TEMPORARILY); + void set_error(uint32_t scode); + + size_t formatLeader(char* buffer, size_t size) const override; + HttpError parseLeader(const char* line, size_t len) override; +}; + +struct HttpTransaction { + HttpRequestData request; + HttpResponseData response; +}; + +////////////////////////////////////////////////////////////////////// +// Http Authentication +////////////////////////////////////////////////////////////////////// + +struct HttpAuthContext { + std::string auth_method; + HttpAuthContext(const std::string& auth) : auth_method(auth) { } + virtual ~HttpAuthContext() { } +}; + +enum HttpAuthResult { HAR_RESPONSE, HAR_IGNORE, HAR_CREDENTIALS, HAR_ERROR }; + +// 'context' is used by this function to record information between calls. +// Start by passing a null pointer, then pass the same pointer each additional +// call. When the authentication attempt is finished, delete the context. +HttpAuthResult HttpAuthenticate( + const char * challenge, size_t len, + const SocketAddress& server, + const std::string& method, const std::string& uri, + const std::string& username, const CryptString& password, + HttpAuthContext *& context, std::string& response, std::string& auth_method); + +////////////////////////////////////////////////////////////////////// + +} // namespace rtc + +#endif // WEBRTC_BASE_HTTPCOMMON_H__ diff --git a/webrtc/base/httpcommon_unittest.cc b/webrtc/base/httpcommon_unittest.cc new file mode 100644 index 0000000000..10e378987a --- /dev/null +++ b/webrtc/base/httpcommon_unittest.cc @@ -0,0 +1,165 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "webrtc/base/gunit.h" +#include "webrtc/base/httpcommon-inl.h" +#include "webrtc/base/httpcommon.h" + +namespace rtc { + +#define TEST_PROTOCOL "http://" +#define TEST_HOST "www.google.com" +#define TEST_PATH "/folder/file.html" +#define TEST_QUERY "?query=x&attr=y" +#define TEST_URL TEST_PROTOCOL TEST_HOST TEST_PATH TEST_QUERY + +TEST(Url, DecomposesUrls) { + Url url(TEST_URL); + EXPECT_TRUE(url.valid()); + EXPECT_FALSE(url.secure()); + EXPECT_STREQ(TEST_HOST, url.host().c_str()); + EXPECT_EQ(80, url.port()); + EXPECT_STREQ(TEST_PATH, url.path().c_str()); + EXPECT_STREQ(TEST_QUERY, url.query().c_str()); + EXPECT_STREQ(TEST_HOST, url.address().c_str()); + EXPECT_STREQ(TEST_PATH TEST_QUERY, url.full_path().c_str()); + EXPECT_STREQ(TEST_URL, url.url().c_str()); +} + +TEST(Url, ComposesUrls) { + // Set in constructor + Url url(TEST_PATH TEST_QUERY, TEST_HOST, 80); + EXPECT_TRUE(url.valid()); + EXPECT_FALSE(url.secure()); + EXPECT_STREQ(TEST_HOST, url.host().c_str()); + EXPECT_EQ(80, url.port()); + EXPECT_STREQ(TEST_PATH, url.path().c_str()); + EXPECT_STREQ(TEST_QUERY, url.query().c_str()); + EXPECT_STREQ(TEST_HOST, url.address().c_str()); + EXPECT_STREQ(TEST_PATH TEST_QUERY, url.full_path().c_str()); + EXPECT_STREQ(TEST_URL, url.url().c_str()); + + url.clear(); + EXPECT_FALSE(url.valid()); + EXPECT_FALSE(url.secure()); + EXPECT_STREQ("", url.host().c_str()); + EXPECT_EQ(80, url.port()); + EXPECT_STREQ("/", url.path().c_str()); + EXPECT_STREQ("", url.query().c_str()); + + // Set component-wise + url.set_host(TEST_HOST); + url.set_port(80); + url.set_path(TEST_PATH); + url.set_query(TEST_QUERY); + EXPECT_TRUE(url.valid()); + EXPECT_FALSE(url.secure()); + EXPECT_STREQ(TEST_HOST, url.host().c_str()); + EXPECT_EQ(80, url.port()); + EXPECT_STREQ(TEST_PATH, url.path().c_str()); + EXPECT_STREQ(TEST_QUERY, url.query().c_str()); + EXPECT_STREQ(TEST_HOST, url.address().c_str()); + EXPECT_STREQ(TEST_PATH TEST_QUERY, url.full_path().c_str()); + EXPECT_STREQ(TEST_URL, url.url().c_str()); +} + +TEST(Url, EnsuresNonEmptyPath) { + Url url(TEST_PROTOCOL TEST_HOST); + EXPECT_TRUE(url.valid()); + EXPECT_STREQ("/", url.path().c_str()); + + url.clear(); + EXPECT_STREQ("/", url.path().c_str()); + url.set_path(""); + EXPECT_STREQ("/", url.path().c_str()); + + url.clear(); + EXPECT_STREQ("/", url.path().c_str()); + url.set_full_path(""); + EXPECT_STREQ("/", url.path().c_str()); +} + +TEST(Url, GetQueryAttributes) { + Url url(TEST_URL); + std::string value; + EXPECT_TRUE(url.get_attribute("query", &value)); + EXPECT_STREQ("x", value.c_str()); + value.clear(); + EXPECT_TRUE(url.get_attribute("attr", &value)); + EXPECT_STREQ("y", value.c_str()); + value.clear(); + EXPECT_FALSE(url.get_attribute("Query", &value)); + EXPECT_TRUE(value.empty()); +} + +TEST(Url, SkipsUserAndPassword) { + Url url("https://mail.google.com:pwd@badsite.com:12345/asdf"); + EXPECT_TRUE(url.valid()); + EXPECT_TRUE(url.secure()); + EXPECT_STREQ("badsite.com", url.host().c_str()); + EXPECT_EQ(12345, url.port()); + EXPECT_STREQ("/asdf", url.path().c_str()); + EXPECT_STREQ("badsite.com:12345", url.address().c_str()); +} + +TEST(Url, SkipsUser) { + Url url("https://mail.google.com@badsite.com:12345/asdf"); + EXPECT_TRUE(url.valid()); + EXPECT_TRUE(url.secure()); + EXPECT_STREQ("badsite.com", url.host().c_str()); + EXPECT_EQ(12345, url.port()); + EXPECT_STREQ("/asdf", url.path().c_str()); + EXPECT_STREQ("badsite.com:12345", url.address().c_str()); +} + +TEST(HttpResponseData, parseLeaderHttp1_0) { + static const char kResponseString[] = "HTTP/1.0 200 OK"; + HttpResponseData response; + EXPECT_EQ(HE_NONE, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); + EXPECT_EQ(HVER_1_0, response.version); + EXPECT_EQ(200U, response.scode); +} + +TEST(HttpResponseData, parseLeaderHttp1_1) { + static const char kResponseString[] = "HTTP/1.1 200 OK"; + HttpResponseData response; + EXPECT_EQ(HE_NONE, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); + EXPECT_EQ(HVER_1_1, response.version); + EXPECT_EQ(200U, response.scode); +} + +TEST(HttpResponseData, parseLeaderHttpUnknown) { + static const char kResponseString[] = "HTTP 200 OK"; + HttpResponseData response; + EXPECT_EQ(HE_NONE, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); + EXPECT_EQ(HVER_UNKNOWN, response.version); + EXPECT_EQ(200U, response.scode); +} + +TEST(HttpResponseData, parseLeaderHttpFailure) { + static const char kResponseString[] = "HTTP/1.1 503 Service Unavailable"; + HttpResponseData response; + EXPECT_EQ(HE_NONE, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); + EXPECT_EQ(HVER_1_1, response.version); + EXPECT_EQ(503U, response.scode); +} + +TEST(HttpResponseData, parseLeaderHttpInvalid) { + static const char kResponseString[] = "Durrrrr, what's HTTP?"; + HttpResponseData response; + EXPECT_EQ(HE_PROTOCOL, response.parseLeader(kResponseString, + sizeof(kResponseString) - 1)); +} + +} // namespace rtc diff --git a/webrtc/base/httpserver.cc b/webrtc/base/httpserver.cc new file mode 100644 index 0000000000..b19069123b --- /dev/null +++ b/webrtc/base/httpserver.cc @@ -0,0 +1,288 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include + +#include "webrtc/base/httpcommon-inl.h" + +#include "webrtc/base/asyncsocket.h" +#include "webrtc/base/checks.h" +#include "webrtc/base/httpserver.h" +#include "webrtc/base/logging.h" +#include "webrtc/base/socketstream.h" +#include "webrtc/base/thread.h" + +namespace rtc { + +/////////////////////////////////////////////////////////////////////////////// +// HttpServer +/////////////////////////////////////////////////////////////////////////////// + +HttpServer::HttpServer() : next_connection_id_(1), closing_(false) { +} + +HttpServer::~HttpServer() { + if (closing_) { + LOG(LS_WARNING) << "HttpServer::CloseAll has not completed"; + } + for (ConnectionMap::iterator it = connections_.begin(); + it != connections_.end(); + ++it) { + StreamInterface* stream = it->second->EndProcess(); + delete stream; + delete it->second; + } +} + +int +HttpServer::HandleConnection(StreamInterface* stream) { + int connection_id = next_connection_id_++; + RTC_DCHECK(connection_id != HTTP_INVALID_CONNECTION_ID); + Connection* connection = new Connection(connection_id, this); + connections_.insert(ConnectionMap::value_type(connection_id, connection)); + connection->BeginProcess(stream); + return connection_id; +} + +void +HttpServer::Respond(HttpServerTransaction* transaction) { + int connection_id = transaction->connection_id(); + if (Connection* connection = Find(connection_id)) { + connection->Respond(transaction); + } else { + delete transaction; + // We may be tempted to SignalHttpComplete, but that implies that a + // connection still exists. + } +} + +void +HttpServer::Close(int connection_id, bool force) { + if (Connection* connection = Find(connection_id)) { + connection->InitiateClose(force); + } +} + +void +HttpServer::CloseAll(bool force) { + if (connections_.empty()) { + SignalCloseAllComplete(this); + return; + } + closing_ = true; + std::list connections; + for (ConnectionMap::const_iterator it = connections_.begin(); + it != connections_.end(); ++it) { + connections.push_back(it->second); + } + for (std::list::const_iterator it = connections.begin(); + it != connections.end(); ++it) { + (*it)->InitiateClose(force); + } +} + +HttpServer::Connection* +HttpServer::Find(int connection_id) { + ConnectionMap::iterator it = connections_.find(connection_id); + if (it == connections_.end()) + return nullptr; + return it->second; +} + +void +HttpServer::Remove(int connection_id) { + ConnectionMap::iterator it = connections_.find(connection_id); + if (it == connections_.end()) { + RTC_NOTREACHED(); + return; + } + Connection* connection = it->second; + connections_.erase(it); + SignalConnectionClosed(this, connection_id, connection->EndProcess()); + delete connection; + if (closing_ && connections_.empty()) { + closing_ = false; + SignalCloseAllComplete(this); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// HttpServer::Connection +/////////////////////////////////////////////////////////////////////////////// + +HttpServer::Connection::Connection(int connection_id, HttpServer* server) + : connection_id_(connection_id), + server_(server), + current_(nullptr), + signalling_(false), + close_(false) {} + +HttpServer::Connection::~Connection() { + // It's possible that an object hosted inside this transaction signalled + // an event which caused the connection to close. + Thread::Current()->Dispose(current_); +} + +void +HttpServer::Connection::BeginProcess(StreamInterface* stream) { + base_.notify(this); + base_.attach(stream); + current_ = new HttpServerTransaction(connection_id_); + if (base_.mode() != HM_CONNECT) + base_.recv(¤t_->request); +} + +StreamInterface* +HttpServer::Connection::EndProcess() { + base_.notify(nullptr); + base_.abort(HE_DISCONNECTED); + return base_.detach(); +} + +void +HttpServer::Connection::Respond(HttpServerTransaction* transaction) { + RTC_DCHECK(current_ == nullptr); + current_ = transaction; + if (current_->response.begin() == current_->response.end()) { + current_->response.set_error(HC_INTERNAL_SERVER_ERROR); + } + bool keep_alive = HttpShouldKeepAlive(current_->request); + current_->response.setHeader(HH_CONNECTION, + keep_alive ? "Keep-Alive" : "Close", + false); + close_ = !HttpShouldKeepAlive(current_->response); + base_.send(¤t_->response); +} + +void +HttpServer::Connection::InitiateClose(bool force) { + bool request_in_progress = (HM_SEND == base_.mode()) || (nullptr == current_); + if (!signalling_ && (force || !request_in_progress)) { + server_->Remove(connection_id_); + } else { + close_ = true; + } +} + +// +// IHttpNotify Implementation +// + +HttpError +HttpServer::Connection::onHttpHeaderComplete(bool chunked, size_t& data_size) { + if (data_size == SIZE_UNKNOWN) { + data_size = 0; + } + RTC_DCHECK(current_ != nullptr); + bool custom_document = false; + server_->SignalHttpRequestHeader(server_, current_, &custom_document); + if (!custom_document) { + current_->request.document.reset(new MemoryStream); + } + return HE_NONE; +} + +void +HttpServer::Connection::onHttpComplete(HttpMode mode, HttpError err) { + if (mode == HM_SEND) { + RTC_DCHECK(current_ != nullptr); + signalling_ = true; + server_->SignalHttpRequestComplete(server_, current_, err); + signalling_ = false; + if (close_) { + // Force a close + err = HE_DISCONNECTED; + } + } + if (err != HE_NONE) { + server_->Remove(connection_id_); + } else if (mode == HM_CONNECT) { + base_.recv(¤t_->request); + } else if (mode == HM_RECV) { + RTC_DCHECK(current_ != nullptr); + // TODO: do we need this? + //request_.document_->rewind(); + HttpServerTransaction* transaction = current_; + current_ = nullptr; + server_->SignalHttpRequest(server_, transaction); + } else if (mode == HM_SEND) { + Thread::Current()->Dispose(current_->response.document.release()); + current_->request.clear(true); + current_->response.clear(true); + base_.recv(¤t_->request); + } else { + RTC_NOTREACHED(); + } +} + +void +HttpServer::Connection::onHttpClosed(HttpError err) { + server_->Remove(connection_id_); +} + +/////////////////////////////////////////////////////////////////////////////// +// HttpListenServer +/////////////////////////////////////////////////////////////////////////////// + +HttpListenServer::HttpListenServer() { + SignalConnectionClosed.connect(this, &HttpListenServer::OnConnectionClosed); +} + +HttpListenServer::~HttpListenServer() { +} + +int HttpListenServer::Listen(const SocketAddress& address) { + AsyncSocket* sock = + Thread::Current()->socketserver()->CreateAsyncSocket(address.family(), + SOCK_STREAM); + if (!sock) { + return SOCKET_ERROR; + } + listener_.reset(sock); + listener_->SignalReadEvent.connect(this, &HttpListenServer::OnReadEvent); + if ((listener_->Bind(address) != SOCKET_ERROR) && + (listener_->Listen(5) != SOCKET_ERROR)) + return 0; + return listener_->GetError(); +} + +bool HttpListenServer::GetAddress(SocketAddress* address) const { + if (!listener_) { + return false; + } + *address = listener_->GetLocalAddress(); + return !address->IsNil(); +} + +void HttpListenServer::StopListening() { + if (listener_) { + listener_->Close(); + } +} + +void HttpListenServer::OnReadEvent(AsyncSocket* socket) { + RTC_DCHECK(socket == listener_.get()); + AsyncSocket* incoming = listener_->Accept(nullptr); + if (incoming) { + StreamInterface* stream = new SocketStream(incoming); + //stream = new LoggingAdapter(stream, LS_VERBOSE, "HttpServer", false); + HandleConnection(stream); + } +} + +void HttpListenServer::OnConnectionClosed(HttpServer* server, + int connection_id, + StreamInterface* stream) { + Thread::Current()->Dispose(stream); +} + +/////////////////////////////////////////////////////////////////////////////// + +} // namespace rtc diff --git a/webrtc/base/httpserver.h b/webrtc/base/httpserver.h new file mode 100644 index 0000000000..cbee734873 --- /dev/null +++ b/webrtc/base/httpserver.h @@ -0,0 +1,139 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_BASE_HTTPSERVER_H__ +#define WEBRTC_BASE_HTTPSERVER_H__ + +#include +#include + +#include "webrtc/base/httpbase.h" + +namespace rtc { + +class AsyncSocket; +class HttpServer; +class SocketAddress; + +////////////////////////////////////////////////////////////////////// +// HttpServer +////////////////////////////////////////////////////////////////////// + +const int HTTP_INVALID_CONNECTION_ID = 0; + +struct HttpServerTransaction : public HttpTransaction { +public: + HttpServerTransaction(int id) : connection_id_(id) { } + int connection_id() const { return connection_id_; } + +private: + int connection_id_; +}; + +class HttpServer { +public: + HttpServer(); + virtual ~HttpServer(); + + int HandleConnection(StreamInterface* stream); + // Due to sigslot issues, we can't destroy some streams at an arbitrary time. + sigslot::signal3 SignalConnectionClosed; + + // This signal occurs when the HTTP request headers have been received, but + // before the request body is written to the request document. By default, + // the request document is a MemoryStream. By handling this signal, the + // document can be overridden, in which case the third signal argument should + // be set to true. In the case where the request body should be ignored, + // the document can be set to null. Note that the transaction object is still + // owened by the HttpServer at this point. + sigslot::signal3 + SignalHttpRequestHeader; + + // An HTTP request has been made, and is available in the transaction object. + // Populate the transaction's response, and then return the object via the + // Respond method. Note that during this time, ownership of the transaction + // object is transferred, so it may be passed between threads, although + // respond must be called on the server's active thread. + sigslot::signal2 SignalHttpRequest; + void Respond(HttpServerTransaction* transaction); + + // If you want to know when a request completes, listen to this event. + sigslot::signal3 + SignalHttpRequestComplete; + + // Stop processing the connection indicated by connection_id. + // Unless force is true, the server will complete sending a response that is + // in progress. + void Close(int connection_id, bool force); + void CloseAll(bool force); + + // After calling CloseAll, this event is signalled to indicate that all + // outstanding connections have closed. + sigslot::signal1 SignalCloseAllComplete; + +private: + class Connection : private IHttpNotify { + public: + Connection(int connection_id, HttpServer* server); + ~Connection() override; + + void BeginProcess(StreamInterface* stream); + StreamInterface* EndProcess(); + + void Respond(HttpServerTransaction* transaction); + void InitiateClose(bool force); + + // IHttpNotify Interface + HttpError onHttpHeaderComplete(bool chunked, size_t& data_size) override; + void onHttpComplete(HttpMode mode, HttpError err) override; + void onHttpClosed(HttpError err) override; + + int connection_id_; + HttpServer* server_; + HttpBase base_; + HttpServerTransaction* current_; + bool signalling_, close_; + }; + + Connection* Find(int connection_id); + void Remove(int connection_id); + + friend class Connection; + typedef std::map ConnectionMap; + + ConnectionMap connections_; + int next_connection_id_; + bool closing_; +}; + +////////////////////////////////////////////////////////////////////// + +class HttpListenServer : public HttpServer, public sigslot::has_slots<> { +public: + HttpListenServer(); + ~HttpListenServer() override; + + int Listen(const SocketAddress& address); + bool GetAddress(SocketAddress* address) const; + void StopListening(); + +private: + void OnReadEvent(AsyncSocket* socket); + void OnConnectionClosed(HttpServer* server, int connection_id, + StreamInterface* stream); + + std::unique_ptr listener_; +}; + +////////////////////////////////////////////////////////////////////// + +} // namespace rtc + +#endif // WEBRTC_BASE_HTTPSERVER_H__ diff --git a/webrtc/base/httpserver_unittest.cc b/webrtc/base/httpserver_unittest.cc new file mode 100644 index 0000000000..96d5fb64b4 --- /dev/null +++ b/webrtc/base/httpserver_unittest.cc @@ -0,0 +1,130 @@ +/* + * Copyright 2007 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "webrtc/base/gunit.h" +#include "webrtc/base/httpserver.h" +#include "webrtc/base/testutils.h" + +using namespace testing; + +namespace rtc { + +namespace { + const char* const kRequest = + "GET /index.html HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n"; + + struct HttpServerMonitor : public sigslot::has_slots<> { + HttpServerTransaction* transaction; + bool server_closed, connection_closed; + + HttpServerMonitor(HttpServer* server) + : transaction(nullptr), server_closed(false), connection_closed(false) { + server->SignalCloseAllComplete.connect(this, + &HttpServerMonitor::OnClosed); + server->SignalHttpRequest.connect(this, &HttpServerMonitor::OnRequest); + server->SignalHttpRequestComplete.connect(this, + &HttpServerMonitor::OnRequestComplete); + server->SignalConnectionClosed.connect(this, + &HttpServerMonitor::OnConnectionClosed); + } + void OnRequest(HttpServer*, HttpServerTransaction* t) { + ASSERT_FALSE(transaction); + transaction = t; + transaction->response.set_success(); + transaction->response.setHeader(HH_CONNECTION, "Close"); + } + void OnRequestComplete(HttpServer*, HttpServerTransaction* t, int) { + ASSERT_EQ(transaction, t); + transaction = nullptr; + } + void OnClosed(HttpServer*) { + server_closed = true; + } + void OnConnectionClosed(HttpServer*, int, StreamInterface* stream) { + connection_closed = true; + delete stream; + } + }; + + void CreateClientConnection(HttpServer& server, + HttpServerMonitor& monitor, + bool send_request) { + StreamSource* client = new StreamSource; + client->SetState(SS_OPEN); + server.HandleConnection(client); + EXPECT_FALSE(monitor.server_closed); + EXPECT_FALSE(monitor.transaction); + + if (send_request) { + // Simulate a request + client->QueueString(kRequest); + EXPECT_FALSE(monitor.server_closed); + } + } +} // anonymous namespace + +TEST(HttpServer, DoesNotSignalCloseUnlessCloseAllIsCalled) { + HttpServer server; + HttpServerMonitor monitor(&server); + // Add an active client connection + CreateClientConnection(server, monitor, true); + // Simulate a response + ASSERT_TRUE(nullptr != monitor.transaction); + server.Respond(monitor.transaction); + EXPECT_FALSE(monitor.transaction); + // Connection has closed, but no server close signal + EXPECT_FALSE(monitor.server_closed); + EXPECT_TRUE(monitor.connection_closed); +} + +TEST(HttpServer, SignalsCloseWhenNoConnectionsAreActive) { + HttpServer server; + HttpServerMonitor monitor(&server); + // Add an idle client connection + CreateClientConnection(server, monitor, false); + // Perform graceful close + server.CloseAll(false); + // Connections have all closed + EXPECT_TRUE(monitor.server_closed); + EXPECT_TRUE(monitor.connection_closed); +} + +TEST(HttpServer, SignalsCloseAfterGracefulCloseAll) { + HttpServer server; + HttpServerMonitor monitor(&server); + // Add an active client connection + CreateClientConnection(server, monitor, true); + // Initiate a graceful close + server.CloseAll(false); + EXPECT_FALSE(monitor.server_closed); + // Simulate a response + ASSERT_TRUE(nullptr != monitor.transaction); + server.Respond(monitor.transaction); + EXPECT_FALSE(monitor.transaction); + // Connections have all closed + EXPECT_TRUE(monitor.server_closed); + EXPECT_TRUE(monitor.connection_closed); +} + +TEST(HttpServer, SignalsCloseAfterForcedCloseAll) { + HttpServer server; + HttpServerMonitor monitor(&server); + // Add an active client connection + CreateClientConnection(server, monitor, true); + // Initiate a forceful close + server.CloseAll(true); + // Connections have all closed + EXPECT_TRUE(monitor.server_closed); + EXPECT_TRUE(monitor.connection_closed); +} + +} // namespace rtc diff --git a/webrtc/base/proxy_unittest.cc b/webrtc/base/proxy_unittest.cc new file mode 100644 index 0000000000..b72d228ea0 --- /dev/null +++ b/webrtc/base/proxy_unittest.cc @@ -0,0 +1,76 @@ +/* + * Copyright 2009 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include +#include +#include "webrtc/base/gunit.h" +#include "webrtc/base/httpserver.h" +#include "webrtc/base/proxyserver.h" +#include "webrtc/base/socketadapters.h" +#include "webrtc/base/testclient.h" +#include "webrtc/base/testechoserver.h" +#include "webrtc/base/virtualsocketserver.h" + +using rtc::Socket; +using rtc::Thread; +using rtc::SocketAddress; + +static const SocketAddress kSocksProxyIntAddr("1.2.3.4", 1080); +static const SocketAddress kSocksProxyExtAddr("1.2.3.5", 0); +static const SocketAddress kHttpsProxyIntAddr("1.2.3.4", 443); +static const SocketAddress kHttpsProxyExtAddr("1.2.3.5", 0); +static const SocketAddress kBogusProxyIntAddr("1.2.3.4", 999); + +// Sets up a virtual socket server and HTTPS/SOCKS5 proxy servers. +class ProxyTest : public testing::Test { + public: + ProxyTest() : ss_(new rtc::VirtualSocketServer(nullptr)) { + Thread::Current()->set_socketserver(ss_.get()); + socks_.reset(new rtc::SocksProxyServer( + ss_.get(), kSocksProxyIntAddr, ss_.get(), kSocksProxyExtAddr)); + https_.reset(new rtc::HttpListenServer()); + https_->Listen(kHttpsProxyIntAddr); + } + ~ProxyTest() { Thread::Current()->set_socketserver(nullptr); } + + rtc::SocketServer* ss() { return ss_.get(); } + + private: + std::unique_ptr ss_; + std::unique_ptr socks_; + // TODO: Make this a real HTTPS proxy server. + std::unique_ptr https_; +}; + +// Tests whether we can use a SOCKS5 proxy to connect to a server. +TEST_F(ProxyTest, TestSocks5Connect) { + rtc::AsyncSocket* socket = + ss()->CreateAsyncSocket(kSocksProxyIntAddr.family(), SOCK_STREAM); + rtc::AsyncSocksProxySocket* proxy_socket = + new rtc::AsyncSocksProxySocket(socket, kSocksProxyIntAddr, + "", rtc::CryptString()); + // TODO: IPv6-ize these tests when proxy supports IPv6. + + rtc::TestEchoServer server(Thread::Current(), + SocketAddress(INADDR_ANY, 0)); + + rtc::AsyncTCPSocket* packet_socket = rtc::AsyncTCPSocket::Create( + proxy_socket, SocketAddress(INADDR_ANY, 0), server.address()); + EXPECT_TRUE(packet_socket != nullptr); + rtc::TestClient client(packet_socket); + + EXPECT_EQ(Socket::CS_CONNECTING, proxy_socket->GetState()); + EXPECT_TRUE(client.CheckConnected()); + EXPECT_EQ(Socket::CS_CONNECTED, proxy_socket->GetState()); + EXPECT_EQ(server.address(), client.remote_address()); + client.Send("foo", 3); + EXPECT_TRUE(client.CheckNextPacket("foo", 3, nullptr)); + EXPECT_TRUE(client.CheckNoPacket()); +} diff --git a/webrtc/base/proxyinfo.cc b/webrtc/base/proxyinfo.cc new file mode 100644 index 0000000000..76c7708286 --- /dev/null +++ b/webrtc/base/proxyinfo.cc @@ -0,0 +1,24 @@ +/* + * Copyright 2004 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "webrtc/base/proxyinfo.h" + +namespace rtc { + +const char * ProxyToString(ProxyType proxy) { + const char * const PROXY_NAMES[] = { "none", "https", "socks5", "unknown" }; + return PROXY_NAMES[proxy]; +} + +ProxyInfo::ProxyInfo() : type(PROXY_NONE), autodetect(false) { +} +ProxyInfo::~ProxyInfo() = default; + +} // namespace rtc diff --git a/webrtc/base/proxyinfo.h b/webrtc/base/proxyinfo.h index cd5c3875dd..2251b13ee2 100644 --- a/webrtc/base/proxyinfo.h +++ b/webrtc/base/proxyinfo.h @@ -11,15 +11,31 @@ #ifndef WEBRTC_BASE_PROXYINFO_H__ #define WEBRTC_BASE_PROXYINFO_H__ +#include +#include "webrtc/base/socketaddress.h" +#include "webrtc/base/cryptstring.h" + namespace rtc { -// TODO(deadbeef): Remove this; it's not used any more but it's referenced in -// some places, including chromium. enum ProxyType { PROXY_NONE, + PROXY_HTTPS, + PROXY_SOCKS5, + PROXY_UNKNOWN }; +const char * ProxyToString(ProxyType proxy); struct ProxyInfo { + ProxyType type; + SocketAddress address; + std::string autoconfig_url; + bool autodetect; + std::string bypass_list; + std::string username; + CryptString password; + + ProxyInfo(); + ~ProxyInfo(); }; } // namespace rtc diff --git a/webrtc/base/proxyserver.cc b/webrtc/base/proxyserver.cc index fd36ef123e..713a98679a 100644 --- a/webrtc/base/proxyserver.cc +++ b/webrtc/base/proxyserver.cc @@ -149,4 +149,8 @@ void ProxyBinding::Destroy() { SignalDestroyed(this); } +AsyncProxyServerSocket* SocksProxyServer::WrapSocket(AsyncSocket* socket) { + return new AsyncSocksProxyServerSocket(socket); +} + } // namespace rtc diff --git a/webrtc/base/proxyserver.h b/webrtc/base/proxyserver.h index 1622daf13b..86007c3606 100644 --- a/webrtc/base/proxyserver.h +++ b/webrtc/base/proxyserver.h @@ -83,6 +83,18 @@ class ProxyServer : public sigslot::has_slots<> { RTC_DISALLOW_COPY_AND_ASSIGN(ProxyServer); }; +// SocksProxyServer is a simple extension of ProxyServer to implement SOCKS. +class SocksProxyServer : public ProxyServer { + public: + SocksProxyServer(SocketFactory* int_factory, const SocketAddress& int_addr, + SocketFactory* ext_factory, const SocketAddress& ext_ip) + : ProxyServer(int_factory, int_addr, ext_factory, ext_ip) { + } + protected: + AsyncProxyServerSocket* WrapSocket(AsyncSocket* socket) override; + RTC_DISALLOW_COPY_AND_ASSIGN(SocksProxyServer); +}; + } // namespace rtc #endif // WEBRTC_BASE_PROXYSERVER_H_ diff --git a/webrtc/base/socketadapters.cc b/webrtc/base/socketadapters.cc index bc15b870c1..060029c1c7 100644 --- a/webrtc/base/socketadapters.cc +++ b/webrtc/base/socketadapters.cc @@ -28,6 +28,7 @@ #include "webrtc/base/bytebuffer.h" #include "webrtc/base/checks.h" +#include "webrtc/base/httpcommon.h" #include "webrtc/base/logging.h" #include "webrtc/base/socketadapters.h" #include "webrtc/base/stringencode.h" @@ -241,4 +242,607 @@ void AsyncSSLServerSocket::ProcessInput(char* data, size_t* len) { BufferInput(false); } +/////////////////////////////////////////////////////////////////////////////// + +AsyncHttpsProxySocket::AsyncHttpsProxySocket(AsyncSocket* socket, + const std::string& user_agent, + const SocketAddress& proxy, + const std::string& username, + const CryptString& password) + : BufferedReadAdapter(socket, 1024), proxy_(proxy), agent_(user_agent), + user_(username), pass_(password), force_connect_(false), state_(PS_ERROR), + context_(0) { +} + +AsyncHttpsProxySocket::~AsyncHttpsProxySocket() { + delete context_; +} + +int AsyncHttpsProxySocket::Connect(const SocketAddress& addr) { + int ret; + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket::Connect(" + << proxy_.ToSensitiveString() << ")"; + dest_ = addr; + state_ = PS_INIT; + if (ShouldIssueConnect()) { + BufferInput(true); + } + ret = BufferedReadAdapter::Connect(proxy_); + // TODO: Set state_ appropriately if Connect fails. + return ret; +} + +SocketAddress AsyncHttpsProxySocket::GetRemoteAddress() const { + return dest_; +} + +int AsyncHttpsProxySocket::Close() { + headers_.clear(); + state_ = PS_ERROR; + dest_.Clear(); + delete context_; + context_ = nullptr; + return BufferedReadAdapter::Close(); +} + +Socket::ConnState AsyncHttpsProxySocket::GetState() const { + if (state_ < PS_TUNNEL) { + return CS_CONNECTING; + } else if (state_ == PS_TUNNEL) { + return CS_CONNECTED; + } else { + return CS_CLOSED; + } +} + +void AsyncHttpsProxySocket::OnConnectEvent(AsyncSocket * socket) { + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket::OnConnectEvent"; + if (!ShouldIssueConnect()) { + state_ = PS_TUNNEL; + BufferedReadAdapter::OnConnectEvent(socket); + return; + } + SendRequest(); +} + +void AsyncHttpsProxySocket::OnCloseEvent(AsyncSocket * socket, int err) { + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket::OnCloseEvent(" << err << ")"; + if ((state_ == PS_WAIT_CLOSE) && (err == 0)) { + state_ = PS_ERROR; + Connect(dest_); + } else { + BufferedReadAdapter::OnCloseEvent(socket, err); + } +} + +void AsyncHttpsProxySocket::ProcessInput(char* data, size_t* len) { + size_t start = 0; + for (size_t pos = start; state_ < PS_TUNNEL && pos < *len;) { + if (state_ == PS_SKIP_BODY) { + size_t consume = std::min(*len - pos, content_length_); + pos += consume; + start = pos; + content_length_ -= consume; + if (content_length_ == 0) { + EndResponse(); + } + continue; + } + + if (data[pos++] != '\n') + continue; + + size_t len = pos - start - 1; + if ((len > 0) && (data[start + len - 1] == '\r')) + --len; + + data[start + len] = 0; + ProcessLine(data + start, len); + start = pos; + } + + *len -= start; + if (*len > 0) { + memmove(data, data + start, *len); + } + + if (state_ != PS_TUNNEL) + return; + + bool remainder = (*len > 0); + BufferInput(false); + SignalConnectEvent(this); + + // FIX: if SignalConnect causes the socket to be destroyed, we are in trouble + if (remainder) + SignalReadEvent(this); // TODO: signal this?? +} + +bool AsyncHttpsProxySocket::ShouldIssueConnect() const { + // TODO: Think about whether a more sophisticated test + // than dest port == 80 is needed. + return force_connect_ || (dest_.port() != 80); +} + +void AsyncHttpsProxySocket::SendRequest() { + std::stringstream ss; + ss << "CONNECT " << dest_.ToString() << " HTTP/1.0\r\n"; + ss << "User-Agent: " << agent_ << "\r\n"; + ss << "Host: " << dest_.HostAsURIString() << "\r\n"; + ss << "Content-Length: 0\r\n"; + ss << "Proxy-Connection: Keep-Alive\r\n"; + ss << headers_; + ss << "\r\n"; + std::string str = ss.str(); + DirectSend(str.c_str(), str.size()); + state_ = PS_LEADER; + expect_close_ = true; + content_length_ = 0; + headers_.clear(); + + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket >> " << str; +} + +void AsyncHttpsProxySocket::ProcessLine(char * data, size_t len) { + LOG(LS_VERBOSE) << "AsyncHttpsProxySocket << " << data; + + if (len == 0) { + if (state_ == PS_TUNNEL_HEADERS) { + state_ = PS_TUNNEL; + } else if (state_ == PS_ERROR_HEADERS) { + Error(defer_error_); + return; + } else if (state_ == PS_SKIP_HEADERS) { + if (content_length_) { + state_ = PS_SKIP_BODY; + } else { + EndResponse(); + return; + } + } else { + static bool report = false; + if (!unknown_mechanisms_.empty() && !report) { + report = true; + std::string msg( + "Unable to connect to the Google Talk service due to an incompatibility " + "with your proxy.\r\nPlease help us resolve this issue by submitting the " + "following information to us using our technical issue submission form " + "at:\r\n\r\n" + "http://www.google.com/support/talk/bin/request.py\r\n\r\n" + "We apologize for the inconvenience.\r\n\r\n" + "Information to submit to Google: " + ); + //std::string msg("Please report the following information to foo@bar.com:\r\nUnknown methods: "); + msg.append(unknown_mechanisms_); +#if defined(WEBRTC_WIN) + MessageBoxA(0, msg.c_str(), "Oops!", MB_OK); +#endif +#if defined(WEBRTC_POSIX) + // TODO: Raise a signal so the UI can be separated. + LOG(LS_ERROR) << "Oops!\n\n" << msg; +#endif + } + // Unexpected end of headers + Error(0); + return; + } + } else if (state_ == PS_LEADER) { + unsigned int code; + if (sscanf(data, "HTTP/%*u.%*u %u", &code) != 1) { + Error(0); + return; + } + switch (code) { + case 200: + // connection good! + state_ = PS_TUNNEL_HEADERS; + return; +#if defined(HTTP_STATUS_PROXY_AUTH_REQ) && (HTTP_STATUS_PROXY_AUTH_REQ != 407) +#error Wrong code for HTTP_STATUS_PROXY_AUTH_REQ +#endif + case 407: // HTTP_STATUS_PROXY_AUTH_REQ + state_ = PS_AUTHENTICATE; + return; + default: + defer_error_ = 0; + state_ = PS_ERROR_HEADERS; + return; + } + } else if ((state_ == PS_AUTHENTICATE) + && (_strnicmp(data, "Proxy-Authenticate:", 19) == 0)) { + std::string response, auth_method; + switch (HttpAuthenticate(data + 19, len - 19, + proxy_, "CONNECT", "/", + user_, pass_, context_, response, auth_method)) { + case HAR_IGNORE: + LOG(LS_VERBOSE) << "Ignoring Proxy-Authenticate: " << auth_method; + if (!unknown_mechanisms_.empty()) + unknown_mechanisms_.append(", "); + unknown_mechanisms_.append(auth_method); + break; + case HAR_RESPONSE: + headers_ = "Proxy-Authorization: "; + headers_.append(response); + headers_.append("\r\n"); + state_ = PS_SKIP_HEADERS; + unknown_mechanisms_.clear(); + break; + case HAR_CREDENTIALS: + defer_error_ = SOCKET_EACCES; + state_ = PS_ERROR_HEADERS; + unknown_mechanisms_.clear(); + break; + case HAR_ERROR: + defer_error_ = 0; + state_ = PS_ERROR_HEADERS; + unknown_mechanisms_.clear(); + break; + } + } else if (_strnicmp(data, "Content-Length:", 15) == 0) { + content_length_ = strtoul(data + 15, 0, 0); + } else if (_strnicmp(data, "Proxy-Connection: Keep-Alive", 28) == 0) { + expect_close_ = false; + /* + } else if (_strnicmp(data, "Connection: close", 17) == 0) { + expect_close_ = true; + */ + } +} + +void AsyncHttpsProxySocket::EndResponse() { + if (!expect_close_) { + SendRequest(); + return; + } + + // No point in waiting for the server to close... let's close now + // TODO: Refactor out PS_WAIT_CLOSE + state_ = PS_WAIT_CLOSE; + BufferedReadAdapter::Close(); + OnCloseEvent(this, 0); +} + +void AsyncHttpsProxySocket::Error(int error) { + BufferInput(false); + Close(); + SetError(error); + SignalCloseEvent(this, error); +} + +/////////////////////////////////////////////////////////////////////////////// + +AsyncSocksProxySocket::AsyncSocksProxySocket(AsyncSocket* socket, + const SocketAddress& proxy, + const std::string& username, + const CryptString& password) + : BufferedReadAdapter(socket, 1024), state_(SS_ERROR), proxy_(proxy), + user_(username), pass_(password) { +} + +AsyncSocksProxySocket::~AsyncSocksProxySocket() = default; + +int AsyncSocksProxySocket::Connect(const SocketAddress& addr) { + int ret; + dest_ = addr; + state_ = SS_INIT; + BufferInput(true); + ret = BufferedReadAdapter::Connect(proxy_); + // TODO: Set state_ appropriately if Connect fails. + return ret; +} + +SocketAddress AsyncSocksProxySocket::GetRemoteAddress() const { + return dest_; +} + +int AsyncSocksProxySocket::Close() { + state_ = SS_ERROR; + dest_.Clear(); + return BufferedReadAdapter::Close(); +} + +Socket::ConnState AsyncSocksProxySocket::GetState() const { + if (state_ < SS_TUNNEL) { + return CS_CONNECTING; + } else if (state_ == SS_TUNNEL) { + return CS_CONNECTED; + } else { + return CS_CLOSED; + } +} + +void AsyncSocksProxySocket::OnConnectEvent(AsyncSocket* socket) { + SendHello(); +} + +void AsyncSocksProxySocket::ProcessInput(char* data, size_t* len) { + RTC_DCHECK(state_ < SS_TUNNEL); + + ByteBufferReader response(data, *len); + + if (state_ == SS_HELLO) { + uint8_t ver, method; + if (!response.ReadUInt8(&ver) || + !response.ReadUInt8(&method)) + return; + + if (ver != 5) { + Error(0); + return; + } + + if (method == 0) { + SendConnect(); + } else if (method == 2) { + SendAuth(); + } else { + Error(0); + return; + } + } else if (state_ == SS_AUTH) { + uint8_t ver, status; + if (!response.ReadUInt8(&ver) || + !response.ReadUInt8(&status)) + return; + + if ((ver != 1) || (status != 0)) { + Error(SOCKET_EACCES); + return; + } + + SendConnect(); + } else if (state_ == SS_CONNECT) { + uint8_t ver, rep, rsv, atyp; + if (!response.ReadUInt8(&ver) || + !response.ReadUInt8(&rep) || + !response.ReadUInt8(&rsv) || + !response.ReadUInt8(&atyp)) + return; + + if ((ver != 5) || (rep != 0)) { + Error(0); + return; + } + + uint16_t port; + if (atyp == 1) { + uint32_t addr; + if (!response.ReadUInt32(&addr) || + !response.ReadUInt16(&port)) + return; + LOG(LS_VERBOSE) << "Bound on " << addr << ":" << port; + } else if (atyp == 3) { + uint8_t len; + std::string addr; + if (!response.ReadUInt8(&len) || + !response.ReadString(&addr, len) || + !response.ReadUInt16(&port)) + return; + LOG(LS_VERBOSE) << "Bound on " << addr << ":" << port; + } else if (atyp == 4) { + std::string addr; + if (!response.ReadString(&addr, 16) || + !response.ReadUInt16(&port)) + return; + LOG(LS_VERBOSE) << "Bound on :" << port; + } else { + Error(0); + return; + } + + state_ = SS_TUNNEL; + } + + // Consume parsed data + *len = response.Length(); + memmove(data, response.Data(), *len); + + if (state_ != SS_TUNNEL) + return; + + bool remainder = (*len > 0); + BufferInput(false); + SignalConnectEvent(this); + + // FIX: if SignalConnect causes the socket to be destroyed, we are in trouble + if (remainder) + SignalReadEvent(this); // TODO: signal this?? +} + +void AsyncSocksProxySocket::SendHello() { + ByteBufferWriter request; + request.WriteUInt8(5); // Socks Version + if (user_.empty()) { + request.WriteUInt8(1); // Authentication Mechanisms + request.WriteUInt8(0); // No authentication + } else { + request.WriteUInt8(2); // Authentication Mechanisms + request.WriteUInt8(0); // No authentication + request.WriteUInt8(2); // Username/Password + } + DirectSend(request.Data(), request.Length()); + state_ = SS_HELLO; +} + +void AsyncSocksProxySocket::SendAuth() { + ByteBufferWriter request; + request.WriteUInt8(1); // Negotiation Version + request.WriteUInt8(static_cast(user_.size())); + request.WriteString(user_); // Username + request.WriteUInt8(static_cast(pass_.GetLength())); + size_t len = pass_.GetLength() + 1; + char * sensitive = new char[len]; + pass_.CopyTo(sensitive, true); + request.WriteString(sensitive); // Password + memset(sensitive, 0, len); + delete [] sensitive; + DirectSend(request.Data(), request.Length()); + state_ = SS_AUTH; +} + +void AsyncSocksProxySocket::SendConnect() { + ByteBufferWriter request; + request.WriteUInt8(5); // Socks Version + request.WriteUInt8(1); // CONNECT + request.WriteUInt8(0); // Reserved + if (dest_.IsUnresolvedIP()) { + std::string hostname = dest_.hostname(); + request.WriteUInt8(3); // DOMAINNAME + request.WriteUInt8(static_cast(hostname.size())); + request.WriteString(hostname); // Destination Hostname + } else { + request.WriteUInt8(1); // IPV4 + request.WriteUInt32(dest_.ip()); // Destination IP + } + request.WriteUInt16(dest_.port()); // Destination Port + DirectSend(request.Data(), request.Length()); + state_ = SS_CONNECT; +} + +void AsyncSocksProxySocket::Error(int error) { + state_ = SS_ERROR; + BufferInput(false); + Close(); + SetError(SOCKET_EACCES); + SignalCloseEvent(this, error); +} + +AsyncSocksProxyServerSocket::AsyncSocksProxyServerSocket(AsyncSocket* socket) + : AsyncProxyServerSocket(socket, kBufferSize), state_(SS_HELLO) { + BufferInput(true); +} + +void AsyncSocksProxyServerSocket::ProcessInput(char* data, size_t* len) { + // TODO: See if the whole message has arrived + RTC_DCHECK(state_ < SS_CONNECT_PENDING); + + ByteBufferReader response(data, *len); + if (state_ == SS_HELLO) { + HandleHello(&response); + } else if (state_ == SS_AUTH) { + HandleAuth(&response); + } else if (state_ == SS_CONNECT) { + HandleConnect(&response); + } + + // Consume parsed data + *len = response.Length(); + memmove(data, response.Data(), *len); +} + +void AsyncSocksProxyServerSocket::DirectSend(const ByteBufferWriter& buf) { + BufferedReadAdapter::DirectSend(buf.Data(), buf.Length()); +} + +void AsyncSocksProxyServerSocket::HandleHello(ByteBufferReader* request) { + uint8_t ver, num_methods; + if (!request->ReadUInt8(&ver) || + !request->ReadUInt8(&num_methods)) { + Error(0); + return; + } + + if (ver != 5) { + Error(0); + return; + } + + // Handle either no-auth (0) or user/pass auth (2) + uint8_t method = 0xFF; + if (num_methods > 0 && !request->ReadUInt8(&method)) { + Error(0); + return; + } + + // TODO: Ask the server which method to use. + SendHelloReply(method); + if (method == 0) { + state_ = SS_CONNECT; + } else if (method == 2) { + state_ = SS_AUTH; + } else { + state_ = SS_ERROR; + } +} + +void AsyncSocksProxyServerSocket::SendHelloReply(uint8_t method) { + ByteBufferWriter response; + response.WriteUInt8(5); // Socks Version + response.WriteUInt8(method); // Auth method + DirectSend(response); +} + +void AsyncSocksProxyServerSocket::HandleAuth(ByteBufferReader* request) { + uint8_t ver, user_len, pass_len; + std::string user, pass; + if (!request->ReadUInt8(&ver) || + !request->ReadUInt8(&user_len) || + !request->ReadString(&user, user_len) || + !request->ReadUInt8(&pass_len) || + !request->ReadString(&pass, pass_len)) { + Error(0); + return; + } + + // TODO: Allow for checking of credentials. + SendAuthReply(0); + state_ = SS_CONNECT; +} + +void AsyncSocksProxyServerSocket::SendAuthReply(uint8_t result) { + ByteBufferWriter response; + response.WriteUInt8(1); // Negotiation Version + response.WriteUInt8(result); + DirectSend(response); +} + +void AsyncSocksProxyServerSocket::HandleConnect(ByteBufferReader* request) { + uint8_t ver, command, reserved, addr_type; + uint32_t ip; + uint16_t port; + if (!request->ReadUInt8(&ver) || + !request->ReadUInt8(&command) || + !request->ReadUInt8(&reserved) || + !request->ReadUInt8(&addr_type) || + !request->ReadUInt32(&ip) || + !request->ReadUInt16(&port)) { + Error(0); + return; + } + + if (ver != 5 || command != 1 || + reserved != 0 || addr_type != 1) { + Error(0); + return; + } + + SignalConnectRequest(this, SocketAddress(ip, port)); + state_ = SS_CONNECT_PENDING; +} + +void AsyncSocksProxyServerSocket::SendConnectResult(int result, + const SocketAddress& addr) { + if (state_ != SS_CONNECT_PENDING) + return; + + ByteBufferWriter response; + response.WriteUInt8(5); // Socks version + response.WriteUInt8((result != 0)); // 0x01 is generic error + response.WriteUInt8(0); // reserved + response.WriteUInt8(1); // IPv4 address + response.WriteUInt32(addr.ip()); + response.WriteUInt16(addr.port()); + DirectSend(response); + BufferInput(false); + state_ = SS_TUNNEL; +} + +void AsyncSocksProxyServerSocket::Error(int error) { + state_ = SS_ERROR; + BufferInput(false); + Close(); + SetError(SOCKET_EACCES); + SignalCloseEvent(this, error); +} + } // namespace rtc diff --git a/webrtc/base/socketadapters.h b/webrtc/base/socketadapters.h index dcf56abd79..3b5be10c61 100644 --- a/webrtc/base/socketadapters.h +++ b/webrtc/base/socketadapters.h @@ -16,10 +16,12 @@ #include "webrtc/base/asyncsocket.h" #include "webrtc/base/constructormagic.h" +#include "webrtc/base/cryptstring.h" #include "webrtc/base/logging.h" namespace rtc { +struct HttpAuthContext; class ByteBufferReader; class ByteBufferWriter; @@ -92,6 +94,114 @@ class AsyncSSLServerSocket : public BufferedReadAdapter { RTC_DISALLOW_COPY_AND_ASSIGN(AsyncSSLServerSocket); }; +/////////////////////////////////////////////////////////////////////////////// + +// Implements a socket adapter that speaks the HTTP/S proxy protocol. +class AsyncHttpsProxySocket : public BufferedReadAdapter { + public: + AsyncHttpsProxySocket(AsyncSocket* socket, const std::string& user_agent, + const SocketAddress& proxy, + const std::string& username, const CryptString& password); + ~AsyncHttpsProxySocket() override; + + // If connect is forced, the adapter will always issue an HTTP CONNECT to the + // target address. Otherwise, it will connect only if the destination port + // is not port 80. + void SetForceConnect(bool force) { force_connect_ = force; } + + int Connect(const SocketAddress& addr) override; + SocketAddress GetRemoteAddress() const override; + int Close() override; + ConnState GetState() const override; + + protected: + void OnConnectEvent(AsyncSocket* socket) override; + void OnCloseEvent(AsyncSocket* socket, int err) override; + void ProcessInput(char* data, size_t* len) override; + + bool ShouldIssueConnect() const; + void SendRequest(); + void ProcessLine(char* data, size_t len); + void EndResponse(); + void Error(int error); + + private: + SocketAddress proxy_, dest_; + std::string agent_, user_, headers_; + CryptString pass_; + bool force_connect_; + size_t content_length_; + int defer_error_; + bool expect_close_; + enum ProxyState { + PS_INIT, PS_LEADER, PS_AUTHENTICATE, PS_SKIP_HEADERS, PS_ERROR_HEADERS, + PS_TUNNEL_HEADERS, PS_SKIP_BODY, PS_TUNNEL, PS_WAIT_CLOSE, PS_ERROR + } state_; + HttpAuthContext * context_; + std::string unknown_mechanisms_; + RTC_DISALLOW_COPY_AND_ASSIGN(AsyncHttpsProxySocket); +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Implements a socket adapter that speaks the SOCKS proxy protocol. +class AsyncSocksProxySocket : public BufferedReadAdapter { + public: + AsyncSocksProxySocket(AsyncSocket* socket, const SocketAddress& proxy, + const std::string& username, const CryptString& password); + ~AsyncSocksProxySocket() override; + + int Connect(const SocketAddress& addr) override; + SocketAddress GetRemoteAddress() const override; + int Close() override; + ConnState GetState() const override; + + protected: + void OnConnectEvent(AsyncSocket* socket) override; + void ProcessInput(char* data, size_t* len) override; + + void SendHello(); + void SendConnect(); + void SendAuth(); + void Error(int error); + + private: + enum State { + SS_INIT, SS_HELLO, SS_AUTH, SS_CONNECT, SS_TUNNEL, SS_ERROR + }; + State state_; + SocketAddress proxy_, dest_; + std::string user_; + CryptString pass_; + RTC_DISALLOW_COPY_AND_ASSIGN(AsyncSocksProxySocket); +}; + +// Implements a proxy server socket for the SOCKS protocol. +class AsyncSocksProxyServerSocket : public AsyncProxyServerSocket { + public: + explicit AsyncSocksProxyServerSocket(AsyncSocket* socket); + + private: + void ProcessInput(char* data, size_t* len) override; + void DirectSend(const ByteBufferWriter& buf); + + void HandleHello(ByteBufferReader* request); + void SendHelloReply(uint8_t method); + void HandleAuth(ByteBufferReader* request); + void SendAuthReply(uint8_t result); + void HandleConnect(ByteBufferReader* request); + void SendConnectResult(int result, const SocketAddress& addr) override; + + void Error(int error); + + static const int kBufferSize = 1024; + enum State { + SS_HELLO, SS_AUTH, SS_CONNECT, SS_CONNECT_PENDING, SS_TUNNEL, SS_ERROR + }; + State state_; + RTC_DISALLOW_COPY_AND_ASSIGN(AsyncSocksProxyServerSocket); +}; + } // namespace rtc #endif // WEBRTC_BASE_SOCKETADAPTERS_H_ diff --git a/webrtc/base/stream.cc b/webrtc/base/stream.cc index 520d1dd202..67ef104044 100644 --- a/webrtc/base/stream.cc +++ b/webrtc/base/stream.cc @@ -237,6 +237,61 @@ void StreamAdapterInterface::OnEvent(StreamInterface* stream, SignalEvent(this, events, err); } +/////////////////////////////////////////////////////////////////////////////// +// StreamTap +/////////////////////////////////////////////////////////////////////////////// + +StreamTap::StreamTap(StreamInterface* stream, StreamInterface* tap) + : StreamAdapterInterface(stream), tap_(), tap_result_(SR_SUCCESS), + tap_error_(0) { + AttachTap(tap); +} + +StreamTap::~StreamTap() = default; + +void StreamTap::AttachTap(StreamInterface* tap) { + tap_.reset(tap); +} + +StreamInterface* StreamTap::DetachTap() { + return tap_.release(); +} + +StreamResult StreamTap::GetTapResult(int* error) { + if (error) { + *error = tap_error_; + } + return tap_result_; +} + +StreamResult StreamTap::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + size_t backup_read; + if (!read) { + read = &backup_read; + } + StreamResult res = StreamAdapterInterface::Read(buffer, buffer_len, + read, error); + if ((res == SR_SUCCESS) && (tap_result_ == SR_SUCCESS)) { + tap_result_ = tap_->WriteAll(buffer, *read, nullptr, &tap_error_); + } + return res; +} + +StreamResult StreamTap::Write(const void* data, size_t data_len, + size_t* written, int* error) { + size_t backup_written; + if (!written) { + written = &backup_written; + } + StreamResult res = StreamAdapterInterface::Write(data, data_len, + written, error); + if ((res == SR_SUCCESS) && (tap_result_ == SR_SUCCESS)) { + tap_result_ = tap_->WriteAll(data, *written, nullptr, &tap_error_); + } + return res; +} + /////////////////////////////////////////////////////////////////////////////// // NullStream /////////////////////////////////////////////////////////////////////////////// @@ -606,6 +661,24 @@ StreamResult MemoryStream::DoReserve(size_t size, int* error) { return SR_ERROR; } +/////////////////////////////////////////////////////////////////////////////// + +ExternalMemoryStream::ExternalMemoryStream() { +} + +ExternalMemoryStream::ExternalMemoryStream(void* data, size_t length) { + SetData(data, length); +} + +ExternalMemoryStream::~ExternalMemoryStream() { +} + +void ExternalMemoryStream::SetData(void* data, size_t length) { + data_length_ = buffer_length_ = length; + buffer_ = static_cast(data); + seek_position_ = 0; +} + /////////////////////////////////////////////////////////////////////////////// // FifoBuffer /////////////////////////////////////////////////////////////////////////////// @@ -821,6 +894,66 @@ StreamResult FifoBuffer::WriteOffsetLocked(const void* buffer, return SR_SUCCESS; } + + +/////////////////////////////////////////////////////////////////////////////// +// LoggingAdapter +/////////////////////////////////////////////////////////////////////////////// + +LoggingAdapter::LoggingAdapter(StreamInterface* stream, LoggingSeverity level, + const std::string& label, bool hex_mode) + : StreamAdapterInterface(stream), level_(level), hex_mode_(hex_mode) { + set_label(label); +} + +void LoggingAdapter::set_label(const std::string& label) { + label_.assign("["); + label_.append(label); + label_.append("]"); +} + +StreamResult LoggingAdapter::Read(void* buffer, size_t buffer_len, + size_t* read, int* error) { + size_t local_read; if (!read) read = &local_read; + StreamResult result = StreamAdapterInterface::Read(buffer, buffer_len, read, + error); + if (result == SR_SUCCESS) { + LogMultiline(level_, label_.c_str(), true, buffer, *read, hex_mode_, &lms_); + } + return result; +} + +StreamResult LoggingAdapter::Write(const void* data, size_t data_len, + size_t* written, int* error) { + size_t local_written; + if (!written) written = &local_written; + StreamResult result = StreamAdapterInterface::Write(data, data_len, written, + error); + if (result == SR_SUCCESS) { + LogMultiline(level_, label_.c_str(), false, data, *written, hex_mode_, + &lms_); + } + return result; +} + +void LoggingAdapter::Close() { + LogMultiline(level_, label_.c_str(), false, nullptr, 0, hex_mode_, &lms_); + LogMultiline(level_, label_.c_str(), true, nullptr, 0, hex_mode_, &lms_); + LOG_V(level_) << label_ << " Closed locally"; + StreamAdapterInterface::Close(); +} + +void LoggingAdapter::OnEvent(StreamInterface* stream, int events, int err) { + if (events & SE_OPEN) { + LOG_V(level_) << label_ << " Open"; + } else if (events & SE_CLOSE) { + LogMultiline(level_, label_.c_str(), false, nullptr, 0, hex_mode_, &lms_); + LogMultiline(level_, label_.c_str(), true, nullptr, 0, hex_mode_, &lms_); + LOG_V(level_) << label_ << " Closed with error: " << err; + } + StreamAdapterInterface::OnEvent(stream, events, err); +} + /////////////////////////////////////////////////////////////////////////////// // StringStream - Reads/Writes to an external std::string /////////////////////////////////////////////////////////////////////////////// @@ -899,6 +1032,31 @@ bool StringStream::ReserveSize(size_t size) { return true; } +/////////////////////////////////////////////////////////////////////////////// +// StreamReference +/////////////////////////////////////////////////////////////////////////////// + +StreamReference::StreamReference(StreamInterface* stream) + : StreamAdapterInterface(stream, false) { + // owner set to false so the destructor does not free the stream. + stream_ref_count_ = new StreamRefCount(stream); +} + +StreamInterface* StreamReference::NewReference() { + stream_ref_count_->AddReference(); + return new StreamReference(stream_ref_count_, stream()); +} + +StreamReference::~StreamReference() { + stream_ref_count_->Release(); +} + +StreamReference::StreamReference(StreamRefCount* stream_ref_count, + StreamInterface* stream) + : StreamAdapterInterface(stream, false), + stream_ref_count_(stream_ref_count) { +} + /////////////////////////////////////////////////////////////////////////////// StreamResult Flow(StreamInterface* source, diff --git a/webrtc/base/stream.h b/webrtc/base/stream.h index 211c1e1e4b..dbc2ad750a 100644 --- a/webrtc/base/stream.h +++ b/webrtc/base/stream.h @@ -309,6 +309,38 @@ class StreamAdapterInterface : public StreamInterface, RTC_DISALLOW_COPY_AND_ASSIGN(StreamAdapterInterface); }; +/////////////////////////////////////////////////////////////////////////////// +// StreamTap is a non-modifying, pass-through adapter, which copies all data +// in either direction to the tap. Note that errors or blocking on writing to +// the tap will prevent further tap writes from occurring. +/////////////////////////////////////////////////////////////////////////////// + +class StreamTap : public StreamAdapterInterface { + public: + explicit StreamTap(StreamInterface* stream, StreamInterface* tap); + ~StreamTap() override; + + void AttachTap(StreamInterface* tap); + StreamInterface* DetachTap(); + StreamResult GetTapResult(int* error); + + // StreamAdapterInterface Interface + StreamResult Read(void* buffer, + size_t buffer_len, + size_t* read, + int* error) override; + StreamResult Write(const void* data, + size_t data_len, + size_t* written, + int* error) override; + + private: + std::unique_ptr tap_; + StreamResult tap_result_; + int tap_error_; + RTC_DISALLOW_COPY_AND_ASSIGN(StreamTap); +}; + /////////////////////////////////////////////////////////////////////////////// // NullStream gives errors on read, and silently discards all written data. /////////////////////////////////////////////////////////////////////////////// @@ -448,6 +480,18 @@ class MemoryStream : public MemoryStreamBase { char* buffer_alloc_; }; +// ExternalMemoryStream adapts an external memory buffer, so writes which would +// extend past the end of the buffer will return end-of-stream. + +class ExternalMemoryStream : public MemoryStreamBase { + public: + ExternalMemoryStream(); + ExternalMemoryStream(void* data, size_t length); + ~ExternalMemoryStream() override; + + void SetData(void* data, size_t length); +}; + // FifoBuffer allows for efficient, thread-safe buffering of data between // writer and reader. As the data can wrap around the end of the buffer, // MemoryStreamBase can't help us here. @@ -525,6 +569,37 @@ class FifoBuffer : public StreamInterface { RTC_DISALLOW_COPY_AND_ASSIGN(FifoBuffer); }; +/////////////////////////////////////////////////////////////////////////////// + +class LoggingAdapter : public StreamAdapterInterface { + public: + LoggingAdapter(StreamInterface* stream, LoggingSeverity level, + const std::string& label, bool hex_mode = false); + + void set_label(const std::string& label); + + StreamResult Read(void* buffer, + size_t buffer_len, + size_t* read, + int* error) override; + StreamResult Write(const void* data, + size_t data_len, + size_t* written, + int* error) override; + void Close() override; + + protected: + void OnEvent(StreamInterface* stream, int events, int err) override; + + private: + LoggingSeverity level_; + std::string label_; + bool hex_mode_; + LogMultilineState lms_; + + RTC_DISALLOW_COPY_AND_ASSIGN(LoggingAdapter); +}; + /////////////////////////////////////////////////////////////////////////////// // StringStream - Reads/Writes to an external std::string /////////////////////////////////////////////////////////////////////////////// @@ -556,6 +631,66 @@ class StringStream : public StreamInterface { bool read_only_; }; +/////////////////////////////////////////////////////////////////////////////// +// StreamReference - A reference counting stream adapter +/////////////////////////////////////////////////////////////////////////////// + +// Keep in mind that the streams and adapters defined in this file are +// not thread-safe, so this has limited uses. + +// A StreamRefCount holds the reference count and a pointer to the +// wrapped stream. It deletes the wrapped stream when there are no +// more references. We can then have multiple StreamReference +// instances pointing to one StreamRefCount, all wrapping the same +// stream. + +class StreamReference : public StreamAdapterInterface { + class StreamRefCount; + public: + // Constructor for the first reference to a stream + // Note: get more references through NewReference(). Use this + // constructor only once on a given stream. + explicit StreamReference(StreamInterface* stream); + StreamInterface* GetStream() { return stream(); } + StreamInterface* NewReference(); + ~StreamReference() override; + + private: + class StreamRefCount { + public: + explicit StreamRefCount(StreamInterface* stream) + : stream_(stream), ref_count_(1) { + } + void AddReference() { + CritScope lock(&cs_); + ++ref_count_; + } + void Release() { + int ref_count; + { // Atomic ops would have been a better fit here. + CritScope lock(&cs_); + ref_count = --ref_count_; + } + if (ref_count == 0) { + delete stream_; + delete this; + } + } + private: + StreamInterface* stream_; + int ref_count_; + CriticalSection cs_; + RTC_DISALLOW_COPY_AND_ASSIGN(StreamRefCount); + }; + + // Constructor for adding references + explicit StreamReference(StreamRefCount* stream_ref_count, + StreamInterface* stream); + + StreamRefCount* stream_ref_count_; + RTC_DISALLOW_COPY_AND_ASSIGN(StreamReference); +}; + /////////////////////////////////////////////////////////////////////////////// // Flow attempts to move bytes from source to sink via buffer of size diff --git a/webrtc/p2p/base/basicpacketsocketfactory.cc b/webrtc/p2p/base/basicpacketsocketfactory.cc index 1519135882..c478d63112 100644 --- a/webrtc/p2p/base/basicpacketsocketfactory.cc +++ b/webrtc/p2p/base/basicpacketsocketfactory.cc @@ -120,6 +120,16 @@ AsyncPacketSocket* BasicPacketSocketFactory::CreateClientTcpSocket( return NULL; } + // If using a proxy, wrap the socket in a proxy socket. + if (proxy_info.type == PROXY_SOCKS5) { + socket = new AsyncSocksProxySocket( + socket, proxy_info.address, proxy_info.username, proxy_info.password); + } else if (proxy_info.type == PROXY_HTTPS) { + socket = + new AsyncHttpsProxySocket(socket, user_agent, proxy_info.address, + proxy_info.username, proxy_info.password); + } + // Assert that at most one TLS option is used. int tlsOpts = opts & (PacketSocketFactory::OPT_TLS | PacketSocketFactory::OPT_TLS_FAKE | diff --git a/webrtc/p2p/base/p2ptransportchannel_unittest.cc b/webrtc/p2p/base/p2ptransportchannel_unittest.cc index b891f01e61..9b7a92ad8b 100644 --- a/webrtc/p2p/base/p2ptransportchannel_unittest.cc +++ b/webrtc/p2p/base/p2ptransportchannel_unittest.cc @@ -23,6 +23,7 @@ #include "webrtc/base/natserver.h" #include "webrtc/base/natsocketfactory.h" #include "webrtc/base/physicalsocketserver.h" +#include "webrtc/base/proxyserver.h" #include "webrtc/base/socketaddress.h" #include "webrtc/base/ssladapter.h" #include "webrtc/base/thread.h" @@ -63,6 +64,12 @@ static const SocketAddress kAlternateAddrs[2] = { static const SocketAddress kIPv6AlternateAddrs[2] = { SocketAddress("2401:4030:1:2c00:be30:abcd:efab:cdef", 0), SocketAddress("2601:0:1000:1b03:2e41:38ff:fea6:f2a4", 0)}; +// Addresses for HTTP proxy servers. +static const SocketAddress kHttpsProxyAddrs[2] = + { SocketAddress("11.11.11.1", 443), SocketAddress("22.22.22.1", 443) }; +// Addresses for SOCKS proxy servers. +static const SocketAddress kSocksProxyAddrs[2] = + { SocketAddress("11.11.11.1", 1080), SocketAddress("22.22.22.1", 1080) }; // Internal addresses for NAT boxes. static const SocketAddress kNatAddrs[2] = { SocketAddress("192.168.1.1", 0), SocketAddress("192.168.2.1", 0) }; @@ -182,6 +189,14 @@ class P2PTransportChannelTestBase : public testing::Test, ss_scope_(ss_.get()), stun_server_(TestStunServer::Create(main_, kStunAddr)), turn_server_(main_, kTurnUdpIntAddr, kTurnUdpExtAddr), + socks_server1_(ss_.get(), + kSocksProxyAddrs[0], + ss_.get(), + kSocksProxyAddrs[0]), + socks_server2_(ss_.get(), + kSocksProxyAddrs[1], + ss_.get(), + kSocksProxyAddrs[1]), force_relay_(false) { ep1_.role_ = ICEROLE_CONTROLLING; ep2_.role_ = ICEROLE_CONTROLLED; @@ -213,6 +228,9 @@ class P2PTransportChannelTestBase : public testing::Test, NAT_SYMMETRIC_THEN_CONE, // Double NAT, symmetric outer, cone inner BLOCK_UDP, // Firewall, UDP in/out blocked BLOCK_UDP_AND_INCOMING_TCP, // Firewall, UDP in/out and TCP in blocked + BLOCK_ALL_BUT_OUTGOING_HTTP, // Firewall, only TCP out on 80/443 + PROXY_HTTPS, // All traffic through HTTPS proxy + PROXY_SOCKS, // All traffic through SOCKS proxy NUM_CONFIGS }; @@ -435,6 +453,13 @@ class P2PTransportChannelTestBase : public testing::Test, GetEndpoint(endpoint)->network_manager_.RemoveInterface(addr); fw()->AddRule(false, rtc::FP_ANY, rtc::FD_ANY, addr); } + void SetProxy(int endpoint, rtc::ProxyType type) { + rtc::ProxyInfo info; + info.type = type; + info.address = (type == rtc::PROXY_HTTPS) ? + kHttpsProxyAddrs[endpoint] : kSocksProxyAddrs[endpoint]; + GetAllocator(endpoint)->set_proxy("unittest/1.0", info); + } void SetAllocatorFlags(int endpoint, int flags) { GetAllocator(endpoint)->set_flags(flags); } @@ -857,6 +882,8 @@ class P2PTransportChannelTestBase : public testing::Test, rtc::SocketServerScope ss_scope_; std::unique_ptr stun_server_; TestTurnServer turn_server_; + rtc::SocksProxyServer socks_server1_; + rtc::SocksProxyServer socks_server2_; Endpoint ep1_; Endpoint ep2_; RemoteIceParameterSource remote_ice_parameter_source_ = FROM_CANDIDATE; @@ -998,6 +1025,9 @@ class P2PTransportChannelTest : public P2PTransportChannelTestBase { break; case BLOCK_UDP: case BLOCK_UDP_AND_INCOMING_TCP: + case BLOCK_ALL_BUT_OUTGOING_HTTP: + case PROXY_HTTPS: + case PROXY_SOCKS: AddAddress(endpoint, kPublicAddrs[endpoint]); // Block all UDP fw()->AddRule(false, rtc::FP_UDP, rtc::FD_ANY, @@ -1006,6 +1036,28 @@ class P2PTransportChannelTest : public P2PTransportChannelTestBase { // Block TCP inbound to the endpoint fw()->AddRule(false, rtc::FP_TCP, SocketAddress(), kPublicAddrs[endpoint]); + } else if (config == BLOCK_ALL_BUT_OUTGOING_HTTP) { + // Block all TCP to/from the endpoint except 80/443 out + fw()->AddRule(true, rtc::FP_TCP, kPublicAddrs[endpoint], + SocketAddress(rtc::IPAddress(INADDR_ANY), 80)); + fw()->AddRule(true, rtc::FP_TCP, kPublicAddrs[endpoint], + SocketAddress(rtc::IPAddress(INADDR_ANY), 443)); + fw()->AddRule(false, rtc::FP_TCP, rtc::FD_ANY, + kPublicAddrs[endpoint]); + } else if (config == PROXY_HTTPS) { + // Block all TCP to/from the endpoint except to the proxy server + fw()->AddRule(true, rtc::FP_TCP, kPublicAddrs[endpoint], + kHttpsProxyAddrs[endpoint]); + fw()->AddRule(false, rtc::FP_TCP, rtc::FD_ANY, + kPublicAddrs[endpoint]); + SetProxy(endpoint, rtc::PROXY_HTTPS); + } else if (config == PROXY_SOCKS) { + // Block all TCP to/from the endpoint except to the proxy server + fw()->AddRule(true, rtc::FP_TCP, kPublicAddrs[endpoint], + kSocksProxyAddrs[endpoint]); + fw()->AddRule(false, rtc::FP_TCP, rtc::FD_ANY, + kPublicAddrs[endpoint]); + SetProxy(endpoint, rtc::PROXY_SOCKS5); } break; default: @@ -1036,19 +1088,23 @@ class P2PTransportChannelTest : public P2PTransportChannelTestBase { // Test matrix. Originator behavior defined by rows, receiever by columns. // TODO: Fix NULLs caused by lack of TCP support in NATSocket. +// TODO: Fix NULLs caused by no HTTP proxy support. // TODO: Rearrange rows/columns from best to worst. const P2PTransportChannelTest::Result* P2PTransportChannelTest::kMatrix[NUM_CONFIGS][NUM_CONFIGS] = { - // OPEN CONE ADDR PORT SYMM 2CON SCON !UDP !TCP - /*OP*/ {LULU, LUSU, LUSU, LUSU, LUPU, LUSU, LUPU, LTPT, LTPT}, - /*CO*/ {SULU, SUSU, SUSU, SUSU, SUPU, SUSU, SUPU, NULL, NULL}, - /*AD*/ {SULU, SUSU, SUSU, SUSU, SUPU, SUSU, SUPU, NULL, NULL}, - /*PO*/ {SULU, SUSU, SUSU, SUSU, RUPU, SUSU, RUPU, NULL, NULL}, - /*SY*/ {PULU, PUSU, PUSU, PURU, PURU, PUSU, PURU, NULL, NULL}, - /*2C*/ {SULU, SUSU, SUSU, SUSU, SUPU, SUSU, SUPU, NULL, NULL}, - /*SC*/ {PULU, PUSU, PUSU, PURU, PURU, PUSU, PURU, NULL, NULL}, - /*!U*/ {LTPT, NULL, NULL, NULL, NULL, NULL, NULL, LTPT, LTPT}, - /*!T*/ {PTLT, NULL, NULL, NULL, NULL, NULL, NULL, PTLT, LTRT}, +// OPEN CONE ADDR PORT SYMM 2CON SCON !UDP !TCP HTTP PRXH PRXS +/*OP*/ {LULU, LUSU, LUSU, LUSU, LUPU, LUSU, LUPU, LTPT, LTPT, LSRS, NULL, LTPT}, +/*CO*/ {SULU, SUSU, SUSU, SUSU, SUPU, SUSU, SUPU, NULL, NULL, LSRS, NULL, LTRT}, +/*AD*/ {SULU, SUSU, SUSU, SUSU, SUPU, SUSU, SUPU, NULL, NULL, LSRS, NULL, LTRT}, +/*PO*/ {SULU, SUSU, SUSU, SUSU, RUPU, SUSU, RUPU, NULL, NULL, LSRS, NULL, LTRT}, +/*SY*/ {PULU, PUSU, PUSU, PURU, PURU, PUSU, PURU, NULL, NULL, LSRS, NULL, LTRT}, +/*2C*/ {SULU, SUSU, SUSU, SUSU, SUPU, SUSU, SUPU, NULL, NULL, LSRS, NULL, LTRT}, +/*SC*/ {PULU, PUSU, PUSU, PURU, PURU, PUSU, PURU, NULL, NULL, LSRS, NULL, LTRT}, +/*!U*/ {LTPT, NULL, NULL, NULL, NULL, NULL, NULL, LTPT, LTPT, LSRS, NULL, LTRT}, +/*!T*/ {PTLT, NULL, NULL, NULL, NULL, NULL, NULL, PTLT, LTRT, LSRS, NULL, LTRT}, +/*HT*/ {LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, LSRS, NULL, LSRS}, +/*PR*/ {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}, +/*PR*/ {LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LTRT, LSRS, NULL, LTRT}, }; // The actual tests that exercise all the various configurations. @@ -1066,16 +1122,19 @@ const P2PTransportChannelTest::Result* #define P2P_TEST(x, y) \ P2P_TEST_DECLARATION(x, y,) -#define P2P_TEST_SET(x) \ - P2P_TEST(x, OPEN) \ - P2P_TEST(x, NAT_FULL_CONE) \ - P2P_TEST(x, NAT_ADDR_RESTRICTED) \ - P2P_TEST(x, NAT_PORT_RESTRICTED) \ - P2P_TEST(x, NAT_SYMMETRIC) \ - P2P_TEST(x, NAT_DOUBLE_CONE) \ - P2P_TEST(x, NAT_SYMMETRIC_THEN_CONE) \ - P2P_TEST(x, BLOCK_UDP) \ - P2P_TEST(x, BLOCK_UDP_AND_INCOMING_TCP) +#define P2P_TEST_SET(x) \ + P2P_TEST(x, OPEN) \ + P2P_TEST(x, NAT_FULL_CONE) \ + P2P_TEST(x, NAT_ADDR_RESTRICTED) \ + P2P_TEST(x, NAT_PORT_RESTRICTED) \ + P2P_TEST(x, NAT_SYMMETRIC) \ + P2P_TEST(x, NAT_DOUBLE_CONE) \ + P2P_TEST(x, NAT_SYMMETRIC_THEN_CONE) \ + P2P_TEST(x, BLOCK_UDP) \ + P2P_TEST(x, BLOCK_UDP_AND_INCOMING_TCP) \ + P2P_TEST(x, BLOCK_ALL_BUT_OUTGOING_HTTP) \ + P2P_TEST(x, PROXY_HTTPS) \ + P2P_TEST(x, PROXY_SOCKS) P2P_TEST_SET(OPEN) P2P_TEST_SET(NAT_FULL_CONE) @@ -1086,6 +1145,9 @@ P2P_TEST_SET(NAT_DOUBLE_CONE) P2P_TEST_SET(NAT_SYMMETRIC_THEN_CONE) P2P_TEST_SET(BLOCK_UDP) P2P_TEST_SET(BLOCK_UDP_AND_INCOMING_TCP) +P2P_TEST_SET(BLOCK_ALL_BUT_OUTGOING_HTTP) +P2P_TEST_SET(PROXY_HTTPS) +P2P_TEST_SET(PROXY_SOCKS) // Test that we restart candidate allocation when local ufrag&pwd changed. // Standard Ice protocol is used. diff --git a/webrtc/p2p/base/packetsocketfactory.h b/webrtc/p2p/base/packetsocketfactory.h index dac0408297..c3b2417526 100644 --- a/webrtc/p2p/base/packetsocketfactory.h +++ b/webrtc/p2p/base/packetsocketfactory.h @@ -13,7 +13,6 @@ #include "webrtc/base/constructormagic.h" #include "webrtc/base/proxyinfo.h" -#include "webrtc/base/socketaddress.h" namespace rtc { diff --git a/webrtc/p2p/base/portallocator.h b/webrtc/p2p/base/portallocator.h index eec7580a1c..a77b037e72 100644 --- a/webrtc/p2p/base/portallocator.h +++ b/webrtc/p2p/base/portallocator.h @@ -19,6 +19,7 @@ #include "webrtc/p2p/base/port.h" #include "webrtc/p2p/base/portinterface.h" #include "webrtc/base/helpers.h" +#include "webrtc/base/proxyinfo.h" #include "webrtc/base/sigslot.h" #include "webrtc/base/thread.h" @@ -383,6 +384,16 @@ class PortAllocator : public sigslot::has_slots<> { uint32_t flags() const { return flags_; } void set_flags(uint32_t flags) { flags_ = flags; } + // These three methods are deprecated. If connections need to go through a + // proxy, the application should create a BasicPortAllocator given a custom + // PacketSocketFactory that creates proxy sockets. + const std::string& user_agent() const { return agent_; } + const rtc::ProxyInfo& proxy() const { return proxy_; } + void set_proxy(const std::string& agent, const rtc::ProxyInfo& proxy) { + agent_ = agent; + proxy_ = proxy; + } + // Gets/Sets the port range to use when choosing client ports. int min_port() const { return min_port_; } int max_port() const { return max_port_; } @@ -435,6 +446,8 @@ class PortAllocator : public sigslot::has_slots<> { } uint32_t flags_; + std::string agent_; + rtc::ProxyInfo proxy_; int min_port_; int max_port_; uint32_t step_delay_; diff --git a/webrtc/p2p/base/relayport.cc b/webrtc/p2p/base/relayport.cc index 3d56c4acb4..2ab71dc3e4 100644 --- a/webrtc/p2p/base/relayport.cc +++ b/webrtc/p2p/base/relayport.cc @@ -210,7 +210,15 @@ RelayPort::~RelayPort() { } void RelayPort::AddServerAddress(const ProtocolAddress& addr) { - server_addr_.push_back(addr); + // Since HTTP proxies usually only allow 443, + // let's up the priority on PROTO_SSLTCP + if (addr.proto == PROTO_SSLTCP && + (proxy().type == rtc::PROXY_HTTPS || + proxy().type == rtc::PROXY_UNKNOWN)) { + server_addr_.push_front(addr); + } else { + server_addr_.push_back(addr); + } } void RelayPort::AddExternalAddress(const ProtocolAddress& addr) { diff --git a/webrtc/p2p/client/basicportallocator.cc b/webrtc/p2p/client/basicportallocator.cc index aebe0f24ee..94de2b55f5 100644 --- a/webrtc/p2p/client/basicportallocator.cc +++ b/webrtc/p2p/client/basicportallocator.cc @@ -699,6 +699,8 @@ void BasicPortAllocatorSession::AddAllocatedPort(Port* port, port->set_content_name(content_name()); port->set_component(component()); port->set_generation(generation()); + if (allocator_->proxy().type != rtc::PROXY_NONE) + port->set_proxy(allocator_->user_agent(), allocator_->proxy()); port->set_send_retransmit_count_attribute( (flags() & PORTALLOCATOR_ENABLE_STUN_RETRANSMIT_ATTRIBUTE) != 0);