]> git.tdb.fi Git - libs/net.git/commitdiff
Merge branch 'http-master'
authorMikko Rasa <tdb@tdb.fi>
Wed, 10 Aug 2011 18:14:42 +0000 (21:14 +0300)
committerMikko Rasa <tdb@tdb.fi>
Wed, 10 Aug 2011 18:14:42 +0000 (21:14 +0300)
Conflicts:
.gitignore
Build

17 files changed:
Build
source/http/client.cpp [new file with mode: 0644]
source/http/client.h [new file with mode: 0644]
source/http/message.cpp [new file with mode: 0644]
source/http/message.h [new file with mode: 0644]
source/http/request.cpp [new file with mode: 0644]
source/http/request.h [new file with mode: 0644]
source/http/response.cpp [new file with mode: 0644]
source/http/response.h [new file with mode: 0644]
source/http/server.cpp [new file with mode: 0644]
source/http/server.h [new file with mode: 0644]
source/http/status.cpp [new file with mode: 0644]
source/http/status.h [new file with mode: 0644]
source/http/utils.cpp [new file with mode: 0644]
source/http/utils.h [new file with mode: 0644]
source/http/version.cpp [new file with mode: 0644]
source/http/version.h [new file with mode: 0644]

diff --git a/Build b/Build
index 444fc30f53dae783b69e73a29d5c8234d93696b8..1ed9db85befa7188f494ecf7db832e83285b4347 100644 (file)
--- 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 (file)
index 0000000..0729544
--- /dev/null
@@ -0,0 +1,164 @@
+#include <msp/core/except.h>
+#include <msp/core/refptr.h>
+#include <msp/net/resolve.h>
+#include <msp/time/units.h>
+#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<Net::SockAddr> 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 (file)
index 0000000..ee10f8c
--- /dev/null
@@ -0,0 +1,52 @@
+#ifndef MSP_HTTP_CLIENT_H_
+#define MSP_HTTP_CLIENT_H_
+
+#include <string>
+#include <sigc++/signal.h>
+#include <msp/io/eventdispatcher.h>
+#include <msp/net/streamsocket.h>
+
+namespace Msp {
+namespace Http {
+
+class Request;
+class Response;
+
+class Client
+{
+public:
+       sigc::signal<void, const Response &> signal_response_complete;
+       sigc::signal<void, const std::exception *> 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 (file)
index 0000000..2b3e40b
--- /dev/null
@@ -0,0 +1,152 @@
+#include <cstdlib>
+#include <msp/core/maputils.h>
+#include <msp/strings/formatter.h>
+#include <msp/strings/utils.h>
+#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<unsigned>(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<d.size())
+               {
+                       if(chunk_length==0)
+                       {
+                               unsigned lf = d.find('\n', pos);
+                               if(lf==string::npos)
+                                       return pos;
+                               chunk_length = lexical_cast<unsigned>(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 (file)
index 0000000..4d23102
--- /dev/null
@@ -0,0 +1,47 @@
+#ifndef MSP_HTTP_MESSAGE_H_
+#define MSP_HTTP_MESSAGE_H_
+
+#include <map>
+#include <string>
+#include <msp/core/variant.h>
+#include "version.h"
+
+namespace Msp {
+namespace Http {
+
+class Message
+{
+protected:
+       typedef std::map<std::string, std::string> 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 (file)
index 0000000..c948755
--- /dev/null
@@ -0,0 +1,65 @@
+#include <msp/strings/formatter.h>
+#include <msp/strings/regex.h>
+#include <msp/strings/utils.h>
+#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<string> 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 (file)
index 0000000..ec5030e
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef MSP_HTTP_REQUEST_H_
+#define MSP_HTTP_REQUEST_H_
+
+#include <string>
+#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 (file)
index 0000000..ede249b
--- /dev/null
@@ -0,0 +1,42 @@
+#include <msp/strings/formatter.h>
+#include <msp/strings/utils.h>
+#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<int>(status), status);
+       result += str_common();
+
+       return result;
+}
+
+Response Response::parse(const string &str)
+{
+       Response result;
+
+       unsigned lf = str.find('\n');
+       vector<string> 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<Status>(lexical_cast<unsigned>(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 (file)
index 0000000..d721ad8
--- /dev/null
@@ -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 (file)
index 0000000..9205cc1
--- /dev/null
@@ -0,0 +1,186 @@
+#include <exception>
+#include <msp/core/refptr.h>
+#include <msp/net/inet.h>
+#include <msp/net/resolve.h>
+#include <msp/net/streamsocket.h>
+#include <msp/strings/format.h>
+#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<Net::SockAddr> 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<const Net::InetAddr &>(addr).get_port();
+       return 0;
+}
+
+void Server::use_event_dispatcher(IO::EventDispatcher *ed)
+{
+       if(event_disp)
+       {
+               event_disp->remove(sock);
+               for(list<Client>::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<Client>::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<Client>::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> 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<Client>::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<Net::StreamSocket> 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 (file)
index 0000000..41894db
--- /dev/null
@@ -0,0 +1,53 @@
+#ifndef MSP_HTTP_SERVER_H_
+#define MSP_HTTP_SERVER_H_
+
+#include <msp/core/refptr.h>
+#include <msp/io/eventdispatcher.h>
+#include <msp/net/streamserversocket.h>
+
+namespace Msp {
+namespace Http {
+
+class Request;
+class Response;
+
+class Server
+{
+public:
+       sigc::signal<void, const Request &, Response &> signal_request;
+
+private:
+       struct Client
+       {
+               RefPtr<Net::StreamSocket> sock;
+               std::string in_buf;
+               Request *request;
+               Response *response;
+               bool async;
+               bool stale;
+
+               Client(RefPtr<Net::StreamSocket>);
+               ~Client();
+       };
+
+       Net::StreamServerSocket sock;
+       std::list<Client> 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 (file)
index 0000000..6b95822
--- /dev/null
@@ -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 (file)
index 0000000..075016d
--- /dev/null
@@ -0,0 +1,25 @@
+#ifndef MSP_HTTPSERVER_STATUS_H_
+#define MSP_HTTPSERVER_STATUS_H_
+
+#include <ostream>
+
+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 (file)
index 0000000..5a5dc04
--- /dev/null
@@ -0,0 +1,149 @@
+#include <algorithm>
+#include <msp/strings/formatter.h>
+#include <msp/strings/regex.h>
+#include <msp/strings/utils.h>
+#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; i<str.size(); ++i)
+       {
+               char c = str[i];
+               if(c=='%')
+               {
+                       if(i+3>str.size())
+                               throw invalid_argument("urldecode");
+                       result += lexical_cast<unsigned char>(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<string> parts = split(str, '&');
+       Query query;
+       for(vector<string>::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 (file)
index 0000000..a25a748
--- /dev/null
@@ -0,0 +1,39 @@
+#ifndef MSP_HTTP_UTILS_H_
+#define MSP_HTTP_UTILS_H_
+
+#include <map>
+#include <string>
+
+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<std::string, std::string> 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 (file)
index 0000000..f22b46e
--- /dev/null
@@ -0,0 +1,25 @@
+#include <msp/strings/formatter.h>
+#include <msp/strings/lexicalcast.h>
+#include <msp/strings/regex.h>
+#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<unsigned>(match[1].str)<<4 | lexical_cast<unsigned>(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 (file)
index 0000000..19a50b9
--- /dev/null
@@ -0,0 +1,17 @@
+#ifndef MSP_HTTP_MISC_H_
+#define MSP_HTTP_MISC_H_
+
+#include <string>
+
+namespace Msp {
+namespace Http {
+
+typedef unsigned Version;
+
+Version parse_version(const std::string &);
+std::string version_str(Version);
+
+} // namespace Http
+} // namespace Msp
+
+#endif