nfd-status-http-server.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
2 """
3 Copyright (c) 2014-2024, Regents of the University of California,
4  Arizona Board of Regents,
5  Colorado State University,
6  University Pierre & Marie Curie, Sorbonne University,
7  Washington University in St. Louis,
8  Beijing Institute of Technology,
9  The University of Memphis.
10 
11 This file is part of NFD (Named Data Networking Forwarding Daemon).
12 See AUTHORS.md for complete list of NFD authors and contributors.
13 
14 NFD is free software: you can redistribute it and/or modify it under the terms
15 of the GNU General Public License as published by the Free Software Foundation,
16 either version 3 of the License, or (at your option) any later version.
17 
18 NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
19 without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
20 PURPOSE. See the GNU General Public License for more details.
21 
22 You should have received a copy of the GNU General Public License along with
23 NFD, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>.
24 """
25 
26 import argparse
27 import ipaddress
28 import os
29 import socket
30 import subprocess
31 from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
32 from urllib.parse import urlsplit
33 
34 
35 class NfdStatusHandler(SimpleHTTPRequestHandler):
36  """The handler class to handle HTTP requests."""
37 
38  def do_GET(self):
39  path = urlsplit(self.path).path
40  if path == "/":
41  self.__serve_report__serve_report()
42  elif path == "/robots.txt" and self.server.allow_robots:
43  self.send_error(404)
44  else:
45  super().do_GET()
46 
47  def __serve_report(self):
48  """Obtain XML-formatted NFD status report and send it back as response body."""
49  try:
50  proc = subprocess.run(
51  ["nfdc", "status", "report", "xml"], capture_output=True, check=True, text=True, timeout=10
52  )
53  output = proc.stdout
54  except OSError as err:
55  super().log_message(f"error invoking nfdc: {err}")
56  self.send_error(500)
57  except subprocess.SubprocessError as err:
58  super().log_message(f"error invoking nfdc: {err}")
59  self.send_error(504, "Cannot connect to NFD")
60  else:
61  # add stylesheet processing instruction after the XML document type declaration
62  # (yes, this is a ugly hack)
63  if (pos := output.find(">") + 1) != 0:
64  xml = output[:pos] + '<?xml-stylesheet type="text/xsl" href="nfd-status.xsl"?>' + output[pos:]
65  self.send_response(200)
66  self.send_header("Content-Type", "text/xml; charset=UTF-8")
67  self.end_headers()
68  self.wfile.write(xml.encode())
69  else:
70  super().log_message(f"malformed nfdc output: {output}")
71  self.send_error(500)
72 
73  # override
74  def log_message(self, *args):
75  if self.server.verbose:
76  super().log_message(*args)
77 
78 
79 class NfdStatusHttpServer(ThreadingHTTPServer):
80  def __init__(self, addr, port, *, robots=False, verbose=False):
81  # socketserver.BaseServer defaults to AF_INET even if you provide an IPv6 address
82  # see https://bugs.python.org/issue20215 and https://bugs.python.org/issue24209
83  if addr.version == 6:
84  self.address_familyaddress_family = socket.AF_INET6
85  self.allow_robotsallow_robots = robots
86  self.verboseverbose = verbose
87  super().__init__((str(addr), port), NfdStatusHandler)
88 
89 
90 def main():
91  def ip_address(arg, /):
92  """Validate IP address."""
93  try:
94  value = ipaddress.ip_address(arg)
95  except ValueError:
96  raise argparse.ArgumentTypeError(f"{arg!r} is not a valid IP address")
97  return value
98 
99  def port_number(arg, /):
100  """Validate port number."""
101  try:
102  value = int(arg)
103  except ValueError:
104  value = -1
105  if value < 0 or value > 65535:
106  raise argparse.ArgumentTypeError(f"{arg!r} is not a valid port number")
107  return value
108 
109  parser = argparse.ArgumentParser(description="Serves NFD status page via HTTP")
110  parser.add_argument("-V", "--version", action="version", version="@VERSION@")
111  parser.add_argument("-a", "--address", default="127.0.0.1", type=ip_address, metavar="ADDR",
112  help="bind to this IP address (default: %(default)s)")
113  parser.add_argument("-p", "--port", default=6380, type=port_number,
114  help="bind to this port number (default: %(default)s)")
115  parser.add_argument("-f", "--workdir", default="@DATAROOTDIR@/ndn", metavar="DIR",
116  help="server's working directory (default: %(default)s)")
117  parser.add_argument("-r", "--robots", action="store_true", help="allow crawlers and other HTTP bots")
118  parser.add_argument("-v", "--verbose", action="store_true", help="turn on verbose logging")
119  args = parser.parse_args()
120 
121  os.chdir(args.workdir)
122 
123  with NfdStatusHttpServer(args.address, args.port, robots=args.robots, verbose=args.verbose) as httpd:
124  if httpd.address_family == socket.AF_INET6:
125  url = "http://[{}]:{}"
126  else:
127  url = "http://{}:{}"
128  print("Server started at", url.format(*httpd.server_address))
129 
130  try:
131  httpd.serve_forever()
132  except KeyboardInterrupt:
133  pass
134 
135 
136 if __name__ == "__main__":
137  main()
def __init__(self, addr, port, *robots=False, verbose=False)