add: exercise answer for chapter 6 and 7

This commit is contained in:
Changkun Ou
2018-05-06 14:58:02 +02:00
parent d75f10db41
commit 70250c2b0a
11 changed files with 668 additions and 0 deletions

38
exercises/6/Makefile Normal file
View File

@@ -0,0 +1,38 @@
#
# Makefile
# web_server
#
# created by changkun at labex.io
#
CXX = g++
EXEC_HTTP = server.http
EXEC_HTTPS = server.https
SOURCE_HTTP = main.http.cpp
SOURCE_HTTPS = main.https.cpp
OBJECTS_HTTP = main.http.o
OBJECTS_HTTPS = main.https.o
LDFLAGS_COMMON = -std=c++11 -O3 -pthread -lboost_system
LDFLAGS_HTTP =
LDFLAGS_HTTPS = -lssl -lcrypto
LPATH_COMMON = -I/usr/include/boost
LPATH_HTTP =
LPATH_HTTPS = -I/usr/include/openssl
LLIB_COMMON = -L/usr/lib
all:
make http
make https
http:
$(CXX) $(SOURCE_HTTP) $(LDFLAGS_COMMON) $(LDFLAGS_HTTP) $(LPATH_COMMON) $(LPATH_HTTP) $(LLIB_COMMON) $(LLIB_HTTP) -o $(EXEC_HTTP)
https:
$(CXX) $(SOURCE_HTTPS) $(LDFLAGS_COMMON) $(LDFLAGS_HTTPS) $(LPATH_COMMON) $(LPATH_HTTPS) $(LLIB_COMMON) $(LLIB_HTTPS) -o $(EXEC_HTTPS)
clean:
rm -f $(EXEC_HTTP) $(EXEC_HTTPS) *.o

96
exercises/6/handler.hpp Normal file
View File

@@ -0,0 +1,96 @@
//
// handler.hpp
// web_server
// created by changkun at changkun.de/modern-cpp
//
#include "server.base.hpp"
#include <fstream>
using namespace std;
using namespace LabexWeb;
template<typename SERVER_TYPE>
void start_server(SERVER_TYPE &server) {
// resources request
// processing POST /string, return the string from POST
server.resource["^/string/?$"]["POST"] = [](ostream& response, Request& request) {
// fetch string from istream (*request.content)
stringstream ss;
*request.content >> ss.rdbuf(); // read request to stringstream
string content=ss.str();
// return response
response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" << content;
};
// process GET request from /info, return response
server.resource["^/info/?$"]["GET"] = [](ostream& response, Request& request) {
stringstream content_stream;
content_stream << "<h1>Request:</h1>";
content_stream << request.method << " " << request.path << " HTTP/" << request.http_version << "<br>";
for(auto& header: request.header) {
content_stream << header.first << ": " << header.second << "<br>";
}
// get the length of content_stream (use content.tellp() to get)
content_stream.seekp(0, ios::end);
response << "HTTP/1.1 200 OK\r\nContent-Length: " << content_stream.tellp() << "\r\n\r\n" << content_stream.rdbuf();
};
// process GET request for /match/[digit+numbers], e.g. GET request is /match/abc123, will return abc123
server.resource["^/match/([0-9a-zA-Z]+)/?$"]["GET"] = [](ostream& response, Request& request) {
string number=request.path_match[1];
response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" << number;
};
// peocess default GET request; anonymous function will be called if no other matches
// response files in folder web/
// default: index.html
server.default_resource["^/?(.*)$"]["GET"] = [](ostream& response, Request& request) {
string filename = "www/";
string path = request.path_match[1];
// forbidden use `..` access content outside folder web/
size_t last_pos = path.rfind(".");
size_t current_pos = 0;
size_t pos;
while((pos=path.find('.', current_pos)) != string::npos && pos != last_pos) {
current_pos = pos;
path.erase(pos, 1);
last_pos--;
}
filename += path;
ifstream ifs;
// folder inspection across platform
if(filename.find('.') == string::npos) {
if(filename[filename.length()-1]!='/')
filename+='/';
filename += "index.html";
}
ifs.open(filename, ifstream::in);
if(ifs) {
ifs.seekg(0, ios::end);
size_t length=ifs.tellg();
ifs.seekg(0, ios::beg);
// copy file to response-stream , shouldn't use for large files
response << "HTTP/1.1 200 OK\r\nContent-Length: " << length << "\r\n\r\n" << ifs.rdbuf();
ifs.close();
} else {
// return unable to open if file doesn't exists
string content="Could not open file "+filename;
response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << content.length() << "\r\n\r\n" << content;
}
};
// start HTTP(S) server
server.start();
}

19
exercises/6/main.http.cpp Normal file
View File

@@ -0,0 +1,19 @@
//
// main_http.cpp
// web_server
// created by changkun at changkun.de/modern-cpp
//
#include <iostream>
#include "server.http.hpp"
#include "handler.hpp"
using namespace LabexWeb;
int main() {
// HTTP server runs in port 12345 HTTP, enable 4 threads
Server<HTTP> server(12345, 4);
std::cout << "Server starting at port: 12345" << std::endl;
start_server<Server<HTTP>>(server);
return 0;
}

View File

@@ -0,0 +1,18 @@
//
// main_https.cpp
// web_server
// created by changkun at changkun.de/modern-cpp
//
#include <iostream>
#include "server.https.hpp"
#include "handler.hpp"
using namespace LabexWeb;
int main() {
// HTTPS server runs in port 12345, enable 4 threads
// Use certificates for security
Server<HTTPS> server(12345, 4, "server.crt", "server.key");
std::cout << "Server starting at port: 12345" << std::endl;
start_server<Server<HTTPS>>(server);
return 0;
}

197
exercises/6/server.base.hpp Normal file
View File

@@ -0,0 +1,197 @@
//
// server_base.hpp
// web_server
// created by changkun at changkun.de/modern-cpp
//
#ifndef SERVER_BASE_HPP
#define SERVER_BASE_HPP
#include <boost/asio.hpp>
#include <regex>
#include <unordered_map>
#include <thread>
namespace LabexWeb {
struct Request {
// request method, POST, GET; path; HTTP version
std::string method, path, http_version;
// use smart pointer for reference counting of content
std::shared_ptr<std::istream> content;
// hash container, key-value dict
std::unordered_map<std::string, std::string> header;
// use regular expression for path match
std::smatch path_match;
};
// use typedef simplify resource type
typedef std::map<std::string, std::unordered_map<std::string,
std::function<void(std::ostream&, Request&)>>> resource_type;
// socket_type is HTTP or HTTPS
template <typename socket_type>
class ServerBase {
public:
resource_type resource;
resource_type default_resource;
// construct server, initalize port, default: 1 thread
ServerBase(unsigned short port, size_t num_threads = 1) :
endpoint(boost::asio::ip::tcp::v4(), port),
acceptor(m_io_service, endpoint),
num_threads(num_threads) {}
void start() {
// default resource in the end of vector, as response method
for(auto it = resource.begin(); it != resource.end(); it++) {
all_resources.push_back(it);
}
for(auto it = default_resource.begin(); it != default_resource.end(); it++) {
all_resources.push_back(it);
}
// socket connection
accept();
// if num_threads>1, then m_io_service.run()
// it will start (num_threads-1) threads as thread pool
for(size_t c = 1;c < num_threads; c++) {
threads.emplace_back([this](){
m_io_service.run();
});
}
// main thread
m_io_service.run();
// wait for other threads finish
for(auto& t: threads)
t.join();
}
protected:
// io_service is a dispatcher in asio library, all asynchronous io events are dispatched by it
// in another word, constructor of IO object need a io_service object as parameter
boost::asio::io_service m_io_service;
// IP address, port and protocol version as a endpoint, and generated on serverside
// tcp::acceptor object, wait for connection
boost::asio::ip::tcp::endpoint endpoint;
// thus, a acceptor object requires io_service and endpoint as parameters
boost::asio::ip::tcp::acceptor acceptor;
// server side threads
size_t num_threads;
std::vector<std::thread> threads;
// all resource will be append to the end of vector, and created in start()
std::vector<resource_type::iterator> all_resources;
// requires to implement this method for different type of server
virtual void accept() {}
void process_request_and_respond(std::shared_ptr<socket_type> socket) const {
// created cache for async_read_untile()
// shared_ptr will use for passing object to anonymous function
// the type will be deduce as std::shared_ptr<boost::asio::streambuf>
auto read_buffer = std::make_shared<boost::asio::streambuf>();
boost::asio::async_read_until(*socket, *read_buffer, "\r\n\r\n",
[this, socket, read_buffer](const boost::system::error_code& ec, size_t bytes_transferred) {
if(!ec) {
// Note: read_buffer->size() always equal to bytes_transferred, the document of Boost indicates:
// after async_read_until operation, streambuf contains some extra data out of delimiter
// thus, the best way is to read and parse the content from the left of read_buffer, then append the content of async_read
size_t total = read_buffer->size();
// convert istream to string-lines
std::istream stream(read_buffer.get());
// deduce the type of std::shared_ptr<Request>
auto request = std::make_shared<Request>();
*request = parse_request(stream);
size_t num_additional_bytes = total-bytes_transferred;
// if satisfy then also read
if(request->header.count("Content-Length")>0) {
boost::asio::async_read(*socket, *read_buffer,
boost::asio::transfer_exactly(stoull(request->header["Content-Length"]) - num_additional_bytes),
[this, socket, read_buffer, request](const boost::system::error_code& ec, size_t bytes_transferred) {
if(!ec) {
// pointer as istream object stored in read_buffer
request->content = std::shared_ptr<std::istream>(new std::istream(read_buffer.get()));
respond(socket, request);
}
});
} else {
respond(socket, request);
}
}
});
}
Request parse_request(std::istream& stream) const {
Request request;
std::regex e("^([^ ]*) ([^ ]*) HTTP/([^ ]*)$");
std::smatch sub_match;
// read method, path and http version from the frist line
std::string line;
getline(stream, line);
line.pop_back();
if(std::regex_match(line, sub_match, e)) {
request.method = sub_match[1];
request.path = sub_match[2];
request.http_version = sub_match[3];
bool matched;
e="^([^:]*): ?(.*)$";
// parse head information
do {
getline(stream, line);
line.pop_back();
matched=std::regex_match(line, sub_match, e);
if(matched) {
request.header[sub_match[1]] = sub_match[2];
}
} while(matched==true);
}
return request;
}
void respond(std::shared_ptr<socket_type> socket, std::shared_ptr<Request> request) const {
// response after search requested path and method
for(auto res_it: all_resources) {
std::regex e(res_it->first);
std::smatch sm_res;
if(std::regex_match(request->path, sm_res, e)) {
if(res_it->second.count(request->method)>0) {
request->path_match = move(sm_res);
// will be deduce to std::shared_ptr<boost::asio::streambuf>
auto write_buffer = std::make_shared<boost::asio::streambuf>();
std::ostream response(write_buffer.get());
res_it->second[request->method](response, *request);
// capture write_buffer in lambda, make sure it can be destroyed after async_write
boost::asio::async_write(*socket, *write_buffer,
[this, socket, request, write_buffer](const boost::system::error_code& ec, size_t bytes_transferred) {
// HTTP 1.1 connection
if(!ec && stof(request->http_version)>1.05)
process_request_and_respond(socket);
});
return;
}
}
}
}
};
template<typename socket_type>
class Server : public ServerBase<socket_type> {};
}
#endif /* SERVER_BASE_HPP */

View File

@@ -0,0 +1,38 @@
//
// server_http.hpp
// web_server
// created by changkun at changkun.de/modern-cpp
//
#ifndef SERVER_HTTP_HPP
#define SERVER_HTTP_HPP
#include "server.base.hpp"
namespace LabexWeb {
typedef boost::asio::ip::tcp::socket HTTP;
template<>
class Server<HTTP> : public ServerBase<HTTP> {
public:
// use port, thread number to construct web server
// http server is much simple than https since it doesn't need to initial config file
Server(unsigned short port, size_t num_threads=1) :
ServerBase<HTTP>::ServerBase(port, num_threads) {};
private:
// implement accept() method
void accept() {
// create a new socket for current connection
// shared_ptr is used for passing temporal object to anonymous function
// socket will be deduce as type of std::shared_ptr<HTTP>
auto socket = std::make_shared<HTTP>(m_io_service);
acceptor.async_accept(*socket, [this, socket](const boost::system::error_code& ec) {
// establish a connection
accept();
// if no error
if(!ec) process_request_and_respond(socket);
});
}
};
}
#endif /* SERVER_HTTP_HPP */

View File

@@ -0,0 +1,65 @@
//
// server_https.hpp
// web_server
// created by changkun at changkun.de/modern-cpp
//
#ifndef SERVER_HTTPS_HPP
#define SERVER_HTTPS_HPP
#include "server.http.hpp"
#include <boost/asio/ssl.hpp>
namespace LabexWeb {
// define HTTPS type
typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> HTTPS;
// define HTTPS service, template type is HTTPS
template<>
class Server<HTTPS> : public ServerBase<HTTPS> {
public:
// a HTTPS server requires two more parameters: certificate file and private key file
Server(unsigned short port, size_t num_threads,
const std::string& cert_file, const std::string& private_key_file) :
ServerBase<HTTPS>::ServerBase(port, num_threads),
context(boost::asio::ssl::context::sslv23) {
// use certificate file
context.use_certificate_chain_file(cert_file);
// use private key file, we need pass a new parameter to specify the format
context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem);
}
private:
// compare to HTTP server, we must define ssl context object
boost::asio::ssl::context context;
// the difference between HTTPS and HTTP server
// is the construct difference of socket object
// HTTPS will encrypt the IO stream socket
// thus, accept() method must initialize ssl context
void accept() {
// create a new socket for current connection
// shared_ptr is used for passing temporal object to anonymous function
// socket will be deduce as std::shared_ptr<HTTPS>
auto socket = std::make_shared<HTTPS>(m_io_service, context);
acceptor.async_accept(
(*socket).lowest_layer(),
[this, socket](const boost::system::error_code& ec) {
// accept a new connection
accept();
// if no error
if(!ec) {
(*socket).async_handshake(boost::asio::ssl::stream_base::server,
[this, socket](const boost::system::error_code& ec) {
if(!ec) process_request_and_respond(socket);
});
}
});
}
};
}
#endif /* SERVER_HTTPS_HPP */

View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>LabEx Web Server Test</title>
</head>
<body>
Hello world in index.html.
</body>
</html>

View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>LabEx Web Server Test</title>
</head>
<body>
Hello world in test.html.
</body>
</html>