diff --git a/wpiutil/src/main/native/cpp/HttpParser.cpp b/wpiutil/src/main/native/cpp/HttpParser.cpp new file mode 100644 index 0000000000..f0105851a7 --- /dev/null +++ b/wpiutil/src/main/native/cpp/HttpParser.cpp @@ -0,0 +1,164 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "wpi/HttpParser.h" + +using namespace wpi; + +uint32_t HttpParser::GetParserVersion() { + return static_cast(http_parser_version()); +} + +HttpParser::HttpParser(Type type) { + http_parser_init(&m_parser, + static_cast(static_cast(type))); + m_parser.data = this; + + http_parser_settings_init(&m_settings); + + // Unlike the underlying http_parser library, we don't perform callbacks + // (other than body) with partial data; instead we buffer and call the user + // callback only when the data is complete. + + // on_message_begin: initialize our state, call user callback + m_settings.on_message_begin = [](http_parser* p) -> int { + auto& self = *static_cast(p->data); + self.m_urlBuf.clear(); + self.m_state = kStart; + self.messageBegin(); + return self.m_aborted; + }; + + // on_url: collect into buffer + m_settings.on_url = [](http_parser* p, const char* at, size_t length) -> int { + auto& self = *static_cast(p->data); + // append to buffer + if ((self.m_urlBuf.size() + length) > self.m_maxLength) return 1; + self.m_urlBuf += StringRef{at, length}; + self.m_state = kUrl; + return 0; + }; + + // on_status: collect into buffer, call user URL callback + m_settings.on_status = [](http_parser* p, const char* at, + size_t length) -> int { + auto& self = *static_cast(p->data); + // use valueBuf for the status + if ((self.m_valueBuf.size() + length) > self.m_maxLength) return 1; + self.m_valueBuf += StringRef{at, length}; + self.m_state = kStatus; + return 0; + }; + + // on_header_field: collect into buffer, call user header/status callback + m_settings.on_header_field = [](http_parser* p, const char* at, + size_t length) -> int { + auto& self = *static_cast(p->data); + + // once we're in header, we know the URL is complete + if (self.m_state == kUrl) { + self.url(self.m_urlBuf); + if (self.m_aborted) return 1; + } + + // once we're in header, we know the status is complete + if (self.m_state == kStatus) { + self.status(self.m_valueBuf); + if (self.m_aborted) return 1; + } + + // if we previously were in value state, that means we finished a header + if (self.m_state == kValue) { + self.header(self.m_fieldBuf, self.m_valueBuf); + if (self.m_aborted) return 1; + } + + // clear field and value when we enter this state + if (self.m_state != kField) { + self.m_state = kField; + self.m_fieldBuf.clear(); + self.m_valueBuf.clear(); + } + + // append data to field buffer + if ((self.m_fieldBuf.size() + length) > self.m_maxLength) return 1; + self.m_fieldBuf += StringRef{at, length}; + return 0; + }; + + // on_header_field: collect into buffer + m_settings.on_header_value = [](http_parser* p, const char* at, + size_t length) -> int { + auto& self = *static_cast(p->data); + + // if we weren't previously in value state, clear the buffer + if (self.m_state != kValue) { + self.m_state = kValue; + self.m_valueBuf.clear(); + } + + // append data to value buffer + if ((self.m_valueBuf.size() + length) > self.m_maxLength) return 1; + self.m_valueBuf += StringRef{at, length}; + return 0; + }; + + // on_headers_complete: call user status/header/complete callback + m_settings.on_headers_complete = [](http_parser* p) -> int { + auto& self = *static_cast(p->data); + + // if we previously were in url state, that means we finished the url + if (self.m_state == kUrl) { + self.url(self.m_urlBuf); + if (self.m_aborted) return 1; + } + + // if we previously were in status state, that means we finished the status + if (self.m_state == kStatus) { + self.status(self.m_valueBuf); + if (self.m_aborted) return 1; + } + + // if we previously were in value state, that means we finished a header + if (self.m_state == kValue) { + self.header(self.m_fieldBuf, self.m_valueBuf); + if (self.m_aborted) return 1; + } + + self.headersComplete(self.ShouldKeepAlive()); + return self.m_aborted; + }; + + // on_body: call user callback + m_settings.on_body = [](http_parser* p, const char* at, + size_t length) -> int { + auto& self = *static_cast(p->data); + self.body(StringRef{at, length}, self.IsBodyFinal()); + return self.m_aborted; + }; + + // on_message_complete: call user callback + m_settings.on_message_complete = [](http_parser* p) -> int { + auto& self = *static_cast(p->data); + self.messageComplete(self.ShouldKeepAlive()); + return self.m_aborted; + }; + + // on_chunk_header: call user callback + m_settings.on_chunk_header = [](http_parser* p) -> int { + auto& self = *static_cast(p->data); + self.chunkHeader(p->content_length); + return self.m_aborted; + }; + + // on_chunk_complete: call user callback + m_settings.on_chunk_complete = [](http_parser* p) -> int { + auto& self = *static_cast(p->data); + self.chunkComplete(); + return self.m_aborted; + }; +} diff --git a/wpiutil/src/main/native/include/wpi/HttpParser.h b/wpiutil/src/main/native/include/wpi/HttpParser.h new file mode 100644 index 0000000000..8a09fe82a8 --- /dev/null +++ b/wpiutil/src/main/native/include/wpi/HttpParser.h @@ -0,0 +1,216 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef WPIUTIL_WPI_HTTPPARSER_H_ +#define WPIUTIL_WPI_HTTPPARSER_H_ + +#include + +#include "wpi/Signal.h" +#include "wpi/SmallString.h" +#include "wpi/StringRef.h" +#include "wpi/http_parser.h" + +namespace wpi { + +/** + * HTTP protocol parser. Performs incremental parsing with callbacks for each + * part of the HTTP protocol. As this is incremental, it's suitable for use + * with event based frameworks that provide arbitrary chunks of data. + */ +class HttpParser { + public: + enum Type { + kRequest = HTTP_REQUEST, + kResponse = HTTP_RESPONSE, + kBoth = HTTP_BOTH + }; + + /** + * Returns the library version. Bits 16-23 contain the major version number, + * bits 8-15 the minor version number and bits 0-7 the patch level. + */ + static uint32_t GetParserVersion(); + + explicit HttpParser(Type type); + + /** + * Set the maximum accepted length for URLs, field names, and field values. + * The default is 1024. + * @param len maximum length + */ + void SetMaxLength(size_t len) { m_maxLength = len; } + + /** + * Executes the parser. An empty input is treated as EOF. + * @param in input data + * @return Number of parsed bytes. + */ + size_t Execute(StringRef in) { + return http_parser_execute(&m_parser, &m_settings, in.data(), in.size()); + } + + /** + * Get HTTP major version. + */ + unsigned int GetMajor() const { return m_parser.http_major; } + + /** + * Get HTTP minor version. + */ + unsigned int GetMinor() const { return m_parser.http_minor; } + + /** + * Get HTTP status code. Valid only on responses. Valid in and after + * the OnStatus() callback has been called. + */ + unsigned int GetStatusCode() const { return m_parser.status_code; } + + /** + * Get HTTP method. Valid only on requests. + */ + http_method GetMethod() const { + return static_cast(m_parser.method); + } + + /** + * Determine if an error occurred. + * @return False if no error. + */ + bool HasError() const { return m_parser.http_errno != HPE_OK; } + + /** + * Get error number. + */ + http_errno GetError() const { + return static_cast(m_parser.http_errno); + } + + /** + * Abort the parse. Call this from a callback handler to indicate an error. + * This will result in GetError() returning one of the callback-related + * errors (e.g. HPE_CB_message_begin). + */ + void Abort() { m_aborted = true; } + + /** + * Determine if an upgrade header was present and the parser has exited + * because of that. Should be checked when Execute() returns in addition to + * checking GetError(). + * @return True if upgrade header, false otherwise. + */ + bool IsUpgrade() const { return m_parser.upgrade; } + + /** + * If this returns false in the headersComplete or messageComplete + * callback, then this should be the last message on the connection. + * If you are the server, respond with the "Connection: close" header. + * If you are the client, close the connection. + */ + bool ShouldKeepAlive() const { return http_should_keep_alive(&m_parser); } + + /** + * Pause the parser. + * @param paused True to pause, false to unpause. + */ + void Pause(bool paused) { http_parser_pause(&m_parser, paused); } + + /** + * Checks if this is the final chunk of the body. + */ + bool IsBodyFinal() const { return http_body_is_final(&m_parser); } + + /** + * Get URL. Valid in and after the url callback has been called. + */ + StringRef GetUrl() const { return m_urlBuf; } + + /** + * Message begin callback. + */ + sig::Signal<> messageBegin; + + /** + * URL callback. + * + * The parameter to the callback is the complete URL string. + */ + sig::Signal url; + + /** + * Status callback. + * + * The parameter to the callback is the complete status string. + * GetStatusCode() can be used to get the numeric status code. + */ + sig::Signal status; + + /** + * Header field callback. + * + * The parameters to the callback are the field name and field value. + */ + sig::Signal header; + + /** + * Headers complete callback. + * + * The parameter to the callback is whether the connection should be kept + * alive. If this is false, then this should be the last message on the + * connection. If you are the server, respond with the "Connection: close" + * header. If you are the client, close the connection. + */ + sig::Signal headersComplete; + + /** + * Body data callback. + * + * The parameters to the callback is the data chunk and whether this is the + * final chunk of data in the message. Note this callback will be called + * multiple times arbitrarily (e.g. it's possible that it may be called with + * just a few characters at a time). + */ + sig::Signal body; + + /** + * Headers complete callback. + * + * The parameter to the callback is whether the connection should be kept + * alive. If this is false, then this should be the last message on the + * connection. If you are the server, respond with the "Connection: close" + * header. If you are the client, close the connection. + */ + sig::Signal messageComplete; + + /** + * Chunk header callback. + * + * The parameter to the callback is the chunk size. + */ + sig::Signal chunkHeader; + + /** + * Chunk complete callback. + */ + sig::Signal<> chunkComplete; + + private: + http_parser m_parser; + http_parser_settings m_settings; + + size_t m_maxLength = 1024; + enum { kStart, kUrl, kStatus, kField, kValue } m_state = kStart; + SmallString<128> m_urlBuf; + SmallString<32> m_fieldBuf; + SmallString<128> m_valueBuf; + + bool m_aborted = false; +}; + +} // namespace wpi + +#endif // WPIUTIL_WPI_HTTPPARSER_H_ diff --git a/wpiutil/src/main/native/include/wpi/UrlParser.h b/wpiutil/src/main/native/include/wpi/UrlParser.h new file mode 100644 index 0000000000..5104805196 --- /dev/null +++ b/wpiutil/src/main/native/include/wpi/UrlParser.h @@ -0,0 +1,92 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef WPIUTIL_WPI_URLPARSER_H_ +#define WPIUTIL_WPI_URLPARSER_H_ + +#include "wpi/StringRef.h" +#include "wpi/http_parser.h" + +namespace wpi { + +/** + * Parses a URL into its constiuent components. + * `schema://userinfo@host:port/the/path?query#fragment` + */ +class UrlParser { + public: + /** + * Parse a URL. + * @param in input + * @param isConnect + */ + UrlParser(StringRef in, bool isConnect) { + m_data = in; + http_parser_url_init(&m_url); + m_error = http_parser_parse_url(in.data(), in.size(), isConnect, &m_url); + } + + /** + * Determine if the URL is valid (e.g. the parse was successful). + */ + bool IsValid() const { return !m_error; } + + bool HasSchema() const { return (m_url.field_set & UF_SCHEMA) != 0; } + + bool HasHost() const { return (m_url.field_set & UF_HOST) != 0; } + + bool HasPort() const { return (m_url.field_set & UF_PORT) != 0; } + + bool HasPath() const { return (m_url.field_set & UF_PATH) != 0; } + + bool HasQuery() const { return (m_url.field_set & UF_QUERY) != 0; } + + bool HasFragment() const { return (m_url.field_set & UF_FRAGMENT) != 0; } + + bool HasUserInfo() const { return (m_url.field_set & UF_USERINFO) != 0; } + + StringRef GetSchema() const { + return m_data.substr(m_url.field_data[UF_SCHEMA].off, + m_url.field_data[UF_SCHEMA].len); + } + + StringRef GetHost() const { + return m_data.substr(m_url.field_data[UF_HOST].off, + m_url.field_data[UF_HOST].len); + } + + unsigned int GetPort() const { return m_url.port; } + + StringRef GetPath() const { + return m_data.substr(m_url.field_data[UF_PATH].off, + m_url.field_data[UF_PATH].len); + } + + StringRef GetQuery() const { + return m_data.substr(m_url.field_data[UF_QUERY].off, + m_url.field_data[UF_QUERY].len); + } + + StringRef GetFragment() const { + return m_data.substr(m_url.field_data[UF_FRAGMENT].off, + m_url.field_data[UF_FRAGMENT].len); + } + + StringRef GetUserInfo() const { + return m_data.substr(m_url.field_data[UF_USERINFO].off, + m_url.field_data[UF_USERINFO].len); + } + + private: + bool m_error; + StringRef m_data; + http_parser_url m_url; +}; + +} // namespace wpi + +#endif // WPIUTIL_WPI_URLPARSER_H_ diff --git a/wpiutil/src/test/native/cpp/HttpParserTest.cpp b/wpiutil/src/test/native/cpp/HttpParserTest.cpp new file mode 100644 index 0000000000..bf236de299 --- /dev/null +++ b/wpiutil/src/test/native/cpp/HttpParserTest.cpp @@ -0,0 +1,193 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "wpi/HttpParser.h" // NOLINT(build/include_order) + +#include "gtest/gtest.h" + +namespace wpi { + +TEST(HttpParserTest, UrlMethodHeadersComplete) { + HttpParser p{HttpParser::kRequest}; + int callbacks = 0; + p.url.connect([&](StringRef path) { + ASSERT_EQ(path, "/foo/bar"); + ASSERT_EQ(p.GetUrl(), "/foo/bar"); + ++callbacks; + }); + p.Execute("GET /foo"); + p.Execute("/bar"); + ASSERT_EQ(callbacks, 0); + p.Execute(" HTTP/1.1\r\n\r\n"); + ASSERT_EQ(callbacks, 1); + ASSERT_EQ(p.GetUrl(), "/foo/bar"); + ASSERT_EQ(p.GetMethod(), HTTP_GET); + ASSERT_FALSE(p.HasError()); +} + +TEST(HttpParserTest, UrlMethodHeader) { + HttpParser p{HttpParser::kRequest}; + int callbacks = 0; + p.url.connect([&](StringRef path) { + ASSERT_EQ(path, "/foo/bar"); + ASSERT_EQ(p.GetUrl(), "/foo/bar"); + ++callbacks; + }); + p.Execute("GET /foo"); + p.Execute("/bar"); + ASSERT_EQ(callbacks, 0); + p.Execute(" HTTP/1.1\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("F"); + ASSERT_EQ(callbacks, 1); + ASSERT_EQ(p.GetUrl(), "/foo/bar"); + ASSERT_EQ(p.GetMethod(), HTTP_GET); + ASSERT_FALSE(p.HasError()); +} + +TEST(HttpParserTest, StatusHeadersComplete) { + HttpParser p{HttpParser::kResponse}; + int callbacks = 0; + p.status.connect([&](StringRef status) { + ASSERT_EQ(status, "OK"); + ASSERT_EQ(p.GetStatusCode(), 200u); + ++callbacks; + }); + p.Execute("HTTP/1.1 200"); + p.Execute(" OK"); + ASSERT_EQ(callbacks, 0); + p.Execute("\r\n\r\n"); + ASSERT_EQ(callbacks, 1); + ASSERT_EQ(p.GetStatusCode(), 200u); + ASSERT_FALSE(p.HasError()); +} + +TEST(HttpParserTest, StatusHeader) { + HttpParser p{HttpParser::kResponse}; + int callbacks = 0; + p.status.connect([&](StringRef status) { + ASSERT_EQ(status, "OK"); + ASSERT_EQ(p.GetStatusCode(), 200u); + ++callbacks; + }); + p.Execute("HTTP/1.1 200"); + p.Execute(" OK\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("F"); + ASSERT_EQ(callbacks, 1); + ASSERT_EQ(p.GetStatusCode(), 200u); + ASSERT_FALSE(p.HasError()); +} + +TEST(HttpParserTest, HeaderFieldComplete) { + HttpParser p{HttpParser::kRequest}; + int callbacks = 0; + p.header.connect([&](StringRef name, StringRef value) { + ASSERT_EQ(name, "Foo"); + ASSERT_EQ(value, "Bar"); + ++callbacks; + }); + p.Execute("GET / HTTP/1.1\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("Fo"); + ASSERT_EQ(callbacks, 0); + p.Execute("o: "); + ASSERT_EQ(callbacks, 0); + p.Execute("Bar"); + ASSERT_EQ(callbacks, 0); + p.Execute("\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("\r\n"); + ASSERT_EQ(callbacks, 1); + ASSERT_FALSE(p.HasError()); +} + +TEST(HttpParserTest, HeaderFieldNext) { + HttpParser p{HttpParser::kRequest}; + int callbacks = 0; + p.header.connect([&](StringRef name, StringRef value) { + ASSERT_EQ(name, "Foo"); + ASSERT_EQ(value, "Bar"); + ++callbacks; + }); + p.Execute("GET / HTTP/1.1\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("Fo"); + ASSERT_EQ(callbacks, 0); + p.Execute("o: "); + ASSERT_EQ(callbacks, 0); + p.Execute("Bar"); + ASSERT_EQ(callbacks, 0); + p.Execute("\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("F"); + ASSERT_EQ(callbacks, 1); + ASSERT_FALSE(p.HasError()); +} + +TEST(HttpParserTest, HeadersComplete) { + HttpParser p{HttpParser::kRequest}; + int callbacks = 0; + p.headersComplete.connect([&](bool keepAlive) { + ASSERT_EQ(keepAlive, false); + ++callbacks; + }); + p.Execute("GET / HTTP/1.0\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("\r\n"); + ASSERT_EQ(callbacks, 1); + ASSERT_FALSE(p.HasError()); +} + +TEST(HttpParserTest, HeadersCompleteHTTP11) { + HttpParser p{HttpParser::kRequest}; + int callbacks = 0; + p.headersComplete.connect([&](bool keepAlive) { + ASSERT_EQ(keepAlive, true); + ++callbacks; + }); + p.Execute("GET / HTTP/1.1\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("\r\n"); + ASSERT_EQ(callbacks, 1); + ASSERT_FALSE(p.HasError()); +} + +TEST(HttpParserTest, HeadersCompleteKeepAlive) { + HttpParser p{HttpParser::kRequest}; + int callbacks = 0; + p.headersComplete.connect([&](bool keepAlive) { + ASSERT_EQ(keepAlive, true); + ++callbacks; + }); + p.Execute("GET / HTTP/1.0\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("Connection: Keep-Alive\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("\r\n"); + ASSERT_EQ(callbacks, 1); + ASSERT_FALSE(p.HasError()); +} + +TEST(HttpParserTest, HeadersCompleteUpgrade) { + HttpParser p{HttpParser::kRequest}; + int callbacks = 0; + p.headersComplete.connect([&](bool) { + ASSERT_TRUE(p.IsUpgrade()); + ++callbacks; + }); + p.Execute("GET / HTTP/1.0\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("Connection: Upgrade\r\n"); + p.Execute("Upgrade: websocket\r\n"); + ASSERT_EQ(callbacks, 0); + p.Execute("\r\n"); + ASSERT_EQ(callbacks, 1); + ASSERT_FALSE(p.HasError()); +} + +} // namespace wpi