| /* |
| * Copyright 2026-present Alibaba Inc. |
| * |
| * 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 "paimon/rest/mock_rest_server.h" |
| |
| #include <netinet/in.h> |
| #include <sys/socket.h> |
| #include <sys/time.h> |
| #include <unistd.h> |
| |
| #include <cerrno> |
| #include <cstring> |
| #include <utility> |
| #include <vector> |
| |
| #include "fmt/format.h" |
| #include "paimon/common/utils/string_utils.h" |
| #include "paimon/common/utils/url_utils.h" |
| |
| namespace paimon { |
| |
| namespace { |
| |
| bool ReceiveAll(int32_t fd, std::string* buffer, size_t min_size) { |
| char chunk[4096]; |
| while (buffer->size() < min_size) { |
| ssize_t received = ::recv(fd, chunk, sizeof(chunk), 0); |
| if (received <= 0) { |
| return false; |
| } |
| buffer->append(chunk, static_cast<size_t>(received)); |
| } |
| return true; |
| } |
| |
| std::map<std::string, std::string> ParseQuery(const std::string& query) { |
| std::map<std::string, std::string> params; |
| for (const std::string& pair : StringUtils::Split(query, "&", /*ignore_empty=*/true)) { |
| size_t eq = pair.find('='); |
| if (eq == std::string::npos) { |
| params[UrlUtils::DecodeString(pair)] = ""; |
| } else { |
| params[UrlUtils::DecodeString(pair.substr(0, eq))] = |
| UrlUtils::DecodeString(pair.substr(eq + 1)); |
| } |
| } |
| return params; |
| } |
| |
| } // namespace |
| |
| MockRestServer::MockRestServer(Handler handler, int32_t listen_fd, int32_t port) |
| : handler_(std::move(handler)), listen_fd_(listen_fd), port_(port) { |
| accept_thread_ = std::thread([this] { AcceptLoop(); }); |
| } |
| |
| Result<std::unique_ptr<MockRestServer>> MockRestServer::Start(Handler handler) { |
| int32_t listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); |
| if (listen_fd < 0) { |
| return Status::IOError("mock rest server: failed to create socket: ", std::strerror(errno)); |
| } |
| int32_t reuse = 1; |
| ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); |
| struct sockaddr_in address; |
| std::memset(&address, 0, sizeof(address)); |
| address.sin_family = AF_INET; |
| address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| address.sin_port = 0; |
| if (::bind(listen_fd, reinterpret_cast<struct sockaddr*>(&address), sizeof(address)) < 0) { |
| ::close(listen_fd); |
| return Status::IOError("mock rest server: failed to bind: ", std::strerror(errno)); |
| } |
| if (::listen(listen_fd, 16) < 0) { |
| ::close(listen_fd); |
| return Status::IOError("mock rest server: failed to listen: ", std::strerror(errno)); |
| } |
| socklen_t address_len = sizeof(address); |
| if (::getsockname(listen_fd, reinterpret_cast<struct sockaddr*>(&address), &address_len) < 0) { |
| ::close(listen_fd); |
| return Status::IOError("mock rest server: failed to get port: ", std::strerror(errno)); |
| } |
| int32_t port = ntohs(address.sin_port); |
| return std::unique_ptr<MockRestServer>(new MockRestServer(std::move(handler), listen_fd, port)); |
| } |
| |
| MockRestServer::~MockRestServer() { |
| Stop(); |
| } |
| |
| void MockRestServer::Stop() { |
| if (stopped_.exchange(true)) { |
| return; |
| } |
| // `shutdown` wakes the blocked `accept`; the fd is closed only after the accept |
| // thread has joined, so it cannot be recycled by another thread while the accept |
| // thread might still use it. |
| ::shutdown(listen_fd_, SHUT_RDWR); |
| if (accept_thread_.joinable()) { |
| accept_thread_.join(); |
| } |
| ::close(listen_fd_); |
| } |
| |
| std::string MockRestServer::GetBaseUri() const { |
| return fmt::format("http://127.0.0.1:{}", port_); |
| } |
| |
| void MockRestServer::AcceptLoop() { |
| while (!stopped_.load()) { |
| int32_t connection_fd = ::accept(listen_fd_, nullptr, nullptr); |
| if (connection_fd < 0) { |
| if (stopped_.load()) { |
| return; |
| } |
| continue; |
| } |
| // The connection is handled on the accept thread; the socket timeouts keep a |
| // wedged peer from blocking `Stop()` indefinitely. |
| struct timeval timeout; |
| timeout.tv_sec = 30; |
| timeout.tv_usec = 0; |
| ::setsockopt(connection_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); |
| ::setsockopt(connection_fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); |
| HandleConnection(connection_fd); |
| ::close(connection_fd); |
| } |
| } |
| |
| void MockRestServer::HandleConnection(int32_t connection_fd) { |
| std::string buffer; |
| size_t header_end; |
| while ((header_end = buffer.find("\r\n\r\n")) == std::string::npos) { |
| if (!ReceiveAll(connection_fd, &buffer, buffer.size() + 1)) { |
| return; |
| } |
| } |
| |
| Request request; |
| std::string header_part = buffer.substr(0, header_end); |
| std::vector<std::string> lines = StringUtils::Split(header_part, "\r\n", |
| /*ignore_empty=*/true); |
| if (lines.empty()) { |
| return; |
| } |
| std::vector<std::string> request_line = StringUtils::Split(lines[0], " ", |
| /*ignore_empty=*/true); |
| if (request_line.size() < 2) { |
| return; |
| } |
| request.method = request_line[0]; |
| std::string target = request_line[1]; |
| size_t question = target.find('?'); |
| if (question == std::string::npos) { |
| request.path = UrlUtils::DecodeString(target); |
| } else { |
| request.path = UrlUtils::DecodeString(target.substr(0, question)); |
| request.query_params = ParseQuery(target.substr(question + 1)); |
| } |
| size_t content_length = 0; |
| for (size_t i = 1; i < lines.size(); i++) { |
| size_t colon = lines[i].find(':'); |
| if (colon == std::string::npos) { |
| continue; |
| } |
| std::string name = lines[i].substr(0, colon); |
| std::string value = lines[i].substr(colon + 1); |
| StringUtils::Trim(&name); |
| StringUtils::Trim(&value); |
| request.headers[StringUtils::ToLowerCase(name)] = value; |
| } |
| auto length_iter = request.headers.find("content-length"); |
| if (length_iter != request.headers.end()) { |
| content_length = |
| static_cast<size_t>(std::strtoul(length_iter->second.c_str(), nullptr, 10)); |
| } |
| size_t body_begin = header_end + 4; |
| if (!ReceiveAll(connection_fd, &buffer, body_begin + content_length)) { |
| return; |
| } |
| request.body = buffer.substr(body_begin, content_length); |
| |
| Response response = handler_(request); |
| if (response.close_without_response) { |
| return; |
| } |
| std::string extra_headers; |
| for (const auto& [name, value] : response.headers) { |
| extra_headers += fmt::format("{}: {}\r\n", name, value); |
| } |
| std::string payload = fmt::format( |
| "HTTP/1.1 {} MOCK\r\nContent-Type: {}\r\nContent-Length: {}\r\n{}Connection: " |
| "close\r\n\r\n{}", |
| response.code, response.content_type, response.body.size() + response.missing_body_bytes, |
| extra_headers, response.body); |
| size_t sent = 0; |
| while (sent < payload.size()) { |
| ssize_t written = ::send(connection_fd, payload.data() + sent, payload.size() - sent, 0); |
| if (written <= 0) { |
| return; |
| } |
| sent += static_cast<size_t>(written); |
| } |
| } |
| |
| } // namespace paimon |