From 923a0de91f53d9102235cc6a9cdc21c11ae55f46 Mon Sep 17 00:00:00 2001 From: David Malcolm Date: Tue, 23 May 2017 12:50:55 -0400 Subject: [PATCH 21/28] Add http-server.h and http-server.c This patch implements an abstract class of HTTP server, as a subclass of the server class implemented in the previous patch. gcc/ChangeLog: * Makefile.in (OBJS): Add http-server.o. * http-server.c: New file. * http-server.h: New file. * selftest-run-tests.c (selftest::run_tests): Call selftest::http_server_c_tests. * selftest.h (selftest::http_server_c_tests): New decl. --- gcc/Makefile.in | 1 + gcc/http-server.c | 358 +++++++++++++++++++++++++++++++++++++++++++++++ gcc/http-server.h | 101 +++++++++++++ gcc/selftest-run-tests.c | 1 + gcc/selftest.h | 1 + 5 files changed, 462 insertions(+) create mode 100644 gcc/http-server.c create mode 100644 gcc/http-server.h diff --git a/gcc/Makefile.in b/gcc/Makefile.in index 4e60bc0..0c361f1 100644 --- a/gcc/Makefile.in +++ b/gcc/Makefile.in @@ -1332,6 +1332,7 @@ OBJS = \ hsa-regalloc.o \ hsa-brig.o \ hsa-dump.o \ + http-server.o \ hw-doloop.o \ hwint.o \ ifcvt.o \ diff --git a/gcc/http-server.c b/gcc/http-server.c new file mode 100644 index 0000000..88c1c17 --- /dev/null +++ b/gcc/http-server.c @@ -0,0 +1,358 @@ +/* HTTP server implementation. + Copyright (C) 2017 Free Software Foundation, Inc. + +This file is part of GCC. + +GCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +GCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with GCC; see the file COPYING3. If not see +. */ + +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "http-server.h" +#include "pretty-print.h" +#include "selftest.h" +#include "diagnostic.h" + +/* class http::message. */ + +/* Set the content of this message. */ + +void +http::message::set_content (size_t length, const char *content) +{ + m_length = length; + m_buffer = xstrndup (content, length); +} + +/* class http::request : public http::message. */ + +/* http::request's constructor. */ + +http::request::request () +: message (), m_parsing_body (false), m_pending_data (), + m_content_length (-1), m_header_map () +{ +} + +/* http::request's destructor. */ + +http::request::~request() +{ + for (header_map_t::iterator it = m_header_map.begin (); + it != m_header_map.end (); ++it) + { + free (const_cast ((*it).first)); + free (const_cast ((*it).second)); + } +} + +/* Access the given header within this request, or NULL if not present. */ + +const char * +http::request::get_header (const char *header) const +{ + char **slot = const_cast (this)->m_header_map.get (header); + if (slot) + return *slot; + return NULL; +} + +/* Consume up to LENGTH bytes from BUF. + Return the number of bytes consumed. */ + +size_t +http::request::parse_buffer (size_t length, const char *buf) +{ + size_t idx; + for (idx = 0; idx < length; idx++) + { + if (consume_octet (buf[idx])) + return idx + 1; + } + return idx; +} + +/* Parse CH. Return true if the request has finished parsing, or false + if more data is expected. */ + +bool +http::request::consume_octet (char ch) +{ + if (m_parsing_body) + return consume_body_octet (ch); + else + return consume_header_octet (ch); +} + +/* Parse CH within the headers. Return true if the request has finished + parsing, or false if more data is expected. */ + +bool +http::request::consume_header_octet (char ch) +{ + /* We're parsing the headers. */ + m_pending_data.safe_push (ch); + /* Detect "\r\n". */ + size_t len = m_pending_data.length (); + if (len >= 2 && m_pending_data[len - 2] == '\r' && ch == '\n') + { + /* Is this a blank line? If so, then we're done with headers. */ + if (len == 2) + { + m_pending_data.truncate (0); + const char *content_length_str = get_header ("Content-Length"); + if (content_length_str) + { + m_content_length = atoi (content_length_str); + // FIXME: error-handling for non-int values + /* We're not yet finished; we must read the body. */ + m_parsing_body = true; + return false; + } + else + /* If there was no Content-Length, we have nothing + more to read. */ + return true; + } + else + { + /* Otherwise we have a header (or the initial verb line). */ + parse_header (m_pending_data.length () - 2, &m_pending_data[0]); + m_pending_data.truncate (0); + /* We're not yet finished. */ + return false; + } + } + + /* Not yet finished. */ + return false; +} + +/* Parse CH within the body. Return true if the request has finished + parsing, or false if more data is expected. */ + +bool +http::request::consume_body_octet (char ch) +{ + /* Accumulate data until we've seen Content-Length octets. */ + gcc_assert (m_content_length > 0); + m_pending_data.safe_push (ch); + if (m_pending_data.length () == m_content_length) + { + set_content (m_content_length, &m_pending_data[0]); + m_pending_data.truncate (0); + /* We've finished parsing this request. */ + return true; + } + else + /* We're not yet finished. */ + return false; +} + +/* FIXME. */ + +void +http::request::parse_header (size_t length, const char *buf) +{ + /* header-field = field-name ":" OWS field-value OWS */ + for (size_t colon_idx = 0; colon_idx + 1 < length; colon_idx++) + // FIXME: whitespace after colon is optional + // FIXME: optional trailing whitespace after header value + if (buf[colon_idx] == ':' && buf[colon_idx + 1] == ' ') + { + char *key = xstrndup (buf, colon_idx); + char *value = xstrndup (buf + colon_idx + 2, + length - (colon_idx + 2)); + m_header_map.put (key, value); + return; + } + // FIXME: error-handling +} + +/* class http::response : public http::message. */ + +/* Generate a string form of this response. + The caller is responsible for freeing it. */ + +char * +http::response::to_str () const +{ + pretty_printer pp; + pp_string (&pp, "HTTP/1.1 200 OK\r\n"); + pp_printf (&pp, "Content-Length: %i\r\n", (int)get_content_length ()); + pp_string (&pp, "\r\n"); + if (get_content ()) + pp_string (&pp, get_content ()); + return xstrdup (pp_formatted_text (&pp)); +} + +/* Implementation of ::server::on_read for http::server. + Read up to LENGTH bytes from BUF, and potentially call + on_http_request for any requests seen. + Write any responses back to FD. */ + +void +http::server::on_read (file_descriptor fd, size_t length, const char *buf) +{ + if (m_verbose) + inform (UNKNOWN_LOCATION, "received http request: %qs", + buf); // FIXME respect length + + /* FIXME: this assumes we have a full request i.e. two "\r\n\r\n" + If we don't we should read more until we do. */ + size_t req_start = 0; + while (req_start < length) + { + request req; + req_start += req.parse_buffer (length, buf); + http::response resp; + on_http_request (req, resp); + char *resp_str = resp.to_str (); + if (1) + inform (UNKNOWN_LOCATION, "sending http response: %qs", + resp_str); + write (fd.m_fd, resp_str, strlen (resp_str)); + free (resp_str); + } +} + +#if CHECKING_P + +namespace selftest { + +/* Selftests. */ + +/* Verify that we can parse an HTTP request. */ + +static void +test_parse_request () +{ + const char *in = ("POST /jsonrpc HTTP/1.1\r\n" + "Host: localhost:4000\r\n" + "Content-Length: 12\r\n" + "content-type: application/json\r\n" + "Accept-Encoding: gzip, deflate, compress\r\n" + "Accept: */*\r\n" + "User-Agent: test-user-agent\r\n" + "\r\n" + "test-content"); + http::request r; + size_t consumed = r.parse_buffer (strlen (in), in); + ASSERT_EQ (consumed, strlen (in)); + ASSERT_STREQ ("test-user-agent", r.get_header ("User-Agent")); + ASSERT_STREQ ("12", r.get_header ("Content-Length")); + ASSERT_EQ (NULL, r.get_header ("Not-A-Header")); + ASSERT_EQ (12, r.get_content_length ()); + ASSERT_TRUE (0 == strncmp ("test-content", r.get_content (), 12)); +} + +/* Verify that we can split up the parsing of a request at arbitrary + places. */ + +static void +test_parse_split_request () +{ + const char *in = ("POST /jsonrpc HTTP/1.1\r\n" + "Host: localhost:4000\r\n" + "Content-Length: 12\r\n" + "content-type: application/json\r\n" + "Accept-Encoding: gzip, deflate, compress\r\n" + "Accept: */*\r\n" + "User-Agent: test-user-agent\r\n" + "\r\n" + "test-content"); + for (size_t split = 0; split < strlen (in); split++) + { + http::request r; + + size_t consumed_1 = r.parse_buffer (split, in); + ASSERT_EQ (consumed_1, split); + + size_t consumed_2 = r.parse_buffer (strlen (in) - split, in + split); + ASSERT_EQ (consumed_2, strlen (in) - split); + + ASSERT_STREQ ("test-user-agent", r.get_header ("User-Agent")); + ASSERT_STREQ ("12", r.get_header ("Content-Length")); + ASSERT_EQ (NULL, r.get_header ("Not-A-Header")); + ASSERT_EQ (12, r.get_content_length ()); + ASSERT_TRUE (0 == strncmp ("test-content", r.get_content (), 12)); + } +} + +/* Verify that we can parse multiple requests out of one buffer, + honoring the Content-Length headers. */ + +static void +test_parse_multiple_requests () +{ + const char *in = ("POST /test HTTP/1.1\r\n" + "Content-Length: 25\r\n" + "\r\n" + "This is the first request" + "POST /test HTTP/1.1\r\n" + "Content-Length: 26\r\n" + "\r\n" + "This is the second request"); + http::request r1; + size_t consumed_1 = r1.parse_buffer (strlen (in), in); + ASSERT_EQ (68, consumed_1); + ASSERT_STREQ ("25", r1.get_header ("Content-Length")); + ASSERT_EQ (25, r1.get_content_length ()); + ASSERT_TRUE (0 == strncmp ("This is the first request", + r1.get_content (), 25)); + + http::request r2; + size_t consumed_2 = r2.parse_buffer (strlen (in) - consumed_1, + in + consumed_1); + ASSERT_EQ (69, consumed_2); + ASSERT_STREQ ("26", r2.get_header ("Content-Length")); + ASSERT_EQ (26, r2.get_content_length ()); + ASSERT_TRUE (0 == strncmp ("This is the second request", + r2.get_content (), 26)); + + ASSERT_EQ (strlen (in), consumed_1 + consumed_2); +} + +/* Verify http::response::to_str. */ + +static void +test_emit_response () +{ + http::response r; + const char *msg = "hello world"; + r.set_content (strlen (msg), msg); + + char *str = r.to_str (); + ASSERT_STREQ ("HTTP/1.1 200 OK\r\n" + "Content-Length: 11\r\n" + "\r\n" + "hello world", str); + free (str); +} + +/* Run all of the selftests within this file. */ + +void +http_server_c_tests () +{ + test_parse_request (); + test_parse_split_request (); + test_parse_multiple_requests (); + test_emit_response (); +} + +} // namespace selftest + +#endif /* #if CHECKING_P */ diff --git a/gcc/http-server.h b/gcc/http-server.h new file mode 100644 index 0000000..25d542d --- /dev/null +++ b/gcc/http-server.h @@ -0,0 +1,101 @@ +/* HTTP server implementation. + Copyright (C) 2017 Free Software Foundation, Inc. + +This file is part of GCC. + +GCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +GCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with GCC; see the file COPYING3. If not see +. */ + +#ifndef GCC_HTTP_SERVER_H +#define GCC_HTTP_SERVER_H + +#include "server.h" + +namespace http { + +/* http::message is a base class for encapsulating a collection of + octets received or to be sent. */ + +class message +{ + public: + message () : m_length (0), m_buffer (NULL) {} + ~message () { free (m_buffer); } + + size_t get_content_length () const { return m_length; } + const char *get_content () const { return m_buffer; } + + void set_content (size_t length, const char *content); + + private: + size_t m_length; + char *m_buffer; +}; + +/* http::request encapsulates an HTTP request. */ + +class request : public message +{ + public: + request (); + ~request(); + + const char *get_header (const char *header) const; + size_t parse_buffer (size_t length, const char *buf); + + private: + bool consume_octet (char ch); + bool consume_header_octet (char ch); + bool consume_body_octet (char ch); + void parse_header (size_t sz, const char *buf); + + bool m_parsing_body; + auto_vec m_pending_data; + size_t m_content_length; + + typedef hash_map > header_map_t; + header_map_t m_header_map; +}; + +/* http::response encapsulates an HTTP response. */ + +class response : public message +{ + public: + char *to_str () const; +}; + +/* Subclass of ::server than expects an HTTP-like protocol, + with header lines ended by '\r\n', then a '\r\n' line, then + the content. */ + +class server : public ::server +{ + public: + server (bool verbose) : m_verbose (verbose) {} + + void on_read (file_descriptor fd, size_t length, + const char *buf) OVERRIDE FINAL; + + virtual void on_http_request (const http::request &request, + http::response &response) = 0; + + private: + bool m_verbose; +}; + +} // namespace http + +#endif /* GCC_HTTP_SERVER_H */ diff --git a/gcc/selftest-run-tests.c b/gcc/selftest-run-tests.c index 0388e32..434174a 100644 --- a/gcc/selftest-run-tests.c +++ b/gcc/selftest-run-tests.c @@ -66,6 +66,7 @@ selftest::run_tests () sreal_c_tests (); fibonacci_heap_c_tests (); typed_splay_tree_c_tests (); + http_server_c_tests (); json_c_tests (); /* Mid-level data structures. */ diff --git a/gcc/selftest.h b/gcc/selftest.h index 8ab71b2..9b41ebd 100644 --- a/gcc/selftest.h +++ b/gcc/selftest.h @@ -182,6 +182,7 @@ extern void gimple_c_tests (); extern void ggc_tests_c_tests (); extern void hash_map_tests_c_tests (); extern void hash_set_tests_c_tests (); +extern void http_server_c_tests (); extern void input_c_tests (); extern void json_c_tests (); extern void pretty_print_c_tests (); -- 1.8.5.3