Implemented basic node hosting and page serving.

This commit is contained in:
Mark Qvist 2021-08-26 15:26:12 +02:00
parent 55e8479979
commit 862f4835c7
7 changed files with 243 additions and 24 deletions

View file

@ -26,7 +26,7 @@ class Conversation:
# Add the announce to the directory announce # Add the announce to the directory announce
# stream logger # stream logger
app.directory.announce_received(destination_hash, app_data) app.directory.lxmf_announce_received(destination_hash, app_data)
@staticmethod @staticmethod
def query_for_peer(source_hash): def query_for_peer(source_hash):

View file

@ -5,7 +5,7 @@ import time
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
class Directory: class Directory:
ANNOUNCE_STREAM_MAXLENGTH = 256 ANNOUNCE_STREAM_MAXLENGTH = 64
def __init__(self, app): def __init__(self, app):
self.directory_entries = {} self.directory_entries = {}
@ -54,7 +54,13 @@ class Directory:
except Exception as e: except Exception as e:
RNS.log("Could not load directory from disk. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Could not load directory from disk. The contained exception was: "+str(e), RNS.LOG_ERROR)
def announce_received(self, source_hash, app_data): def lxmf_announce_received(self, source_hash, app_data):
timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
def node_announce_received(self, source_hash, app_data):
timestamp = time.time() timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data)) self.announce_stream.insert(0, (timestamp, source_hash, app_data))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH: while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:

124
nomadnet/Node.py Normal file
View file

@ -0,0 +1,124 @@
import os
import RNS
import time
import threading
import RNS.vendor.umsgpack as msgpack
class Node:
JOB_INTERVAL = 5
def __init__(self, app):
RNS.log("Nomad Network Node starting...", RNS.LOG_VERBOSE)
self.app = app
self.identity = self.app.identity
self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, "nomadnetwork", "node")
self.last_announce = None
self.announce_interval = self.app.node_announce_interval
self.job_interval = Node.JOB_INTERVAL
self.should_run_jobs = True
self.app_data = None
self.name = self.app.node_name
self.register_pages()
self.register_files()
if self.app.node_announce_at_start:
self.announce()
if self.name == None:
name_string = self.app.peer_settings["display_name"]+"'s Node"
else:
name_string = self.name
RNS.log("Node \""+name_string+"\" ready for incoming connections on "+RNS.prettyhexrep(self.destination.hash), RNS.LOG_VERBOSE)
def register_pages(self):
self.servedpages = []
self.scan_pages(self.app.pagespath)
if not self.app.pagespath+"index.mu" in self.servedpages:
self.destination.register_request_handler(
"/page/index.mu",
response_generator = self.serve_default_index,
allow = RNS.Destination.ALLOW_ALL
)
for page in self.servedpages:
request_path = "/page"+page.replace(self.app.pagespath, "")
self.destination.register_request_handler(
request_path,
response_generator = self.serve_page,
allow = RNS.Destination.ALLOW_ALL
)
def register_files(self):
self.servedfiles = []
self.scan_files(self.app.filespath)
def scan_pages(self, base_path):
files = [file for file in os.listdir(base_path) if os.path.isfile(os.path.join(base_path, file)) and file[:1] != "."]
directories = [file for file in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, file)) and file[:1] != "."]
for file in files:
self.servedpages.append(base_path+"/"+file)
for directory in directories:
self.scan_pages(base_path+"/"+directory)
def scan_files(self, base_path):
files = [file for file in os.listdir(base_path) if os.path.isfile(os.path.join(base_path, file)) and file[:1] != "."]
directories = [file for file in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, file)) and file[:1] != "."]
for file in files:
self.servedfiles.append(base_path+"/"+file)
for directory in directories:
self.scan_files(base_path+"/"+directory)
def serve_page(self, path, data, request_id, remote_identity, requested_at):
RNS.log("Request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_VERBOSE)
file_path = path.replace("/page", self.app.pagespath, 1)
try:
RNS.log("Serving file: "+file_path, RNS.LOG_VERBOSE)
fh = open(file_path, "rb")
response_data = fh.read()
fh.close()
return response_data
except Exception as e:
RNS.log("Error occurred while handling request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
return None
def serve_default_index(self, path, data, request_id, remote_identity, requested_at):
RNS.log("Serving default index for request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_VERBOSE)
return DEFAULT_INDEX.encode("utf-8")
def announce(self):
self.app_data = self.name.encode("utf-8")
self.last_announce = time.time()
self.destination.announce(app_data=self.app_data)
def __jobs(self):
while self.should_run_jobs:
now = time.time()
if now > self.last_announce + self.announce_interval:
self.announce()
time.sleep(self.job_interval)
def peer_connected(link):
RNS.log("Peer connected to "+str(self.destination), RNS.LOG_INFO)
link.set_link_closed_callback(self.peer_disconnected)
DEFAULT_INDEX = '''>Default Home Page
This node is serving pages, but no home page file (index.mu) was found in the page storage directory. This is an auto-generated placeholder.
If you are the node operator, you can define your own home page by creating a file named `*index.mu`* in the page storage directory.
'''

View file

@ -51,6 +51,9 @@ class NomadNetworkApp:
self.directorypath = self.configdir+"/storage/directory" self.directorypath = self.configdir+"/storage/directory"
self.peersettingspath = self.configdir+"/storage/peersettings" self.peersettingspath = self.configdir+"/storage/peersettings"
self.pagespath = self.configdir+"/storage/pages"
self.filespath = self.configdir+"/storage/files"
if not os.path.isdir(self.storagepath): if not os.path.isdir(self.storagepath):
os.makedirs(self.storagepath) os.makedirs(self.storagepath)
@ -63,6 +66,12 @@ class NomadNetworkApp:
if not os.path.isdir(self.conversationpath): if not os.path.isdir(self.conversationpath):
os.makedirs(self.conversationpath) os.makedirs(self.conversationpath)
if not os.path.isdir(self.pagespath):
os.makedirs(self.pagespath)
if not os.path.isdir(self.filespath):
os.makedirs(self.filespath)
if os.path.isfile(self.configpath): if os.path.isfile(self.configpath):
try: try:
self.config = ConfigObj(self.configpath) self.config = ConfigObj(self.configpath)
@ -151,6 +160,11 @@ class NomadNetworkApp:
self.directory = nomadnet.Directory.Directory(self) self.directory = nomadnet.Directory.Directory(self)
if self.enable_node:
self.node = nomadnet.Node(self)
else:
self.node = None
nomadnet.ui.spawn(self.uimode) nomadnet.ui.spawn(self.uimode)
def set_display_name(self, display_name): def set_display_name(self, display_name):
@ -295,13 +309,40 @@ class NomadNetworkApp:
self.uimode = nomadnet.ui.UI_WEB self.uimode = nomadnet.ui.UI_WEB
if "node" in self.config: if "node" in self.config:
for option in self.config["node"]: if not "enable_node" in self.config["node"]:
value = self.config["node"][option] self.enable_node = False
else:
self.enable_node = self.config["node"].as_bool("enable_node")
if option == "enable_node": if not "node_name" in self.config["node"]:
value = self.config["node"].as_bool(option) self.node_name = None
self.enable_node = value else:
value = self.config["node"]["node_name"]
if value.lower() == "none":
self.node_name = None
else:
self.node_name = self.config["node"]["node_name"]
if not "announce_at_start" in self.config["node"]:
self.node_announce_at_start = False
else:
self.node_announce_at_start = self.config["announce_at_start"].as_bool("announce_at_start")
if not "announce_interval" in self.config["node"]:
self.node_announce_interval = 720
else:
value = self.config["announce_interval"].as_int("announce_interval")
if value < 1:
value = 1
self.node_announce_interval = value
if "pages_path" in self.config["node"]:
self.pagespath = self.config["node"]["pages_path"]
if "files_path" in self.config["node"]:
self.filespath = self.config["node"]["files_path"]
@staticmethod @staticmethod
def get_shared_instance(): def get_shared_instance():
if NomadNetworkApp._shared_instance != None: if NomadNetworkApp._shared_instance != None:
@ -385,6 +426,23 @@ hide_guide = no
[node] [node]
# Whether to enable node hosting
enable_node = no enable_node = no
# The node name will be visible to other
# peers on the network, and included in
# announces.
node_name = None
# Automatic announce interval in minutes.
# 12 hours by default.
announce_interval = 720
# Whether to announce when the node starts
announce_at_start = No
'''.splitlines() '''.splitlines()

View file

@ -3,6 +3,7 @@ import glob
from .NomadNetworkApp import NomadNetworkApp from .NomadNetworkApp import NomadNetworkApp
from .Conversation import Conversation from .Conversation import Conversation
from .Node import Node
from .ui import * from .ui import *

View file

@ -168,7 +168,7 @@ A `*peer`* refers to another Nomad Network client, which will generally be opera
An `*announce`* can be sent by any peer on the network, which will notify other peers of its existence, and contains the cryptographic keys that allows other peers to communicate with it. An `*announce`* can be sent by any peer on the network, which will notify other peers of its existence, and contains the cryptographic keys that allows other peers to communicate with it.
In the `![ Network ]`! section of the program, you can monitor announces on the network, initiate conversations with announced peers, and announce your own peer on the network. In the `![ Network ]`! section of the program, you can monitor announces on the network, initiate conversations with announced peers, and announce your own peer on the network. You can also connect to nodes on the network and browse information shared by them.
>>Conversations >>Conversations
@ -188,8 +188,7 @@ If no nodes exist on a network, all peers will still be able to communicate dire
''' '''
TOPIC_CONVERSATIONS = '''Conversations TOPIC_CONVERSATIONS = '''>Conversations
=============
Conversations in Nomad Network Conversations in Nomad Network
''' '''

View file

@ -10,7 +10,7 @@ class NetworkDisplayShortcuts():
self.app = app self.app = app
g = app.ui.glyphs g = app.ui.glyphs
self.widget = urwid.AttrMap(urwid.Text("[C-"+g["arrow_u"]+g["arrow_d"]+"] Navigate announces"), "shortcutbar") self.widget = urwid.AttrMap(urwid.Text("[C-l] View Nodes/Announces [C-"+g["arrow_u"]+g["arrow_d"]+"] Navigate Lists"), "shortcutbar")
class DialogLineBox(urwid.LineBox): class DialogLineBox(urwid.LineBox):
@ -93,7 +93,7 @@ class AnnounceInfo(urwid.WidgetWrap):
def show_announce_stream(sender): def show_announce_stream(sender):
options = self.parent.left_pile.options(height_type="weight", height_amount=1) options = self.parent.left_pile.options(height_type="weight", height_amount=1)
self.parent.left_pile.contents[1] = (AnnounceStream(self.app, self.parent), options) self.parent.left_pile.contents[0] = (AnnounceStream(self.app, self.parent), options)
def converse(sender): def converse(sender):
show_announce_stream(None) show_announce_stream(None)
@ -176,7 +176,7 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
parent = self.app.ui.main_display.sub_displays.network_display parent = self.app.ui.main_display.sub_displays.network_display
info_widget = AnnounceInfo(announce, parent, self.app) info_widget = AnnounceInfo(announce, parent, self.app)
options = parent.left_pile.options(height_type="weight", height_amount=1) options = parent.left_pile.options(height_type="weight", height_amount=1)
parent.left_pile.contents[1] = (info_widget, options) parent.left_pile.contents[0] = (info_widget, options)
class AnnounceStream(urwid.WidgetWrap): class AnnounceStream(urwid.WidgetWrap):
def __init__(self, app, parent): def __init__(self, app, parent):
@ -203,6 +203,12 @@ class AnnounceStream(urwid.WidgetWrap):
self.display_widget = self.ilb self.display_widget = self.ilb
urwid.WidgetWrap.__init__(self, urwid.LineBox(self.display_widget, title="Announce Stream")) urwid.WidgetWrap.__init__(self, urwid.LineBox(self.display_widget, title="Announce Stream"))
def keypress(self, size, key):
if key == "up":
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header")
return super(AnnounceStream, self).keypress(size, key)
def rebuild_widget_list(self): def rebuild_widget_list(self):
self.added_entries = [] self.added_entries = []
self.widget_list = [] self.widget_list = []
@ -287,7 +293,8 @@ class KnownNodes(urwid.WidgetWrap):
else: else:
self.no_content = True self.no_content = True
widget_style = "inactive_text" widget_style = "inactive_text"
self.display_widget = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no nodes are known\n\n"), align="center")]) self.pile = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no nodes are known\n\n"), align="center")])
self.display_widget = urwid.Filler(self.pile, valign="top", height="pack")
urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title="Known Nodes"), widget_style)) urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title="Known Nodes"), widget_style))
@ -361,7 +368,7 @@ class LocalPeer(urwid.WidgetWrap):
def save_query(sender): def save_query(sender):
def dismiss_dialog(sender): def dismiss_dialog(sender):
self.dialog_open = False self.dialog_open = False
self.parent.left_pile.contents[3] = (LocalPeer(self.app, self.parent), options) self.parent.left_pile.contents[2] = (LocalPeer(self.app, self.parent), options)
self.app.set_display_name(e_name.get_edit_text()) self.app.set_display_name(e_name.get_edit_text())
@ -378,13 +385,13 @@ class LocalPeer(urwid.WidgetWrap):
overlay = dialog overlay = dialog
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type="pack", height_amount=None)
self.dialog_open = True self.dialog_open = True
self.parent.left_pile.contents[3] = (overlay, options) self.parent.left_pile.contents[2] = (overlay, options)
def announce_query(sender): def announce_query(sender):
def dismiss_dialog(sender): def dismiss_dialog(sender):
self.dialog_open = False self.dialog_open = False
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type="pack", height_amount=None)
self.parent.left_pile.contents[3] = (LocalPeer(self.app, self.parent), options) self.parent.left_pile.contents[2] = (LocalPeer(self.app, self.parent), options)
self.app.announce_now() self.app.announce_now()
@ -402,11 +409,11 @@ class LocalPeer(urwid.WidgetWrap):
self.dialog_open = True self.dialog_open = True
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type="pack", height_amount=None)
self.parent.left_pile.contents[3] = (overlay, options) self.parent.left_pile.contents[2] = (overlay, options)
def node_settings_query(sender): def node_settings_query(sender):
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type="pack", height_amount=None)
self.parent.left_pile.contents[3] = (self.parent.node_settings_display, options) self.parent.left_pile.contents[2] = (self.parent.node_settings_display, options)
if LocalPeer.announce_timer == None: if LocalPeer.announce_timer == None:
self.t_last_announce = AnnounceTime(self.app) self.t_last_announce = AnnounceTime(self.app)
@ -443,7 +450,7 @@ class NodeSettings(urwid.WidgetWrap):
def show_peer_info(sender): def show_peer_info(sender):
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type="pack", height_amount=None)
self.parent.left_pile.contents[3] = (LocalPeer(self.app, self.parent), options) self.parent.left_pile.contents[2] = (LocalPeer(self.app, self.parent), options)
widget_style = "inactive_text" widget_style = "inactive_text"
pile = urwid.Pile([ pile = urwid.Pile([
@ -518,6 +525,16 @@ class NetworkStats(urwid.WidgetWrap):
self.w_heard_peers.start() self.w_heard_peers.start()
self.w_known_nodes.start() self.w_known_nodes.start()
class NetworkLeftPile(urwid.Pile):
def keypress(self, size, key):
if key == "ctrl l":
self.parent.toggle_list()
else:
return super(NetworkLeftPile, self).keypress(size, key)
class NetworkDisplay(): class NetworkDisplay():
list_width = 0.33 list_width = 0.33
@ -525,19 +542,21 @@ class NetworkDisplay():
self.app = app self.app = app
g = self.app.ui.glyphs g = self.app.ui.glyphs
self.known_nodes_display = KnownNodes(self.app) self.known_nodes_display = None
self.network_stats_display = NetworkStats(self.app, self) self.network_stats_display = NetworkStats(self.app, self)
self.announce_stream_display = AnnounceStream(self.app, self) self.announce_stream_display = AnnounceStream(self.app, self)
self.local_peer_display = LocalPeer(self.app, self) self.local_peer_display = LocalPeer(self.app, self)
self.node_settings_display = NodeSettings(self.app, self) self.node_settings_display = NodeSettings(self.app, self)
self.left_pile = urwid.Pile([ self.list_display = 0
("pack", self.known_nodes_display), self.left_pile = NetworkLeftPile([
("weight", 1, self.announce_stream_display), ("weight", 1, self.announce_stream_display),
("pack", self.network_stats_display), ("pack", self.network_stats_display),
("pack", self.local_peer_display), ("pack", self.local_peer_display),
]) ])
self.left_pile.parent = self
self.left_area = self.left_pile self.left_area = self.left_pile
self.right_area = urwid.AttrMap(urwid.LineBox(urwid.Filler(urwid.Text("Disconnected\n"+g["arrow_l"]+" "+g["arrow_r"], align="center"), "middle"), title="Remote Node"), "inactive_text") self.right_area = urwid.AttrMap(urwid.LineBox(urwid.Filler(urwid.Text("Disconnected\n"+g["arrow_l"]+" "+g["arrow_r"], align="center"), "middle"), title="Remote Node"), "inactive_text")
@ -552,6 +571,18 @@ class NetworkDisplay():
self.shortcuts_display = NetworkDisplayShortcuts(self.app) self.shortcuts_display = NetworkDisplayShortcuts(self.app)
self.widget = self.columns self.widget = self.columns
def toggle_list(self):
if self.list_display != 0:
self.announce_stream_display = AnnounceStream(self.app, self)
options = self.left_pile.options(height_type="weight", height_amount=1)
self.left_pile.contents[0] = (self.announce_stream_display, options)
self.list_display = 0
else:
self.known_nodes_display = KnownNodes(self.app)
options = self.left_pile.options(height_type="weight", height_amount=1)
self.left_pile.contents[0] = (self.known_nodes_display, options)
self.list_display = 1
def start(self): def start(self):
self.local_peer_display.start() self.local_peer_display.start()
self.network_stats_display.start() self.network_stats_display.start()