xref: /haiku/src/tests/kits/net/netservices2/TestServer.cpp (revision 52c4471a3024d2eb81fe88e2c3982b9f8daa5e56)
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 
24 namespace {
25 
26 
27 template<typename T>
28 std::string
29 to_string(T value)
30 {
31 	std::ostringstream s;
32 	s << value;
33 	return s.str();
34 }
35 
36 
37 void
38 exec(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.
53 std::string
54 TestFilePath(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 
67 RandomTCPServerPort::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 
127 RandomTCPServerPort::~RandomTCPServerPort()
128 {
129 	if (fSocketFd != -1) {
130 		::close(fSocketFd);
131 		fSocketFd = -1;
132 		fInitStatus = B_NOT_INITIALIZED;
133 	}
134 }
135 
136 
137 status_t
138 RandomTCPServerPort::InitCheck() const
139 {
140 	return fInitStatus;
141 }
142 
143 
144 int
145 RandomTCPServerPort::FileDescriptor() const
146 {
147 	return fSocketFd;
148 }
149 
150 
151 uint16_t
152 RandomTCPServerPort::Port() const
153 {
154 	return fServerPort;
155 }
156 
157 
158 ChildProcess::ChildProcess()
159 	:
160 	fChildPid(-1)
161 {
162 }
163 
164 
165 ChildProcess::~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.
180 status_t
181 ChildProcess::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 
211 TestServer::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.
220 status_t
221 TestServer::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 
257 BUrl
258 TestServer::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.
279 status_t
280 TestProxyServer::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 
311 uint16_t
312 TestProxyServer::Port() const
313 {
314 	return fPort.Port();
315 }
316