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