mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-26 01:51:41 +00:00
Add wpi::HttpServerConnection and an example of its use (#1281)
This commit is contained in:
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
86
wpiutil/examples/webserver/webserver.cpp
Normal file
86
wpiutil/examples/webserver/webserver.cpp
Normal file
@@ -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 <cstdio>
|
||||
|
||||
#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<uv::Stream> 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 << "<html><head><title>WebServer Example</title></head>";
|
||||
os << "<body><p>This is an example root page from the webserver.";
|
||||
os << "</body></html>";
|
||||
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<MyHttpServerConnection>(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();
|
||||
}
|
||||
126
wpiutil/src/main/native/cpp/HttpServerConnection.cpp
Normal file
126
wpiutil/src/main/native/cpp/HttpServerConnection.cpp
Normal file
@@ -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<uv::Stream> 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<uv::Buffer> bufs,
|
||||
bool closeAfter) {
|
||||
m_stream.Write(bufs, [ closeAfter, stream = &m_stream ](
|
||||
MutableArrayRef<uv::Buffer> 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<uv::Buffer, 4> 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);
|
||||
}
|
||||
123
wpiutil/src/main/native/include/wpi/HttpServerConnection.h
Normal file
123
wpiutil/src/main/native/include/wpi/HttpServerConnection.h
Normal file
@@ -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 <memory>
|
||||
|
||||
#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<uv::Stream> 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<uv::Buffer> 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_
|
||||
Reference in New Issue
Block a user