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