1/*
2 * Copyright 2020 Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *   Kyle Ambroff-Kao, kyle@ambroffkao.com
7 */
8#include "TestServer.h"
9
10#include <errno.h>
11#include <netinet/in.h>
12#include <posix/libgen.h>
13#include <sstream>
14#include <string>
15#include <sys/socket.h>
16#include <sys/wait.h>
17#include <unistd.h>
18#include <vector>
19
20#include <AutoDeleter.h>
21#include <TestShell.h>
22
23
24namespace {
25
26
27template<typename T>
28std::string
29to_string(T value)
30{
31	std::ostringstream s;
32	s << value;
33	return s.str();
34}
35
36
37void
38exec(const std::vector<std::string>& args)
39{
40	const char** argv = new const char*[args.size() + 1];
41	ArrayDeleter<const char*> _(argv);
42
43	for (size_t i = 0; i < args.size(); ++i) {
44		argv[i] = args[i].c_str();
45	}
46	argv[args.size()] = NULL;
47
48	execv(args[0].c_str(), const_cast<char* const*>(argv));
49}
50
51
52// Return the path of a file path relative to this source file.
53std::string
54TestFilePath(const std::string& relativePath)
55{
56	char* testFileSource = strdup(__FILE__);
57	MemoryDeleter _(testFileSource);
58
59	std::string testSrcDir(::dirname(testFileSource));
60
61	return testSrcDir + "/" + relativePath;
62}
63
64} // namespace
65
66
67RandomTCPServerPort::RandomTCPServerPort()
68	:
69	fInitStatus(B_NOT_INITIALIZED),
70	fSocketFd(-1),
71	fServerPort(0)
72{
73	// Create socket with port 0 to get an unused one selected by the
74	// kernel.
75	int socket_fd = ::socket(AF_INET, SOCK_STREAM, 0);
76	if (socket_fd == -1) {
77		fprintf(stderr, "ERROR: Unable to create socket: %s\n", strerror(errno));
78		fInitStatus = B_ERROR;
79		return;
80	}
81
82	fSocketFd = socket_fd;
83
84	// We may quickly reclaim the same socket between test runs, so allow
85	// for reuse.
86	{
87		int reuse = 1;
88		int result = ::setsockopt(socket_fd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse));
89		if (result == -1) {
90			fInitStatus = errno;
91			fprintf(stderr, "ERROR: Unable to set socket options on fd %d: %s\n", socket_fd,
92				strerror(fInitStatus));
93			return;
94		}
95	}
96
97	// Bind to loopback 127.0.0.1
98	struct sockaddr_in server_address;
99	server_address.sin_family = AF_INET;
100	server_address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
101	int bind_result = ::bind(
102		socket_fd, reinterpret_cast<struct sockaddr*>(&server_address), sizeof(server_address));
103	if (bind_result == -1) {
104		fInitStatus = errno;
105		fprintf(stderr, "ERROR: Unable to bind to loopback interface: %s\n", strerror(fInitStatus));
106		return;
107	}
108
109	// Listen is apparently required before getsockname will work.
110	if (::listen(socket_fd, 32) == -1) {
111		fInitStatus = errno;
112		fprintf(stderr, "ERROR: listen() failed: %s\n", strerror(fInitStatus));
113
114		return;
115	}
116
117	// Now get the port from the socket.
118	socklen_t server_address_length = sizeof(server_address);
119	::getsockname(
120		socket_fd, reinterpret_cast<struct sockaddr*>(&server_address), &server_address_length);
121	fServerPort = ntohs(server_address.sin_port);
122
123	fInitStatus = B_OK;
124}
125
126
127RandomTCPServerPort::~RandomTCPServerPort()
128{
129	if (fSocketFd != -1) {
130		::close(fSocketFd);
131		fSocketFd = -1;
132		fInitStatus = B_NOT_INITIALIZED;
133	}
134}
135
136
137status_t
138RandomTCPServerPort::InitCheck() const
139{
140	return fInitStatus;
141}
142
143
144int
145RandomTCPServerPort::FileDescriptor() const
146{
147	return fSocketFd;
148}
149
150
151uint16_t
152RandomTCPServerPort::Port() const
153{
154	return fServerPort;
155}
156
157
158ChildProcess::ChildProcess()
159	:
160	fChildPid(-1)
161{
162}
163
164
165ChildProcess::~ChildProcess()
166{
167	if (fChildPid != -1) {
168		::kill(fChildPid, SIGTERM);
169
170		pid_t result = -1;
171		while (result != fChildPid) {
172			result = ::waitpid(fChildPid, NULL, 0);
173		}
174	}
175}
176
177
178// The job of this method is to spawn a child process that will later be killed
179// by the destructor.
180status_t
181ChildProcess::Start(const std::vector<std::string>& args)
182{
183	if (fChildPid != -1) {
184		return B_ALREADY_RUNNING;
185	}
186
187	pid_t child = ::fork();
188	if (child < 0)
189		return B_ERROR;
190
191	if (child > 0) {
192		fChildPid = child;
193		return B_OK;
194	}
195
196	// This is the child process. We can exec image provided in args.
197	exec(args);
198
199	// If we reach this point we failed to load the Python image.
200	std::ostringstream ostr;
201
202	for (std::vector<std::string>::const_iterator iter = args.begin(); iter != args.end(); ++iter) {
203		ostr << " " << *iter;
204	}
205
206	fprintf(stderr, "Unable to spawn `%s': %s\n", ostr.str().c_str(), strerror(errno));
207	exit(1);
208}
209
210
211TestServer::TestServer(TestServerMode mode)
212	:
213	fMode(mode)
214{
215}
216
217
218// Start a child testserver.py process with the random TCP port chosen by
219// fPort.
220status_t
221TestServer::Start()
222{
223	if (fPort.InitCheck() != B_OK) {
224		return fPort.InitCheck();
225	}
226
227	auto testFilePath = TestFilePath("testserver.py");
228	if (::access(testFilePath.data(), R_OK) != 0) {
229		fprintf(stderr, "ERROR: No access to the test server script at: %s\n", testFilePath.data());
230		return B_IO_ERROR;
231	}
232
233	// This is the child process. We can exec the server process.
234	std::vector<std::string> child_process_args;
235	child_process_args.push_back("/bin/python3");
236	child_process_args.push_back(testFilePath);
237	child_process_args.push_back("--port");
238	child_process_args.push_back(to_string(fPort.Port()));
239	child_process_args.push_back("--fd");
240	child_process_args.push_back(to_string(fPort.FileDescriptor()));
241
242	if (fMode == TestServerMode::Https) {
243		child_process_args.push_back("--use-tls");
244	}
245
246	// After this the child process has started. It may take a short amount of
247	// time before the child process is ready to call accept(), but that's OK.
248	//
249	// Since the socket has already been created above, the tests will not
250	// get ECONNREFUSED and will block until the child process calls
251	// accept(). So we don't have to busy loop here waiting for a
252	// connection to the child.
253	return fChildProcess.Start(child_process_args);
254}
255
256
257BUrl
258TestServer::BaseUrl() const
259{
260	std::string scheme;
261	switch (fMode) {
262		case TestServerMode::Http:
263			scheme = "http://";
264			break;
265
266		case TestServerMode::Https:
267			scheme = "https://";
268			break;
269	}
270
271	std::string port_string = to_string(fPort.Port());
272
273	std::string baseUrl = scheme + "127.0.0.1:" + port_string + "/";
274	return BUrl(baseUrl.c_str());
275}
276
277
278// Start a child proxy.py process using the random TCP port chosen by fPort.
279status_t
280TestProxyServer::Start()
281{
282	if (fPort.InitCheck() != B_OK) {
283		return fPort.InitCheck();
284	}
285
286	auto testFilePath = TestFilePath("proxy.py");
287	if (::access(testFilePath.data(), R_OK) != 0) {
288		fprintf(stderr, "ERROR: No access to the test server script at: %s\n", testFilePath.data());
289		return B_IO_ERROR;
290	}
291
292	std::vector<std::string> child_process_args;
293	child_process_args.push_back("/bin/python3");
294	child_process_args.push_back(testFilePath);
295	child_process_args.push_back("--port");
296	child_process_args.push_back(to_string(fPort.Port()));
297	child_process_args.push_back("--fd");
298	child_process_args.push_back(to_string(fPort.FileDescriptor()));
299
300	// After this the child process has started. It may take a short amount of
301	// time before the child process is ready to call accept(), but that's OK.
302	//
303	// Since the socket has already been created above, the tests will not
304	// get ECONNREFUSED and will block until the child process calls
305	// accept(). So we don't have to busy loop here waiting for a
306	// connection to the child.
307	return fChildProcess.Start(child_process_args);
308}
309
310
311uint16_t
312TestProxyServer::Port() const
313{
314	return fPort.Port();
315}
316