diff --git a/wpiutil/build.gradle b/wpiutil/build.gradle index 67ddd86c00..5d9c1e8d8f 100644 --- a/wpiutil/build.gradle +++ b/wpiutil/build.gradle @@ -106,6 +106,16 @@ ext { } } +def examplesMap = [:]; +file("$projectDir/examples").list(new FilenameFilter() { + @Override + public boolean accept(File current, String name) { + return new File(current, name).isDirectory(); + } +}).each { + examplesMap.put(it, []) +} + apply from: "${rootDir}/shared/javacpp/setupBuild.gradle" model { @@ -125,4 +135,21 @@ model { '_TI3?AVout_of_range', '_CT??_R0?AVbad_cast'] } } + components { + examplesMap.each { key, value -> + "${key}"(NativeExecutableSpec) { + binaries.all { + lib library: 'wpiutil', linkage: 'shared' + } + sources { + cpp { + source { + srcDirs 'examples/' + "${key}" + include '**/*.cpp' + } + } + } + } + } + } } diff --git a/wpiutil/examples/webserver/webserver.cpp b/wpiutil/examples/webserver/webserver.cpp new file mode 100644 index 0000000000..e112d04844 --- /dev/null +++ b/wpiutil/examples/webserver/webserver.cpp @@ -0,0 +1,86 @@ +/*----------------------------------------------------------------------------*/ +/* 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 + +#include "wpi/EventLoopRunner.h" +#include "wpi/HttpServerConnection.h" +#include "wpi/UrlParser.h" +#include "wpi/raw_ostream.h" +#include "wpi/uv/Loop.h" +#include "wpi/uv/Tcp.h" + +namespace uv = wpi::uv; + +class MyHttpServerConnection : public wpi::HttpServerConnection { + public: + explicit MyHttpServerConnection(std::shared_ptr stream) + : HttpServerConnection(stream) {} + + protected: + void ProcessRequest() override; +}; + +void MyHttpServerConnection::ProcessRequest() { + wpi::errs() << "HTTP request: '" << m_request.GetUrl() << "'\n"; + wpi::UrlParser url{m_request.GetUrl(), + m_request.GetMethod() == wpi::HTTP_CONNECT}; + if (!url.IsValid()) { + // failed to parse URL + SendError(400); + return; + } + + wpi::StringRef path; + if (url.HasPath()) path = url.GetPath(); + wpi::errs() << "path: \"" << path << "\"\n"; + + wpi::StringRef query; + if (url.HasQuery()) query = url.GetQuery(); + wpi::errs() << "query: \"" << query << "\"\n"; + + const bool isGET = m_request.GetMethod() == wpi::HTTP_GET; + if (isGET && path.equals("/")) { + // build HTML root page + wpi::SmallString<256> buf; + wpi::raw_svector_ostream os{buf}; + os << "WebServer Example"; + os << "

This is an example root page from the webserver."; + os << ""; + SendResponse(200, "OK", "text/html", os.str()); + } else { + SendError(404, "Resource not found"); + } +} + +int main() { + // Kick off the event loop on a separate thread + wpi::EventLoopRunner loop; + loop.ExecAsync([](uv::Loop& loop) { + auto tcp = uv::Tcp::Create(loop); + + // bind to listen address and port + tcp->Bind("", 8080); + + // when we get a connection, accept it and start reading + tcp->connection.connect([srv = tcp.get()] { + auto tcp = srv->Accept(); + if (!tcp) return; + wpi::errs() << "Got a connection\n"; + auto conn = std::make_shared(tcp); + tcp->SetData(conn); + }); + + // start listening for incoming connections + tcp->Listen(); + + wpi::errs() << "Listening on port 8080\n"; + }); + + // wait for a keypress to terminate + std::getchar(); +} diff --git a/wpiutil/src/main/native/cpp/HttpServerConnection.cpp b/wpiutil/src/main/native/cpp/HttpServerConnection.cpp new file mode 100644 index 0000000000..80872a15ed --- /dev/null +++ b/wpiutil/src/main/native/cpp/HttpServerConnection.cpp @@ -0,0 +1,126 @@ +/*----------------------------------------------------------------------------*/ +/* 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/HttpServerConnection.h" + +#include "wpi/SmallString.h" +#include "wpi/SmallVector.h" +#include "wpi/raw_uv_ostream.h" + +using namespace wpi; + +HttpServerConnection::HttpServerConnection(std::shared_ptr stream) + : m_stream(*stream) { + // process HTTP messages + m_request.messageComplete.connect([this](bool keepAlive) { + m_keepAlive = keepAlive; + ProcessRequest(); + }); + + // pass incoming data to HTTP parser + stream->data.connect([this](uv::Buffer& buf, size_t size) { + m_request.Execute(StringRef{buf.base, size}); + if (m_request.HasError()) { + // could not parse; just close the connection + m_stream.Close(); + } + }); + + // close when remote side closes + stream->end.connect([h = stream.get()] { h->Close(); }); + + // start reading + stream->StartRead(); +} + +void HttpServerConnection::BuildCommonHeaders(raw_ostream& os) { + os << "Server: WebServer/1.0\r\n" + "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, " + "post-check=0, max-age=0\r\n" + "Pragma: no-cache\r\n" + "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"; +} + +void HttpServerConnection::BuildHeader(raw_ostream& os, int code, + const Twine& codeText, + const Twine& contentType, + uint64_t contentLength, + const Twine& extra) { + os << "HTTP/" << m_request.GetMajor() << '.' << m_request.GetMinor() << ' ' + << code << ' ' << codeText << "\r\n"; + if (contentLength == 0) m_keepAlive = false; + if (!m_keepAlive) os << "Connection: close\r\n"; + BuildCommonHeaders(os); + os << "Content-Type: " << contentType << "\r\n"; + if (contentLength != 0) os << "Content-Length: " << contentLength << "\r\n"; + os << "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: *\r\n"; + SmallString<128> extraBuf; + StringRef extraStr = extra.toStringRef(extraBuf); + if (!extraStr.empty()) os << extraStr << "\r\n"; + os << "\r\n"; // header ends with a blank line +} + +void HttpServerConnection::SendData(ArrayRef bufs, + bool closeAfter) { + m_stream.Write(bufs, [ closeAfter, stream = &m_stream ]( + MutableArrayRef bufs, uv::Error) { + for (auto&& buf : bufs) buf.Deallocate(); + if (closeAfter) stream->Close(); + }); +} + +void HttpServerConnection::SendResponse(int code, const Twine& codeText, + const Twine& contentType, + StringRef content, + const Twine& extraHeader) { + SmallVector toSend; + raw_uv_ostream os{toSend, 4096}; + BuildHeader(os, code, codeText, contentType, content.size(), extraHeader); + os << content; + // close after write completes if we aren't keeping alive + SendData(os.bufs(), !m_keepAlive); +} + +void HttpServerConnection::SendError(int code, const Twine& message) { + StringRef codeText, extra, baseMessage; + switch (code) { + case 401: + codeText = "Unauthorized"; + extra = "WWW-Authenticate: Basic realm=\"CameraServer\""; + baseMessage = "401: Not Authenticated!"; + break; + case 404: + codeText = "Not Found"; + baseMessage = "404: Not Found!"; + break; + case 500: + codeText = "Internal Server Error"; + baseMessage = "500: Internal Server Error!"; + break; + case 400: + codeText = "Bad Request"; + baseMessage = "400: Not Found!"; + break; + case 403: + codeText = "Forbidden"; + baseMessage = "403: Forbidden!"; + break; + case 503: + codeText = "Service Unavailable"; + baseMessage = "503: Service Unavailable"; + break; + default: + code = 501; + codeText = "Not Implemented"; + baseMessage = "501: Not Implemented!"; + break; + } + SmallString<256> content = baseMessage; + content += "\r\n"; + message.toVector(content); + SendResponse(code, codeText, "text/plain", content, extra); +} diff --git a/wpiutil/src/main/native/include/wpi/HttpServerConnection.h b/wpiutil/src/main/native/include/wpi/HttpServerConnection.h new file mode 100644 index 0000000000..51701ed071 --- /dev/null +++ b/wpiutil/src/main/native/include/wpi/HttpServerConnection.h @@ -0,0 +1,123 @@ +/*----------------------------------------------------------------------------*/ +/* 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_HTTPSERVERCONNECTION_H_ +#define WPIUTIL_WPI_HTTPSERVERCONNECTION_H_ + +#include + +#include "wpi/ArrayRef.h" +#include "wpi/HttpParser.h" +#include "wpi/StringRef.h" +#include "wpi/Twine.h" +#include "wpi/uv/Stream.h" + +namespace wpi { + +class raw_ostream; + +class HttpServerConnection { + public: + explicit HttpServerConnection(std::shared_ptr stream); + virtual ~HttpServerConnection() = default; + + protected: + /** + * Process an incoming HTTP request. This is called after the incoming + * message completes (e.g. from the HttpParser::messageComplete callback). + * + * The implementation should read request details from m_request and call the + * appropriate Send() functions to send a response back to the client. + */ + virtual void ProcessRequest() = 0; + + /** + * Build common response headers. + * + * Called by SendHeader() to send headers common to every response. + * Each line must be terminated with \r\n. + * + * The default implementation sends the following: + * "Server: WebServer/1.0\r\n" + * "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, " + * "post-check=0, max-age=0\r\n" + * "Pragma: no-cache\r\n" + * "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n" + * + * These parameters should ensure the browser does not cache the response. + * A browser should connect for each file and not serve files from its cache. + * + * @param os response stream + */ + virtual void BuildCommonHeaders(raw_ostream& os); + + /** + * Build HTTP response header, along with other header information like + * mimetype. Calls BuildCommonHeaders(). + * + * @param os response stream + * @param code HTTP response code (e.g. 200) + * @param codeText HTTP response code text (e.g. "OK") + * @param contentType MIME content type (e.g. "text/plain") + * @param contentLength Length of content. If 0 is provided, m_keepAlive will + * be set to false. + * @param extra Extra HTTP headers to send, not including final "\r\n" + */ + void BuildHeader(raw_ostream& os, int code, const Twine& codeText, + const Twine& contentType, uint64_t contentLength, + const Twine& extra = Twine{}); + + /** + * Send data to client. + * + * This is a convenience wrapper around m_stream.Write() to provide + * auto-close functionality. + * + * @param bufs Buffers to write. Deallocate() will be called on each + * buffer after the write completes. If different behavior + * is desired, call m_stream.Write() directly instead. + * @param closeAfter close the connection after the write completes + */ + void SendData(ArrayRef bufs, bool closeAfter = false); + + /** + * Send HTTP response, along with other header information like mimetype. + * Calls BuildHeader(). + * + * @param code HTTP response code (e.g. 200) + * @param codeText HTTP response code text (e.g. "OK") + * @param contentType MIME content type (e.g. "text/plain") + * @param content Response message content + * @param extraHeader Extra HTTP headers to send, not including final "\r\n" + */ + void SendResponse(int code, const Twine& codeText, const Twine& contentType, + StringRef content, const Twine& extraHeader = Twine{}); + + /** + * Send error header and message. + * This provides standard code responses for 400, 401, 403, 404, 500, and 503. + * Other codes will be reported as 501. For arbitrary code handling, use + * SendResponse() instead. + * + * @param code HTTP error code (e.g. 404) + * @param message Additional message text + */ + void SendError(int code, const Twine& message = Twine{}); + + /** The HTTP request. */ + HttpParser m_request{HttpParser::kRequest}; + + /** Whether the connection should be kept alive. */ + bool m_keepAlive = false; + + /** The underlying stream for the connection. */ + uv::Stream& m_stream; +}; + +} // namespace wpi + +#endif // WPIUTIL_WPI_HTTPSERVERCONNECTION_H_