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
# stream logger
app.directory.announce_received(destination_hash, app_data)
app.directory.lxmf_announce_received(destination_hash, app_data)
@staticmethod
def query_for_peer(source_hash):

View File

@ -5,7 +5,7 @@ import time
import RNS.vendor.umsgpack as msgpack
class Directory:
ANNOUNCE_STREAM_MAXLENGTH = 256
ANNOUNCE_STREAM_MAXLENGTH = 64
def __init__(self, app):
self.directory_entries = {}
@ -54,7 +54,13 @@ class Directory:
except Exception as e:
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()
self.announce_stream.insert(0, (timestamp, source_hash, app_data))
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.peersettingspath = self.configdir+"/storage/peersettings"
self.pagespath = self.configdir+"/storage/pages"
self.filespath = self.configdir+"/storage/files"
if not os.path.isdir(self.storagepath):
os.makedirs(self.storagepath)
@ -63,6 +66,12 @@ class NomadNetworkApp:
if not os.path.isdir(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):
try:
self.config = ConfigObj(self.configpath)
@ -151,6 +160,11 @@ class NomadNetworkApp:
self.directory = nomadnet.Directory.Directory(self)
if self.enable_node:
self.node = nomadnet.Node(self)
else:
self.node = None
nomadnet.ui.spawn(self.uimode)
def set_display_name(self, display_name):
@ -295,13 +309,40 @@ class NomadNetworkApp:
self.uimode = nomadnet.ui.UI_WEB
if "node" in self.config:
for option in self.config["node"]:
value = self.config["node"][option]
if not "enable_node" in self.config["node"]:
self.enable_node = False
else:
self.enable_node = self.config["node"].as_bool("enable_node")
if option == "enable_node":
value = self.config["node"].as_bool(option)
self.enable_node = value
if not "node_name" in self.config["node"]:
self.node_name = None
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
def get_shared_instance():
if NomadNetworkApp._shared_instance != None:
@ -385,6 +426,23 @@ hide_guide = no
[node]
# Whether to enable node hosting
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()

View File

@ -3,6 +3,7 @@ import glob
from .NomadNetworkApp import NomadNetworkApp
from .Conversation import Conversation
from .Node import Node
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.
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
@ -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
'''

View File

@ -10,7 +10,7 @@ class NetworkDisplayShortcuts():
self.app = app
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):
@ -93,7 +93,7 @@ class AnnounceInfo(urwid.WidgetWrap):
def show_announce_stream(sender):
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):
show_announce_stream(None)
@ -176,7 +176,7 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
parent = self.app.ui.main_display.sub_displays.network_display
info_widget = AnnounceInfo(announce, parent, self.app)
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):
def __init__(self, app, parent):
@ -203,6 +203,12 @@ class AnnounceStream(urwid.WidgetWrap):
self.display_widget = self.ilb
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):
self.added_entries = []
self.widget_list = []
@ -287,7 +293,8 @@ class KnownNodes(urwid.WidgetWrap):
else:
self.no_content = True
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))
@ -361,7 +368,7 @@ class LocalPeer(urwid.WidgetWrap):
def save_query(sender):
def dismiss_dialog(sender):
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())
@ -378,13 +385,13 @@ class LocalPeer(urwid.WidgetWrap):
overlay = dialog
options = self.parent.left_pile.options(height_type="pack", height_amount=None)
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 dismiss_dialog(sender):
self.dialog_open = False
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()
@ -402,11 +409,11 @@ class LocalPeer(urwid.WidgetWrap):
self.dialog_open = True
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):
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:
self.t_last_announce = AnnounceTime(self.app)
@ -443,7 +450,7 @@ class NodeSettings(urwid.WidgetWrap):
def show_peer_info(sender):
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"
pile = urwid.Pile([
@ -518,6 +525,16 @@ class NetworkStats(urwid.WidgetWrap):
self.w_heard_peers.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():
list_width = 0.33
@ -525,19 +542,21 @@ class NetworkDisplay():
self.app = app
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.announce_stream_display = AnnounceStream(self.app, self)
self.local_peer_display = LocalPeer(self.app, self)
self.node_settings_display = NodeSettings(self.app, self)
self.left_pile = urwid.Pile([
("pack", self.known_nodes_display),
self.list_display = 0
self.left_pile = NetworkLeftPile([
("weight", 1, self.announce_stream_display),
("pack", self.network_stats_display),
("pack", self.local_peer_display),
])
self.left_pile.parent = self
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")
@ -552,6 +571,18 @@ class NetworkDisplay():
self.shortcuts_display = NetworkDisplayShortcuts(self.app)
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):
self.local_peer_display.start()
self.network_stats_display.start()