#! """DECnet/Python access via HTTP """ import http.server import socketserver import traceback import sys import io import os.path from urllib.parse import urlparse, parse_qs import re import mimetypes import time try: import ssl except ImportError: ssl = None from .common import * from . import logging from . import html from . import mapper from . import host packagedir = os.path.dirname (__file__) SvnFileRev = "$LastChangedRevision: 570 $" def revno (s): return int (s.split ()[1]) DNREV = None def setdnrev (): # This has to be in a function called during program startup, # rather than top level code. Otherwise it runs whenever this # module happens to be imported in the order in which the various # sources are read. By delaying until we start execution, all our # modules have been imported. global DNREV, DNFULLVERSION, bottom if DNREV is None: DNREV = 0 for m in sys.modules.values (): fn = getattr (m, "__file__", None) if isinstance (fn, str) and fn.startswith (packagedir): # It's a DECnet module, get its rev r = getattr (m, "SvnFileRev", None) if r: r = revno (r) DNREV = max (DNREV, r) DNFULLVERSION = "{}-{} © 2013-{} by {}".format (DNVERSION, DNREV, CYEAR, AUTHORS) htmlversion = DNFULLVERSION.replace ("©", "©") bottom = html.footer ("{}
{}".format (htmlversion, PYTHONVERSION)) PYTHONVERSION = "Python {}.{}.{} ({}) on {}".format (sys.version_info.major, sys.version_info.minor, sys.version_info.micro, sys.version_info.releaselevel, sys.platform) class Monitor: def __init__ (self, config, nodelist): self.config = config self.nodelist = nodelist httproot = config.http_root or packagedir self.resources = os.path.join (httproot, "resources") if config.mapper: self.mapserver = mapper.Mapper (config, nodelist) else: self.mapserver = None def start (self): html.setstarttime () setdnrev () ports = list () mapserver = self.mapserver if mapserver: mapserver.start () if self.config.http_port: if self.config.https_port: # Both are defined, start https server in a thread t = StopThread (target = self.serverstart, name = "https", args = (True,)) t.start () self.serverstart (False) else: self.serverstart (True) def serverstart (self, secure): config = self.config if secure: server_addr = host.SourceAddress (config, config.https_port) logging.debug ("Starting https server on port {}", config.https_port) else: server_addr = host.SourceAddress (config, config.http_port) logging.debug ("Starting http server on port {}", config.http_port) httpd = DECnetMonitor (server_addr, DECnetMonitorRequest, self.nodelist, config, self.resources, self.mapserver, secure) if secure: httpd.socket = ssl.wrap_socket (httpd.socket, certfile = config.certificate, server_side = True) httpd.serve_forever () class DECnetMonitor (socketserver.ThreadingMixIn, http.server.HTTPServer): def __init__ (self, source_addr, rclass, nodelist, config, resources, mapserver, secure): self.nodelist = nodelist self.api = config.api self.mapserver = mapserver if mapserver: self.addlinks = (("/map", "Network map"),) else: self.addlinks = () self.resources = resources self.secure = secure or config.insecure_api super ().__init__ (source_addr.sockaddr, rclass, False) # Now replace the socket by what we actually want self.socket = source_addr.create_server () self.server_address = self.socket.getsockname () def server_bind (self): pass def server_activate (self): pass class DECnetMonitorRequest (http.server.BaseHTTPRequestHandler): def setup (self): super ().setup () self.wtfile = io.TextIOWrapper (self.wfile) def log_message (self, fmt, *args): logging.trace (fmt % (args)) def findnode (self): # Identify the node addressed by the request's query argument, # required if there is more than one. Return the node index, # node object, and the split-apart path string. p = urlparse (self.path) if p.scheme or p.netloc or p.params or p.fragment: logging.trace ("Invalid path: {}", self.path) self.send_error (400, "Invalid request") return None, None, None logging.trace ("http from {} get {}", self.client_address, p.path) parts = os.path.realpath (p.path).split ("/") if not parts or parts[0]: self.send_error (400, "Invalid request") logging.trace ("Invalid path: {}", self.path) return None, None, None parts = parts[1:] nodelist = self.server.nodelist mapserver = self.server.mapserver if len (nodelist) > 1: if not p.query: return 0, None, parts q = parse_qs (p.query) node = q["system"][0].upper () for i, n in enumerate (nodelist): if n.nodename and n.nodename.upper () == node: return i, n, parts self.send_error (404, "System not found") return 0, None, None return 0, nodelist[0], parts msg_500 = "Exception during server processing.

{}
" def handle_exception (self, op): logging.exception ("Exception handling http {} of {}", op, self.path) # Replace the "explanation" part of the message for code 500 self.responses[500] = [self.responses[500][0], self.msg_500.format (traceback.format_exc ())] self.send_error (500) def do_GET (self): try: nodeidx, tnode, parts = self.findnode () if parts is None: return mapserver = self.server.mapserver if parts[0] == "robots.txt": parts = [ "resources", "robots.txt" ] if parts[0] == "api" and self.server.api: if not self.server.secure: self.send_error (401, "API access requires HTTPS") return ret = self.json_get (parts[1:], tnode) if not ret: return ctype = "application/json" ret = str (ret).encode ("utf-8", "ignore") elif parts[0] == "resources": # Fetching a resource (a constant file) fn = os.path.join (self.server.resources, *parts[1:]) ctype = mimetypes.guess_type (fn, False)[0] try: with open (fn, "rb") as f: ret = f.read () except OSError: self.send_error (404, "File not found") return else: mobile = False if parts[0] == "m": # Mobile format page requested mobile = True parts = parts[1:] if not parts: parts = [''] ctype = "text/html" if mapserver and parts[0] == "map": ctype = "text/html" title, top, body = mapserver.html (mobile, parts[1:]) if not title: self.send_error (404, "File not found") return ret = html.mapdoc (mobile, title, top, body, bottom) elif not tnode: if parts != ['']: logging.trace ("Missing system parameter") self.send_error (400, "Missing system parameter") return ret = self.node_list (mobile) else: ret = tnode.http_get (mobile, parts) if not ret: self.send_error (404, "File not found") return title, sb, body = ret if len (self.server.nodelist) > 1: sb.insert (0, self.node_sidebar (mobile, nodeidx)) sb = html.sidebar (*sb) top = html.page_title (title, links = self.server.addlinks) ret = html.doc (mobile, title, top, html.middle (sb, body), bottom) ret = str (ret).encode ("utf-8", "ignore") self.send_response (200) self.send_header ("Content-type", ctype) self.send_header ("Content-Length", str (len (ret))) self.end_headers () self.wfile.write (ret) except Exception: self.handle_exception ("GET") def do_POST (self): try: nodeidx, tnode, parts = self.findnode () if parts is None: return if not tnode: logging.trace ("Missing system parameter") self.send_error (400, "Missing system parameter") return if parts[0] == "api" and self.server.api: if not self.server.secure: self.send_error (401, "API access requires HTTPS") return ret = self.json_post (parts[1:], tnode) if not ret: return ctype = "application/json" else: self.send_error (405, "No such object or not supported with POST") return if isinstance (ret, str): ret = ret.encode ("utf-8", "ignore") self.send_response (200) self.send_header ("Content-type", ctype) self.send_header ("Content-Length", str(len (ret))) self.end_headers () self.wfile.write (ret) except Exception: self.handle_exception ("POST") def node_sidebar (self, mobile, idx = -1): ret = [ (html.sbbutton_active if idx == i else html.sbbutton) (mobile, n.description (mobile)) for i, n in enumerate (self.server.nodelist) ] return html.sbelement (html.sblabel ("Systems"), *ret) def node_list (self, mobile): title = "DECnet/Python monitoring" top = html.page_title (title, links = self.server.addlinks) return html.doc (mobile, title, top, html.sidebar (self.node_sidebar (mobile)), bottom) def getapientity (self, what, tnode): logging.trace ("getentity node {} path {}", tnode, what) for ent in what: if ent: # Ignore empty entries in the path logging.trace ("current entity {} looking for {}", tnode, ent) tnode = tnode.getentity (ent) logging.trace ("getentity: found {}", tnode) return tnode def json_get (self, what, tnode): logging.trace ("API GET request for {}, node {}", what, tnode) data = dict () if not what or what == ['']: for n in self.server.nodelist: data.update (n.json_description ()) return dnEncoder.encode (data) elif not tnode: self.send_error (404, "No such API object") return try: ent = self.getapientity (what, tnode) handler = ent.get_api except (KeyError, AttributeError): logging.trace ("API GET handler lookup error", exc_info = True) self.send_error (404, "No such API object") return None try: handler = ent.get_api except AttributeError: self.send_error (405, "No such object or not supported with GET") return None try: data = handler () except Exception: logging.debug ("API GET handler error", exc_info = True) data = { "status" : "exception", "exception" : traceback.format_exc () } return dnEncoder.encode (data) def json_post (self, what, tnode): logging.trace ("API POST request for {}, node {}", what, tnode) nbytes = 0 length = self.headers.get ("content-length") if length: nbytes = int (length) data = None if nbytes: data = dnDecoder.decode (self.rfile.read (nbytes)) logging.trace ("POST input data: {}", str (data)) try: ent = self.getapientity (what, tnode) except (KeyError, AttributeError): logging.trace ("API POST handler lookup error", exc_info = True) self.send_error (404, "No such API object") return None try: handler = ent.post_api except AttributeError: self.send_error (405, "No such object or not supported with POST") return None try: ret = handler (data) except Exception: logging.debug ("API POST handler error", exc_info = True) ret = { "status" : "exception", "exception" : traceback.format_exc () } return dnEncoder.encode (ret)