2021-03-23 21:12:55 -05:00
|
|
|
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
|
|
|
|
/*
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
#include <cstdint>
|
|
|
|
#include <iostream>
|
|
|
|
#include <fstream>
|
|
|
|
#include <memory>
|
2021-03-23 22:57:53 -05:00
|
|
|
#include <condition_variable>
|
|
|
|
#include <mutex>
|
2021-03-23 21:12:55 -05:00
|
|
|
#include <sstream>
|
|
|
|
#include <string>
|
|
|
|
|
|
|
|
#include "Common.hpp"
|
2021-03-23 22:57:53 -05:00
|
|
|
#include <common/MessageQueue.hpp>
|
2021-03-23 21:12:55 -05:00
|
|
|
#include "NetUtil.hpp"
|
|
|
|
#include <net/Socket.hpp>
|
|
|
|
#include <net/HttpRequest.hpp>
|
|
|
|
#include <net/WebSocketHandler.hpp>
|
|
|
|
#include <utility>
|
|
|
|
#if ENABLE_SSL
|
|
|
|
#include <net/SslSocket.hpp>
|
|
|
|
#endif
|
|
|
|
#include "Log.hpp"
|
|
|
|
#include "Util.hpp"
|
|
|
|
|
|
|
|
// This is a partial implementation of RFC 6455
|
|
|
|
// The WebSocket Protocol.
|
|
|
|
|
|
|
|
namespace http
|
|
|
|
{
|
2021-03-23 22:57:53 -05:00
|
|
|
/// A client socket for asynchronous Web-Socket protocol.
|
2021-03-23 21:12:55 -05:00
|
|
|
class WebSocketSession final : public WebSocketHandler
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
enum class Protocol
|
|
|
|
{
|
|
|
|
HttpUnencrypted,
|
|
|
|
HttpSsl,
|
|
|
|
};
|
|
|
|
|
|
|
|
private:
|
|
|
|
WebSocketSession(const std::string& hostname, Protocol protocolType, int portNumber)
|
2021-03-23 22:57:53 -05:00
|
|
|
: WebSocketHandler(true)
|
|
|
|
, _host(hostname)
|
2021-03-23 21:12:55 -05:00
|
|
|
, _port(std::to_string(portNumber))
|
|
|
|
, _protocol(protocolType)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the given protocol's scheme.
|
|
|
|
static const char* getProtocolScheme(Protocol protocol)
|
|
|
|
{
|
|
|
|
switch (protocol)
|
|
|
|
{
|
|
|
|
case Protocol::HttpUnencrypted:
|
|
|
|
return "ws";
|
|
|
|
case Protocol::HttpSsl:
|
|
|
|
return "wss";
|
|
|
|
}
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
public:
|
2021-03-24 22:18:02 -05:00
|
|
|
|
|
|
|
/// Destroy WebSocketSession.
|
|
|
|
/// Note: must never be called with the owning poll thread still active.
|
|
|
|
~WebSocketSession()
|
|
|
|
{
|
|
|
|
shutdown();
|
|
|
|
}
|
|
|
|
|
2021-03-23 21:12:55 -05:00
|
|
|
/// Create a new HTTP WebSocketSession to the given host.
|
|
|
|
/// The port defaults to the protocol's default port.
|
|
|
|
static std::shared_ptr<WebSocketSession> create(const std::string& host, Protocol protocol,
|
|
|
|
int port = 0)
|
|
|
|
{
|
|
|
|
port = (port > 0 ? port : getDefaultPort(protocol));
|
|
|
|
return std::shared_ptr<WebSocketSession>(new WebSocketSession(host, protocol, port));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a new HTTP WebSocketSession to the given URI.
|
|
|
|
/// The @uri must include the scheme, e.g. https://domain.com:9980
|
|
|
|
static std::shared_ptr<WebSocketSession> create(const std::string& uri)
|
|
|
|
{
|
2021-03-24 21:38:31 -05:00
|
|
|
std::string scheme;
|
|
|
|
std::string host;
|
|
|
|
std::string port;
|
|
|
|
if (!net::parseUri(uri, scheme, host, port))
|
2021-03-23 21:12:55 -05:00
|
|
|
{
|
2021-03-24 21:38:31 -05:00
|
|
|
LOG_ERR("Invalid URI while creating WebSocketSession: " << uri);
|
2021-03-23 21:12:55 -05:00
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2021-03-24 21:38:31 -05:00
|
|
|
const std::string lowerScheme = Util::toLower(scheme);
|
|
|
|
if (!Util::startsWith(lowerScheme, "http") && !Util::startsWith(lowerScheme, "ws"))
|
2021-03-23 21:12:55 -05:00
|
|
|
{
|
2021-03-24 21:38:31 -05:00
|
|
|
LOG_ERR("Unsupported scheme in URI while creating WebSocketSession: " << uri);
|
2021-03-23 21:12:55 -05:00
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2021-03-24 21:38:31 -05:00
|
|
|
const bool secure
|
|
|
|
= Util::startsWith(lowerScheme, "https") || Util::startsWith(lowerScheme, "wss");
|
2021-03-23 21:12:55 -05:00
|
|
|
|
2021-03-24 21:38:31 -05:00
|
|
|
const int portInt = port.empty() ? 0 : std::stoi(port);
|
|
|
|
return create(host, secure ? Protocol::HttpSsl : Protocol::HttpUnencrypted, portInt);
|
2021-03-23 21:12:55 -05:00
|
|
|
}
|
|
|
|
|
2021-03-24 22:18:02 -05:00
|
|
|
/// Create a WebSocketSession and make a request to given @url.
|
|
|
|
static std::shared_ptr<WebSocketSession> create(SocketPoll& socketPoll, const std::string& uri,
|
|
|
|
const std::string& url)
|
|
|
|
{
|
|
|
|
auto session = create(uri);
|
|
|
|
if (session)
|
|
|
|
{
|
|
|
|
http::Request req(url);
|
|
|
|
session->asyncRequest(req, socketPoll);
|
|
|
|
}
|
|
|
|
|
|
|
|
return session;
|
|
|
|
}
|
|
|
|
|
2021-03-23 21:12:55 -05:00
|
|
|
/// Returns the given protocol's default port.
|
|
|
|
static int getDefaultPort(Protocol protocol)
|
|
|
|
{
|
|
|
|
switch (protocol)
|
|
|
|
{
|
|
|
|
case Protocol::HttpUnencrypted:
|
|
|
|
return 80;
|
|
|
|
case Protocol::HttpSsl:
|
|
|
|
return 443;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the current protocol scheme.
|
|
|
|
const char* getProtocolScheme() const { return getProtocolScheme(_protocol); }
|
|
|
|
|
|
|
|
const std::string& host() const { return _host; }
|
|
|
|
const std::string& port() const { return _port; }
|
|
|
|
Protocol protocol() const { return _protocol; }
|
|
|
|
bool isSecure() const { return _protocol == Protocol::HttpSsl; }
|
|
|
|
|
2021-03-24 21:30:04 -05:00
|
|
|
bool asyncRequest(http::Request& req, SocketPoll& poll)
|
2021-03-23 21:12:55 -05:00
|
|
|
{
|
|
|
|
LOG_TRC("asyncRequest: " << req.getVerb() << ' ' << host() << ':' << port() << ' '
|
|
|
|
<< req.getUrl());
|
|
|
|
|
2021-03-24 12:48:05 -05:00
|
|
|
return wsRequest(req, host(), port(), isSecure(), poll);
|
2021-03-23 21:12:55 -05:00
|
|
|
}
|
|
|
|
|
2021-03-23 22:57:53 -05:00
|
|
|
/// Wait until the given prefix is matched and return the payload.
|
|
|
|
std::vector<char> waitForMessage(const std::string& prefix, std::chrono::milliseconds timeout)
|
|
|
|
{
|
|
|
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
|
|
|
LOG_DBG("Waiting for [" << prefix << "] for " << timeout);
|
|
|
|
|
|
|
|
std::unique_lock<std::mutex> lock(_mutex);
|
|
|
|
do
|
|
|
|
{
|
|
|
|
// Drain the queue, first.
|
|
|
|
while (!_queue.isEmpty())
|
|
|
|
{
|
|
|
|
std::vector<char> message = _queue.pop();
|
|
|
|
if (matchMessage(prefix, message))
|
|
|
|
return message;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Timed wait, if we must.
|
|
|
|
} while (_cv.wait_until(lock, deadline, [this]() { return !_queue.isEmpty(); }));
|
|
|
|
|
|
|
|
LOG_DBG("Giving up waiting for [" << prefix << "] after " << timeout);
|
|
|
|
return std::vector<char>();
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
void handleMessage(const std::vector<char>& data) override
|
|
|
|
{
|
|
|
|
LOG_DBG("Got message: " << LOOLProtocol::getFirstLine(data));
|
|
|
|
std::unique_lock<std::mutex> lock(_mutex);
|
|
|
|
_queue.put(data);
|
|
|
|
_cv.notify_one();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool matchMessage(const std::string& prefix, const std::vector<char>& message)
|
|
|
|
{
|
|
|
|
const auto header = LOOLProtocol::getFirstLine(message);
|
|
|
|
LOG_DBG("Evaluating message: " << header);
|
|
|
|
return LOOLProtocol::matchPrefix(prefix, header);
|
|
|
|
}
|
2021-03-23 21:12:55 -05:00
|
|
|
|
2021-03-24 22:18:02 -05:00
|
|
|
using WebSocketHandler::shutdown;
|
|
|
|
|
|
|
|
void shutdown()
|
|
|
|
{
|
|
|
|
LOG_TRC("shutdown");
|
|
|
|
shutdown(true, "Shutting down");
|
|
|
|
}
|
|
|
|
|
2021-03-23 21:12:55 -05:00
|
|
|
private:
|
|
|
|
const std::string _host;
|
|
|
|
const std::string _port;
|
|
|
|
const Protocol _protocol;
|
|
|
|
Request _request;
|
2021-03-23 22:57:53 -05:00
|
|
|
MessageQueue _queue; //< The incoming message queue.
|
|
|
|
std::condition_variable _cv;
|
|
|
|
std::mutex _mutex; //< The queue lock.
|
2021-03-23 21:12:55 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
} // namespace http
|
|
|
|
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|