Compare commits

...
Author SHA1 Message Date
Andreas Traczyk 538051b5e3 deps: win32: migrate from http_parser to llhttp 2024-11-26 10:41:41 -05:00
Adrien Béraud cdba5b33cc proxy server: add options to listen route 2024-11-20 11:40:12 -05:00
Adrien Béraud 81bd05fe84 proxy node: remove jquery, update for new APIs 2024-11-20 11:40:12 -05:00
Adrien Béraud cf135d18b4 c node: fix typo 2024-11-20 11:40:12 -05:00
Adrien Béraud c7ba7f6703 utils: use fmt::format for time 2024-11-20 11:40:12 -05:00
Adrien Beraud c73c8c77f2 search: combine callbacks in case of multiple equivalent puts 2024-11-20 11:39:19 -05:00
Adrien Beraud c3bb16e92f tests: add testPutDuplicate 2024-11-20 11:39:19 -05:00
15 changed files with 372 additions and 246 deletions

No files matched your search

+5 -5
View File
@@ -122,7 +122,7 @@ if (NOT MSVC)
endif()
endif()
set(http_lib "-lllhttp")
if (NOT Jsoncpp_FOUND)
message(SEND_ERROR "Jsoncpp is required for DHT proxy support")
endif()
@@ -156,7 +156,7 @@ else ()
${WIN32_DEP_DIR}/openssl/include
${WIN32_DEP_DIR}/restinio/dev
${WIN32_DEP_DIR}/fmt/include
${WIN32_DEP_DIR}/http_parser
${WIN32_DEP_DIR}/llhttp/include
)
endif ()
endif ()
@@ -343,12 +343,12 @@ if (MSVC)
)
if (OPENDHT_HTTP)
add_obj_lib (win32_fmt ${WIN32_DEP_DIR}/fmt/msvc/Release/fmt.lib)
add_obj_lib (win32_http_parser ${WIN32_DEP_DIR}/http_parser/x64/Release/http-parser.lib)
add_obj_lib (win32_llhttp ${WIN32_DEP_DIR}/llhttp/build/Release/llhttp.lib)
add_obj_lib (win32_ssl ${WIN32_DEP_DIR}/openssl/libssl_static.lib)
add_obj_lib (win32_crypto ${WIN32_DEP_DIR}/openssl/libcrypto_static.lib)
list (APPEND obj_libs
$<TARGET_OBJECTS:win32_fmt>
$<TARGET_OBJECTS:win32_http_parser>
$<TARGET_OBJECTS:win32_llhttp>
$<TARGET_OBJECTS:win32_ssl>
$<TARGET_OBJECTS:win32_crypto>
)
@@ -361,7 +361,7 @@ if (MSVC)
)
list (APPEND win32_Libs
${PROJECT_SOURCE_DIR}/../fmt/msvc/Release/fmt.lib
${PROJECT_SOURCE_DIR}/../http_parser/x64/Release/http-parser.lib
${PROJECT_SOURCE_DIR}/../llhttp/build/Release/llhttp.lib
${PROJECT_SOURCE_DIR}/../openssl/libssl.lib
${PROJECT_SOURCE_DIR}/../openssl/libcrypto.lib
)
+10 -10
View File
@@ -150,22 +150,22 @@ public:
std::shared_ptr<NodeInfo> nodeInfo {};
std::string toString() const {
std::ostringstream ss;
ss << "Listens: " << listenCount << " Puts: " << putCount << " PushListeners: " << pushListenersCount << std::endl;
ss << "Push requests in the last " << print_duration(lastUpdated - serverStartTime) << ": "
<< "[Android: " << androidPush.toString() << "], "
<< "[iOS: " << iosPush.toString() << "], "
<< "[Unified: " << unifiedPush.toString() << "]" << std::endl;
ss << "Requests: " << requestRate << " per second." << std::endl;
auto ret = fmt::format("Listens: {}, Puts: {}, PushListeners: {}\n"
"Push requests in the last {}: [Android: {}], [iOS: {}], [Unified: {}]\n"
"Requests: {} per second.",
listenCount, putCount, pushListenersCount,
print_duration(lastUpdated - serverStartTime),
androidPush.toString(), iosPush.toString(), unifiedPush.toString(),
requestRate);
if (nodeInfo) {
auto& ipv4 = nodeInfo->ipv4;
if (ipv4.table_depth > 1)
ss << "IPv4 Network estimation: " << ipv4.getNetworkSizeEstimation() << std::endl;;
ret += fmt::format("IPv4 Network estimation: {}\n", ipv4.getNetworkSizeEstimation());
auto& ipv6 = nodeInfo->ipv6;
if (ipv6.table_depth > 1)
ss << "IPv6 Network estimation: " << ipv6.getNetworkSizeEstimation() << std::endl;;
ret += fmt::format("IPv6 Network estimation: {}\n", ipv6.getNetworkSizeEstimation());
}
return ss.str();
return ret;
}
/**
+1 -1
View File
@@ -72,7 +72,7 @@ class OPENDHT_PUBLIC Url
{
public:
Url() = default;
Url(const std::string& url);
Url(std::string_view url);
std::string url;
std::string protocol {"http"};
std::string host;
+9 -22
View File
@@ -21,6 +21,7 @@
#include "def.h"
#include <msgpack.hpp>
#include <fmt/format.h>
#include <chrono>
#include <random>
@@ -60,7 +61,7 @@ void erase_if(std::map<Key, Item>& map, const Condition& condition)
* Split "[host]:port" or "host:port" to pair<"host", "port">.
*/
OPENDHT_PUBLIC std::pair<std::string, std::string>
splitPort(const std::string& s);
splitPort(std::string_view s);
class OPENDHT_PUBLIC DhtException : public std::runtime_error {
public:
@@ -84,37 +85,23 @@ using duration = clock::duration;
time_point from_time_t(std::time_t t);
std::time_t to_time_t(time_point t);
inline std::string
to_str(double d) {
char buf[16];
auto ret = snprintf(buf, sizeof(buf), "%.3g", d);
return (ret < 0) ? std::to_string(d) : std::string(buf, ret);
}
/**
* Converts std::chrono::duration to floating-point seconds.
*/
template <class DT>
static double
print_dt(DT d) {
return std::chrono::duration_cast<std::chrono::duration<double>>(d).count();
}
template <class DT>
static std::string
print_duration(DT d) {
if (d < std::chrono::seconds(0)) {
return "-" + print_duration(-d);
} else if (d < std::chrono::milliseconds(1)) {
return to_str(std::chrono::duration_cast<std::chrono::duration<double, std::micro>>(d).count()) + " us";
return fmt::format("{:.3g} us", std::chrono::duration_cast<std::chrono::duration<double, std::micro>>(d).count());
} else if (d < std::chrono::seconds(1)) {
return to_str(std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(d).count()) + " ms";
return fmt::format("{:.3g} ms", std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(d).count());
} else if (d < std::chrono::minutes(1)) {
return to_str(std::chrono::duration_cast<std::chrono::duration<double>>(d).count()) + " s";
return fmt::format("{:.3g} s", std::chrono::duration_cast<std::chrono::duration<double>>(d).count());
} else if (d < std::chrono::hours(1)) {
return to_str(std::chrono::duration_cast<std::chrono::duration<double, std::ratio<60>>>(d).count()) + " min";
return fmt::format("{:.3g} min", std::chrono::duration_cast<std::chrono::duration<double, std::ratio<60>>>(d).count());
} else if (d < std::chrono::hours(72)) {
return fmt::format("{:.3g} h", std::chrono::duration_cast<std::chrono::duration<double, std::ratio<3600>>>(d).count());
} else {
return to_str(std::chrono::duration_cast<std::chrono::duration<double, std::ratio<3600>>>(d).count()) + " h";
return fmt::format("{:.3g} days", std::chrono::duration_cast<std::chrono::duration<double, std::ratio<86400>>>(d).count());
}
}
+2 -2
View File
@@ -93,7 +93,7 @@ Dht::shutdown(ShutdownCallback cb, bool stop)
r.second.done_cb(false, {});
sr.second->callbacks.clear();
for (const auto& a : sr.second->announce) {
if (a.callback) a.callback(false, {});
for (auto& cb : a.callbacks) cb(false, {});
}
sr.second->announce.clear();
sr.second->listeners.clear();
@@ -904,7 +904,7 @@ Dht::listenTo(const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filte
{
if (!isRunning(af))
return 0;
// logger__ERR("[search %s IPv%c] search_time is now in %lfs", sr->id.toString().c_str(), (sr->af == AF_INET) ? '4' : '6', print_dt(tm-clock::now()));
// logger__ERR("[search %s IPv%c] search_time is now in %lfs", sr->id.toString().c_str(), (sr->af == AF_INET) ? '4' : '6', print_duration(tm-clock::now()));
//logger__WARN("listenTo %s", id.toString().c_str());
auto& srs = searches(af);
+2 -2
View File
@@ -813,8 +813,8 @@ SockAddr
DhtProxyClient::parsePublicAddress(const Json::Value& val)
{
auto public_ip = val.asString();
auto hostAndService = splitPort(public_ip);
auto sa = SockAddr::resolve(hostAndService.first);
auto [host, service] = splitPort(public_ip);
auto sa = SockAddr::resolve(host);
if (sa.empty()) return {};
return sa.front().getMappedIPv4();
}
+2
View File
@@ -631,6 +631,8 @@ DhtProxyServer::createRestRouter()
router->http_get("/key/:hash", std::bind(&DhtProxyServer::get, this, _1, _2));
// key.post
router->http_post("/key/:hash", std::bind(&DhtProxyServer::put, this, _1, _2));
router->add_handler(restinio::http_method_options(),
"/key/:hash/listen", std::bind(&DhtProxyServer::options, this, _1, _2));
// key.listen
router->http_get("/key/:hash/listen", std::bind(&DhtProxyServer::listen, this, _1, _2));
#ifdef OPENDHT_PUSH_NOTIFICATIONS
+4 -5
View File
@@ -54,7 +54,7 @@ constexpr const char HTTPS_PROTOCOL[] = "https://";
constexpr const char ORIGIN_PROTOCOL[] = "//";
constexpr unsigned MAX_REDIRECTS {5};
Url::Url(const std::string& url): url(url)
Url::Url(std::string_view url): url(url)
{
size_t addr_begin = 0;
// protocol
@@ -69,10 +69,9 @@ Url::Url(const std::string& url): url(url)
size_t addr_size = url.substr(addr_begin).find("/");
if (addr_size == std::string::npos)
addr_size = url.size() - addr_begin;
auto host_service = splitPort(url.substr(addr_begin, addr_size));
host = host_service.first;
if (!host_service.second.empty())
service = host_service.second;
auto [h, s] = splitPort(url.substr(addr_begin, addr_size));
host = std::move(h);
service = std::move(s);
// target, query and fragment
size_t query_begin = url.find("?");
auto addr_end = addr_begin + addr_size;
+26 -19
View File
@@ -45,7 +45,7 @@ struct Dht::Announce {
bool permanent;
Sp<Value> value;
time_point created;
DoneCallback callback;
std::vector<DoneCallback> callbacks;
};
struct Dht::SearchNode {
@@ -442,8 +442,9 @@ struct Dht::Search {
g.second.done_cb = {};
}
for (auto& a : announce) {
a.callback(false, {});
a.callback = {};
for (auto& cb : a.callbacks)
cb(false, {});
a.callbacks.clear();
}
}
@@ -627,7 +628,9 @@ struct Dht::Search {
return a.value->id == value->id;
});
if (a_sr == announce.end()) {
announce.emplace_back(Announce {permanent, value, created, callback});
auto& a = announce.emplace_back(Announce {permanent, value, created, {}} );
if (callback)
a.callbacks.emplace_back(std::move(callback));
for (auto& n : nodes) {
n->probe_query.reset();
n->acked[value->id].req.reset();
@@ -635,24 +638,26 @@ struct Dht::Search {
} else {
a_sr->permanent = permanent;
a_sr->created = created;
if (a_sr->value != value) {
if (a_sr->value != value && *a_sr->value != *value) {
// Value is updated, previous ops are failed
a_sr->value = value;
for (auto& n : nodes) {
n->acked[value->id].req.reset();
n->probe_query.reset();
}
}
if (isAnnounced(value->id)) {
if (a_sr->callback)
a_sr->callback(true, {});
a_sr->callback = {};
for (auto& cb: a_sr->callbacks)
cb(false, {});
a_sr->callbacks.clear();
if (callback)
a_sr->callbacks.emplace_back(std::move(callback));
} else if (isAnnounced(value->id)) {
// Same value, already announced
if (callback)
callback(true, {});
return;
} else {
if (a_sr->callback)
a_sr->callback(false, {});
a_sr->callback = callback;
// Same value, not announced yet
if (callback)
a_sr->callbacks.emplace_back(std::move(callback));
}
}
}
@@ -722,8 +727,8 @@ struct Dht::Search {
std::vector<DoneCallback> a_cbs;
a_cbs.reserve(announce.size());
for (auto ait = announce.begin() ; ait != announce.end(); ) {
if (ait->callback)
a_cbs.emplace_back(std::move(ait->callback));
a_cbs.insert(a_cbs.end(), std::make_move_iterator(ait->callbacks.begin()), std::make_move_iterator(ait->callbacks.end()));
ait->callbacks.clear();
if (not ait->permanent)
ait = announce.erase(ait);
else
@@ -747,9 +752,11 @@ struct Dht::Search {
if (vid != Value::INVALID_ID and (!a.value || a.value->id != vid))
return true;
if (isAnnounced(a.value->id)) {
if (a.callback) {
a.callback(true, getNodes());
a.callback = nullptr;
if (!a.callbacks.empty()) {
const auto& nodes = getNodes();
for (auto& cb : a.callbacks)
cb(true, nodes);
a.callbacks.clear();
}
if (not a.permanent)
return false;
+9 -9
View File
@@ -44,23 +44,23 @@ const HexMap hex_map = {};
static constexpr std::array<uint8_t, 12> MAPPED_IPV4_PREFIX {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}};
std::pair<std::string, std::string>
splitPort(const std::string& s) {
splitPort(std::string_view s) {
if (s.empty())
return {};
if (s[0] == '[') {
std::size_t closure = s.find_first_of(']');
std::size_t found = s.find_last_of(':');
if (closure == std::string::npos)
return {s, ""};
if (found == std::string::npos or found < closure)
return {s.substr(1,closure-1), ""};
return {s.substr(1,closure-1), s.substr(found+1)};
if (closure == std::string_view::npos)
return {std::string(s), ""};
if (found == std::string_view::npos or found < closure)
return {std::string(s.substr(1,closure-1)), ""};
return {std::string(s.substr(1,closure-1)), std::string(s.substr(found+1))};
}
std::size_t found = s.find_last_of(':');
std::size_t first = s.find_first_of(':');
if (found == std::string::npos or found != first)
return {s, ""};
return {s.substr(0,found), s.substr(found+1)};
if (found == std::string_view::npos or found != first)
return {std::string(s), ""};
return {std::string(s.substr(0,found)), std::string(s.substr(found+1))};
}
std::vector<SockAddr>
+53
View File
@@ -90,6 +90,59 @@ DhtRunnerTester::testGetPut() {
CPPUNIT_ASSERT(vals.front()->data == val_data);
}
void
DhtRunnerTester::testPutDuplicate() {
auto key = dht::InfoHash::get("123");
auto val = std::make_shared<dht::Value>("hey");
val->id = 42;
auto val_data = val->data;
std::promise<bool> p1;
std::promise<bool> p2;
node2.put(key, val, [&](bool ok){
p1.set_value(ok);
});
node2.put(key, val, [&](bool ok){
p2.set_value(ok);
});
auto p1ret = p1.get_future().get();
auto p2ret = p2.get_future().get();
CPPUNIT_ASSERT(p1ret);
CPPUNIT_ASSERT(p2ret);
auto vals = node1.get(key).get();
CPPUNIT_ASSERT(not vals.empty());
CPPUNIT_ASSERT(vals.size() == 1);
CPPUNIT_ASSERT(vals.front()->data == val_data);
}
void
DhtRunnerTester::testPutOverride() {
auto key = dht::InfoHash::get("123");
auto val = std::make_shared<dht::Value>("meh");
val->id = 42;
auto val2 = std::make_shared<dht::Value>("hey");
val2->id = 42;
CPPUNIT_ASSERT_EQUAL(val->id, val2->id);
auto val_data = val2->data;
std::promise<bool> p1;
std::promise<bool> p2;
node2.put(key, val, [&](bool ok){
p1.set_value(ok);
});
node2.put(key, val2, [&](bool ok){
p2.set_value(ok);
});
auto p1ret = p1.get_future().get();
auto p2ret = p2.get_future().get();
CPPUNIT_ASSERT(!p1ret);
CPPUNIT_ASSERT(p2ret);
auto vals = node1.get(key).get();
CPPUNIT_ASSERT(not vals.empty());
CPPUNIT_ASSERT(vals.size() == 1);
CPPUNIT_ASSERT(vals.front()->data == val_data);
}
void
DhtRunnerTester::testListen() {
std::mutex mutex;
+10
View File
@@ -31,6 +31,8 @@ class DhtRunnerTester : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(DhtRunnerTester);
CPPUNIT_TEST(testConstructors);
CPPUNIT_TEST(testGetPut);
CPPUNIT_TEST(testPutDuplicate);
CPPUNIT_TEST(testPutOverride);
CPPUNIT_TEST(testListen);
CPPUNIT_TEST(testListenLotOfBytes);
CPPUNIT_TEST(testIdOps);
@@ -55,6 +57,14 @@ class DhtRunnerTester : public CppUnit::TestFixture {
* Test get and put methods
*/
void testGetPut();
/**
* Test get and multiple put
*/
void testPutDuplicate();
/**
* Test get and multiple put with changing value
*/
void testPutOverride();
/**
* Test listen method
*/
+2 -2
View File
@@ -107,8 +107,8 @@ main(int argc, char **argv)
token = dht.listen<dht::ImMessage>(room, [&](dht::ImMessage&& msg) {
if (msg.from != myid)
std::cout << msg.from.toString() << " at " << printTime(msg.date)
<< " (took " << print_dt(std::chrono::system_clock::now() - std::chrono::system_clock::from_time_t(msg.date))
<< "s) " << (msg.to == myid ? "ENCRYPTED ":"") << ": " << msg.id << " - " << msg.msg << std::endl;
<< " (took " << print_duration(std::chrono::system_clock::now() - std::chrono::system_clock::from_time_t(msg.date))
<< ") " << (msg.to == myid ? "ENCRYPTED ":"") << ": " << msg.id << " - " << msg.msg << std::endl;
return true;
});
connected = true;
+1 -1
View File
@@ -265,7 +265,7 @@ int main(int argc, char **argv)
dht_value_unref(val);
}
else {
printf("Unkown command: %s\n", cmd);
printf("Unknown command: %s\n", cmd);
}
}
+236 -168
View File
@@ -1,190 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>OpenDHT tester</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script type="text/javascript">"use strict";
let onGet;
let onPut;
let setServer;
const valueGetElement = function(o) {
const d = window.atob(o.data);
return d;
};
$(function() {
let request = undefined;
let server;
const getTools = $("#getTools");
const getBtn = $("#getBtn");
const getDropdown = $("#getDropdown");
const listenBtn = $("#listenBtn").click(function(){onGet('LISTEN');});
const setGetRequest = function() {
getBtn.button('loading');
getStopBtn.appendTo(getTools);
getDropdown.hide();
}
const clearGetRequest = function() {
if (request === undefined)
return;
request.abort();
request = undefined;
getStopBtn.detach();
getDropdown.show();
getBtn.button('reset');
}
const getStopBtn = $("#getStopBtn").detach().click(clearGetRequest);
const putBtn = $("#putBtn");
const result = $("#dhtResult");
const group = $('<ul class="list-group"/>').appendTo(result);
onGet = function (method) {
if (request !== undefined)
return false;
const input = $("#getKey").val();
group.empty();
let lastAppended = 0;
let start = new Date().getTime();
request = new XMLHttpRequest();
request.onreadystatechange = function(event) {
if (this.readyState >= XMLHttpRequest.LOADING) {
if (this.readyState == XMLHttpRequest.DONE) {
clearGetRequest();
}
if (this.status === 200) {
const elements = this.responseText.split("\n");
const elementsLength = elements.length;
const now = new Date().getTime();
for (let i = lastAppended; i < elementsLength; i++) {
const element = elements[i];
if (!element || element.length == 0)
return;
const o = JSON.parse(element);
if (o.expired) {
$('#value'+o.id).slideUp(100, (e) => $(e.target).remove());
} else {
const d = window.atob(o.data);
const delay = Math.max(0, start-now);
$('<li class="list-group-item" id="value'+o.id+'"/>').append(valueGetElement(o)).appendTo(group).hide().delay(delay).slideDown(100);
lastAppended = i+1;
start = Math.max(start, now)+25;
}
}
} else if (this.status !== 0) {
group.empty().append($('<li class="list-group-item list-group-item-danger"/>').text("Error loading content: " + this.statusText));
}
}
};
request.onerror = function(event) {
clearGetRequest();
group.empty().append($('<li class="list-group-item list-group-item-danger"/>').text("Error loading content."));
};
request.open(method, server + input, true);
request.send(null);
setGetRequest();
return false;
};
onPut = function( ) {
const key = $("#getKey").val();
const value = $("#putValue").val();
$.ajax({
url: server + key,
type: 'POST',
data: JSON.stringify({
data:window.btoa(value)
}),
contentType: 'application/json; charset=utf-8',
dataType: 'json',
success: function( result ) {
putBtn.button('reset');
//$('<li class="list-group-item list-group-item-success"/>').append(valueGetElement(result)).appendTo(group.empty());
},
error: function(result) {
putBtn.button('reset');
group.empty().append($('<li class="list-group-item list-group-item-danger"/>').text(result.statusText));
}
});
putBtn.button('loading');
return false;
};
const serverValue = $("#serverValue");
const serverStatus = $("#serverStatus");
const serverBtn = $("#serverBtn");
setServer = function(event) {
server = serverValue.val() + '/';
serverStatus.empty();
serverBtn.button('loading');
$.getJSON(server, function(data){
$('<span><b>Node</b> '+data.node_id+'</span>').appendTo(serverStatus).hide().fadeIn();
}).fail(function(error) {
var message = " Cant' access node."
if (serverValue.val().indexOf("https") != -1){
message += "</br></br>Self-signed certificate must be allowed in browser."
}
serverStatus.html("<div class='alert alert-danger' style='margin-bottom: 0px;'>" +
"<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>" +
message + "</div>");
}).always(function(error) {
serverBtn.button('reset');
});
return false;
};
setServer();
});
</script>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>OpenDHT web node</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.3/css/bootstrap.min.css" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body>
<div class="container" style="max-width: 730px;">
<header class="page-header">
<div class="container" style="max-width: 880px;">
<header class="m-3 p-3">
<div class="row">
<div class="col-sm-5">
<h1>OpenDHT tester</h1>
<div class="col-sm-5 d-flex align-items-center">
<h2>OpenDHT web node</h2>
</div>
<div class="col-sm-7">
<div class="well well-sm" style="margin-top:10px; margin-bottom:0px;">
<form id="serverForm" class="form-inline" onsubmit="return setServer();" style="margin-bottom:4px;">
<div class="card card-body bg-light" style="margin-top:10px; margin-bottom:0px;">
<form id="serverForm" class="form-inline" onsubmit="return setServer();"
style="margin-bottom:4px;">
<div class="input-group">
<input type="text" class="form-control" id="serverValue" placeholder="Proxy server" value="http://127.0.0.1:8080"/>
<span class="input-group-btn">
<button id="serverBtn" type="submit" class="btn btn-default" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i>"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span></button>
</span>
<input type="text" class="form-control" id="serverValue" placeholder="Proxy server" value="http://127.0.0.1:8080" />
<button id="serverBtn" type="submit" class="btn btn-outline-secondary">
<div class='spinner-border spinner-border-sm' role='status'
style="display: none;">
<span class='visually-hidden'>Loading...</span>
</div>
<span class="btn-icon"><i class="bi bi-arrow-clockwise"></i></span>
</button>
</div>
</form>
<div id="serverStatus"><i class='fa fa-circle-o-notch fa-spin'></i></div>
<div id="serverStatus">
<div class='spinner-border spinner-border-sm' role='status'><span
class='visually-hidden'>Loading...</span></div>
</div>
</div>
</div>
</div>
</header>
<div class="panel panel-default" id="dhtResult">
<div class="panel-heading">
<div class="row">
<div class="col-xs-6">
<form class="form-inline" onsubmit="return onGet('GET');">
<div class="input-group">
<div class="list-group" id="dhtResult">
<div class="list-group-item list-group-item-light">
<div class="row">
<div class="col-sm-6">
<form class="form-inline" onsubmit="return onGet('');">
<div class="input-group" id="getTools">
<input type="text" class="form-control" id="getKey" placeholder="Key" aria-label="Key" />
<span class="input-group-btn" id="getTools">
<button id="getBtn" class="btn btn-default" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i>" type="submit">Get</button>
<button id="getDropdown"type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span> </button>
<ul class="dropdown-menu">
<li><a id="listenBtn" href="#">Listen</a></li>
</ul>
<button id="getStopBtn" class="btn btn-default" type="submit"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></button>
</span>
<button id="getBtn" class="btn btn-outline-secondary" type="submit">
<div class='spinner-border spinner-border-sm' role='status' style="display: none;">
<span class='visually-hidden'>Loading...</span>
</div>
<span class="btn-text">Get</span>
</button>
<button id="menuBtn" type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" id="listenBtn" href="#">Listen</a></li>
</ul>
<button id="getStopBtn" class="btn btn-outline-secondary">Stop</button>
</div>
</form>
</div>
<div class="col-xs-6">
<div class="col-sm-6">
<form class="form-inline" onsubmit="return onPut();">
<div class="input-group">
<input type="text" class="form-control input-group-input" id="putValue" placeholder="Value" />
<span class="input-group-btn">
<button id="putBtn" type="submit" class="btn btn-default" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> Loading">Put</button>
</span>
<input type="text" class="form-control input-group-input" id="putValue"
placeholder="Value" />
<button id="putBtn" type="submit" class="btn btn-outline-secondary">
<div class='spinner-border spinner-border-sm' role='status' style="display: none;">
<span class='visually-hidden'>Loading...</span>
</div>
<span class="btn-text">Put</span>
</button>
</div>
</form>
</div>
@@ -192,5 +80,185 @@ $(function() {
</div>
</div>
</div>
<script type="text/javascript">"use strict";
const valueGetElement = (o) => window.atob(o.data);
const getTools = document.getElementById("getTools");
const getBtn = document.getElementById("getBtn");
const menuBtn = document.getElementById("menuBtn");
const getStopBtn = document.getElementById("getStopBtn");
const listenBtn = document.getElementById("listenBtn");
const putBtn = document.getElementById("putBtn");
const group = document.getElementById("dhtResult");
const serverValue = document.getElementById("serverValue");
const serverStatus = document.getElementById("serverStatus");
const serverBtn = document.getElementById("serverBtn");
var request = undefined;
const setGetRequest = () => {
getBtn.disabled = true;
getBtn.querySelector('.spinner-border').style.display = 'inline-block';
getBtn.querySelector('.btn-text').style.display = 'none';
menuBtn.style.display = 'none';
getTools.appendChild(getStopBtn);
};
const clearGetRequest = () => {
if (request === undefined)
return;
request.abort();
request = undefined;
if (getStopBtn.parentNode) {
getStopBtn.parentNode.removeChild(getStopBtn);
}
menuBtn.style.display = '';
getBtn.disabled = false;
getBtn.querySelector('.spinner-border').style.display = 'none';
getBtn.querySelector('.btn-text').style.display = 'inline';
};
const clearResults = () => {
while (group.children.length > 1) {
group.removeChild(group.lastChild);
}
};
var server;
const setServer = (event) => {
server = serverValue.value + '/';
serverStatus.innerHTML = '';
serverBtn.disabled = true;
serverBtn.querySelector('.spinner-border').style.display = 'inline-block';
serverBtn.querySelector('.btn-icon').style.display = 'none';
var xhr = new XMLHttpRequest();
xhr.open('GET', server, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
serverBtn.disabled = false;
serverBtn.querySelector('.spinner-border').style.display = 'none';
serverBtn.querySelector('.btn-icon').style.display = 'inline';
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
const span = document.createElement('span');
span.innerHTML = '<b>Node</b> ' + data.node_id;
serverStatus.appendChild(span);
} else {
const message = " Can't access node.";
if (serverValue.value.indexOf("https") != -1) {
message += "</br></br>Self-signed certificate must be allowed in browser.";
}
serverStatus.innerHTML = "<div class='alert alert-danger' style='margin-bottom: 0px;'>" +
"<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>" +
message + "</div>";
}
}
};
xhr.send();
return false;
};
setServer();
const onGet = (path) => {
if (request !== undefined)
return false;
const input = document.getElementById("getKey").value;
clearResults()
let lastAppended = 0;
let start = new Date().getTime();
request = new XMLHttpRequest();
request.onreadystatechange = (event) => {
if (request.readyState >= 2) { // LOADING
if (request.status === 200) {
const elements = request.responseText.split("\n");
const elementsLength = elements.length;
const now = new Date().getTime();
for (let i = lastAppended; i < elementsLength; i++) {
const element = elements[i];
if (!element || element.length == 0)
break;
const o = JSON.parse(element);
if (o.expired) {
const expiredElement = document.getElementById('value' + o.id);
if (expiredElement) {
expiredElement.style.display = 'none';
if (expiredElement.parentNode) {
expiredElement.parentNode.removeChild(expiredElement);
}
}
} else {
const d = window.atob(o.data);
const delay = Math.max(0, start - now);
const li = document.createElement('li');
li.className = 'list-group-item';
li.id = 'value' + o.id;
li.innerHTML = valueGetElement(o);
li.style.opacity = 0;
li.style.transition = 'opacity 0.1s ease-in';
group.appendChild(li);
setTimeout(() => {
li.style.opacity = 1;
}, delay);
lastAppended = i + 1;
start = Math.max(start, now) + 25;
}
}
} else if (request.status !== 0) {
clearResults()
const li = document.createElement('li');
li.className = 'list-group-item list-group-item-danger';
li.textContent = "Error loading content: " + request.statusText;
group.appendChild(li);
}
}
if (request.readyState == 4) { // DONE
clearGetRequest();
}
};
request.onerror = (event) => {
clearGetRequest();
clearResults()
const li = document.createElement('li');
li.className = 'list-group-item list-group-item-danger';
li.textContent = "Error loading content.";
group.appendChild(li);
};
request.open('GET', server + 'key/' + input + path, true);
request.send(null);
setGetRequest();
return false;
};
const onPut = () => {
const key = document.getElementById("getKey").value;
const value = document.getElementById("putValue").value;
putBtn.disabled = true;
putBtn.querySelector('.spinner-border').style.display = 'inline-block';
putBtn.querySelector('.btn-text').style.display = 'none';
fetch(server + key, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ data: window.btoa(value) })
})
.then(response => {
if (!response.ok)
throw new Error(response.statusText);
})
.catch(error => {
group.innerHTML = '';
const li = document.createElement('li');
li.className = 'list-group-item list-group-item-danger';
li.textContent = error.message;
group.appendChild(li);
})
.finally(() => {
putBtn.disabled = false;
putBtn.querySelector('.spinner-border').style.display = 'none';
putBtn.querySelector('.btn-text').style.display = 'inline';
});
return false;
};
listenBtn.addEventListener('click', () => { onGet('/listen'); });
getStopBtn.parentNode.removeChild(getStopBtn);
getStopBtn.addEventListener('click', clearGetRequest);
</script>
</body>
</html>
</html>