bool BHttpRequest::_ResolveHostName() { _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Resolving %s", fUrl.UrlString().String()); uint16_t port; if (fUrl.HasPort()) port = fUrl.Port(); else port = fSSL ? 443 : 80; // FIXME stop forcing AF_INET, when BNetworkAddress stops giving IPv6 // addresses when there isn't an IPv6 link available. fRemoteAddr = BNetworkAddress(AF_INET, fUrl.Host(), port); if (fRemoteAddr.InitCheck() != B_OK) return false; //! ProtocolHook:HostnameResolved if (fListener != NULL) fListener->HostnameResolved(this, fRemoteAddr.ToString().String()); _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Hostname resolved to: %s", fRemoteAddr.ToString().String()); return true; }
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()); } }
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; readSize = upFile.Read(readBuffer, sizeof(readBuffer)); while (readSize > 0) { fSocket->Write(readBuffer, readSize); readSize = upFile.Read(readBuffer, sizeof(readBuffer)); } 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) { // Chunked transfer char hexSize[16]; size_t hexLength = sprintf(hexSize, "%ld", read); fSocket->Write(hexSize, hexLength); fSocket->Write("\r\n", 2); 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); } } }
void BHttpRequest::_SendHeaders() { 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()); // 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); } fSocket->Write(headerData.String(), headerData.Length()); }
status_t BHttpRequest::_MakeRequest() { delete fSocket; if (fSSL) fSocket = new(std::nothrow) 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."); _SendRequest(); _SendHeaders(); fSocket->Write("\r\n", 2); _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent."); _SendPostData(); fRequestStatus = kRequestInitialState; // Receive loop bool receiveEnd = false; bool parseEnd = false; bool readByChunks = false; bool decompress = false; status_t readError = B_OK; ssize_t bytesRead = 0; ssize_t bytesReceived = 0; ssize_t bytesTotal = 0; off_t bytesUnpacked = 0; char* inputTempBuffer = new(std::nothrow) char[kHttpBufferSize]; ssize_t inputTempSize = kHttpBufferSize; ssize_t chunkSize = -1; DynamicBuffer decompressorStorage; BDataIO* decompressingStream; ObjectDeleter<BDataIO> decompressingStreamDeleter; while (!fQuit && !(receiveEnd && parseEnd)) { if (!receiveEnd) { fSocket->WaitForReadable(); BStackOrHeapArray<char, 4096> chunk(kHttpBufferSize); bytesRead = fSocket->Read(chunk, kHttpBufferSize); if (bytesRead < 0) { readError = bytesRead; break; } else if (bytesRead == 0) receiveEnd = true; fInputBuffer.AppendData(chunk, bytesRead); } else bytesRead = 0; if (fRequestStatus < kRequestStatusReceived) { _ParseStatus(); //! ProtocolHook:ResponseStarted if (fRequestStatus >= kRequestStatusReceived && fListener != NULL) fListener->ResponseStarted(this); } if (fRequestStatus < kRequestHeadersReceived) { _ParseHeaders(); if (fRequestStatus >= kRequestHeadersReceived) { _ResultHeaders() = fHeaders; //! ProtocolHook:HeadersReceived if (fListener != NULL) fListener->HeadersReceived(this); // 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); } } } 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 = atoi(fHeaders.HeaderAt(index).Value()); else bytesTotal = -1; } } 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) { delete[] inputTempBuffer; inputTempSize = chunkSize + 2; inputTempBuffer = new(std::nothrow) char[inputTempSize]; } 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); PRINT(("BHP[%p] Chunk %s=%ld\n", this, chunkHeader.String(), chunkSize)); 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; delete[] inputTempBuffer; inputTempBuffer = new(std::nothrow) char[bytesRead]; } if (inputTempBuffer == NULL) { readError = B_NO_MEMORY; break; } fInputBuffer.RemoveData(inputTempBuffer, bytesRead); } } if (bytesRead >= 0) { bytesReceived += bytesRead; if (fListener != NULL) { 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) { fListener->DataReceived(this, buffer, bytesUnpacked, size); bytesUnpacked += size; } } else if (bytesRead > 0) { fListener->DataReceived(this, inputTempBuffer, bytesReceived - bytesRead, bytesRead); } fListener->DownloadProgress(this, bytesReceived, std::max((ssize_t)0, bytesTotal)); } if (bytesTotal >= 0 && bytesReceived >= bytesTotal) receiveEnd = true; if (decompress && receiveEnd) { 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 (fListener != NULL && size > 0) { fListener->DataReceived(this, buffer, bytesUnpacked, size); bytesUnpacked += size; } } } } parseEnd = (fInputBuffer.Size() == 0); } fSocket->Disconnect(); delete[] inputTempBuffer; if (readError != B_OK) return readError; return fQuit ? B_INTERRUPTED : B_OK; }
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, "%ld headers and %ld bytes of data remaining", fHeaders.CountHeaders(), fInputBuffer.Size()); if (fResult.StatusCode() == 404) return B_RESOURCE_NOT_FOUND; return B_OK; }
void BHttpRequest::_SendHeaders() { // HTTP 1.1 additional headers if (fHttpVersion == B_HTTP_11) { fOutputHeaders.AddHeader("Host", Url().Host()); fOutputHeaders.AddHeader("Accept", "*/*"); fOutputHeaders.AddHeader("Accept-Encoding", "gzip,deflate"); // Allow the remote server to send dynamic content by chunks // rather than waiting for the full content to be generated and // sending us data. fOutputHeaders.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) fOutputHeaders.AddHeader("User-Agent", fOptUserAgent.String()); if (fOptReferer.CountChars() > 0) fOutputHeaders.AddHeader("Referer", fOptReferer.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); fOutputHeaders.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; } fOutputHeaders.AddHeader("Content-Type", contentType); fOutputHeaders.AddHeader("Content-Length", fOptPostFields->ContentLength()); } else if (fOptInputData != NULL && (fRequestMethod == B_HTTP_POST || fRequestMethod == B_HTTP_PUT)) { if (fOptInputDataSize >= 0) fOutputHeaders.AddHeader("Content-Length", fOptInputDataSize); else fOutputHeaders.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 = fOutputHeaders.HasHeader(optHeader.Name()); // Add or replace the current option header to the // output header list if (replaceIndex == -1) fOutputHeaders.AddHeader(optHeader.Name(), optHeader.Value()); else fOutputHeaders[replaceIndex].SetValue(optHeader.Value()); } } // Context cookies if (fOptSetCookies && fContext != NULL) { BString cookieString; BNetworkCookieJar::UrlIterator iterator = fContext->GetCookieJar().GetUrlIterator(fUrl); BNetworkCookie* cookie = iterator.Next(); if (cookie != NULL) { while (true) { cookieString << cookie->RawCookie(false); cookie = iterator.Next(); if (cookie == NULL) break; cookieString << "; "; } fOutputHeaders.AddHeader("Cookie", cookieString); } } // Write output headers to output stream for (int32 headerIndex = 0; headerIndex < fOutputHeaders.CountHeaders(); headerIndex++) { const char* header = fOutputHeaders.HeaderAt(headerIndex).Header(); fSocket->Write(header, strlen(header)); fSocket->Write("\r\n", 2); _EmitDebug(B_URL_PROTOCOL_DEBUG_HEADER_OUT, "%s", header); } }
status_t BHttpRequest::_MakeRequest() { 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."); _SendRequest(); _SendHeaders(); fSocket->Write("\r\n", 2); _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent."); 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; readSize = upFile.Read(readBuffer, sizeof(readBuffer)); while (readSize > 0) { fSocket->Write(readBuffer, readSize); readSize = upFile.Read(readBuffer, sizeof(readBuffer)); } 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) { for (;;) { char outputTempBuffer[kHttpBufferSize]; ssize_t read = fOptInputData->Read(outputTempBuffer, sizeof(outputTempBuffer)); if (read <= 0) break; if (fOptInputDataSize < 0) { // Chunked transfer char hexSize[16]; size_t hexLength = sprintf(hexSize, "%ld", read); fSocket->Write(hexSize, hexLength); fSocket->Write("\r\n", 2); 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); } } fRequestStatus = kRequestInitialState; // Receive loop bool receiveEnd = false; bool parseEnd = false; bool readByChunks = false; bool decompress = false; status_t readError = B_OK; ssize_t bytesRead = 0; ssize_t bytesReceived = 0; ssize_t bytesTotal = 0; char* inputTempBuffer = new(std::nothrow) char[kHttpBufferSize]; ssize_t inputTempSize = kHttpBufferSize; ssize_t chunkSize = -1; DynamicBuffer decompressorStorage; BPrivate::ZlibDecompressor decompressor(&decompressorStorage); while (!fQuit && !(receiveEnd && parseEnd)) { if (!receiveEnd) { fSocket->WaitForReadable(); BNetBuffer chunk(kHttpBufferSize); bytesRead = fSocket->Read(chunk.Data(), kHttpBufferSize); if (bytesRead < 0) { readError = bytesRead; break; } else if (bytesRead == 0) receiveEnd = true; fInputBuffer.AppendData(chunk.Data(), bytesRead); } else bytesRead = 0; if (fRequestStatus < kRequestStatusReceived) { _ParseStatus(); //! ProtocolHook:ResponseStarted if (fRequestStatus >= kRequestStatusReceived && fListener != NULL) fListener->ResponseStarted(this); } if (fRequestStatus < kRequestHeadersReceived) { _ParseHeaders(); if (fRequestStatus >= kRequestHeadersReceived) { _ResultHeaders() = fHeaders; //! ProtocolHook:HeadersReceived if (fListener != NULL) fListener->HeadersReceived(this); // 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); } } } if (BString(fHeaders["Transfer-Encoding"]) == "chunked") readByChunks = true; BString contentEncoding(fHeaders["Content-Encoding"]); if (contentEncoding == "gzip" || contentEncoding == "deflate") { decompress = true; decompressor.Init(); } int32 index = fHeaders.HasHeader("Content-Length"); if (index != B_ERROR) bytesTotal = atoi(fHeaders.HeaderAt(index).Value()); else bytesTotal = 0; } } 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) { delete[] inputTempBuffer; inputTempSize = chunkSize + 2; inputTempBuffer = new(std::nothrow) char[inputTempSize]; } 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); PRINT(("BHP[%p] Chunk %s=%ld\n", this, chunkHeader.String(), chunkSize)); 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; delete[] inputTempBuffer; inputTempBuffer = new(std::nothrow) char[bytesRead]; } if (inputTempBuffer == NULL) { readError = B_NO_MEMORY; break; } fInputBuffer.RemoveData(inputTempBuffer, bytesRead); } } if (bytesRead > 0) { bytesReceived += bytesRead; if (fListener != NULL) { if (decompress) { decompressor.DecompressNext(inputTempBuffer, bytesRead); ssize_t size = decompressorStorage.Size(); BStackOrHeapArray<char, 4096> buffer(size); size = decompressorStorage.Read(buffer, size); if (size > 0) { fListener->DataReceived(this, buffer, size); } } else { fListener->DataReceived(this, inputTempBuffer, bytesRead); } fListener->DownloadProgress(this, bytesReceived, bytesTotal); } if (bytesTotal > 0 && bytesReceived >= bytesTotal) { receiveEnd = true; if (decompress) { decompressor.Finish(); ssize_t size = decompressorStorage.Size(); BStackOrHeapArray<char, 4096> buffer(size); size = decompressorStorage.Read(buffer, size); if (fListener != NULL && size > 0) { fListener->DataReceived(this, buffer, size); } } } } } parseEnd = (fInputBuffer.Size() == 0); } fSocket->Disconnect(); delete[] inputTempBuffer; if (readError != B_OK) return readError; return fQuit ? B_INTERRUPTED : B_OK; }
status_t BHttpRequest::_ProtocolLoop() { // Initialize the request redirection loop int8 maxRedirs = fOptMaxRedirs; bool newRequest; do { newRequest = false; // Result reset fOutputHeaders.Clear(); fHeaders.Clear(); _ResultHeaders().Clear(); if (!_ResolveHostName()) { _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; // TODO: Some browsers seems to translate POST requests to // GET when following a 302 redirection if (fResult.StatusCode() == B_HTTP_STATUS_MOVED_PERMANENTLY) { BString locationUrl = fHeaders["Location"]; fUrl = BUrl(fUrl, locationUrl); if (--maxRedirs > 0) { newRequest = true; _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. authentication = new(std::nothrow) BHttpAuthentication(); if (authentication == NULL) status = B_NO_MEMORY; else { status = authentication->Initialize( fHeaders["WWW-Authenticate"]); fContext->AddAuthentication(fUrl, authentication); } } 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, "%ld headers and %ld bytes of data remaining", fHeaders.CountHeaders(), fInputBuffer.Size()); if (fResult.StatusCode() == 404) return B_RESOURCE_NOT_FOUND; return B_OK; }
void BGopherRequest::_ParseInput(bool last) { BString line; while (_GetLine(line) == B_OK) { char type = GOPHER_TYPE_NONE; BStringList fields; line.MoveInto(&type, 0, 1); line.Split("\t", false, fields); if (type != GOPHER_TYPE_ENDOFPAGE && fields.CountStrings() < FIELD_GPFLAG) _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Unterminated gopher item (type '%c')", type); BString pageTitle; BString item; BString title = fields.StringAt(FIELD_NAME); BString link("gopher://"); BString user; if (fields.CountStrings() > 3) { link << fields.StringAt(FIELD_HOST); if (fields.StringAt(FIELD_PORT).Length()) link << ":" << fields.StringAt(FIELD_PORT); link << "/" << type; //if (fields.StringAt(FIELD_SELECTOR).ByteAt(0) != '/') // link << "/"; link << fields.StringAt(FIELD_SELECTOR); } _HTMLEscapeString(title); _HTMLEscapeString(link); switch (type) { case GOPHER_TYPE_ENDOFPAGE: /* end of the page */ break; case GOPHER_TYPE_TEXTPLAIN: item << "<a href=\"" << link << "\">" "<span class=\"text\">" << title << "</span></a>" "<br/>\n"; break; case GOPHER_TYPE_BINARY: case GOPHER_TYPE_BINHEX: case GOPHER_TYPE_BINARCHIVE: case GOPHER_TYPE_UUENCODED: item << "<a href=\"" << link << "\">" "<span class=\"binary\">" << title << "</span></a>" "<br/>\n"; break; case GOPHER_TYPE_DIRECTORY: /* * directory link */ item << "<a href=\"" << link << "\">" "<span class=\"dir\">" << title << "</span></a>" "<br/>\n"; break; case GOPHER_TYPE_ERROR: item << "<span class=\"error\">" << title << "</span>" "<br/>\n"; if (fPosition == 0 && pageTitle.Length() == 0) pageTitle << "Error: " << title; break; case GOPHER_TYPE_QUERY: /* TODO: handle search better. * For now we use an unnamed input field and accept sending ?=foo * as it seems at least Veronica-2 ignores the = but it's unclean. */ item << "<form method=\"get\" action=\"" << link << "\" " "onsubmit=\"window.location = this.action + '?' + " "this.elements['q'].value; return false;\">" "<span class=\"query\">" "<label>" << title << " " "<input id=\"q\" name=\"\" type=\"text\" align=\"right\" />" "</label>" "</span></form>" "<br/>\n"; break; case GOPHER_TYPE_TELNET: /* telnet: links * cf. gopher://78.80.30.202/1/ps3 * -> gopher://78.80.30.202:23/8/ps3/new -> [email protected] */ link = "telnet://"; user = fields.StringAt(FIELD_SELECTOR); if (user.FindLast('/') > -1) { user.Remove(0, user.FindLast('/')); link << user << "@"; } link << fields.StringAt(FIELD_HOST); if (fields.StringAt(FIELD_PORT) != "23") link << ":" << fields.StringAt(FIELD_PORT); item << "<a href=\"" << link << "\">" "<span class=\"telnet\">" << title << "</span></a>" "<br/>\n"; break; case GOPHER_TYPE_TN3270: /* tn3270: URI scheme, cf. http://tools.ietf.org/html/rfc6270 */ link = "tn3270://"; user = fields.StringAt(FIELD_SELECTOR); if (user.FindLast('/') > -1) { user.Remove(0, user.FindLast('/')); link << user << "@"; } link << fields.StringAt(FIELD_HOST); if (fields.StringAt(FIELD_PORT) != "23") link << ":" << fields.StringAt(FIELD_PORT); item << "<a href=\"" << link << "\">" "<span class=\"telnet\">" << title << "</span></a>" "<br/>\n"; break; case GOPHER_TYPE_CSO_SEARCH: /* CSO search. * At least Lynx supports a cso:// URI scheme: * http://lynx.isc.org/lynx2.8.5/lynx2-8-5/lynx_help/lynx_url_support.html */ link = "cso://"; user = fields.StringAt(FIELD_SELECTOR); if (user.FindLast('/') > -1) { user.Remove(0, user.FindLast('/')); link << user << "@"; } link << fields.StringAt(FIELD_HOST); if (fields.StringAt(FIELD_PORT) != "105") link << ":" << fields.StringAt(FIELD_PORT); item << "<a href=\"" << link << "\">" "<span class=\"cso\">" << title << "</span></a>" "<br/>\n"; break; case GOPHER_TYPE_GIF: case GOPHER_TYPE_IMAGE: case GOPHER_TYPE_PNG: case GOPHER_TYPE_BITMAP: /* quite dangerous, cf. gopher://namcub.accela-labs.com/1/pics */ if (kInlineImages) { item << "<a href=\"" << link << "\">" "<span class=\"img\">" << title << " " "<img src=\"" << link << "\" " "alt=\"" << title << "\"/>" "</span></a>" "<br/>\n"; break; } /* fallback to default, link them */ item << "<a href=\"" << link << "\">" "<span class=\"img\">" << title << "</span></a>" "<br/>\n"; break; case GOPHER_TYPE_HTML: /* cf. gopher://pineapple.vg/1 */ if (fields.StringAt(FIELD_SELECTOR).StartsWith("URL:")) { link = fields.StringAt(FIELD_SELECTOR); link.Remove(0, 4); } /* cf. gopher://sdf.org/1/sdf/classes/ */ item << "<a href=\"" << link << "\">" "<span class=\"html\">" << title << "</span></a>" "<br/>\n"; break; case GOPHER_TYPE_INFO: // TITLE resource, cf. // gopher://gophernicus.org/0/doc/gopher/gopher-title-resource.txt if (fPosition == 0 && pageTitle.Length() == 0 && fields.StringAt(FIELD_SELECTOR) == "TITLE") { pageTitle = title; break; } item << "<span class=\"info\">" << title << "</span>" "<br/>\n"; break; case GOPHER_TYPE_AUDIO: case GOPHER_TYPE_SOUND: item << "<a href=\"" << link << "\">" "<span class=\"audio\">" << title << "</span></a>" "<audio src=\"" << link << "\" " //TODO:Fix crash in WebPositive with these //"controls=\"controls\" " //"width=\"300\" height=\"50\" " "alt=\"" << title << "\"/>" "<span>[player]</span></audio>" "<br/>\n"; break; case GOPHER_TYPE_PDF: case GOPHER_TYPE_DOC: /* generic case for known-to-work items */ item << "<a href=\"" << link << "\">" "<span class=\"document\">" << title << "</span></a>" "<br/>\n"; break; case GOPHER_TYPE_MOVIE: item << "<a href=\"" << link << "\">" "<span class=\"video\">" << title << "</span></a>" "<video src=\"" << link << "\" " //TODO:Fix crash in WebPositive with these //"controls=\"controls\" " //"width=\"300\" height=\"300\" " "alt=\"" << title << "\"/>" "<span>[player]</span></audio>" "<br/>\n"; break; default: _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Unknown gopher item (type 0x%02x '%c')", type, type); item << "<a href=\"" << link << "\">" "<span class=\"unknown\">" << title << "</span></a>" "<br/>\n"; break; } if (fPosition == 0) { if (pageTitle.Length() == 0) pageTitle << "Index of " << Url(); const char *uplink = "."; if (fPath.EndsWith("/")) uplink = ".."; // emit header BString header; header << "<html>\n" "<head>\n" "<meta http-equiv=\"Content-Type\"" " content=\"text/html; charset=UTF-8\" />\n" //FIXME: fix links //"<link rel=\"icon\" type=\"image/png\"" // " href=\"resource:icons/directory.png\">\n" "<style type=\"text/css\">\n" << kStyleSheet << "</style>\n" "<title>" << pageTitle << "</title>\n" "</head>\n" "<body id=\"gopher\">\n" "<div class=\"uplink dontprint\">\n" "<a href=" << uplink << ">[up]</a>\n" "<a href=\"/\">[top]</a>\n" "</div>\n" "<h1>" << pageTitle << "</h1>\n"; fListener->DataReceived(this, header.String(), fPosition, header.Length()); fPosition += header.Length(); } if (item.Length()) { fListener->DataReceived(this, item.String(), fPosition, item.Length()); fPosition += item.Length(); } } if (last) { // emit footer BString footer = "</div>\n" "</body>\n" "</html>\n"; fListener->DataReceived(this, footer.String(), fPosition, footer.Length()); fPosition += footer.Length(); } }
status_t BGopherRequest::_ProtocolLoop() { if (fSocket == NULL) return B_NO_MEMORY; if (!_ResolveHostName(fUrl.Host(), fUrl.HasPort() ? fUrl.Port() : 70)) { _EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, "Unable to resolve hostname (%s), aborting.", fUrl.Host().String()); return B_SERVER_NOT_FOUND; } _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."); _SendRequest(); _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent."); // Receive loop bool receiveEnd = false; status_t readError = B_OK; ssize_t bytesRead = 0; //ssize_t bytesReceived = 0; //ssize_t bytesTotal = 0; bool dataValidated = false; BStackOrHeapArray<char, 4096> chunk(kGopherBufferSize); while (!fQuit && !receiveEnd) { fSocket->WaitForReadable(); bytesRead = fSocket->Read(chunk, kGopherBufferSize); if (bytesRead < 0) { readError = bytesRead; break; } else if (bytesRead == 0) receiveEnd = true; fInputBuffer.AppendData(chunk, bytesRead); if (!dataValidated) { size_t i; // on error (file doesn't exist, ...) the server sends // a faked directory entry with an error message if (fInputBuffer.Size() && fInputBuffer.Data()[0] == '3') { int tabs = 0; bool crlf = false; // make sure the buffer only contains printable characters // and has at least 3 tabs before a CRLF for (i = 0; i < fInputBuffer.Size(); i++) { char c = fInputBuffer.Data()[i]; if (c == '\t') { if (!crlf) tabs++; } else if (c == '\r' || c == '\n') { if (tabs < 3) break; crlf = true; } else if (!isprint(fInputBuffer.Data()[i])) { crlf = false; break; } } if (crlf && tabs > 2 && tabs < 5) { // TODO: //if enough data // else continue fItemType = GOPHER_TYPE_DIRECTORY; readError = B_RESOURCE_NOT_FOUND; // continue parsing the error text anyway } } // special case for buggy(?) Gophernicus/1.5 static const char *buggy = "Error: File or directory not found!"; if (fInputBuffer.Size() > strlen(buggy) && !memcmp(fInputBuffer.Data(), buggy, strlen(buggy))) { fItemType = GOPHER_TYPE_DIRECTORY; readError = B_RESOURCE_NOT_FOUND; // continue parsing the error text anyway // but it won't look good } // now we probably have correct data dataValidated = true; //! ProtocolHook:ResponseStarted if (fListener != NULL) fListener->ResponseStarted(this); // we don't really have headers but well... //! ProtocolHook:HeadersReceived if (fListener != NULL) fListener->HeadersReceived(this); // now we can assign MIME type if we know it const char *mime = "application/octet-stream"; for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) { if (gopher_type_map[i].type == fItemType) { mime = gopher_type_map[i].mime; break; } } fResult.SetContentType(mime); } if (_NeedsParsing()) _ParseInput(receiveEnd); else if (fInputBuffer.Size()) { // send input directly if (fListener != NULL) { fListener->DataReceived(this, (const char *)fInputBuffer.Data(), fPosition, fInputBuffer.Size()); } fPosition += fInputBuffer.Size(); // XXX: this is plain stupid, we already copied the data // and just want to drop it... char *inputTempBuffer = new(std::nothrow) char[bytesRead]; if (inputTempBuffer == NULL) { readError = B_NO_MEMORY; break; } fInputBuffer.RemoveData(inputTempBuffer, fInputBuffer.Size()); delete[] inputTempBuffer; } } if (fPosition > 0) { fResult.SetLength(fPosition); if (fListener != NULL) fListener->DownloadProgress(this, fPosition, fPosition); } fSocket->Disconnect(); if (readError != B_OK) return readError; return fQuit ? B_INTERRUPTED : B_OK; }
void BHttpRequest::_AddOutputBufferLine(const char* line) { _EmitDebug(B_URL_PROTOCOL_DEBUG_HEADER_OUT, "%s", line); fOutputBuffer << line << "\r\n"; }