Compare commits
14 Commits
feature/vc
...
feature/ht
Author | SHA1 | Date | |
---|---|---|---|
fcfb7c1739 | |||
92c2a7756d | |||
f458cd297e | |||
19a959dc31 | |||
cfc1b8f1c2 | |||
22b8fc831c | |||
15fa328a65 | |||
74cc6b815a | |||
44f37a4140 | |||
43deaba547 | |||
2d02ae0f0c | |||
a8879da4fc | |||
5f4a430845 | |||
b9231be305 |
16
.travis.yml
16
.travis.yml
@ -31,11 +31,11 @@ matrix:
|
|||||||
# - CXX=clang++
|
# - CXX=clang++
|
||||||
|
|
||||||
# Windows
|
# Windows
|
||||||
- os: windows
|
# - os: windows
|
||||||
env:
|
# env:
|
||||||
- CMAKE_PATH="/c/Program Files/CMake/bin"
|
# - CMAKE_PATH="/c/Program Files/CMake/bin"
|
||||||
script:
|
# script:
|
||||||
- export PATH=$CMAKE_PATH:$PATH
|
# - export PATH=$CMAKE_PATH:$PATH
|
||||||
- cmake -DUSE_TLS=1 -DUSE_WS=1 -DUSE_MBED_TLS=1 -DUSE_VENDORED_THIRD_PARTY=1 .
|
# # - cmake -DUSE_TLS=1 -DUSE_WS=1 -DUSE_MBED_TLS=1 -DUSE_VENDORED_THIRD_PARTY=1 .
|
||||||
- cmake --build --parallel .
|
# # - cmake --build --parallel .
|
||||||
- python test/run.py
|
# - python test/run.py
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [unreleased] - 2019-06-09
|
## [5.0.0] - 2019-06-23
|
||||||
### Changed
|
### Changed
|
||||||
|
- New HTTP server / still very early. ws gained a new command, httpd can run a simple webserver serving local files.
|
||||||
|
- IXDNSLookup. Uses weak pointer + smart_ptr + shared_from_this instead of static sets + mutex to handle object going away before dns lookup has resolved
|
||||||
|
- cobra_to_sentry / backtraces are reversed and line number is not extracted correctly
|
||||||
- mbedtls and zlib are searched with find_package, and we use the vendored version if nothing is found
|
- mbedtls and zlib are searched with find_package, and we use the vendored version if nothing is found
|
||||||
- travis CI uses g++ on Linux
|
- travis CI uses g++ on Linux
|
||||||
|
|
||||||
|
@ -25,7 +25,9 @@ set( IXWEBSOCKET_SOURCES
|
|||||||
ixwebsocket/IXCancellationRequest.cpp
|
ixwebsocket/IXCancellationRequest.cpp
|
||||||
ixwebsocket/IXConnectionState.cpp
|
ixwebsocket/IXConnectionState.cpp
|
||||||
ixwebsocket/IXDNSLookup.cpp
|
ixwebsocket/IXDNSLookup.cpp
|
||||||
|
ixwebsocket/IXHttp.cpp
|
||||||
ixwebsocket/IXHttpClient.cpp
|
ixwebsocket/IXHttpClient.cpp
|
||||||
|
ixwebsocket/IXHttpServer.cpp
|
||||||
ixwebsocket/IXNetSystem.cpp
|
ixwebsocket/IXNetSystem.cpp
|
||||||
ixwebsocket/IXSelectInterrupt.cpp
|
ixwebsocket/IXSelectInterrupt.cpp
|
||||||
ixwebsocket/IXSelectInterruptFactory.cpp
|
ixwebsocket/IXSelectInterruptFactory.cpp
|
||||||
@ -51,7 +53,9 @@ set( IXWEBSOCKET_HEADERS
|
|||||||
ixwebsocket/IXCancellationRequest.h
|
ixwebsocket/IXCancellationRequest.h
|
||||||
ixwebsocket/IXConnectionState.h
|
ixwebsocket/IXConnectionState.h
|
||||||
ixwebsocket/IXDNSLookup.h
|
ixwebsocket/IXDNSLookup.h
|
||||||
|
ixwebsocket/IXHttp.h
|
||||||
ixwebsocket/IXHttpClient.h
|
ixwebsocket/IXHttpClient.h
|
||||||
|
ixwebsocket/IXHttpServer.h
|
||||||
ixwebsocket/IXNetSystem.h
|
ixwebsocket/IXNetSystem.h
|
||||||
ixwebsocket/IXProgressCallback.h
|
ixwebsocket/IXProgressCallback.h
|
||||||
ixwebsocket/IXSelectInterrupt.h
|
ixwebsocket/IXSelectInterrupt.h
|
||||||
@ -134,6 +138,11 @@ if (APPLE AND USE_TLS AND NOT USE_MBED_TLS)
|
|||||||
target_link_libraries(ixwebsocket "-framework foundation" "-framework security")
|
target_link_libraries(ixwebsocket "-framework foundation" "-framework security")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (UNIX)
|
||||||
|
find_package(Threads)
|
||||||
|
target_link_libraries(ixwebsocket ${CMAKE_THREAD_LIBS_INIT})
|
||||||
|
endif()
|
||||||
|
|
||||||
if (USE_OPEN_SSL)
|
if (USE_OPEN_SSL)
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
add_definitions(${OPENSSL_DEFINITIONS})
|
add_definitions(${OPENSSL_DEFINITIONS})
|
||||||
|
@ -1 +1 @@
|
|||||||
4.0.0
|
4.0.4
|
||||||
|
42
README.md
42
README.md
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex and bi-directionnal communication channels over a single TCP connection. *IXWebSocket* is a C++ library for client and server Websocket communication, and for client HTTP communication. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient) and from the [Satori C SDK](https://github.com/satori-com/satori-rtm-sdk-c). It has been tested on the following platforms.
|
[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex and bi-directionnal communication channels over a single TCP connection. *IXWebSocket* is a C++ library for client and server Websocket communication, and for client and server HTTP communication. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient) and from the [Satori C SDK](https://github.com/satori-com/satori-rtm-sdk-c). It has been tested on the following platforms.
|
||||||
|
|
||||||
* macOS
|
* macOS
|
||||||
* iOS
|
* iOS
|
||||||
@ -117,7 +117,7 @@ server.wait();
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is what the HTTP client API looks like. Note that HTTP client support is very recent and subject to changes.
|
Here is what the HTTP client API looks like.
|
||||||
|
|
||||||
```
|
```
|
||||||
//
|
//
|
||||||
@ -196,6 +196,44 @@ bool ok = httpClient.performRequest(args, [](const HttpResponsePtr& response)
|
|||||||
// ok will be false if your httpClient is not async
|
// ok will be false if your httpClient is not async
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Here is what the HTTP server API looks like. Note that HTTP server support is very, very recent and subject to changes.
|
||||||
|
|
||||||
|
```
|
||||||
|
ix::HttpServer server(port, hostname);
|
||||||
|
|
||||||
|
auto res = server.listen();
|
||||||
|
if (!res.first)
|
||||||
|
{
|
||||||
|
std::cerr << res.second << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
server.wait();
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to handle how requests are processed, implement the setOnConnectionCallback callback, which takes an HttpRequestPtr as input, and returns an HttpResponsePtr. You can look at HttpServer::setDefaultConnectionCallback for a slightly more advanced callback example.
|
||||||
|
|
||||||
|
```
|
||||||
|
setOnConnectionCallback(
|
||||||
|
[this](HttpRequestPtr request,
|
||||||
|
std::shared_ptr<ConnectionState> /*connectionState*/) -> HttpResponsePtr
|
||||||
|
{
|
||||||
|
// Build a string for the response
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << request->method
|
||||||
|
<< " "
|
||||||
|
<< request->uri;
|
||||||
|
|
||||||
|
std::string content = ss.str();
|
||||||
|
|
||||||
|
return std::make_shared<HttpResponse>(200, "OK",
|
||||||
|
HttpErrorCode::Ok,
|
||||||
|
WebSocketHttpHeaders(),
|
||||||
|
content);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
CMakefiles for the library and the examples are available. This library has few dependencies, so it is possible to just add the source files into your project. Otherwise the usual way will suffice.
|
CMakefiles for the library and the examples are available. This library has few dependencies, so it is possible to just add the source files into your project. Otherwise the usual way will suffice.
|
||||||
|
@ -14,27 +14,15 @@ namespace ix
|
|||||||
{
|
{
|
||||||
const int64_t DNSLookup::kDefaultWait = 10; // ms
|
const int64_t DNSLookup::kDefaultWait = 10; // ms
|
||||||
|
|
||||||
std::atomic<uint64_t> DNSLookup::_nextId(0);
|
|
||||||
std::set<uint64_t> DNSLookup::_activeJobs;
|
|
||||||
std::mutex DNSLookup::_activeJobsMutex;
|
|
||||||
|
|
||||||
DNSLookup::DNSLookup(const std::string& hostname, int port, int64_t wait) :
|
DNSLookup::DNSLookup(const std::string& hostname, int port, int64_t wait) :
|
||||||
_port(port),
|
_port(port),
|
||||||
_wait(wait),
|
_wait(wait),
|
||||||
_res(nullptr),
|
_res(nullptr),
|
||||||
_done(false),
|
_done(false)
|
||||||
_id(_nextId++)
|
|
||||||
{
|
{
|
||||||
setHostname(hostname);
|
setHostname(hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
DNSLookup::~DNSLookup()
|
|
||||||
{
|
|
||||||
// Remove this job from the active jobs list
|
|
||||||
std::lock_guard<std::mutex> lock(_activeJobsMutex);
|
|
||||||
_activeJobs.erase(_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct addrinfo* DNSLookup::getAddrInfo(const std::string& hostname,
|
struct addrinfo* DNSLookup::getAddrInfo(const std::string& hostname,
|
||||||
int port,
|
int port,
|
||||||
std::string& errMsg)
|
std::string& errMsg)
|
||||||
@ -94,17 +82,14 @@ namespace ix
|
|||||||
// if you need a second lookup.
|
// if you need a second lookup.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record job in the active Job set
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(_activeJobsMutex);
|
|
||||||
_activeJobs.insert(_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Good resource on thread forced termination
|
// Good resource on thread forced termination
|
||||||
// https://www.bo-yang.net/2017/11/19/cpp-kill-detached-thread
|
// https://www.bo-yang.net/2017/11/19/cpp-kill-detached-thread
|
||||||
//
|
//
|
||||||
_thread = std::thread(&DNSLookup::run, this, _id, getHostname(), _port);
|
auto ptr = shared_from_this();
|
||||||
|
std::weak_ptr<DNSLookup> self(ptr);
|
||||||
|
|
||||||
|
_thread = std::thread(&DNSLookup::run, this, self, getHostname(), _port);
|
||||||
_thread.detach();
|
_thread.detach();
|
||||||
|
|
||||||
std::unique_lock<std::mutex> lock(_conditionVariableMutex);
|
std::unique_lock<std::mutex> lock(_conditionVariableMutex);
|
||||||
@ -138,7 +123,7 @@ namespace ix
|
|||||||
return getRes();
|
return getRes();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DNSLookup::run(uint64_t id, const std::string& hostname, int port) // thread runner
|
void DNSLookup::run(std::weak_ptr<DNSLookup> self, const std::string& hostname, int port) // thread runner
|
||||||
{
|
{
|
||||||
// We don't want to read or write into members variables of an object that could be
|
// We don't want to read or write into members variables of an object that could be
|
||||||
// gone, so we use temporary variables (res) or we pass in by copy everything that
|
// gone, so we use temporary variables (res) or we pass in by copy everything that
|
||||||
@ -146,21 +131,15 @@ namespace ix
|
|||||||
std::string errMsg;
|
std::string errMsg;
|
||||||
struct addrinfo* res = getAddrInfo(hostname, port, errMsg);
|
struct addrinfo* res = getAddrInfo(hostname, port, errMsg);
|
||||||
|
|
||||||
// if this isn't an active job, and the control thread is gone
|
if (self.lock())
|
||||||
// there is nothing to do, and we don't want to touch the defunct
|
|
||||||
// object data structure such as _errMsg or _condition
|
|
||||||
std::lock_guard<std::mutex> lock(_activeJobsMutex);
|
|
||||||
if (_activeJobs.count(id) == 0)
|
|
||||||
{
|
{
|
||||||
return;
|
// Copy result into the member variables
|
||||||
|
setRes(res);
|
||||||
|
setErrMsg(errMsg);
|
||||||
|
|
||||||
|
_condition.notify_one();
|
||||||
|
_done = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy result into the member variables
|
|
||||||
setRes(res);
|
|
||||||
setErrMsg(errMsg);
|
|
||||||
|
|
||||||
_condition.notify_one();
|
|
||||||
_done = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DNSLookup::setHostname(const std::string& hostname)
|
void DNSLookup::setHostname(const std::string& hostname)
|
||||||
|
@ -16,16 +16,17 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
struct addrinfo;
|
struct addrinfo;
|
||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
class DNSLookup
|
class DNSLookup : public std::enable_shared_from_this<DNSLookup>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DNSLookup(const std::string& hostname, int port, int64_t wait = DNSLookup::kDefaultWait);
|
DNSLookup(const std::string& hostname, int port, int64_t wait = DNSLookup::kDefaultWait);
|
||||||
~DNSLookup();
|
~DNSLookup() = default;
|
||||||
|
|
||||||
struct addrinfo* resolve(std::string& errMsg,
|
struct addrinfo* resolve(std::string& errMsg,
|
||||||
const CancellationRequest& isCancellationRequested,
|
const CancellationRequest& isCancellationRequested,
|
||||||
@ -41,7 +42,7 @@ namespace ix
|
|||||||
int port,
|
int port,
|
||||||
std::string& errMsg);
|
std::string& errMsg);
|
||||||
|
|
||||||
void run(uint64_t id, const std::string& hostname, int port); // thread runner
|
void run(std::weak_ptr<DNSLookup> self, const std::string& hostname, int port); // thread runner
|
||||||
|
|
||||||
void setHostname(const std::string& hostname);
|
void setHostname(const std::string& hostname);
|
||||||
const std::string& getHostname();
|
const std::string& getHostname();
|
||||||
@ -69,11 +70,6 @@ namespace ix
|
|||||||
std::condition_variable _condition;
|
std::condition_variable _condition;
|
||||||
std::mutex _conditionVariableMutex;
|
std::mutex _conditionVariableMutex;
|
||||||
|
|
||||||
uint64_t _id;
|
|
||||||
static std::atomic<uint64_t> _nextId;
|
|
||||||
static std::set<uint64_t> _activeJobs;
|
|
||||||
static std::mutex _activeJobsMutex;
|
|
||||||
|
|
||||||
const static int64_t kDefaultWait;
|
const static int64_t kDefaultWait;
|
||||||
};
|
};
|
||||||
} // namespace ix
|
} // namespace ix
|
||||||
|
138
ixwebsocket/IXHttp.cpp
Normal file
138
ixwebsocket/IXHttp.cpp
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* IXHttp.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXHttp.h"
|
||||||
|
#include "IXCancellationRequest.h"
|
||||||
|
#include "IXSocket.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
std::string Http::trim(const std::string& str)
|
||||||
|
{
|
||||||
|
std::string out;
|
||||||
|
for (auto c : str)
|
||||||
|
{
|
||||||
|
if (c != ' ' && c != '\n' && c != '\r')
|
||||||
|
{
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<std::string, std::string, std::string> Http::parseRequestLine(const std::string& line)
|
||||||
|
{
|
||||||
|
// Request-Line = Method SP Request-URI SP HTTP-Version CRLF
|
||||||
|
std::string token;
|
||||||
|
std::stringstream tokenStream(line);
|
||||||
|
std::vector<std::string> tokens;
|
||||||
|
|
||||||
|
// Split by ' '
|
||||||
|
while (std::getline(tokenStream, token, ' '))
|
||||||
|
{
|
||||||
|
tokens.push_back(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string method;
|
||||||
|
if (tokens.size() >= 1)
|
||||||
|
{
|
||||||
|
method = trim(tokens[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string requestUri;
|
||||||
|
if (tokens.size() >= 2)
|
||||||
|
{
|
||||||
|
requestUri = trim(tokens[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string httpVersion;
|
||||||
|
if (tokens.size() >= 3)
|
||||||
|
{
|
||||||
|
httpVersion = trim(tokens[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_tuple(method, requestUri, httpVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<bool, std::string, HttpRequestPtr> Http::parseRequest(std::shared_ptr<Socket> socket)
|
||||||
|
{
|
||||||
|
HttpRequestPtr httpRequest;
|
||||||
|
|
||||||
|
std::atomic<bool> requestInitCancellation(false);
|
||||||
|
|
||||||
|
int timeoutSecs = 5; // FIXME
|
||||||
|
|
||||||
|
auto isCancellationRequested =
|
||||||
|
makeCancellationRequestWithTimeout(timeoutSecs, requestInitCancellation);
|
||||||
|
|
||||||
|
// Read first line
|
||||||
|
auto lineResult = socket->readLine(isCancellationRequested);
|
||||||
|
auto lineValid = lineResult.first;
|
||||||
|
auto line = lineResult.second;
|
||||||
|
|
||||||
|
if (!lineValid)
|
||||||
|
{
|
||||||
|
return std::make_tuple(false, "Error reading HTTP request line", httpRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request line (GET /foo HTTP/1.1\r\n)
|
||||||
|
auto requestLine = Http::parseRequestLine(line);
|
||||||
|
auto method = std::get<0>(requestLine);
|
||||||
|
auto uri = std::get<1>(requestLine);
|
||||||
|
auto httpVersion = std::get<2>(requestLine);
|
||||||
|
|
||||||
|
// Retrieve and validate HTTP headers
|
||||||
|
auto result = parseHttpHeaders(socket, isCancellationRequested);
|
||||||
|
auto headersValid = result.first;
|
||||||
|
auto headers = result.second;
|
||||||
|
|
||||||
|
if (!headersValid)
|
||||||
|
{
|
||||||
|
return std::make_tuple(false, "Error parsing HTTP headers", httpRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRequest = std::make_shared<HttpRequest>(uri, method, httpVersion, headers);
|
||||||
|
return std::make_tuple(true, "", httpRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Http::sendResponse(HttpResponsePtr response, std::shared_ptr<Socket> socket)
|
||||||
|
{
|
||||||
|
// Write the response to the socket
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "HTTP/1.1 ";
|
||||||
|
ss << response->statusCode;
|
||||||
|
ss << " ";
|
||||||
|
ss << response->description;
|
||||||
|
ss << "\r\n";
|
||||||
|
|
||||||
|
if (!socket->writeBytes(ss.str(), nullptr))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write headers
|
||||||
|
ss.str("");
|
||||||
|
ss << "Content-Length: " << response->payload.size() << "\r\n";
|
||||||
|
for (auto&& it : response->headers)
|
||||||
|
{
|
||||||
|
ss << it.first << ": " << it.second << "\r\n";
|
||||||
|
}
|
||||||
|
ss << "\r\n";
|
||||||
|
|
||||||
|
if (!socket->writeBytes(ss.str(), nullptr))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response->payload.empty()
|
||||||
|
? true
|
||||||
|
: socket->writeBytes(response->payload, nullptr);
|
||||||
|
}
|
||||||
|
}
|
120
ixwebsocket/IXHttp.h
Normal file
120
ixwebsocket/IXHttp.h
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* IXHttp.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXWebSocketHttpHeaders.h"
|
||||||
|
#include "IXProgressCallback.h"
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
enum class HttpErrorCode : int
|
||||||
|
{
|
||||||
|
Ok = 0,
|
||||||
|
CannotConnect = 1,
|
||||||
|
Timeout = 2,
|
||||||
|
Gzip = 3,
|
||||||
|
UrlMalformed = 4,
|
||||||
|
CannotCreateSocket = 5,
|
||||||
|
SendError = 6,
|
||||||
|
ReadError = 7,
|
||||||
|
CannotReadStatusLine = 8,
|
||||||
|
MissingStatus = 9,
|
||||||
|
HeaderParsingError = 10,
|
||||||
|
MissingLocation = 11,
|
||||||
|
TooManyRedirects = 12,
|
||||||
|
ChunkReadError = 13,
|
||||||
|
CannotReadBody = 14,
|
||||||
|
Invalid = 100
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HttpResponse
|
||||||
|
{
|
||||||
|
int statusCode;
|
||||||
|
std::string description;
|
||||||
|
HttpErrorCode errorCode;
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
std::string payload;
|
||||||
|
std::string errorMsg;
|
||||||
|
uint64_t uploadSize;
|
||||||
|
uint64_t downloadSize;
|
||||||
|
|
||||||
|
HttpResponse(int s = 0,
|
||||||
|
const std::string& des = std::string(),
|
||||||
|
const HttpErrorCode& c = HttpErrorCode::Ok,
|
||||||
|
const WebSocketHttpHeaders& h = WebSocketHttpHeaders(),
|
||||||
|
const std::string& p = std::string(),
|
||||||
|
const std::string& e = std::string(),
|
||||||
|
uint64_t u = 0,
|
||||||
|
uint64_t d = 0)
|
||||||
|
: statusCode(s)
|
||||||
|
, description(des)
|
||||||
|
, errorCode(c)
|
||||||
|
, headers(h)
|
||||||
|
, payload(p)
|
||||||
|
, errorMsg(e)
|
||||||
|
, uploadSize(u)
|
||||||
|
, downloadSize(d)
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using HttpResponsePtr = std::shared_ptr<HttpResponse>;
|
||||||
|
using HttpParameters = std::map<std::string, std::string>;
|
||||||
|
using Logger = std::function<void(const std::string&)>;
|
||||||
|
using OnResponseCallback = std::function<void(const HttpResponsePtr&)>;
|
||||||
|
|
||||||
|
struct HttpRequestArgs
|
||||||
|
{
|
||||||
|
std::string url;
|
||||||
|
std::string verb;
|
||||||
|
WebSocketHttpHeaders extraHeaders;
|
||||||
|
std::string body;
|
||||||
|
int connectTimeout;
|
||||||
|
int transferTimeout;
|
||||||
|
bool followRedirects;
|
||||||
|
int maxRedirects;
|
||||||
|
bool verbose;
|
||||||
|
bool compress;
|
||||||
|
Logger logger;
|
||||||
|
OnProgressCallback onProgressCallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
using HttpRequestArgsPtr = std::shared_ptr<HttpRequestArgs>;
|
||||||
|
|
||||||
|
struct HttpRequest
|
||||||
|
{
|
||||||
|
std::string uri;
|
||||||
|
std::string method;
|
||||||
|
std::string version;
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
|
||||||
|
HttpRequest(const std::string& u,
|
||||||
|
const std::string& m,
|
||||||
|
const std::string& v,
|
||||||
|
const WebSocketHttpHeaders& h = WebSocketHttpHeaders())
|
||||||
|
: uri(u)
|
||||||
|
, method(m)
|
||||||
|
, version(v)
|
||||||
|
, headers(h)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using HttpRequestPtr = std::shared_ptr<HttpRequest>;
|
||||||
|
|
||||||
|
class Http
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static std::tuple<bool, std::string, HttpRequestPtr> parseRequest(std::shared_ptr<Socket> socket);
|
||||||
|
static bool sendResponse(HttpResponsePtr response, std::shared_ptr<Socket> socket);
|
||||||
|
|
||||||
|
static std::tuple<std::string, std::string, std::string> parseRequestLine(const std::string& line);
|
||||||
|
static std::string trim(const std::string& str);
|
||||||
|
};
|
||||||
|
}
|
@ -118,6 +118,7 @@ namespace ix
|
|||||||
int code = 0;
|
int code = 0;
|
||||||
WebSocketHttpHeaders headers;
|
WebSocketHttpHeaders headers;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
|
std::string description;
|
||||||
|
|
||||||
std::string protocol, host, path, query;
|
std::string protocol, host, path, query;
|
||||||
int port;
|
int port;
|
||||||
@ -126,9 +127,9 @@ namespace ix
|
|||||||
{
|
{
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << "Cannot parse url: " << url;
|
ss << "Cannot parse url: " << url;
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::UrlMalformed,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::UrlMalformed,
|
||||||
headers, payload, ss.str(),
|
headers, payload, ss.str(),
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool tls = protocol == "https";
|
bool tls = protocol == "https";
|
||||||
@ -137,9 +138,9 @@ namespace ix
|
|||||||
|
|
||||||
if (!_socket)
|
if (!_socket)
|
||||||
{
|
{
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::CannotCreateSocket,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::CannotCreateSocket,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build request string
|
// Build request string
|
||||||
@ -200,7 +201,7 @@ namespace ix
|
|||||||
{
|
{
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << "Cannot connect to url: " << url << " / error : " << errMsg;
|
ss << "Cannot connect to url: " << url << " / error : " << errMsg;
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::CannotConnect,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::CannotConnect,
|
||||||
headers, payload, ss.str(),
|
headers, payload, ss.str(),
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -226,7 +227,7 @@ namespace ix
|
|||||||
if (!_socket->writeBytes(req, isCancellationRequested))
|
if (!_socket->writeBytes(req, isCancellationRequested))
|
||||||
{
|
{
|
||||||
std::string errorMsg("Cannot send request");
|
std::string errorMsg("Cannot send request");
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::SendError,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::SendError,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -240,7 +241,7 @@ namespace ix
|
|||||||
if (!lineValid)
|
if (!lineValid)
|
||||||
{
|
{
|
||||||
std::string errorMsg("Cannot retrieve status line");
|
std::string errorMsg("Cannot retrieve status line");
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::CannotReadStatusLine,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::CannotReadStatusLine,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -255,7 +256,7 @@ namespace ix
|
|||||||
if (sscanf(line.c_str(), "HTTP/1.1 %d", &code) != 1)
|
if (sscanf(line.c_str(), "HTTP/1.1 %d", &code) != 1)
|
||||||
{
|
{
|
||||||
std::string errorMsg("Cannot parse response code from status line");
|
std::string errorMsg("Cannot parse response code from status line");
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::MissingStatus,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::MissingStatus,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -267,7 +268,7 @@ namespace ix
|
|||||||
if (!headersValid)
|
if (!headersValid)
|
||||||
{
|
{
|
||||||
std::string errorMsg("Cannot parse http headers");
|
std::string errorMsg("Cannot parse http headers");
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::HeaderParsingError,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::HeaderParsingError,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -278,7 +279,7 @@ namespace ix
|
|||||||
if (headers.find("Location") == headers.end())
|
if (headers.find("Location") == headers.end())
|
||||||
{
|
{
|
||||||
std::string errorMsg("Missing location header for redirect");
|
std::string errorMsg("Missing location header for redirect");
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::MissingLocation,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::MissingLocation,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -287,7 +288,7 @@ namespace ix
|
|||||||
{
|
{
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << "Too many redirects: " << redirects;
|
ss << "Too many redirects: " << redirects;
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::TooManyRedirects,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::TooManyRedirects,
|
||||||
headers, payload, ss.str(),
|
headers, payload, ss.str(),
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -299,7 +300,7 @@ namespace ix
|
|||||||
|
|
||||||
if (verb == "HEAD")
|
if (verb == "HEAD")
|
||||||
{
|
{
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::Ok,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::Ok,
|
||||||
headers, payload, std::string(),
|
headers, payload, std::string(),
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -320,7 +321,7 @@ namespace ix
|
|||||||
if (!chunkResult.first)
|
if (!chunkResult.first)
|
||||||
{
|
{
|
||||||
errorMsg = "Cannot read chunk";
|
errorMsg = "Cannot read chunk";
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::ChunkReadError,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::ChunkReadError,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -338,7 +339,7 @@ namespace ix
|
|||||||
|
|
||||||
if (!lineResult.first)
|
if (!lineResult.first)
|
||||||
{
|
{
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::ChunkReadError,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::ChunkReadError,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -365,7 +366,7 @@ namespace ix
|
|||||||
if (!chunkResult.first)
|
if (!chunkResult.first)
|
||||||
{
|
{
|
||||||
errorMsg = "Cannot read chunk";
|
errorMsg = "Cannot read chunk";
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::ChunkReadError,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::ChunkReadError,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -376,7 +377,7 @@ namespace ix
|
|||||||
|
|
||||||
if (!lineResult.first)
|
if (!lineResult.first)
|
||||||
{
|
{
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::ChunkReadError,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::ChunkReadError,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -391,7 +392,7 @@ namespace ix
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
std::string errorMsg("Cannot read http body");
|
std::string errorMsg("Cannot read http body");
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::CannotReadBody,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::CannotReadBody,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
@ -405,14 +406,14 @@ namespace ix
|
|||||||
if (!gzipInflate(payload, decompressedPayload))
|
if (!gzipInflate(payload, decompressedPayload))
|
||||||
{
|
{
|
||||||
std::string errorMsg("Error decompressing payload");
|
std::string errorMsg("Error decompressing payload");
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::Gzip,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::Gzip,
|
||||||
headers, payload, errorMsg,
|
headers, payload, errorMsg,
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
payload = decompressedPayload;
|
payload = decompressedPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::make_shared<HttpResponse>(code, HttpErrorCode::Ok,
|
return std::make_shared<HttpResponse>(code, description, HttpErrorCode::Ok,
|
||||||
headers, payload, std::string(),
|
headers, payload, std::string(),
|
||||||
uploadSize, downloadSize);
|
uploadSize, downloadSize);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "IXSocket.h"
|
#include "IXSocket.h"
|
||||||
#include "IXWebSocketHttpHeaders.h"
|
#include "IXWebSocketHttpHeaders.h"
|
||||||
|
#include "IXHttp.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
@ -20,78 +21,6 @@
|
|||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
enum class HttpErrorCode : int
|
|
||||||
{
|
|
||||||
Ok = 0,
|
|
||||||
CannotConnect = 1,
|
|
||||||
Timeout = 2,
|
|
||||||
Gzip = 3,
|
|
||||||
UrlMalformed = 4,
|
|
||||||
CannotCreateSocket = 5,
|
|
||||||
SendError = 6,
|
|
||||||
ReadError = 7,
|
|
||||||
CannotReadStatusLine = 8,
|
|
||||||
MissingStatus = 9,
|
|
||||||
HeaderParsingError = 10,
|
|
||||||
MissingLocation = 11,
|
|
||||||
TooManyRedirects = 12,
|
|
||||||
ChunkReadError = 13,
|
|
||||||
CannotReadBody = 14,
|
|
||||||
Invalid = 100
|
|
||||||
};
|
|
||||||
|
|
||||||
struct HttpResponse
|
|
||||||
{
|
|
||||||
int statusCode;
|
|
||||||
HttpErrorCode errorCode;
|
|
||||||
WebSocketHttpHeaders headers;
|
|
||||||
std::string payload;
|
|
||||||
std::string errorMsg;
|
|
||||||
uint64_t uploadSize;
|
|
||||||
uint64_t downloadSize;
|
|
||||||
|
|
||||||
HttpResponse(int s = 0,
|
|
||||||
const HttpErrorCode& c = HttpErrorCode::Ok,
|
|
||||||
const WebSocketHttpHeaders& h = WebSocketHttpHeaders(),
|
|
||||||
const std::string& p = std::string(),
|
|
||||||
const std::string& e = std::string(),
|
|
||||||
uint64_t u = 0,
|
|
||||||
uint64_t d = 0)
|
|
||||||
: statusCode(s)
|
|
||||||
, errorCode(c)
|
|
||||||
, headers(h)
|
|
||||||
, payload(p)
|
|
||||||
, errorMsg(e)
|
|
||||||
, uploadSize(u)
|
|
||||||
, downloadSize(d)
|
|
||||||
{
|
|
||||||
;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
using HttpResponsePtr = std::shared_ptr<HttpResponse>;
|
|
||||||
using HttpParameters = std::map<std::string, std::string>;
|
|
||||||
using Logger = std::function<void(const std::string&)>;
|
|
||||||
using OnResponseCallback = std::function<void(const HttpResponsePtr&)>;
|
|
||||||
|
|
||||||
struct HttpRequestArgs
|
|
||||||
{
|
|
||||||
std::string url;
|
|
||||||
std::string verb;
|
|
||||||
WebSocketHttpHeaders extraHeaders;
|
|
||||||
std::string body;
|
|
||||||
int connectTimeout;
|
|
||||||
int transferTimeout;
|
|
||||||
bool followRedirects;
|
|
||||||
int maxRedirects;
|
|
||||||
bool verbose;
|
|
||||||
bool compress;
|
|
||||||
Logger logger;
|
|
||||||
OnProgressCallback onProgressCallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
using HttpRequestArgsPtr = std::shared_ptr<HttpRequestArgs>;
|
|
||||||
|
|
||||||
class HttpClient
|
class HttpClient
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
158
ixwebsocket/IXHttpServer.cpp
Normal file
158
ixwebsocket/IXHttpServer.cpp
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* IXHttpServer.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXHttpServer.h"
|
||||||
|
#include "IXSocketConnect.h"
|
||||||
|
#include "IXSocketFactory.h"
|
||||||
|
#include "IXNetSystem.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::pair<bool, std::vector<uint8_t>> load(const std::string& path)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> memblock;
|
||||||
|
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (!file.is_open()) return std::make_pair(false, memblock);
|
||||||
|
|
||||||
|
file.seekg(0, file.end);
|
||||||
|
std::streamoff size = file.tellg();
|
||||||
|
file.seekg(0, file.beg);
|
||||||
|
|
||||||
|
memblock.resize((size_t) size);
|
||||||
|
file.read((char*)&memblock.front(), static_cast<std::streamsize>(size));
|
||||||
|
|
||||||
|
return std::make_pair(true, memblock);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> readAsString(const std::string& path)
|
||||||
|
{
|
||||||
|
auto res = load(path);
|
||||||
|
auto vec = res.second;
|
||||||
|
return std::make_pair(res.first, std::string(vec.begin(), vec.end()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
HttpServer::HttpServer(int port,
|
||||||
|
const std::string& host,
|
||||||
|
int backlog,
|
||||||
|
size_t maxConnections) : SocketServer(port, host, backlog, maxConnections),
|
||||||
|
_connectedClientsCount(0)
|
||||||
|
{
|
||||||
|
setDefaultConnectionCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServer::~HttpServer()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpServer::stop()
|
||||||
|
{
|
||||||
|
stopAcceptingConnections();
|
||||||
|
|
||||||
|
// FIXME: cancelling / closing active clients ...
|
||||||
|
|
||||||
|
SocketServer::stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpServer::setOnConnectionCallback(const OnConnectionCallback& callback)
|
||||||
|
{
|
||||||
|
_onConnectionCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpServer::handleConnection(
|
||||||
|
int fd,
|
||||||
|
std::shared_ptr<ConnectionState> connectionState)
|
||||||
|
{
|
||||||
|
_connectedClientsCount++;
|
||||||
|
|
||||||
|
std::string errorMsg;
|
||||||
|
auto socket = createSocket(fd, errorMsg);
|
||||||
|
|
||||||
|
// Set the socket to non blocking mode + other tweaks
|
||||||
|
SocketConnect::configure(fd);
|
||||||
|
|
||||||
|
auto ret = Http::parseRequest(socket);
|
||||||
|
// FIXME: handle errors in parseRequest
|
||||||
|
|
||||||
|
if (std::get<0>(ret))
|
||||||
|
{
|
||||||
|
auto response = _onConnectionCallback(std::get<2>(ret), connectionState);
|
||||||
|
if (!Http::sendResponse(response, socket))
|
||||||
|
{
|
||||||
|
logError("Cannot send response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectionState->setTerminated();
|
||||||
|
|
||||||
|
_connectedClientsCount--;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t HttpServer::getConnectedClientsCount()
|
||||||
|
{
|
||||||
|
return _connectedClientsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpServer::setDefaultConnectionCallback()
|
||||||
|
{
|
||||||
|
setOnConnectionCallback(
|
||||||
|
[this](HttpRequestPtr request,
|
||||||
|
std::shared_ptr<ConnectionState> /*connectionState*/) -> HttpResponsePtr
|
||||||
|
{
|
||||||
|
std::string uri(request->uri);
|
||||||
|
if (uri.empty() || uri == "/")
|
||||||
|
{
|
||||||
|
uri = "/index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string path("." + uri);
|
||||||
|
auto res = readAsString(path);
|
||||||
|
bool found = res.first;
|
||||||
|
if (!found)
|
||||||
|
{
|
||||||
|
return std::make_shared<HttpResponse>(404, "Not Found",
|
||||||
|
HttpErrorCode::Ok,
|
||||||
|
WebSocketHttpHeaders(),
|
||||||
|
std::string());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content = res.second;
|
||||||
|
|
||||||
|
// Log request
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << request->method
|
||||||
|
<< " "
|
||||||
|
<< request->uri
|
||||||
|
<< " "
|
||||||
|
<< content.size();
|
||||||
|
logInfo(ss.str());
|
||||||
|
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
// FIXME: check extensions to set the content type
|
||||||
|
// headers["Content-Type"] = "application/octet-stream";
|
||||||
|
headers["Accept-Ranges"] = "none";
|
||||||
|
|
||||||
|
for (auto&& it : request->headers)
|
||||||
|
{
|
||||||
|
headers[it.first] = it.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_shared<HttpResponse>(200, "OK",
|
||||||
|
HttpErrorCode::Ok,
|
||||||
|
headers,
|
||||||
|
content);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
50
ixwebsocket/IXHttpServer.h
Normal file
50
ixwebsocket/IXHttpServer.h
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* IXHttpServer.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IXSocketServer.h"
|
||||||
|
#include "IXWebSocket.h"
|
||||||
|
#include "IXHttp.h"
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility> // pair
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
class HttpServer final : public SocketServer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using OnConnectionCallback =
|
||||||
|
std::function<HttpResponsePtr(HttpRequestPtr, std::shared_ptr<ConnectionState>)>;
|
||||||
|
|
||||||
|
HttpServer(int port = SocketServer::kDefaultPort,
|
||||||
|
const std::string& host = SocketServer::kDefaultHost,
|
||||||
|
int backlog = SocketServer::kDefaultTcpBacklog,
|
||||||
|
size_t maxConnections = SocketServer::kDefaultMaxConnections);
|
||||||
|
virtual ~HttpServer();
|
||||||
|
virtual void stop() final;
|
||||||
|
|
||||||
|
void setOnConnectionCallback(const OnConnectionCallback& callback);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Member variables
|
||||||
|
OnConnectionCallback _onConnectionCallback;
|
||||||
|
std::atomic<int> _connectedClientsCount;
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
virtual void handleConnection(int fd,
|
||||||
|
std::shared_ptr<ConnectionState> connectionState) final;
|
||||||
|
virtual size_t getConnectedClientsCount() final;
|
||||||
|
|
||||||
|
void setDefaultConnectionCallback();
|
||||||
|
};
|
||||||
|
} // namespace ix
|
||||||
|
|
@ -232,19 +232,28 @@ namespace ix
|
|||||||
bool Socket::writeBytes(const std::string& str,
|
bool Socket::writeBytes(const std::string& str,
|
||||||
const CancellationRequest& isCancellationRequested)
|
const CancellationRequest& isCancellationRequested)
|
||||||
{
|
{
|
||||||
|
char* buffer = const_cast<char*>(str.c_str());
|
||||||
|
int len = (int) str.size();
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (isCancellationRequested && isCancellationRequested()) return false;
|
if (isCancellationRequested && isCancellationRequested()) return false;
|
||||||
|
|
||||||
char* buffer = const_cast<char*>(str.c_str());
|
|
||||||
int len = (int) str.size();
|
|
||||||
|
|
||||||
ssize_t ret = send(buffer, len);
|
ssize_t ret = send(buffer, len);
|
||||||
|
|
||||||
// We wrote some bytes, as needed, all good.
|
// We wrote some bytes, as needed, all good.
|
||||||
if (ret > 0)
|
if (ret > 0)
|
||||||
{
|
{
|
||||||
return ret == len;
|
if (ret == len)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buffer += ret;
|
||||||
|
len -= ret;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// There is possibly something to be writen, try again
|
// There is possibly something to be writen, try again
|
||||||
else if (ret < 0 && Socket::isWaitNeeded())
|
else if (ret < 0 && Socket::isWaitNeeded())
|
||||||
|
@ -128,8 +128,8 @@ namespace ix
|
|||||||
//
|
//
|
||||||
// First do DNS resolution
|
// First do DNS resolution
|
||||||
//
|
//
|
||||||
DNSLookup dnsLookup(hostname, port);
|
auto dnsLookup = std::make_shared<DNSLookup>(hostname, port);
|
||||||
struct addrinfo *res = dnsLookup.resolve(errMsg, isCancellationRequested);
|
struct addrinfo *res = dnsLookup->resolve(errMsg, isCancellationRequested);
|
||||||
if (res == nullptr)
|
if (res == nullptr)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <future>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#include "IXWebSocketHandshake.h"
|
#include "IXWebSocketHandshake.h"
|
||||||
#include "IXSocketConnect.h"
|
#include "IXSocketConnect.h"
|
||||||
#include "IXUrlParser.h"
|
#include "IXUrlParser.h"
|
||||||
|
#include "IXHttp.h"
|
||||||
|
|
||||||
#include "libwshandshake.hpp"
|
#include "libwshandshake.hpp"
|
||||||
|
|
||||||
@ -31,15 +32,6 @@ namespace ix
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string WebSocketHandshake::trim(const std::string& str)
|
|
||||||
{
|
|
||||||
std::string out(str);
|
|
||||||
out.erase(std::remove(out.begin(), out.end(), ' '), out.end());
|
|
||||||
out.erase(std::remove(out.begin(), out.end(), '\r'), out.end());
|
|
||||||
out.erase(std::remove(out.begin(), out.end(), '\n'), out.end());
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WebSocketHandshake::insensitiveStringCompare(const std::string& a, const std::string& b)
|
bool WebSocketHandshake::insensitiveStringCompare(const std::string& a, const std::string& b)
|
||||||
{
|
{
|
||||||
return std::equal(a.begin(), a.end(),
|
return std::equal(a.begin(), a.end(),
|
||||||
@ -50,40 +42,6 @@ namespace ix
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::tuple<std::string, std::string, std::string> WebSocketHandshake::parseRequestLine(const std::string& line)
|
|
||||||
{
|
|
||||||
// Request-Line = Method SP Request-URI SP HTTP-Version CRLF
|
|
||||||
std::string token;
|
|
||||||
std::stringstream tokenStream(line);
|
|
||||||
std::vector<std::string> tokens;
|
|
||||||
|
|
||||||
// Split by ' '
|
|
||||||
while (std::getline(tokenStream, token, ' '))
|
|
||||||
{
|
|
||||||
tokens.push_back(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string method;
|
|
||||||
if (tokens.size() >= 1)
|
|
||||||
{
|
|
||||||
method = trim(tokens[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string requestUri;
|
|
||||||
if (tokens.size() >= 2)
|
|
||||||
{
|
|
||||||
requestUri = trim(tokens[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string httpVersion;
|
|
||||||
if (tokens.size() >= 3)
|
|
||||||
{
|
|
||||||
httpVersion = trim(tokens[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::make_tuple(method, requestUri, httpVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string WebSocketHandshake::genRandomString(const int len)
|
std::string WebSocketHandshake::genRandomString(const int len)
|
||||||
{
|
{
|
||||||
std::string alphanum =
|
std::string alphanum =
|
||||||
@ -294,7 +252,7 @@ namespace ix
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate request line (GET /foo HTTP/1.1\r\n)
|
// Validate request line (GET /foo HTTP/1.1\r\n)
|
||||||
auto requestLine = parseRequestLine(line);
|
auto requestLine = Http::parseRequestLine(line);
|
||||||
auto method = std::get<0>(requestLine);
|
auto method = std::get<0>(requestLine);
|
||||||
auto uri = std::get<1>(requestLine);
|
auto uri = std::get<1>(requestLine);
|
||||||
auto httpVersion = std::get<2>(requestLine);
|
auto httpVersion = std::get<2>(requestLine);
|
||||||
|
@ -64,8 +64,6 @@ namespace ix
|
|||||||
// Parse HTTP headers
|
// Parse HTTP headers
|
||||||
WebSocketInitResult sendErrorResponse(int code, const std::string& reason);
|
WebSocketInitResult sendErrorResponse(int code, const std::string& reason);
|
||||||
|
|
||||||
std::tuple<std::string, std::string, std::string> parseRequestLine(const std::string& line);
|
|
||||||
std::string trim(const std::string& str);
|
|
||||||
bool insensitiveStringCompare(const std::string& a, const std::string& b);
|
bool insensitiveStringCompare(const std::string& a, const std::string& b);
|
||||||
|
|
||||||
std::atomic<bool>& _requestInitCancellation;
|
std::atomic<bool>& _requestInitCancellation;
|
||||||
|
@ -19,12 +19,12 @@
|
|||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
using OnConnectionCallback =
|
|
||||||
std::function<void(std::shared_ptr<WebSocket>, std::shared_ptr<ConnectionState>)>;
|
|
||||||
|
|
||||||
class WebSocketServer final : public SocketServer
|
class WebSocketServer final : public SocketServer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
using OnConnectionCallback =
|
||||||
|
std::function<void(std::shared_ptr<WebSocket>, std::shared_ptr<ConnectionState>)>;
|
||||||
|
|
||||||
WebSocketServer(int port = SocketServer::kDefaultPort,
|
WebSocketServer(int port = SocketServer::kDefaultPort,
|
||||||
const std::string& host = SocketServer::kDefaultHost,
|
const std::string& host = SocketServer::kDefaultHost,
|
||||||
int backlog = SocketServer::kDefaultTcpBacklog,
|
int backlog = SocketServer::kDefaultTcpBacklog,
|
||||||
|
4
makefile
4
makefile
@ -9,10 +9,10 @@ install: brew
|
|||||||
# on osx it is good practice to make /usr/local user writable
|
# on osx it is good practice to make /usr/local user writable
|
||||||
# sudo chown -R `whoami`/staff /usr/local
|
# sudo chown -R `whoami`/staff /usr/local
|
||||||
brew:
|
brew:
|
||||||
mkdir -p build && (cd build ; cmake -DUSE_TLS=1 -DUSE_WS=1 .. ; make -j install)
|
mkdir -p build && (cd build ; cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_TLS=1 -DUSE_WS=1 .. ; make -j install)
|
||||||
|
|
||||||
ws:
|
ws:
|
||||||
mkdir -p build && (cd build ; cmake -DUSE_TLS=1 -DUSE_WS=1 -DUSE_MBED_TLS=1 -DUSE_VENDORED_THIRD_PARTY=1 .. ; make -j)
|
mkdir -p build && (cd build ; cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_TLS=1 -DUSE_WS=1 -DUSE_MBED_TLS=1 -DUSE_VENDORED_THIRD_PARTY=1 .. ; make -j)
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
xargs rm -fv < build/install_manifest.txt
|
xargs rm -fv < build/install_manifest.txt
|
||||||
|
@ -7,7 +7,7 @@ project (ixwebsocket_unittest)
|
|||||||
|
|
||||||
set (CMAKE_CXX_STANDARD 14)
|
set (CMAKE_CXX_STANDARD 14)
|
||||||
|
|
||||||
if (NOT WIN32)
|
if (MAC)
|
||||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../third_party/sanitizers-cmake/cmake" ${CMAKE_MODULE_PATH})
|
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../third_party/sanitizers-cmake/cmake" ${CMAKE_MODULE_PATH})
|
||||||
find_package(Sanitizers)
|
find_package(Sanitizers)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
|
||||||
@ -28,6 +28,7 @@ include_directories(
|
|||||||
set (SOURCES
|
set (SOURCES
|
||||||
test_runner.cpp
|
test_runner.cpp
|
||||||
IXTest.cpp
|
IXTest.cpp
|
||||||
|
IXGetFreePort.cpp
|
||||||
../third_party/msgpack11/msgpack11.cpp
|
../third_party/msgpack11/msgpack11.cpp
|
||||||
../ws/ixcore/utils/IXCoreLogger.cpp
|
../ws/ixcore/utils/IXCoreLogger.cpp
|
||||||
|
|
||||||
@ -38,6 +39,8 @@ set (SOURCES
|
|||||||
IXUrlParserTest.cpp
|
IXUrlParserTest.cpp
|
||||||
IXWebSocketServerTest.cpp
|
IXWebSocketServerTest.cpp
|
||||||
IXHttpClientTest.cpp
|
IXHttpClientTest.cpp
|
||||||
|
IXHttpServerTest.cpp
|
||||||
|
IXUnityBuildsTest.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# Some unittest don't work on windows yet
|
# Some unittest don't work on windows yet
|
||||||
@ -64,7 +67,7 @@ endif()
|
|||||||
|
|
||||||
add_executable(ixwebsocket_unittest ${SOURCES})
|
add_executable(ixwebsocket_unittest ${SOURCES})
|
||||||
|
|
||||||
if (NOT WIN32)
|
if (MAC)
|
||||||
add_sanitizers(ixwebsocket_unittest)
|
add_sanitizers(ixwebsocket_unittest)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
@ -17,33 +17,33 @@ TEST_CASE("dns", "[net]")
|
|||||||
{
|
{
|
||||||
SECTION("Test resolving a known hostname")
|
SECTION("Test resolving a known hostname")
|
||||||
{
|
{
|
||||||
DNSLookup dnsLookup("www.google.com", 80);
|
auto dnsLookup = std::make_shared<DNSLookup>("www.google.com", 80);
|
||||||
|
|
||||||
std::string errMsg;
|
std::string errMsg;
|
||||||
struct addrinfo* res;
|
struct addrinfo* res;
|
||||||
|
|
||||||
res = dnsLookup.resolve(errMsg, [] { return false; });
|
res = dnsLookup->resolve(errMsg, [] { return false; });
|
||||||
std::cerr << "Error message: " << errMsg << std::endl;
|
std::cerr << "Error message: " << errMsg << std::endl;
|
||||||
REQUIRE(res != nullptr);
|
REQUIRE(res != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("Test resolving a non-existing hostname")
|
SECTION("Test resolving a non-existing hostname")
|
||||||
{
|
{
|
||||||
DNSLookup dnsLookup("wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", 80);
|
auto dnsLookup = std::make_shared<DNSLookup>("wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", 80);
|
||||||
|
|
||||||
std::string errMsg;
|
std::string errMsg;
|
||||||
struct addrinfo* res = dnsLookup.resolve(errMsg, [] { return false; });
|
struct addrinfo* res = dnsLookup->resolve(errMsg, [] { return false; });
|
||||||
std::cerr << "Error message: " << errMsg << std::endl;
|
std::cerr << "Error message: " << errMsg << std::endl;
|
||||||
REQUIRE(res == nullptr);
|
REQUIRE(res == nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("Test resolving a good hostname, with cancellation")
|
SECTION("Test resolving a good hostname, with cancellation")
|
||||||
{
|
{
|
||||||
DNSLookup dnsLookup("www.google.com", 80, 1);
|
auto dnsLookup = std::make_shared<DNSLookup>("www.google.com", 80, 1);
|
||||||
|
|
||||||
std::string errMsg;
|
std::string errMsg;
|
||||||
// The callback returning true means we are requesting cancellation
|
// The callback returning true means we are requesting cancellation
|
||||||
struct addrinfo* res = dnsLookup.resolve(errMsg, [] { return true; });
|
struct addrinfo* res = dnsLookup->resolve(errMsg, [] { return true; });
|
||||||
std::cerr << "Error message: " << errMsg << std::endl;
|
std::cerr << "Error message: " << errMsg << std::endl;
|
||||||
REQUIRE(res == nullptr);
|
REQUIRE(res == nullptr);
|
||||||
}
|
}
|
||||||
|
93
test/IXGetFreePort.cpp
Normal file
93
test/IXGetFreePort.cpp
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* IXGetFreePort.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "IXGetFreePort.h"
|
||||||
|
#include <ixwebsocket/IXNetSystem.h>
|
||||||
|
#include <ixwebsocket/IXSocket.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
int getAnyFreePortRandom()
|
||||||
|
{
|
||||||
|
std::random_device rd;
|
||||||
|
std::uniform_int_distribution<int> dist(1024 + 1, 65535);
|
||||||
|
|
||||||
|
return dist(rd);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getAnyFreePort()
|
||||||
|
{
|
||||||
|
int defaultPort = 8090;
|
||||||
|
int sockfd;
|
||||||
|
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
|
||||||
|
{
|
||||||
|
return getAnyFreePortRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
int enable = 1;
|
||||||
|
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
|
||||||
|
(char*) &enable, sizeof(enable)) < 0)
|
||||||
|
{
|
||||||
|
return getAnyFreePortRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind to port 0. This is the standard way to get a free port.
|
||||||
|
struct sockaddr_in server; // server address information
|
||||||
|
server.sin_family = AF_INET;
|
||||||
|
server.sin_port = htons(0);
|
||||||
|
server.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||||
|
|
||||||
|
if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0)
|
||||||
|
{
|
||||||
|
Socket::closeSocket(sockfd);
|
||||||
|
return getAnyFreePortRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sockaddr_in sa; // server address information
|
||||||
|
socklen_t len = sizeof(sa);
|
||||||
|
if (getsockname(sockfd, (struct sockaddr *) &sa, &len) < 0)
|
||||||
|
{
|
||||||
|
Socket::closeSocket(sockfd);
|
||||||
|
return getAnyFreePortRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
int port = ntohs(sa.sin_port);
|
||||||
|
Socket::closeSocket(sockfd);
|
||||||
|
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getFreePort()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
#if defined(__has_feature)
|
||||||
|
# if __has_feature(address_sanitizer)
|
||||||
|
int port = getAnyFreePortRandom();
|
||||||
|
# else
|
||||||
|
int port = getAnyFreePort();
|
||||||
|
# endif
|
||||||
|
#else
|
||||||
|
int port = getAnyFreePort();
|
||||||
|
#endif
|
||||||
|
//
|
||||||
|
// Only port above 1024 can be used by non root users, but for some
|
||||||
|
// reason I got port 7 returned with macOS when binding on port 0...
|
||||||
|
//
|
||||||
|
if (port > 1024)
|
||||||
|
{
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} // namespace ix
|
||||||
|
|
||||||
|
|
12
test/IXGetFreePort.h
Normal file
12
test/IXGetFreePort.h
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* IXGetFreePort.h
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
int getFreePort();
|
||||||
|
} // namespace ix
|
70
test/IXHttpServerTest.cpp
Normal file
70
test/IXHttpServerTest.cpp
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* IXSocketTest.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <ixwebsocket/IXHttpClient.h>
|
||||||
|
#include <ixwebsocket/IXHttpServer.h>
|
||||||
|
#include "IXGetFreePort.h"
|
||||||
|
|
||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
using namespace ix;
|
||||||
|
|
||||||
|
TEST_CASE("http server", "[httpd]")
|
||||||
|
{
|
||||||
|
SECTION("Connect to a local HTTP server")
|
||||||
|
{
|
||||||
|
int port = getFreePort();
|
||||||
|
ix::HttpServer server(port, "127.0.0.1");
|
||||||
|
|
||||||
|
auto res = server.listen();
|
||||||
|
REQUIRE(res.first);
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
HttpClient httpClient;
|
||||||
|
WebSocketHttpHeaders headers;
|
||||||
|
|
||||||
|
std::string url("http://127.0.0.1:");
|
||||||
|
url += std::to_string(port);
|
||||||
|
url += "/data/foo.txt";
|
||||||
|
auto args = httpClient.createRequest(url);
|
||||||
|
|
||||||
|
args->extraHeaders = headers;
|
||||||
|
args->connectTimeout = 60;
|
||||||
|
args->transferTimeout = 60;
|
||||||
|
args->followRedirects = true;
|
||||||
|
args->maxRedirects = 10;
|
||||||
|
args->verbose = true;
|
||||||
|
args->compress = true;
|
||||||
|
args->logger = [](const std::string& msg)
|
||||||
|
{
|
||||||
|
std::cout << msg;
|
||||||
|
};
|
||||||
|
args->onProgressCallback = [](int current, int total) -> bool
|
||||||
|
{
|
||||||
|
std::cerr << "\r" << "Downloaded "
|
||||||
|
<< current << " bytes out of " << total;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto response = httpClient.get(url, args);
|
||||||
|
|
||||||
|
for (auto it : response->headers)
|
||||||
|
{
|
||||||
|
std::cerr << it.first << ": " << it.second << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << "Upload size: " << response->uploadSize << std::endl;
|
||||||
|
std::cerr << "Download size: " << response->downloadSize << std::endl;
|
||||||
|
std::cerr << "Status: " << response->statusCode << std::endl;
|
||||||
|
std::cerr << "Error message: " << response->errorMsg << std::endl;
|
||||||
|
|
||||||
|
REQUIRE(response->errorCode == HttpErrorCode::Ok);
|
||||||
|
REQUIRE(response->statusCode == 200);
|
||||||
|
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
}
|
@ -72,88 +72,6 @@ namespace ix
|
|||||||
Logger() << msg;
|
Logger() << msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
int getAnyFreePortRandom()
|
|
||||||
{
|
|
||||||
std::random_device rd;
|
|
||||||
std::uniform_int_distribution<int> dist(1024 + 1, 65535);
|
|
||||||
|
|
||||||
return dist(rd);
|
|
||||||
}
|
|
||||||
|
|
||||||
int getAnyFreePort()
|
|
||||||
{
|
|
||||||
int defaultPort = 8090;
|
|
||||||
int sockfd;
|
|
||||||
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
|
|
||||||
{
|
|
||||||
log("Cannot compute a free port. socket error.");
|
|
||||||
return getAnyFreePortRandom();
|
|
||||||
}
|
|
||||||
|
|
||||||
int enable = 1;
|
|
||||||
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
|
|
||||||
(char*) &enable, sizeof(enable)) < 0)
|
|
||||||
{
|
|
||||||
log("Cannot compute a free port. setsockopt error.");
|
|
||||||
return getAnyFreePortRandom();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind to port 0. This is the standard way to get a free port.
|
|
||||||
struct sockaddr_in server; // server address information
|
|
||||||
server.sin_family = AF_INET;
|
|
||||||
server.sin_port = htons(0);
|
|
||||||
server.sin_addr.s_addr = inet_addr("127.0.0.1");
|
|
||||||
|
|
||||||
if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0)
|
|
||||||
{
|
|
||||||
log("Cannot compute a free port. bind error.");
|
|
||||||
|
|
||||||
Socket::closeSocket(sockfd);
|
|
||||||
return getAnyFreePortRandom();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sockaddr_in sa; // server address information
|
|
||||||
socklen_t len = sizeof(sa);
|
|
||||||
if (getsockname(sockfd, (struct sockaddr *) &sa, &len) < 0)
|
|
||||||
{
|
|
||||||
log("Cannot compute a free port. getsockname error.");
|
|
||||||
|
|
||||||
Socket::closeSocket(sockfd);
|
|
||||||
return getAnyFreePortRandom();
|
|
||||||
}
|
|
||||||
|
|
||||||
int port = ntohs(sa.sin_port);
|
|
||||||
Socket::closeSocket(sockfd);
|
|
||||||
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getFreePort()
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
#if defined(__has_feature)
|
|
||||||
# if __has_feature(address_sanitizer)
|
|
||||||
int port = getAnyFreePortRandom();
|
|
||||||
# else
|
|
||||||
int port = getAnyFreePort();
|
|
||||||
# endif
|
|
||||||
#else
|
|
||||||
int port = getAnyFreePort();
|
|
||||||
#endif
|
|
||||||
//
|
|
||||||
// Only port above 1024 can be used by non root users, but for some
|
|
||||||
// reason I got port 7 returned with macOS when binding on port 0...
|
|
||||||
//
|
|
||||||
if (port > 1024)
|
|
||||||
{
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void hexDump(const std::string& prefix,
|
void hexDump(const std::string& prefix,
|
||||||
const std::string& s)
|
const std::string& s)
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <ixwebsocket/IXWebSocketServer.h>
|
#include <ixwebsocket/IXWebSocketServer.h>
|
||||||
|
#include "IXGetFreePort.h"
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@ -46,7 +47,5 @@ namespace ix
|
|||||||
|
|
||||||
void log(const std::string& msg);
|
void log(const std::string& msg);
|
||||||
|
|
||||||
int getFreePort();
|
|
||||||
|
|
||||||
bool startWebSocketEchoServer(ix::WebSocketServer& server);
|
bool startWebSocketEchoServer(ix::WebSocketServer& server);
|
||||||
} // namespace ix
|
} // namespace ix
|
||||||
|
52
test/IXUnityBuildsTest.cpp
Normal file
52
test/IXUnityBuildsTest.cpp
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* IXUnityBuildsTest.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2019 Machine Zone. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <ixwebsocket/IXCancellationRequest.h>
|
||||||
|
#include <ixwebsocket/IXConnectionState.h>
|
||||||
|
#include <ixwebsocket/IXDNSLookup.h>
|
||||||
|
#include <ixwebsocket/IXHttp.h>
|
||||||
|
#include <ixwebsocket/IXHttpClient.h>
|
||||||
|
#include <ixwebsocket/IXHttpServer.h>
|
||||||
|
#include <ixwebsocket/IXNetSystem.h>
|
||||||
|
#include <ixwebsocket/IXProgressCallback.h>
|
||||||
|
#include <ixwebsocket/IXSelectInterrupt.h>
|
||||||
|
#include <ixwebsocket/IXSelectInterruptFactory.h>
|
||||||
|
#include <ixwebsocket/IXSetThreadName.h>
|
||||||
|
#include <ixwebsocket/IXSocket.h>
|
||||||
|
#include <ixwebsocket/IXSocketConnect.h>
|
||||||
|
#include <ixwebsocket/IXSocketFactory.h>
|
||||||
|
#include <ixwebsocket/IXSocketServer.h>
|
||||||
|
#include <ixwebsocket/IXUrlParser.h>
|
||||||
|
#include <ixwebsocket/IXWebSocket.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketCloseConstants.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketCloseInfo.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketErrorInfo.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketHandshake.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketHttpHeaders.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketMessage.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketMessageQueue.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketMessageType.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketOpenInfo.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketPerMessageDeflate.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketPerMessageDeflateCodec.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketPerMessageDeflateOptions.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketSendInfo.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketServer.h>
|
||||||
|
#include <ixwebsocket/IXWebSocketTransport.h>
|
||||||
|
#include <ixwebsocket/LUrlParser.h>
|
||||||
|
#include <ixwebsocket/libwshandshake.hpp>
|
||||||
|
|
||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
using namespace ix;
|
||||||
|
|
||||||
|
TEST_CASE("unity build", "[unity_build]")
|
||||||
|
{
|
||||||
|
SECTION("dummy test")
|
||||||
|
{
|
||||||
|
REQUIRE(true);
|
||||||
|
}
|
||||||
|
}
|
1
test/data/foo.txt
Normal file
1
test/data/foo.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hello world
|
@ -99,8 +99,10 @@ def runCMake(sanitizer, buildDir):
|
|||||||
#generator = '"NMake Makefiles"'
|
#generator = '"NMake Makefiles"'
|
||||||
#generator = '"Visual Studio 16 2019"'
|
#generator = '"Visual Studio 16 2019"'
|
||||||
generator = '"Visual Studio 15 2017"'
|
generator = '"Visual Studio 15 2017"'
|
||||||
|
USE_VENDORED_THIRD_PARTY = 'ON'
|
||||||
else:
|
else:
|
||||||
generator = '"Unix Makefiles"'
|
generator = '"Unix Makefiles"'
|
||||||
|
USE_VENDORED_THIRD_PARTY = 'ON'
|
||||||
|
|
||||||
CMAKE_BUILD_TYPE = BUILD_TYPE
|
CMAKE_BUILD_TYPE = BUILD_TYPE
|
||||||
|
|
||||||
@ -109,7 +111,9 @@ def runCMake(sanitizer, buildDir):
|
|||||||
-B"{buildDir}" \
|
-B"{buildDir}" \
|
||||||
-DCMAKE_BUILD_TYPE={CMAKE_BUILD_TYPE} \
|
-DCMAKE_BUILD_TYPE={CMAKE_BUILD_TYPE} \
|
||||||
-DUSE_TLS=1 \
|
-DUSE_TLS=1 \
|
||||||
|
-DUSE_MBED_TLS=1 \
|
||||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||||
|
-DUSE_VENDORED_THIRD_PARTY={USE_VENDORED_THIRD_PARTY} \
|
||||||
-G{generator}'
|
-G{generator}'
|
||||||
|
|
||||||
cmakeCmd = fmt.format(**locals())
|
cmakeCmd = fmt.format(**locals())
|
||||||
|
@ -68,6 +68,7 @@ add_executable(ws
|
|||||||
ws_cobra_to_statsd.cpp
|
ws_cobra_to_statsd.cpp
|
||||||
ws_cobra_to_sentry.cpp
|
ws_cobra_to_sentry.cpp
|
||||||
ws_snake.cpp
|
ws_snake.cpp
|
||||||
|
ws_httpd.cpp
|
||||||
ws.cpp)
|
ws.cpp)
|
||||||
|
|
||||||
target_link_libraries(ws ixwebsocket)
|
target_link_libraries(ws ixwebsocket)
|
||||||
|
@ -72,7 +72,6 @@ namespace ix
|
|||||||
std::string line;
|
std::string line;
|
||||||
std::stringstream tokenStream(stack);
|
std::stringstream tokenStream(stack);
|
||||||
|
|
||||||
std::stringstream ss;
|
|
||||||
std::smatch group;
|
std::smatch group;
|
||||||
|
|
||||||
while (std::getline(tokenStream, line))
|
while (std::getline(tokenStream, line))
|
||||||
@ -84,6 +83,7 @@ namespace ix
|
|||||||
const auto linenoStr = group.str(2);
|
const auto linenoStr = group.str(2);
|
||||||
const auto function = group.str(3);
|
const auto function = group.str(3);
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
ss << linenoStr;
|
ss << linenoStr;
|
||||||
uint64_t lineno;
|
uint64_t lineno;
|
||||||
ss >> lineno;
|
ss >> lineno;
|
||||||
@ -97,6 +97,8 @@ namespace ix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::reverse(frames.begin(), frames.end());
|
||||||
|
|
||||||
return frames;
|
return frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,6 +216,10 @@ int main(int argc, char** argv)
|
|||||||
->check(CLI::ExistingPath);
|
->check(CLI::ExistingPath);
|
||||||
runApp->add_flag("-v", verbose, "Verbose");
|
runApp->add_flag("-v", verbose, "Verbose");
|
||||||
|
|
||||||
|
CLI::App* httpServerApp = app.add_subcommand("httpd", "HTTP server");
|
||||||
|
httpServerApp->add_option("--port", port, "Port");
|
||||||
|
httpServerApp->add_option("--host", hostname, "Hostname");
|
||||||
|
|
||||||
CLI11_PARSE(app, argc, argv);
|
CLI11_PARSE(app, argc, argv);
|
||||||
|
|
||||||
// pid file handling
|
// pid file handling
|
||||||
@ -313,6 +317,10 @@ int main(int argc, char** argv)
|
|||||||
redisPassword, verbose,
|
redisPassword, verbose,
|
||||||
appsConfigPath);
|
appsConfigPath);
|
||||||
}
|
}
|
||||||
|
else if (app.got_subcommand("httpd"))
|
||||||
|
{
|
||||||
|
ret = ix::ws_httpd_main(port, hostname);
|
||||||
|
}
|
||||||
|
|
||||||
ix::uninitNetSystem();
|
ix::uninitNetSystem();
|
||||||
return ret;
|
return ret;
|
||||||
|
2
ws/ws.h
2
ws/ws.h
@ -93,4 +93,6 @@ namespace ix
|
|||||||
const std::string& redisPassword,
|
const std::string& redisPassword,
|
||||||
bool verbose,
|
bool verbose,
|
||||||
const std::string& appsConfigPath);
|
const std::string& appsConfigPath);
|
||||||
|
|
||||||
|
int ws_httpd_main(int port, const std::string& hostname);
|
||||||
} // namespace ix
|
} // namespace ix
|
||||||
|
34
ws/ws_httpd.cpp
Normal file
34
ws/ws_httpd.cpp
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* ws_httpd.cpp
|
||||||
|
* Author: Benjamin Sergeant
|
||||||
|
* Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <ixwebsocket/IXHttpServer.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
namespace ix
|
||||||
|
{
|
||||||
|
int ws_httpd_main(int port, const std::string& hostname)
|
||||||
|
{
|
||||||
|
spdlog::info("Listening on {}:{}", hostname, port);
|
||||||
|
|
||||||
|
ix::HttpServer server(port, hostname);
|
||||||
|
|
||||||
|
auto res = server.listen();
|
||||||
|
if (!res.first)
|
||||||
|
{
|
||||||
|
std::cerr << res.second << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
server.wait();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user