blob: fac81782cd55a3799f525aac4dc411ff62fa7fa5 [file] [log] [blame]
/*
* Copyright (c) 2024 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#include "HTTPSRequest.h"
#if CHIP_HAVE_CONFIG_H
#include <crypto/CryptoBuildConfig.h>
#endif // CHIP_HAVE_CONFIG_H
#include <lib/support/Base64.h>
#include <lib/support/SafeInt.h>
#include <lib/support/ScopedBuffer.h>
#include <lib/support/logging/CHIPLogging.h>
#include <system/SystemError.h>
#ifdef CONFIG_ENABLE_HTTPS_REQUESTS
#if (CHIP_CRYPTO_OPENSSL || CHIP_CRYPTO_BORINGSSL)
#include <netdb.h>
#include <openssl/ssl.h>
#include <unistd.h>
#ifdef SHA256_DIGEST_LENGTH
#define USE_CHIP_CRYPTO 1
#endif
#endif //(CHIP_CRYPTO_OPENSSL || CHIP_CRYPTO_BORINGSSL)
#endif // CONFIG_ENABLE_HTTPS_REQUESTS
namespace {
constexpr const char * kHttpsPrefix = "https://";
constexpr uint16_t kHttpsPort = 443;
constexpr const char * kErrorJsonParse = "Failed to parse JSON: ";
constexpr const char * kErrorHTTPSPrefix = "URL must start with 'https://': ";
constexpr const char * kErrorHTTPSPort = "Invalid port: 0";
constexpr const char * kErrorHTTPSHostName = "Invalid hostname: empty";
constexpr const char * kErrorBase64Decode = "Error while decoding base64 data";
constexpr const char * kErrorSizeMismatch = "The response size does not match the expected size: ";
} // namespace
namespace chip {
namespace tool {
namespace https {
namespace {
#ifndef USE_CHIP_CRYPTO
/**
* @brief Stub implementation of HTTPSSessionHolder when neither OpenSSL nor BoringSSL is enabled.
*
* This class provides placeholder methods that log errors indicating the lack of SSL library support
* and encourages contributions for new implementations.
*/
class HTTPSSessionHolder
{
public:
CHIP_ERROR Init(std::string & hostname, uint16_t port, HttpsSecurityMode securityMode) { return LogNotImplementedError(); }
CHIP_ERROR SendRequest(std::string & request) { return LogNotImplementedError(); }
CHIP_ERROR ReceiveResponse(std::string & response) { return LogNotImplementedError(); }
private:
CHIP_ERROR LogNotImplementedError() const
{
#ifndef CONFIG_ENABLE_HTTPS_REQUESTS
ChipLogError(chipTool, "HTTPS requests are disabled via build configuration (config_enable_https_requests=false).");
#elif !(CHIP_CRYPTO_OPENSSL || CHIP_CRYPTO_BORINGSSL)
ChipLogError(chipTool,
"HTTPS requests are not available because neither OpenSSL nor BoringSSL is enabled. Contributions for "
"alternative implementations are welcome!");
#elif !defined(SHA256_DIGEST_LENGTH)
ChipLogError(chipTool,
"HTTPS requests are not available because SHA256_DIGEST_LENGTH is not defined, meaning response integrity "
"verification via SHA-256 digest checking cannot be performed.");
#endif
return CHIP_ERROR_NOT_IMPLEMENTED;
}
};
#else // USE_CHIP_CRYPTO
constexpr uint16_t kResponseBufferSize = 4096;
constexpr const char * kErrorSendHTTPRequest = "Failed to send HTTP request";
constexpr const char * kErrorReceiveHTTPResponse = "Failed to read HTTP response";
constexpr const char * kErrorConnection = "Failed to connect to: ";
constexpr const char * kErrorSSLContextCreate = "Failed to create SSL context";
constexpr const char * kErrorSSLObjectCreate = "Failed to create SSL object";
constexpr const char * kErrorSSLHandshake = "SSL handshake failed";
constexpr const char * kErrorDigestMismatch = "The response digest does not match the expected digest";
class AddressInfoHolder
{
public:
AddressInfoHolder(std::string & hostname, uint16_t port)
{
struct addrinfo hints = {};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
int err = getaddrinfo(hostname.c_str(), std::to_string(port).c_str(), &hints, &mRes);
#if CHIP_ERROR_LOGGING
constexpr const char * kErrorGetAddressInfo = "Failed to get address info: ";
VerifyOrDo(nullptr != mRes, ChipLogError(chipTool, "%s%s", kErrorGetAddressInfo, gai_strerror(err)));
#else
(void) err;
#endif
}
~AddressInfoHolder()
{
if (mRes != nullptr)
{
freeaddrinfo(mRes);
}
}
bool HasInfo() const { return mRes != nullptr; }
struct addrinfo * Get() const { return mRes; }
private:
struct addrinfo * mRes = nullptr;
};
class HTTPSSessionHolder
{
public:
HTTPSSessionHolder(){};
~HTTPSSessionHolder()
{
if (nullptr != mContext)
{
SSL_free(mSSL);
SSL_CTX_free(mContext);
}
if (mSock >= 0)
{
close(mSock);
}
#if !defined(OPENSSL_IS_BORINGSSL)
EVP_cleanup();
#endif
}
CHIP_ERROR Init(std::string & hostname, uint16_t port, HttpsSecurityMode securityMode)
{
int sock;
VerifyOrReturnError(securityMode == HttpsSecurityMode::kDefault, CHIP_ERROR_NOT_IMPLEMENTED);
ReturnErrorOnFailure(InitSocket(hostname, port, sock));
ReturnErrorOnFailure(InitSSL(sock));
return CHIP_NO_ERROR;
}
CHIP_ERROR SendRequest(std::string & request)
{
int written = SSL_write(mSSL, request.c_str(), (int) request.size());
VerifyOrReturnError(written > 0, CHIP_ERROR_BAD_REQUEST, ChipLogError(chipTool, "%s", kErrorSendHTTPRequest));
return CHIP_NO_ERROR;
}
CHIP_ERROR ReceiveResponse(std::string & response)
{
char buffer[kResponseBufferSize];
ssize_t n = -1;
while ((n = SSL_read(mSSL, buffer, sizeof(buffer))) > 0)
{
VerifyOrReturnError(CanCastTo<size_t>(n), CHIP_ERROR_INVALID_ARGUMENT);
response.append(buffer, static_cast<size_t>(n));
}
VerifyOrReturnError(n >= 0, CHIP_ERROR_INTERNAL, ChipLogError(chipTool, "%s", kErrorReceiveHTTPResponse));
return CHIP_NO_ERROR;
}
private:
CHIP_ERROR InitSocket(std::string & hostname, uint16_t port, int & sock)
{
AddressInfoHolder addressInfoHolder(hostname, port);
VerifyOrReturnError(addressInfoHolder.HasInfo(), CHIP_ERROR_NOT_CONNECTED);
auto * res = addressInfoHolder.Get();
for (struct addrinfo * p = res; p != nullptr; p = p->ai_next)
{
sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sock < 0)
{
continue; // Try the next address
}
if (connect(sock, p->ai_addr, p->ai_addrlen) != 0)
{
close(sock);
sock = -1;
continue; // Try the next address
}
return CHIP_NO_ERROR;
}
ChipLogError(chipTool, "%s%s:%u", kErrorConnection, hostname.c_str(), port);
return CHIP_ERROR_NOT_CONNECTED;
}
CHIP_ERROR InitSSL(int sock)
{
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();
auto * context = SSL_CTX_new(TLS_client_method());
VerifyOrReturnError(nullptr != context, CHIP_ERROR_NOT_CONNECTED, ChipLogError(chipTool, "%s", kErrorSSLContextCreate));
auto * ssl = SSL_new(context);
VerifyOrReturnError(nullptr != ssl, CHIP_ERROR_NOT_CONNECTED, ChipLogError(chipTool, "%s", kErrorSSLObjectCreate));
SSL_set_fd(ssl, sock);
VerifyOrReturnError(SSL_connect(ssl) > 0, CHIP_ERROR_NOT_CONNECTED, ChipLogError(chipTool, "%s", kErrorSSLHandshake));
mContext = context;
mSSL = ssl;
mSock = sock;
return CHIP_NO_ERROR;
}
SSL_CTX * mContext = nullptr;
SSL * mSSL = nullptr;
int mSock = -1;
};
#endif // USE_CHIP_CRYPTO
std::string BuildRequest(std::string & hostname, std::string & path)
{
return "GET " + path + " HTTP/1.1\r\n" + //
"Host: " + hostname + "\r\n" + //
"Accept: application/json\r\n" + //
"Connection: close\r\n\r\n"; //
}
CHIP_ERROR RemoveHeader(std::string & response)
{
size_t headerEnd = response.find("\r\n\r\n");
VerifyOrReturnError(std::string::npos != headerEnd, CHIP_ERROR_INVALID_ARGUMENT);
auto body = response.substr(headerEnd + 4);
response = body;
return CHIP_NO_ERROR;
}
CHIP_ERROR MaybeCheckResponseSize(const std::string & response, const chip::Optional<uint32_t> & optionalExpectedSize)
{
VerifyOrReturnError(optionalExpectedSize.HasValue(), CHIP_NO_ERROR);
VerifyOrReturnError(chip::CanCastTo<uint32_t>(response.size()), CHIP_ERROR_INVALID_ARGUMENT);
uint32_t responseSize = static_cast<uint32_t>(response.size());
uint32_t expectedSize = optionalExpectedSize.Value();
VerifyOrReturnError(expectedSize == responseSize, CHIP_ERROR_INVALID_ARGUMENT,
ChipLogError(chipTool, "%s%u != %u", kErrorSizeMismatch, responseSize, expectedSize));
return CHIP_NO_ERROR;
}
CHIP_ERROR MaybeCheckResponseDigest(const std::string & response, const chip::Optional<const char *> & optionalExpectedDigest)
{
VerifyOrReturnError(optionalExpectedDigest.HasValue(), CHIP_NO_ERROR);
VerifyOrReturnError(CanCastTo<uint16_t>(strlen(optionalExpectedDigest.Value())), CHIP_ERROR_INVALID_ARGUMENT);
const char * encodedData = optionalExpectedDigest.Value();
uint16_t encodedDataSize = static_cast<uint16_t>(strlen(encodedData));
size_t expectedMaxDecodedSize = BASE64_MAX_DECODED_LEN(encodedDataSize);
chip::Platform::ScopedMemoryBuffer<uint8_t> decodedData;
VerifyOrReturnError(decodedData.Calloc(expectedMaxDecodedSize + 1 /* for null */), CHIP_ERROR_INVALID_ARGUMENT);
size_t decodedDataSize = chip::Base64Decode(encodedData, encodedDataSize, decodedData.Get());
VerifyOrReturnError(0 != decodedDataSize, CHIP_ERROR_INVALID_ARGUMENT, ChipLogError(chipTool, "%s", kErrorBase64Decode));
#ifdef USE_CHIP_CRYPTO
// Compute the SHA-256 hash of the response
unsigned char responseDigest[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char *>(response.c_str()), response.size(), responseDigest);
VerifyOrReturnError(memcmp(responseDigest, decodedData.Get(), SHA256_DIGEST_LENGTH) == 0, CHIP_ERROR_INVALID_ARGUMENT,
ChipLogError(chipTool, "%s", kErrorDigestMismatch));
#else
return CHIP_ERROR_NOT_IMPLEMENTED;
#endif // USE_CHIP_CRYPTO
return CHIP_NO_ERROR;
}
CHIP_ERROR ConvertResponseToJSON(std::string & body, Json::Value & jsonResponse)
{
Json::CharReaderBuilder readerBuilder;
std::string errors;
std::istringstream jsonStream(body);
bool success = Json::parseFromStream(readerBuilder, jsonStream, &jsonResponse, &errors);
VerifyOrReturnError(success, CHIP_ERROR_INTERNAL, ChipLogError(chipTool, "%s%s", kErrorJsonParse, errors.c_str()));
return CHIP_NO_ERROR;
}
CHIP_ERROR ExtractHostAndPath(const std::string & url, std::string & hostAndPort, std::string & outPath)
{
VerifyOrReturnError(url.compare(0, strlen(kHttpsPrefix), kHttpsPrefix) == 0, CHIP_ERROR_INVALID_ARGUMENT,
ChipLogError(chipTool, "%s%s", kErrorHTTPSPrefix, url.c_str()));
auto strippedUrl = url.substr(strlen(kHttpsPrefix));
VerifyOrReturnError("" != strippedUrl, CHIP_ERROR_INVALID_ARGUMENT, ChipLogError(chipTool, "%s", kErrorHTTPSHostName));
size_t position = strippedUrl.find('/');
if (position == std::string::npos)
{
hostAndPort = strippedUrl;
outPath = "/";
}
else
{
hostAndPort = strippedUrl.substr(0, position);
outPath = strippedUrl.substr(position);
}
return CHIP_NO_ERROR;
}
CHIP_ERROR ExtractHostAndPort(const std::string & hostAndPort, std::string & outHostName, uint16_t & outPort)
{
size_t position = hostAndPort.find(':');
if (position == std::string::npos)
{
outHostName = hostAndPort;
outPort = kHttpsPort;
}
else
{
outHostName = hostAndPort.substr(0, position);
auto portString = hostAndPort.substr(position + 1);
outPort = static_cast<uint16_t>(std::atoi(portString.c_str()));
VerifyOrReturnError(0 != outPort, CHIP_ERROR_INVALID_ARGUMENT, ChipLogError(chipTool, "%s", kErrorHTTPSPort));
}
return CHIP_NO_ERROR;
}
CHIP_ERROR ExtractHostNamePortPath(std::string url, std::string & outHostName, uint16_t & outPort, std::string & outPath)
{
std::string hostAndPort;
ReturnErrorOnFailure(ExtractHostAndPath(url, hostAndPort, outPath));
ReturnErrorOnFailure(ExtractHostAndPort(hostAndPort, outHostName, outPort));
return CHIP_NO_ERROR;
}
} // namespace
CHIP_ERROR Request(std::string url, Json::Value & jsonResponse, const Optional<uint32_t> & optionalExpectedSize,
const Optional<const char *> & optionalExpectedDigest, HttpsSecurityMode securityMode)
{
std::string hostname;
uint16_t port;
std::string path;
ReturnErrorOnFailure(ExtractHostNamePortPath(url, hostname, port, path));
return Request(hostname, port, path, jsonResponse, optionalExpectedSize, optionalExpectedDigest, securityMode);
}
CHIP_ERROR Request(std::string hostname, uint16_t port, std::string path, Json::Value & jsonResponse,
const Optional<uint32_t> & optionalExpectedSize, const Optional<const char *> & optionalExpectedDigest,
HttpsSecurityMode securityMode)
{
VerifyOrDo(port != 0, port = kHttpsPort);
ChipLogDetail(chipTool, "HTTPS request to %s:%u%s", hostname.c_str(), port, path.c_str());
std::string request = BuildRequest(hostname, path);
std::string response;
HTTPSSessionHolder session;
ReturnErrorOnFailure(session.Init(hostname, port, securityMode));
ReturnErrorOnFailure(session.SendRequest(request));
ReturnErrorOnFailure(session.ReceiveResponse(response));
ReturnErrorOnFailure(RemoveHeader(response));
ReturnErrorOnFailure(MaybeCheckResponseSize(response, optionalExpectedSize));
ReturnErrorOnFailure(MaybeCheckResponseDigest(response, optionalExpectedDigest));
ReturnErrorOnFailure(ConvertResponseToJSON(response, jsonResponse));
return CHIP_NO_ERROR;
}
} // namespace https
} // namespace tool
} // namespace chip