8 #include <boost/algorithm/string/case_conv.hpp>
10 #include <simgear/simgear_config.h>
12 #include "HTTPClient.hxx"
13 #include "HTTPRequest.hxx"
15 #include <simgear/io/sg_netChat.hxx>
16 #include <simgear/misc/strutils.hxx>
17 #include <simgear/timing/timestamp.hxx>
18 #include <simgear/debug/logstream.hxx>
20 #if defined(ENABLE_CURL)
21 #include <curl/multi.h>
28 using std::stringstream;
30 using namespace simgear;
32 const char* BODY1 = "The quick brown fox jumps over a lazy dog.";
33 const char* BODY3 = "Cras ut neque nulla. Duis ut velit neque, sit amet "
34 "pharetra risus. In est ligula, lacinia vitae congue in, sollicitudin at "
35 "libero. Mauris pharetra pretium elit, nec placerat dui semper et. Maecenas "
36 "magna magna, placerat sed luctus ac, commodo et ligula. Mauris at purus et "
37 "nisl molestie auctor placerat at quam. Donec sapien magna, venenatis sed "
38 "iaculis id, fringilla vel arcu. Duis sed neque nisi. Cras a arcu sit amet "
39 "risus ultrices varius. Integer sagittis euismod dui id varius. Cras vel "
40 "justo gravida metus.";
42 const unsigned int body2Size = 8 * 1024;
43 char body2[body2Size];
45 #define COMPARE(a, b) \
47 cerr << "failed:" << #a << " != " << #b << endl; \
48 cerr << "\tgot:'" << a << "'" << endl; \
54 cerr << "failed:" << #a << endl; \
58 class TestRequest : public HTTP::Request
65 TestRequest(const std::string& url, const std::string method = "GET") :
66 HTTP::Request(url, method),
73 std::map<string, string> headers;
86 virtual void gotBodyData(const char* s, int n)
88 //std::cout << "got body data:'" << string(s, n) << "'" <<std::endl;
89 bodyData += string(s, n);
92 virtual void responseHeader(const string& header, const string& value)
94 Request::responseHeader(header, value);
95 headers[header] = value;
99 class TestServerChannel : public NetChat
113 setTerminator("\r\n");
117 virtual void collectIncomingData(const char* s, int n)
119 buffer += string(s, n);
122 virtual void foundTerminator(void)
124 if (state == STATE_IDLE) {
125 state = STATE_HEADERS;
126 string_list line = strutils::split(buffer, NULL, 3);
127 if (line.size() < 3) {
128 cerr << "malformed request:" << buffer << endl;
135 string::size_type queryPos = path.find('?');
136 if (queryPos != string::npos) {
137 parseArgs(path.substr(queryPos + 1));
138 path = path.substr(0, queryPos);
141 httpVersion = line[2];
142 requestHeaders.clear();
144 } else if (state == STATE_HEADERS) {
145 string s = strutils::simplify(buffer);
148 receivedRequestHeaders();
152 string::size_type colonPos = buffer.find(':');
153 if (colonPos == string::npos) {
154 cerr << "test malformed HTTP response header:" << buffer << endl;
159 string key = strutils::simplify(buffer.substr(0, colonPos));
160 string value = strutils::strip(buffer.substr(colonPos + 1));
161 requestHeaders[key] = value;
163 } else if (state == STATE_REQUEST_BODY) {
165 setTerminator("\r\n");
166 } else if (state == STATE_CLOSING) {
171 void parseArgs(const string& argData)
173 string_list argv = strutils::split(argData, "&");
174 for (unsigned int a=0; a<argv.size(); ++a) {
175 string::size_type eqPos = argv[a].find('=');
176 if (eqPos == string::npos) {
177 cerr << "malformed HTTP argument:" << argv[a] << endl;
181 string key = argv[a].substr(0, eqPos);
182 string value = argv[a].substr(eqPos + 1);
187 void receivedRequestHeaders()
190 if (path == "/test1") {
191 string contentStr(BODY1);
193 d << "HTTP/1.1 " << 200 << " " << reasonForCode(200) << "\r\n";
194 d << "Content-Length:" << contentStr.size() << "\r\n";
195 d << "\r\n"; // final CRLF to terminate the headers
197 push(d.str().c_str());
198 } else if (path == "/testLorem") {
199 string contentStr(BODY3);
201 d << "HTTP/1.1 " << 200 << " " << reasonForCode(200) << "\r\n";
202 d << "Content-Length:" << contentStr.size() << "\r\n";
203 d << "\r\n"; // final CRLF to terminate the headers
205 push(d.str().c_str());
206 } else if (path == "/test_zero_length_content") {
209 d << "HTTP/1.1 " << 200 << " " << reasonForCode(200) << "\r\n";
210 d << "Content-Length:" << contentStr.size() << "\r\n";
211 d << "\r\n"; // final CRLF to terminate the headers
213 push(d.str().c_str());
214 } else if (path == "/test_headers") {
215 COMPARE(requestHeaders["X-Foo"], string("Bar"));
216 COMPARE(requestHeaders["X-AnotherHeader"], string("A longer value"));
218 string contentStr(BODY1);
220 d << "HTTP/1.1 " << 200 << " " << reasonForCode(200) << "\r\n";
221 d << "Content-Length:" << contentStr.size() << "\r\n";
222 d << "\r\n"; // final CRLF to terminate the headers
224 push(d.str().c_str());
225 } else if (path == "/test2") {
227 } else if (path == "/testchunked") {
229 d << "HTTP/1.1 " << 200 << " " << reasonForCode(200) << "\r\n";
230 d << "Transfer-Encoding:chunked\r\n";
232 d << "8\r\n"; // first chunk
234 d << "6\r\n"; // second chunk
236 d << "10\r\n"; // third chunk
237 d << "ABCDSTUVABCDSTUV\r\n";
238 d << "0\r\n"; // start of trailer
239 d << "X-Foobar: wibble\r\n"; // trailer data
241 push(d.str().c_str());
242 } else if (path == "http://www.google.com/test2") {
244 if (requestHeaders["Host"] != "www.google.com") {
245 sendErrorResponse(400, true, "bad destination");
248 if (requestHeaders["Proxy-Authorization"] != string()) {
249 sendErrorResponse(401, false, "bad auth, not empty"); // shouldn't supply auth
253 } else if (path == "http://www.google.com/test3") {
255 if (requestHeaders["Host"] != "www.google.com") {
256 sendErrorResponse(400, true, "bad destination");
259 string credentials = requestHeaders["Proxy-Authorization"];
260 if (credentials.substr(0, 5) != "Basic") {
261 // request basic auth
263 d << "HTTP/1.1 " << 407 << " " << reasonForCode(407) << "\r\n";
264 d << "WWW-Authenticate: Basic real=\"simgear\"\r\n";
265 d << "\r\n"; // final CRLF to terminate the headers
266 push(d.str().c_str());
270 std::vector<unsigned char> userAndPass;
271 strutils::decodeBase64(credentials.substr(6), userAndPass);
272 std::string decodedUserPass((char*) userAndPass.data(), userAndPass.size());
274 if (decodedUserPass != "johndoe:swordfish") {
275 std::map<string, string>::const_iterator it;
276 for (it = requestHeaders.begin(); it != requestHeaders.end(); ++it) {
277 cerr << "header:" << it->first << " = " << it->second << endl;
280 sendErrorResponse(401, false, "bad auth, not as set"); // forbidden
284 } else if (strutils::starts_with(path, "/test_1_0")) {
285 string contentStr(BODY1);
286 if (strutils::ends_with(path, "/B")) {
290 d << "HTTP/1.0 " << 200 << " " << reasonForCode(200) << "\r\n";
291 d << "\r\n"; // final CRLF to terminate the headers
293 push(d.str().c_str());
295 } else if (path == "/test_close") {
296 string contentStr(BODY1);
298 d << "HTTP/1.1 " << 200 << " " << reasonForCode(200) << "\r\n";
299 d << "Connection: close\r\n";
300 d << "\r\n"; // final CRLF to terminate the headers
302 push(d.str().c_str());
304 } else if (path == "/test_abrupt_close") {
305 // simulate server doing socket close before sending any
306 // response - this used to cause a TerraSync failure since we
307 // would get stuck restarting the request
310 } else if (path == "/test_args") {
311 if ((args["foo"] != "abc") || (args["bar"] != "1234") || (args["username"] != "johndoe")) {
312 sendErrorResponse(400, true, "bad arguments");
316 string contentStr(BODY1);
318 d << "HTTP/1.1 " << 200 << " " << reasonForCode(200) << "\r\n";
319 d << "Content-Length:" << contentStr.size() << "\r\n";
320 d << "\r\n"; // final CRLF to terminate the headers
322 push(d.str().c_str());
323 } else if (path == "/test_post") {
324 if (requestHeaders["Content-Type"] != "application/x-www-form-urlencoded") {
325 cerr << "bad content type: '" << requestHeaders["Content-Type"] << "'" << endl;
326 sendErrorResponse(400, true, "bad content type");
330 requestContentLength = strutils::to_int(requestHeaders["Content-Length"]);
331 setByteCount(requestContentLength);
332 state = STATE_REQUEST_BODY;
333 } else if ((path == "/test_put") || (path == "/test_create")) {
334 if (requestHeaders["Content-Type"] != "x-application/foobar") {
335 cerr << "bad content type: '" << requestHeaders["Content-Type"] << "'" << endl;
336 sendErrorResponse(400, true, "bad content type");
340 requestContentLength = strutils::to_int(requestHeaders["Content-Length"]);
341 setByteCount(requestContentLength);
342 state = STATE_REQUEST_BODY;
344 sendErrorResponse(404, false, "");
348 void closeAfterSending()
350 state = STATE_CLOSING;
357 if (method == "POST") {
361 if (path == "/test_post") {
362 if ((args["foo"] != "abc") || (args["bar"] != "1234") || (args["username"] != "johndoe")) {
363 sendErrorResponse(400, true, "bad arguments");
368 d << "HTTP/1.1 " << 204 << " " << reasonForCode(204) << "\r\n";
369 d << "\r\n"; // final CRLF to terminate the headers
370 push(d.str().c_str());
371 } else if (path == "/test_put") {
372 std::cerr << "sending PUT response" << std::endl;
374 COMPARE(buffer, BODY3);
376 d << "HTTP/1.1 " << 204 << " " << reasonForCode(204) << "\r\n";
377 d << "\r\n"; // final CRLF to terminate the headers
378 push(d.str().c_str());
379 } else if (path == "/test_create") {
380 std::cerr << "sending create response" << std::endl;
382 std::string entityStr = "http://localhost:2000/something.txt";
384 COMPARE(buffer, BODY3);
386 d << "HTTP/1.1 " << 201 << " " << reasonForCode(201) << "\r\n";
387 d << "Location:" << entityStr << "\r\n";
388 d << "Content-Length:" << entityStr.size() << "\r\n";
389 d << "\r\n"; // final CRLF to terminate the headers
392 push(d.str().c_str());
394 std::cerr << "weird URL " << path << std::endl;
395 sendErrorResponse(400, true, "bad URL:" + path);
404 d << "HTTP/1.1 " << 200 << " " << reasonForCode(200) << "\r\n";
405 d << "Content-Length:" << body2Size << "\r\n";
406 d << "\r\n"; // final CRLF to terminate the headers
407 push(d.str().c_str());
408 bufferSend(body2, body2Size);
411 void sendErrorResponse(int code, bool close, string content)
413 cerr << "sending error " << code << " for " << path << endl;
414 cerr << "\tcontent:" << content << endl;
416 stringstream headerData;
417 headerData << "HTTP/1.1 " << code << " " << reasonForCode(code) << "\r\n";
418 headerData << "Content-Length:" << content.size() << "\r\n";
419 headerData << "\r\n"; // final CRLF to terminate the headers
420 push(headerData.str().c_str());
421 push(content.c_str());
428 string reasonForCode(int code)
431 case 200: return "OK";
432 case 201: return "Created";
433 case 204: return "no content";
434 case 404: return "not found";
435 case 407: return "proxy authentication required";
436 default: return "unknown code";
445 std::map<string, string> requestHeaders;
446 std::map<string, string> args;
447 int requestContentLength;
450 class TestServer : public NetChannel
452 simgear::NetChannelPoller _poller;
456 Socket::initSockets();
459 bind(NULL, 2000); // localhost, any port
462 _poller.addChannel(this);
465 virtual ~TestServer()
469 virtual bool writable (void) { return false ; }
471 virtual void handleAccept (void)
473 simgear::IPAddress addr ;
474 int handle = accept ( &addr ) ;
475 //cout << "did accept from " << addr.getHost() << ":" << addr.getPort() << endl;
476 TestServerChannel* chan = new TestServerChannel();
477 chan->setHandle(handle);
479 _poller.addChannel(chan);
488 TestServer testServer;
490 void waitForComplete(HTTP::Client* cl, TestRequest* tr)
492 SGTimeStamp start(SGTimeStamp::now());
493 while (start.elapsedMSec() < 10000) {
500 SGTimeStamp::sleepForMSec(15);
503 cerr << "timed out" << endl;
506 void waitForFailed(HTTP::Client* cl, TestRequest* tr)
508 SGTimeStamp start(SGTimeStamp::now());
509 while (start.elapsedMSec() < 10000) {
516 SGTimeStamp::sleepForMSec(15);
519 cerr << "timed out waiting for failure" << endl;
522 int main(int argc, char* argv[])
524 sglog().setLogLevels( SG_ALL, SG_INFO );
527 // force all requests to use the same connection for this test
528 cl.setMaxConnections(1);
531 TestRequest* tr1 = new TestRequest("http://localhost.woo.zar:2000/test1?foo=bar");
532 COMPARE(tr1->scheme(), "http");
533 COMPARE(tr1->hostAndPort(), "localhost.woo.zar:2000");
534 COMPARE(tr1->host(), "localhost.woo.zar");
535 COMPARE(tr1->port(), 2000);
536 COMPARE(tr1->path(), "/test1");
538 TestRequest* tr2 = new TestRequest("http://192.168.1.1/test1/dir/thing/file.png");
539 COMPARE(tr2->scheme(), "http");
540 COMPARE(tr2->hostAndPort(), "192.168.1.1");
541 COMPARE(tr2->host(), "192.168.1.1");
542 COMPARE(tr2->port(), 80);
543 COMPARE(tr2->path(), "/test1/dir/thing/file.png");
547 TestRequest* tr = new TestRequest("http://localhost:2000/test1");
548 HTTP::Request_ptr own(tr);
551 waitForComplete(&cl, tr);
552 COMPARE(tr->responseCode(), 200);
553 COMPARE(tr->responseReason(), string("OK"));
554 COMPARE(tr->responseLength(), strlen(BODY1));
555 COMPARE(tr->responseBytesReceived(), strlen(BODY1));
556 COMPARE(tr->bodyData, string(BODY1));
560 TestRequest* tr = new TestRequest("http://localhost:2000/testLorem");
561 HTTP::Request_ptr own(tr);
564 waitForComplete(&cl, tr);
565 COMPARE(tr->responseCode(), 200);
566 COMPARE(tr->responseReason(), string("OK"));
567 COMPARE(tr->responseLength(), strlen(BODY3));
568 COMPARE(tr->responseBytesReceived(), strlen(BODY3));
569 COMPARE(tr->bodyData, string(BODY3));
573 TestRequest* tr = new TestRequest("http://localhost:2000/test_args?foo=abc&bar=1234&username=johndoe");
574 HTTP::Request_ptr own(tr);
576 waitForComplete(&cl, tr);
577 COMPARE(tr->responseCode(), 200);
581 TestRequest* tr = new TestRequest("http://localhost:2000/test_headers");
582 HTTP::Request_ptr own(tr);
583 tr->requestHeader("X-Foo") = "Bar";
584 tr->requestHeader("X-AnotherHeader") = "A longer value";
587 waitForComplete(&cl, tr);
588 COMPARE(tr->responseCode(), 200);
589 COMPARE(tr->responseReason(), string("OK"));
590 COMPARE(tr->responseLength(), strlen(BODY1));
591 COMPARE(tr->responseBytesReceived(), strlen(BODY1));
592 COMPARE(tr->bodyData, string(BODY1));
595 // larger get request
596 for (unsigned int i=0; i<body2Size; ++i) {
597 body2[i] = (i << 4) | (i >> 2);
601 TestRequest* tr = new TestRequest("http://localhost:2000/test2");
602 HTTP::Request_ptr own(tr);
604 waitForComplete(&cl, tr);
605 COMPARE(tr->responseCode(), 200);
606 COMPARE(tr->responseBytesReceived(), body2Size);
607 COMPARE(tr->bodyData, string(body2, body2Size));
610 cerr << "testing chunked transfer encoding" << endl;
612 TestRequest* tr = new TestRequest("http://localhost:2000/testchunked");
613 HTTP::Request_ptr own(tr);
616 waitForComplete(&cl, tr);
617 COMPARE(tr->responseCode(), 200);
618 COMPARE(tr->responseReason(), string("OK"));
619 COMPARE(tr->responseBytesReceived(), 30);
620 COMPARE(tr->bodyData, "ABCDEFGHABCDEFABCDSTUVABCDSTUV");
621 // check trailers made it too
622 COMPARE(tr->headers["x-foobar"], string("wibble"));
627 TestRequest* tr = new TestRequest("http://localhost:2000/not-found");
628 HTTP::Request_ptr own(tr);
630 waitForComplete(&cl, tr);
631 COMPARE(tr->responseCode(), 404);
632 COMPARE(tr->responseReason(), string("not found"));
633 COMPARE(tr->responseLength(), 0);
636 cout << "done 404 test" << endl;
639 TestRequest* tr = new TestRequest("http://localhost:2000/test_args?foo=abc&bar=1234&username=johndoe");
640 HTTP::Request_ptr own(tr);
642 waitForComplete(&cl, tr);
643 COMPARE(tr->responseCode(), 200);
646 cout << "done1" << endl;
649 TestRequest* tr = new TestRequest("http://localhost:2000/test_1_0");
650 HTTP::Request_ptr own(tr);
652 waitForComplete(&cl, tr);
653 COMPARE(tr->responseCode(), 200);
654 COMPARE(tr->responseLength(), strlen(BODY1));
655 COMPARE(tr->bodyData, string(BODY1));
658 cout << "done2" << endl;
659 // test HTTP/1.1 Connection::close
661 TestRequest* tr = new TestRequest("http://localhost:2000/test_close");
662 HTTP::Request_ptr own(tr);
664 waitForComplete(&cl, tr);
665 COMPARE(tr->responseCode(), 200);
666 COMPARE(tr->responseLength(), strlen(BODY1));
667 COMPARE(tr->bodyData, string(BODY1));
669 cout << "done3" << endl;
670 // test connectToHost failure
673 TestRequest* tr = new TestRequest("http://not.found/something");
674 HTTP::Request_ptr own(tr);
676 waitForFailed(&cl, tr);
680 #if defined(ENABLE_CURL)
681 const int HOST_NOT_FOUND_CODE = CURLE_COULDNT_RESOLVE_HOST;
683 const int HOST_NOT_FOUND_CODE = ENOENT;
685 COMPARE(tr->responseCode(), HOST_NOT_FOUND_CODE);
688 cout << "testing abrupt close" << endl;
689 // test server-side abrupt close
691 TestRequest* tr = new TestRequest("http://localhost:2000/test_abrupt_close");
692 HTTP::Request_ptr own(tr);
694 waitForFailed(&cl, tr);
696 #if defined(ENABLE_CURL)
697 const int SERVER_NO_DATA_CODE = CURLE_GOT_NOTHING;
699 const int SERVER_NO_DATA_CODESERVER_NO_DATA_CODE = 500;
701 COMPARE(tr->responseCode(), SERVER_NO_DATA_CODE);
704 cout << "testing proxy close" << endl;
707 cl.setProxy("localhost", 2000);
708 TestRequest* tr = new TestRequest("http://www.google.com/test2");
709 HTTP::Request_ptr own(tr);
711 waitForComplete(&cl, tr);
712 COMPARE(tr->responseCode(), 200);
713 COMPARE(tr->responseLength(), body2Size);
714 COMPARE(tr->bodyData, string(body2, body2Size));
717 #if defined(ENABLE_CURL)
719 cl.setProxy("localhost", 2000, "johndoe:swordfish");
720 TestRequest* tr = new TestRequest("http://www.google.com/test3");
721 HTTP::Request_ptr own(tr);
723 waitForComplete(&cl, tr);
724 COMPARE(tr->responseCode(), 200);
725 COMPARE(tr->responseBytesReceived(), body2Size);
726 COMPARE(tr->bodyData, string(body2, body2Size));
731 cout << "testing HTTP 1.1 pipelining" << endl;
736 TestRequest* tr = new TestRequest("http://localhost:2000/test1");
737 HTTP::Request_ptr own(tr);
741 TestRequest* tr2 = new TestRequest("http://localhost:2000/testLorem");
742 HTTP::Request_ptr own2(tr2);
745 TestRequest* tr3 = new TestRequest("http://localhost:2000/test1");
746 HTTP::Request_ptr own3(tr3);
749 waitForComplete(&cl, tr3);
750 VERIFY(tr->complete);
751 VERIFY(tr2->complete);
752 COMPARE(tr->bodyData, string(BODY1));
754 COMPARE(tr2->responseLength(), strlen(BODY3));
755 COMPARE(tr2->responseBytesReceived(), strlen(BODY3));
756 COMPARE(tr2->bodyData, string(BODY3));
758 COMPARE(tr3->bodyData, string(BODY1));
761 // multiple requests with an HTTP 1.0 server
763 cout << "http 1.0 multiple requests" << endl;
766 TestRequest* tr = new TestRequest("http://localhost:2000/test_1_0/A");
767 HTTP::Request_ptr own(tr);
770 TestRequest* tr2 = new TestRequest("http://localhost:2000/test_1_0/B");
771 HTTP::Request_ptr own2(tr2);
774 TestRequest* tr3 = new TestRequest("http://localhost:2000/test_1_0/C");
775 HTTP::Request_ptr own3(tr3);
778 waitForComplete(&cl, tr3);
779 VERIFY(tr->complete);
780 VERIFY(tr2->complete);
782 COMPARE(tr->responseLength(), strlen(BODY1));
783 COMPARE(tr->responseBytesReceived(), strlen(BODY1));
784 COMPARE(tr->bodyData, string(BODY1));
786 COMPARE(tr2->responseLength(), strlen(BODY3));
787 COMPARE(tr2->responseBytesReceived(), strlen(BODY3));
788 COMPARE(tr2->bodyData, string(BODY3));
789 COMPARE(tr3->bodyData, string(BODY1));
794 cout << "testing POST" << endl;
795 TestRequest* tr = new TestRequest("http://localhost:2000/test_post?foo=abc&bar=1234&username=johndoe", "POST");
796 tr->setBodyData("", "application/x-www-form-urlencoded");
798 HTTP::Request_ptr own(tr);
800 waitForComplete(&cl, tr);
801 COMPARE(tr->responseCode(), 204);
806 cout << "testing PUT" << endl;
807 TestRequest* tr = new TestRequest("http://localhost:2000/test_put", "PUT");
808 tr->setBodyData(BODY3, "x-application/foobar");
810 HTTP::Request_ptr own(tr);
812 waitForComplete(&cl, tr);
813 COMPARE(tr->responseCode(), 204);
817 cout << "testing PUT create" << endl;
818 TestRequest* tr = new TestRequest("http://localhost:2000/test_create", "PUT");
819 tr->setBodyData(BODY3, "x-application/foobar");
821 HTTP::Request_ptr own(tr);
823 waitForComplete(&cl, tr);
824 COMPARE(tr->responseCode(), 201);
827 // test_zero_length_content
829 cout << "zero-length-content-response" << endl;
830 TestRequest* tr = new TestRequest("http://localhost:2000/test_zero_length_content");
831 HTTP::Request_ptr own(tr);
833 waitForComplete(&cl, tr);
834 COMPARE(tr->responseCode(), 200);
835 COMPARE(tr->bodyData, string());
836 COMPARE(tr->responseBytesReceived(), 0);
840 cout << "all tests passed ok" << endl;