Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
haiku/src/kits/network/libnetservices/HttpRequest.cpp
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1254 lines (1008 sloc)
30 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Copyright 2010-2021 Haiku Inc. All rights reserved. | |
| * Distributed under the terms of the MIT License. | |
| * | |
| * Authors: | |
| * Christophe Huriaux, c.huriaux@gmail.com | |
| * Niels Sascha Reedijk, niels.reedijk@gmail.com | |
| * Adrien Destugues, pulkomandy@pulkomandy.tk | |
| * Stephan Aßmus, superstippi@gmx.de | |
| */ | |
| #include <HttpRequest.h> | |
| #include <arpa/inet.h> | |
| #include <stdio.h> | |
| #include <cstdlib> | |
| #include <deque> | |
| #include <new> | |
| #include <AutoDeleter.h> | |
| #include <Certificate.h> | |
| #include <Debug.h> | |
| #include <DynamicBuffer.h> | |
| #include <File.h> | |
| #include <ProxySecureSocket.h> | |
| #include <Socket.h> | |
| #include <SecureSocket.h> | |
| #include <StackOrHeapArray.h> | |
| #include <ZlibCompressionAlgorithm.h> | |
| using namespace BPrivate::Network; | |
| static const int32 kHttpBufferSize = 4096; | |
| namespace BPrivate { | |
| class CheckedSecureSocket: public BSecureSocket | |
| { | |
| public: | |
| CheckedSecureSocket(BHttpRequest* request); | |
| bool CertificateVerificationFailed(BCertificate& certificate, | |
| const char* message); | |
| private: | |
| BHttpRequest* fRequest; | |
| }; | |
| CheckedSecureSocket::CheckedSecureSocket(BHttpRequest* request) | |
| : | |
| BSecureSocket(), | |
| fRequest(request) | |
| { | |
| } | |
| bool | |
| CheckedSecureSocket::CertificateVerificationFailed(BCertificate& certificate, | |
| const char* message) | |
| { | |
| return fRequest->_CertificateVerificationFailed(certificate, message); | |
| } | |
| class CheckedProxySecureSocket: public BProxySecureSocket | |
| { | |
| public: | |
| CheckedProxySecureSocket(const BNetworkAddress& proxy, BHttpRequest* request); | |
| bool CertificateVerificationFailed(BCertificate& certificate, | |
| const char* message); | |
| private: | |
| BHttpRequest* fRequest; | |
| }; | |
| CheckedProxySecureSocket::CheckedProxySecureSocket(const BNetworkAddress& proxy, | |
| BHttpRequest* request) | |
| : | |
| BProxySecureSocket(proxy), | |
| fRequest(request) | |
| { | |
| } | |
| bool | |
| CheckedProxySecureSocket::CertificateVerificationFailed(BCertificate& certificate, | |
| const char* message) | |
| { | |
| return fRequest->_CertificateVerificationFailed(certificate, message); | |
| } | |
| }; | |
| BHttpRequest::BHttpRequest(const BUrl& url, BDataIO* output, bool ssl, | |
| const char* protocolName, BUrlProtocolListener* listener, | |
| BUrlContext* context) | |
| : | |
| BNetworkRequest(url, output, listener, context, "BUrlProtocol.HTTP", | |
| protocolName), | |
| fSSL(ssl), | |
| fRequestMethod(B_HTTP_GET), | |
| fHttpVersion(B_HTTP_11), | |
| fResult(url), | |
| fRequestStatus(kRequestInitialState), | |
| fOptHeaders(NULL), | |
| fOptPostFields(NULL), | |
| fOptInputData(NULL), | |
| fOptInputDataSize(-1), | |
| fOptRangeStart(-1), | |
| fOptRangeEnd(-1), | |
| fOptFollowLocation(true) | |
| { | |
| _ResetOptions(); | |
| fSocket = NULL; | |
| } | |
| BHttpRequest::BHttpRequest(const BHttpRequest& other) | |
| : | |
| BNetworkRequest(other.Url(), other.Output(), other.fListener, | |
| other.fContext, "BUrlProtocol.HTTP", other.fSSL ? "HTTPS" : "HTTP"), | |
| fSSL(other.fSSL), | |
| fRequestMethod(other.fRequestMethod), | |
| fHttpVersion(other.fHttpVersion), | |
| fResult(other.fUrl), | |
| fRequestStatus(kRequestInitialState), | |
| fOptHeaders(NULL), | |
| fOptPostFields(NULL), | |
| fOptInputData(NULL), | |
| fOptInputDataSize(-1), | |
| fOptRangeStart(other.fOptRangeStart), | |
| fOptRangeEnd(other.fOptRangeEnd), | |
| fOptFollowLocation(other.fOptFollowLocation) | |
| { | |
| _ResetOptions(); | |
| // FIXME some options may be copied from other instead. | |
| fSocket = NULL; | |
| } | |
| BHttpRequest::~BHttpRequest() | |
| { | |
| Stop(); | |
| delete fSocket; | |
| delete fOptInputData; | |
| delete fOptHeaders; | |
| delete fOptPostFields; | |
| } | |
| void | |
| BHttpRequest::SetMethod(const char* const method) | |
| { | |
| fRequestMethod = method; | |
| } | |
| void | |
| BHttpRequest::SetFollowLocation(bool follow) | |
| { | |
| fOptFollowLocation = follow; | |
| } | |
| void | |
| BHttpRequest::SetMaxRedirections(int8 redirections) | |
| { | |
| fOptMaxRedirs = redirections; | |
| } | |
| void | |
| BHttpRequest::SetReferrer(const BString& referrer) | |
| { | |
| fOptReferer = referrer; | |
| } | |
| void | |
| BHttpRequest::SetUserAgent(const BString& agent) | |
| { | |
| fOptUserAgent = agent; | |
| } | |
| void | |
| BHttpRequest::SetDiscardData(bool discard) | |
| { | |
| fOptDiscardData = discard; | |
| } | |
| void | |
| BHttpRequest::SetDisableListener(bool disable) | |
| { | |
| fOptDisableListener = disable; | |
| } | |
| void | |
| BHttpRequest::SetAutoReferrer(bool enable) | |
| { | |
| fOptAutoReferer = enable; | |
| } | |
| void | |
| BHttpRequest::SetStopOnError(bool stop) | |
| { | |
| fOptStopOnError = stop; | |
| } | |
| void | |
| BHttpRequest::SetUserName(const BString& name) | |
| { | |
| fOptUsername = name; | |
| } | |
| void | |
| BHttpRequest::SetPassword(const BString& password) | |
| { | |
| fOptPassword = password; | |
| } | |
| void | |
| BHttpRequest::SetRangeStart(off_t position) | |
| { | |
| // This field is used within the transfer loop, so only | |
| // allow setting it before sending the request. | |
| if (fRequestStatus == kRequestInitialState) | |
| fOptRangeStart = position; | |
| } | |
| void | |
| BHttpRequest::SetRangeEnd(off_t position) | |
| { | |
| // This field could be used in the transfer loop, so only | |
| // allow setting it before sending the request. | |
| if (fRequestStatus == kRequestInitialState) | |
| fOptRangeEnd = position; | |
| } | |
| void | |
| BHttpRequest::SetPostFields(const BHttpForm& fields) | |
| { | |
| AdoptPostFields(new(std::nothrow) BHttpForm(fields)); | |
| } | |
| void | |
| BHttpRequest::SetHeaders(const BHttpHeaders& headers) | |
| { | |
| AdoptHeaders(new(std::nothrow) BHttpHeaders(headers)); | |
| } | |
| void | |
| BHttpRequest::AdoptPostFields(BHttpForm* const fields) | |
| { | |
| delete fOptPostFields; | |
| fOptPostFields = fields; | |
| if (fOptPostFields != NULL) | |
| fRequestMethod = B_HTTP_POST; | |
| } | |
| void | |
| BHttpRequest::AdoptInputData(BDataIO* const data, const ssize_t size) | |
| { | |
| delete fOptInputData; | |
| fOptInputData = data; | |
| fOptInputDataSize = size; | |
| } | |
| void | |
| BHttpRequest::AdoptHeaders(BHttpHeaders* const headers) | |
| { | |
| delete fOptHeaders; | |
| fOptHeaders = headers; | |
| } | |
| /*static*/ bool | |
| BHttpRequest::IsInformationalStatusCode(int16 code) | |
| { | |
| return (code >= B_HTTP_STATUS__INFORMATIONAL_BASE) | |
| && (code < B_HTTP_STATUS__INFORMATIONAL_END); | |
| } | |
| /*static*/ bool | |
| BHttpRequest::IsSuccessStatusCode(int16 code) | |
| { | |
| return (code >= B_HTTP_STATUS__SUCCESS_BASE) | |
| && (code < B_HTTP_STATUS__SUCCESS_END); | |
| } | |
| /*static*/ bool | |
| BHttpRequest::IsRedirectionStatusCode(int16 code) | |
| { | |
| return (code >= B_HTTP_STATUS__REDIRECTION_BASE) | |
| && (code < B_HTTP_STATUS__REDIRECTION_END); | |
| } | |
| /*static*/ bool | |
| BHttpRequest::IsClientErrorStatusCode(int16 code) | |
| { | |
| return (code >= B_HTTP_STATUS__CLIENT_ERROR_BASE) | |
| && (code < B_HTTP_STATUS__CLIENT_ERROR_END); | |
| } | |
| /*static*/ bool | |
| BHttpRequest::IsServerErrorStatusCode(int16 code) | |
| { | |
| return (code >= B_HTTP_STATUS__SERVER_ERROR_BASE) | |
| && (code < B_HTTP_STATUS__SERVER_ERROR_END); | |
| } | |
| /*static*/ int16 | |
| BHttpRequest::StatusCodeClass(int16 code) | |
| { | |
| if (BHttpRequest::IsInformationalStatusCode(code)) | |
| return B_HTTP_STATUS_CLASS_INFORMATIONAL; | |
| else if (BHttpRequest::IsSuccessStatusCode(code)) | |
| return B_HTTP_STATUS_CLASS_SUCCESS; | |
| else if (BHttpRequest::IsRedirectionStatusCode(code)) | |
| return B_HTTP_STATUS_CLASS_REDIRECTION; | |
| else if (BHttpRequest::IsClientErrorStatusCode(code)) | |
| return B_HTTP_STATUS_CLASS_CLIENT_ERROR; | |
| else if (BHttpRequest::IsServerErrorStatusCode(code)) | |
| return B_HTTP_STATUS_CLASS_SERVER_ERROR; | |
| return B_HTTP_STATUS_CLASS_INVALID; | |
| } | |
| const BUrlResult& | |
| BHttpRequest::Result() const | |
| { | |
| return fResult; | |
| } | |
| status_t | |
| BHttpRequest::Stop() | |
| { | |
| if (fSocket != NULL) { | |
| fSocket->Disconnect(); | |
| // Unlock any pending connect, read or write operation. | |
| } | |
| return BNetworkRequest::Stop(); | |
| } | |
| void | |
| BHttpRequest::_ResetOptions() | |
| { | |
| delete fOptPostFields; | |
| delete fOptHeaders; | |
| fOptFollowLocation = true; | |
| fOptMaxRedirs = 8; | |
| fOptReferer = ""; | |
| fOptUserAgent = "Services Kit (Haiku)"; | |
| fOptUsername = ""; | |
| fOptPassword = ""; | |
| fOptAuthMethods = B_HTTP_AUTHENTICATION_BASIC | B_HTTP_AUTHENTICATION_DIGEST | |
| | B_HTTP_AUTHENTICATION_IE_DIGEST; | |
| fOptHeaders = NULL; | |
| fOptPostFields = NULL; | |
| fOptSetCookies = true; | |
| fOptDiscardData = false; | |
| fOptDisableListener = false; | |
| fOptAutoReferer = true; | |
| } | |
| status_t | |
| BHttpRequest::_ProtocolLoop() | |
| { | |
| // Initialize the request redirection loop | |
| int8 maxRedirs = fOptMaxRedirs; | |
| bool newRequest; | |
| do { | |
| newRequest = false; | |
| // Result reset | |
| fHeaders.Clear(); | |
| _ResultHeaders().Clear(); | |
| BString host = fUrl.Host(); | |
| int port = fSSL ? 443 : 80; | |
| if (fUrl.HasPort()) | |
| port = fUrl.Port(); | |
| if (fContext->UseProxy()) { | |
| host = fContext->GetProxyHost(); | |
| port = fContext->GetProxyPort(); | |
| } | |
| status_t result = fInputBuffer.InitCheck(); | |
| if (result != B_OK) | |
| return result; | |
| if (!_ResolveHostName(host, port)) { | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, | |
| "Unable to resolve hostname (%s), aborting.", | |
| fUrl.Host().String()); | |
| return B_SERVER_NOT_FOUND; | |
| } | |
| status_t requestStatus = _MakeRequest(); | |
| if (requestStatus != B_OK) | |
| return requestStatus; | |
| // Prepare the referer for the next request if needed | |
| if (fOptAutoReferer) | |
| fOptReferer = fUrl.UrlString(); | |
| switch (StatusCodeClass(fResult.StatusCode())) { | |
| case B_HTTP_STATUS_CLASS_INFORMATIONAL: | |
| // Header 100:continue should have been | |
| // handled in the _MakeRequest read loop | |
| break; | |
| case B_HTTP_STATUS_CLASS_SUCCESS: | |
| break; | |
| case B_HTTP_STATUS_CLASS_REDIRECTION: | |
| { | |
| // Redirection has been explicitly disabled | |
| if (!fOptFollowLocation) | |
| break; | |
| int code = fResult.StatusCode(); | |
| if (code == B_HTTP_STATUS_MOVED_PERMANENTLY | |
| || code == B_HTTP_STATUS_FOUND | |
| || code == B_HTTP_STATUS_SEE_OTHER | |
| || code == B_HTTP_STATUS_TEMPORARY_REDIRECT) { | |
| BString locationUrl = fHeaders["Location"]; | |
| fUrl = BUrl(fUrl, locationUrl); | |
| // 302 and 303 redirections also convert POST requests to GET | |
| // (and remove the posted form data) | |
| if ((code == B_HTTP_STATUS_FOUND | |
| || code == B_HTTP_STATUS_SEE_OTHER) | |
| && fRequestMethod == B_HTTP_POST) { | |
| SetMethod(B_HTTP_GET); | |
| delete fOptPostFields; | |
| fOptPostFields = NULL; | |
| delete fOptInputData; | |
| fOptInputData = NULL; | |
| fOptInputDataSize = 0; | |
| } | |
| if (--maxRedirs > 0) { | |
| newRequest = true; | |
| // Redirections may need a switch from http to https. | |
| if (fUrl.Protocol() == "https") | |
| fSSL = true; | |
| else if (fUrl.Protocol() == "http") | |
| fSSL = false; | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, | |
| "Following: %s\n", | |
| fUrl.UrlString().String()); | |
| } | |
| } | |
| break; | |
| } | |
| case B_HTTP_STATUS_CLASS_CLIENT_ERROR: | |
| if (fResult.StatusCode() == B_HTTP_STATUS_UNAUTHORIZED) { | |
| BHttpAuthentication* authentication | |
| = &fContext->GetAuthentication(fUrl); | |
| status_t status = B_OK; | |
| if (authentication->Method() == B_HTTP_AUTHENTICATION_NONE) { | |
| // There is no authentication context for this | |
| // url yet, so let's create one. | |
| BHttpAuthentication newAuth; | |
| newAuth.Initialize(fHeaders["WWW-Authenticate"]); | |
| fContext->AddAuthentication(fUrl, newAuth); | |
| // Get the copy of the authentication we just added. | |
| // That copy is owned by the BUrlContext and won't be | |
| // deleted (unlike the temporary object above) | |
| authentication = &fContext->GetAuthentication(fUrl); | |
| } | |
| newRequest = false; | |
| if (fOptUsername.Length() > 0 && status == B_OK) { | |
| // If we received an username and password, add them | |
| // to the request. This will either change the | |
| // credentials for an existing request, or set them | |
| // for a new one we created just above. | |
| // | |
| // If this request handles HTTP redirections, it will | |
| // also automatically retry connecting and send the | |
| // login information. | |
| authentication->SetUserName(fOptUsername); | |
| authentication->SetPassword(fOptPassword); | |
| newRequest = true; | |
| } | |
| } | |
| break; | |
| case B_HTTP_STATUS_CLASS_SERVER_ERROR: | |
| break; | |
| default: | |
| case B_HTTP_STATUS_CLASS_INVALID: | |
| break; | |
| } | |
| } while (newRequest); | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, | |
| "%" B_PRId32 " headers and %" B_PRIuSIZE " bytes of data remaining", | |
| fHeaders.CountHeaders(), fInputBuffer.Size()); | |
| if (fResult.StatusCode() == 404) | |
| return B_RESOURCE_NOT_FOUND; | |
| return B_OK; | |
| } | |
| status_t | |
| BHttpRequest::_MakeRequest() | |
| { | |
| delete fSocket; | |
| if (fSSL) { | |
| if (fContext->UseProxy()) { | |
| BNetworkAddress proxy(fContext->GetProxyHost(), fContext->GetProxyPort()); | |
| fSocket = new(std::nothrow) BPrivate::CheckedProxySecureSocket(proxy, this); | |
| } else | |
| fSocket = new(std::nothrow) BPrivate::CheckedSecureSocket(this); | |
| } else | |
| fSocket = new(std::nothrow) BSocket(); | |
| if (fSocket == NULL) | |
| return B_NO_MEMORY; | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Connection to %s on port %d.", | |
| fUrl.Authority().String(), fRemoteAddr.Port()); | |
| status_t connectError = fSocket->Connect(fRemoteAddr); | |
| if (connectError != B_OK) { | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, "Socket connection error %s", | |
| strerror(connectError)); | |
| return connectError; | |
| } | |
| //! ProtocolHook:ConnectionOpened | |
| if (fListener != NULL) | |
| fListener->ConnectionOpened(this); | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, | |
| "Connection opened, sending request."); | |
| BString requestHeaders; | |
| requestHeaders.Append(_SerializeRequest()); | |
| requestHeaders.Append(_SerializeHeaders()); | |
| requestHeaders.Append("\r\n"); | |
| fSocket->Write(requestHeaders.String(), requestHeaders.Length()); | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent."); | |
| _SendPostData(); | |
| fRequestStatus = kRequestInitialState; | |
| // Receive loop | |
| bool disableListener = false; | |
| bool receiveEnd = false; | |
| bool parseEnd = false; | |
| bool readByChunks = false; | |
| bool decompress = false; | |
| status_t readError = B_OK; | |
| ssize_t bytesRead = 0; | |
| off_t bytesReceived = 0; | |
| off_t bytesTotal = 0; | |
| size_t previousBufferSize = 0; | |
| off_t bytesUnpacked = 0; | |
| char* inputTempBuffer = new(std::nothrow) char[kHttpBufferSize]; | |
| ArrayDeleter<char> inputTempBufferDeleter(inputTempBuffer); | |
| ssize_t inputTempSize = kHttpBufferSize; | |
| ssize_t chunkSize = -1; | |
| DynamicBuffer decompressorStorage; | |
| BDataIO* decompressingStream; | |
| ObjectDeleter<BDataIO> decompressingStreamDeleter; | |
| while (!fQuit && !(receiveEnd && parseEnd)) { | |
| if ((!receiveEnd) && (fInputBuffer.Size() == previousBufferSize)) { | |
| BStackOrHeapArray<char, 4096> chunk(kHttpBufferSize); | |
| bytesRead = fSocket->Read(chunk, kHttpBufferSize); | |
| if (bytesRead < 0) { | |
| readError = bytesRead; | |
| break; | |
| } else if (bytesRead == 0) { | |
| // Check if we got the expected number of bytes. | |
| // Exceptions: | |
| // - If the content-length is not known (bytesTotal is 0), for | |
| // example in the case of a chunked transfer, we can't know | |
| // - If the request method is "HEAD" which explicitly asks the | |
| // server to not send any data (only the headers) | |
| if (bytesTotal > 0 && bytesReceived != bytesTotal) { | |
| readError = B_IO_ERROR; | |
| break; | |
| } | |
| receiveEnd = true; | |
| } | |
| fInputBuffer.AppendData(chunk, bytesRead); | |
| } else | |
| bytesRead = 0; | |
| previousBufferSize = fInputBuffer.Size(); | |
| if (fRequestStatus < kRequestStatusReceived) { | |
| _ParseStatus(); | |
| if (fOptFollowLocation | |
| && IsRedirectionStatusCode(fResult.StatusCode())) | |
| disableListener = true; | |
| if (fOptStopOnError | |
| && fResult.StatusCode() >= B_HTTP_STATUS_CLASS_CLIENT_ERROR) | |
| { | |
| fQuit = true; | |
| break; | |
| } | |
| //! ProtocolHook:ResponseStarted | |
| if (fRequestStatus >= kRequestStatusReceived && fListener != NULL | |
| && !disableListener) | |
| fListener->ResponseStarted(this); | |
| } | |
| if (fRequestStatus < kRequestHeadersReceived) { | |
| _ParseHeaders(); | |
| if (fRequestStatus >= kRequestHeadersReceived) { | |
| _ResultHeaders() = fHeaders; | |
| // Parse received cookies | |
| if (fContext != NULL) { | |
| for (int32 i = 0; i < fHeaders.CountHeaders(); i++) { | |
| if (fHeaders.HeaderAt(i).NameIs("Set-Cookie")) { | |
| fContext->GetCookieJar().AddCookie( | |
| fHeaders.HeaderAt(i).Value(), fUrl); | |
| } | |
| } | |
| } | |
| //! ProtocolHook:HeadersReceived | |
| if (fListener != NULL && !disableListener) | |
| fListener->HeadersReceived(this); | |
| if (BString(fHeaders["Transfer-Encoding"]) == "chunked") | |
| readByChunks = true; | |
| BString contentEncoding(fHeaders["Content-Encoding"]); | |
| // We don't advertise "deflate" support (see above), but we | |
| // still try to decompress it, if a server ever sends a deflate | |
| // stream despite it not being in our Accept-Encoding list. | |
| if (contentEncoding == "gzip" | |
| || contentEncoding == "deflate") { | |
| decompress = true; | |
| readError = BZlibCompressionAlgorithm() | |
| .CreateDecompressingOutputStream(&decompressorStorage, | |
| NULL, decompressingStream); | |
| if (readError != B_OK) | |
| break; | |
| decompressingStreamDeleter.SetTo(decompressingStream); | |
| } | |
| int32 index = fHeaders.HasHeader("Content-Length"); | |
| if (index != B_ERROR) | |
| bytesTotal = atoll(fHeaders.HeaderAt(index).Value()); | |
| else | |
| bytesTotal = -1; | |
| if (fRequestMethod == B_HTTP_HEAD | |
| || fResult.StatusCode() == 204) { | |
| // In the case of a HEAD request or if the server replies | |
| // 204 ("no content"), we don't expect to receive anything | |
| // more, and the socket will be closed. | |
| receiveEnd = true; | |
| } | |
| } | |
| } | |
| if (fRequestStatus >= kRequestHeadersReceived) { | |
| // If Transfer-Encoding is chunked, we should read a complete | |
| // chunk in buffer before handling it | |
| if (readByChunks) { | |
| if (chunkSize >= 0) { | |
| if ((ssize_t)fInputBuffer.Size() >= chunkSize + 2) { | |
| // 2 more bytes to handle the closing CR+LF | |
| bytesRead = chunkSize; | |
| if (inputTempSize < chunkSize + 2) { | |
| inputTempSize = chunkSize + 2; | |
| inputTempBuffer | |
| = new(std::nothrow) char[inputTempSize]; | |
| inputTempBufferDeleter.SetTo(inputTempBuffer); | |
| } | |
| if (inputTempBuffer == NULL) { | |
| readError = B_NO_MEMORY; | |
| break; | |
| } | |
| fInputBuffer.RemoveData(inputTempBuffer, | |
| chunkSize + 2); | |
| chunkSize = -1; | |
| } else { | |
| // Not enough data, try again later | |
| bytesRead = -1; | |
| } | |
| } else { | |
| BString chunkHeader; | |
| if (_GetLine(chunkHeader) == B_ERROR) { | |
| chunkSize = -1; | |
| bytesRead = -1; | |
| } else { | |
| // Format of a chunk header: | |
| // <chunk size in hex>[; optional data] | |
| int32 semiColonIndex = chunkHeader.FindFirst(';', 0); | |
| // Cut-off optional data if present | |
| if (semiColonIndex != -1) { | |
| chunkHeader.Remove(semiColonIndex, | |
| chunkHeader.Length() - semiColonIndex); | |
| } | |
| chunkSize = strtol(chunkHeader.String(), NULL, 16); | |
| if (chunkSize == 0) | |
| fRequestStatus = kRequestContentReceived; | |
| bytesRead = -1; | |
| } | |
| } | |
| // A chunk of 0 bytes indicates the end of the chunked transfer | |
| if (bytesRead == 0) | |
| receiveEnd = true; | |
| } else { | |
| bytesRead = fInputBuffer.Size(); | |
| if (bytesRead > 0) { | |
| if (inputTempSize < bytesRead) { | |
| inputTempSize = bytesRead; | |
| inputTempBuffer = new(std::nothrow) char[bytesRead]; | |
| inputTempBufferDeleter.SetTo(inputTempBuffer); | |
| } | |
| if (inputTempBuffer == NULL) { | |
| readError = B_NO_MEMORY; | |
| break; | |
| } | |
| fInputBuffer.RemoveData(inputTempBuffer, bytesRead); | |
| } | |
| } | |
| if (bytesRead >= 0) { | |
| bytesReceived += bytesRead; | |
| if (fOutput != NULL && !disableListener) { | |
| if (decompress) { | |
| readError = decompressingStream->WriteExactly( | |
| inputTempBuffer, bytesRead); | |
| if (readError != B_OK) | |
| break; | |
| ssize_t size = decompressorStorage.Size(); | |
| BStackOrHeapArray<char, 4096> buffer(size); | |
| size = decompressorStorage.Read(buffer, size); | |
| if (size > 0) { | |
| size_t written = 0; | |
| readError = fOutput->WriteExactly(buffer, | |
| size, &written); | |
| if (fListener != NULL && written > 0) | |
| fListener->BytesWritten(this, written); | |
| if (readError != B_OK) | |
| break; | |
| bytesUnpacked += size; | |
| } | |
| } else if (bytesRead > 0) { | |
| size_t written = 0; | |
| readError = fOutput->WriteExactly(inputTempBuffer, | |
| bytesRead, &written); | |
| if (fListener != NULL && written > 0) | |
| fListener->BytesWritten(this, written); | |
| if (readError != B_OK) | |
| break; | |
| } | |
| } | |
| if (fListener != NULL && !disableListener) | |
| fListener->DownloadProgress(this, bytesReceived, | |
| std::max((off_t)0, bytesTotal)); | |
| if (bytesTotal >= 0 && bytesReceived >= bytesTotal) | |
| receiveEnd = true; | |
| if (decompress && receiveEnd && !disableListener) { | |
| readError = decompressingStream->Flush(); | |
| if (readError == B_BUFFER_OVERFLOW) | |
| readError = B_OK; | |
| if (readError != B_OK) | |
| break; | |
| ssize_t size = decompressorStorage.Size(); | |
| BStackOrHeapArray<char, 4096> buffer(size); | |
| size = decompressorStorage.Read(buffer, size); | |
| if (fOutput != NULL && size > 0 && !disableListener) { | |
| size_t written = 0; | |
| readError = fOutput->WriteExactly(buffer, size, | |
| &written); | |
| if (fListener != NULL && written > 0) | |
| fListener->BytesWritten(this, written); | |
| if (readError != B_OK) | |
| break; | |
| bytesUnpacked += size; | |
| } | |
| } | |
| } | |
| } | |
| parseEnd = (fInputBuffer.Size() == 0); | |
| } | |
| fSocket->Disconnect(); | |
| if (readError != B_OK) | |
| return readError; | |
| return fQuit ? B_INTERRUPTED : B_OK; | |
| } | |
| void | |
| BHttpRequest::_ParseStatus() | |
| { | |
| // Status line should be formatted like: HTTP/M.m SSS ... | |
| // With: M = Major version of the protocol | |
| // m = Minor version of the protocol | |
| // SSS = three-digit status code of the response | |
| // ... = additional text info | |
| BString statusLine; | |
| if (_GetLine(statusLine) == B_ERROR) | |
| return; | |
| if (statusLine.CountChars() < 12) | |
| return; | |
| fRequestStatus = kRequestStatusReceived; | |
| BString statusCodeStr; | |
| BString statusText; | |
| statusLine.CopyInto(statusCodeStr, 9, 3); | |
| _SetResultStatusCode(atoi(statusCodeStr.String())); | |
| statusLine.CopyInto(_ResultStatusText(), 13, statusLine.Length() - 13); | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Status line received: Code %d (%s)", | |
| atoi(statusCodeStr.String()), _ResultStatusText().String()); | |
| } | |
| void | |
| BHttpRequest::_ParseHeaders() | |
| { | |
| BString currentHeader; | |
| while (_GetLine(currentHeader) != B_ERROR) { | |
| // An empty line means the end of the header section | |
| if (currentHeader.Length() == 0) { | |
| fRequestStatus = kRequestHeadersReceived; | |
| return; | |
| } | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_HEADER_IN, "%s", | |
| currentHeader.String()); | |
| fHeaders.AddHeader(currentHeader.String()); | |
| } | |
| } | |
| BString | |
| BHttpRequest::_SerializeRequest() | |
| { | |
| BString request(fRequestMethod); | |
| request << ' '; | |
| if (fContext->UseProxy()) { | |
| // When there is a proxy, the request must include the host and port so | |
| // the proxy knows where to send the request. | |
| request << Url().Protocol() << "://" << Url().Host(); | |
| if (Url().HasPort()) | |
| request << ':' << Url().Port(); | |
| } | |
| if (Url().HasPath() && Url().Path().Length() > 0) | |
| request << Url().Path(); | |
| else | |
| request << '/'; | |
| if (Url().HasRequest()) | |
| request << '?' << Url().Request(); | |
| switch (fHttpVersion) { | |
| case B_HTTP_11: | |
| request << " HTTP/1.1\r\n"; | |
| break; | |
| default: | |
| case B_HTTP_10: | |
| request << " HTTP/1.0\r\n"; | |
| break; | |
| } | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_HEADER_OUT, "%s", request.String()); | |
| return request; | |
| } | |
| BString | |
| BHttpRequest::_SerializeHeaders() | |
| { | |
| BHttpHeaders outputHeaders; | |
| // HTTP 1.1 additional headers | |
| if (fHttpVersion == B_HTTP_11) { | |
| BString host = Url().Host(); | |
| if (Url().HasPort() && !_IsDefaultPort()) | |
| host << ':' << Url().Port(); | |
| outputHeaders.AddHeader("Host", host); | |
| outputHeaders.AddHeader("Accept", "*/*"); | |
| outputHeaders.AddHeader("Accept-Encoding", "gzip"); | |
| // Allows the server to compress data using the "gzip" format. | |
| // "deflate" is not supported, because there are two interpretations | |
| // of what it means (the RFC and Microsoft products), and we don't | |
| // want to handle this. Very few websites support only deflate, | |
| // and most of them will send gzip, or at worst, uncompressed data. | |
| outputHeaders.AddHeader("Connection", "close"); | |
| // Let the remote server close the connection after response since | |
| // we don't handle multiple request on a single connection | |
| } | |
| // Classic HTTP headers | |
| if (fOptUserAgent.CountChars() > 0) | |
| outputHeaders.AddHeader("User-Agent", fOptUserAgent.String()); | |
| if (fOptReferer.CountChars() > 0) | |
| outputHeaders.AddHeader("Referer", fOptReferer.String()); | |
| // Optional range requests headers | |
| if (fOptRangeStart != -1 || fOptRangeEnd != -1) { | |
| if (fOptRangeStart == -1) | |
| fOptRangeStart = 0; | |
| BString range; | |
| if (fOptRangeEnd != -1) { | |
| range.SetToFormat("bytes=%" B_PRIdOFF "-%" B_PRIdOFF, | |
| fOptRangeStart, fOptRangeEnd); | |
| } else { | |
| range.SetToFormat("bytes=%" B_PRIdOFF "-", fOptRangeStart); | |
| } | |
| outputHeaders.AddHeader("Range", range.String()); | |
| } | |
| // Authentication | |
| if (fContext != NULL) { | |
| BHttpAuthentication& authentication = fContext->GetAuthentication(fUrl); | |
| if (authentication.Method() != B_HTTP_AUTHENTICATION_NONE) { | |
| if (fOptUsername.Length() > 0) { | |
| authentication.SetUserName(fOptUsername); | |
| authentication.SetPassword(fOptPassword); | |
| } | |
| BString request(fRequestMethod); | |
| outputHeaders.AddHeader("Authorization", | |
| authentication.Authorization(fUrl, request)); | |
| } | |
| } | |
| // Required headers for POST data | |
| if (fOptPostFields != NULL && fRequestMethod == B_HTTP_POST) { | |
| BString contentType; | |
| switch (fOptPostFields->GetFormType()) { | |
| case B_HTTP_FORM_MULTIPART: | |
| contentType << "multipart/form-data; boundary=" | |
| << fOptPostFields->GetMultipartBoundary() << ""; | |
| break; | |
| case B_HTTP_FORM_URL_ENCODED: | |
| contentType << "application/x-www-form-urlencoded"; | |
| break; | |
| } | |
| outputHeaders.AddHeader("Content-Type", contentType); | |
| outputHeaders.AddHeader("Content-Length", | |
| fOptPostFields->ContentLength()); | |
| } else if (fOptInputData != NULL | |
| && (fRequestMethod == B_HTTP_POST | |
| || fRequestMethod == B_HTTP_PUT)) { | |
| if (fOptInputDataSize >= 0) | |
| outputHeaders.AddHeader("Content-Length", fOptInputDataSize); | |
| else | |
| outputHeaders.AddHeader("Transfer-Encoding", "chunked"); | |
| } | |
| // Optional headers specified by the user | |
| if (fOptHeaders != NULL) { | |
| for (int32 headerIndex = 0; headerIndex < fOptHeaders->CountHeaders(); | |
| headerIndex++) { | |
| BHttpHeader& optHeader = (*fOptHeaders)[headerIndex]; | |
| int32 replaceIndex = outputHeaders.HasHeader(optHeader.Name()); | |
| // Add or replace the current option header to the | |
| // output header list | |
| if (replaceIndex == -1) | |
| outputHeaders.AddHeader(optHeader.Name(), optHeader.Value()); | |
| else | |
| outputHeaders[replaceIndex].SetValue(optHeader.Value()); | |
| } | |
| } | |
| // Context cookies | |
| if (fOptSetCookies && fContext != NULL) { | |
| BString cookieString; | |
| BNetworkCookieJar::UrlIterator iterator | |
| = fContext->GetCookieJar().GetUrlIterator(fUrl); | |
| const BNetworkCookie* cookie = iterator.Next(); | |
| if (cookie != NULL) { | |
| while (true) { | |
| cookieString << cookie->RawCookie(false); | |
| cookie = iterator.Next(); | |
| if (cookie == NULL) | |
| break; | |
| cookieString << "; "; | |
| } | |
| outputHeaders.AddHeader("Cookie", cookieString); | |
| } | |
| } | |
| // Write output headers to output stream | |
| BString headerData; | |
| for (int32 headerIndex = 0; headerIndex < outputHeaders.CountHeaders(); | |
| headerIndex++) { | |
| const char* header = outputHeaders.HeaderAt(headerIndex).Header(); | |
| headerData << header; | |
| headerData << "\r\n"; | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_HEADER_OUT, "%s", header); | |
| } | |
| return headerData; | |
| } | |
| void | |
| BHttpRequest::_SendPostData() | |
| { | |
| if (fRequestMethod == B_HTTP_POST && fOptPostFields != NULL) { | |
| if (fOptPostFields->GetFormType() != B_HTTP_FORM_MULTIPART) { | |
| BString outputBuffer = fOptPostFields->RawData(); | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_TRANSFER_OUT, | |
| "%s", outputBuffer.String()); | |
| fSocket->Write(outputBuffer.String(), outputBuffer.Length()); | |
| } else { | |
| for (BHttpForm::Iterator it = fOptPostFields->GetIterator(); | |
| const BHttpFormData* currentField = it.Next(); | |
| ) { | |
| _EmitDebug(B_URL_PROTOCOL_DEBUG_TRANSFER_OUT, | |
| it.MultipartHeader().String()); | |
| fSocket->Write(it.MultipartHeader().String(), | |
| it.MultipartHeader().Length()); | |
| switch (currentField->Type()) { | |
| default: | |
| case B_HTTPFORM_UNKNOWN: | |
| ASSERT(0); | |
| break; | |
| case B_HTTPFORM_STRING: | |
| fSocket->Write(currentField->String().String(), | |
| currentField->String().Length()); | |
| break; | |
| case B_HTTPFORM_FILE: | |
| { | |
| BFile upFile(currentField->File().Path(), | |
| B_READ_ONLY); | |
| char readBuffer[kHttpBufferSize]; | |
| ssize_t readSize; | |
| off_t totalSize; | |
| if (upFile.GetSize(&totalSize) != B_OK) | |
| ASSERT(0); | |
| readSize = upFile.Read(readBuffer, | |
| sizeof(readBuffer)); | |
| while (readSize > 0) { | |
| fSocket->Write(readBuffer, readSize); | |
| readSize = upFile.Read(readBuffer, | |
| sizeof(readBuffer)); | |
| fListener->UploadProgress(this, readSize, | |
| std::max((off_t)0, totalSize)); | |
| } | |
| break; | |
| } | |
| case B_HTTPFORM_BUFFER: | |
| fSocket->Write(currentField->Buffer(), | |
| currentField->BufferSize()); | |
| break; | |
| } | |
| fSocket->Write("\r\n", 2); | |
| } | |
| BString footer = fOptPostFields->GetMultipartFooter(); | |
| fSocket->Write(footer.String(), footer.Length()); | |
| } | |
| } else if ((fRequestMethod == B_HTTP_POST || fRequestMethod == B_HTTP_PUT) | |
| && fOptInputData != NULL) { | |
| // If the input data is seekable, we rewind it for each new request. | |
| BPositionIO* seekableData | |
| = dynamic_cast<BPositionIO*>(fOptInputData); | |
| if (seekableData) | |
| seekableData->Seek(0, SEEK_SET); | |
| for (;;) { | |
| char outputTempBuffer[kHttpBufferSize]; | |
| ssize_t read = fOptInputData->Read(outputTempBuffer, | |
| sizeof(outputTempBuffer)); | |
| if (read <= 0) | |
| break; | |
| if (fOptInputDataSize < 0) { | |
| // Input data size unknown, so we have to use chunked transfer | |
| char hexSize[18]; | |
| // The string does not need to be NULL terminated. | |
| size_t hexLength = snprintf(hexSize, sizeof(hexSize), "%lx\r\n", | |
| read); | |
| fSocket->Write(hexSize, hexLength); | |
| fSocket->Write(outputTempBuffer, read); | |
| fSocket->Write("\r\n", 2); | |
| } else { | |
| fSocket->Write(outputTempBuffer, read); | |
| } | |
| } | |
| if (fOptInputDataSize < 0) { | |
| // Chunked transfer terminating sequence | |
| fSocket->Write("0\r\n\r\n", 5); | |
| } | |
| } | |
| } | |
| BHttpHeaders& | |
| BHttpRequest::_ResultHeaders() | |
| { | |
| return fResult.fHeaders; | |
| } | |
| void | |
| BHttpRequest::_SetResultStatusCode(int32 statusCode) | |
| { | |
| fResult.fStatusCode = statusCode; | |
| } | |
| BString& | |
| BHttpRequest::_ResultStatusText() | |
| { | |
| return fResult.fStatusString; | |
| } | |
| bool | |
| BHttpRequest::_CertificateVerificationFailed(BCertificate& certificate, | |
| const char* message) | |
| { | |
| if (fContext->HasCertificateException(certificate)) | |
| return true; | |
| if (fListener != NULL | |
| && fListener->CertificateVerificationFailed(this, certificate, message)) { | |
| // User asked us to continue anyway, let's add a temporary exception for this certificate | |
| fContext->AddCertificateException(certificate); | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool | |
| BHttpRequest::_IsDefaultPort() | |
| { | |
| if (fSSL && Url().Port() == 443) | |
| return true; | |
| if (!fSSL && Url().Port() == 80) | |
| return true; | |
| return false; | |
| } |