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