From: Mikko Rasa Date: Wed, 10 Aug 2011 18:14:42 +0000 (+0300) Subject: Merge branch 'http-master' X-Git-Url: http://git.tdb.fi/?p=libs%2Fnet.git;a=commitdiff_plain;h=debe1004676d5431e571d9c4361072661dcc88c4;hp=50e9f9ea7f7385a2c5931fca2b8fb1103078e67c Merge branch 'http-master' Conflicts: .gitignore Build --- diff --git a/Build b/Build index 444fc30..1ed9db8 100644 --- a/Build +++ b/Build @@ -15,9 +15,16 @@ package "mspnet" install true; }; + headers "msp/http" + { + source "source/http"; + install true; + }; + library "mspnet" { source "source/net"; + source "source/http"; install true; }; diff --git a/source/http/client.cpp b/source/http/client.cpp new file mode 100644 index 0000000..0729544 --- /dev/null +++ b/source/http/client.cpp @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include "client.h" +#include "request.h" +#include "response.h" + +using namespace std; + +namespace Msp { +namespace Http { + +Client::Client(): + sock(0), + event_disp(0), + user_agent("libmsphttp/0.1"), + request(0), + response(0) +{ } + +Client::~Client() +{ + delete sock; + delete request; + delete response; +} + +void Client::use_event_dispatcher(IO::EventDispatcher *ed) +{ + if(event_disp && sock) + event_disp->remove(*sock); + event_disp = ed; + if(event_disp && sock) + event_disp->add(*sock); +} + +void Client::start_request(const Request &r) +{ + if(request) + throw InvalidState("Already processing a request"); + + string host = r.get_header("Host"); + if(host.find(':')==string::npos) + host += ":80"; + RefPtr addr = Net::resolve(host); + + delete sock; + sock = new Net::StreamSocket(addr->get_family()); + sock->set_block(false); + + sock->signal_data_available.connect(sigc::mem_fun(this, &Client::data_available)); + sock->signal_connect_finished.connect(sigc::mem_fun(this, &Client::connect_finished)); + if(event_disp) + event_disp->add(*sock); + + sock->connect(*addr); + + request = new Request(r); + if(!user_agent.empty()) + request->set_header("User-Agent", user_agent); + + delete response; + response = 0; + in_buf.clear(); +} + +const Response *Client::get_url(const std::string &url) +{ + start_request(Request::from_url(url)); + wait_response(); + return response; +} + +void Client::tick() +{ + if(!request) + return; + + while(IO::PollEvent ev = IO::poll(*sock, sock->get_events(), Time::zero)) + sock->event(ev); + + if(response && response->is_complete()) + { + signal_response_complete.emit(*response); + + delete sock; + sock = 0; + delete request; + request = 0; + } +} + +void Client::wait_response() +{ + while(request && (!response || !response->is_complete())) + tick(); +} + +void Client::abort() +{ + delete sock; + sock = 0; + delete request; + request = 0; +} + +void Client::connect_finished(const exception *err) +{ + if(err) + { + signal_socket_error.emit(err); + + delete request; + request = 0; + } + else + sock->write(request->str()); +} + +void Client::data_available() +{ + char rbuf[4096]; + unsigned len; + try + { + len = sock->read(rbuf, sizeof(rbuf)); + } + catch(const exception &e) + { + signal_socket_error.emit(&e); + return; + } + + if(!len) + return; + in_buf.append(rbuf, len); + + if(!response) + { + if(in_buf.find("\r\n\r\n")!=string::npos || in_buf.find("\n\n")!=string::npos) + { + response = new Response(Response::parse(in_buf)); + response->set_user_data(request->get_user_data()); + in_buf = string(); + } + } + else + { + len = response->parse_content(in_buf); + in_buf.erase(0, len); + } + + if(response && response->is_complete()) + { + signal_response_complete.emit(*response); + + delete request; + request = 0; + } +} + +} // namespace Http +} // namespace Msp diff --git a/source/http/client.h b/source/http/client.h new file mode 100644 index 0000000..ee10f8c --- /dev/null +++ b/source/http/client.h @@ -0,0 +1,52 @@ +#ifndef MSP_HTTP_CLIENT_H_ +#define MSP_HTTP_CLIENT_H_ + +#include +#include +#include +#include + +namespace Msp { +namespace Http { + +class Request; +class Response; + +class Client +{ +public: + sigc::signal signal_response_complete; + sigc::signal signal_socket_error; + +private: + Net::StreamSocket *sock; + IO::EventDispatcher *event_disp; + std::string user_agent; + Request *request; + Response *response; + std::string in_buf; + + Client(const Client &); + Client &operator=(const Client &); +public: + Client(); + ~Client(); + + void use_event_dispatcher(IO::EventDispatcher *); + void set_user_agent(const std::string &); + void start_request(const Request &); + const Response *get_url(const std::string &); + void tick(); + void wait_response(); + void abort(); + const Request *get_request() const { return request; } + const Response *get_response() const { return response; } +private: + void connect_finished(const std::exception *); + void data_available(); +}; + +} // namespace Http +} // namespace Msp + +#endif diff --git a/source/http/message.cpp b/source/http/message.cpp new file mode 100644 index 0000000..2b3e40b --- /dev/null +++ b/source/http/message.cpp @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include "message.h" + +using namespace std; + +namespace Msp { +namespace Http { + +Message::Message(): + http_version(0x11), + chunk_length(0), + complete(false) +{ } + +void Message::set_header(const string &hdr, const string &val) +{ + headers[normalize_header_name(hdr)] = val; +} + +bool Message::has_header(const string &hdr) const +{ + return headers.count(normalize_header_name(hdr)); +} + +const string &Message::get_header(const string &hdr) const +{ + return get_item(headers, normalize_header_name(hdr)); +} + +void Message::add_content(const string &d) +{ + content += d; + if(headers.count("Content-Type")==0) + set_header("Content-Type", "text/plain"); + set_header("Content-Length", lexical_cast(content.size())); +} + +void Message::set_user_data(const Variant &d) +{ + user_data = d; +} + +unsigned Message::parse_content(const string &d) +{ + if(complete) + return 0; + + HeaderMap::const_iterator i = headers.find("Content-Length"); + if(i!=headers.end()) + { + unsigned needed = lexical_cast(i->second)-content.size(); + unsigned len = min(needed, d.size()); + + content.append(d, 0, len); + + if(len==needed) + complete = true; + + return len; + } + + i = headers.find("Transfer-Encoding"); + if(i!=headers.end() && strcasecmp(i->second, "chunked")==0) + { + unsigned pos = 0; + while(!complete && pos(strip(d.substr(pos, lf-pos)), "x"); + if(chunk_length==0) + complete = true; + pos = lf+1; + } + else + { + unsigned len = min(chunk_length, d.size()-pos); + content.append(d, pos, len); + chunk_length -= len; + if((pos = d.find('\n', pos+len))!=string::npos) + ++pos; + } + } + + return pos; + } + + complete = true; + return 0; +} + +unsigned Message::parse_headers(const string &d) +{ + unsigned start = 0; + while(1) + { + unsigned lf = d.find('\n', start); + if(lf==string::npos) + throw invalid_argument("Message::parse_headers"); + if(lf==start || (d[start]=='\r' && lf==start+1)) + return lf+1; + + unsigned colon = d.find(':', start); + if(colon>lf) + throw invalid_argument("Message::parse_headers"); + + set_header(d.substr(start, colon-start), strip(d.substr(colon+1, lf-colon-1))); + + start = lf+1; + } +} + +string Message::str_common() const +{ + string result; + + for(HeaderMap::const_iterator i=headers.begin(); i!=headers.end(); ++i) + if(i->first[0]!='-') + result += format("%s: %s\r\n", i->first, i->second); + result += "\r\n"; + result += content; + + return result; +} + +string Message::normalize_header_name(const string &hdr) const +{ + string result = hdr; + bool upper = true; + for(string::iterator i=result.begin(); i!=result.end(); ++i) + { + if(upper) + { + *i = toupper(*i); + upper = false; + } + else if(*i=='-') + upper = true; + else + *i = tolower(*i); + } + return result; +} + +} // namespace Http +} // namespace Msp diff --git a/source/http/message.h b/source/http/message.h new file mode 100644 index 0000000..4d23102 --- /dev/null +++ b/source/http/message.h @@ -0,0 +1,47 @@ +#ifndef MSP_HTTP_MESSAGE_H_ +#define MSP_HTTP_MESSAGE_H_ + +#include +#include +#include +#include "version.h" + +namespace Msp { +namespace Http { + +class Message +{ +protected: + typedef std::map HeaderMap; + + Version http_version; + HeaderMap headers; + std::string content; + unsigned chunk_length; + bool complete; + Variant user_data; + + Message(); +public: + virtual ~Message() { } + + void set_header(const std::string &, const std::string &); + bool has_header(const std::string &) const; + const std::string &get_header(const std::string &) const; + void add_content(const std::string &); + const std::string &get_content() const { return content; } + void set_user_data(const Variant &); + const Variant &get_user_data() const { return user_data; } + bool is_complete() const { return complete; } + unsigned parse_content(const std::string &); + virtual std::string str() const = 0; +protected: + unsigned parse_headers(const std::string &); + std::string str_common() const; + std::string normalize_header_name(const std::string &) const; +}; + +} // namespace Http +} // namespace Msp + +#endif diff --git a/source/http/request.cpp b/source/http/request.cpp new file mode 100644 index 0000000..c948755 --- /dev/null +++ b/source/http/request.cpp @@ -0,0 +1,65 @@ +#include +#include +#include +#include "request.h" +#include "utils.h" + +using namespace std; + +namespace Msp { +namespace Http { + +Request::Request(const string &m, const string &p): + method(m), + path(p) +{ } + +string Request::str() const +{ + string result = format("%s %s %s\r\n", method, path, version_str(http_version)); + result += str_common(); + + return result; +} + +Request Request::parse(const string &str) +{ + unsigned lf = str.find('\n'); + vector parts = split(str.substr(0, lf-(str[lf-1]=='\r')), ' ', 2); + if(parts.size()<3) + throw invalid_argument("Request::parse"); + + Request result(parts[0], parts[1]); + result.http_version = parse_version(parts[2]); + + lf += result.parse_headers(str.substr(lf+1)); + + result.parse_content(str.substr(lf+1)); + + return result; +} + +Request Request::from_url(const string &str) +{ + Url url = parse_url(str); + if(url.scheme!="http") + throw invalid_argument("Request::from_url"); + + string path = url.path; + if(path.empty()) + path = "/"; + if(!url.query.empty()) + { + path += '?'; + path += url.query; + } + + Request result("GET", path); + result.set_header("Host", url.host); + result.set_header("Connection", "close"); + + return result; +} + +} // namespace Http +} // namespace Msp diff --git a/source/http/request.h b/source/http/request.h new file mode 100644 index 0000000..ec5030e --- /dev/null +++ b/source/http/request.h @@ -0,0 +1,29 @@ +#ifndef MSP_HTTP_REQUEST_H_ +#define MSP_HTTP_REQUEST_H_ + +#include +#include "message.h" + +namespace Msp { +namespace Http { + +class Request: public Message +{ +private: + std::string method; + std::string path; + +public: + Request(const std::string &, const std::string &); + const std::string &get_method() const { return method; } + const std::string &get_path() const { return path; } + virtual std::string str() const; + + static Request parse(const std::string &); + static Request from_url(const std::string &); +}; + +} // namespace Http +} // namespace Msp + +#endif diff --git a/source/http/response.cpp b/source/http/response.cpp new file mode 100644 index 0000000..ede249b --- /dev/null +++ b/source/http/response.cpp @@ -0,0 +1,42 @@ +#include +#include +#include "response.h" + +using namespace std; + +namespace Msp { +namespace Http { + +Response::Response(Status s): + status(s) +{ } + +string Response::str() const +{ + string result = format("%s %d %s\r\n", version_str(http_version), static_cast(status), status); + result += str_common(); + + return result; +} + +Response Response::parse(const string &str) +{ + Response result; + + unsigned lf = str.find('\n'); + vector parts = split(str.substr(0, lf), ' ', 2); + if(parts.size()<2) + throw invalid_argument("Response::parse"); + + result.http_version = parse_version(parts[0]); + result.status = static_cast(lexical_cast(parts[1])); + + lf += result.parse_headers(str.substr(lf+1)); + + result.parse_content(str.substr(lf+1)); + + return result; +} + +} // namespace Http +} // namespace Msp diff --git a/source/http/response.h b/source/http/response.h new file mode 100644 index 0000000..d721ad8 --- /dev/null +++ b/source/http/response.h @@ -0,0 +1,27 @@ +#ifndef MSP_HTTP_RESPONSE_H_ +#define MSP_HTTO_RESPONSE_H_ + +#include "message.h" +#include "status.h" + +namespace Msp { +namespace Http { + +class Response: public Message +{ +private: + Status status; + + Response() { } +public: + Response(Status); + Status get_status() const { return status; } + virtual std::string str() const; + + static Response parse(const std::string &); +}; + +} // namespace Http +} // namespace Msp + +#endif diff --git a/source/http/server.cpp b/source/http/server.cpp new file mode 100644 index 0000000..9205cc1 --- /dev/null +++ b/source/http/server.cpp @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include +#include +#include "request.h" +#include "response.h" +#include "server.h" + +using namespace std; + +namespace Msp { +namespace Http { + +Server::Server(unsigned port): + sock(Net::INET), + event_disp(0) +{ + sock.signal_data_available.connect(sigc::mem_fun(this, &Server::data_available)); + RefPtr addr = Net::resolve("*", format("%d", port)); + sock.listen(*addr, 8); +} + +unsigned Server::get_port() const +{ + const Net::SockAddr &addr = sock.get_local_address(); + if(addr.get_family()==Net::INET) + return static_cast(addr).get_port(); + return 0; +} + +void Server::use_event_dispatcher(IO::EventDispatcher *ed) +{ + if(event_disp) + { + event_disp->remove(sock); + for(list::iterator i=clients.begin(); i!=clients.end(); ++i) + event_disp->remove(*i->sock); + } + event_disp = ed; + if(event_disp) + { + event_disp->add(sock); + for(list::iterator i=clients.begin(); i!=clients.end(); ++i) + event_disp->add(*i->sock); + } +} + +void Server::delay_response(Response &resp) +{ + get_client_by_response(resp).async = true; +} + +void Server::submit_response(Response &resp) +{ + Client &cl = get_client_by_response(resp); + if(cl.async) + { + cl.sock->write(resp.str()); + cl.stale = true; + } +} + +void Server::data_available() +{ + Net::StreamSocket *csock = sock.accept(); + clients.push_back(Client(csock)); + csock->signal_data_available.connect(sigc::bind(sigc::mem_fun(this, &Server::client_data_available), sigc::ref(clients.back()))); + csock->signal_end_of_file.connect(sigc::bind(sigc::mem_fun(this, &Server::client_end_of_file), sigc::ref(clients.back()))); + if(event_disp) + event_disp->add(*csock); +} + +void Server::client_data_available(Client &cl) +{ + for(list::iterator i=clients.begin(); i!=clients.end(); ++i) + if(i->stale && &*i!=&cl) + { + clients.erase(i); + break; + } + + char rbuf[4096]; + unsigned len = cl.sock->read(rbuf, sizeof(rbuf)); + cl.in_buf.append(rbuf, len); + + RefPtr response; + if(!cl.request) + { + if(cl.in_buf.find("\r\n\r\n")!=string::npos || cl.in_buf.find("\n\n")!=string::npos) + { + try + { + cl.request = new Request(Request::parse(cl.in_buf)); + + string addr_str = cl.sock->get_peer_address().str(); + unsigned colon = addr_str.find(':'); + cl.request->set_header("-Client-Host", addr_str.substr(0, colon)); + + if(cl.request->get_method()!="GET" && cl.request->get_method()!="POST") + { + response = new Response(NOT_IMPLEMENTED); + response->add_content("Method not implemented\n"); + } + } + catch(const exception &e) + { + response = new Response(BAD_REQUEST); + response->add_content(e.what()); + } + cl.in_buf = string(); + } + } + else + { + len = cl.request->parse_content(cl.in_buf); + cl.in_buf.erase(0, len); + } + + if(cl.request && cl.request->is_complete() && !response) + { + response = new Response(NONE); + try + { + cl.response = response.get(); + signal_request.emit(*cl.request, *response); + if(cl.async) + response.release(); + else + { + cl.response = 0; + if(response->get_status()==NONE) + { + response = new Response(NOT_FOUND); + response->add_content("The requested resource was not found\n"); + } + } + } + catch(const exception &e) + { + cl.response = 0; + response = new Response(INTERNAL_ERROR); + response->add_content(e.what()); + } + } + + if(response) + { + cl.sock->write(response->str()); + cl.stale = true; + } +} + +void Server::client_end_of_file(Client &cl) +{ + cl.stale = true; +} + +Server::Client &Server::get_client_by_response(Response &resp) +{ + for(list::iterator i=clients.begin(); i!=clients.end(); ++i) + if(i->response==&resp) + return *i; + + // XXX Do this differently + throw invalid_argument("Response does not belong to any client"); +} + + +Server::Client::Client(RefPtr s): + sock(s), + request(0), + response(0), + async(false), + stale(false) +{ } + +Server::Client::~Client() +{ + delete request; + delete response; +} + +} // namespace Http +} // namespace Msp diff --git a/source/http/server.h b/source/http/server.h new file mode 100644 index 0000000..41894db --- /dev/null +++ b/source/http/server.h @@ -0,0 +1,53 @@ +#ifndef MSP_HTTP_SERVER_H_ +#define MSP_HTTP_SERVER_H_ + +#include +#include +#include + +namespace Msp { +namespace Http { + +class Request; +class Response; + +class Server +{ +public: + sigc::signal signal_request; + +private: + struct Client + { + RefPtr sock; + std::string in_buf; + Request *request; + Response *response; + bool async; + bool stale; + + Client(RefPtr); + ~Client(); + }; + + Net::StreamServerSocket sock; + std::list clients; + IO::EventDispatcher *event_disp; + +public: + Server(unsigned); + unsigned get_port() const; + void use_event_dispatcher(IO::EventDispatcher *); + void delay_response(Response &); + void submit_response(Response &); +private: + void data_available(); + void client_data_available(Client &); + void client_end_of_file(Client &); + Client &get_client_by_response(Response &); +}; + +} // namespace Http +} // namespace Msp + +#endif diff --git a/source/http/status.cpp b/source/http/status.cpp new file mode 100644 index 0000000..6b95822 --- /dev/null +++ b/source/http/status.cpp @@ -0,0 +1,26 @@ +#include "status.h" + +using namespace std; + +namespace Msp { +namespace Http { + +ostream &operator<<(ostream &out, Status status) +{ + switch(status) + { + case NONE: out<<"None"; break; + case OK: out<<"OK"; break; + case BAD_REQUEST: out<<"Bad Request"; break; + case FORBIDDEN: out<<"Forbidden"; break; + case NOT_FOUND: out<<"Not Found"; break; + case INTERNAL_ERROR: out<<"Internal Error"; break; + case NOT_IMPLEMENTED: out<<"Not Implemented"; break; + default: out<<"Unknown Status"; break; + } + + return out; +} + +} // namespace Http +} // namespace Msp diff --git a/source/http/status.h b/source/http/status.h new file mode 100644 index 0000000..075016d --- /dev/null +++ b/source/http/status.h @@ -0,0 +1,25 @@ +#ifndef MSP_HTTPSERVER_STATUS_H_ +#define MSP_HTTPSERVER_STATUS_H_ + +#include + +namespace Msp { +namespace Http { + +enum Status +{ + NONE = 0, + OK = 200, + BAD_REQUEST = 400, + FORBIDDEN = 403, + NOT_FOUND = 404, + INTERNAL_ERROR = 500, + NOT_IMPLEMENTED = 501 +}; + +extern std::ostream &operator<<(std::ostream &, Status); + +} // namespace Http +} // namespace Msp + +#endif diff --git a/source/http/utils.cpp b/source/http/utils.cpp new file mode 100644 index 0000000..5a5dc04 --- /dev/null +++ b/source/http/utils.cpp @@ -0,0 +1,149 @@ +#include +#include +#include +#include +#include "utils.h" + +using namespace std; + +namespace { + +const char *reserved[]= +{ + " #%&+=?", + " #%&*+:;=?@[]", + " !#$%&'()*+,/:;=?@[]", +}; + +bool is_reserved(char c, unsigned level) +{ + for(const char *r=reserved[level]; *r; ++r) + if(c==*r) + return true; + return false; +} + +} + +namespace Msp { +namespace Http { + +string urlencode(const string &str, EncodeLevel level) +{ + string result; + for(string::const_iterator i=str.begin(); i!=str.end(); ++i) + { + if(is_reserved(*i, level)) + result += format("%%%02X", *i); + else + result += *i; + } + return result; +} + +string urlencode_plus(const string &str, EncodeLevel level) +{ + string result; + for(string::const_iterator i=str.begin(); i!=str.end(); ++i) + { + if(*i==' ') + result += '+'; + else if(is_reserved(*i, level)) + result += format("%%%02X", *i); + else + result += *i; + } + return result; +} + +string urldecode(const string &str) +{ + string result; + for(unsigned i=0; istr.size()) + throw invalid_argument("urldecode"); + result += lexical_cast(str.substr(i+1, 2), "x"); + i += 2; + } + else if(c=='+') + result += ' '; + else + result += c; + } + return result; +} + +Url parse_url(const string &str) +{ + static Regex r_url("(([a-z]+)://)?([a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*(:[0-9])?)?(/[^?#]*)?(\\?([^#]+))?(#(.*))?"); + if(RegMatch m = r_url.match(str)) + { + Url url; + url.scheme = m[2].str; + url.host = m[3].str; + url.path = urldecode(m[6].str); + url.query = m[8].str; + url.fragment = m[10].str; + return url; + } + else + throw invalid_argument("parse_url"); +} + +string build_url(const Url &url) +{ + if(!url.path.empty() && url.path[0]!='/') + throw invalid_argument("build_url"); + + string str; + if(!url.scheme.empty()) + str += url.scheme+"://"; + str += url.host; + str += urlencode(url.path); + if(!url.query.empty()) + { + str += '?'; + str += url.query; + } + if(!url.fragment.empty()) + { + str += '#'; + str += url.fragment; + } + return str; +} + +Query parse_query(const std::string &str) +{ + vector parts = split(str, '&'); + Query query; + for(vector::const_iterator i=parts.begin(); i!=parts.end(); ++i) + { + unsigned equals = i->find('='); + string &value = query[urldecode(i->substr(0, equals))]; + if(equals!=string::npos) + value = urldecode(i->substr(equals+1)); + } + return query; +} + +string build_query(const Query &query) +{ + string str; + for(Query::const_iterator i=query.begin(); i!=query.end(); ++i) + { + if(i!=query.begin()) + str += '&'; + str += urlencode_plus(i->first); + str += '='; + str += urlencode_plus(i->second); + } + return str; +} + +} // namespace Http +} // namespace Msp diff --git a/source/http/utils.h b/source/http/utils.h new file mode 100644 index 0000000..a25a748 --- /dev/null +++ b/source/http/utils.h @@ -0,0 +1,39 @@ +#ifndef MSP_HTTP_UTILS_H_ +#define MSP_HTTP_UTILS_H_ + +#include +#include + +namespace Msp { +namespace Http { + +enum EncodeLevel +{ + MINIMAL, + SAFE, + PARANOID +}; + +struct Url +{ + std::string scheme; + std::string host; + std::string path; + std::string query; + std::string fragment; +}; + +typedef std::map Query; + +std::string urlencode(const std::string &, EncodeLevel =SAFE); +std::string urlencode_plus(const std::string &, EncodeLevel =SAFE); +std::string urldecode(const std::string &); +Url parse_url(const std::string &); +std::string build_url(const Url &); +Query parse_query(const std::string &); +std::string build_query(const Query &); + +} // namespace Http +} // namespace Msp + +#endif diff --git a/source/http/version.cpp b/source/http/version.cpp new file mode 100644 index 0000000..f22b46e --- /dev/null +++ b/source/http/version.cpp @@ -0,0 +1,25 @@ +#include +#include +#include +#include "version.h" + +using namespace std; + +namespace Msp { +namespace Http { + +Version parse_version(const std::string &ver) +{ + if(RegMatch match = Regex("^HTTP/([0-9]+).([0-9]+)$").match(ver)) + return lexical_cast(match[1].str)<<4 | lexical_cast(match[2].str); + else + throw invalid_argument("parse_version"); +} + +string version_str(Version ver) +{ + return format("HTTP/%u.%u", (ver>>4)&0xF, ver&0xF); +} + +} // namespace Http +} // namespace Msp diff --git a/source/http/version.h b/source/http/version.h new file mode 100644 index 0000000..19a50b9 --- /dev/null +++ b/source/http/version.h @@ -0,0 +1,17 @@ +#ifndef MSP_HTTP_MISC_H_ +#define MSP_HTTP_MISC_H_ + +#include + +namespace Msp { +namespace Http { + +typedef unsigned Version; + +Version parse_version(const std::string &); +std::string version_str(Version); + +} // namespace Http +} // namespace Msp + +#endif