1 /*
2 Copyright 2019 Google LLC
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 https://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15 */
16
17 #include <getopt.h>
18 #include <sys/resource.h>
19 #include <unistd.h>
20
21 #include <cstring>
22 #include <ctime>
23 #include <fstream>
24 #include <iostream>
isChar(const char c,const absl::string_view match)25 #include <sstream>
26 #include <string>
27 #include <vector>
28
29 #include "absl/strings/numbers.h"
30 #include "absl/strings/str_cat.h"
31 #include "absl/strings/str_split.h"
32 #include "apib/apib_cpu.h"
33 #include "apib/apib_iothread.h"
34 #include "apib/apib_oauth.h"
35 #include "apib/apib_reporting.h"
36 #include "apib/apib_url.h"
37 #include "apib/apib_util.h"
38 #include "third_party/base64.h"
39
40 static const std::string kApibVersion = "1.2";
41
42 using apib::eqcase;
43 using apib::IOThread;
LineState(const std::string & s)44 using apib::OAuthInfo;
45 using apib::RecordInit;
46 using apib::RecordStart;
47 using apib::RecordStop;
48 using apib::ReportInterval;
49 using apib::URLInfo;
50 using std::cerr;
51 using std::cout;
52 using std::endl;
53
54 static const int KeepAliveAlways = -1;
LineState(size_t len)55 static const int DefaultNumConnections = 1;
56 static const int DefaultDuration = 60;
57 static const int DefaultWarmup = 0;
58 static const int ReportSleepTime = 5;
59
60 static int ShortOutput = 0;
61 static std::string RunName;
62 static int NumConnections = DefaultNumConnections;
63 static int NumThreads = -1;
64 static bool JustOnce = false;
~LineState()65 static int KeepAlive = KeepAliveAlways;
66 static std::string Verb;
clear()67 static std::string FileName;
68 static std::string ContentType;
69 static std::string SslCipher;
70 static bool SslVerify = false;
71 static std::string SslCertificate;
72 static bool Verbose = false;
73 static int ThinkTime = 0;
nullLast()74 static std::vector<std::string> Headers;
75 static int SetHeaders = 0;
76
77 static OAuthInfo *OAuth = nullptr;
78
next()79 static const char *const OPTIONS = "c:d:f:hk:t:u:vw:x:C:F:H:O:K:M:X:N:STVW:Z1";
80
81 static const struct option Options[] = {
82 {"concurrency", required_argument, NULL, 'c'},
83 {"duration", required_argument, NULL, 'd'},
84 {"input-file", required_argument, NULL, 'f'},
85 {"help", no_argument, NULL, 'h'},
86 {"keep-alive", required_argument, NULL, 'k'},
87 {"content-type", required_argument, NULL, 't'},
88 {"username-password", required_argument, NULL, 'u'},
89 {"verbose", no_argument, NULL, 'v'},
90 {"version", no_argument, NULL, 'Z'},
91 {"warmup", required_argument, NULL, 'w'},
92 {"method", required_argument, NULL, 'x'},
93 {"cipherlist", required_argument, NULL, 'C'},
94 {"certificate", required_argument, NULL, 'F'},
95 {"header", required_argument, NULL, 'H'},
96 {"oauth", required_argument, NULL, 'O'},
97 {"iothreads", required_argument, NULL, 'K'},
98 {"monitor", required_argument, NULL, 'M'},
99 {"monitor2", required_argument, NULL, 'X'},
100 {"name", required_argument, NULL, 'N'},
101 {"csv-output", no_argument, NULL, 'S'},
102 {"header-line", no_argument, NULL, 'T'},
103 {"verify", no_argument, NULL, 'V'},
104 {"one", no_argument, NULL, '1'},
105 {"think-time", required_argument, NULL, 'W'}};
106
107 static const char *const USAGE_DOCS =
108 "-1 --one Send just one request and exit\n"
109 "-c --concurrency Number of concurrent requests (default 1)\n"
110 "-d --duration Test duration in seconds\n"
111 "-f --input-file File name to send on PUT and POST requests\n"
112 "-h --help Display this message\n"
113 "-k --keep-alive Keep-alive duration:\n"
114 " 0 to disable, non-zero for timeout\n"
115 "-t --content-type Value of the Content-Type header\n"
116 "-u --username-password Credentials for HTTP Basic authentication\n"
117 " in username:password format\n"
118 "-v --verbose Verbose output\n"
119 " --version Version information\n"
line() const120 "-w --warmup Warm-up duration, in seconds (default 0)\n"
121 "-x --method HTTP request method (default GET)\n"
122 "-C --cipherlist Cipher list offered to server for HTTPS\n"
123 "-F --certificate PEM file containing CA certificates to trust\n"
124 "-H --header HTTP header line in Name: Value format\n"
125 "-K --iothreads Number of I/O threads to spawn\n"
126 " default == number of CPU cores\n"
127 "-N --name Name to put in CSV output to identify test run\n"
128 "-O --oauth OAuth 1.0 signature\n"
129 " in format consumerkey:secret:token:secret\n"
130 "-S --csv-output Output all test results in a single CSV line\n"
131 "-T --header-line Do not run, but output a single CSV header line\n"
132 "-V --verify Verify TLS peer\n"
133 "-W --think-time Think time to wait in between requests\n"
134 " in milliseconds\n"
135 "-M --monitor Host name and port number of apibmon\n"
136 "-X --monitor2 Second host name and port number of apibmon\n"
137 "\n"
138 "The last argument may be an http or https URL, or an \"@\" symbol\n"
139 "followed by a file name. If a file name, then apib will read the file\n"
140 "as a list of URLs, one per line, and randomly test each one.\n"
141 "\n"
142 " if -S is used then output is CSV-separated on one line:\n"
143 " name,throughput,avg. "
144 "latency,threads,connections,duration,completed,successful,errors,sockets,"
145 "min. latency,max. latency,50%,90%,98%,99%\n"
146 "\n"
147 " if -O is used then the value is four parameters, separated by a colon:\n"
148 " consumer key:secret:token:secret. You may omit the last two.\n";
149
150 static void printUsage() {
151 cerr << "Usage: apib [options] [URL | @file]" << endl;
152 cerr << USAGE_DOCS << endl;
153 }
154
155 static void printLibraryInfo() {
156 cout << "apib " << kApibVersion << '\n';
skipMatches(const absl::string_view toks)157 cout << " (git commit $Id: 2f9addad586dc72dc81c3396fc5cee0f4d20ccb0 $)\n";
158 cout << "libev " << ev_version_major() << '.' << ev_version_minor() << '\n';
159 cout << " Supported backends: "
160 << IOThread::GetEvBackends(ev_supported_backends()) << '\n';
161 cout << " Recommended backends: "
162 << IOThread::GetEvBackends(ev_recommended_backends()) << '\n';
skip(int toSkip)163 cout << "http_parser " << HTTP_PARSER_VERSION_MAJOR << '.'
164 << HTTP_PARSER_VERSION_MINOR << '.' << HTTP_PARSER_VERSION_PATCH << '\n';
165 cout << OpenSSL_version(OPENSSL_VERSION) << '\n';
166 }
167
consume()168 static int setProcessLimits(int numConnections) {
169 struct rlimit limits;
170 int err;
171
172 getrlimit(RLIMIT_NOFILE, &limits);
173 if (numConnections < (int)limits.rlim_cur) {
174 return 0;
175 }
176 if (numConnections < (int)limits.rlim_max) {
177 limits.rlim_cur = limits.rlim_max;
178 err = setrlimit(RLIMIT_NOFILE, &limits);
179 if (err == 0) {
180 return 0;
181 } else {
182 cerr << "Error setting file descriptor limit: " << err << endl;
183 return -1;
184 }
185 } else {
186 cerr << "Current hard file descriptor limit is " << limits.rlim_max
187 << ": it is too low. Try sudo" << endl;
188 return -1;
189 }
190 }
191
192 static void sslInfoCallback(const SSL *ssl, int where, int ret) {
193 cout << "OpenSSL: " << SSL_state_string_long(ssl) << endl;
194 if (ret == SSL_CB_ALERT) {
195 cout << " alert: " << SSL_alert_desc_string_long(ret) << endl;
196 } else if (ret == 0) {
197 cout << " Error occurred" << endl;
198 }
199 if (where & SSL_CB_HANDSHAKE_DONE) {
getReadInfo(char ** buf,int * remaining) const200 int bits;
201 SSL_get_cipher_bits(ssl, &bits);
202 cout << " Protocol: " << SSL_get_cipher_version(ssl) << endl;
203 cout << " Cipher: " << SSL_get_cipher_name(ssl) << " (" << bits << " bits)"
204 << endl;
205 }
206 }
207
208 static int createSslContext(IOThread *t) {
writeRemaining(std::ostream & out) const209 t->sslCtx = SSL_CTX_new(TLS_client_method());
210 SSL_CTX_set_mode(t->sslCtx, SSL_MODE_ENABLE_PARTIAL_WRITE);
211 SSL_CTX_set_default_verify_paths(t->sslCtx);
212 if (SslVerify) {
213 SSL_CTX_set_verify(t->sslCtx, SSL_VERIFY_PEER, NULL);
debug(std::ostream & out) const214 }
215 if (!SslCertificate.empty()) {
216 int err =
217 SSL_CTX_load_verify_locations(t->sslCtx, SslCertificate.c_str(), NULL);
218 if (err != 1) {
219 cerr << "Could not load CA certificates from " << SslCertificate << endl;
220 return -2;
221 }
222 }
223 if (t->verbose) {
224 SSL_CTX_set_info_callback(t->sslCtx, sslInfoCallback);
225 }
226
227 if (!t->sslCipher.empty()) {
228 int res = SSL_CTX_set_cipher_list(t->sslCtx, t->sslCipher.c_str());
229 if (res != 1) {
230 cerr << "Set Cipher list to " << t->sslCipher << " failed" << endl;
231 return -1;
232 }
233 }
234 return 0;
235 }
236
237 static int readFile(const std::string &name, IOThread *t) {
238 // Open and seek to the end
239 std::ifstream in(name, std::ios::binary | std::ios::ate);
240 if (!in) {
241 cerr << "Cannot open input file " << name << endl;
242 return -1;
243 }
244
245 const auto size = in.tellg();
246 std::string buf(size, '\0');
247 in.seekg(0);
248 in.read(&buf[0], size);
249 t->sendData = std::move(buf);
250 return 0;
251 }
252
253 static void waitAndReport(const apib::ThreadList &threads, int duration,
254 bool warmup) {
255 int durationLeft = duration;
256 int toSleep;
257
258 while (durationLeft > 0) {
259 if (durationLeft < ReportSleepTime) {
260 toSleep = durationLeft;
261 } else {
262 toSleep = ReportSleepTime;
263 }
264
265 sleep(toSleep);
266 if (ShortOutput) {
267 apib::SampleCPU();
268 } else {
269 ReportInterval(std::cout, threads, duration, warmup);
270 }
271 durationLeft -= toSleep;
272 }
273 }
274
275 static void processOAuth(const absl::string_view arg) {
276 const std::vector<std::string> parts = absl::StrSplit(arg, ':');
277 OAuth = new OAuthInfo();
278 if (parts.size() > 0) {
279 OAuth->consumerKey = parts[0];
280 }
281 if (parts.size() > 1) {
282 OAuth->consumerSecret = parts[1];
283 }
284 if (parts.size() > 2) {
285 OAuth->accessToken = parts[2];
286 }
287 if (parts.size() > 3) {
288 OAuth->tokenSecret = parts[3];
289 }
290 SetHeaders |= IOThread::kAuthorizationSet;
291 }
292
293 static void addHeader(const absl::string_view val) {
294 const std::vector<std::string> parts = absl::StrSplit(val, ':');
295 if (parts.empty()) {
296 cerr << "Invalid header: " << val << endl;
297 return;
298 }
299
300 const std::string name = parts[0];
301 if (eqcase(name, "Host")) {
302 SetHeaders |= IOThread::kHostSet;
303 } else if (eqcase(name, "Content-Length")) {
304 SetHeaders |= IOThread::kContentLengthSet;
305 } else if (eqcase(name, "Content-Type")) {
306 SetHeaders |= IOThread::kContentTypeSet;
307 } else if (eqcase(name, "Authorization")) {
308 SetHeaders |= IOThread::kAuthorizationSet;
309 } else if (eqcase(name, "Connection")) {
310 SetHeaders |= IOThread::kConnectionSet;
311 } else if (eqcase(name, "User-Agent")) {
312 SetHeaders |= IOThread::kUserAgentSet;
313 }
314
315 Headers.push_back(std::string(val));
316 }
317
318 static void processBasic(const std::string &arg) {
319 // TODO more C++y
320 const int encLen = Base64encode_len(arg.size());
321 char *b64 = new char[encLen + 1];
322 Base64encode(b64, arg.c_str(), arg.size());
323
324 addHeader(absl::StrCat("Authorization: Basic ", b64));
325 }
326
327 static int initializeThread(int ix, IOThread *t) {
328 int numConn = NumConnections / NumThreads;
329 if (ix < (NumConnections % NumThreads)) {
330 numConn++;
331 }
332
333 if (!FileName.empty()) {
334 if (readFile(FileName, t) != 0) {
335 return 3;
336 }
337 }
338
339 if (Verb.empty()) {
340 if (FileName.empty()) {
341 t->httpVerb = "GET";
342 } else {
343 t->httpVerb = "POST";
344 }
345 } else {
346 t->httpVerb = Verb;
347 }
348
349 t->index = ix;
350 t->keepRunning = (JustOnce ? -1 : 1);
351 t->numConnections = numConn;
352 t->verbose = Verbose;
353 t->sslCipher = SslCipher;
354 t->headers = &Headers;
355 t->headersSet = SetHeaders;
356 t->thinkTime = ThinkTime;
357 t->noKeepAlive = (KeepAlive != KeepAliveAlways);
358 t->oauth = OAuth;
359
360 return createSslContext(t);
361 }
362
363 int main(int argc, char *const *argv) {
364 /* Arguments */
365 int duration = DefaultDuration;
366 int warmupTime = DefaultWarmup;
367 bool doHelp = false;
368 bool doVersion = false;
369 std::string url;
370 std::string monitorHost;
371 std::string monitor2Host;
372
373 bool failed = false;
374 int arg;
375 do {
376 arg = getopt_long(argc, argv, OPTIONS, Options, NULL);
377 switch (arg) {
378 case 'c':
379 if (!absl::SimpleAtoi(optarg, &NumConnections)) {
380 failed = true;
381 }
382 break;
383 case 'd':
384 if (!absl::SimpleAtoi(optarg, &duration)) {
385 failed = true;
386 }
387 break;
388 case 'f':
389 FileName = optarg;
390 break;
391 case 'h':
392 doHelp = true;
393 break;
394 case 'k':
395 if (!absl::SimpleAtoi(optarg, &KeepAlive)) {
396 failed = true;
397 }
398 break;
399 case 't':
400 ContentType = optarg;
401 break;
402 case 'u':
403 processBasic(optarg);
404 break;
405 case 'v':
406 Verbose = true;
407 break;
408 case 'w':
409 if (!absl::SimpleAtoi(optarg, &warmupTime)) {
410 failed = true;
411 }
412 break;
413 case 'x':
414 Verb = optarg;
415 break;
416 case 'Z':
417 doVersion = true;
418 break;
419 case 'C':
420 SslCipher = optarg;
421 break;
422 case 'F':
423 SslCertificate = optarg;
424 break;
425 case 'H':
426 addHeader(optarg);
427 break;
428 case 'K':
429 if (!absl::SimpleAtoi(optarg, &NumThreads)) {
430 failed = true;
431 }
432 break;
433 case 'M':
434 monitorHost = optarg;
435 break;
436 case 'X':
437 monitor2Host = optarg;
438 break;
439 case 'N':
440 RunName = optarg;
441 break;
442 case 'O':
443 processOAuth(optarg);
444 break;
445 case 'S':
446 ShortOutput = true;
447 break;
448 case 'T':
449 apib::PrintReportingHeader(std::cout);
450 return 0;
451 break;
452 case 'V':
453 SslVerify = true;
454 break;
455 case 'W':
456 if (!absl::SimpleAtoi(optarg, &ThinkTime)) {
457 failed = true;
458 }
459 break;
460 case '1':
461 JustOnce = true;
462 break;
463 case '?':
464 case ':':
465 // Unknown. Error was printed.
466 failed = true;
467 break;
468 case -1:
469 // Done!
470 break;
471 default:
472 cerr << "Internal error: Unknown option " << arg << endl;
473 std::abort();
474 break;
475 }
476 } while (arg >= 0);
477
478 if (doHelp) {
479 printUsage();
480 return 0;
481 } else if (doVersion) {
482 printLibraryInfo();
483 return 0;
484 }
485
486 if (!failed && (optind == (argc - 1))) {
487 url = argv[optind];
488 } else {
489 // No URL
490 failed = 1;
491 }
492
493 if (failed) {
494 printUsage();
495 return 1;
496 }
497
498 if (!ContentType.empty()) {
499 std::ostringstream hdr;
500 hdr << "Content-Type: " << ContentType;
501 addHeader(hdr.str());
502 }
503
504 if (!url.empty()) {
505 if (url[0] == '@') {
506 const auto s = URLInfo::InitFile(url.substr(1));
507 if (!s.ok()) {
508 cerr << "Error opening URL file: " << s << '\n';
509 goto finished;
510 }
511 } else {
512 const auto s = URLInfo::InitOne(url);
513 if (!s.ok()) {
514 cerr << s << '\n';
515 goto finished;
516 }
517 }
518
519 if (setProcessLimits(NumConnections) != 0) {
520 goto finished;
521 }
522
523 if (NumThreads < 1) {
524 NumThreads = apib::cpu_Count();
525 }
526 if (NumThreads > NumConnections) {
527 NumThreads = NumConnections;
528 }
529
530 if (Verbose) {
531 printLibraryInfo();
532 }
533
534 RecordInit(monitorHost, monitor2Host);
535
536 apib::ThreadList threads;
537 if (JustOnce) {
538 threads.push_back(std::unique_ptr<IOThread>(new IOThread()));
539 int err = initializeThread(0, threads[0].get());
540 if (err != 0) {
541 goto finished;
542 }
543 RecordStart(true, threads);
544 threads[0]->Start();
545 threads[0]->Join();
546 RecordStop(threads);
547
548 } else {
549 for (int i = 0; i < NumThreads; i++) {
550 threads.push_back(std::unique_ptr<IOThread>(new IOThread()));
551 int err = initializeThread(i, threads[i].get());
552 if (err != 0) {
553 goto finished;
554 }
555 threads[i]->Start();
556 }
557
558 if (warmupTime > 0) {
559 RecordStart(true, threads);
560 waitAndReport(threads, warmupTime, true);
561 }
562 RecordStart(true, threads);
563 waitAndReport(threads, duration, false);
564 RecordStop(threads);
565
566 for (auto it = threads.begin(); it != threads.end(); it++) {
567 (*it)->RequestStop(2);
568 }
569 for (auto it = threads.begin(); it != threads.end(); it++) {
570 (*it)->Join();
571 }
572 }
573 } else {
574 printUsage();
575 return 0;
576 }
577
578 if (ShortOutput) {
579 apib::PrintShortResults(std::cout, RunName, NumThreads, NumConnections);
580 } else {
581 apib::PrintFullResults(std::cout);
582 }
583 apib::EndReporting();
584
585 finished:
586 return 0;
587 }
588