Rename project from csoap to webcc

This commit is contained in:
Adam Gu
2018-04-04 13:23:19 +08:00
parent 964be5fdb0
commit 47e94bffd2
56 changed files with 296 additions and 297 deletions
+12
View File
@@ -0,0 +1,12 @@
option(WEBCC_DEBUG_OUTPUT "Enable debug output?" OFF)
if(WEBCC_DEBUG_OUTPUT)
add_definitions(-DWEBCC_DEBUG_OUTPUT)
endif()
# Don't use any deprecated definitions (e.g., io_service).
add_definitions(-DBOOST_ASIO_NO_DEPRECATED)
file(GLOB SRCS *.cc *.h)
add_library(webcc ${SRCS})
+106
View File
@@ -0,0 +1,106 @@
#include "webcc/common.h"
namespace webcc {
// NOTE:
// Field names are case-insensitive.
// See: https://stackoverflow.com/a/5259004
const std::string kContentType = "Content-Type";
const std::string kContentLength = "Content-Length";
const std::string kSoapAction = "SOAPAction";
const std::string kHost = "Host";
// According to www.w3.org when placing SOAP messages in HTTP bodies, the HTTP
// Content-type header must be chosen as "application/soap+xml" [RFC 3902].
// But in practice, many web servers cannot understand it.
// See: https://www.w3.org/TR/2007/REC-soap12-part0-20070427/#L26854
const std::string kTextXmlUtf8 = "text/xml; charset=utf-8";
const std::string kTextJsonUtf8 = "text/json; charset=utf-8";
////////////////////////////////////////////////////////////////////////////////
const char* GetErrorMessage(Error error) {
switch (error) {
case kHostResolveError:
return "Cannot resolve the host.";
case kEndpointConnectError:
return "Cannot connect to remote endpoint.";
case kSocketTimeoutError:
return "Operation timeout.";
case kSocketReadError:
return "Socket read error.";
case kSocketWriteError:
return "Socket write error.";
case kHttpStartLineError:
return "[HTTP Response] Start line is invalid.";
case kHttpStatusError:
return "[HTTP Response] Status is not OK.";
case kHttpContentLengthError:
return "[HTTP Response] Content-Length is invalid or missing.";
case kXmlError:
return "XML error";
default:
return "No error";
}
}
////////////////////////////////////////////////////////////////////////////////
const Namespace kSoapEnvNamespace{
"soap",
"http://schemas.xmlsoap.org/soap/envelope/"
};
////////////////////////////////////////////////////////////////////////////////
Parameter::Parameter(const std::string& key, const char* value)
: key_(key), value_(value) {
}
Parameter::Parameter(const std::string& key, const std::string& value)
: key_(key), value_(value) {
}
Parameter::Parameter(const std::string& key, std::string&& value)
: key_(key), value_(std::move(value)) {
}
Parameter::Parameter(const std::string& key, int value)
: key_(key) {
value_ = std::to_string(value);
}
Parameter::Parameter(const std::string& key, double value)
: key_(key) {
value_ = std::to_string(value);
}
Parameter::Parameter(const std::string& key, bool value)
: key_(key) {
value_ = value ? "true" : "false";
}
Parameter::Parameter(Parameter&& rhs)
: key_(std::move(rhs.key_))
, value_(std::move(rhs.value_)) {
}
Parameter& Parameter::operator=(Parameter&& rhs) {
if (this != &rhs) {
key_ = std::move(rhs.key_);
value_ = std::move(rhs.value_);
}
return *this;
}
} // namespace webcc
+160
View File
@@ -0,0 +1,160 @@
#ifndef WEBCC_COMMON_H_
#define WEBCC_COMMON_H_
// Common definitions.
#include <string>
#include <vector>
namespace webcc {
////////////////////////////////////////////////////////////////////////////////
// Buffer size for sending HTTP request and receiving HTTP response.
// TODO: Configurable for client and server separately.
const std::size_t kBufferSize = 1024;
const std::size_t kInvalidLength = static_cast<std::size_t>(-1);
extern const std::string kContentType;
extern const std::string kContentLength;
extern const std::string kSoapAction;
extern const std::string kHost;
extern const std::string kTextXmlUtf8;
extern const std::string kTextJsonUtf8;
////////////////////////////////////////////////////////////////////////////////
// Error codes.
enum Error {
kNoError = 0, // OK
kHostResolveError,
kEndpointConnectError,
kSocketTimeoutError,
kSocketReadError,
kSocketWriteError,
// Invalid start line in the HTTP response.
kHttpStartLineError,
// Status is not 200 in the HTTP response.
kHttpStatusError,
// Invalid or missing Content-Length in the HTTP response.
kHttpContentLengthError,
kXmlError,
};
// Return a descriptive message for the given error code.
const char* GetErrorMessage(Error error);
////////////////////////////////////////////////////////////////////////////////
// HTTP methods (verbs).
// NOTE: Don't use enum to avoid converting back and forth.
const std::string kHttpHead = "HEAD";
const std::string kHttpGet = "GET";
const std::string kHttpPost = "POST";
const std::string kHttpPatch = "PATCH";
const std::string kHttpPut = "PUT";
const std::string kHttpDelete = "DELETE";
//enum class HttpMethod : int {
// kUnknown,
// kHead,
// kGet,
// kPost,
// kPatch,
// kPut,
// kDelete,
//};
// HTTP response status.
// This is not a full list.
// Full list: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
// NTOE: Don't use enum class because we want to convert to/from int easily.
struct HttpStatus {
enum Enum {
kOK = 200,
kCreated = 201,
kAccepted = 202,
kNoContent = 204,
kNotModified = 304,
kBadRequest = 400,
kNotFound = 404,
InternalServerError = 500,
kNotImplemented = 501,
kServiceUnavailable = 503,
};
};
////////////////////////////////////////////////////////////////////////////////
// XML namespace name/url pair.
// E.g., { "soap", "http://schemas.xmlsoap.org/soap/envelope/" }
// TODO: Rename (add soap prefix)
class Namespace {
public:
std::string name;
std::string url;
bool IsValid() const {
return !name.empty() && !url.empty();
}
};
// CSoap's default namespace for SOAP Envelope.
extern const Namespace kSoapEnvNamespace;
////////////////////////////////////////////////////////////////////////////////
// Parameter in the SOAP request envelope.
// TODO: Rename (add soap prefix)
class Parameter {
public:
Parameter() = default;
Parameter(const Parameter& rhs) = default;
Parameter& operator=(const Parameter& rhs) = default;
Parameter(const std::string& key, const char* value);
Parameter(const std::string& key, const std::string& value);
Parameter(const std::string& key, std::string&& value);
Parameter(const std::string& key, int value);
Parameter(const std::string& key, double value);
Parameter(const std::string& key, bool value);
// Use "= default" if drop the support of VS 2013.
Parameter(Parameter&& rhs);
// Use "= default" if drop the support of VS 2013.
Parameter& operator=(Parameter&& rhs);
const std::string& key() const {
return key_;
}
const std::string& value() const {
return value_;
}
const char* c_key() const {
return key_.c_str();
}
const char* c_value() const {
return value_.c_str();
}
private:
std::string key_;
std::string value_;
};
} // namespace webcc
#endif // WEBCC_COMMON_H_
+141
View File
@@ -0,0 +1,141 @@
#include "webcc/http_client.h"
#if WEBCC_DEBUG_OUTPUT
#include <iostream>
#endif
#if 0
#include "boost/asio.hpp"
#else
#include "boost/asio/connect.hpp"
#include "boost/asio/ip/tcp.hpp"
#include "boost/asio/read.hpp"
#include "boost/asio/write.hpp"
#endif
#include "webcc/http_response_parser.h"
#include "webcc/http_request.h"
#include "webcc/http_response.h"
namespace webcc {
////////////////////////////////////////////////////////////////////////////////
// See https://stackoverflow.com/a/9079092
static void SetTimeout(boost::asio::ip::tcp::socket& socket,
int timeout_seconds) {
#if defined _WINDOWS
int ms = timeout_seconds * 1000;
const char* optval = reinterpret_cast<const char*>(&ms);
size_t optlen = sizeof(ms);
setsockopt(socket.native_handle(), SOL_SOCKET, SO_RCVTIMEO, optval, optlen);
setsockopt(socket.native_handle(), SOL_SOCKET, SO_SNDTIMEO, optval, optlen);
#else // POSIX
struct timeval tv;
tv.tv_sec = timeout_seconds_;
tv.tv_usec = 0;
setsockopt(socket.native_handle(), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(socket.native_handle(), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
#endif
}
////////////////////////////////////////////////////////////////////////////////
HttpClient::HttpClient()
: timeout_seconds_(15) {
}
Error HttpClient::SendRequest(const HttpRequest& request,
HttpResponse* response) {
assert(response != NULL);
using boost::asio::ip::tcp;
tcp::socket socket(io_context_);
tcp::resolver resolver(io_context_);
std::string port = request.port();
if (port.empty()) {
port = "80";
}
boost::system::error_code ec;
tcp::resolver::results_type endpoints =
resolver.resolve(tcp::v4(), request.host(), port, ec);
if (ec) {
return kHostResolveError;
}
boost::asio::connect(socket, endpoints, ec);
if (ec) {
return kEndpointConnectError;
}
SetTimeout(socket, timeout_seconds_);
// Send HTTP request.
#if WEBCC_DEBUG_OUTPUT
std::cout << "# REQUEST" << std::endl << request << std::endl;
#endif
try {
boost::asio::write(socket, request.ToBuffers());
} catch (boost::system::system_error&) {
return kSocketWriteError;
}
#if WEBCC_DEBUG_OUTPUT
std::cout << "# RESPONSE" << std::endl;
#endif
// Read and parse HTTP response.
HttpResponseParser parser(response);
// NOTE:
// We must stop trying to read once all content has been received,
// because some servers will block extra call to read_some().
while (!parser.finished()) {
size_t length = socket.read_some(boost::asio::buffer(buffer_), ec);
if (length == 0 || ec) {
if (ec.value() == WSAETIMEDOUT) {
return kSocketTimeoutError;
} else {
return kSocketReadError;
}
}
#if WEBCC_DEBUG_OUTPUT
// NOTE: the content XML might not be well formated.
std::cout.write(buffer_.data(), length);
#endif
// Parse the response piece just read.
// If the content has been fully received, next time flag "finished_"
// will be set.
Error error = parser.Parse(buffer_.data(), length);
if (error != kNoError) {
return error;
}
}
#if WEBCC_DEBUG_OUTPUT
std::cout << std::endl;
std::cout << "# RESPONSE (PARSED)" << std::endl;
std::cout << *response << std::endl;
#endif
return kNoError;
}
} // namespace webcc
+36
View File
@@ -0,0 +1,36 @@
#ifndef WEBCC_HTTP_CLIENT_H_
#define WEBCC_HTTP_CLIENT_H_
#include <array>
#include "boost/asio/io_context.hpp"
#include "webcc/common.h"
namespace webcc {
class HttpRequest;
class HttpResponse;
class HttpClient {
public:
HttpClient();
// Set socket send & recv timeout.
void set_timeout_seconds(int seconds) {
timeout_seconds_ = seconds;
}
// Send an HTTP request, wait until the response is received.
Error SendRequest(const HttpRequest& request,
HttpResponse* response);
private:
boost::asio::io_context io_context_;
std::array<char, kBufferSize> buffer_;
int timeout_seconds_;
};
} // namespace webcc
#endif // WEBCC_HTTP_CLIENT_H_
+16
View File
@@ -0,0 +1,16 @@
#include "webcc/http_message.h"
namespace webcc {
void HttpMessage::SetHeader(const std::string& name, const std::string& value) {
for (HttpHeader& h : headers_) {
if (h.name == name) {
h.value = value;
return;
}
}
headers_.push_back({ name, value });
}
} // namespace webcc
+86
View File
@@ -0,0 +1,86 @@
#ifndef WEBCC_HTTP_MESSAGE_H_
#define WEBCC_HTTP_MESSAGE_H_
#include <cassert>
#include <string>
#include "webcc/common.h"
namespace webcc {
class HttpHeader {
public:
std::string name;
std::string value;
};
// Base class for HTTP request and response messages.
class HttpMessage {
public:
HttpMessage() = default;
HttpMessage(const HttpMessage&) = default;
HttpMessage& operator=(const HttpMessage&) = default;
virtual ~HttpMessage() = default;
const std::string& start_line() const {
return start_line_;
}
void set_start_line(const std::string& start_line) {
start_line_ = start_line;
}
size_t content_length() const {
return content_length_;
}
const std::string& content() const {
return content_;
}
void SetHeader(const std::string& name, const std::string& value);
// E.g., "text/xml; charset=utf-8"
void SetContentType(const std::string& content_type) {
SetHeader(kContentType, content_type);
}
void SetContentLength(size_t content_length) {
content_length_ = content_length;
SetHeader(kContentLength, std::to_string(content_length));
}
// Use move semantics to avoid copy.
void set_content(std::string&& content) {
content_ = std::move(content);
}
void AppendContent(const char* data, size_t count) {
content_.append(data, count);
}
void AppendContent(const std::string& data) {
content_.append(data);
}
bool IsContentFull() const {
assert(IsContentLengthValid());
return content_.length() >= content_length_;
}
bool IsContentLengthValid() const {
return content_length_ != kInvalidLength;
}
protected:
// Start line with trailing "\r\n".
std::string start_line_;
std::size_t content_length_ = kInvalidLength;
std::vector<HttpHeader> headers_;
std::string content_;
};
} // namespace webcc
#endif // WEBCC_HTTP_MESSAGE_H_
+110
View File
@@ -0,0 +1,110 @@
#include "webcc/http_parser.h"
#include "boost/algorithm/string.hpp"
#include "boost/lexical_cast.hpp"
#include "webcc/http_message.h"
namespace webcc {
HttpParser::HttpParser(HttpMessage* message)
: message_(message)
, start_line_parsed_(false)
, header_parsed_(false)
, finished_(false) {
}
Error HttpParser::Parse(const char* data, size_t len) {
if (header_parsed_) {
// Add the data to the content.
message_->AppendContent(data, len);
if (message_->IsContentFull()) {
// All content has been read.
finished_ = true;
}
return kNoError;
}
pending_data_.append(data, len);
size_t off = 0;
while (true) {
size_t pos = pending_data_.find("\r\n", off);
if (pos == std::string::npos) {
break;
}
if (pos == off) { // End of headers.
off = pos + 2; // Skip CRLF.
header_parsed_ = true;
break;
}
std::string line = pending_data_.substr(off, pos - off);
if (!start_line_parsed_) {
start_line_parsed_ = true;
Error error = ParseStartLine(line);
if (error != kNoError) {
return error;
}
} else {
// Currently, only Content-Length is important to us.
// Other fields are ignored.
if (!message_->IsContentLengthValid()) {
ParseContentLength(line);
}
}
off = pos + 2; // Skip CRLF.
}
if (header_parsed_) {
// Headers just ended.
if (!message_->IsContentLengthValid()) {
// No Content-Length?
return kHttpContentLengthError;
}
message_->AppendContent(pending_data_.substr(off));
if (message_->IsContentFull()) {
// All content has been read.
finished_ = true;
}
} else {
// Save the unparsed piece for next parsing.
pending_data_ = pending_data_.substr(off);
}
return kNoError;
}
void HttpParser::ParseContentLength(const std::string& line) {
size_t pos = line.find(':');
if (pos == std::string::npos) {
return;
}
std::string name = line.substr(0, pos);
if (boost::iequals(name, kContentLength)) {
++pos; // Skip ':'.
while (line[pos] == ' ') { // Skip spaces.
++pos;
}
std::string value = line.substr(pos);
try {
message_->SetContentLength(boost::lexical_cast<size_t>(value));
} catch (boost::bad_lexical_cast&) {
// TODO
}
}
}
} // namespace webcc
+45
View File
@@ -0,0 +1,45 @@
#ifndef WEBCC_HTTP_PARSER_H_
#define WEBCC_HTTP_PARSER_H_
#include <string>
#include "webcc/common.h"
namespace webcc {
class HttpMessage;
// HttpParser parses HTTP request and response.
class HttpParser {
public:
explicit HttpParser(HttpMessage* message);
bool finished() const {
return finished_;
}
Error Parse(const char* data, size_t len);
protected:
// Parse HTTP start line.
virtual Error ParseStartLine(const std::string& line) = 0;
void ParseContentLength(const std::string& line);
protected:
// The result HTTP message.
HttpMessage* message_;
Error error_;
// Data waiting to be parsed.
std::string pending_data_;
// Parsing helper flags.
bool start_line_parsed_;
bool header_parsed_;
bool finished_;
};
} // namespace webcc
#endif // WEBCC_HTTP_PARSER_H_
+71
View File
@@ -0,0 +1,71 @@
#include "webcc/http_request.h"
#include "boost/algorithm/string.hpp"
namespace webcc {
std::ostream& operator<<(std::ostream& os, const HttpRequest& request) {
os << request.start_line();
for (const HttpHeader& h : request.headers_) {
os << h.name << ": " << h.value << std::endl;
}
os << std::endl;
os << request.content() << std::endl;
return os;
}
void HttpRequest::SetHost(const std::string& host, const std::string& port) {
host_ = host;
port_ = port;
if (port.empty()) {
SetHeader(kHost, host);
} else {
SetHeader(kHost, host + ":" + port);
}
}
void HttpRequest::MakeStartLine() {
if (start_line_.empty()) {
start_line_ = method_;
start_line_ += " ";
start_line_ += url_;
start_line_ += " HTTP/1.1\r\n";
}
}
namespace misc_strings {
const char NAME_VALUE_SEPARATOR[] = { ':', ' ' };
const char CRLF[] = { '\r', '\n' };
} // misc_strings
// ATTENTION: The buffers don't hold the memory!
std::vector<boost::asio::const_buffer> HttpRequest::ToBuffers() const {
assert(!start_line_.empty());
assert(IsContentLengthValid());
std::vector<boost::asio::const_buffer> buffers;
buffers.push_back(boost::asio::buffer(start_line_));
for (const HttpHeader& h : headers_) {
buffers.push_back(boost::asio::buffer(h.name));
buffers.push_back(boost::asio::buffer(misc_strings::NAME_VALUE_SEPARATOR));
buffers.push_back(boost::asio::buffer(h.value));
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
}
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
buffers.push_back(boost::asio::buffer(content_));
return buffers;
}
} // namespace webcc
+78
View File
@@ -0,0 +1,78 @@
#ifndef WEBCC_HTTP_REQUEST_H_
#define WEBCC_HTTP_REQUEST_H_
#include <string>
#include "boost/asio/buffer.hpp" // for const_buffer
#include "webcc/http_message.h"
namespace webcc {
class HttpRequest;
std::ostream& operator<<(std::ostream& os, const HttpRequest& request);
class HttpRequest : public HttpMessage {
friend std::ostream& operator<<(std::ostream& os,
const HttpRequest& request);
public:
HttpRequest() = default;
HttpRequest(const HttpRequest&) = default;
HttpRequest& operator=(const HttpRequest&) = default;
virtual ~HttpRequest() = default;
const std::string& method() const {
return method_;
}
void set_method(const std::string& method) {
method_ = method;
}
const std::string& url() const {
return url_;
}
void set_url(const std::string& url) {
url_ = url;
}
const std::string& host() const {
return host_;
}
const std::string& port() const {
return port_;
}
// \param host Descriptive host name or numeric IP address.
// \param port Numeric port number, "80" will be used if it's empty.
void SetHost(const std::string& host, const std::string& port);
// Compose start line from method, url, etc.
void MakeStartLine();
// Convert the response into a vector of buffers. The buffers do not own the
// underlying memory blocks, therefore the request object must remain valid
// and not be changed until the write operation has completed.
// NOTE: Please call MakeStartLine() before.
std::vector<boost::asio::const_buffer> ToBuffers() const;
private:
// HTTP method.
std::string method_;
// Request URL.
// A complete URL naming the requested resource, or the path component of
// the URL.
std::string url_;
std::string host_;
std::string port_;
};
} // namespace webcc
#endif // WEBCC_HTTP_REQUEST_H_
+80
View File
@@ -0,0 +1,80 @@
#include "webcc/http_request_handler.h"
#include <sstream>
#if WEBCC_DEBUG_OUTPUT
#include <iostream>
#endif
#include "webcc/common.h"
#include "webcc/http_request.h"
#include "webcc/http_response.h"
namespace webcc {
void HttpRequestHandler::Enqueue(HttpSessionPtr session) {
queue_.Push(session);
}
void HttpRequestHandler::Start(std::size_t count) {
assert(count > 0 && workers_.size() == 0);
for (std::size_t i = 0; i < count; ++i) {
#if WEBCC_DEBUG_OUTPUT
boost::thread* worker =
#endif
workers_.create_thread(std::bind(&HttpRequestHandler::WorkerRoutine, this));
#if WEBCC_DEBUG_OUTPUT
std::cout << "Worker is running (thread: " << worker->get_id() << ")\n";
#endif
}
}
void HttpRequestHandler::Stop() {
#if WEBCC_DEBUG_OUTPUT
std::cout << "Stopping workers...\n";
#endif
// Close pending sessions.
for (HttpSessionPtr conn = queue_.Pop(); conn; conn = queue_.Pop()) {
#if WEBCC_DEBUG_OUTPUT
std::cout << "Closing pending session...\n";
#endif
conn->Stop();
}
// Enqueue a null session to trigger the first worker to stop.
queue_.Push(HttpSessionPtr());
workers_.join_all();
#if WEBCC_DEBUG_OUTPUT
std::cout << "All workers have been stopped.\n";
#endif
}
void HttpRequestHandler::WorkerRoutine() {
#if WEBCC_DEBUG_OUTPUT
boost::thread::id thread_id = boost::this_thread::get_id();
#endif
for (;;) {
HttpSessionPtr session = queue_.PopOrWait();
if (!session) {
#if WEBCC_DEBUG_OUTPUT
std::cout << "Worker is going to stop (thread: " << thread_id << ")\n";
#endif
// For stopping next worker.
queue_.Push(HttpSessionPtr());
// Stop the worker.
break;
}
HandleSession(session);
}
}
} // namespace webcc
+51
View File
@@ -0,0 +1,51 @@
#ifndef WEBCC_HTTP_REQUEST_HANDLER_H_
#define WEBCC_HTTP_REQUEST_HANDLER_H_
#include <list>
#include <vector>
#include "boost/thread/thread.hpp"
#include "webcc/http_session.h"
#include "webcc/queue.h"
#include "webcc/soap_service.h"
namespace webcc {
class HttpRequest;
class HttpResponse;
// The common handler for all incoming requests.
class HttpRequestHandler {
public:
HttpRequestHandler(const HttpRequestHandler&) = delete;
HttpRequestHandler& operator=(const HttpRequestHandler&) = delete;
HttpRequestHandler() = default;
virtual ~HttpRequestHandler() {
}
// Put the session into the queue.
void Enqueue(HttpSessionPtr session);
// Start worker threads.
void Start(std::size_t count);
// Close pending sessions and stop worker threads.
void Stop();
private:
void WorkerRoutine();
// Called by the worker routine.
virtual HttpStatus::Enum HandleSession(HttpSessionPtr session) = 0;
private:
Queue<HttpSessionPtr> queue_;
boost::thread_group workers_;
};
} // namespace webcc
#endif // WEBCC_HTTP_REQUEST_HANDLER_H_
+30
View File
@@ -0,0 +1,30 @@
#include "webcc/http_request_parser.h"
#include <vector>
#include "boost/algorithm/string.hpp"
#include "webcc/http_request.h"
namespace webcc {
HttpRequestParser::HttpRequestParser(HttpRequest* request)
: HttpParser(request), request_(request) {
}
Error HttpRequestParser::ParseStartLine(const std::string& line) {
std::vector<std::string> strs;
boost::split(strs, line, boost::is_any_of(" "), boost::token_compress_on);
if (strs.size() != 3) {
return kHttpStartLineError;
}
request_->set_method(strs[0]);
request_->set_url(strs[1]);
// HTTP version is currently ignored.
return kNoError;
}
} // namespace webcc
+23
View File
@@ -0,0 +1,23 @@
#ifndef WEBCC_HTTP_REQUEST_PARSER_H_
#define WEBCC_HTTP_REQUEST_PARSER_H_
#include "webcc/http_parser.h"
namespace webcc {
class HttpRequest;
class HttpRequestParser : public HttpParser {
public:
explicit HttpRequestParser(HttpRequest* request);
private:
Error ParseStartLine(const std::string& line) override;
private:
HttpRequest* request_;
};
} // namespace webcc
#endif // WEBCC_HTTP_REQUEST_PARSER_H_
+121
View File
@@ -0,0 +1,121 @@
#include "webcc/http_response.h"
#include "webcc/common.h"
#include "webcc/xml.h"
namespace webcc {
std::ostream& operator<<(std::ostream& os, const HttpResponse& response) {
os << response.start_line();
for (const HttpHeader& h : response.headers_) {
os << h.name << ": " << h.value << std::endl;
}
os << std::endl;
// Pretty print the SOAP response XML.
if (!xml::PrettyPrintXml(os, response.content())) {
os << response.content();
}
return os;
}
namespace status_strings {
const std::string OK = "HTTP/1.1 200 OK\r\n";
const std::string CREATED = "HTTP/1.0 201 Created\r\n";
const std::string ACCEPTED = "HTTP/1.0 202 Accepted\r\n";
const std::string NO_CONTENT = "HTTP/1.0 204 No Content\r\n";
const std::string NOT_MODIFIED = "HTTP/1.0 304 Not Modified\r\n";
const std::string BAD_REQUEST = "HTTP/1.1 400 Bad Request\r\n";
const std::string NOT_FOUND = "HTTP/1.0 404 Not Found\r\n";
const std::string INTERNAL_SERVER_ERROR =
"HTTP/1.1 500 Internal Server Error\r\n";
const std::string NOT_IMPLEMENTED = "HTTP/1.1 501 Not Implemented\r\n";
const std::string SERVICE_UNAVAILABLE = "HTTP/1.1 503 Service Unavailable\r\n";
boost::asio::const_buffer ToBuffer(int status) {
switch (status) {
case HttpStatus::kOK:
return boost::asio::buffer(OK);
case HttpStatus::kCreated:
return boost::asio::buffer(CREATED);
case HttpStatus::kAccepted:
return boost::asio::buffer(ACCEPTED);
case HttpStatus::kNoContent:
return boost::asio::buffer(NO_CONTENT);
case HttpStatus::kNotModified:
return boost::asio::buffer(NOT_MODIFIED);
case HttpStatus::kBadRequest:
return boost::asio::buffer(BAD_REQUEST);
case HttpStatus::kNotFound:
return boost::asio::buffer(NOT_FOUND);
case HttpStatus::InternalServerError:
return boost::asio::buffer(INTERNAL_SERVER_ERROR);
case HttpStatus::kNotImplemented:
return boost::asio::buffer(NOT_IMPLEMENTED);
case HttpStatus::kServiceUnavailable:
return boost::asio::buffer(SERVICE_UNAVAILABLE);
default:
return boost::asio::buffer(NOT_IMPLEMENTED);
}
}
} // namespace status_strings
namespace misc_strings {
const char NAME_VALUE_SEPARATOR[] = { ':', ' ' };
const char CRLF[] = { '\r', '\n' };
} // misc_strings
////////////////////////////////////////////////////////////////////////////////
// ATTENTION: The buffers don't hold the memory!
std::vector<boost::asio::const_buffer> HttpResponse::ToBuffers() const {
std::vector<boost::asio::const_buffer> buffers;
// Status line
buffers.push_back(status_strings::ToBuffer(status_));
// Header fields (optional)
for (const HttpHeader& h : headers_) {
buffers.push_back(boost::asio::buffer(h.name));
buffers.push_back(boost::asio::buffer(misc_strings::NAME_VALUE_SEPARATOR));
buffers.push_back(boost::asio::buffer(h.value));
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
}
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
// Content (optional)
if (IsContentLengthValid()) {
buffers.push_back(boost::asio::buffer(content_));
}
return buffers;
}
HttpResponse HttpResponse::Fault(HttpStatus::Enum status) {
assert(status != HttpStatus::kOK);
HttpResponse response;
response.set_status(status);
return response;
}
} // namespace webcc
+43
View File
@@ -0,0 +1,43 @@
#ifndef WEBCC_HTTP_RESPONSE_H_
#define WEBCC_HTTP_RESPONSE_H_
#include <string>
#include "boost/asio/buffer.hpp" // for const_buffer
#include "webcc/http_message.h"
namespace webcc {
class HttpResponse;
std::ostream& operator<<(std::ostream& os, const HttpResponse& response);
class HttpResponse : public HttpMessage {
friend std::ostream& operator<<(std::ostream& os,
const HttpResponse& response);
public:
HttpResponse() {
}
int status() const {
return status_;
}
void set_status(int status) {
status_ = status;
}
// Convert the response into a vector of buffers. The buffers do not own the
// underlying memory blocks, therefore the response object must remain valid
// and not be changed until the write operation has completed.
std::vector<boost::asio::const_buffer> ToBuffers() const;
// Get a fault response when HTTP status is not OK.
static HttpResponse Fault(HttpStatus::Enum status);
private:
int status_ = HttpStatus::kOK; // TODO: HttpStatus
};
} // namespace webcc
#endif // WEBCC_HTTP_RESPONSE_H_
+49
View File
@@ -0,0 +1,49 @@
#include "webcc/http_response_parser.h"
#include "boost/lexical_cast.hpp"
#include "webcc/http_response.h"
namespace webcc {
HttpResponseParser::HttpResponseParser(HttpResponse* response)
: HttpParser(response)
, response_(response) {
}
Error HttpResponseParser::ParseStartLine(const std::string& line) {
response_->set_start_line(line + "\r\n");
size_t off = 0;
size_t pos = line.find(' ');
if (pos == std::string::npos) {
return kHttpStartLineError;
}
// HTTP version
off = pos + 1; // Skip space.
pos = line.find(' ', off);
if (pos == std::string::npos) {
return kHttpStartLineError;
}
// Status code
std::string status_str = line.substr(off, pos - off);
try {
response_->set_status(boost::lexical_cast<int>(status_str));
} catch (boost::bad_lexical_cast&) {
return kHttpStartLineError;
}
off = pos + 1; // Skip space.
if (response_->status() != HttpStatus::kOK) {
return kHttpStatusError;
}
return kNoError;
}
} // namespace webcc
+25
View File
@@ -0,0 +1,25 @@
#ifndef WEBCC_HTTP_RESPONSE_PARSER_H_
#define WEBCC_HTTP_RESPONSE_PARSER_H_
#include "webcc/http_parser.h"
namespace webcc {
class HttpResponse;
class HttpResponseParser : public HttpParser {
public:
explicit HttpResponseParser(HttpResponse* response);
private:
// Parse HTTP start line; E.g., "HTTP/1.1 200 OK".
Error ParseStartLine(const std::string& line) override;
private:
// The result response message.
HttpResponse* response_;
};
} // namespace webcc
#endif // WEBCC_HTTP_RESPONSE_PARSER_H_
+98
View File
@@ -0,0 +1,98 @@
#include "webcc/http_server.h"
#include <signal.h>
#if WEBCC_DEBUG_OUTPUT
#include <iostream>
#endif
#include "webcc/http_request_handler.h"
#include "webcc/soap_service.h"
#include "webcc/utility.h"
using tcp = boost::asio::ip::tcp;
namespace webcc {
HttpServer::HttpServer(unsigned short port, std::size_t workers)
: signals_(io_context_)
, workers_(workers) {
// Register to handle the signals that indicate when the server should exit.
// It is safe to register for the same signal multiple times in a program,
// provided all registration for the specified signal is made through asio.
// TODO: Verify if this works for Windows.
signals_.add(SIGINT);
signals_.add(SIGTERM);
#if defined(SIGQUIT)
signals_.add(SIGQUIT);
#endif
DoAwaitStop();
// NOTE:
// "reuse_addr=true" means option SO_REUSEADDR will be set.
// For more details about SO_REUSEADDR, see:
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms740621(v=vs.85).aspx
// http://www.andy-pearce.com/blog/posts/2013/Feb/so_reuseaddr-on-windows/
// TODO: SO_EXCLUSIVEADDRUSE
acceptor_.reset(new tcp::acceptor(io_context_,
tcp::endpoint(tcp::v4(), port),
true)); // reuse_addr
DoAccept();
}
HttpServer::~HttpServer() {
}
void HttpServer::Run() {
assert(request_handler_ != NULL);
#if WEBCC_DEBUG_OUTPUT
boost::thread::id thread_id = boost::this_thread::get_id();
std::cout << "Server main thread: " << thread_id << std::endl;
#endif
// Start worker threads.
request_handler_->Start(workers_);
// The io_context::run() call will block until all asynchronous operations
// have finished. While the server is running, there is always at least one
// asynchronous operation outstanding: the asynchronous accept call waiting
// for new incoming connections.
io_context_.run();
}
void HttpServer::DoAccept() {
acceptor_->async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
// Check whether the server was stopped by a signal before this
// completion handler had a chance to run.
if (!acceptor_->is_open()) {
return;
}
if (!ec) {
HttpSessionPtr conn{
new HttpSession(std::move(socket), request_handler_)
};
conn->Start();
}
DoAccept();
});
}
void HttpServer::DoAwaitStop() {
signals_.async_wait(
[this](boost::system::error_code, int /*signo*/) {
// The server is stopped by canceling all outstanding asynchronous
// operations. Once all operations have finished the io_context::run()
// call will exit.
acceptor_->close();
request_handler_->Stop();
});
}
} // namespace webcc
+62
View File
@@ -0,0 +1,62 @@
#ifndef WEBCC_HTTP_SERVER_H_
#define WEBCC_HTTP_SERVER_H_
#include <string>
#include <vector>
#include "boost/scoped_ptr.hpp"
#include "boost/thread/thread.hpp"
#include "boost/asio/io_context.hpp"
#include "boost/asio/signal_set.hpp"
#include "boost/asio/ip/tcp.hpp"
#include "webcc/http_session.h"
namespace webcc {
class HttpRequestHandler;
// HTTP server accepts TCP connections from TCP clients.
// NOTE: Only support IPv4.
class HttpServer {
public:
HttpServer(const HttpServer&) = delete;
HttpServer& operator=(const HttpServer&) = delete;
HttpServer(unsigned short port, std::size_t workers);
virtual ~HttpServer();
// Run the server's io_service loop.
void Run();
private:
// Initiate an asynchronous accept operation.
void DoAccept();
// Wait for a request to stop the server.
void DoAwaitStop();
protected:
// The handler for all incoming requests.
// TODO: Replace with virtual GetRequestHandler()?
HttpRequestHandler* request_handler_;
private:
// The number of worker threads.
std::size_t workers_;
// The io_context used to perform asynchronous operations.
boost::asio::io_context io_context_;
// The signal_set is used to register for process termination notifications.
boost::asio::signal_set signals_;
// Acceptor used to listen for incoming connections.
boost::scoped_ptr<boost::asio::ip::tcp::acceptor> acceptor_;
};
} // namespace webcc
#endif // WEBCC_HTTP_SERVER_H_
+109
View File
@@ -0,0 +1,109 @@
#include "webcc/http_session.h"
#include <vector>
#if WEBCC_DEBUG_OUTPUT
#include <iostream>
#endif
#include "boost/asio/write.hpp"
#include "webcc/http_request_handler.h"
namespace webcc {
HttpSession::HttpSession(boost::asio::ip::tcp::socket socket,
HttpRequestHandler* handler)
: socket_(std::move(socket))
, request_handler_(handler)
, request_parser_(&request_) {
}
void HttpSession::Start() {
DoRead();
}
void HttpSession::Stop() {
socket_.close();
}
void HttpSession::SetResponseContent(const std::string& content_type,
std::size_t content_length,
std::string&& content) {
response_.SetContentType(content_type);
response_.SetContentLength(content.length());
response_.set_content(std::move(content));
}
void HttpSession::SendResponse() {
DoWrite();
}
void HttpSession::DoRead() {
auto handler = std::bind(&HttpSession::HandleRead,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2);
socket_.async_read_some(boost::asio::buffer(buffer_), handler);
}
void HttpSession::DoWrite() {
auto handler = std::bind(&HttpSession::HandleWrite,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2);
boost::asio::async_write(socket_, response_.ToBuffers(), handler);
}
void HttpSession::HandleRead(boost::system::error_code ec,
std::size_t length) {
if (ec) {
if (ec != boost::asio::error::operation_aborted) {
Stop();
}
return;
}
Error error = request_parser_.Parse(buffer_.data(), length);
if (error != kNoError) {
// Bad request.
response_ = HttpResponse::Fault(HttpStatus::kBadRequest);
DoWrite();
return;
}
if (!request_parser_.finished()) {
// Continue to read the request.
DoRead();
return;
}
// Enqueue this session.
// Some worker thread will handle it later.
// And DoWrite() will be called in the worker thread.
request_handler_->Enqueue(shared_from_this());
}
// NOTE:
// This write handler will be called from main thread (the thread calling
// io_context.run), even though DoWrite() is invoked by worker threads. This is
// ensured by Asio.
void HttpSession::HandleWrite(boost::system::error_code ec,
size_t length) {
#if WEBCC_DEBUG_OUTPUT
boost::thread::id thread_id = boost::this_thread::get_id();
std::cout << "Response has been sent back (thread: " << thread_id << ")\n";
#endif
if (!ec) {
// Initiate graceful connection closure.
boost::system::error_code ignored_ec;
socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignored_ec);
}
if (ec != boost::asio::error::operation_aborted) {
Stop();
}
}
} // namespace webcc
+82
View File
@@ -0,0 +1,82 @@
#ifndef WEBCC_HTTP_SESSION_H_
#define WEBCC_HTTP_SESSION_H_
#include <array>
#include <memory>
#include "boost/asio/ip/tcp.hpp" // for ip::tcp::socket
#include "webcc/common.h"
#include "webcc/http_request.h"
#include "webcc/http_request_parser.h"
#include "webcc/http_response.h"
namespace webcc {
class HttpRequestHandler;
class HttpSession : public std::enable_shared_from_this<HttpSession> {
public:
friend class HttpRequestHandler;
HttpSession(const HttpSession&) = delete;
HttpSession& operator=(const HttpSession&) = delete;
HttpSession(boost::asio::ip::tcp::socket socket,
HttpRequestHandler* handler);
const HttpRequest& request() const {
return request_;
}
void Start();
void Stop();
void SetResponseStatus(int status) {
response_.set_status(status);
}
void SetResponseContent(const std::string& content_type,
std::size_t content_length,
std::string&& content);
// Write response back to the client.
void SendResponse();
private:
void DoRead();
void DoWrite();
void HandleRead(boost::system::error_code ec,
std::size_t length);
void HandleWrite(boost::system::error_code ec,
std::size_t length);
private:
// Socket for the connection.
boost::asio::ip::tcp::socket socket_;
// The handler used to process the incoming request.
HttpRequestHandler* request_handler_;
// Buffer for incoming data.
std::array<char, kBufferSize> buffer_;
// The incoming request.
HttpRequest request_;
// The parser for the incoming request.
HttpRequestParser request_parser_;
// The response to be sent back to the client.
HttpResponse response_;
};
typedef std::shared_ptr<HttpSession> HttpSessionPtr;
} // namespace webcc
#endif // WEBCC_HTTP_SESSION_H_
+62
View File
@@ -0,0 +1,62 @@
#ifndef WEBCC_QUEUE_H_
#define WEBCC_QUEUE_H_
// A general message queue.
#include <list>
#include <queue>
#include "boost/thread/condition_variable.hpp"
#include "boost/thread/locks.hpp"
#include "boost/thread/mutex.hpp"
namespace webcc {
template <typename T>
class Queue {
public:
Queue(const Queue& rhs) = delete;
Queue& operator=(const Queue& rhs) = delete;
Queue() = default;
T PopOrWait() {
boost::unique_lock<boost::mutex> lock(mutex_);
// Wait for a message.
not_empty_cv_.wait(lock, [this] { return !message_list_.empty(); });
T message = message_list_.front();
message_list_.pop_front();
return message;
}
T Pop() {
boost::lock_guard<boost::mutex> lock(mutex_);
if (message_list_.empty()) {
return T();
}
T message = message_list_.front();
message_list_.pop_front();
return message;
}
void Push(const T& message) {
{
boost::lock_guard<boost::mutex> lock(mutex_);
message_list_.push_back(message);
}
not_empty_cv_.notify_one();
}
private:
std::list<T> message_list_;
boost::mutex mutex_;
boost::condition_variable not_empty_cv_;
};
} // namespace webcc
#endif // WEBCC_QUEUE_H_
+120
View File
@@ -0,0 +1,120 @@
#include "webcc/rest_server.h"
#if WEBCC_DEBUG_OUTPUT
#include <iostream>
#endif
#include "webcc/url.h"
namespace webcc {
////////////////////////////////////////////////////////////////////////////////
bool RestServiceManager::AddService(RestServicePtr service,
const std::string& url) {
assert(service);
ServiceItem item(service, url);
std::regex::flag_type flags = std::regex::ECMAScript | std::regex::icase;
try {
// Compile the regex.
item.url_regex.assign(url, flags);
service_items_.push_back(item);
return true;
} catch (std::regex_error& e) {
#if WEBCC_DEBUG_OUTPUT
std::cout << e.what() << std::endl;
#endif
}
return false;
}
RestServicePtr RestServiceManager::GetService(
const std::string& url,
std::vector<std::string>* sub_matches) {
assert(sub_matches != NULL);
for (ServiceItem& item : service_items_) {
std::smatch match;
if (std::regex_match(url, match, item.url_regex)) {
// Any sub-matches?
// NOTE: Start from 1 because match[0] is the whole string itself.
for (size_t i = 1; i < match.size(); ++i) {
sub_matches->push_back(match[i].str());
}
return item.service;
}
}
return RestServicePtr();
}
////////////////////////////////////////////////////////////////////////////////
bool RestRequestHandler::RegisterService(RestServicePtr service,
const std::string& url) {
return service_manager_.AddService(service, url);
}
HttpStatus::Enum RestRequestHandler::HandleSession(HttpSessionPtr session) {
Url url(session->request().url());
if (!url.IsValid()) {
session->SetResponseStatus(HttpStatus::kBadRequest);
session->SendResponse();
return HttpStatus::kBadRequest;
}
std::vector<std::string> sub_matches;
RestServicePtr service = service_manager_.GetService(url.path(), &sub_matches);
if (!service) {
#if WEBCC_DEBUG_OUTPUT
std::cout << "No service matches the URL: " << url.path() << std::endl;
#endif
session->SetResponseStatus(HttpStatus::kBadRequest);
session->SendResponse();
return HttpStatus::kBadRequest;
}
// TODO: Error handling.
std::string content;
service->Handle(session->request().method(),
session->request().content(),
&content);
session->SetResponseStatus(HttpStatus::kOK);
session->SetResponseContent(kTextJsonUtf8,
content.length(),
std::move(content));
session->SendResponse();
return HttpStatus::kOK;
}
////////////////////////////////////////////////////////////////////////////////
RestServer::RestServer(unsigned short port, std::size_t workers)
: HttpServer(port, workers)
, rest_request_handler_(new RestRequestHandler()) {
request_handler_ = rest_request_handler_;
}
RestServer::~RestServer() {
request_handler_ = NULL;
delete rest_request_handler_;
}
bool RestServer::RegisterService(RestServicePtr service,
const std::string& url) {
return rest_request_handler_->RegisterService(service, url);
}
} // namespace webcc
+105
View File
@@ -0,0 +1,105 @@
#ifndef WEBCC_REST_SERVER_H_
#define WEBCC_REST_SERVER_H_
// HTTP server handling REST requests.
#include <regex>
#include <string>
#include <vector>
#include "webcc/http_request_handler.h"
#include "webcc/http_server.h"
#include "webcc/rest_service.h"
namespace webcc {
class Url;
////////////////////////////////////////////////////////////////////////////////
class RestServiceManager {
public:
RestServiceManager() = default;
RestServiceManager(const RestServiceManager&) = delete;
RestServiceManager& operator=(const RestServiceManager&) = delete;
// Add a service and bind it with the given URL.
// The URL should start with "/" and could be a regular expression or not.
// E.g., "/instances". "/instances/(\\d+)"
bool AddService(RestServicePtr service, const std::string& url);
// Parameter 'sub_matches' is only available when the URL bound to the
// service is a regular expression and has sub-expressions.
// E.g., the URL bound to the service is "/instances/(\\d+)", now match
// "/instances/12345" against it, you will get one sub-match of "12345".
RestServicePtr GetService(const std::string& url,
std::vector<std::string>* sub_matches);
private:
class ServiceItem {
public:
ServiceItem(RestServicePtr _service, const std::string& _url)
: service(_service), url(_url) {
}
ServiceItem(const ServiceItem& rhs) = default;
ServiceItem& operator=(const ServiceItem& rhs) = default;
ServiceItem(ServiceItem&& rhs)
: url(std::move(rhs.url))
, url_regex(std::move(rhs.url_regex))
, service(rhs.service) { // No move
}
RestServicePtr service;
// URL string, e.g., "/instances/(\\d+)".
std::string url;
// Compiled regex for URL string.
std::regex url_regex;
};
std::vector<ServiceItem> service_items_;
};
////////////////////////////////////////////////////////////////////////////////
class RestRequestHandler : public HttpRequestHandler {
public:
RestRequestHandler() = default;
// Register a REST service to the given URL path.
// The URL should start with "/" and could be a regular expression or not.
// E.g., "/instances". "/instances/(\\d+)"
bool RegisterService(RestServicePtr service, const std::string& url);
private:
HttpStatus::Enum HandleSession(HttpSessionPtr session) override;
private:
RestServiceManager service_manager_;
};
////////////////////////////////////////////////////////////////////////////////
class RestServer : public HttpServer {
public:
RestServer(unsigned short port, std::size_t workers);
~RestServer() override;
// Register a REST service to the given URL path.
// The URL should start with "/" and could be a regular expression or not.
// E.g., "/instances". "/instances/(\\d+)"
// NOTE: Registering to the same URL multiple times is allowed, but only the
// last one takes effect.
bool RegisterService(RestServicePtr service, const std::string& url);
private:
RestRequestHandler* rest_request_handler_;
};
} // namespace webcc
#endif // WEBCC_REST_SERVER_H_
+29
View File
@@ -0,0 +1,29 @@
#ifndef WEBCC_REST_SERVICE_H_
#define WEBCC_REST_SERVICE_H_
#include <string>
#include <memory>
#include "webcc/common.h"
namespace webcc {
// Base class for your REST service.
class RestService {
public:
virtual ~RestService() {
}
// Handle REST request, output the response.
// Both the request and response parameters should be JSON.
// TODO: Query parameters.
virtual bool Handle(const std::string& http_method,
const std::string& request,
std::string* response) = 0;
};
typedef std::shared_ptr<RestService> RestServicePtr;
} // namespace webcc
#endif // WEBCC_REST_SERVICE_H_
+71
View File
@@ -0,0 +1,71 @@
#include "webcc/soap_client.h"
#include <cassert>
#include "webcc/http_client.h"
#include "webcc/http_request.h"
#include "webcc/http_response.h"
#include "webcc/soap_request.h"
#include "webcc/soap_response.h"
namespace webcc {
Error SoapClient::Call(const std::string& operation,
std::vector<Parameter>&& parameters,
std::string* result) {
assert(service_ns_.IsValid());
assert(!url_.empty() && !host_.empty());
assert(!result_name_.empty());
if (!soapenv_ns_.IsValid()) {
soapenv_ns_ = kSoapEnvNamespace;
}
SoapRequest soap_request;
soap_request.set_soapenv_ns(soapenv_ns_);
soap_request.set_service_ns(service_ns_);
soap_request.set_operation(operation);
for (Parameter& p : parameters) {
soap_request.AddParameter(std::move(p));
}
std::string http_content;
soap_request.ToXml(&http_content);
HttpRequest http_request;
http_request.set_method(kHttpPost);
http_request.set_url(url_);
http_request.SetContentType(kTextXmlUtf8);
http_request.SetContentLength(http_content.size());
http_request.SetHost(host_, port_);
http_request.SetHeader(kSoapAction, operation);
http_request.set_content(std::move(http_content));
http_request.MakeStartLine();
HttpResponse http_response;
HttpClient http_client;
Error error = http_client.SendRequest(http_request, &http_response);
if (error != kNoError) {
return error;
}
SoapResponse soap_response;
soap_response.set_result_name(result_name_);
if (!soap_response.FromXml(http_response.content())) {
return kXmlError;
}
*result = soap_response.result();
return kNoError;
}
} // namespace webcc
+46
View File
@@ -0,0 +1,46 @@
#ifndef WEBCC_SOAP_CLIENT_H_
#define WEBCC_SOAP_CLIENT_H_
#include <string>
#include <vector>
#include "webcc/common.h"
namespace webcc {
// Base class for your SOAP client.
// Set URL, host, port, etc. in your sub-class before make the call.
//
class SoapClient {
public:
virtual ~SoapClient() {
}
protected:
SoapClient() {
}
// A generic wrapper to make a call.
// NOTE: The parameters should be movable.
Error Call(const std::string& operation,
std::vector<Parameter>&& parameters,
std::string* result);
protected:
Namespace soapenv_ns_; // SOAP envelope namespace.
Namespace service_ns_; // Namespace for your web service.
// Request URL.
std::string url_;
std::string host_;
std::string port_; // Leave this empty to use default 80.
// Response result XML node name.
// E.g., "Result".
std::string result_name_;
};
} // namespace webcc
#endif // WEBCC_SOAP_CLIENT_H_
+54
View File
@@ -0,0 +1,54 @@
#include "webcc/soap_message.h"
#include <cassert>
#include "webcc/xml.h"
namespace webcc {
void SoapMessage::ToXml(std::string* xml_string) {
assert(soapenv_ns_.IsValid() &&
service_ns_.IsValid() &&
!operation_.empty());
pugi::xml_document xdoc;
// TODO:
// When save with format_default, declaration will be generated
// automatically but without encoding.
// pugi::xml_node xdecl = xdoc.prepend_child(pugi::node_declaration);
// xdecl.append_attribute("version").set_value("1.0");
pugi::xml_node xroot = xml::AddChild(xdoc, soapenv_ns_.name, "Envelope");
xml::AddNSAttr(xroot, soapenv_ns_.name, soapenv_ns_.url);
pugi::xml_node xbody = xml::AddChild(xroot, soapenv_ns_.name, "Body");
ToXmlBody(xbody);
xml::XmlStrRefWriter writer(xml_string);
xdoc.save(writer, "\t", pugi::format_default, pugi::encoding_utf8);
}
bool SoapMessage::FromXml(const std::string& xml_string) {
pugi::xml_document xdoc;
pugi::xml_parse_result result = xdoc.load_string(xml_string.c_str());
if (!result) {
return false;
}
pugi::xml_node xroot = xdoc.document_element();
soapenv_ns_.name = xml::GetPrefix(xroot);
soapenv_ns_.url = xml::GetNSAttr(xroot, soapenv_ns_.name);
pugi::xml_node xbody = xml::GetChild(xroot, soapenv_ns_.name, "Body");
if (xbody) {
return FromXmlBody(xbody);
}
return false;
}
} // namespace webcc
+55
View File
@@ -0,0 +1,55 @@
#ifndef WEBCC_SOAP_MESSAGE_H_
#define WEBCC_SOAP_MESSAGE_H_
#include <string>
#include "pugixml/pugixml.hpp"
#include "webcc/common.h"
namespace webcc {
// Base class for SOAP request and response.
class SoapMessage {
public:
// E.g., set as kSoapEnvNamespace.
void set_soapenv_ns(const Namespace& soapenv_ns) {
soapenv_ns_ = soapenv_ns;
}
void set_service_ns(const Namespace& service_ns) {
service_ns_ = service_ns;
}
const std::string& operation() const {
return operation_;
}
void set_operation(const std::string& operation) {
operation_ = operation;
}
// Convert to SOAP request XML.
void ToXml(std::string* xml_string);
// Parse from SOAP request XML.
bool FromXml(const std::string& xml_string);
protected:
SoapMessage() {
}
// Convert to SOAP body XML.
virtual void ToXmlBody(pugi::xml_node xbody) = 0;
// Parse from SOAP body XML.
virtual bool FromXmlBody(pugi::xml_node xbody) = 0;
protected:
Namespace soapenv_ns_; // SOAP envelope namespace.
Namespace service_ns_; // Namespace for your web service.
std::string operation_;
};
} // namespace webcc
#endif // WEBCC_SOAP_MESSAGE_H_
+57
View File
@@ -0,0 +1,57 @@
#include "webcc/soap_request.h"
#include "webcc/xml.h"
namespace webcc {
void SoapRequest::AddParameter(const Parameter& parameter) {
parameters_.push_back(parameter);
}
void SoapRequest::AddParameter(Parameter&& parameter) {
parameters_.push_back(std::move(parameter));
}
const std::string& SoapRequest::GetParameter(const std::string& key) const {
for (const Parameter& p : parameters_) {
if (p.key() == key) {
return p.value();
}
}
static const std::string kEmptyValue;
return kEmptyValue;
}
void SoapRequest::ToXmlBody(pugi::xml_node xbody) {
pugi::xml_node xop = xml::AddChild(xbody, service_ns_.name, operation_);
xml::AddNSAttr(xop, service_ns_.name, service_ns_.url);
for (Parameter& p : parameters_) {
pugi::xml_node xparam = xml::AddChild(xop, service_ns_.name, p.key());
xparam.text().set(p.value().c_str());
}
}
bool SoapRequest::FromXmlBody(pugi::xml_node xbody) {
pugi::xml_node xoperation = xbody.first_child();
if (!xoperation) {
return false;
}
xml::SplitName(xoperation, &service_ns_.name, &operation_);
service_ns_.url = xml::GetNSAttr(xoperation, service_ns_.name);
pugi::xml_node xparameter = xoperation.first_child();
while (xparameter) {
parameters_.push_back({
xml::GetNameNoPrefix(xparameter),
std::string(xparameter.text().as_string())
});
xparameter = xparameter.next_sibling();
}
return true;
}
} // namespace webcc
+31
View File
@@ -0,0 +1,31 @@
#ifndef WEBCC_SOAP_REQUEST_H_
#define WEBCC_SOAP_REQUEST_H_
#include <vector>
#include "webcc/soap_message.h"
namespace webcc {
// SOAP request.
// Used to compose the SOAP request envelope XML which will be sent as the HTTP
// request body.
class SoapRequest : public SoapMessage {
public:
void AddParameter(const Parameter& parameter);
void AddParameter(Parameter&& parameter);
// Get parameter value by key.
const std::string& GetParameter(const std::string& key) const;
protected:
void ToXmlBody(pugi::xml_node xbody) override;
bool FromXmlBody(pugi::xml_node xbody) override;
private:
std::vector<Parameter> parameters_;
};
} // namespace webcc
#endif // WEBCC_SOAP_REQUEST_H_
+35
View File
@@ -0,0 +1,35 @@
#include "webcc/soap_response.h"
#include <cassert>
#include "webcc/xml.h"
namespace webcc {
void SoapResponse::ToXmlBody(pugi::xml_node xbody) {
std::string rsp_operation = operation_ + "Response";
pugi::xml_node xop = xml::AddChild(xbody, service_ns_.name, rsp_operation);
xml::AddNSAttr(xop, service_ns_.name, service_ns_.url);
pugi::xml_node xresult = xml::AddChild(xop, service_ns_.name, result_name_);
xresult.text().set(result_.c_str());
}
bool SoapResponse::FromXmlBody(pugi::xml_node xbody) {
assert(!result_name_.empty());
pugi::xml_node xresponse = xbody.first_child();
if (xresponse) {
xml::SplitName(xresponse, &service_ns_.name, NULL);
service_ns_.url = xml::GetNSAttr(xresponse, service_ns_.name);
pugi::xml_node xresult = xml::GetChildNoNS(xresponse, result_name_);
if (xresult) {
result_ = xresult.text().get();
return true;
}
}
return false;
}
} // namespace webcc
+50
View File
@@ -0,0 +1,50 @@
#ifndef WEBCC_SOAP_RESPONSE_H_
#define WEBCC_SOAP_RESPONSE_H_
#include "webcc/soap_message.h"
namespace webcc {
// SOAP response.
class SoapResponse : public SoapMessage {
public:
// Could be "Price" for an operation/method like "GetXyzPrice".
// Really depend on the service.
// Most services use a general name "Result".
void set_result_name(const std::string& result_name) {
result_name_ = result_name;
}
const std::string& result() const {
return result_;
}
void set_result(const std::string& result) {
result_ = result;
}
void set_result(std::string&& result) {
result_ = std::move(result);
}
protected:
void ToXmlBody(pugi::xml_node xbody) override;
bool FromXmlBody(pugi::xml_node xbody) override;
private:
// NOTE:
// Multiple results might be necessary. But for most cases, single result
// should be enough, because an API normally returns only one value.
// Result XML node name.
// Used to parse the response XML from client side.
std::string result_name_;
// Result value.
std::string result_;
};
} // namespace webcc
#endif // WEBCC_SOAP_RESPONSE_H_
+89
View File
@@ -0,0 +1,89 @@
#include "webcc/soap_server.h"
#if WEBCC_DEBUG_OUTPUT
#include <iostream>
#endif
#include "webcc/soap_request.h"
#include "webcc/soap_response.h"
namespace webcc {
////////////////////////////////////////////////////////////////////////////////
bool SoapRequestHandler::RegisterService(SoapServicePtr service,
const std::string& url) {
assert(service);
url_service_map_[url] = service;
return true;
}
HttpStatus::Enum SoapRequestHandler::HandleSession(HttpSessionPtr session) {
SoapServicePtr service = GetServiceByUrl(session->request().url());
if (!service) {
session->SetResponseStatus(HttpStatus::kBadRequest);
session->SendResponse();
return HttpStatus::kBadRequest;
}
// Parse the SOAP request XML.
SoapRequest soap_request;
if (!soap_request.FromXml(session->request().content())) {
session->SetResponseStatus(HttpStatus::kBadRequest);
session->SendResponse();
return HttpStatus::kBadRequest;
}
// TODO: Error handling.
SoapResponse soap_response;
service->Handle(soap_request, &soap_response);
std::string content;
soap_response.ToXml(&content);
session->SetResponseStatus(HttpStatus::kOK);
session->SetResponseContent(kTextXmlUtf8,
content.length(),
std::move(content));
session->SendResponse();
return HttpStatus::kOK;
}
SoapServicePtr SoapRequestHandler::GetServiceByUrl(const std::string& url) {
UrlServiceMap::const_iterator it = url_service_map_.find(url);
if (it != url_service_map_.end()) {
#if WEBCC_DEBUG_OUTPUT
std::cout << "Service matches the URL: " << url << std::endl;
#endif
return it->second;
}
#if WEBCC_DEBUG_OUTPUT
std::cout << "No service matches the URL: " << url << std::endl;
#endif
return SoapServicePtr();
}
////////////////////////////////////////////////////////////////////////////////
SoapServer::SoapServer(unsigned short port, std::size_t workers)
: HttpServer(port, workers)
, soap_request_handler_(new SoapRequestHandler()) {
request_handler_ = soap_request_handler_;
}
SoapServer::~SoapServer() {
request_handler_ = NULL;
delete soap_request_handler_;
}
bool SoapServer::RegisterService(SoapServicePtr service,
const std::string& url) {
return soap_request_handler_->RegisterService(service, url);
}
} // namespace webcc
+52
View File
@@ -0,0 +1,52 @@
#ifndef WEBCC_SOAP_SERVER_H_
#define WEBCC_SOAP_SERVER_H_
// HTTP server handling SOAP requests.
#include <map>
#include <string>
#include "webcc/http_request_handler.h"
#include "webcc/http_server.h"
namespace webcc {
////////////////////////////////////////////////////////////////////////////////
class SoapRequestHandler : public HttpRequestHandler {
public:
SoapRequestHandler() = default;
// Register a SOAP service to the given URL path.
// \url URL path, must start with "/". E.g., "/calculator".
// NOTE: Registering to the same URL multiple times is allowed, but only the
// last one takes effect.
bool RegisterService(SoapServicePtr service, const std::string& url);
private:
HttpStatus::Enum HandleSession(HttpSessionPtr session) override;
SoapServicePtr GetServiceByUrl(const std::string& url);
private:
typedef std::map<std::string, SoapServicePtr> UrlServiceMap;
UrlServiceMap url_service_map_;
};
////////////////////////////////////////////////////////////////////////////////
class SoapServer : public HttpServer {
public:
SoapServer(unsigned short port, std::size_t workers);
~SoapServer() override;
bool RegisterService(SoapServicePtr service, const std::string& url);
private:
SoapRequestHandler* soap_request_handler_;
};
} // namespace webcc
#endif // WEBCC_SOAP_SERVER_H_
+26
View File
@@ -0,0 +1,26 @@
#ifndef WEBCC_SOAP_SERVICE_H_
#define WEBCC_SOAP_SERVICE_H_
#include <memory>
namespace webcc {
class SoapRequest;
class SoapResponse;
// Base class for your SOAP service.
class SoapService {
public:
virtual ~SoapService() {
}
// Handle SOAP request, output the response.
virtual bool Handle(const SoapRequest& soap_request,
SoapResponse* soap_response) = 0;
};
typedef std::shared_ptr<SoapService> SoapServicePtr;
} // namespace webcc
#endif // WEBCC_SOAP_SERVICE_H_
+76
View File
@@ -0,0 +1,76 @@
#include "webcc/url.h"
#include <sstream>
namespace webcc {
Url::Url(const std::string& str) {
std::size_t pos = str.find('?');
if (pos == std::string::npos) {
path_ = str;
} else {
path_ = str.substr(0, pos);
query_ = str.substr(pos + 1);
}
}
bool Url::IsValid() const {
return !path_.empty();
}
std::vector<std::string> Url::SplitPath(const std::string& path) {
std::vector<std::string> results;
std::stringstream iss(path);
std::string s;
while (std::getline(iss, s, '/')) {
if (!s.empty()) {
results.push_back(s);
}
}
return results;
}
static bool SplitKeyValue(const std::string& kv,
std::string* key,
std::string* value) {
std::size_t i = kv.find_first_of('=');
if (i == std::string::npos || i == 0) {
return false;
}
*key = kv.substr(0, i);
*value = kv.substr(i + 1);
return true;
}
// static
Url::Query Url::SplitQuery(const std::string& query) {
const std::size_t NPOS = std::string::npos;
Query result;
// Split into key value pairs separated by '&'.
std::size_t i = 0;
while (i != NPOS) {
std::size_t j = query.find_first_of('&', i);
std::string kv;
if (j == NPOS) {
kv = query.substr(i);
i = NPOS;
} else {
kv = query.substr(i, j - i);
i = j + 1;
}
std::string key;
std::string value;
if (SplitKeyValue(kv, &key, &value)) {
result[key] = value; // TODO: Move
}
}
return result;
}
} // namespace webcc
+53
View File
@@ -0,0 +1,53 @@
#ifndef WEBCC_URL_H_
#define WEBCC_URL_H_
// A simplified implementation of URL (or URI).
// The URL should start with "/".
// The parameters (separated by ";") are not supported.
// Example:
// /inventory-check.cgi?item=12731&color=blue&size=large
#include <map>
#include <string>
#include <vector>
namespace webcc {
class Url {
public:
typedef std::map<std::string, std::string> Query;
Url(const std::string& str);
bool IsValid() const;
const std::string& path() const {
return path_;
}
void set_path(const std::string& path) {
path_ = path;
}
const std::string& query() const {
return query_;
}
void set_query(const std::string& query) {
query_ = query;
}
// Split a path into its hierarchical components.
static std::vector<std::string> SplitPath(const std::string& path);
// Split query string into key-value map.
static Query SplitQuery(const std::string& query);
private:
std::string path_;
std::string query_;
};
} // namespace webcc
#endif // WEBCC_URL_H_
+28
View File
@@ -0,0 +1,28 @@
#include "webcc/utility.h"
#include <iostream>
using tcp = boost::asio::ip::tcp;
namespace webcc {
// Print the resolved endpoints.
// NOTE: Endpoint is one word, don't use "end point".
// TODO
void DumpEndpoints(tcp::resolver::results_type& endpoints) {
std::cout << "Endpoints: " << endpoints.size() << std::endl;
tcp::resolver::results_type::iterator it = endpoints.begin();
for (; it != endpoints.end(); ++it) {
std::cout << " - " << it->endpoint();
if (it->endpoint().protocol() == tcp::v4()) {
std::cout << ", v4";
} else if (it->endpoint().protocol() == tcp::v6()) {
std::cout << ", v6";
}
std::cout << std::endl;
}
}
} // namespace webcc
+14
View File
@@ -0,0 +1,14 @@
#ifndef WEBCC_UTILITY_H_
#define WEBCC_UTILITY_H_
#include "boost/asio/ip/tcp.hpp"
namespace webcc {
// Print the resolved endpoints.
// NOTE: Endpoint is one word, don't use "end point".
void DumpEndpoints(boost::asio::ip::tcp::resolver::results_type& endpoints);
} // namespace webcc
#endif // WEBCC_UTILITY_H_
+109
View File
@@ -0,0 +1,109 @@
#include "webcc/xml.h"
namespace webcc {
namespace xml {
void SplitName(const pugi::xml_node& xnode,
std::string* prefix,
std::string* name) {
std::string full_name = xnode.name();
size_t pos = full_name.find(':');
if (pos != std::string::npos) {
if (prefix != NULL) {
*prefix = full_name.substr(0, pos);
}
if (name != NULL) {
*name = full_name.substr(pos + 1);
}
} else {
if (prefix != NULL) {
*prefix = "";
}
if (name != NULL) {
*name = full_name;
}
}
}
std::string GetPrefix(const pugi::xml_node& xnode) {
std::string ns_prefix;
SplitName(xnode, &ns_prefix, nullptr);
return ns_prefix;
}
std::string GetNameNoPrefix(const pugi::xml_node& xnode) {
std::string name;
SplitName(xnode, nullptr, &name);
return name;
}
pugi::xml_node AddChild(pugi::xml_node& xnode,
const std::string& ns,
const std::string& name) {
return xnode.append_child((ns + ":" + name).c_str());
}
pugi::xml_node GetChild(pugi::xml_node& xnode,
const std::string& ns,
const std::string& name) {
return xnode.child((ns + ":" + name).c_str());
}
pugi::xml_node GetChildNoNS(pugi::xml_node& xnode, const std::string& name) {
pugi::xml_node xchild = xnode.first_child();
while (xchild) {
std::string child_name = xchild.name();
// Remove NS prefix.
size_t pos = child_name.find(':');
if (pos != std::string::npos) {
child_name = child_name.substr(pos + 1);
}
if (child_name == name) {
return xchild;
}
xchild = xchild.next_sibling();
}
return pugi::xml_node();
}
void AddAttr(pugi::xml_node& xnode,
const std::string& ns,
const std::string& name,
const std::string& value) {
std::string ns_name = ns + ":" + name;
xnode.append_attribute(ns_name.c_str()) = value.c_str();
}
void AddNSAttr(pugi::xml_node& xnode,
const std::string& ns_name,
const std::string& ns_url) {
AddAttr(xnode, "xmlns", ns_name, ns_url);
}
std::string GetNSAttr(pugi::xml_node& xnode,
const std::string& ns_name) {
std::string attr_name = "xmlns:" + ns_name;
return xnode.attribute(attr_name.c_str()).as_string();
}
bool PrettyPrintXml(std::ostream& os,
const std::string& xml_string,
const char* indent) {
pugi::xml_document xdoc;
if (!xdoc.load_string(xml_string.c_str())) {
os << "Invalid XML" << std::endl;
return false;
}
xdoc.save(os, indent);
return true;
}
} // namespace xml
} // namespace webcc
+91
View File
@@ -0,0 +1,91 @@
#ifndef WEBCC_XML_H_
#define WEBCC_XML_H_
// XML utilities.
#include <string>
#include "pugixml/pugixml.hpp"
namespace webcc {
namespace xml {
// Split the node name into namespace prefix and real name.
// E.g., if the node name is "soapenv:Envelope", it will be splited to
// "soapenv" and "Envelope".
void SplitName(const pugi::xml_node& xnode,
std::string* prefix = NULL,
std::string* name = NULL);
// Get the namespace prefix from node name.
// E.g., if the node name is "soapenv:Envelope", NS prefix will be "soapenv".
std::string GetPrefix(const pugi::xml_node& xnode);
// Get the node name without namespace prefix.
std::string GetNameNoPrefix(const pugi::xml_node& xnode);
// Add a child with the given name which is prefixed by a namespace.
// E.g., AppendChild(xnode, "soapenv", "Envelope") will append a child with
// name "soapenv:Envelope".
pugi::xml_node AddChild(pugi::xml_node& xnode,
const std::string& ns,
const std::string& name);
pugi::xml_node GetChild(pugi::xml_node& xnode,
const std::string& ns,
const std::string& name);
// TODO: Remove
pugi::xml_node GetChildNoNS(pugi::xml_node& xnode,
const std::string& name);
// Add an attribute with the given name which is prefixed by a namespace.
void AddAttr(pugi::xml_node& xnode,
const std::string& ns,
const std::string& name,
const std::string& value);
// Append "xmlns" attribute.
// E.g., if the namespace is
// { "soapenv", "http://schemas.xmlsoap.org/soap/envelope/" }
// the attribute added will be
// xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
void AddNSAttr(pugi::xml_node& xnode,
const std::string& ns_name,
const std::string& ns_url);
// Get namespace attribute value.
// E.g., if the given namespace name is "soapenv", the value of
// attribute "xmlns:soapenv" will be returned.
std::string GetNSAttr(pugi::xml_node& xnode,
const std::string& ns_name);
// An XML writer writing to a referenced string.
// Example:
// pugi::xml_document xdoc;
// ...
// std::string xml_string;
// XmlStrRefWriter writer(&xml_string);
// xdoc.save(writer, "\t", pugi::format_default, pugi::encoding_utf8);
class XmlStrRefWriter : public pugi::xml_writer {
public:
explicit XmlStrRefWriter(std::string* result)
: result_(result) {
result_->clear();
}
virtual void write(const void* data, size_t size) override {
result_->append(static_cast<const char*>(data), size);
}
private:
std::string* result_;
};
bool PrettyPrintXml(std::ostream& os,
const std::string& xml_string,
const char* indent = "\t");
} // namespace xml
} // namespace webcc
#endif // WEBCC_XML_H_