From patchwork Mon Jul 24 20:05:10 2017 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Malcolm X-Patchwork-Id: 793011 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (mailfrom) smtp.mailfrom=gcc.gnu.org (client-ip=209.132.180.131; helo=sourceware.org; envelope-from=gcc-patches-return-458805-incoming=patchwork.ozlabs.org@gcc.gnu.org; receiver=) Authentication-Results: ozlabs.org; dkim=pass (1024-bit key; unprotected) header.d=gcc.gnu.org header.i=@gcc.gnu.org header.b="wqCBtET0"; dkim-atps=neutral Received: from sourceware.org (server1.sourceware.org [209.132.180.131]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 3xGWkS5k1Vz9ryr for ; Tue, 25 Jul 2017 05:33:20 +1000 (AEST) DomainKey-Signature: a=rsa-sha1; c=nofws; d=gcc.gnu.org; h=list-id :list-unsubscribe:list-archive:list-post:list-help:sender:from :to:cc:subject:date:message-id:in-reply-to:references; q=dns; s= default; b=M6HD/pIpnb0GOEoP6AdwpYQ7cJ/atMgqj0di3zZNq9Ug1YzWAj9EO /nPB5lLq0HIujAVTc2Kcw1LjB7pDfjnct8+Ps7+W46khirRRmDnifRAdi0ad3qwo J7x0MABPR5atqFHp77eHrF8opcGcqesWGgx0pw0Wmi2bZL+ATcfTLo= DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=gcc.gnu.org; h=list-id :list-unsubscribe:list-archive:list-post:list-help:sender:from :to:cc:subject:date:message-id:in-reply-to:references; s= default; bh=oFMoGBTcYDPW/uO4Pkv5peVd2Q4=; b=wqCBtET0VEmbeQ5hdoOo 7eZ2rNkQ/hWl3uJAli+N31tsiNZt2nTId8n8H9G2Bbh9fb84lSR72FaanGn5QQqn aSenSHZfJMpiSA74j++IeWKhm23IRDx4qkFJrmoiclw24T9ckEl5ELaxsdJ5YFyW 0b1yvlNA3mKYu2BK81ccy3Q= Received: (qmail 85046 invoked by alias); 24 Jul 2017 19:31:21 -0000 Mailing-List: contact gcc-patches-help@gcc.gnu.org; run by ezmlm Precedence: bulk List-Id: List-Unsubscribe: List-Archive: List-Post: List-Help: Sender: gcc-patches-owner@gcc.gnu.org Delivered-To: mailing list gcc-patches@gcc.gnu.org Received: (qmail 84417 invoked by uid 89); 24 Jul 2017 19:31:19 -0000 Authentication-Results: sourceware.org; auth=none X-Virus-Found: No X-Spam-SWARE-Status: No, score=-26.9 required=5.0 tests=BAYES_00, GIT_PATCH_0, GIT_PATCH_1, GIT_PATCH_2, GIT_PATCH_3, RP_MATCHES_RCVD, SPF_HELO_PASS autolearn=ham version=3.3.2 spammy=honoring X-HELO: mx1.redhat.com Received: from mx1.redhat.com (HELO mx1.redhat.com) (209.132.183.28) by sourceware.org (qpsmtpd/0.93/v0.84-503-g423c35a) with ESMTP; Mon, 24 Jul 2017 19:31:12 +0000 Received: from smtp.corp.redhat.com (int-mx06.intmail.prod.int.phx2.redhat.com [10.5.11.16]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 4782163141 for ; Mon, 24 Jul 2017 19:31:11 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.3.2 mx1.redhat.com 4782163141 Authentication-Results: ext-mx09.extmail.prod.ext.phx2.redhat.com; dmarc=none (p=none dis=none) header.from=redhat.com Authentication-Results: ext-mx09.extmail.prod.ext.phx2.redhat.com; spf=pass smtp.mailfrom=dmalcolm@redhat.com DKIM-Filter: OpenDKIM Filter v2.11.0 mx1.redhat.com 4782163141 Received: from c64.redhat.com (ovpn-112-25.phx2.redhat.com [10.3.112.25]) by smtp.corp.redhat.com (Postfix) with ESMTP id 67BE34D74A; Mon, 24 Jul 2017 19:31:10 +0000 (UTC) From: David Malcolm To: gcc-patches@gcc.gnu.org Cc: David Malcolm Subject: [PATCH 13/17] Add http-server.h and http-server.c Date: Mon, 24 Jul 2017 16:05:10 -0400 Message-Id: <1500926714-56988-14-git-send-email-dmalcolm@redhat.com> In-Reply-To: <1500926714-56988-1-git-send-email-dmalcolm@redhat.com> References: <1500926714-56988-1-git-send-email-dmalcolm@redhat.com> X-IsSubscribed: yes 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 a8668f4..7e75680 100644 --- a/gcc/selftest-run-tests.c +++ b/gcc/selftest-run-tests.c @@ -68,6 +68,7 @@ selftest::run_tests () typed_splay_tree_c_tests (); blt_c_tests (); json_c_tests (); + http_server_c_tests (); /* Mid-level data structures. */ input_c_tests (); diff --git a/gcc/selftest.h b/gcc/selftest.h index 281658b..ad4e957 100644 --- a/gcc/selftest.h +++ b/gcc/selftest.h @@ -183,6 +183,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 ();