From e415ca66b683334d2c0849edbfb0cffa5c56310d Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Thu, 8 Sep 2016 00:05:10 -0700 Subject: [PATCH] Initial HTTPSink implementation. Untested. --- src/HTTPSinkImpl.cpp | 469 +++++++++++++++++++++++++++++++++++++++ src/HTTPSinkImpl.h | 75 +++++++ src/cameraserver_c.cpp | 9 - src/cameraserver_cpp.cpp | 13 -- 4 files changed, 544 insertions(+), 22 deletions(-) create mode 100644 src/HTTPSinkImpl.cpp create mode 100644 src/HTTPSinkImpl.h diff --git a/src/HTTPSinkImpl.cpp b/src/HTTPSinkImpl.cpp new file mode 100644 index 0000000000..3f0ee2e2c7 --- /dev/null +++ b/src/HTTPSinkImpl.cpp @@ -0,0 +1,469 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) FIRST 2016. 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 "HTTPSinkImpl.h" + +#include + +#include "llvm/SmallString.h" +#include "llvm/StringExtras.h" +#include "support/raw_socket_istream.h" +#include "support/raw_socket_ostream.h" +#include "tcpsockets/TCPAcceptor.h" + +#include "cameraserver_cpp.h" +#include "Handle.h" +#include "Log.h" +#include "SourceImpl.h" + +using namespace cs; + + +// The boundary used for the M-JPEG stream. +// It separates the multipart stream of pictures +#define BOUNDARY "boundarydonotcross" + +// Standard header to send along with other header information like mimetype. +// +// The parameters should ensure the browser does not cache our answer. +// A browser should connect for each file and not serve files from its cache. +// Using cached pictures would lead to showing old/outdated pictures. +// Many browsers seem to ignore, or at least not always obey, those headers. +void HTTPSinkImpl::SendHeader(llvm::raw_ostream& os, int code, + llvm::StringRef codeText, + llvm::StringRef contentType, + llvm::StringRef extra) { + os << "HTTP/1.0 " << code << ' ' << codeText << "\r\n"; + os << "Connection: close\r\n" + "Server: CameraServer/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"; + os << "Content-Type: " << contentType << "\r\n"; + if (!extra.empty()) os << extra << "\r\n"; + os << "\r\n"; // header ends with a blank line +} + +// Send error header and message +// @param code HTTP error code (e.g. 404) +// @param message Additional message text +void HTTPSinkImpl::SendError(llvm::raw_ostream& os, int code, + llvm::StringRef message) { + llvm::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; + default: + code = 501; + codeText = "Not Implemented"; + baseMessage = "501: Not Implemented!"; + break; + } + SendHeader(os, code, codeText, "text/plain", extra); + os << baseMessage << "\r\n" << message; +} + +// Read a line from an input stream (up to a maximum length). +// The returned buffer will contain the trailing \n (unless the maximum length +// was reached). +bool HTTPSinkImpl::ReadLine(wpi::raw_istream& istream, + llvm::SmallVectorImpl& buffer, int maxLen) { + buffer.clear(); + for (int i = 0; i < maxLen; ++i) { + char c; + istream.read(c); + if (istream.has_error()) return false; + buffer.push_back(c); + if (c == '\n') break; + } + return true; +} + +// Unescape a %xx-encoded URI. Returns false on error. +bool HTTPSinkImpl::UnescapeURI(llvm::StringRef str, + llvm::SmallVectorImpl& out) { + for (auto i = str.begin(), end = str.end(); i != end; ++i) { + // pass non-escaped characters to output + if (*i != '%') { + // decode + to space + if (*i == '+') + out.push_back(' '); + else + out.push_back(*i); + continue; + } + + // are there enough characters left? + if (i + 2 < end) return false; + + // replace %xx with the corresponding character + unsigned val1 = llvm::hexDigitValue(*++i); + if (val1 == -1U) return false; + unsigned val2 = llvm::hexDigitValue(*++i); + if (val2 == -1U) return false; + out.push_back((val1 << 4) | val2); + } + + return true; +} + +// Perform a command specified by HTTP GET parameters. +bool HTTPSinkImpl::ProcessCommand(llvm::raw_ostream& os, SourceImpl& source, + llvm::StringRef parameters, bool respond) { + // command format: param1=value1¶m2=value2... + while (!parameters.empty()) { + // split out next param and value + llvm::StringRef rawParam, rawValue; + std::tie(rawParam, parameters) = parameters.split('&'); + if (rawParam.empty()) continue; // ignore "&&" + std::tie(rawParam, rawValue) = rawParam.split('='); + if (rawParam.empty() || rawValue.empty()) continue; // ignore "param=" + + // unescape param + llvm::SmallString<64> param; + if (!UnescapeURI(rawParam, param)) { + llvm::SmallString<128> error; + llvm::raw_svector_ostream oss{error}; + oss << "could not unescape parameter \"" << rawParam << "\""; + SendError(os, 500, error.str()); + DEBUG(error.str()); + return false; + } + + // unescape value + llvm::SmallString<64> value; + if (!UnescapeURI(rawValue, value)) { + llvm::SmallString<128> error; + llvm::raw_svector_ostream oss{error}; + oss << "could not unescape value \"" << rawValue << "\""; + SendError(os, 500, error.str()); + DEBUG(error.str()); + return false; + } + + // try to assign parameter + } + + if (respond) { + // Send HTTP response + SendHeader(os, 200, "OK", "text/plain"); + //os << command << ": " << res; + } + + return true; +} + +// Send a JSON file which is contains information about the source parameters. +void HTTPSinkImpl::SendJSON(llvm::raw_ostream& os, SourceImpl& source, + bool header) { + if (header) SendHeader(os, 200, "OK", "application/x-javascript"); + + os << "{\n\"controls\": [\n"; + llvm::SmallVector properties; + source.EnumerateProperties(properties); + bool first = true; + for (auto prop : properties) { + if (first) + first = false; + else + os << ",\n"; + os << "{"; + llvm::SmallString<128> name; + source.GetPropertyName(prop, name); + auto type = source.GetPropertyType(prop); + os << "\n\"name\": \"" << name << '"'; + os << ",\n\"id\": \"" << prop << '"'; + os << ",\n\"type\": \"" << type << '"'; + os << ",\n\"min\": \"" << source.GetPropertyMin(prop) << '"'; + os << ",\n\"max\": \"" << source.GetPropertyMax(prop) << '"'; + // os << ",\n\"step\": \"" << param->step << '"'; + // os << ",\n\"default\": \"" << param->default_value << '"'; + os << ",\n\"value\": \""; + switch (type) { + case CS_PROP_BOOLEAN: + os << (source.GetBooleanProperty(prop) ? "1" : "0"); + break; + case CS_PROP_DOUBLE: + os << source.GetDoubleProperty(prop); + break; + case CS_PROP_STRING: { + llvm::SmallString<128> strval; + source.GetStringProperty(prop, strval); + os << strval.str(); + break; + } + case CS_PROP_ENUM: + os << source.GetEnumProperty(prop); + break; + default: + break; + } + os << '"'; + os << ",\n\"dest\": \"0\","; + // os << ",\n\"flags\": \"" << param->flags << '"'; + // os << ",\n\"group\": \"" << param->group << '"'; + + // append the menu object to the menu typecontrols + if (source.GetPropertyType(prop) == CS_PROP_ENUM) { + os << ",\n\"menu\": {"; + auto choices = source.GetEnumPropertyChoices(prop); + int j = 0; + for (auto choice = choices.begin(), end = choices.end(); choice != end; + ++j, ++choice) { + if (j != 0) os << ", "; + // replace any non-printable characters in name with spaces + llvm::SmallString<128> ch_name; + for (char ch : *choice) ch_name.push_back(isprint(ch) ? ch : ' '); + os << '"' << j << "\": \"" << ch_name << '"'; + } + os << "}\n"; + } + os << '}'; + } + os << "\n]\n}\n"; + os.flush(); +} + +HTTPSinkImpl::HTTPSinkImpl(llvm::StringRef name, + std::unique_ptr acceptor) + : SinkImpl{name}, m_acceptor{std::move(acceptor)} { + m_serverThread = std::thread(&HTTPSinkImpl::ServerThreadMain, this); +} + +HTTPSinkImpl::~HTTPSinkImpl() { Stop(); } + +void HTTPSinkImpl::Stop() { + m_active = false; + + // wake up server thread by shutting down the socket + m_acceptor->shutdown(); + + // join server thread + if (m_serverThread.joinable()) m_serverThread.join(); + + // close streams + for (auto& stream : m_connStreams) stream->close(); + + // wake up connection threads by forcing an empty frame to be sent + if (auto source = GetSource()) + source->Wakeup(); + + // join connection threads + for (auto& connThread : m_connThreads) connThread.join(); +} + +// Send HTTP response and a stream of JPG-frames +void HTTPSinkImpl::SendStream(wpi::raw_socket_ostream& os) { + os.SetUnbuffered(); + + llvm::SmallString<256> header; + llvm::raw_svector_ostream oss{header}; + + SendHeader(oss, 200, "OK", "multipart/x-mixed-replace;boundary=" BOUNDARY, + "Access-Control-Allow-Origin: *"); + oss << "--" BOUNDARY "\r\n"; + os << oss.str(); + + DEBUG("Headers send, sending stream now"); + + while (m_active && !os.has_error()) { + auto source = GetSource(); + if (!source) { + // Source disconnected; sleep for one second + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + Frame frame = source->GetNextFrame(); // blocks + if (!m_active) break; + if (!frame) { + // Bad frame; sleep for 10 ms so we don't consume all processor time. + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // print the individual mimetype and the length + // sending the content-length fixes random stream disruption observed + // with firefox + double timestamp = std::chrono::duration_cast( + frame.time().time_since_epoch()) + .count(); + header.clear(); + oss << "Content-Type: image/jpeg\r\n" + << "Content-Length: " << frame.size(m_sourceChannel) << "\r\n" + << "X-Timestamp: " << timestamp << "\r\n" + << "\r\n"; + os << oss.str(); + os << frame.data(m_sourceChannel); + os << "\r\n--" BOUNDARY "\r\n"; + // os.flush(); + } +} + + +// thread for clients that connected to this server +void HTTPSinkImpl::ConnThreadMain(wpi::NetworkStream* stream) { + wpi::raw_socket_istream is{*stream}; + + // Read the request string from the stream + llvm::SmallString<128> buf; + if (!ReadLine(is, buf, 4096)) return; + + wpi::raw_socket_ostream os{*stream}; + enum { kCommand, kStream, kGetSettings } type; + llvm::StringRef parameters; + size_t pos; + + // Determine request type. Most of these are for mjpgstreamer + // compatibility. + if ((pos = buf.find("POST /stream")) != llvm::StringRef::npos) { + type = kStream; + parameters = buf.substr(buf.find('?', pos + 12)).substr(1); + } else if ((pos = buf.find("GET /?action=stream")) != llvm::StringRef::npos) { + type = kStream; + parameters = buf.substr(buf.find('&', pos + 19)).substr(1); + } else if (buf.find("GET /input") != llvm::StringRef::npos && + buf.find(".json") != llvm::StringRef::npos) { + type = kGetSettings; + } else if (buf.find("GET /output") != llvm::StringRef::npos && + buf.find(".json") != llvm::StringRef::npos) { + type = kGetSettings; + } else if ((pos = buf.find("GET /?action=command")) != + llvm::StringRef::npos) { + type = kCommand; + parameters = buf.substr(buf.find('&', pos + 20)).substr(1); + } else { + DEBUG("HTTP request resource not found"); + SendError(os, 404, "Resource not found"); + return; + } + + // Parameter can only be certain characters. This also strips the EOL. + pos = parameters.find_first_not_of( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_" + "-=&1234567890%./"); + parameters = parameters.substr(0, pos); + DEBUG("command parameters: \"" << parameters << "\""); + + // Read the rest of the HTTP request. + // The end of the request is marked by a single, empty line with "\r\n" + do { + if (!ReadLine(is, buf, 4096)) return; + } while (!buf.startswith("\r\n")); + + // Send response + switch (type) { + case kStream: + if (auto source = GetSource()) { + DEBUG("request for stream " << source->GetName()); + if (!ProcessCommand(os, *source, parameters, false)) return; + } + SendStream(os); + break; + case kCommand: + if (auto source = GetSource()) { + if (!ProcessCommand(os, *source, parameters, true)) return; + SendHeader(os, 200, "OK", "text/plain"); + } else { + SendHeader(os, 200, "OK", "text/plain"); + os << "Ignored due to no connected source." << "\r\n"; + DEBUG("Ignored due to no connected source."); + } + break; + case kGetSettings: + DEBUG("request for JSON file"); + if (auto source = GetSource()) + SendJSON(os, *source, true); + else + SendError(os, 404, "Resource not found"); + break; + } + + DEBUG("leaving HTTP client thread"); +} + +// Main server thread +void HTTPSinkImpl::ServerThreadMain() { + if (m_acceptor->start() != 0) { + m_active = false; + return; + } + + DEBUG("waiting for clients to connect"); + while (m_active) { + auto stream = m_acceptor->accept(); + if (!stream) { + m_active = false; + return; + } + if (!m_active) return; + + DEBUG("server: client connection from " << stream->getPeerIP()); + + m_connThreads.emplace_back(&HTTPSinkImpl::ConnThreadMain, this, + stream.get()); + m_connStreams.emplace_back(std::move(stream)); + } + + DEBUG("leaving server thread"); +} + +namespace cs { + +CS_Sink CreateHTTPSink(llvm::StringRef name, llvm::StringRef listenAddress, + int port, CS_Status* status) { + llvm::SmallString<128> str{listenAddress}; + auto sink = std::make_shared( + name, std::unique_ptr(new wpi::TCPAcceptor( + port, str.c_str(), Logger::GetInstance()))); + return Sinks::GetInstance().Allocate(SinkData::kHTTP, sink); +} + +void SetSinkSourceChannel(CS_Sink sink, int channel, CS_Status* status) { + auto data = Sinks::GetInstance().Get(sink); + if (!data) { + *status = CS_INVALID_HANDLE; + return; + } + if (data->type != SinkData::kHTTP) { + *status = CS_WRONG_HANDLE_SUBTYPE; + return; + } + static_cast(data->sink.get())->SetSourceChannel(channel); +} + +} // namespace cs + +extern "C" { + +CS_Sink CS_CreateHTTPSink(const char* name, const char* listenAddress, int port, + CS_Status* status) { + return cs::CreateHTTPSink(name, listenAddress, port, status); +} + +void CS_SetSinkSourceChannel(CS_Sink sink, int channel, CS_Status* status) { + return cs::SetSinkSourceChannel(sink, channel, status); +} + +} // extern "C" diff --git a/src/HTTPSinkImpl.h b/src/HTTPSinkImpl.h new file mode 100644 index 0000000000..bef53ada91 --- /dev/null +++ b/src/HTTPSinkImpl.h @@ -0,0 +1,75 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) FIRST 2016. 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 CAMERASERVER_HTTPSINKIMPL_H_ +#define CAMERASERVER_HTTPSINKIMPL_H_ + +#include +#include +#include +#include +#include +#include + +#include "llvm/raw_ostream.h" +#include "llvm/SmallVector.h" +#include "llvm/StringRef.h" +#include "support/raw_istream.h" +#include "support/raw_socket_ostream.h" +#include "tcpsockets/NetworkAcceptor.h" +#include "tcpsockets/NetworkStream.h" + +#include "SinkImpl.h" + +namespace cs { + +class SourceImpl; + +class HTTPSinkImpl : public SinkImpl { + public: + HTTPSinkImpl(llvm::StringRef name, + std::unique_ptr acceptor); + ~HTTPSinkImpl() override; + + void GetDescription(llvm::SmallVectorImpl& desc) const override; + + void SetSourceChannel(int channel) { m_sourceChannel.store(channel); } + + void Stop(); + + static void SendHeader(llvm::raw_ostream& os, int code, + llvm::StringRef codeText, llvm::StringRef contentType, + llvm::StringRef extra = llvm::StringRef{}); + static void SendError(llvm::raw_ostream& os, int code, + llvm::StringRef message); + static bool ReadLine(wpi::raw_istream& istream, + llvm::SmallVectorImpl& buffer, int maxLen); + static bool UnescapeURI(llvm::StringRef str, + llvm::SmallVectorImpl& out); + static bool ProcessCommand(llvm::raw_ostream& os, SourceImpl& source, + llvm::StringRef parameters, bool respond); + static void SendJSON(llvm::raw_ostream& os, SourceImpl& source, bool header); + + void SendStream(wpi::raw_socket_ostream& os); + + private: + void ServerThreadMain(); + void ConnThreadMain(wpi::NetworkStream* stream); + + std::unique_ptr m_acceptor; + std::atomic_int m_sourceChannel; + std::atomic_bool m_active; // set to false to terminate threads + std::thread m_serverThread; + + std::mutex m_mutex; + std::vector m_connThreads; + std::vector> m_connStreams; +}; + +} // namespace cs + +#endif // CAMERASERVER_HTTPSINKIMPL_H_ diff --git a/src/cameraserver_c.cpp b/src/cameraserver_c.cpp index 96093d3bbf..b58bea5c91 100644 --- a/src/cameraserver_c.cpp +++ b/src/cameraserver_c.cpp @@ -212,11 +212,6 @@ void CS_RemoveSourcePropertyByName(CS_Source source, const char* name, return cs::RemoveSourceProperty(source, name, status); } -CS_Sink CS_CreateHTTPSink(const char* name, const char* listenAddress, int port, - CS_Status* status) { - return cs::CreateHTTPSink(name, listenAddress, port, status); -} - CS_Sink CS_CreateCvSink(const char* name, CS_Status* status) { return cs::CreateCvSink(name, status); } @@ -263,10 +258,6 @@ void CS_ReleaseSink(CS_Sink sink, CS_Status* status) { return cs::ReleaseSink(sink, status); } -void CS_SetSinkSourceChannel(CS_Sink sink, int channel, CS_Status* status) { - return cs::SetSinkSourceChannel(sink, channel, status); -} - uint64_t CS_SinkWaitForFrame(CS_Sink sink, CS_Status* status) { return cs::SinkWaitForFrame(sink, status); } diff --git a/src/cameraserver_cpp.cpp b/src/cameraserver_cpp.cpp index 951a1e0fae..fd80c1cece 100644 --- a/src/cameraserver_cpp.cpp +++ b/src/cameraserver_cpp.cpp @@ -336,11 +336,6 @@ void RemoveSourceProperty(CS_Source source, llvm::StringRef name, // Sink Creation Functions // -CS_Sink CreateHTTPSink(llvm::StringRef name, llvm::StringRef listenAddress, - int port, CS_Status* status) { - return 0; // TODO -} - CS_Sink CreateCvSink(llvm::StringRef name, CS_Status* status) { return 0; // TODO } @@ -451,14 +446,6 @@ void ReleaseSink(CS_Sink sink, CS_Status* status) { if (--(data->refCount) == 0) inst.Free(sink); } -// -// Server Sink (e.g. HTTP) Functions -// - -void SetSinkSourceChannel(CS_Sink sink, int channel, CS_Status* status) { - // TODO -} - // // OpenCV Sink Functions //