diff --git a/README.md b/README.md index 5412937..7a9f39c 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ int main() { server.Route("/", std::make_shared()); - server.Start(); + server.Run(); } catch (const std::exception&) { return 1; @@ -274,8 +274,8 @@ The detailed implementation is out of the scope of this README, but here is an e ```cpp webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) { if (request->args().size() != 1) { - // Using kNotFound means the resource specified by the URL cannot be found. - // kBadRequest could be another choice. + // NotFound means the resource specified by the URL cannot be found. + // BadRequest could be another choice. return webcc::ResponseBuilder{}.NotFound()(); } @@ -290,7 +290,8 @@ webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) { } // Convert the book to JSON string and set as response data. - return webcc::ResponseBuilder{}.OK().Data().Json().Utf8(); + return webcc::ResponseBuilder{}.OK().Data(). + Json().Utf8()(); } ``` diff --git a/autotest/client_autotest.cc b/autotest/client_autotest.cc index b3caacb..3218504 100644 --- a/autotest/client_autotest.cc +++ b/autotest/client_autotest.cc @@ -3,6 +3,7 @@ #include "gtest/gtest.h" #include "boost/algorithm/string.hpp" +#include "boost/filesystem/fstream.hpp" #include "boost/filesystem/operations.hpp" #include "json/json.h" @@ -10,6 +11,8 @@ #include "webcc/client_session.h" #include "webcc/logger.h" +namespace bfs = boost::filesystem; + // ----------------------------------------------------------------------------- // JSON helper functions (based on jsoncpp). @@ -30,7 +33,7 @@ static Json::Value StringToJson(const std::string& str) { // ----------------------------------------------------------------------------- -TEST(ClientTest, Head_RequestFunc) { +TEST(ClientTest, Head) { webcc::ClientSession session; try { @@ -115,7 +118,7 @@ static void AssertGet(webcc::ResponsePtr r) { #endif // WEBCC_ENABLE_GZIP } -TEST(ClientTest, Get_RequestFunc) { +TEST(ClientTest, Get) { webcc::ClientSession session; try { @@ -169,127 +172,15 @@ TEST(ClientTest, Get_SSL) { } #endif // WEBCC_ENABLE_SSL -// ----------------------------------------------------------------------------- - -#if WEBCC_ENABLE_GZIP - -// Test Gzip compressed response. -TEST(ClientTest, Compression_Gzip) { - webcc::ClientSession session; - - try { - auto r = session.Get("http://httpbin.org/gzip"); - - Json::Value json = StringToJson(r->data()); - - EXPECT_EQ(true, json["gzipped"].asBool()); - - } catch (const webcc::Error& error) { - std::cerr << error << std::endl; - } -} - -// Test Deflate compressed response. -TEST(ClientTest, Compression_Deflate) { - webcc::ClientSession session; - - try { - auto r = session.Get("http://httpbin.org/deflate"); - - Json::Value json = StringToJson(r->data()); - - EXPECT_EQ(true, json["deflated"].asBool()); - - } catch (const webcc::Error& error) { - std::cerr << error << std::endl; - } -} - -// Test trying to compress the request. -// TODO -TEST(ClientTest, Compression_Request) { +// Get a JPEG image (without streaming). +TEST(ClientTest, Get_Jpeg_NoStream) { webcc::ClientSession session; try { - const std::string data = "{'name'='Adam', 'age'=20}"; - - // This doesn't really compress the body! auto r = session.Request(webcc::RequestBuilder{}. - Post("http://httpbin.org/post"). - Body(data).Json(). - Gzip() + Get("http://httpbin.org/image/jpeg") ()); - //Json::Value json = StringToJson(r->data()); - - } catch (const webcc::Error& error) { - std::cerr << error << std::endl; - } -} -#endif // WEBCC_ENABLE_GZIP - -// ----------------------------------------------------------------------------- - -// Test persistent (keep-alive) connections. -// -// NOTE: -// Boost.org doesn't support persistent connection and always includes -// "Connection: Close" header in the response. -// Both Google and GitHub support persistent connection but they don't like -// to include "Connection: Keep-Alive" header in the responses. -// URLs: -// "http://httpbin.org/get"; -// "https://www.boost.org/LICENSE_1_0.txt"; -// "https://www.google.com"; -// "https://api.github.com/events"; -// -TEST(ClientTest, KeepAlive) { - webcc::ClientSession session; - - std::string url = "http://httpbin.org/get"; - try { - - // Keep-Alive by default. - auto r = session.Get(url); - - using boost::iequals; - - EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Keep-alive")); - - // Close by setting Connection header. - r = session.Get(url, {}, { "Connection", "Close" }); - - EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Close")); - - // Close by using request builder. - r = session.Request(webcc::RequestBuilder{}. - Get(url).KeepAlive(false) - ()); - - EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Close")); - - // Keep-Alive explicitly by using request builder. - r = session.Request(webcc::RequestBuilder{}. - Get(url).KeepAlive(true) - ()); - - EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Keep-alive")); - - } catch (const webcc::Error& error) { - std::cerr << error << std::endl; - } -} - -// ----------------------------------------------------------------------------- - -// Get a JPEG image (without streaming). -TEST(ClientTest, GetImageJpeg_NoStream) { - webcc::ClientSession session; - - try { - - auto r = session.Get("http://httpbin.org/image/jpeg"); - // TODO: Verify the response is a valid JPEG image. //std::ofstream ofs(, std::ios::binary); //ofs << r->data(); @@ -299,16 +190,13 @@ TEST(ClientTest, GetImageJpeg_NoStream) { } } -// ----------------------------------------------------------------------------- - -// Streaming - -TEST(ClientTest, Stream_GetImageJpeg) { +TEST(ClientTest, Get_Jpeg_Stream) { webcc::ClientSession session; try { auto r = session.Request(webcc::RequestBuilder{}. - Get("http://httpbin.org/image/jpeg")(), + Get("http://httpbin.org/image/jpeg") + (), true); auto file_body = r->file_body(); @@ -338,7 +226,7 @@ TEST(ClientTest, Stream_GetImageJpeg) { // Test whether the streamed file will be deleted or not at the end if it's // not moved to another path by the user. -TEST(ClientTest, Stream_GetImageJpeg_NoMove) { +TEST(ClientTest, Get_Jpeg_Stream_NoMove) { webcc::ClientSession session; try { @@ -346,7 +234,8 @@ TEST(ClientTest, Stream_GetImageJpeg_NoMove) { { auto r = session.Request(webcc::RequestBuilder{}. - Get("http://httpbin.org/image/jpeg")(), + Get("http://httpbin.org/image/jpeg") + (), true); auto file_body = r->file_body(); @@ -369,7 +258,49 @@ TEST(ClientTest, Stream_GetImageJpeg_NoMove) { // ----------------------------------------------------------------------------- -TEST(ClientTest, Post_RequestFunc) { +#if WEBCC_ENABLE_GZIP +// Test Gzip compressed response. +TEST(ClientTest, Get_Gzip) { + webcc::ClientSession session; + + try { + auto r = session.Request(webcc::RequestBuilder{}. + Get("http://httpbin.org/gzip") + ()); + + Json::Value json = StringToJson(r->data()); + + EXPECT_EQ(true, json["gzipped"].asBool()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} +#endif // WEBCC_ENABLE_GZIP + +#if WEBCC_ENABLE_GZIP +// Test Deflate compressed response. +TEST(ClientTest, Get_Deflate) { + webcc::ClientSession session; + + try { + auto r = session.Request(webcc::RequestBuilder{}. + Get("http://httpbin.org/deflate") + ()); + + Json::Value json = StringToJson(r->data()); + + EXPECT_EQ(true, json["deflated"].asBool()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} +#endif // WEBCC_ENABLE_GZIP + +// ----------------------------------------------------------------------------- + +TEST(ClientTest, Post) { webcc::ClientSession session; try { @@ -412,6 +343,80 @@ TEST(ClientTest, Post_Shortcut) { } } +static bfs::path GenerateTempFile(const std::string& data) { + try { + bfs::path path = bfs::temp_directory_path() / bfs::unique_path(); + + bfs::ofstream ofs; + ofs.open(path, std::ios::binary); + if (ofs.fail()) { + return bfs::path{}; + } + + ofs << data; + + return path; + + } catch (const bfs::filesystem_error&) { + return bfs::path{}; + } +} + +TEST(ClientTest, Post_FileBody) { + webcc::ClientSession session; + + const std::string data = "{'name'='Adam', 'age'=20}"; + + auto path = GenerateTempFile(data); + if (path.empty()) { + return; + } + + try { + auto r = session.Request(webcc::RequestBuilder{}. + Post("http://httpbin.org/post"). + File(path) // Use the file as body + ()); + + EXPECT_EQ(webcc::Status::kOK, r->status()); + EXPECT_EQ("OK", r->reason()); + + Json::Value json = StringToJson(r->data()); + + EXPECT_EQ(data, json["data"].asString()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } + + // Remove the temp file. + boost::system::error_code ec; + bfs::remove(path, ec); +} + +#if WEBCC_ENABLE_GZIP +TEST(ClientTest, Post_Gzip_SmallData) { + webcc::ClientSession session; + + try { + // This data is too small to be compressed. + const std::string data = "{'name'='Adam', 'age'=20}"; + + // This doesn't really compress the body! + auto r = session.Request(webcc::RequestBuilder{}. + Post("http://httpbin.org/post"). + Body(data).Json(). + Gzip() + ()); + + //Json::Value json = StringToJson(r->data()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} +#endif // WEBCC_ENABLE_GZIP + #if (WEBCC_ENABLE_GZIP && WEBCC_ENABLE_SSL) // NOTE: Most servers don't support compressed requests! TEST(ClientTest, Post_Gzip) { @@ -438,6 +443,63 @@ TEST(ClientTest, Post_Gzip) { // ----------------------------------------------------------------------------- +// Test persistent (keep-alive) connections. +// +// NOTE: +// Boost.org doesn't support persistent connection and always includes +// "Connection: Close" header in the response. +// Both Google and GitHub support persistent connection but they don't like +// to include "Connection: Keep-Alive" header in the responses. +// URLs: +// "http://httpbin.org/get"; +// "https://www.boost.org/LICENSE_1_0.txt"; +// "https://www.google.com"; +// "https://api.github.com/events"; +// +TEST(ClientTest, KeepAlive) { + webcc::ClientSession session; + + const std::string url = "http://httpbin.org/get"; + + try { + // Keep-Alive by default. + auto r = session.Request(webcc::RequestBuilder{}.Get(url)()); + + using boost::iequals; + + EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Keep-alive")); + + // Close by setting Connection header directly. + r = session.Request(webcc::RequestBuilder{}. + Get(url). + Header("Connection", "Close") + ()); + + EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Close")); + + // Close by using request builder. + r = session.Request(webcc::RequestBuilder{}. + Get(url). + KeepAlive(false) + ()); + + EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Close")); + + // Keep-Alive explicitly by using request builder. + r = session.Request(webcc::RequestBuilder{}. + Get(url). + KeepAlive(true) + ()); + + EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Keep-alive")); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} + +// ----------------------------------------------------------------------------- + int main(int argc, char* argv[]) { // Set webcc::LOG_CONSOLE to enable logging. WEBCC_LOG_INIT("", 0); diff --git a/examples/file_downloader.cc b/examples/file_downloader.cc index aa800ee..df9ad3d 100644 --- a/examples/file_downloader.cc +++ b/examples/file_downloader.cc @@ -6,18 +6,15 @@ #include "webcc/client_session.h" #include "webcc/logger.h" -void Help(const char* argv0) { - std::cout << "Usage: file_downloader " << std::endl; - std::cout << "E.g.," << std::endl; - std::cout << " file_downloader http://httpbin.org/image/jpeg D:/test.jpg" - << std::endl; - std::cout << " file_downloader https://www.google.com/favicon.ico" - << " D:/test.ico" << std::endl; -} - int main(int argc, char* argv[]) { if (argc != 3) { - Help(argv[0]); + std::cout << "usage: file_downloader " << std::endl; + std::cout << std::endl; + std::cout << "examples:" << std::endl; + std::cout << " $ file_downloader http://httpbin.org/image/jpeg D:/test.jpg" + << std::endl; + std::cout << " $ file_downloader https://www.google.com/favicon.ico" + << " D:/test.ico" << std::endl; return 1; } diff --git a/examples/file_server.cc b/examples/file_server.cc index facbd05..76fcf66 100644 --- a/examples/file_server.cc +++ b/examples/file_server.cc @@ -6,17 +6,14 @@ #include "webcc/logger.h" #include "webcc/server.h" -void Help() { - std::cout << "Usage:" << std::endl; - std::cout << " file_server [chunk_size]" << std::endl; - std::cout << "E.g.," << std::endl; - std::cout << " file_server 8080 D:/www" << std::endl; - std::cout << " file_server 8080 D:/www 10000" << std::endl; -} - int main(int argc, char* argv[]) { if (argc < 3) { - Help(); + std::cout << "usage: file_server [chunk_size]" + << std::endl; + std::cout << std::endl; + std::cout << "examples:" << std::endl; + std::cout << " $ file_server 8080 D:/www" << std::endl; + std::cout << " $ file_server 8080 D:/www 10000" << std::endl; return 1; } diff --git a/examples/file_upload_client.cc b/examples/file_upload_client.cc index d281dad..17dee71 100644 --- a/examples/file_upload_client.cc +++ b/examples/file_upload_client.cc @@ -5,20 +5,17 @@ #include "webcc/client_session.h" #include "webcc/logger.h" -void Help() { - std::cout << "Usage:" << std::endl; - std::cout << " file_upload_client [url]" << std::endl; - std::cout << "Default Url: http://httpbin.org/post" << std::endl; - std::cout << "E.g.," << std::endl; - std::cout << " file_upload_client E:/github/webcc/data/upload" - << std::endl; - std::cout << " file_upload_client E:/github/webcc/data/upload" - << " http://httpbin.org/post" << std::endl; -} - int main(int argc, char* argv[]) { if (argc < 2) { - Help(); + std::cout << "usage: file_upload_client [url]" << std::endl; + std::cout << std::endl; + std::cout << "default url: http://httpbin.org/post" << std::endl; + std::cout << std::endl; + std::cout << "examples:" << std::endl; + std::cout << " $ file_upload_client E:/github/webcc/data/upload" + << std::endl; + std::cout << " $ file_upload_client E:/github/webcc/data/upload " + << "http://httpbin.org/post" << std::endl; return 1; } @@ -45,8 +42,8 @@ int main(int argc, char* argv[]) { try { auto r = session.Request(webcc::RequestBuilder{}. Post(url). - File("file", upload_dir / "remember.txt"). - Form("json", "{}", "application/json") + FormFile("file", upload_dir / "remember.txt"). + FormData("json", "{}", "application/json") ()); std::cout << r->status() << std::endl; diff --git a/examples/file_upload_server.cc b/examples/file_upload_server.cc index 160b297..502cbf7 100644 --- a/examples/file_upload_server.cc +++ b/examples/file_upload_server.cc @@ -32,15 +32,9 @@ private: // ----------------------------------------------------------------------------- -void Help(const char* argv0) { - std::cout << "Usage: " << argv0 << " " << std::endl; - std::cout << " E.g.," << std::endl; - std::cout << " " << argv0 << " 8080" << std::endl; -} - int main(int argc, char* argv[]) { if (argc < 2) { - Help(argv[0]); + std::cout << "usage: file_upload_server " << std::endl; return 1; } diff --git a/examples/rest_book_client.cc b/examples/rest_book_client.cc index 6ab0478..dc35f55 100644 --- a/examples/rest_book_client.cc +++ b/examples/rest_book_client.cc @@ -188,17 +188,13 @@ void PrintBookList(const std::list& books) { // ----------------------------------------------------------------------------- -void Help() { - std::cout << "Usage:" << std::endl; - std::cout << " rest_book_client [timeout]" << std::endl; - std::cout << "E.g.," << std::endl; - std::cout << " rest_book_client http://localhost:8080" << std::endl; - std::cout << " rest_book_client http://localhost:8080 2" << std::endl; -} - int main(int argc, char* argv[]) { if (argc < 2) { - Help(); + std::cout << "usage: rest_book_client [timeout]" << std::endl; + std::cout << std::endl; + std::cout << "examples:" << std::endl; + std::cout << " $ rest_book_client http://localhost:8080" << std::endl; + std::cout << " $ rest_book_client http://localhost:8080 2" << std::endl; return 1; } diff --git a/examples/rest_book_server.cc b/examples/rest_book_server.cc index e24acad..cd80dc7 100644 --- a/examples/rest_book_server.cc +++ b/examples/rest_book_server.cc @@ -145,8 +145,8 @@ webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) { Sleep(sleep_seconds_); if (request->args().size() != 1) { - // Using kNotFound means the resource specified by the URL cannot be found. - // kBadRequest could be another choice. + // NotFound means the resource specified by the URL cannot be found. + // BadRequest could be another choice. return webcc::ResponseBuilder{}.NotFound()(); } @@ -157,8 +157,8 @@ webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) { return webcc::ResponseBuilder{}.NotFound()(); } - return webcc::ResponseBuilder{}.OK().Body(BookToJsonString(book)).Json(). - Utf8()(); + return webcc::ResponseBuilder{}.OK().Body(BookToJsonString(book)). + Json().Utf8()(); } webcc::ResponsePtr BookDetailView::Put(webcc::RequestPtr request) { @@ -199,19 +199,17 @@ webcc::ResponsePtr BookDetailView::Delete(webcc::RequestPtr request) { // ----------------------------------------------------------------------------- -void Help(const char* argv0) { - std::cout << "Usage: " << argv0 << " [seconds]" << std::endl; - std::cout << "If |seconds| is provided, the server will sleep these seconds " - "before sending back each response." - << std::endl; - std::cout << " E.g.," << std::endl; - std::cout << " " << argv0 << " 8080" << std::endl; - std::cout << " " << argv0 << " 8080 3" << std::endl; -} - int main(int argc, char* argv[]) { if (argc < 2) { - Help(argv[0]); + std::cout << "usage: rest_book_server [seconds]" << std::endl; + std::cout << std::endl; + std::cout << "If |seconds| is provided, the server will sleep, for testing " + << "timeout, before " << std::endl + << "send back each response." << std::endl; + std::cout << std::endl; + std::cout << "examples:" << std::endl; + std::cout << " $ rest_book_server 8080" << std::endl; + std::cout << " $ rest_book_server 8080 3" << std::endl; return 1; } diff --git a/webcc/request_builder.cc b/webcc/request_builder.cc index ea5eda0..546c812 100644 --- a/webcc/request_builder.cc +++ b/webcc/request_builder.cc @@ -51,20 +51,29 @@ RequestPtr RequestBuilder::operator()() { return request; } -RequestBuilder& RequestBuilder::File(const std::string& name, - const Path& path, - const std::string& media_type) { - assert(!name.empty()); - form_parts_.push_back(FormPart::NewFile(name, path, media_type)); +RequestBuilder& RequestBuilder::File(const Path& path, bool infer_media_type, + std::size_t chunk_size) { + body_.reset(new FileBody{ path, chunk_size }); + + if (infer_media_type) { + media_type_ = media_types::FromExtension(path.extension().string()); + } + return *this; } -RequestBuilder& RequestBuilder::Form(const std::string& name, - std::string&& data, - const std::string& media_type) { +RequestBuilder& RequestBuilder::FormFile(const std::string& name, + const Path& path, + const std::string& media_type) { assert(!name.empty()); - form_parts_.push_back(FormPart::New(name, std::move(data), media_type)); - return *this; + return Form(FormPart::NewFile(name, path, media_type)); +} + +RequestBuilder& RequestBuilder::FormData(const std::string& name, + std::string&& data, + const std::string& media_type) { + assert(!name.empty()); + return Form(FormPart::New(name, std::move(data), media_type)); } RequestBuilder& RequestBuilder::Auth(const std::string& type, diff --git a/webcc/request_builder.h b/webcc/request_builder.h index 244a8ce..26787d6 100644 --- a/webcc/request_builder.h +++ b/webcc/request_builder.h @@ -95,17 +95,23 @@ public: return *this; } - // Add a file to upload. - RequestBuilder& File(const std::string& name, const Path& path, - const std::string& media_type = ""); + // Use the file content as body. + RequestBuilder& File(const Path& path, bool infer_media_type = true, + std::size_t chunk_size = 1024); + // Add a form part. RequestBuilder& Form(FormPartPtr part) { form_parts_.push_back(part); return *this; } - RequestBuilder& Form(const std::string& name, std::string&& data, - const std::string& media_type = ""); + // Add a form part of file. + RequestBuilder& FormFile(const std::string& name, const Path& path, + const std::string& media_type = ""); + + // Add a form part of string data. + RequestBuilder& FormData(const std::string& name, std::string&& data, + const std::string& media_type = ""); RequestBuilder& Header(const std::string& key, const std::string& value) { headers_.push_back(key);