More backports from Pro edition, among which HTTPS/TLS support
This commit is contained in:
parent
2432bbdfc3
commit
cc9e970ea3
26 changed files with 733 additions and 324 deletions
|
@ -107,7 +107,7 @@ static inline void show_stackframe(){}
|
|||
#define STATS_INPUT_DELAY 2
|
||||
|
||||
#ifndef INPUT_TIMEOUT
|
||||
#define INPUT_TIMEOUT STATS_DELAY
|
||||
#define INPUT_TIMEOUT STATS_DELAY*2
|
||||
#endif
|
||||
|
||||
/// The size used for stream headers for live streams
|
||||
|
|
|
@ -59,16 +59,10 @@ namespace HTTP{
|
|||
|
||||
/// Returns a reference to the internal Socket::Connection class instance.
|
||||
Socket::Connection &Downloader::getSocket(){
|
||||
#ifdef SSL
|
||||
if (ssl){return S_SSL;}
|
||||
#endif
|
||||
return S;
|
||||
}
|
||||
|
||||
Downloader::~Downloader(){
|
||||
#ifdef SSL
|
||||
if (ssl){S_SSL.close();}
|
||||
#endif
|
||||
S.close();
|
||||
}
|
||||
|
||||
|
@ -87,12 +81,12 @@ namespace HTTP{
|
|||
connectedPort = link.getPort();
|
||||
#ifdef SSL
|
||||
if (needSSL){
|
||||
S_SSL = Socket::SSLConnection(connectedHost, connectedPort, true);
|
||||
S.open(connectedHost, connectedPort, true, true);
|
||||
}else{
|
||||
S = Socket::Connection(connectedHost, connectedPort, true);
|
||||
S.open(connectedHost, connectedPort, true);
|
||||
}
|
||||
#else
|
||||
S = Socket::Connection(connectedHost, connectedPort, true);
|
||||
S.open(connectedHost, connectedPort, true);
|
||||
#endif
|
||||
}
|
||||
}else{
|
||||
|
@ -101,7 +95,7 @@ namespace HTTP{
|
|||
getSocket().close();
|
||||
connectedHost = proxyUrl.host;
|
||||
connectedPort = proxyUrl.getPort();
|
||||
S = Socket::Connection(connectedHost, connectedPort, true);
|
||||
S.open(connectedHost, connectedPort, true);
|
||||
}
|
||||
}
|
||||
ssl = needSSL;
|
||||
|
@ -218,8 +212,8 @@ namespace HTTP{
|
|||
MEDIUM_MSG("Posting to %s (%zu/%" PRIu32 ")", link.getUrl().c_str(), retryCount - loop + 1,
|
||||
retryCount);
|
||||
doRequest(link, "POST", payload);
|
||||
// Not synced? Ignore the response and immediately return false.
|
||||
if (!sync){return false;}
|
||||
// Not synced? Ignore the response and immediately return true.
|
||||
if (!sync){return true;}
|
||||
uint64_t reqTime = Util::bootSecs();
|
||||
while (getSocket() && Util::bootSecs() < reqTime + dataTimeout){
|
||||
// No data? Wait for a second or so.
|
||||
|
|
|
@ -36,9 +36,6 @@ namespace HTTP{
|
|||
uint32_t connectedPort; ///< Currently connected port number
|
||||
Parser H; ///< HTTP parser for downloader
|
||||
Socket::Connection S; ///< TCP socket for downloader
|
||||
#ifdef SSL
|
||||
Socket::SSLConnection S_SSL; ///< SSL socket for downloader
|
||||
#endif
|
||||
bool ssl; ///< True if ssl is currently in use.
|
||||
std::string authStr; ///< Most recently seen WWW-Authenticate request
|
||||
std::string proxyAuthStr; ///< Most recently seen Proxy-Authenticate request
|
||||
|
|
|
@ -1513,6 +1513,7 @@ namespace DTSC {
|
|||
}
|
||||
|
||||
void Meta::update(long long packTime, long long packOffset, long long packTrack, long long packDataSize, uint64_t packBytePos, bool isKeyframe, long long packSendSize, unsigned long segment_size){
|
||||
DONTEVEN_MSG("Updating meta with: t=%lld, o=%lld, s=%lld, t=%lld, p=%lld", packTime, packOffset, packDataSize, packTrack, packBytePos);
|
||||
if (!packSendSize){
|
||||
//time and trackID are part of the 20-byte header.
|
||||
//the container object adds 4 bytes (plus 2+namelen for each content, see below)
|
||||
|
|
|
@ -150,6 +150,7 @@ uint32_t HTTP::URL::getDefaultPort() const{
|
|||
if (protocol == "http"){return 80;}
|
||||
if (protocol == "https"){return 443;}
|
||||
if (protocol == "rtmp"){return 1935;}
|
||||
if (protocol == "rtmps"){return 443;}
|
||||
if (protocol == "dtsc"){return 4200;}
|
||||
if (protocol == "rtsp"){return 554;}
|
||||
return 0;
|
||||
|
@ -418,8 +419,29 @@ std::string &HTTP::Parser::BuildRequest(){
|
|||
/// Creates and sends a valid HTTP 1.0 or 1.1 request.
|
||||
/// The request is build from internal variables set before this call is made.
|
||||
/// To be precise, method, url, protocol, headers and body are used.
|
||||
void HTTP::Parser::SendRequest(Socket::Connection &conn, const std::string &reqbody){
|
||||
void HTTP::Parser::SendRequest(Socket::Connection &conn, const std::string &reqbody, bool allAtOnce){
|
||||
/// \todo Include GET/POST variable parsing?
|
||||
if (allAtOnce){
|
||||
/// \TODO Make this less duplicated / more pretty.
|
||||
|
||||
std::map<std::string, std::string>::iterator it;
|
||||
if (protocol.size() < 5 || protocol[4] != '/'){protocol = "HTTP/1.0";}
|
||||
builder = method + " " + url + " " + protocol + "\r\n";
|
||||
if (reqbody.size()){SetHeader("Content-Length", reqbody.length());}
|
||||
for (it = headers.begin(); it != headers.end(); it++){
|
||||
if ((*it).first != "" && (*it).second != ""){
|
||||
builder += (*it).first + ": " + (*it).second + "\r\n";
|
||||
}
|
||||
}
|
||||
builder += "\r\n";
|
||||
if (reqbody.size()){
|
||||
builder += reqbody;
|
||||
}else{
|
||||
builder += body;
|
||||
}
|
||||
conn.SendNow(builder);
|
||||
return;
|
||||
}
|
||||
std::map<std::string, std::string>::iterator it;
|
||||
if (protocol.size() < 5 || protocol[4] != '/'){protocol = "HTTP/1.0";}
|
||||
builder = method + " " + url + " " + protocol + "\r\n";
|
||||
|
@ -827,6 +849,10 @@ bool HTTP::Parser::parse(std::string &HTTPbuffer){
|
|||
length = atoi(GetHeader("Content-Length").c_str());
|
||||
if (body.capacity() < length){body.reserve(length);}
|
||||
}
|
||||
if (GetHeader("Content-length") != ""){
|
||||
length = atoi(GetHeader("Content-length").c_str());
|
||||
if (body.capacity() < length){body.reserve(length);}
|
||||
}
|
||||
if (GetHeader("Transfer-Encoding") == "chunked"){
|
||||
getChunks = true;
|
||||
doingChunk = 0;
|
||||
|
|
|
@ -37,7 +37,7 @@ namespace HTTP{
|
|||
std::string &BuildRequest();
|
||||
std::string &BuildResponse();
|
||||
std::string &BuildResponse(std::string code, std::string message);
|
||||
void SendRequest(Socket::Connection &conn, const std::string &reqbody = "");
|
||||
void SendRequest(Socket::Connection &conn, const std::string &reqbody = "", bool allAtOnce = false);
|
||||
void SendResponse(std::string code, std::string message, Socket::Connection &conn);
|
||||
void StartResponse(std::string code, std::string message, Parser &request,
|
||||
Socket::Connection &conn, bool bufferAllChunks = false);
|
||||
|
|
18
lib/ogg.cpp
18
lib/ogg.cpp
|
@ -107,6 +107,7 @@ namespace OGG {
|
|||
/// Reads an OGG Page from the source and if valid, removes it from source.
|
||||
bool Page::read(std::string & newData){
|
||||
int len = newData.size();
|
||||
int total = 0;
|
||||
segments.clear();
|
||||
if (newData.size() < 27){
|
||||
return false;
|
||||
|
@ -119,14 +120,25 @@ namespace OGG {
|
|||
if (newData.size() < 27u + getPageSegments()){ //check input size
|
||||
return false;
|
||||
}
|
||||
newData.erase(0, 27);
|
||||
memcpy(data + 27, newData.c_str(), getPageSegments());
|
||||
newData.erase(0, getPageSegments());
|
||||
memcpy(data + 27, newData.data()+27, getPageSegments());
|
||||
std::deque<unsigned int> segSizes = decodeXiphSize(data + 27, getPageSegments());
|
||||
for (std::deque<unsigned int>::iterator it = segSizes.begin(); it != segSizes.end(); it++){
|
||||
total += *it;
|
||||
}
|
||||
|
||||
total += 27;
|
||||
//return false if the segment is not complete
|
||||
total += getPageSegments();
|
||||
if(total >= len){
|
||||
return false;
|
||||
}
|
||||
|
||||
newData.erase(0, getPageSegments()+27);
|
||||
for (std::deque<unsigned int>::iterator it = segSizes.begin(); it != segSizes.end(); it++){
|
||||
segments.push_back(std::string(newData.data(), *it));
|
||||
newData.erase(0, *it);
|
||||
}
|
||||
|
||||
INFO_MSG("Erased %lu bytes from the input", len - newData.size());
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -100,30 +100,29 @@ void Util::Procs::exit_handler() {
|
|||
return;
|
||||
}
|
||||
|
||||
WARN_MSG("Sending SIGINT to remaining %d children", (int)listcopy.size());
|
||||
//send sigint to all remaining
|
||||
if (!listcopy.empty()) {
|
||||
for (it = listcopy.begin(); it != listcopy.end(); it++) {
|
||||
DEBUG_MSG(DLVL_DEVEL, "SIGINT %d", *it);
|
||||
kill(*it, SIGINT);
|
||||
}
|
||||
}
|
||||
|
||||
INFO_MSG("Waiting up to 5 seconds for %d children to terminate.", (int)listcopy.size());
|
||||
INFO_MSG("Sending SIGINT and waiting up to 10 seconds for %d children to terminate.", (int)listcopy.size());
|
||||
waiting = 0;
|
||||
//wait up to 5 seconds for applications to shut down
|
||||
while (!listcopy.empty() && waiting <= 250) {
|
||||
//wait up to 10 seconds for applications to shut down
|
||||
while (!listcopy.empty() && waiting <= 500) {
|
||||
bool doWait = true;
|
||||
for (it = listcopy.begin(); it != listcopy.end(); it++) {
|
||||
if (!childRunning(*it)) {
|
||||
listcopy.erase(it);
|
||||
doWait = false;
|
||||
break;
|
||||
}
|
||||
if (!listcopy.empty()) {
|
||||
}
|
||||
if (doWait && !listcopy.empty()) {
|
||||
if ((waiting % 50) == 0){
|
||||
for (it = listcopy.begin(); it != listcopy.end(); it++) {
|
||||
INFO_MSG("SIGINT %d", *it);
|
||||
kill(*it, SIGINT);
|
||||
}
|
||||
}
|
||||
Util::wait(20);
|
||||
++waiting;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (listcopy.empty()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1192,8 +1192,6 @@ namespace IPC {
|
|||
///\brief The deconstructor
|
||||
sharedClient::~sharedClient() {
|
||||
mySemaphore.close();
|
||||
|
||||
|
||||
}
|
||||
|
||||
///\brief Writes data to the shared data
|
||||
|
|
544
lib/socket.cpp
544
lib/socket.cpp
|
@ -402,25 +402,40 @@ void Socket::Connection::setBoundAddr(){
|
|||
}
|
||||
}
|
||||
|
||||
//Cleans up the socket by dropping the connection.
|
||||
//Does not call close because it calls shutdown, which would destroy any copies of this socket too.
|
||||
Socket::Connection::~Connection(){
|
||||
drop();
|
||||
}
|
||||
|
||||
|
||||
/// Create a new base socket. This is a basic constructor for converting any valid socket to a
|
||||
/// Socket::Connection. \param sockNo Integer representing the socket to convert.
|
||||
Socket::Connection::Connection(int sockNo){
|
||||
sSend = sockNo;
|
||||
sRecv = -1;
|
||||
isTrueSocket = Socket::checkTrueSocket(sSend);
|
||||
setBoundAddr();
|
||||
up = 0;
|
||||
down = 0;
|
||||
conntime = Util::epoch();
|
||||
Error = false;
|
||||
Blocking = false;
|
||||
skipCount = 0;
|
||||
clear();
|
||||
open(sockNo, sockNo);
|
||||
}// Socket::Connection basic constructor
|
||||
|
||||
/// Open from existing socket connection.
|
||||
/// Closes any existing connections and resets all internal values beforehand.
|
||||
/// Simply calls open(sockNo, sockNo) internally.
|
||||
void Socket::Connection::open(int sockNo){
|
||||
open(sockNo, sockNo);
|
||||
}
|
||||
|
||||
/// Simulate a socket using two file descriptors.
|
||||
/// \param write The filedescriptor to write to.
|
||||
/// \param read The filedescriptor to read from.
|
||||
Socket::Connection::Connection(int write, int read){
|
||||
clear();
|
||||
open(write, read);
|
||||
}// Socket::Connection basic constructor
|
||||
|
||||
/// Open from two existing file descriptors.
|
||||
/// Closes any existing connections and resets all internal values beforehand.
|
||||
void Socket::Connection::open(int write, int read){
|
||||
drop();
|
||||
clear();
|
||||
sSend = write;
|
||||
if (write != read){
|
||||
sRecv = read;
|
||||
|
@ -429,17 +444,9 @@ Socket::Connection::Connection(int write, int read){
|
|||
}
|
||||
isTrueSocket = Socket::checkTrueSocket(sSend);
|
||||
setBoundAddr();
|
||||
up = 0;
|
||||
down = 0;
|
||||
conntime = Util::epoch();
|
||||
Error = false;
|
||||
Blocking = false;
|
||||
skipCount = 0;
|
||||
}// Socket::Connection basic constructor
|
||||
}
|
||||
|
||||
/// Create a new disconnected base socket. This is a basic constructor for placeholder purposes.
|
||||
/// A socket created like this is always disconnected and should/could be overwritten at some point.
|
||||
Socket::Connection::Connection(){
|
||||
void Socket::Connection::clear(){
|
||||
sSend = -1;
|
||||
sRecv = -1;
|
||||
isTrueSocket = false;
|
||||
|
@ -449,6 +456,20 @@ Socket::Connection::Connection(){
|
|||
Error = false;
|
||||
Blocking = false;
|
||||
skipCount = 0;
|
||||
#ifdef SSL
|
||||
sslConnected = false;
|
||||
server_fd = 0;
|
||||
ssl = 0;
|
||||
conf = 0;
|
||||
ctr_drbg = 0;
|
||||
entropy = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Create a new disconnected base socket. This is a basic constructor for placeholder purposes.
|
||||
/// A socket created like this is always disconnected and should/could be overwritten at some point.
|
||||
Socket::Connection::Connection(){
|
||||
clear();
|
||||
}// Socket::Connection basic constructor
|
||||
|
||||
void Socket::Connection::resetCounter(){
|
||||
|
@ -483,12 +504,28 @@ bool isFDBlocking(int FD){
|
|||
|
||||
/// Set this socket to be blocking (true) or nonblocking (false).
|
||||
void Socket::Connection::setBlocking(bool blocking){
|
||||
#ifdef SSL
|
||||
if (sslConnected){
|
||||
if (blocking == Blocking){return;}
|
||||
if (blocking){
|
||||
mbedtls_net_set_block(server_fd);
|
||||
Blocking = true;
|
||||
}else{
|
||||
mbedtls_net_set_nonblock(server_fd);
|
||||
Blocking = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (sSend >= 0){setFDBlocking(sSend, blocking);}
|
||||
if (sRecv >= 0 && sSend != sRecv){setFDBlocking(sRecv, blocking);}
|
||||
}
|
||||
|
||||
/// Set this socket to be blocking (true) or nonblocking (false).
|
||||
bool Socket::Connection::isBlocking(){
|
||||
#ifdef SSL
|
||||
if (sslConnected){return Blocking;}
|
||||
#endif
|
||||
if (sSend >= 0){return isFDBlocking(sSend);}
|
||||
if (sRecv >= 0){return isFDBlocking(sRecv);}
|
||||
return false;
|
||||
|
@ -508,6 +545,41 @@ void Socket::Connection::close(){
|
|||
/// This function does *not* call shutdown, allowing continued use in other
|
||||
/// processes.
|
||||
void Socket::Connection::drop(){
|
||||
#ifdef SSL
|
||||
if (sslConnected){
|
||||
DONTEVEN_MSG("SSL close");
|
||||
if (ssl){
|
||||
mbedtls_ssl_close_notify(ssl);
|
||||
}
|
||||
if (server_fd){
|
||||
mbedtls_net_free(server_fd);
|
||||
delete server_fd;
|
||||
server_fd = 0;
|
||||
}
|
||||
if (ssl){
|
||||
mbedtls_ssl_free(ssl);
|
||||
delete ssl;
|
||||
ssl = 0;
|
||||
}
|
||||
if (conf){
|
||||
mbedtls_ssl_config_free(conf);
|
||||
delete conf;
|
||||
conf = 0;
|
||||
}
|
||||
if (ctr_drbg){
|
||||
mbedtls_ctr_drbg_free(ctr_drbg);
|
||||
delete ctr_drbg;
|
||||
ctr_drbg = 0;
|
||||
}
|
||||
if (entropy){
|
||||
mbedtls_entropy_free(entropy);
|
||||
delete entropy;
|
||||
entropy = 0;
|
||||
}
|
||||
sslConnected = false;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (connected()){
|
||||
if (sSend != -1){
|
||||
HIGH_MSG("Socket %d closed", sSend);
|
||||
|
@ -547,8 +619,15 @@ std::string Socket::Connection::getError(){
|
|||
/// \param address String containing the location of the Unix socket to connect to.
|
||||
/// \param nonblock Whether the socket should be nonblocking. False by default.
|
||||
Socket::Connection::Connection(std::string address, bool nonblock){
|
||||
skipCount = 0;
|
||||
sRecv = -1;
|
||||
clear();
|
||||
open(address, nonblock);
|
||||
}// Socket::Connection Unix Constructor
|
||||
|
||||
/// Open Unix connection.
|
||||
/// Closes any existing connections and resets all internal values beforehand.
|
||||
void Socket::Connection::open(std::string address, bool nonblock){
|
||||
drop();
|
||||
clear();
|
||||
isTrueSocket = true;
|
||||
sSend = socket(PF_UNIX, SOCK_STREAM, 0);
|
||||
if (sSend < 0){
|
||||
|
@ -556,11 +635,6 @@ Socket::Connection::Connection(std::string address, bool nonblock){
|
|||
FAIL_MSG("Could not create socket! Error: %s", remotehost.c_str());
|
||||
return;
|
||||
}
|
||||
Error = false;
|
||||
Blocking = false;
|
||||
up = 0;
|
||||
down = 0;
|
||||
conntime = Util::epoch();
|
||||
sockaddr_un addr;
|
||||
addr.sun_family = AF_UNIX;
|
||||
strncpy(addr.sun_path, address.c_str(), address.size() + 1);
|
||||
|
@ -576,22 +650,100 @@ Socket::Connection::Connection(std::string address, bool nonblock){
|
|||
FAIL_MSG("Could not connect to %s! Error: %s", address.c_str(), remotehost.c_str());
|
||||
close();
|
||||
}
|
||||
}// Socket::Connection Unix Constructor
|
||||
}
|
||||
|
||||
#ifdef SSL
|
||||
///Local-only function for debugging SSL sockets
|
||||
static void my_debug(void *ctx, int level, const char *file, int line, const char *str){
|
||||
((void)level);
|
||||
fprintf((FILE *)ctx, "%s:%04d: %s", file, line, str);
|
||||
fflush((FILE *)ctx);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Create a new TCP Socket. This socket will (try to) connect to the given host/port right away.
|
||||
/// \param host String containing the hostname to connect to.
|
||||
/// \param port String containing the port to connect to.
|
||||
/// \param nonblock Whether the socket should be nonblocking.
|
||||
Socket::Connection::Connection(std::string host, int port, bool nonblock){
|
||||
skipCount = 0;
|
||||
sRecv = -1;
|
||||
Socket::Connection::Connection(std::string host, int port, bool nonblock, bool with_ssl){
|
||||
clear();
|
||||
open(host, port, nonblock, with_ssl);
|
||||
}
|
||||
|
||||
/// Open TCP connection.
|
||||
/// Closes any existing connections and resets all internal values beforehand.
|
||||
void Socket::Connection::open(std::string host, int port, bool nonblock, bool with_ssl){
|
||||
drop();
|
||||
clear();
|
||||
if (with_ssl){
|
||||
#ifdef SSL
|
||||
mbedtls_debug_set_threshold(0);
|
||||
server_fd = new mbedtls_net_context;
|
||||
ssl = new mbedtls_ssl_context;
|
||||
conf = new mbedtls_ssl_config;
|
||||
ctr_drbg = new mbedtls_ctr_drbg_context;
|
||||
entropy = new mbedtls_entropy_context;
|
||||
mbedtls_net_init(server_fd);
|
||||
mbedtls_ssl_init(ssl);
|
||||
mbedtls_ssl_config_init(conf);
|
||||
mbedtls_ctr_drbg_init(ctr_drbg);
|
||||
mbedtls_entropy_init(entropy);
|
||||
DONTEVEN_MSG("SSL init");
|
||||
if (mbedtls_ctr_drbg_seed(ctr_drbg, mbedtls_entropy_func, entropy, (const unsigned char *)"meow",
|
||||
4) != 0){
|
||||
FAIL_MSG("SSL socket init failed");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
DONTEVEN_MSG("SSL connect");
|
||||
int ret = 0;
|
||||
if ((ret = mbedtls_net_connect(server_fd, host.c_str(), JSON::Value(port).asString().c_str(), MBEDTLS_NET_PROTO_TCP)) != 0){
|
||||
FAIL_MSG(" failed\n ! mbedtls_net_connect returned %d\n\n", ret);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if ((ret = mbedtls_ssl_config_defaults(conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
|
||||
MBEDTLS_SSL_PRESET_DEFAULT)) != 0){
|
||||
FAIL_MSG(" failed\n ! mbedtls_ssl_config_defaults returned %d\n\n", ret);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
mbedtls_ssl_conf_authmode(conf, MBEDTLS_SSL_VERIFY_NONE);
|
||||
mbedtls_ssl_conf_rng(conf, mbedtls_ctr_drbg_random, ctr_drbg);
|
||||
mbedtls_ssl_conf_dbg(conf, my_debug, stderr);
|
||||
if ((ret = mbedtls_ssl_setup(ssl, conf)) != 0){
|
||||
char estr[200];
|
||||
mbedtls_strerror(ret, estr, 200);
|
||||
FAIL_MSG("SSL setup error %d: %s", ret, estr);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if ((ret = mbedtls_ssl_set_hostname(ssl, host.c_str())) != 0){
|
||||
FAIL_MSG(" failed\n ! mbedtls_ssl_set_hostname returned %d\n\n", ret);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
mbedtls_ssl_set_bio(ssl, server_fd, mbedtls_net_send, mbedtls_net_recv, NULL);
|
||||
while ((ret = mbedtls_ssl_handshake(ssl)) != 0){
|
||||
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE){
|
||||
char estr[200];
|
||||
mbedtls_strerror(ret, estr, 200);
|
||||
FAIL_MSG("SSL handshake error %d: %s", ret, estr);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
sslConnected = true;
|
||||
Blocking = true;
|
||||
if (nonblock){setBlocking(false);}
|
||||
DONTEVEN_MSG("SSL connect success");
|
||||
return;
|
||||
#endif
|
||||
FAIL_MSG("Attempted to open SSL socket without SSL support compiled in!");
|
||||
return;
|
||||
}
|
||||
isTrueSocket = true;
|
||||
struct addrinfo *result, *rp, hints;
|
||||
Error = false;
|
||||
Blocking = false;
|
||||
up = 0;
|
||||
down = 0;
|
||||
conntime = Util::epoch();
|
||||
std::stringstream ss;
|
||||
ss << port;
|
||||
|
||||
|
@ -630,7 +782,7 @@ Socket::Connection::Connection(std::string host, int port, bool nonblock){
|
|||
setsockopt(sSend, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen);
|
||||
setBoundAddr();
|
||||
}
|
||||
}// Socket::Connection TCP Constructor
|
||||
}
|
||||
|
||||
/// Returns the connected-state for this socket.
|
||||
/// Note that this function might be slightly behind the real situation.
|
||||
|
@ -638,6 +790,9 @@ Socket::Connection::Connection(std::string host, int port, bool nonblock){
|
|||
/// and when the socket is closed manually.
|
||||
/// \returns True if socket is connected, false otherwise.
|
||||
bool Socket::Connection::connected() const{
|
||||
#ifdef SSL
|
||||
if (sslConnected){return true;}
|
||||
#endif
|
||||
return (sSend >= 0) || (sRecv >= 0);
|
||||
}
|
||||
|
||||
|
@ -721,6 +876,38 @@ void Socket::Connection::skipBytes(uint32_t byteCount){
|
|||
/// \param len Amount of bytes to write.
|
||||
/// \returns The amount of bytes actually written.
|
||||
unsigned int Socket::Connection::iwrite(const void *buffer, int len){
|
||||
#ifdef SSL
|
||||
if (sslConnected){
|
||||
DONTEVEN_MSG("SSL iwrite");
|
||||
if (!connected() || len < 1){return 0;}
|
||||
int r;
|
||||
r = mbedtls_ssl_write(ssl, (const unsigned char *)buffer, len);
|
||||
if (r < 0){
|
||||
char estr[200];
|
||||
mbedtls_strerror(r, estr, 200);
|
||||
INFO_MSG("Write returns %d: %s", r, estr);
|
||||
}
|
||||
if (r < 0){
|
||||
switch (errno){
|
||||
case MBEDTLS_ERR_SSL_WANT_WRITE: return 0; break;
|
||||
case MBEDTLS_ERR_SSL_WANT_READ: return 0; break;
|
||||
case EWOULDBLOCK: return 0; break;
|
||||
default:
|
||||
Error = true;
|
||||
INSANE_MSG("Could not iwrite data! Error: %s", strerror(errno));
|
||||
close();
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r == 0 && (sSend >= 0)){
|
||||
DONTEVEN_MSG("Socket closed by remote");
|
||||
close();
|
||||
}
|
||||
up += r;
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
if (!connected() || len < 1){return 0;}
|
||||
if (skipCount){
|
||||
// We have bytes to skip writing.
|
||||
|
@ -766,6 +953,38 @@ unsigned int Socket::Connection::iwrite(const void *buffer, int len){
|
|||
/// \param flags Flags to use in the recv call. Ignored on fake sockets.
|
||||
/// \returns The amount of bytes actually read.
|
||||
int Socket::Connection::iread(void *buffer, int len, int flags){
|
||||
#ifdef SSL
|
||||
if (sslConnected){
|
||||
DONTEVEN_MSG("SSL iread");
|
||||
if (!connected() || len < 1){return 0;}
|
||||
int r;
|
||||
/// \TODO Flags ignored... Bad.
|
||||
r = mbedtls_ssl_read(ssl, (unsigned char *)buffer, len);
|
||||
if (r < 0){
|
||||
switch (errno){
|
||||
case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY: close(); return 0; break;
|
||||
case MBEDTLS_ERR_SSL_WANT_WRITE: return 0; break;
|
||||
case MBEDTLS_ERR_SSL_WANT_READ: return 0; break;
|
||||
case EWOULDBLOCK: return 0; break;
|
||||
case EINTR: return 0; break;
|
||||
default:
|
||||
Error = true;
|
||||
char estr[200];
|
||||
mbedtls_strerror(r, estr, 200);
|
||||
INFO_MSG("Read returns %d: %s (%s)", r, estr, strerror(errno));
|
||||
close();
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r == 0){
|
||||
DONTEVEN_MSG("Socket closed by remote");
|
||||
close();
|
||||
}
|
||||
down += r;
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
if (!connected() || len < 1){return 0;}
|
||||
int r;
|
||||
if (sRecv != -1 || !isTrueSocket){
|
||||
|
@ -873,6 +1092,56 @@ Socket::Connection::operator bool() const{
|
|||
return connected();
|
||||
}
|
||||
|
||||
//Copy constructor
|
||||
Socket::Connection::Connection(const Connection& rhs){
|
||||
clear();
|
||||
if (!rhs){return;}
|
||||
#if DEBUG >= DLVL_DEVEL
|
||||
INFO_MSG("Copying %s socket", rhs.sslConnected?"SSL":"regular");
|
||||
BACKTRACE;
|
||||
#endif
|
||||
conntime = rhs.conntime;
|
||||
isTrueSocket = rhs.isTrueSocket;
|
||||
remotehost = rhs.remotehost;
|
||||
boundaddr = rhs.boundaddr;
|
||||
up = rhs.up;
|
||||
down = rhs.down;
|
||||
downbuffer = rhs.downbuffer;
|
||||
if (!rhs.sslConnected){
|
||||
if (rhs.sSend >= 0){sSend = dup(rhs.sSend);}
|
||||
if (rhs.sRecv >= 0){sRecv = dup(rhs.sRecv);}
|
||||
#if DEBUG >= DLVL_DEVEL
|
||||
INFO_MSG("Socket original = (%d / %d), copy = (%d / %d)", rhs.sSend, rhs.sRecv, sSend, sRecv);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
//Assignment constructor
|
||||
Socket::Connection& Socket::Connection::operator=(const Socket::Connection& rhs){
|
||||
drop();
|
||||
clear();
|
||||
if (!rhs){return *this;}
|
||||
#if DEBUG >= DLVL_DEVEL
|
||||
INFO_MSG("Assigning %s socket", rhs.sslConnected?"SSL":"regular");
|
||||
BACKTRACE;
|
||||
#endif
|
||||
conntime = rhs.conntime;
|
||||
isTrueSocket = rhs.isTrueSocket;
|
||||
remotehost = rhs.remotehost;
|
||||
boundaddr = rhs.boundaddr;
|
||||
up = rhs.up;
|
||||
down = rhs.down;
|
||||
downbuffer = rhs.downbuffer;
|
||||
if (!rhs.sslConnected){
|
||||
if (rhs.sSend >= 0){sSend = dup(rhs.sSend);}
|
||||
if (rhs.sRecv >= 0){sRecv = dup(rhs.sRecv);}
|
||||
#if DEBUG >= DLVL_DEVEL
|
||||
INFO_MSG("Socket original = (%d / %d), copy = (%d / %d)", rhs.sSend, rhs.sRecv, sSend, sRecv);
|
||||
#endif
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Returns true if the given address can be matched with the remote host.
|
||||
/// Can no longer return true after any socket error have occurred.
|
||||
bool Socket::Connection::isAddress(const std::string &addr){
|
||||
|
@ -885,207 +1154,6 @@ bool Socket::Connection::isLocal(){
|
|||
return Socket::isLocal(remotehost);
|
||||
}
|
||||
|
||||
#ifdef SSL
|
||||
Socket::SSLConnection::SSLConnection() : Socket::Connection::Connection(){
|
||||
isConnected = false;
|
||||
server_fd = 0;
|
||||
ssl = 0;
|
||||
conf = 0;
|
||||
ctr_drbg = 0;
|
||||
entropy = 0;
|
||||
}
|
||||
|
||||
static void my_debug(void *ctx, int level, const char *file, int line, const char *str){
|
||||
((void)level);
|
||||
fprintf((FILE *)ctx, "%s:%04d: %s", file, line, str);
|
||||
fflush((FILE *)ctx);
|
||||
}
|
||||
|
||||
Socket::SSLConnection::SSLConnection(std::string hostname, int port, bool nonblock)
|
||||
: Socket::Connection(){
|
||||
mbedtls_debug_set_threshold(0);
|
||||
isConnected = true;
|
||||
server_fd = new mbedtls_net_context;
|
||||
ssl = new mbedtls_ssl_context;
|
||||
conf = new mbedtls_ssl_config;
|
||||
ctr_drbg = new mbedtls_ctr_drbg_context;
|
||||
entropy = new mbedtls_entropy_context;
|
||||
mbedtls_net_init(server_fd);
|
||||
mbedtls_ssl_init(ssl);
|
||||
mbedtls_ssl_config_init(conf);
|
||||
mbedtls_ctr_drbg_init(ctr_drbg);
|
||||
mbedtls_entropy_init(entropy);
|
||||
DONTEVEN_MSG("SSL init");
|
||||
if (mbedtls_ctr_drbg_seed(ctr_drbg, mbedtls_entropy_func, entropy, (const unsigned char *)"meow",
|
||||
4) != 0){
|
||||
FAIL_MSG("SSL socket init failed");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
DONTEVEN_MSG("SSL connect");
|
||||
int ret = 0;
|
||||
if ((ret = mbedtls_net_connect(server_fd, hostname.c_str(), JSON::Value(port).asString().c_str(), MBEDTLS_NET_PROTO_TCP)) != 0){
|
||||
FAIL_MSG(" failed\n ! mbedtls_net_connect returned %d\n\n", ret);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if ((ret = mbedtls_ssl_config_defaults(conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
|
||||
MBEDTLS_SSL_PRESET_DEFAULT)) != 0){
|
||||
FAIL_MSG(" failed\n ! mbedtls_ssl_config_defaults returned %d\n\n", ret);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
mbedtls_ssl_conf_authmode(conf, MBEDTLS_SSL_VERIFY_NONE);
|
||||
mbedtls_ssl_conf_rng(conf, mbedtls_ctr_drbg_random, ctr_drbg);
|
||||
mbedtls_ssl_conf_dbg(conf, my_debug, stderr);
|
||||
if ((ret = mbedtls_ssl_setup(ssl, conf)) != 0){
|
||||
char estr[200];
|
||||
mbedtls_strerror(ret, estr, 200);
|
||||
FAIL_MSG("SSL setup error %d: %s", ret, estr);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if ((ret = mbedtls_ssl_set_hostname(ssl, hostname.c_str())) != 0){
|
||||
FAIL_MSG(" failed\n ! mbedtls_ssl_set_hostname returned %d\n\n", ret);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
mbedtls_ssl_set_bio(ssl, server_fd, mbedtls_net_send, mbedtls_net_recv, NULL);
|
||||
while ((ret = mbedtls_ssl_handshake(ssl)) != 0){
|
||||
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE){
|
||||
char estr[200];
|
||||
mbedtls_strerror(ret, estr, 200);
|
||||
FAIL_MSG("SSL handshake error %d: %s", ret, estr);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
Blocking = true;
|
||||
if (nonblock){setBlocking(false);}
|
||||
}
|
||||
|
||||
void Socket::SSLConnection::close(){
|
||||
DONTEVEN_MSG("SSL close");
|
||||
if (server_fd){
|
||||
mbedtls_net_free(server_fd);
|
||||
delete server_fd;
|
||||
server_fd = 0;
|
||||
}
|
||||
if (ssl){
|
||||
mbedtls_ssl_free(ssl);
|
||||
delete ssl;
|
||||
ssl = 0;
|
||||
}
|
||||
if (conf){
|
||||
mbedtls_ssl_config_free(conf);
|
||||
delete conf;
|
||||
conf = 0;
|
||||
}
|
||||
if (ctr_drbg){
|
||||
mbedtls_ctr_drbg_free(ctr_drbg);
|
||||
delete ctr_drbg;
|
||||
ctr_drbg = 0;
|
||||
}
|
||||
if (entropy){
|
||||
mbedtls_entropy_free(entropy);
|
||||
delete entropy;
|
||||
entropy = 0;
|
||||
}
|
||||
isConnected = false;
|
||||
}
|
||||
|
||||
/// Incremental read call. This function tries to read len bytes to the buffer from the socket,
|
||||
/// returning the amount of bytes it actually read.
|
||||
/// \param buffer Location of the buffer to read to.
|
||||
/// \param len Amount of bytes to read.
|
||||
/// \param flags Flags to use in the recv call. Ignored on fake sockets.
|
||||
/// \returns The amount of bytes actually read.
|
||||
int Socket::SSLConnection::iread(void *buffer, int len, int flags){
|
||||
DONTEVEN_MSG("SSL iread");
|
||||
if (!connected() || len < 1){return 0;}
|
||||
int r;
|
||||
/// \TODO Flags ignored... Bad.
|
||||
r = mbedtls_ssl_read(ssl, (unsigned char *)buffer, len);
|
||||
if (r < 0){
|
||||
char estr[200];
|
||||
mbedtls_strerror(r, estr, 200);
|
||||
INFO_MSG("Read returns %d: %s", r, estr);
|
||||
}
|
||||
if (r < 0){
|
||||
switch (errno){
|
||||
case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY: close(); return 0; break;
|
||||
case MBEDTLS_ERR_SSL_WANT_WRITE: return 0; break;
|
||||
case MBEDTLS_ERR_SSL_WANT_READ: return 0; break;
|
||||
case EWOULDBLOCK: return 0; break;
|
||||
case EINTR: return 0; break;
|
||||
default:
|
||||
Error = true;
|
||||
INSANE_MSG("Could not iread data! Error: %s", strerror(errno));
|
||||
close();
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r == 0){
|
||||
DONTEVEN_MSG("Socket closed by remote");
|
||||
close();
|
||||
}
|
||||
down += r;
|
||||
return r;
|
||||
}
|
||||
|
||||
/// Incremental write call. This function tries to write len bytes to the socket from the buffer,
|
||||
/// returning the amount of bytes it actually wrote.
|
||||
/// \param buffer Location of the buffer to write from.
|
||||
/// \param len Amount of bytes to write.
|
||||
/// \returns The amount of bytes actually written.
|
||||
unsigned int Socket::SSLConnection::iwrite(const void *buffer, int len){
|
||||
DONTEVEN_MSG("SSL iwrite");
|
||||
if (!connected() || len < 1){return 0;}
|
||||
int r;
|
||||
r = mbedtls_ssl_write(ssl, (const unsigned char *)buffer, len);
|
||||
if (r < 0){
|
||||
char estr[200];
|
||||
mbedtls_strerror(r, estr, 200);
|
||||
INFO_MSG("Write returns %d: %s", r, estr);
|
||||
}
|
||||
if (r < 0){
|
||||
switch (errno){
|
||||
case MBEDTLS_ERR_SSL_WANT_WRITE: return 0; break;
|
||||
case MBEDTLS_ERR_SSL_WANT_READ: return 0; break;
|
||||
case EWOULDBLOCK: return 0; break;
|
||||
default:
|
||||
Error = true;
|
||||
INSANE_MSG("Could not iwrite data! Error: %s", strerror(errno));
|
||||
close();
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r == 0 && (sSend >= 0)){
|
||||
DONTEVEN_MSG("Socket closed by remote");
|
||||
close();
|
||||
}
|
||||
up += r;
|
||||
return r;
|
||||
}
|
||||
|
||||
bool Socket::SSLConnection::connected() const{
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
void Socket::SSLConnection::setBlocking(bool blocking){
|
||||
if (blocking != Blocking){return;}
|
||||
if (blocking){
|
||||
mbedtls_net_set_block(server_fd);
|
||||
Blocking = true;
|
||||
}else{
|
||||
mbedtls_net_set_nonblock(server_fd);
|
||||
Blocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// Create a new base Server. The socket is never connected, and a placeholder for later
|
||||
/// connections.
|
||||
|
|
56
lib/socket.h
56
lib/socket.h
|
@ -71,6 +71,7 @@ namespace Socket{
|
|||
/// If they are not identical and sRecv is closed but sSend still open, reading from sSend will be attempted.
|
||||
class Connection{
|
||||
protected:
|
||||
void clear(); ///< Clears the internal data structure. Meant only for use in constructors.
|
||||
bool isTrueSocket;
|
||||
int sSend; ///< Write end of socket.
|
||||
int sRecv; ///< Read end of socket.
|
||||
|
@ -81,24 +82,46 @@ namespace Socket{
|
|||
uint64_t down;
|
||||
long long int conntime;
|
||||
Buffer downbuffer; ///< Stores temporary data coming in.
|
||||
virtual int iread(void *buffer, int len, int flags = 0); ///< Incremental read call.
|
||||
virtual unsigned int iwrite(const void *buffer, int len); ///< Incremental write call.
|
||||
int iread(void *buffer, int len, int flags = 0); ///< Incremental read call.
|
||||
unsigned int iwrite(const void *buffer, int len); ///< Incremental write call.
|
||||
bool iread(Buffer &buffer, int flags = 0); ///< Incremental write call that is compatible with Socket::Buffer.
|
||||
bool iwrite(std::string &buffer); ///< Write call that is compatible with std::string.
|
||||
void setBoundAddr();
|
||||
#ifdef SSL
|
||||
/// optional extension that uses mbedtls for SSL
|
||||
protected:
|
||||
bool sslConnected;
|
||||
int ssl_iread(void *buffer, int len, int flags = 0); ///< Incremental read call.
|
||||
unsigned int ssl_iwrite(const void *buffer, int len); ///< Incremental write call.
|
||||
mbedtls_net_context * server_fd;
|
||||
mbedtls_entropy_context * entropy;
|
||||
mbedtls_ctr_drbg_context * ctr_drbg;
|
||||
mbedtls_ssl_context * ssl;
|
||||
mbedtls_ssl_config * conf;
|
||||
#endif
|
||||
|
||||
public:
|
||||
// friends
|
||||
friend class ::Buffer::user;
|
||||
// constructors
|
||||
Connection(); ///< Create a new disconnected base socket.
|
||||
Connection(int sockNo); ///< Create a new base socket.
|
||||
Connection(std::string hostname, int port, bool nonblock); ///< Create a new TCP socket.
|
||||
Connection(std::string hostname, int port, bool nonblock, bool with_ssl = false); ///< Create a new TCP socket.
|
||||
Connection(std::string adres, bool nonblock = false); ///< Create a new Unix Socket.
|
||||
Connection(int write, int read); ///< Simulate a socket using two file descriptors.
|
||||
// copy/assignment constructors
|
||||
Connection(const Connection& rhs);
|
||||
Connection& operator=(const Connection& rhs);
|
||||
// destructor
|
||||
~Connection();
|
||||
// generic methods
|
||||
virtual void close(); ///< Close connection.
|
||||
void open(int sockNo);//Open from existing socket connection.
|
||||
void open(std::string hostname, int port, bool nonblock, bool with_ssl = false);//Open TCP connection.
|
||||
void open(std::string adres, bool nonblock = false);//Open Unix connection.
|
||||
void open(int write, int read);//Open from two existing file descriptors.
|
||||
void close(); ///< Close connection.
|
||||
void drop(); ///< Close connection without shutdown.
|
||||
virtual void setBlocking(bool blocking); ///< Set this socket to be blocking (true) or nonblocking (false).
|
||||
void setBlocking(bool blocking); ///< Set this socket to be blocking (true) or nonblocking (false).
|
||||
bool isBlocking(); ///< Check if this socket is blocking (true) or nonblocking (false).
|
||||
std::string getHost() const; ///< Gets hostname for connection, if available.
|
||||
std::string getBinHost();
|
||||
|
@ -107,7 +130,7 @@ namespace Socket{
|
|||
int getSocket(); ///< Returns internal socket number.
|
||||
int getPureSocket(); ///< Returns non-piped internal socket number.
|
||||
std::string getError(); ///< Returns a string describing the last error that occured.
|
||||
virtual bool connected() const; ///< Returns the connected-state for this socket.
|
||||
bool connected() const; ///< Returns the connected-state for this socket.
|
||||
bool isAddress(const std::string &addr);
|
||||
bool isLocal(); ///< Returns true if remote address is a local address.
|
||||
// buffered i/o methods
|
||||
|
@ -136,27 +159,6 @@ namespace Socket{
|
|||
operator bool() const;
|
||||
};
|
||||
|
||||
#ifdef SSL
|
||||
/// Version of Socket::Connection that uses mbedtls for SSL
|
||||
class SSLConnection : public Connection{
|
||||
public:
|
||||
SSLConnection();
|
||||
SSLConnection(std::string hostname, int port, bool nonblock); ///< Create a new TCP socket.
|
||||
void close(); ///< Close connection.
|
||||
bool connected() const; ///< Returns the connected-state for this socket.
|
||||
void setBlocking(bool blocking); ///< Set this socket to be blocking (true) or nonblocking (false).
|
||||
protected:
|
||||
bool isConnected;
|
||||
int iread(void *buffer, int len, int flags = 0); ///< Incremental read call.
|
||||
unsigned int iwrite(const void *buffer, int len); ///< Incremental write call.
|
||||
mbedtls_net_context * server_fd;
|
||||
mbedtls_entropy_context * entropy;
|
||||
mbedtls_ctr_drbg_context * ctr_drbg;
|
||||
mbedtls_ssl_context * ssl;
|
||||
mbedtls_ssl_config * conf;
|
||||
};
|
||||
#endif
|
||||
|
||||
/// This class is for easily setting up listening socket, either TCP or Unix.
|
||||
class Server{
|
||||
private:
|
||||
|
|
|
@ -529,9 +529,13 @@ namespace TS {
|
|||
/// \param len The length of this frame.
|
||||
/// \param PTS The timestamp of the frame.
|
||||
std::string & Packet::getPESVideoLeadIn(unsigned int len, unsigned long long PTS, unsigned long long offset, bool isAligned, uint64_t bps) {
|
||||
if (len){
|
||||
len += (offset ? 13 : 8);
|
||||
}
|
||||
if (bps >= 50){
|
||||
if (len){
|
||||
len += 3;
|
||||
}
|
||||
}else{
|
||||
bps = 0;
|
||||
}
|
||||
|
@ -828,7 +832,7 @@ namespace TS {
|
|||
case 0x15: return "meta PES";
|
||||
case 0x16: return "meta section";
|
||||
case 0x1B: return "H264";
|
||||
case 0x24: return "H265";
|
||||
case 0x24: return "HEVC";
|
||||
case 0x81: return "AC3";
|
||||
default: return "unknown";
|
||||
}
|
||||
|
@ -1232,6 +1236,7 @@ namespace TS {
|
|||
}else if (myMeta.tracks[*it].codec == "MP3"){
|
||||
entry.setStreamType(0x03);
|
||||
}else if (myMeta.tracks[*it].codec == "ID3"){
|
||||
entry.setStreamType(0x15);
|
||||
entry.setESInfo(myMeta.tracks[*it].init);
|
||||
}
|
||||
entry.advance();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue