diff options
Diffstat (limited to '')
-rw-r--r-- | lib/libUPnP/Neptune/Source/Core/NptHttp.cpp | 3483 |
1 files changed, 3483 insertions, 0 deletions
diff --git a/lib/libUPnP/Neptune/Source/Core/NptHttp.cpp b/lib/libUPnP/Neptune/Source/Core/NptHttp.cpp new file mode 100644 index 0000000..db2c507 --- /dev/null +++ b/lib/libUPnP/Neptune/Source/Core/NptHttp.cpp @@ -0,0 +1,3483 @@ +/***************************************************************** +| +| Neptune - HTTP Protocol +| +| Copyright (c) 2002-2008, Axiomatic Systems, LLC. +| All rights reserved. +| +| Redistribution and use in source and binary forms, with or without +| modification, are permitted provided that the following conditions are met: +| * Redistributions of source code must retain the above copyright +| notice, this list of conditions and the following disclaimer. +| * Redistributions in binary form must reproduce the above copyright +| notice, this list of conditions and the following disclaimer in the +| documentation and/or other materials provided with the distribution. +| * Neither the name of Axiomatic Systems nor the +| names of its contributors may be used to endorse or promote products +| derived from this software without specific prior written permission. +| +| THIS SOFTWARE IS PROVIDED BY AXIOMATIC SYSTEMS ''AS IS'' AND ANY +| EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +| WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +| DISCLAIMED. IN NO EVENT SHALL AXIOMATIC SYSTEMS BE LIABLE FOR ANY +| DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +| (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +| LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +| ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +| SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +| + ****************************************************************/ + +/*---------------------------------------------------------------------- +| includes ++---------------------------------------------------------------------*/ +#include "NptHttp.h" +#include "NptSockets.h" +#include "NptBufferedStreams.h" +#include "NptDebug.h" +#include "NptVersion.h" +#include "NptUtils.h" +#include "NptFile.h" +#include "NptSystem.h" +#include "NptLogging.h" +#include "NptTls.h" +#include "NptStreams.h" + +/*---------------------------------------------------------------------- +| logging ++---------------------------------------------------------------------*/ +NPT_SET_LOCAL_LOGGER("neptune.http") + +/*---------------------------------------------------------------------- +| constants ++---------------------------------------------------------------------*/ +const char* const NPT_HTTP_DEFAULT_403_HTML = "<html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>Access to this URL is forbidden.</p></html>"; +const char* const NPT_HTTP_DEFAULT_404_HTML = "<html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></html>"; +const char* const NPT_HTTP_DEFAULT_500_HTML = "<html><head><title>500 Internal Error</title></head><body><h1>Internal Error</h1><p>The server encountered an unexpected condition which prevented it from fulfilling the request.</p></html>"; + +/*---------------------------------------------------------------------- +| NPT_HttpUrl::NPT_HttpUrl ++---------------------------------------------------------------------*/ +NPT_HttpUrl::NPT_HttpUrl(const char* url, bool ignore_scheme) : + NPT_Url(url) +{ + if (!ignore_scheme) { + if (GetSchemeId() != NPT_Uri::SCHEME_ID_HTTP && + GetSchemeId() != NPT_Uri::SCHEME_ID_HTTPS) { + Reset(); + } + } +} + +/*---------------------------------------------------------------------- +| NPT_HttpUrl::NPT_HttpUrl ++---------------------------------------------------------------------*/ +NPT_HttpUrl::NPT_HttpUrl(const char* host, + NPT_UInt16 port, + const char* path, + const char* query, + const char* fragment) : + NPT_Url("http", host, port, path, query, fragment) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpUrl::ToString ++---------------------------------------------------------------------*/ +NPT_String +NPT_HttpUrl::ToString(bool with_fragment) const +{ + NPT_UInt16 default_port; + switch (m_SchemeId) { + case SCHEME_ID_HTTP: default_port = NPT_HTTP_DEFAULT_PORT; break; + case SCHEME_ID_HTTPS: default_port = NPT_HTTPS_DEFAULT_PORT; break; + default: default_port = 0; + } + return NPT_Url::ToStringWithDefaultPort(default_port, with_fragment); +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeader::NPT_HttpHeader ++---------------------------------------------------------------------*/ +NPT_HttpHeader::NPT_HttpHeader(const char* name, const char* value): + m_Name(name), + m_Value(value) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeader::~NPT_HttpHeader ++---------------------------------------------------------------------*/ +NPT_HttpHeader::~NPT_HttpHeader() +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeader::Emit ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpHeader::Emit(NPT_OutputStream& stream) const +{ + stream.WriteString(m_Name); + stream.WriteFully(": ", 2); + stream.WriteString(m_Value); + stream.WriteFully(NPT_HTTP_LINE_TERMINATOR, 2); + NPT_LOG_FINEST_2("header %s: %s", m_Name.GetChars(), m_Value.GetChars()); + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeader::SetName ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpHeader::SetName(const char* name) +{ + m_Name = name; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeader::~NPT_HttpHeader ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpHeader::SetValue(const char* value) +{ + m_Value = value; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeaders::NPT_HttpHeaders ++---------------------------------------------------------------------*/ +NPT_HttpHeaders::NPT_HttpHeaders() +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeaders::~NPT_HttpHeaders ++---------------------------------------------------------------------*/ +NPT_HttpHeaders::~NPT_HttpHeaders() +{ + m_Headers.Apply(NPT_ObjectDeleter<NPT_HttpHeader>()); +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeaders::Parse ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpHeaders::Parse(NPT_BufferedInputStream& stream) +{ + NPT_String header_name; + NPT_String header_value; + bool header_pending = false; + NPT_String line; + + while (NPT_SUCCEEDED(stream.ReadLine(line, NPT_HTTP_PROTOCOL_MAX_LINE_LENGTH))) { + if (line.GetLength() == 0) { + // empty line, end of headers + break; + } + if (header_pending && (line[0] == ' ' || line[0] == '\t')) { + // continuation (folded header) + header_value.Append(line.GetChars()+1, line.GetLength()-1); + } else { + // add the pending header to the list + if (header_pending) { + header_value.Trim(); + AddHeader(header_name, header_value); + header_pending = false; + NPT_LOG_FINEST_2("header - %s: %s", + header_name.GetChars(), + header_value.GetChars()); + } + + // find the colon separating the name and the value + int colon_index = line.Find(':'); + if (colon_index < 1) { + // invalid syntax, ignore + continue; + } + header_name = line.Left(colon_index); + + // the field value starts at the first non-whitespace + const char* value = line.GetChars()+colon_index+1; + while (*value == ' ' || *value == '\t') { + value++; + } + header_value = value; + + // the header is pending + header_pending = true; + } + } + + // if we have a header pending, add it now + if (header_pending) { + header_value.Trim(); + AddHeader(header_name, header_value); + NPT_LOG_FINEST_2("header %s: %s", + header_name.GetChars(), + header_value.GetChars()); + } + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeaders::Emit ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpHeaders::Emit(NPT_OutputStream& stream) const +{ + // for each header in the list + NPT_List<NPT_HttpHeader*>::Iterator header = m_Headers.GetFirstItem(); + while (header) { + // emit the header + NPT_CHECK_WARNING((*header)->Emit(stream)); + ++header; + } + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeaders::GetHeader ++---------------------------------------------------------------------*/ +NPT_HttpHeader* +NPT_HttpHeaders::GetHeader(const char* name) const +{ + // check args + if (name == NULL) return NULL; + + // find a matching header + NPT_List<NPT_HttpHeader*>::Iterator header = m_Headers.GetFirstItem(); + while (header) { + if ((*header)->GetName().Compare(name, true) == 0) { + return *header; + } + ++header; + } + + // not found + return NULL; +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeaders::AddHeader ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpHeaders::AddHeader(const char* name, const char* value) +{ + return m_Headers.Add(new NPT_HttpHeader(name, value)); +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeaders::RemoveHeader ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpHeaders::RemoveHeader(const char* name) +{ + bool found = false; + + NPT_HttpHeader* header = NULL; + while ((header = GetHeader(name))) { + m_Headers.Remove(header); + delete header; + found = true; + } + return found?NPT_SUCCESS:NPT_ERROR_NO_SUCH_ITEM; +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeaders::SetHeader ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpHeaders::SetHeader(const char* name, const char* value, bool replace) +{ + NPT_HttpHeader* header = GetHeader(name); + if (header == NULL) { + return AddHeader(name, value); + } else if (replace) { + return header->SetValue(value); + } else { + return NPT_SUCCESS; + } +} + +/*---------------------------------------------------------------------- +| NPT_HttpHeaders::GetHeaderValue ++---------------------------------------------------------------------*/ +const NPT_String* +NPT_HttpHeaders::GetHeaderValue(const char* name) const +{ + NPT_HttpHeader* header = GetHeader(name); + if (header == NULL) { + return NULL; + } else { + return &header->GetValue(); + } +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntityBodyInputStream ++---------------------------------------------------------------------*/ +class NPT_HttpEntityBodyInputStream : public NPT_InputStream +{ +public: + // constructor and desctructor + NPT_HttpEntityBodyInputStream(NPT_BufferedInputStreamReference& source, + NPT_LargeSize size, + bool size_is_known, + bool chunked, + NPT_HttpClient::Connection* connection, + bool should_persist); + ~NPT_HttpEntityBodyInputStream() override; + + // methods + bool SizeIsKnown() { return m_SizeIsKnown; } + + // NPT_InputStream methods + NPT_Result Read(void* buffer, + NPT_Size bytes_to_read, + NPT_Size* bytes_read = NULL) override; + NPT_Result Seek(NPT_Position /*offset*/) override { + return NPT_ERROR_NOT_SUPPORTED; + } + NPT_Result Tell(NPT_Position& offset) override { + offset = m_Position; + return NPT_SUCCESS; + } + NPT_Result GetSize(NPT_LargeSize& size) override { + size = m_Size; + return NPT_SUCCESS; + } + NPT_Result GetAvailable(NPT_LargeSize& available) override; + +private: + // methods + virtual void OnFullyRead(); + + // members + NPT_LargeSize m_Size; + bool m_SizeIsKnown; + bool m_Chunked; + NPT_HttpClient::Connection* m_Connection; + bool m_ShouldPersist; + NPT_Position m_Position; + NPT_InputStreamReference m_Source; +}; + +/*---------------------------------------------------------------------- +| NPT_HttpEntityBodyInputStream::NPT_HttpEntityBodyInputStream ++---------------------------------------------------------------------*/ +NPT_HttpEntityBodyInputStream::NPT_HttpEntityBodyInputStream( + NPT_BufferedInputStreamReference& source, + NPT_LargeSize size, + bool size_is_known, + bool chunked, + NPT_HttpClient::Connection* connection, + bool should_persist) : + m_Size(size), + m_SizeIsKnown(size_is_known), + m_Chunked(chunked), + m_Connection(connection), + m_ShouldPersist(should_persist), + m_Position(0) +{ + if (size_is_known && size == 0) { + OnFullyRead(); + } else { + if (chunked) { + m_Source = NPT_InputStreamReference(new NPT_HttpChunkedInputStream(source)); + } else { + m_Source = source; + } + } +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntityBodyInputStream::~NPT_HttpEntityBodyInputStream ++---------------------------------------------------------------------*/ +NPT_HttpEntityBodyInputStream::~NPT_HttpEntityBodyInputStream() +{ + delete m_Connection; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntityBodyInputStream::OnFullyRead ++---------------------------------------------------------------------*/ +void +NPT_HttpEntityBodyInputStream::OnFullyRead() +{ + m_Source = NULL; + if (m_Connection && m_ShouldPersist) { + m_Connection->Recycle(); + m_Connection = NULL; + } +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntityBodyInputStream::Read ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntityBodyInputStream::Read(void* buffer, + NPT_Size bytes_to_read, + NPT_Size* bytes_read) +{ + if (bytes_read) *bytes_read = 0; + + // return now if we've already reached the end + if (m_Source.IsNull()) return NPT_ERROR_EOS; + + // clamp to the max possible read size + if (!m_Chunked && m_SizeIsKnown) { + NPT_LargeSize max_can_read = m_Size-m_Position; + if (max_can_read == 0) return NPT_ERROR_EOS; + if (bytes_to_read > max_can_read) bytes_to_read = (NPT_Size)max_can_read; + } + + // read from the source + NPT_Size source_bytes_read = 0; + NPT_Result result = m_Source->Read(buffer, bytes_to_read, &source_bytes_read); + if (NPT_SUCCEEDED(result)) { + m_Position += source_bytes_read; + if (bytes_read) *bytes_read = source_bytes_read; + } + + // check if we've reached the end + if (result == NPT_ERROR_EOS || (m_SizeIsKnown && (m_Position == m_Size))) { + OnFullyRead(); + } + + return result; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntityBodyInputStream::GetAvaialble ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntityBodyInputStream::GetAvailable(NPT_LargeSize& available) +{ + if (m_Source.IsNull()) { + available = 0; + return NPT_SUCCESS; + } + NPT_Result result = m_Source->GetAvailable(available); + if (NPT_FAILED(result)) { + available = 0; + return result; + } + if (available > m_Size-m_Position) { + available = m_Size-m_Position; + } + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::NPT_HttpEntity ++---------------------------------------------------------------------*/ +NPT_HttpEntity::NPT_HttpEntity() : + m_ContentLength(0), + m_ContentLengthIsKnown(false) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::NPT_HttpEntity ++---------------------------------------------------------------------*/ +NPT_HttpEntity::NPT_HttpEntity(const NPT_HttpHeaders& headers) : + m_ContentLength(0), + m_ContentLengthIsKnown(false) +{ + SetHeaders(headers); +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::SetHeaders ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::SetHeaders(const NPT_HttpHeaders& headers) +{ + NPT_HttpHeader* header; + + // Content-Length + header = headers.GetHeader(NPT_HTTP_HEADER_CONTENT_LENGTH); + if (header != NULL) { + m_ContentLengthIsKnown = true; + NPT_LargeSize length; + if (NPT_SUCCEEDED(header->GetValue().ToInteger64(length))) { + m_ContentLength = length; + } else { + m_ContentLength = 0; + } + } + + // Content-Type + header = headers.GetHeader(NPT_HTTP_HEADER_CONTENT_TYPE); + if (header != NULL) { + m_ContentType = header->GetValue(); + } + + // Content-Encoding + header = headers.GetHeader(NPT_HTTP_HEADER_CONTENT_ENCODING); + if (header != NULL) { + m_ContentEncoding = header->GetValue(); + } + + // Transfer-Encoding + header = headers.GetHeader(NPT_HTTP_HEADER_TRANSFER_ENCODING); + if (header != NULL) { + m_TransferEncoding = header->GetValue(); + } + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::~NPT_HttpEntity ++---------------------------------------------------------------------*/ +NPT_HttpEntity::~NPT_HttpEntity() +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::GetInputStream ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::GetInputStream(NPT_InputStreamReference& stream) +{ + // reset output params first + stream = NULL; + + if (m_InputStream.IsNull()) return NPT_FAILURE; + + stream = m_InputStream; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::SetInputStream ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::SetInputStream(const NPT_InputStreamReference& stream, + bool update_content_length /* = false */) +{ + m_InputStream = stream; + + // get the content length from the stream + if (update_content_length && !stream.IsNull()) { + NPT_LargeSize length; + if (NPT_SUCCEEDED(stream->GetSize(length))) { + return SetContentLength(length); + } + } + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::SetInputStream ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::SetInputStream(const void* data, NPT_Size data_size) +{ + NPT_MemoryStream* memory_stream = new NPT_MemoryStream(data, data_size); + NPT_InputStreamReference body(memory_stream); + return SetInputStream(body, true); +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::SetInputStream ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::SetInputStream(const char* string) +{ + if (string == NULL) return NPT_ERROR_INVALID_PARAMETERS; + NPT_MemoryStream* memory_stream = new NPT_MemoryStream((const void*)string, + NPT_StringLength(string)); + NPT_InputStreamReference body(memory_stream); + return SetInputStream(body, true); +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::SetInputStream ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::SetInputStream(const NPT_String& string) +{ + NPT_MemoryStream* memory_stream = new NPT_MemoryStream((const void*)string.GetChars(), + string.GetLength()); + NPT_InputStreamReference body(memory_stream); + return SetInputStream(body, true); +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::Load ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::Load(NPT_DataBuffer& buffer) +{ + // check that we have an input stream + if (m_InputStream.IsNull()) return NPT_ERROR_INVALID_STATE; + + // load the stream into the buffer + if (m_ContentLength != (NPT_Size)m_ContentLength) return NPT_ERROR_OUT_OF_RANGE; + return m_InputStream->Load(buffer, (NPT_Size)m_ContentLength); +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::SetContentLength ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::SetContentLength(NPT_LargeSize length) +{ + m_ContentLength = length; + m_ContentLengthIsKnown = true; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::SetContentType ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::SetContentType(const char* type) +{ + m_ContentType = type; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::SetContentEncoding ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::SetContentEncoding(const char* encoding) +{ + m_ContentEncoding = encoding; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEntity::SetTransferEncoding ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEntity::SetTransferEncoding(const char* encoding) +{ + m_TransferEncoding = encoding; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpMessage::NPT_HttpMessage ++---------------------------------------------------------------------*/ +NPT_HttpMessage::NPT_HttpMessage(const char* protocol) : + m_Protocol(protocol), + m_Entity(NULL) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpMessage::NPT_HttpMessage ++---------------------------------------------------------------------*/ +NPT_HttpMessage::~NPT_HttpMessage() +{ + delete m_Entity; +} + +/*---------------------------------------------------------------------- +| NPT_HttpMessage::SetEntity ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpMessage::SetEntity(NPT_HttpEntity* entity) +{ + if (entity != m_Entity) { + delete m_Entity; + m_Entity = entity; + } + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpMessage::ParseHeaders ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpMessage::ParseHeaders(NPT_BufferedInputStream& stream) +{ + return m_Headers.Parse(stream); +} + +/*---------------------------------------------------------------------- +| NPT_HttpRequest::NPT_HttpRequest ++---------------------------------------------------------------------*/ +NPT_HttpRequest::NPT_HttpRequest(const NPT_HttpUrl& url, + const char* method, + const char* protocol) : + NPT_HttpMessage(protocol), + m_Url(url), + m_Method(method) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpRequest::NPT_HttpRequest ++---------------------------------------------------------------------*/ +NPT_HttpRequest::NPT_HttpRequest(const char* url, + const char* method, + const char* protocol) : + NPT_HttpMessage(protocol), + m_Url(url), + m_Method(method) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpRequest::SetUrl ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpRequest::SetUrl(const char* url) +{ + m_Url = url; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpRequest::SetUrl ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpRequest::SetUrl(const NPT_HttpUrl& url) +{ + m_Url = url; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpRequest::Parse ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpRequest::Parse(NPT_BufferedInputStream& stream, + const NPT_SocketAddress* endpoint, + NPT_HttpRequest*& request) +{ + // default return value + request = NULL; + +skip_first_empty_line: + // read the request line + NPT_String line; + NPT_CHECK_FINER(stream.ReadLine(line, NPT_HTTP_PROTOCOL_MAX_LINE_LENGTH)); + NPT_LOG_FINEST_1("http request: %s", line.GetChars()); + + // cleanup lines that may contain '\0' as first character, clients such + // Spotify desktop app send SSDP M-SEARCH requests followed by an extra + // '\0' character which stays in the buffered stream and messes up parsing + // the next request. + while (line.GetLength() > 0 && line[0] == '\0') { + line = line.Erase(0, 1); + } + + // when using keep-alive connections, clients such as XBox 360 + // incorrectly send a few empty lines as body for GET requests + // so we try to skip them until we find something to parse + if (line.GetLength() == 0) goto skip_first_empty_line; + + // check the request line + int first_space = line.Find(' '); + if (first_space < 0) { + NPT_LOG_FINE_1("http request: %s", line.GetChars()); + return NPT_ERROR_HTTP_INVALID_REQUEST_LINE; + } + int second_space = line.Find(' ', first_space+1); + if (second_space < 0) { + NPT_LOG_FINE_1("http request: %s", line.GetChars()); + return NPT_ERROR_HTTP_INVALID_REQUEST_LINE; + } + + // parse the request line + NPT_String method = line.SubString(0, first_space); + NPT_String uri = line.SubString(first_space+1, second_space-first_space-1); + NPT_String protocol = line.SubString(second_space+1); + + // create a request + bool proxy_style_request = false; + if (uri.StartsWith("http://", true)) { + // proxy-style request with absolute URI + request = new NPT_HttpRequest(uri, method, protocol); + proxy_style_request = true; + } else { + // normal absolute path request + request = new NPT_HttpRequest("http:", method, protocol); + } + + // parse headers + NPT_Result result = request->ParseHeaders(stream); + if (NPT_FAILED(result)) { + delete request; + request = NULL; + return result; + } + + // update the URL + if (!proxy_style_request) { + request->m_Url.SetScheme("http"); + request->m_Url.ParsePathPlus(uri); + request->m_Url.SetPort(NPT_HTTP_DEFAULT_PORT); + + // check for a Host: header + NPT_HttpHeader* host_header = request->GetHeaders().GetHeader(NPT_HTTP_HEADER_HOST); + if (host_header) { + request->m_Url.SetHost(host_header->GetValue()); + + // host sometimes doesn't contain port + if (endpoint) { + request->m_Url.SetPort(endpoint->GetPort()); + } + } else { + // use the endpoint as the host + if (endpoint) { + request->m_Url.SetHost(endpoint->ToString()); + } else { + // use defaults + request->m_Url.SetHost("localhost"); + } + } + } + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpRequest::~NPT_HttpRequest ++---------------------------------------------------------------------*/ +NPT_HttpRequest::~NPT_HttpRequest() +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpRequest::Emit ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpRequest::Emit(NPT_OutputStream& stream, bool use_proxy) const +{ + // write the request line + stream.WriteString(m_Method); + stream.WriteFully(" ", 1); + if (use_proxy) { + stream.WriteString(m_Url.ToString(false)); + } else { + stream.WriteString(m_Url.ToRequestString()); + } + stream.WriteFully(" ", 1); + stream.WriteString(m_Protocol); + stream.WriteFully(NPT_HTTP_LINE_TERMINATOR, 2); + + // emit headers + m_Headers.Emit(stream); + + // finish with an empty line + stream.WriteFully(NPT_HTTP_LINE_TERMINATOR, 2); + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponse::NPT_HttpResponse ++---------------------------------------------------------------------*/ +NPT_HttpResponse::NPT_HttpResponse(NPT_HttpStatusCode status_code, + const char* reason_phrase, + const char* protocol) : + NPT_HttpMessage(protocol), + m_StatusCode(status_code), + m_ReasonPhrase(reason_phrase) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponse::~NPT_HttpResponse ++---------------------------------------------------------------------*/ +NPT_HttpResponse::~NPT_HttpResponse() +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponse::SetStatus ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpResponse::SetStatus(NPT_HttpStatusCode status_code, + const char* reason_phrase, + const char* protocol) +{ + m_StatusCode = status_code; + m_ReasonPhrase = reason_phrase; + if (protocol) m_Protocol = protocol; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponse::SetProtocol ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpResponse::SetProtocol(const char* protocol) +{ + m_Protocol = protocol; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponse::Emit ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpResponse::Emit(NPT_OutputStream& stream) const +{ + // write the request line + stream.WriteString(m_Protocol); + stream.WriteFully(" ", 1); + stream.WriteString(NPT_String::FromInteger(m_StatusCode)); + stream.WriteFully(" ", 1); + stream.WriteString(m_ReasonPhrase); + stream.WriteFully(NPT_HTTP_LINE_TERMINATOR, 2); + + // emit headers + m_Headers.Emit(stream); + + // finish with an empty line + stream.WriteFully(NPT_HTTP_LINE_TERMINATOR, 2); + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponse::Parse ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpResponse::Parse(NPT_BufferedInputStream& stream, + NPT_HttpResponse*& response) +{ + // default return value + response = NULL; + + // read the response line + NPT_String line; + NPT_CHECK_WARNING(stream.ReadLine(line, NPT_HTTP_PROTOCOL_MAX_LINE_LENGTH)); + + NPT_LOG_FINER_1("http response: %s", line.GetChars()); + + // check the response line + // we are lenient here, as we allow the response to deviate slightly from + // strict HTTP (for example, ICY servers response with a method equal to + // ICY insead of HTTP/1.X) + int first_space = line.Find(' '); + if (first_space < 1) return NPT_ERROR_HTTP_INVALID_RESPONSE_LINE; + int second_space = line.Find(' ', first_space+1); + if (second_space < 0) { + // some servers omit (incorrectly) the space and Reason-Code + // but we don't fail them just for that. Just check that the + // status code looks ok + if (line.GetLength() != 12) { + return NPT_ERROR_HTTP_INVALID_RESPONSE_LINE; + } + } else if (second_space-first_space != 4) { + // the status code is not of length 3 + return NPT_ERROR_HTTP_INVALID_RESPONSE_LINE; + } + + // parse the response line + NPT_String protocol = line.SubString(0, first_space); + NPT_String status_code = line.SubString(first_space+1, 3); + NPT_String reason_phrase = line.SubString(first_space+1+3+1, + line.GetLength()-(first_space+1+3+1)); + + // create a response object + NPT_UInt32 status_code_int = 0; + status_code.ToInteger(status_code_int); + response = new NPT_HttpResponse(status_code_int, reason_phrase, protocol); + + // parse headers + NPT_Result result = response->ParseHeaders(stream); + if (NPT_FAILED(result)) { + delete response; + response = NULL; + } + + return result; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEnvProxySelector ++---------------------------------------------------------------------*/ +class NPT_HttpEnvProxySelector : public NPT_HttpProxySelector, + public NPT_AutomaticCleaner::Singleton +{ +public: + static NPT_HttpEnvProxySelector* GetInstance(); + + // NPT_HttpProxySelector methods + NPT_Result GetProxyForUrl(const NPT_HttpUrl& url, NPT_HttpProxyAddress& proxy) override; + +private: + // class variables + static NPT_HttpEnvProxySelector* Instance; + + // class methods + static void ParseProxyEnv(const NPT_String& env, NPT_HttpProxyAddress& proxy); + + // members + NPT_HttpProxyAddress m_HttpProxy; + NPT_HttpProxyAddress m_HttpsProxy; + NPT_List<NPT_String> m_NoProxy; + NPT_HttpProxyAddress m_AllProxy; +}; +NPT_HttpEnvProxySelector* NPT_HttpEnvProxySelector::Instance = NULL; + +/*---------------------------------------------------------------------- +| NPT_HttpEnvProxySelector::GetInstance ++---------------------------------------------------------------------*/ +NPT_HttpEnvProxySelector* +NPT_HttpEnvProxySelector::GetInstance() +{ + if (Instance) return Instance; + + NPT_SingletonLock::GetInstance().Lock(); + if (Instance == NULL) { + // create the shared instance + Instance = new NPT_HttpEnvProxySelector(); + + // prepare for recycling + NPT_AutomaticCleaner::GetInstance()->Register(Instance); + + // parse the http proxy settings + NPT_String http_proxy; + NPT_Environment::Get("http_proxy", http_proxy); + ParseProxyEnv(http_proxy, Instance->m_HttpProxy); + NPT_LOG_FINE_2("http_proxy: %s:%d", Instance->m_HttpProxy.GetHostName().GetChars(), Instance->m_HttpProxy.GetPort()); + + // parse the https proxy settings + NPT_String https_proxy; + if (NPT_FAILED(NPT_Environment::Get("HTTPS_PROXY", https_proxy))) { + NPT_Environment::Get("https_proxy", https_proxy); + } + ParseProxyEnv(https_proxy, Instance->m_HttpsProxy); + NPT_LOG_FINE_2("https_proxy: %s:%d", Instance->m_HttpsProxy.GetHostName().GetChars(), Instance->m_HttpsProxy.GetPort()); + + // parse the all-proxy settings + NPT_String all_proxy; + if (NPT_FAILED(NPT_Environment::Get("ALL_PROXY", all_proxy))) { + NPT_Environment::Get("all_proxy", all_proxy); + } + ParseProxyEnv(all_proxy, Instance->m_AllProxy); + NPT_LOG_FINE_2("all_proxy: %s:%d", Instance->m_AllProxy.GetHostName().GetChars(), Instance->m_AllProxy.GetPort()); + + // parse the no-proxy settings + NPT_String no_proxy; + if (NPT_FAILED(NPT_Environment::Get("NO_PROXY", no_proxy))) { + NPT_Environment::Get("no_proxy", no_proxy); + } + if (no_proxy.GetLength()) { + Instance->m_NoProxy = no_proxy.Split(","); + } + } + NPT_SingletonLock::GetInstance().Unlock(); + + return Instance; +} + +/*---------------------------------------------------------------------- +| NPT_HttpEnvProxySelector::ParseProxyEnv ++---------------------------------------------------------------------*/ +void +NPT_HttpEnvProxySelector::ParseProxyEnv(const NPT_String& env, + NPT_HttpProxyAddress& proxy) +{ + // ignore empty strings + if (env.GetLength() == 0) return; + + NPT_String proxy_spec; + if (env.Find("://") >= 0) { + proxy_spec = env; + } else { + proxy_spec = "http://"+env; + } + NPT_Url url(proxy_spec); + proxy.SetHostName(url.GetHost()); + proxy.SetPort(url.GetPort()); +} + +/*---------------------------------------------------------------------- +| NPT_HttpEnvProxySelector::GetProxyForUrl ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpEnvProxySelector::GetProxyForUrl(const NPT_HttpUrl& url, + NPT_HttpProxyAddress& proxy) +{ + NPT_HttpProxyAddress* protocol_proxy = NULL; + switch (url.GetSchemeId()) { + case NPT_Uri::SCHEME_ID_HTTP: + protocol_proxy = &m_HttpProxy; + break; + + case NPT_Uri::SCHEME_ID_HTTPS: + protocol_proxy = &m_HttpsProxy; + break; + + default: + return NPT_ERROR_HTTP_NO_PROXY; + } + + // check for no-proxy first + if (m_NoProxy.GetItemCount()) { + for (NPT_List<NPT_String>::Iterator i = m_NoProxy.GetFirstItem(); + i; + ++i) { + if ((*i) == "*") { + return NPT_ERROR_HTTP_NO_PROXY; + } + if (url.GetHost().EndsWith(*i, true)) { + if (url.GetHost().GetLength() == (*i).GetLength()) { + // exact match + return NPT_ERROR_HTTP_NO_PROXY; + } + if (url.GetHost().GetChars()[url.GetHost().GetLength()-(*i).GetLength()-1] == '.') { + // subdomain match + return NPT_ERROR_HTTP_NO_PROXY; + } + } + } + } + + // check the protocol proxy + if (protocol_proxy->GetHostName().GetLength()) { + proxy = *protocol_proxy; + return NPT_SUCCESS; + } + + // use the default proxy + proxy = m_AllProxy; + + return proxy.GetHostName().GetLength()?NPT_SUCCESS:NPT_ERROR_HTTP_NO_PROXY; +} + +/*---------------------------------------------------------------------- +| NPT_HttpProxySelector::GetDefault ++---------------------------------------------------------------------*/ +static bool NPT_HttpProxySelector_ConfigChecked = false; +static unsigned int NPT_HttpProxySelector_Config = 0; +const unsigned int NPT_HTTP_PROXY_SELECTOR_CONFIG_NONE = 0; +const unsigned int NPT_HTTP_PROXY_SELECTOR_CONFIG_ENV = 1; +const unsigned int NPT_HTTP_PROXY_SELECTOR_CONFIG_SYSTEM = 2; +NPT_HttpProxySelector* +NPT_HttpProxySelector::GetDefault() +{ + if (!NPT_HttpProxySelector_ConfigChecked) { + NPT_String config; + if (NPT_SUCCEEDED(NPT_Environment::Get("NEPTUNE_NET_CONFIG_PROXY_SELECTOR", config))) { + if (config.Compare("noproxy", true) == 0) { + NPT_HttpProxySelector_Config = NPT_HTTP_PROXY_SELECTOR_CONFIG_NONE; + } else if (config.Compare("env", true) == 0) { + NPT_HttpProxySelector_Config = NPT_HTTP_PROXY_SELECTOR_CONFIG_ENV; + } else if (config.Compare("system", true) == 0) { + NPT_HttpProxySelector_Config = NPT_HTTP_PROXY_SELECTOR_CONFIG_SYSTEM; + } else { + NPT_HttpProxySelector_Config = NPT_HTTP_PROXY_SELECTOR_CONFIG_NONE; + } + } + NPT_HttpProxySelector_ConfigChecked = true; + } + + switch (NPT_HttpProxySelector_Config) { + case NPT_HTTP_PROXY_SELECTOR_CONFIG_NONE: + // no proxy + return NULL; + + case NPT_HTTP_PROXY_SELECTOR_CONFIG_ENV: + // use the shared instance + return NPT_HttpEnvProxySelector::GetInstance(); + + case NPT_HTTP_PROXY_SELECTOR_CONFIG_SYSTEM: + // use the sytem proxy selector + return GetSystemSelector(); + + default: + return NULL; + } +} + +/*---------------------------------------------------------------------- +| NPT_HttpProxySelector::GetSystemSelector ++---------------------------------------------------------------------*/ +#if !defined(NPT_CONFIG_HAVE_SYSTEM_PROXY_SELECTOR) +NPT_HttpProxySelector* +NPT_HttpProxySelector::GetSystemSelector() +{ + return NULL; +} +#endif + +/*---------------------------------------------------------------------- +| NPT_HttpStaticProxySelector ++---------------------------------------------------------------------*/ +class NPT_HttpStaticProxySelector : public NPT_HttpProxySelector +{ +public: + // constructor + NPT_HttpStaticProxySelector(const char* http_propxy_hostname, + NPT_UInt16 http_proxy_port, + const char* https_proxy_hostname, + NPT_UInt16 htts_proxy_port); + + // NPT_HttpProxySelector methods + NPT_Result GetProxyForUrl(const NPT_HttpUrl& url, NPT_HttpProxyAddress& proxy) override; + +private: + // members + NPT_HttpProxyAddress m_HttpProxy; + NPT_HttpProxyAddress m_HttpsProxy; +}; + +/*---------------------------------------------------------------------- +| NPT_HttpStaticProxySelector::NPT_HttpStaticProxySelector ++---------------------------------------------------------------------*/ +NPT_HttpStaticProxySelector::NPT_HttpStaticProxySelector(const char* http_proxy_hostname, + NPT_UInt16 http_proxy_port, + const char* https_proxy_hostname, + NPT_UInt16 https_proxy_port) : + m_HttpProxy( http_proxy_hostname, http_proxy_port), + m_HttpsProxy(https_proxy_hostname, https_proxy_port) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpStaticProxySelector::GetProxyForUrl ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpStaticProxySelector::GetProxyForUrl(const NPT_HttpUrl& url, + NPT_HttpProxyAddress& proxy) +{ + switch (url.GetSchemeId()) { + case NPT_Uri::SCHEME_ID_HTTP: + proxy = m_HttpProxy; + break; + + case NPT_Uri::SCHEME_ID_HTTPS: + proxy = m_HttpsProxy; + break; + + default: + return NPT_ERROR_HTTP_NO_PROXY; + } + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::NPT_HttpConnectionManager ++---------------------------------------------------------------------*/ +NPT_HttpConnectionManager::NPT_HttpConnectionManager() : + m_Lock(true), + m_MaxConnections(NPT_HTTP_CONNECTION_MANAGER_MAX_CONNECTION_POOL_SIZE), + m_MaxConnectionAge(NPT_HTTP_CONNECTION_MANAGER_MAX_CONNECTION_AGE) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::~NPT_HttpConnectionManager ++---------------------------------------------------------------------*/ +NPT_HttpConnectionManager::~NPT_HttpConnectionManager() +{ + // set abort flag and wait for thread to finish + m_Aborted.SetValue(1); + Wait(); + + m_Connections.Apply(NPT_ObjectDeleter<Connection>()); +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::GetInstance ++---------------------------------------------------------------------*/ +NPT_HttpConnectionManager* +NPT_HttpConnectionManager::GetInstance() +{ + if (Instance) return Instance; + + NPT_SingletonLock::GetInstance().Lock(); + if (Instance == NULL) { + // create the shared instance + Instance = new NPT_HttpConnectionManager(); + + // register to for automatic cleanup + NPT_AutomaticCleaner::GetInstance()->RegisterHttpConnectionManager(Instance); + + // Start shared instance + Instance->Start(); + } + NPT_SingletonLock::GetInstance().Unlock(); + + return Instance; +} +NPT_HttpConnectionManager* NPT_HttpConnectionManager::Instance = NULL; + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::Run ++---------------------------------------------------------------------*/ +void +NPT_HttpConnectionManager::Run() +{ + // try to cleanup every 5 secs + while (m_Aborted.WaitUntilEquals(1, 5000) == NPT_ERROR_TIMEOUT) { + NPT_AutoLock lock(m_Lock); + Cleanup(); + } +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::Cleanup ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpConnectionManager::Cleanup() +{ + NPT_TimeStamp now; + NPT_System::GetCurrentTimeStamp(now); + NPT_TimeStamp delta((float)m_MaxConnectionAge); + + NPT_List<Connection*>::Iterator tail = m_Connections.GetLastItem(); + while (tail) { + if (now < (*tail)->m_TimeStamp + delta) break; + NPT_LOG_FINE_1("cleaning up connection (%d remain)", m_Connections.GetItemCount()); + delete *tail; + m_Connections.Erase(tail); + tail = m_Connections.GetLastItem(); + } + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::FindConnection ++---------------------------------------------------------------------*/ +NPT_HttpConnectionManager::Connection* +NPT_HttpConnectionManager::FindConnection(NPT_SocketAddress& address) +{ + NPT_AutoLock lock(m_Lock); + Cleanup(); + + for (NPT_List<Connection*>::Iterator i = m_Connections.GetFirstItem(); + i; + ++i) { + Connection* connection = *i; + + NPT_SocketInfo info; + if (NPT_FAILED(connection->GetInfo(info))) continue; + + if (info.remote_address == address) { + m_Connections.Erase(i); + return connection; + } + } + + // not found + return NULL; +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::Track ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpConnectionManager::Track(NPT_HttpClient* client, NPT_HttpClient::Connection* connection) +{ + NPT_AutoLock lock(m_Lock); + + // look if already tracking client connections + ConnectionList* connections = NULL; + if (NPT_SUCCEEDED(m_ClientConnections.Get(client, connections))) { + // return immediately if connection is already associated with client + if (connections->Find(NPT_ObjectComparator<NPT_HttpClient::Connection*>(connection))) { + NPT_LOG_WARNING("Connection already associated to client."); + return NPT_SUCCESS; + } + connections->Add(connection); + return NPT_SUCCESS; + } + + // new client connections + ConnectionList new_connections; + + // add connection to new client connection list + new_connections.Add(connection); + + // track new client connections + m_ClientConnections.Put(client, new_connections); + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::UntrackConnection ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpConnectionManager::UntrackConnection(NPT_HttpClient::Connection* connection) +{ + NPT_AutoLock lock(m_Lock); + + if (!connection) { + return NPT_ERROR_INVALID_PARAMETERS; + } + + // look for connection by enumerating all client connections + NPT_List<NPT_Map<NPT_HttpClient*, ConnectionList>::Entry*>::Iterator entry = + m_ClientConnections.GetEntries().GetFirstItem(); + while (entry) { + NPT_HttpClient*& client = (NPT_HttpClient*&)(*entry)->GetKey(); + ConnectionList& connections = (ConnectionList&)(*entry)->GetValue(); + + // look for connection in client connection list + NPT_List<NPT_HttpClient::Connection*>::Iterator i = + connections.Find(NPT_ObjectComparator<NPT_HttpClient::Connection*>(connection)); + if (i) { + // remove it + connections.Erase(i); + + // untrack client if no more active connections for it + if (connections.GetItemCount() == 0) { + m_ClientConnections.Erase(client); + } + + return NPT_SUCCESS; + } + ++entry; + } + + return NPT_ERROR_NO_SUCH_ITEM; +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::Untrack ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpConnectionManager::Untrack(NPT_HttpClient::Connection* connection) +{ + // check first if ConnectionCanceller Instance has not been released already + // with static finalizers + if (Instance == NULL) return NPT_FAILURE; + + return GetInstance()->UntrackConnection(connection); +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::Recycle ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpConnectionManager::Recycle(NPT_HttpConnectionManager::Connection* connection) +{ + // Untrack connection + UntrackConnection(connection); + + { + NPT_AutoLock lock(m_Lock); + Cleanup(); + + // remove older connections to make room + while (m_Connections.GetItemCount() >= m_MaxConnections) { + NPT_List<Connection*>::Iterator head = m_Connections.GetFirstItem(); + if (!head) break; + delete *head; + m_Connections.Erase(head); + NPT_LOG_FINER("removing connection from pool to make some room"); + } + + if (connection) { + + // label this connection with the current timestamp and flag + NPT_System::GetCurrentTimeStamp(connection->m_TimeStamp); + connection->m_IsRecycled = true; + + // add the connection to the pool + m_Connections.Add(connection); + } + } + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::AbortConnections ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpConnectionManager::AbortConnections(NPT_HttpClient* client) +{ + NPT_AutoLock lock(m_Lock); + + ConnectionList* connections = NULL; + if (NPT_SUCCEEDED(m_ClientConnections.Get(client, connections))) { + for (NPT_List<NPT_HttpClient::Connection*>::Iterator i = connections->GetFirstItem(); + i; + ++i) { + (*i)->Abort(); + } + } + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::Connection::Connection ++---------------------------------------------------------------------*/ +NPT_HttpConnectionManager::Connection::Connection(NPT_HttpConnectionManager& manager, + NPT_SocketReference& socket, + NPT_InputStreamReference input_stream, + NPT_OutputStreamReference output_stream) : + m_Manager(manager), + m_IsRecycled(false), + m_Socket(socket), + m_InputStream(input_stream), + m_OutputStream(output_stream) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::Connection::~Connection ++---------------------------------------------------------------------*/ +NPT_HttpConnectionManager::Connection::~Connection() +{ + NPT_HttpConnectionManager::Untrack(this); +} + +/*---------------------------------------------------------------------- +| NPT_HttpConnectionManager::Connection::Recycle ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpConnectionManager::Connection::Recycle() +{ + return m_Manager.Recycle(this); +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::NPT_HttpClient ++---------------------------------------------------------------------*/ +NPT_HttpClient::NPT_HttpClient(Connector* connector, bool transfer_ownership) : + m_ProxySelector(NPT_HttpProxySelector::GetDefault()), + m_ProxySelectorIsOwned(false), + m_Connector(connector), + m_ConnectorIsOwned(transfer_ownership), + m_Aborted(false) +{ + if (connector == NULL) { + m_Connector = new NPT_HttpTlsConnector(); + m_ConnectorIsOwned = true; + } +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::~NPT_HttpClient ++---------------------------------------------------------------------*/ +NPT_HttpClient::~NPT_HttpClient() +{ + if (m_ProxySelectorIsOwned) { + delete m_ProxySelector; + } + if (m_ConnectorIsOwned) { + delete m_Connector; + } +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::SetConfig ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::SetConfig(const Config& config) +{ + m_Config = config; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::SetProxy ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::SetProxy(const char* http_proxy_hostname, + NPT_UInt16 http_proxy_port, + const char* https_proxy_hostname, + NPT_UInt16 https_proxy_port) +{ + if (m_ProxySelectorIsOwned) { + delete m_ProxySelector; + m_ProxySelector = NULL; + m_ProxySelectorIsOwned = false; + } + + // use a static proxy to hold on to the settings + m_ProxySelector = new NPT_HttpStaticProxySelector(http_proxy_hostname, + http_proxy_port, + https_proxy_hostname, + https_proxy_port); + m_ProxySelectorIsOwned = true; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::SetProxySelector ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::SetProxySelector(NPT_HttpProxySelector* selector) +{ + if (m_ProxySelectorIsOwned && m_ProxySelector != selector) { + delete m_ProxySelector; + } + m_ProxySelector = selector; + m_ProxySelectorIsOwned = false; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::SetConnector ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::SetConnector(Connector* connector) +{ + if (m_ConnectorIsOwned && m_Connector != connector) { + delete m_Connector; + } + m_Connector = connector; + m_ConnectorIsOwned = false; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::SetTimeouts ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::SetTimeouts(NPT_Timeout connection_timeout, + NPT_Timeout io_timeout, + NPT_Timeout name_resolver_timeout) +{ + m_Config.m_ConnectionTimeout = connection_timeout; + m_Config.m_IoTimeout = io_timeout; + m_Config.m_NameResolverTimeout = name_resolver_timeout; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::SetUserAgent ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::SetUserAgent(const char* user_agent) +{ + m_Config.m_UserAgent = user_agent; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::TrackConnection ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::TrackConnection(Connection* connection) +{ + NPT_AutoLock lock(m_AbortLock); + if (m_Aborted) return NPT_ERROR_CANCELLED; + return NPT_HttpConnectionManager::GetInstance()->Track(this, connection); +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::SendRequestOnce ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::SendRequestOnce(NPT_HttpRequest& request, + NPT_HttpResponse*& response, + NPT_HttpRequestContext* context /* = NULL */) +{ + // setup default values + NPT_Result result = NPT_SUCCESS; + response = NULL; + + NPT_LOG_FINE_1("requesting URL %s", request.GetUrl().ToString().GetChars()); + + // get the address and port to which we need to connect + NPT_HttpProxyAddress proxy; + bool use_proxy = false; + if (m_ProxySelector) { + // we have a proxy selector, ask it to select a proxy for this URL + result = m_ProxySelector->GetProxyForUrl(request.GetUrl(), proxy); + if (NPT_FAILED(result) && result != NPT_ERROR_HTTP_NO_PROXY) { + NPT_LOG_WARNING_1("proxy selector failure (%d)", result); + return result; + } + use_proxy = !proxy.GetHostName().IsEmpty(); + } + + // connect to the server or proxy + Connection* connection = NULL; + bool http_1_1 = (request.GetProtocol() == NPT_HTTP_PROTOCOL_1_1); + NPT_Reference<Connection> cref; + + // send the request to the server (in a loop, since we may need to reconnect with 1.1) + bool reconnect = false; + unsigned int watchdog = NPT_HTTP_MAX_RECONNECTS; + do { + cref = NULL; + connection = NULL; + NPT_LOG_FINE_3("calling connector (proxy:%s) (http 1.1:%s) (url:%s)", + use_proxy?"yes":"no", http_1_1?"yes":"no", request.GetUrl().ToStringWithDefaultPort(0).GetChars()); + NPT_CHECK_WARNING(m_Connector->Connect(request.GetUrl(), + *this, + use_proxy?&proxy:NULL, + http_1_1, + connection)); + NPT_LOG_FINE_1("got connection (reused: %s)", connection->IsRecycled()?"true":"false"); + + NPT_InputStreamReference input_stream = connection->GetInputStream(); + NPT_OutputStreamReference output_stream = connection->GetOutputStream(); + + cref = connection; + reconnect = connection->IsRecycled(); + + // update context if any + if (context) { + NPT_SocketInfo info; + cref->GetInfo(info); + context->SetLocalAddress(info.local_address); + context->SetRemoteAddress(info.remote_address); + } + + NPT_HttpEntity* entity = request.GetEntity(); + NPT_InputStreamReference body_stream; + + if (reconnect && entity && NPT_SUCCEEDED(entity->GetInputStream(body_stream)) && NPT_FAILED(body_stream->Seek(0))) { + // if body is not seekable, we can't afford to reuse a connection + // that could fail, so we reconnect a new one instead + NPT_LOG_FINE("rewinding body stream would fail ... create new connection"); + continue; + } + + // decide if this connection should persist + NPT_HttpHeaders& headers = request.GetHeaders(); + bool should_persist = http_1_1; + if (!connection->SupportsPersistence()) { + should_persist = false; + } + if (should_persist) { + const NPT_String* connection_header = headers.GetHeaderValue(NPT_HTTP_HEADER_CONNECTION); + if (connection_header && (*connection_header == "close")) { + should_persist = false; + } + } + + if (m_Config.m_UserAgent.GetLength()) { + headers.SetHeader(NPT_HTTP_HEADER_USER_AGENT, m_Config.m_UserAgent, false); // set but don't replace + } + + result = WriteRequest(*output_stream.AsPointer(), request, should_persist, use_proxy); + if (NPT_FAILED(result)) { + NPT_LOG_FINE_1("failed to write request headers (%d)", result); + if (reconnect && !m_Aborted) { + if (!body_stream.IsNull()) { + // go back to the start of the body so that we can resend + NPT_LOG_FINE("rewinding body stream in order to resend"); + result = body_stream->Seek(0); + if (NPT_FAILED(result)) { + NPT_CHECK_FINE(NPT_ERROR_HTTP_CANNOT_RESEND_BODY); + } + } + continue; + } else { + return result; + } + } + + result = ReadResponse(input_stream, + should_persist, + request.GetMethod() != NPT_HTTP_METHOD_HEAD, + response, + &cref); + if (NPT_FAILED(result)) { + NPT_LOG_FINE_1("failed to parse the response (%d)", result); + if (reconnect && !m_Aborted /*&& + (result == NPT_ERROR_EOS || + result == NPT_ERROR_CONNECTION_ABORTED || + result == NPT_ERROR_CONNECTION_RESET || + result == NPT_ERROR_READ_FAILED) GBG: don't look for specific error codes */) { + NPT_LOG_FINE("error is not fatal, retrying"); + if (!body_stream.IsNull()) { + // go back to the start of the body so that we can resend + NPT_LOG_FINE("rewinding body stream in order to resend"); + result = body_stream->Seek(0); + if (NPT_FAILED(result)) { + NPT_CHECK_FINE(NPT_ERROR_HTTP_CANNOT_RESEND_BODY); + } + } + continue; + } else { + // don't retry + return result; + } + } + break; + } while (reconnect && --watchdog && !m_Aborted); + + // check that we have a valid connection + if (NPT_FAILED(result) && !m_Aborted) { + NPT_LOG_FINE("failed after max reconnection attempts"); + return NPT_ERROR_HTTP_TOO_MANY_RECONNECTS; + } + + return result; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::WriteRequest ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::WriteRequest(NPT_OutputStream& output_stream, + NPT_HttpRequest& request, + bool should_persist, + bool use_proxy /* = false */) +{ + NPT_Result result = NPT_SUCCESS; + + // add any headers that may be missing + NPT_HttpHeaders& headers = request.GetHeaders(); + + if (!should_persist) { + headers.SetHeader(NPT_HTTP_HEADER_CONNECTION, "close", false); // set but don't replace + } + + NPT_String host = request.GetUrl().GetHost(); + NPT_UInt16 default_port = 0; + switch (request.GetUrl().GetSchemeId()) { + case NPT_Uri::SCHEME_ID_HTTP: default_port = NPT_HTTP_DEFAULT_PORT; break; + case NPT_Uri::SCHEME_ID_HTTPS: default_port = NPT_HTTPS_DEFAULT_PORT; break; + default: break; + } + if (request.GetUrl().GetPort() != default_port) { + host += ":"; + host += NPT_String::FromInteger(request.GetUrl().GetPort()); + } + headers.SetHeader(NPT_HTTP_HEADER_HOST, host, false); // set but don't replace + + // get the request entity to set additional headers + NPT_InputStreamReference body_stream; + NPT_HttpEntity* entity = request.GetEntity(); + if (entity && NPT_SUCCEEDED(entity->GetInputStream(body_stream))) { + // set the content length if known + if (entity->ContentLengthIsKnown()) { + headers.SetHeader(NPT_HTTP_HEADER_CONTENT_LENGTH, + NPT_String::FromInteger(entity->GetContentLength())); + } + + // content type + NPT_String content_type = entity->GetContentType(); + if (!content_type.IsEmpty()) { + headers.SetHeader(NPT_HTTP_HEADER_CONTENT_TYPE, content_type); + } + + // content encoding + NPT_String content_encoding = entity->GetContentEncoding(); + if (!content_encoding.IsEmpty()) { + headers.SetHeader(NPT_HTTP_HEADER_CONTENT_ENCODING, content_encoding); + } + + // transfer encoding + const NPT_String& transfer_encoding = entity->GetTransferEncoding(); + if (!transfer_encoding.IsEmpty()) { + headers.SetHeader(NPT_HTTP_HEADER_TRANSFER_ENCODING, transfer_encoding); + } + } + + // create a memory stream to buffer the headers + NPT_MemoryStream header_stream; + + // emit the request headers into the header buffer + request.Emit(header_stream, use_proxy && request.GetUrl().GetSchemeId()==NPT_Url::SCHEME_ID_HTTP); + + // send the headers + NPT_CHECK_WARNING(output_stream.WriteFully(header_stream.GetData(), header_stream.GetDataSize())); + + // send request body + if (entity && !body_stream.IsNull()) { + // check for chunked transfer encoding + NPT_OutputStream* dest = &output_stream; + if (entity->GetTransferEncoding() == NPT_HTTP_TRANSFER_ENCODING_CHUNKED) { + dest = new NPT_HttpChunkedOutputStream(output_stream); + } + + NPT_LOG_FINE_1("sending body stream, %lld bytes", entity->GetContentLength()); //FIXME: Would be 0 for chunked encoding + NPT_LargeSize bytes_written = 0; + + // content length = 0 means copy until input returns EOS + result = NPT_StreamToStreamCopy(*body_stream.AsPointer(), *dest, 0, entity->GetContentLength(), &bytes_written); + if (NPT_FAILED(result)) { + NPT_LOG_FINE_3("body stream only partially sent, %lld bytes (%d:%s)", + bytes_written, + result, + NPT_ResultText(result)); + } + + // flush to write out any buffered data left in chunked output if used + dest->Flush(); + + // cleanup (this will send zero size chunk followed by CRLF) + if (dest != &output_stream) delete dest; + } + + // flush the output stream so that everything is sent to the server + output_stream.Flush(); + + return result; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::ReadResponse ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::ReadResponse(NPT_InputStreamReference& input_stream, + bool should_persist, + bool expect_entity, + NPT_HttpResponse*& response, + NPT_Reference<Connection>* cref /* = NULL */) +{ + NPT_Result result; + + // setup default values + response = NULL; + + // create a buffered stream for this socket stream + NPT_BufferedInputStreamReference buffered_input_stream(new NPT_BufferedInputStream(input_stream)); + + // parse the response + for (unsigned int watchcat = 0; watchcat < NPT_HTTP_MAX_100_RESPONSES; watchcat++) { + // parse the response + result = NPT_HttpResponse::Parse(*buffered_input_stream, response); + NPT_CHECK_FINE(result); + + if (response->GetStatusCode() >= 100 && response->GetStatusCode() < 200) { + NPT_LOG_FINE_1("got %d response, continuing", response->GetStatusCode()); + delete response; + response = NULL; + continue; + } + NPT_LOG_FINER_2("got response, code=%d, msg=%s", + response->GetStatusCode(), + response->GetReasonPhrase().GetChars()); + break; + } + + // check that we have a valid response + if (response == NULL) { + NPT_LOG_FINE("failed after max continuation attempts"); + return NPT_ERROR_HTTP_TOO_MANY_RECONNECTS; + } + + // unbuffer the stream + buffered_input_stream->SetBufferSize(0); + + // decide if we should still try to reuse this connection later on + if (should_persist) { + const NPT_String* connection_header = response->GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_CONNECTION); + if (response->GetProtocol() == NPT_HTTP_PROTOCOL_1_1) { + if (connection_header && (*connection_header == "close")) { + should_persist = false; + } + } else { + if (!connection_header || (*connection_header != "keep-alive")) { + should_persist = false; + } + } + } + + // create an entity if one is expected in the response + if (expect_entity) { + NPT_HttpEntity* response_entity = new NPT_HttpEntity(response->GetHeaders()); + + // check if the content length is known + bool have_content_length = (response->GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_CONTENT_LENGTH) != NULL); + + // check for chunked Transfer-Encoding + bool chunked = false; + if (response_entity->GetTransferEncoding() == NPT_HTTP_TRANSFER_ENCODING_CHUNKED) { + chunked = true; + response_entity->SetTransferEncoding(NULL); + } + + // prepare to transfer ownership of the connection if needed + Connection* connection = NULL; + if (cref) { + connection = cref->AsPointer(); + cref->Detach(); // release the internal ref + // don't delete connection now so we can abort while readin response body, + // just pass ownership to NPT_HttpEntityBodyInputStream so it can recycle it + // when done if connection should persist + } + + // create the body stream wrapper + NPT_InputStream* response_body_stream = + new NPT_HttpEntityBodyInputStream(buffered_input_stream, + response_entity->GetContentLength(), + have_content_length, + chunked, + connection, + should_persist); + response_entity->SetInputStream(NPT_InputStreamReference(response_body_stream)); + response->SetEntity(response_entity); + } else { + if (should_persist && cref) { + Connection* connection = cref->AsPointer(); + cref->Detach(); // release the internal ref + connection->Recycle(); + } + } + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::SendRequest ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::SendRequest(NPT_HttpRequest& request, + NPT_HttpResponse*& response, + NPT_HttpRequestContext* context /* = NULL */) +{ + NPT_Cardinal watchdog = m_Config.m_MaxRedirects+1; + bool keep_going; + NPT_Result result; + + // reset aborted flag + m_Aborted = false; + + // default value + response = NULL; + + // check that for GET requests there is no entity + if (request.GetEntity() != NULL && + request.GetMethod() == NPT_HTTP_METHOD_GET) { + return NPT_ERROR_HTTP_INVALID_REQUEST; + } + + do { + keep_going = false; + result = SendRequestOnce(request, response, context); + if (NPT_FAILED(result)) break; + if (response && m_Config.m_MaxRedirects && + (request.GetMethod() == NPT_HTTP_METHOD_GET || + request.GetMethod() == NPT_HTTP_METHOD_HEAD) && + (response->GetStatusCode() == 301 || + response->GetStatusCode() == 302 || + response->GetStatusCode() == 303 || + response->GetStatusCode() == 307)) { + // handle redirect + const NPT_String* location = response->GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_LOCATION); + if (location) { + // check for location fields that are not absolute URLs + // (this is not allowed by the standard, but many web servers do it + if (location->StartsWith("/") || + (!location->StartsWith("http://", true) && + !location->StartsWith("https://", true))) { + NPT_LOG_FINE_1("Location: header (%s) is not an absolute URL, using it as a relative URL", location->GetChars()); + if (location->StartsWith("/")) { + NPT_LOG_FINE_1("redirecting to absolute path %s", location->GetChars()); + request.GetUrl().ParsePathPlus(*location); + } else { + NPT_String redirect_path = request.GetUrl().GetPath(); + int slash_pos = redirect_path.ReverseFind('/'); + if (slash_pos >= 0) { + redirect_path.SetLength(slash_pos+1); + } else { + redirect_path = "/"; + } + redirect_path += *location; + NPT_LOG_FINE_1("redirecting to absolute path %s", redirect_path.GetChars()); + request.GetUrl().ParsePathPlus(redirect_path); + } + } else { + // replace the request url + NPT_LOG_FINE_1("redirecting to %s", location->GetChars()); + request.SetUrl(*location); + // remove host header so it is replaced based on new url + request.GetHeaders().RemoveHeader(NPT_HTTP_HEADER_HOST); + } + keep_going = true; + delete response; + response = NULL; + } + } + } while (keep_going && --watchdog && !m_Aborted); + + // check if we were bitten by the watchdog + if (watchdog == 0) { + NPT_LOG_WARNING("too many HTTP redirects"); + return NPT_ERROR_HTTP_TOO_MANY_REDIRECTS; + } + + return result; +} + +/*---------------------------------------------------------------------- +| NPT_HttpClient::Abort ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpClient::Abort() +{ + NPT_AutoLock lock(m_AbortLock); + m_Aborted = true; + + NPT_HttpConnectionManager::GetInstance()->AbortConnections(this); + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpRequestContext::NPT_HttpRequestContext ++---------------------------------------------------------------------*/ +NPT_HttpRequestContext::NPT_HttpRequestContext(const NPT_SocketAddress* local_address, + const NPT_SocketAddress* remote_address) +{ + if (local_address) m_LocalAddress = *local_address; + if (remote_address) m_RemoteAddress = *remote_address; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::NPT_HttpServer ++---------------------------------------------------------------------*/ +NPT_HttpServer::NPT_HttpServer(NPT_UInt16 listen_port, bool cancellable) : + m_Socket(cancellable?NPT_SOCKET_FLAG_CANCELLABLE:0), + m_BoundPort(0), + m_ServerHeader("Neptune/" NPT_NEPTUNE_VERSION_STRING), + m_Run(true) +{ + m_Config.m_ListenAddress = NPT_IpAddress::Any; + m_Config.m_ListenPort = listen_port; + m_Config.m_IoTimeout = NPT_HTTP_SERVER_DEFAULT_IO_TIMEOUT; + m_Config.m_ConnectionTimeout = NPT_HTTP_SERVER_DEFAULT_CONNECTION_TIMEOUT; + m_Config.m_ReuseAddress = true; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::NPT_HttpServer ++---------------------------------------------------------------------*/ +NPT_HttpServer::NPT_HttpServer(NPT_IpAddress listen_address, + NPT_UInt16 listen_port, + bool cancellable) : + m_Socket(cancellable?NPT_SOCKET_FLAG_CANCELLABLE:0), + m_BoundPort(0), + m_ServerHeader("Neptune/" NPT_NEPTUNE_VERSION_STRING), + m_Run(true) +{ + m_Config.m_ListenAddress = listen_address; + m_Config.m_ListenPort = listen_port; + m_Config.m_IoTimeout = NPT_HTTP_SERVER_DEFAULT_IO_TIMEOUT; + m_Config.m_ConnectionTimeout = NPT_HTTP_SERVER_DEFAULT_CONNECTION_TIMEOUT; + m_Config.m_ReuseAddress = true; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::~NPT_HttpServer ++---------------------------------------------------------------------*/ +NPT_HttpServer::~NPT_HttpServer() +{ + m_RequestHandlers.Apply(NPT_ObjectDeleter<HandlerConfig>()); +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::Bind ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::Bind() +{ + // check if we're already bound + if (m_BoundPort != 0) return NPT_SUCCESS; + + // bind + NPT_Result result = m_Socket.Bind( + NPT_SocketAddress(m_Config.m_ListenAddress, m_Config.m_ListenPort), + m_Config.m_ReuseAddress); + if (NPT_FAILED(result)) return result; + + // update the bound port info + NPT_SocketInfo info; + m_Socket.GetInfo(info); + m_BoundPort = info.local_address.GetPort(); + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::SetConfig ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::SetConfig(const Config& config) +{ + m_Config = config; + + // check that we can bind to this listen port + return Bind(); +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::SetListenPort ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::SetListenPort(NPT_UInt16 port, bool reuse_address) +{ + m_Config.m_ListenPort = port; + m_Config.m_ReuseAddress = reuse_address; + return Bind(); +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::SetTimeouts ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::SetTimeouts(NPT_Timeout connection_timeout, + NPT_Timeout io_timeout) +{ + m_Config.m_ConnectionTimeout = connection_timeout; + m_Config.m_IoTimeout = io_timeout; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::SetServerHeader ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::SetServerHeader(const char* server_header) +{ + m_ServerHeader = server_header; + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::Abort ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::Abort() +{ + m_Socket.Cancel(); + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::WaitForNewClient ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::WaitForNewClient(NPT_InputStreamReference& input, + NPT_OutputStreamReference& output, + NPT_HttpRequestContext* context, + NPT_Flags socket_flags) +{ + // ensure that we're bound + NPT_CHECK_FINE(Bind()); + + // wait for a connection + NPT_Socket* client; + NPT_LOG_FINE_2("waiting for new connection on %s:%d...", + (const char*)m_Config.m_ListenAddress.ToString(), + m_BoundPort); + NPT_Result result = m_Socket.WaitForNewClient(client, m_Config.m_ConnectionTimeout, socket_flags); + if (result != NPT_ERROR_TIMEOUT) { + NPT_CHECK_WARNING(result); + } else { + NPT_CHECK_FINE(result); + } + if (client == NULL) return NPT_ERROR_INTERNAL; + + // get the client info + if (context) { + NPT_SocketInfo client_info; + client->GetInfo(client_info); + + context->SetLocalAddress(client_info.local_address); + context->SetRemoteAddress(client_info.remote_address); + + NPT_LOG_FINE_2("client connected (%s <- %s)", + client_info.local_address.ToString().GetChars(), + client_info.remote_address.ToString().GetChars()); + } + + // configure the socket + client->SetReadTimeout(m_Config.m_IoTimeout); + client->SetWriteTimeout(m_Config.m_IoTimeout); + + // get the streams + client->GetInputStream(input); + client->GetOutputStream(output); + + // we don't need the socket anymore + delete client; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::Loop ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::Loop(bool cancellable_sockets) +{ + NPT_InputStreamReference input; + NPT_OutputStreamReference output; + NPT_HttpRequestContext context; + NPT_Result result; + + do { + // wait for a client to connect + NPT_Flags flags = cancellable_sockets?NPT_SOCKET_FLAG_CANCELLABLE:0; + result = WaitForNewClient(input, output, &context, flags); + NPT_LOG_FINE_2("WaitForNewClient returned %d (%s)", + result, + NPT_ResultText(result)); + if (!m_Run) break; + if (result == NPT_ERROR_TIMEOUT) continue; + + // respond to the client + if (NPT_SUCCEEDED(result)) { + // send a response + result = RespondToClient(input, output, context); + NPT_LOG_FINE_2("ResponToClient returned %d (%s)", + result, + NPT_ResultText(result)); + } else { + NPT_LOG_FINE_2("WaitForNewClient returned %d (%s)", + result, + NPT_ResultText(result)); + // if there was an error, wait a short time to avoid spinning + if (result != NPT_ERROR_TERMINATED) { + NPT_LOG_FINE("sleeping before restarting the loop"); + NPT_System::Sleep(1.0); + } + } + + // release the stream references so that the socket can be closed + input = NULL; + output = NULL; + } while (m_Run && result != NPT_ERROR_TERMINATED); + + return result; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::HandlerConfig::HandlerConfig ++---------------------------------------------------------------------*/ +NPT_HttpServer::HandlerConfig::HandlerConfig(NPT_HttpRequestHandler* handler, + const char* path, + bool include_children, + bool transfer_ownership) : + m_Handler(handler), + m_Path(path), + m_IncludeChildren(include_children), + m_HandlerIsOwned(transfer_ownership) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::HandlerConfig::~HandlerConfig ++---------------------------------------------------------------------*/ +NPT_HttpServer::HandlerConfig::~HandlerConfig() +{ + if (m_HandlerIsOwned) delete m_Handler; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::AddRequestHandler ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::AddRequestHandler(NPT_HttpRequestHandler* handler, + const char* path, + bool include_children, + bool transfer_ownership) +{ + return m_RequestHandlers.Add(new HandlerConfig(handler, path, include_children, transfer_ownership)); +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::FindRequestHandler ++---------------------------------------------------------------------*/ +NPT_HttpRequestHandler* +NPT_HttpServer::FindRequestHandler(NPT_HttpRequest& request) +{ + NPT_String path = NPT_Uri::PercentDecode(request.GetUrl().GetPath()); + for (NPT_List<HandlerConfig*>::Iterator it = m_RequestHandlers.GetFirstItem(); + it; + ++it) { + HandlerConfig* config = *it; + if (config->m_IncludeChildren) { + if (path.StartsWith(config->m_Path)) { + return config->m_Handler; + } + } else { + if (path == config->m_Path) { + return config->m_Handler; + } + } + } + + // not found + return NULL; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::FindRequestHandlers ++---------------------------------------------------------------------*/ +NPT_List<NPT_HttpRequestHandler*> +NPT_HttpServer::FindRequestHandlers(NPT_HttpRequest& request) +{ + NPT_List<NPT_HttpRequestHandler*> handlers; + + for (NPT_List<HandlerConfig*>::Iterator it = m_RequestHandlers.GetFirstItem(); + it; + ++it) { + HandlerConfig* config = *it; + if (config->m_IncludeChildren) { + if (request.GetUrl().GetPath(true).StartsWith(config->m_Path)) { + handlers.Add(config->m_Handler); + } + } else { + if (request.GetUrl().GetPath(true) == config->m_Path) { + handlers.Insert(handlers.GetFirstItem(), config->m_Handler); + } + } + } + + return handlers; +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::RespondToClient ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpServer::RespondToClient(NPT_InputStreamReference& input, + NPT_OutputStreamReference& output, + const NPT_HttpRequestContext& context) +{ + NPT_HttpRequest* request; + NPT_HttpResponse* response = NULL; + NPT_Result result = NPT_ERROR_NO_SUCH_ITEM; + bool terminate_server = false; + + NPT_HttpResponder responder(input, output); + NPT_CHECK_WARNING(responder.ParseRequest(request, &context.GetLocalAddress())); + NPT_LOG_FINE_1("request, path=%s", request->GetUrl().ToRequestString(true).GetChars()); + + // prepare the response body + NPT_HttpEntity* body = new NPT_HttpEntity(); + + NPT_HttpRequestHandler* handler = FindRequestHandler(*request); + if (handler) { + // create a response object + response = new NPT_HttpResponse(200, "OK", NPT_HTTP_PROTOCOL_1_0); + response->SetEntity(body); + + // ask the handler to setup the response + result = handler->SetupResponse(*request, context, *response); + } + if (result == NPT_ERROR_NO_SUCH_ITEM || handler == NULL) { + body->SetInputStream(NPT_HTTP_DEFAULT_404_HTML); + body->SetContentType("text/html"); + if (response == NULL) { + response = new NPT_HttpResponse(404, "Not Found", NPT_HTTP_PROTOCOL_1_0); + } else { + response->SetStatus(404, "Not Found"); + } + response->SetEntity(body); + if (handler) { + handler->Completed(NPT_ERROR_NO_SUCH_ITEM); + handler = NULL; + } + } else if (result == NPT_ERROR_PERMISSION_DENIED) { + body->SetInputStream(NPT_HTTP_DEFAULT_403_HTML); + body->SetContentType("text/html"); + response->SetStatus(403, "Forbidden"); + handler->Completed(NPT_ERROR_PERMISSION_DENIED); + handler = NULL; + } else if (result == NPT_ERROR_TERMINATED) { + // mark that we want to exit + terminate_server = true; + } else if (NPT_FAILED(result)) { + body->SetInputStream(NPT_HTTP_DEFAULT_500_HTML); + body->SetContentType("text/html"); + response->SetStatus(500, "Internal Error"); + handler->Completed(result); + handler = NULL; + } + + // augment the headers with server information + if (m_ServerHeader.GetLength()) { + response->GetHeaders().SetHeader(NPT_HTTP_HEADER_SERVER, m_ServerHeader, false); + } + + // send the response headers + result = responder.SendResponseHeaders(*response); + if (NPT_FAILED(result)) { + NPT_LOG_WARNING_2("SendResponseHeaders failed (%d:%s)", result, NPT_ResultText(result)); + goto end; + } + + // send the body + if (request->GetMethod() != NPT_HTTP_METHOD_HEAD) { + if (handler) { + result = handler->SendResponseBody(context, *response, *output); + } else { + // send body manually in case there was an error with the handler or no handler was found + NPT_InputStreamReference body_stream; + body->GetInputStream(body_stream); + if (!body_stream.IsNull()) { + result = NPT_StreamToStreamCopy(*body_stream, *output, 0, body->GetContentLength()); + if (NPT_FAILED(result)) { + NPT_LOG_INFO_2("NPT_StreamToStreamCopy returned %d (%s)", result, NPT_ResultText(result)); + goto end; + } + } + } + } + + // flush + output->Flush(); + + // if we need to die, we return an error code + if (NPT_SUCCEEDED(result) && terminate_server) result = NPT_ERROR_TERMINATED; + +end: + // cleanup + delete response; + delete request; + + if (handler) { + handler->Completed(result); + } + + return result; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponder::NPT_HttpResponder ++---------------------------------------------------------------------*/ +NPT_HttpResponder::NPT_HttpResponder(NPT_InputStreamReference& input, + NPT_OutputStreamReference& output) : + m_Input(new NPT_BufferedInputStream(input)), + m_Output(output) +{ + m_Config.m_IoTimeout = NPT_HTTP_SERVER_DEFAULT_IO_TIMEOUT; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponder::~NPT_HttpResponder ++---------------------------------------------------------------------*/ +NPT_HttpResponder::~NPT_HttpResponder() +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpServer::Terminate ++---------------------------------------------------------------------*/ +void NPT_HttpServer::Terminate() +{ + m_Run = false; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponder::SetConfig ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpResponder::SetConfig(const Config& config) +{ + m_Config = config; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponder::SetTimeout ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpResponder::SetTimeout(NPT_Timeout io_timeout) +{ + m_Config.m_IoTimeout = io_timeout; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponder::ParseRequest ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpResponder::ParseRequest(NPT_HttpRequest*& request, + const NPT_SocketAddress* local_address) +{ + // rebuffer the stream in case we're using a keep-alive connection + m_Input->SetBufferSize(NPT_BUFFERED_BYTE_STREAM_DEFAULT_SIZE); + + // parse the request + NPT_CHECK_FINE(NPT_HttpRequest::Parse(*m_Input, local_address, request)); + + // unbuffer the stream + m_Input->SetBufferSize(0); + + // don't create an entity if no body is expected + if (request->GetMethod() == NPT_HTTP_METHOD_GET || + request->GetMethod() == NPT_HTTP_METHOD_HEAD || + request->GetMethod() == NPT_HTTP_METHOD_TRACE) { + return NPT_SUCCESS; + } + + // set the entity info + NPT_HttpEntity* entity = new NPT_HttpEntity(request->GetHeaders()); + if (entity->GetTransferEncoding() == NPT_HTTP_TRANSFER_ENCODING_CHUNKED) { + entity->SetInputStream(NPT_InputStreamReference(new NPT_HttpChunkedInputStream(m_Input))); + } else { + entity->SetInputStream(m_Input); + } + request->SetEntity(entity); + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpResponder::SendResponseHeaders ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpResponder::SendResponseHeaders(NPT_HttpResponse& response) +{ + // add default headers + NPT_HttpHeaders& headers = response.GetHeaders(); + if (response.GetProtocol() == NPT_HTTP_PROTOCOL_1_0) { + headers.SetHeader(NPT_HTTP_HEADER_CONNECTION, + "close", false); // set but don't replace + } + + // add computed headers + NPT_HttpEntity* entity = response.GetEntity(); + if (entity) { + // content type + const NPT_String& content_type = entity->GetContentType(); + if (!content_type.IsEmpty()) { + headers.SetHeader(NPT_HTTP_HEADER_CONTENT_TYPE, content_type); + } + + // content encoding + const NPT_String& content_encoding = entity->GetContentEncoding(); + if (!content_encoding.IsEmpty()) { + headers.SetHeader(NPT_HTTP_HEADER_CONTENT_ENCODING, content_encoding); + } + + // transfer encoding + const NPT_String& transfer_encoding = entity->GetTransferEncoding(); + if (!transfer_encoding.IsEmpty()) { + headers.SetHeader(NPT_HTTP_HEADER_TRANSFER_ENCODING, transfer_encoding); + } + + // set the content length if known + if (entity->ContentLengthIsKnown()) { + headers.SetHeader(NPT_HTTP_HEADER_CONTENT_LENGTH, + NPT_String::FromInteger(entity->GetContentLength())); + } else if (transfer_encoding.IsEmpty() || transfer_encoding.Compare(NPT_HTTP_TRANSFER_ENCODING_CHUNKED, true)) { + // no content length, the only way client will know we're done + // is when we'll close the connection unless it's chunked encoding + headers.SetHeader(NPT_HTTP_HEADER_CONNECTION, + "close", true); // set and replace + } + } else { + // force content length to 0 if there is no message body + // (necessary for 1.1 or 1.0 with keep-alive connections) + headers.SetHeader(NPT_HTTP_HEADER_CONTENT_LENGTH, "0"); + } + + // create a memory stream to buffer the response line and headers + NPT_MemoryStream buffer; + + // emit the response line + NPT_CHECK_WARNING(response.Emit(buffer)); + + // send the buffer + NPT_CHECK_WARNING(m_Output->WriteFully(buffer.GetData(), buffer.GetDataSize())); + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpRequestHandler Dynamic Cast Anchor ++---------------------------------------------------------------------*/ +NPT_DEFINE_DYNAMIC_CAST_ANCHOR(NPT_HttpRequestHandler) + +/*---------------------------------------------------------------------- +| NPT_HttpRequestHandler::SendResponseBody ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpRequestHandler::SendResponseBody(const NPT_HttpRequestContext& /*context*/, + NPT_HttpResponse& response, + NPT_OutputStream& output) +{ + NPT_HttpEntity* entity = response.GetEntity(); + if (entity == NULL) return NPT_SUCCESS; + + NPT_InputStreamReference body_stream; + entity->GetInputStream(body_stream); + if (body_stream.IsNull()) return NPT_SUCCESS; + + // check for chunked transfer encoding + NPT_OutputStream* dest = &output; + if (entity->GetTransferEncoding() == NPT_HTTP_TRANSFER_ENCODING_CHUNKED) { + dest = new NPT_HttpChunkedOutputStream(output); + } + + // send the body + NPT_LOG_FINE_1("sending body stream, %lld bytes", entity->GetContentLength()); + NPT_LargeSize bytes_written = 0; + NPT_Result result = NPT_StreamToStreamCopy(*body_stream, *dest, 0, entity->GetContentLength(), &bytes_written); + if (NPT_FAILED(result)) { + NPT_LOG_FINE_3("body stream only partially sent, %lld bytes (%d:%s)", + bytes_written, + result, + NPT_ResultText(result)); + } + + // flush to write out any buffered data left in chunked output if used + dest->Flush(); + + // cleanup (this will send zero size chunk followed by CRLF) + if (dest != &output) delete dest; + + return result; +} + +/*---------------------------------------------------------------------- +| NPT_HttpStaticRequestHandler::NPT_HttpStaticRequestHandler ++---------------------------------------------------------------------*/ +NPT_HttpStaticRequestHandler::NPT_HttpStaticRequestHandler(const void* data, + NPT_Size size, + const char* mime_type, + bool copy) : + m_MimeType(mime_type), + m_Buffer(data, size, copy) +{} + +/*---------------------------------------------------------------------- +| NPT_HttpStaticRequestHandler::NPT_HttpStaticRequestHandler ++---------------------------------------------------------------------*/ +NPT_HttpStaticRequestHandler::NPT_HttpStaticRequestHandler(const char* document, + const char* mime_type, + bool copy) : + m_MimeType(mime_type), + m_Buffer(document, NPT_StringLength(document), copy) +{} + +/*---------------------------------------------------------------------- +| NPT_HttpStaticRequestHandler::SetupResponse ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpStaticRequestHandler::SetupResponse(NPT_HttpRequest& /*request*/, + const NPT_HttpRequestContext& /*context*/, + NPT_HttpResponse& response) +{ + NPT_HttpEntity* entity = response.GetEntity(); + if (entity == NULL) return NPT_ERROR_INVALID_STATE; + + entity->SetContentType(m_MimeType); + entity->SetInputStream(m_Buffer.GetData(), m_Buffer.GetDataSize()); + + return NPT_SUCCESS; +} + +const NPT_HttpFileRequestHandler_DefaultFileTypeMapEntry +NPT_HttpFileRequestHandler_DefaultFileTypeMap[] = { + {"xml", "text/xml; charset=\"utf-8\"" }, + {"htm", "text/html" }, + {"html", "text/html" }, + {"c", "text/plain"}, + {"h", "text/plain"}, + {"txt", "text/plain"}, + {"css", "text/css" }, + {"manifest", "text/cache-manifest"}, + {"gif", "image/gif" }, + {"thm", "image/jpeg"}, + {"png", "image/png"}, + {"tif", "image/tiff"}, + {"tiff", "image/tiff"}, + {"jpg", "image/jpeg"}, + {"jpeg", "image/jpeg"}, + {"jpe", "image/jpeg"}, + {"jp2", "image/jp2" }, + {"png", "image/png" }, + {"bmp", "image/bmp" }, + {"aif", "audio/x-aiff"}, + {"aifc", "audio/x-aiff"}, + {"aiff", "audio/x-aiff"}, + {"flac", "audio/x-flac"}, + {"mka", "audio/x-matroska"}, + {"mpa", "audio/mpeg"}, + {"mp2", "audio/mpeg"}, + {"mp3", "audio/mpeg"}, + {"m4a", "audio/mp4"}, + {"wma", "audio/x-ms-wma"}, + {"wav", "audio/x-wav"}, + {"mkv", "video/x-matroska"}, + {"mpeg", "video/mpeg"}, + {"mpg", "video/mpeg"}, + {"mp4", "video/mp4"}, + {"m4v", "video/mp4"}, + {"ts", "video/MP2T"}, // RFC 3555 + {"mpegts", "video/MP2T"}, + {"mov", "video/quicktime"}, + {"qt", "video/quicktime"}, + {"wmv", "video/x-ms-wmv"}, + {"wtv", "video/x-ms-wmv"}, + {"asf", "video/x-ms-asf"}, + {"mkv", "video/x-matroska"}, + {"mk3d", "video/x-matroska-3d"}, + {"flv", "video/x-flv"}, + {"avi", "video/x-msvideo"}, + {"divx", "video/x-msvideo"}, + {"xvid", "video/x-msvideo"}, + {"doc", "application/msword"}, + {"js", "application/javascript"}, + {"m3u8", "application/x-mpegURL"}, + {"pdf", "application/pdf"}, + {"ps", "application/postscript"}, + {"eps", "application/postscript"}, + {"zip", "application/zip"} +}; + +/*---------------------------------------------------------------------- +| NPT_HttpFileRequestHandler::NPT_HttpFileRequestHandler ++---------------------------------------------------------------------*/ +NPT_HttpFileRequestHandler::NPT_HttpFileRequestHandler(const char* url_root, + const char* file_root, + bool auto_dir, + const char* auto_index) : + m_UrlRoot(url_root), + m_FileRoot(file_root), + m_DefaultMimeType("text/html"), + m_UseDefaultFileTypeMap(true), + m_AutoDir(auto_dir), + m_AutoIndex(auto_index) +{ +} + +/*---------------------------------------------------------------------- +| helper functions FIXME: need to move these to a separate module ++---------------------------------------------------------------------*/ +static NPT_UInt32 +_utf8_decode(const char** str) +{ + NPT_UInt32 result; + NPT_UInt32 min_value; + unsigned int bytes_left; + + if (**str == 0) { + return ~0; + } else if ((**str & 0x80) == 0x00) { + result = *(*str)++; + bytes_left = 0; + min_value = 0; + } else if ((**str & 0xE0) == 0xC0) { + result = *(*str)++ & 0x1F; + bytes_left = 1; + min_value = 0x80; + } else if ((**str & 0xF0) == 0xE0) { + result = *(*str)++ & 0x0F; + bytes_left = 2; + min_value = 0x800; + } else if ((**str & 0xF8) == 0xF0) { + result = *(*str)++ & 0x07; + bytes_left = 3; + min_value = 0x10000; + } else { + return ~0; + } + + while (bytes_left--) { + if (**str == 0 || (**str & 0xC0) != 0x80) return ~0; + result = (result << 6) | (*(*str)++ & 0x3F); + } + + if (result < min_value || (result & 0xFFFFF800) == 0xD800 || result > 0x10FFFF) { + return ~0; + } + + return result; +} + +/*---------------------------------------------------------------------- +| NPT_HtmlEncode ++---------------------------------------------------------------------*/ +static NPT_String +NPT_HtmlEncode(const char* str, const char* chars) +{ + NPT_String encoded; + + // check args + if (str == NULL) return encoded; + + // reserve at least the size of the current uri + encoded.Reserve(NPT_StringLength(str)); + + // process each character + while (*str) { + NPT_UInt32 c = _utf8_decode(&str); + bool encode = false; + if (c < ' ' || c > '~') { + encode = true; + } else { + const char* match = chars; + while (*match) { + if (c == (NPT_UInt32)*match) { + encode = true; + break; + } + ++match; + } + } + if (encode) { + // encode + char hex[9]; + encoded += "&#x"; + unsigned int len = 0; + if (c > 0xFFFF) { + NPT_ByteToHex((unsigned char)(c>>24), &hex[0], true); + NPT_ByteToHex((unsigned char)(c>>16), &hex[2], true); + len = 4; + } + NPT_ByteToHex((unsigned char)(c>>8), &hex[len ], true); + NPT_ByteToHex((unsigned char)(c ), &hex[len+2], true); + hex[len+4] = ';'; + encoded.Append(hex, len+5); + } else { + // no encoding required + encoded += (char)c; + } + } + + return encoded; +} + +/*---------------------------------------------------------------------- +| NPT_HttpFileRequestHandler::SetupResponse ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpFileRequestHandler::SetupResponse(NPT_HttpRequest& request, + const NPT_HttpRequestContext& /* context */, + NPT_HttpResponse& response) +{ + NPT_HttpEntity* entity = response.GetEntity(); + if (entity == NULL) return NPT_ERROR_INVALID_STATE; + + // check the method + if (request.GetMethod() != NPT_HTTP_METHOD_GET && + request.GetMethod() != NPT_HTTP_METHOD_HEAD) { + response.SetStatus(405, "Method Not Allowed"); + return NPT_SUCCESS; + } + + // set some default headers + response.GetHeaders().SetHeader(NPT_HTTP_HEADER_ACCEPT_RANGES, "bytes"); + + // declare HTTP/1.1 if the client asked for it + if (request.GetProtocol() == NPT_HTTP_PROTOCOL_1_1) { + response.SetProtocol(NPT_HTTP_PROTOCOL_1_1); + } + + // TODO: we need to normalize the request path + + // check that the request's path is an entry under the url root + if (!request.GetUrl().GetPath(true).StartsWith(m_UrlRoot)) { + return NPT_ERROR_INVALID_PARAMETERS; + } + + // compute the filename + NPT_String filename = m_FileRoot; + NPT_String relative_path = NPT_Url::PercentDecode(request.GetUrl().GetPath().GetChars()+m_UrlRoot.GetLength()); + filename += "/"; + filename += relative_path; + NPT_LOG_FINE_1("filename = %s", filename.GetChars()); + + // get info about the file + NPT_FileInfo info; + NPT_File::GetInfo(filename, &info); + + // check if this is a directory + if (info.m_Type == NPT_FileInfo::FILE_TYPE_DIRECTORY) { + NPT_LOG_FINE("file is a DIRECTORY"); + if (m_AutoDir) { + if (m_AutoIndex.GetLength()) { + NPT_LOG_FINE("redirecting to auto-index"); + filename += NPT_FilePath::Separator; + filename += m_AutoIndex; + if (NPT_File::Exists(filename)) { + NPT_String location = m_UrlRoot+"/"+m_AutoIndex; + response.SetStatus(302, "Found"); + response.GetHeaders().SetHeader(NPT_HTTP_HEADER_LOCATION, location); + } else { + return NPT_ERROR_PERMISSION_DENIED; + } + } else { + NPT_LOG_FINE("doing auto-dir"); + + // get the dir entries + NPT_List<NPT_String> entries; + NPT_File::ListDir(filename, entries); + + NPT_String html; + html.Reserve(1024+128*entries.GetItemCount()); + + NPT_String html_dirname = NPT_HtmlEncode(relative_path, "<>&"); + html += "<hmtl><head><title>Directory Listing for /"; + html += html_dirname; + html += "</title></head><body>"; + html += "<h2>Directory Listing for /"; + html += html_dirname; + html += "</h2><hr><ul>\r\n"; + NPT_String url_base_path = NPT_HtmlEncode(request.GetUrl().GetPath(), "<>&\""); + + for (NPT_List<NPT_String>::Iterator i = entries.GetFirstItem(); + i; + ++i) { + NPT_String url_filename = NPT_HtmlEncode(*i, "<>&"); + html += "<li><a href=\""; + html += url_base_path; + if (!url_base_path.EndsWith("/")) html += "/"; + html += url_filename; + html += "\">"; + html +=url_filename; + + NPT_String full_path = filename; + full_path += "/"; + full_path += *i; + NPT_File::GetInfo(full_path, &info); + if (info.m_Type == NPT_FileInfo::FILE_TYPE_DIRECTORY) html += "/"; + + html += "</a><br>\r\n"; + } + html += "</ul></body></html>"; + + entity->SetContentType("text/html"); + entity->SetInputStream(html); + return NPT_SUCCESS; + } + } else { + return NPT_ERROR_PERMISSION_DENIED; + } + } + + // open the file + NPT_File file(filename); + NPT_Result result = file.Open(NPT_FILE_OPEN_MODE_READ); + if (NPT_FAILED(result)) { + NPT_LOG_FINE("file not found"); + return NPT_ERROR_NO_SUCH_ITEM; + } + NPT_InputStreamReference stream; + file.GetInputStream(stream); + + // check for range requests + const NPT_String* range_spec = request.GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_RANGE); + + // setup entity body + NPT_CHECK(SetupResponseBody(response, stream, range_spec)); + + // set the response body + entity->SetContentType(GetContentType(filename)); + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpFileRequestHandler::SetupResponseBody ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpFileRequestHandler::SetupResponseBody(NPT_HttpResponse& response, + NPT_InputStreamReference& stream, + const NPT_String* range_spec /* = NULL */) +{ + NPT_HttpEntity* entity = response.GetEntity(); + if (entity == NULL) return NPT_ERROR_INVALID_STATE; + + if (range_spec) { + const NPT_String* accept_range = response.GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_ACCEPT_RANGES); + + if (response.GetEntity()->GetTransferEncoding() == NPT_HTTP_TRANSFER_ENCODING_CHUNKED || + (accept_range && accept_range->Compare("bytes"))) { + NPT_LOG_FINE("range request not supported"); + response.SetStatus(416, "Requested Range Not Satisfiable"); + return NPT_SUCCESS; + } + + // measure the stream size + bool has_stream_size = false; + NPT_LargeSize stream_size = 0; + NPT_Result result = stream->GetSize(stream_size); + if (NPT_SUCCEEDED(result)) { + has_stream_size = true; + NPT_LOG_FINE_1("body size=%lld", stream_size); + if (stream_size == 0) return NPT_SUCCESS; + } + + if (!range_spec->StartsWith("bytes=")) { + NPT_LOG_FINE("unknown range spec"); + response.SetStatus(400, "Bad Request"); + return NPT_SUCCESS; + } + NPT_String valid_range; + NPT_String range(range_spec->GetChars()+6); + if (range.Find(',') >= 0) { + NPT_LOG_FINE("multi-range requests not supported"); + if (has_stream_size) { + valid_range = "bytes */"; + valid_range += NPT_String::FromInteger(stream_size); + response.GetHeaders().SetHeader(NPT_HTTP_HEADER_CONTENT_RANGE, valid_range.GetChars()); + } + response.SetStatus(416, "Requested Range Not Satisfiable"); + return NPT_SUCCESS; + } + int sep = range.Find('-'); + NPT_UInt64 range_start = 0; + NPT_UInt64 range_end = 0; + bool has_start = false; + bool has_end = false; + bool satisfied = false; + if (sep < 0) { + NPT_LOG_FINE("invalid syntax"); + response.SetStatus(400, "Bad Request"); + return NPT_SUCCESS; + } else { + if ((unsigned int)sep+1 < range.GetLength()) { + result = NPT_ParseInteger64(range.GetChars()+sep+1, range_end); + if (NPT_FAILED(result)) { + NPT_LOG_FINE("failed to parse range end"); + return result; + } + range.SetLength(sep); + has_end = true; + } + if (sep > 0) { + result = range.ToInteger64(range_start); + if (NPT_FAILED(result)) { + NPT_LOG_FINE("failed to parse range start"); + return result; + } + has_start = true; + } + + if (!has_stream_size) { + if (has_start && range_start == 0 && !has_end) { + bool update_content_length = (entity->GetTransferEncoding() != NPT_HTTP_TRANSFER_ENCODING_CHUNKED); + // use the whole file stream as a body + return entity->SetInputStream(stream, update_content_length); + } else { + NPT_LOG_WARNING_2("file.GetSize() failed (%d:%s)", result, NPT_ResultText(result)); + NPT_LOG_FINE("range request not supported"); + response.SetStatus(416, "Requested Range Not Satisfiable"); + return NPT_SUCCESS; + } + } + + if (has_start) { + // some clients sends incorrect range_end equal to size + // we try to handle it + if (!has_end || range_end == stream_size) range_end = stream_size-1; + } else { + if (has_end) { + if (range_end <= stream_size) { + range_start = stream_size-range_end; + range_end = stream_size-1; + } + } + } + NPT_LOG_FINE_2("final range: start=%lld, end=%lld", range_start, range_end); + if (range_start > range_end) { + NPT_LOG_FINE("invalid range"); + response.SetStatus(400, "Bad Request"); + satisfied = false; + } else if (range_end >= stream_size) { + response.SetStatus(416, "Requested Range Not Satisfiable"); + NPT_LOG_FINE("out of range"); + satisfied = false; + } else { + satisfied = true; + } + } + if (satisfied && range_start != 0) { + // seek in the stream + result = stream->Seek(range_start); + if (NPT_FAILED(result)) { + NPT_LOG_WARNING_2("stream.Seek() failed (%d:%s)", result, NPT_ResultText(result)); + satisfied = false; + } + } + if (!satisfied) { + if (!valid_range.IsEmpty()) response.GetHeaders().SetHeader(NPT_HTTP_HEADER_CONTENT_RANGE, valid_range.GetChars()); + response.SetStatus(416, "Requested Range Not Satisfiable"); + return NPT_SUCCESS; + } + + // use a portion of the file stream as a body + entity->SetInputStream(stream, false); + entity->SetContentLength(range_end-range_start+1); + response.SetStatus(206, "Partial Content"); + valid_range = "bytes "; + valid_range += NPT_String::FromInteger(range_start); + valid_range += "-"; + valid_range += NPT_String::FromInteger(range_end); + valid_range += "/"; + valid_range += NPT_String::FromInteger(stream_size); + response.GetHeaders().SetHeader(NPT_HTTP_HEADER_CONTENT_RANGE, valid_range.GetChars()); + } else { + bool update_content_length = (entity->GetTransferEncoding() != NPT_HTTP_TRANSFER_ENCODING_CHUNKED); + // use the whole file stream as a body + entity->SetInputStream(stream, update_content_length); + } + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpFileRequestHandler::GetContentType ++---------------------------------------------------------------------*/ +const char* +NPT_HttpFileRequestHandler::GetDefaultContentType(const char* extension) +{ + for (unsigned int i=0; i<NPT_ARRAY_SIZE(NPT_HttpFileRequestHandler_DefaultFileTypeMap); i++) { + if (NPT_String::Compare(extension, NPT_HttpFileRequestHandler_DefaultFileTypeMap[i].extension, true) == 0) { + const char* type = NPT_HttpFileRequestHandler_DefaultFileTypeMap[i].mime_type; + NPT_LOG_FINE_1("using type from default list: %s", type); + return type; + } + } + + return NULL; +} + +/*---------------------------------------------------------------------- +| NPT_HttpFileRequestHandler::GetContentType ++---------------------------------------------------------------------*/ +const char* +NPT_HttpFileRequestHandler::GetContentType(const NPT_String& filename) +{ + int last_dot = filename.ReverseFind('.'); + if (last_dot > 0) { + NPT_String extension = filename.GetChars()+last_dot+1; + extension.MakeLowercase(); + + NPT_LOG_FINE_1("extension=%s", extension.GetChars()); + + NPT_String* mime_type; + if (NPT_SUCCEEDED(m_FileTypeMap.Get(extension, mime_type))) { + NPT_LOG_FINE_1("found mime type in map: %s", mime_type->GetChars()); + return mime_type->GetChars(); + } + + // not found, look in the default map if necessary + if (m_UseDefaultFileTypeMap) { + const char* type = NPT_HttpFileRequestHandler::GetDefaultContentType(extension); + if (type) return type; + } + } + + NPT_LOG_FINE("using default mime type"); + return m_DefaultMimeType; +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedInputStream::NPT_HttpChunkedInputStream ++---------------------------------------------------------------------*/ +NPT_HttpChunkedInputStream::NPT_HttpChunkedInputStream( + NPT_BufferedInputStreamReference& stream) : + m_Source(stream), + m_CurrentChunkSize(0), + m_Eos(false) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedInputStream::~NPT_HttpChunkedInputStream ++---------------------------------------------------------------------*/ +NPT_HttpChunkedInputStream::~NPT_HttpChunkedInputStream() +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedInputStream::NPT_HttpChunkedInputStream ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpChunkedInputStream::Read(void* buffer, + NPT_Size bytes_to_read, + NPT_Size* bytes_read /* = NULL */) +{ + // set the initial state of return values + if (bytes_read) *bytes_read = 0; + + // check for end of stream + if (m_Eos) return NPT_ERROR_EOS; + + // shortcut + if (bytes_to_read == 0) return NPT_SUCCESS; + + // read next chunk size if needed + if (m_CurrentChunkSize == 0) { + // buffered mode + m_Source->SetBufferSize(4096); + + NPT_String size_line; + NPT_CHECK_FINE(m_Source->ReadLine(size_line)); + + // decode size (in hex) + m_CurrentChunkSize = 0; + if (size_line.GetLength() < 1) { + NPT_LOG_WARNING("empty chunk size line"); + return NPT_ERROR_INVALID_FORMAT; + } + const char* size_hex = size_line.GetChars(); + while (*size_hex != '\0' && + *size_hex != ' ' && + *size_hex != ';' && + *size_hex != '\r' && + *size_hex != '\n') { + int nibble = NPT_HexToNibble(*size_hex); + if (nibble < 0) { + NPT_LOG_WARNING_1("invalid chunk size format (%s)", size_line.GetChars()); + return NPT_ERROR_INVALID_FORMAT; + } + m_CurrentChunkSize = (m_CurrentChunkSize<<4)|nibble; + ++size_hex; + } + NPT_LOG_FINEST_1("start of chunk, size=%d", m_CurrentChunkSize); + + // 0 = end of body + if (m_CurrentChunkSize == 0) { + NPT_LOG_FINEST("end of chunked stream, reading trailers"); + + // read footers until empty line + NPT_String footer; + do { + NPT_CHECK_FINE(m_Source->ReadLine(footer)); + } while (!footer.IsEmpty()); + m_Eos = true; + + NPT_LOG_FINEST("end of chunked stream, done"); + return NPT_ERROR_EOS; + } + + // unbuffer source + m_Source->SetBufferSize(0); + } + + // read no more than what's left in chunk + NPT_Size chunk_bytes_read; + if (bytes_to_read > m_CurrentChunkSize) bytes_to_read = m_CurrentChunkSize; + NPT_CHECK_FINE(m_Source->Read(buffer, bytes_to_read, &chunk_bytes_read)); + + // ready to go to next chunk? + m_CurrentChunkSize -= chunk_bytes_read; + if (m_CurrentChunkSize == 0) { + NPT_LOG_FINEST("reading end of chunk"); + + // when a chunk is finished, a \r\n follows + char newline[2]; + NPT_CHECK_FINE(m_Source->ReadFully(newline, 2)); + if (newline[0] != '\r' || newline[1] != '\n') { + NPT_LOG_WARNING("invalid end of chunk (expected \\r\\n)"); + return NPT_ERROR_INVALID_FORMAT; + } + } + + // update output params + if (bytes_read) *bytes_read = chunk_bytes_read; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedInputStream::Seek ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpChunkedInputStream::Seek(NPT_Position /*offset*/) +{ + return NPT_ERROR_NOT_SUPPORTED; +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedInputStream::Tell ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpChunkedInputStream::Tell(NPT_Position& offset) +{ + offset = 0; + return NPT_ERROR_NOT_SUPPORTED; +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedInputStream::GetSize ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpChunkedInputStream::GetSize(NPT_LargeSize& size) +{ + return m_Source->GetSize(size); +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedInputStream::GetAvailable ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpChunkedInputStream::GetAvailable(NPT_LargeSize& available) +{ + return m_Source->GetAvailable(available); +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedOutputStream::NPT_HttpChunkedOutputStream ++---------------------------------------------------------------------*/ +NPT_HttpChunkedOutputStream::NPT_HttpChunkedOutputStream(NPT_OutputStream& stream) : + m_Stream(stream) +{ +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedOutputStream::~NPT_HttpChunkedOutputStream ++---------------------------------------------------------------------*/ +NPT_HttpChunkedOutputStream::~NPT_HttpChunkedOutputStream() +{ + // zero size chunk followed by CRLF (no trailer) + m_Stream.WriteFully("0" NPT_HTTP_LINE_TERMINATOR NPT_HTTP_LINE_TERMINATOR, 5); +} + +/*---------------------------------------------------------------------- +| NPT_HttpChunkedOutputStream::Write ++---------------------------------------------------------------------*/ +NPT_Result +NPT_HttpChunkedOutputStream::Write(const void* buffer, + NPT_Size bytes_to_write, + NPT_Size* bytes_written) +{ + // default values + if (bytes_written) *bytes_written = 0; + + // shortcut + if (bytes_to_write == 0) return NPT_SUCCESS; + + // write the chunk header + char size[16]; + size[15] = '\n'; + size[14] = '\r'; + char* c = &size[14]; + unsigned int char_count = 2; + unsigned int value = bytes_to_write; + do { + unsigned int digit = (unsigned int)(value%16); + if (digit < 10) { + *--c = '0'+digit; + } else { + *--c = 'A'+digit-10; + } + char_count++; + value /= 16; + } while(value); + NPT_Result result = m_Stream.WriteFully(c, char_count); + if (NPT_FAILED(result)) return result; + + // write the chunk data + result = m_Stream.WriteFully(buffer, bytes_to_write); + if (NPT_FAILED(result)) return result; + + // finish the chunk + result = m_Stream.WriteFully(NPT_HTTP_LINE_TERMINATOR, 2); + if (NPT_SUCCEEDED(result) && bytes_written) { + *bytes_written = bytes_to_write; + } + return result; +} |