#!/usr/bin/env python3 import os import json import pprint import argparse import datetime import ssl import urllib.request import urllib.parse import urllib.error INFO_METHODS = { "RouterInfo": { "i2p.router.uptime": "", "i2p.router.net.status": "", "i2p.router.netdb.knownpeers": "", "i2p.router.netdb.activepeers": "", "i2p.router.net.bw.inbound.1s": "", "i2p.router.net.bw.outbound.1s": "", "i2p.router.net.tunnels.participating": "", "i2p.router.net.tunnels.successrate": "", "i2p.router.net.total.received.bytes": "", "i2p.router.net.total.sent.bytes": "", }, "ClientServicesInfo": { "I2PTunnel": "", "SOCKS": "", "HTTPProxy": "", "SAM": "", "BOB": "", "I2CP": "", } } STATUS = [ "OK", "TESTING", "FIREWALLED", "HIDDEN", "WARN_FIREWALLED_AND_FAST", "WARN_FIREWALLED_AND_FLOODFILL", "WARN_FIREWALLED_WITH_INBOUND_TCP", "WARN_FIREWALLED_WITH_UDP_DISABLED", "ERROR_I2CP", "ERROR_CLOCK_SKEW", "ERROR_PRIVATE_TCP_ADDRESS", "ERROR_SYMMETRIC_NAT", "ERROR_UDP_PORT_IN_USE", "ERROR_NO_ACTIVE_PEERS_CHECK_CONNECTION_AND_FIREWALL", "ERROR_UDP_DISABLED_AND_TCP_UNSET", ] SUFFIXES = ['B','KB','MB','GB','TB'] def humanize_size(size, precision=2): """Bytes to human readable size""" suffixIndex = 0 while size > 1024: suffixIndex += 1 size = size / 1024.0 return "{}{}".format(round(size ,precision), SUFFIXES[suffixIndex]) def humanize_time(milliseconds): """Seconds to human readable time""" return str(datetime.timedelta(milliseconds=milliseconds)) def do_post(url, data): req = urllib.request.Request(url, data=data.encode()) with urllib.request.urlopen(req, context=ssl._create_unverified_context()) as f: resp = f.read().decode('utf-8') return json.loads(resp) class I2PControl(object): """Talk to I2PControl API""" def __init__(self, url, password='itoopie'): self.url = url self.password = password self._token = None @property def token(self): """Cached authentication token""" if not self._token: self._token = do_post(self.url, json.dumps({'id': 1, 'method': 'Authenticate', 'params': {'API': 1, 'Password': self.password}, 'jsonrpc': '2.0'}))["result"]["Token"] return self._token def request(self, method, params): """Execute authenticated request""" params['Token'] = self.token return do_post(self.url, json.dumps( {'id': 1, 'method': method, 'params': params, 'jsonrpc': '2.0'})) class I2pdctl(object): """i2pd control""" def __init__(self, ctl): self.ctl = ctl def execute(self, args): """Execute raw method""" try: parameters = json.loads(args.parameters) except json.decoder.JSONDecodeError: print("Error: parameters should be a valid JSON object") return resp = self.ctl.request(args.method, parameters) if args.json_output: print(resp.text) elif "result" in resp: pprint.pprint(resp["result"]) else: pprint.pprint(resp) def raw_info(self, args): """Retrieve raw JSON reply from method(s)""" res = {} for m in args.method: if m not in INFO_METHODS: print("Invalid method {}. Supported methods are {}".format(m, ", ".join(INFO_METHODS.keys()))) return else: res[m] = self.ctl.request(m, INFO_METHODS[m])['result'] print(json.dumps(res)) def print_info(self, args): """Print information about a node in a human readable form""" def fancy_title(string): print("\n### {}".format(string)) ri_res = self.ctl.request("RouterInfo", INFO_METHODS["RouterInfo"])['result'] try: csi_res = self.ctl.request("ClientServicesInfo", INFO_METHODS["ClientServicesInfo"])['result'] except KeyError: csi_res = False fancy_title("Router info") print("Uptime: {}".format( humanize_time(int(ri_res["i2p.router.uptime"])))) print("Status: {}".format(STATUS[ri_res["i2p.router.net.status"]])) if "i2p.router.net.tunnels.successrate" in ri_res: print("Tunnel creation success rate: {}%".format( ri_res["i2p.router.net.tunnels.successrate"])) if "i2p.router.net.total.received.bytes" in ri_res: print("Received: {} ({} B/s) / Sent: {} ({} B/s)".format( humanize_size(int(ri_res["i2p.router.net.total.received.bytes"])), humanize_size(int(ri_res["i2p.router.net.bw.inbound.1s"])), humanize_size(int(ri_res["i2p.router.net.total.sent.bytes"])), humanize_size(int(ri_res["i2p.router.net.bw.outbound.1s"])))) else: print("Received: {} B/s / Sent: {} B/s".format( humanize_size(int(ri_res["i2p.router.net.bw.inbound.1s"])), humanize_size(int(ri_res["i2p.router.net.bw.outbound.1s"])))) print("Known routers: {} / Active: {}".format( ri_res["i2p.router.netdb.knownpeers"], ri_res["i2p.router.netdb.activepeers"])) if csi_res: fancy_title("Interfaces") for n in ["HTTPProxy", "SOCKS", "BOB", "SAM", "I2CP"]: print("- {}:".format(n), "ON" if csi_res[n]["enabled"] == "true" else "OFF") if csi_res["I2PTunnel"]["client"]: fancy_title("Client I2P Tunnels") for tunnel in sorted(list(csi_res["I2PTunnel"]["client"].keys())): print("-", tunnel, csi_res["I2PTunnel"]["client"][tunnel]) if csi_res["I2PTunnel"]["server"]: fancy_title("Server I2P Tunnels") for tunnel in sorted(list(csi_res["I2PTunnel"]["server"].keys())): t = csi_res["I2PTunnel"]["server"][tunnel] if "port" in t: print("-", tunnel, "{}:{}".format(t["address"], t["port"])) else: print("-", tunnel, t["address"]) def main(): URL = os.getenv("I2PCONTROL_URL", "https://127.0.0.1:7650/") PASSWORD = os.getenv("I2PCONTROL_PASSWORD", "itoopie") ctl = I2pdctl(I2PControl(URL, PASSWORD)) parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(title="actions",help="Command to execute") exec_parser = subparsers.add_parser("exec", description="Execute RPC method with parameters, return results or raw JSON") exec_parser.add_argument('method', help="RPC method name") exec_parser.add_argument('parameters', help="Parameters as raw JSON string") exec_parser.add_argument('-j', '--json-output', action="store_true", help="Output raw JSON reply") exec_parser.set_defaults(func=ctl.execute) raw_info_parser = subparsers.add_parser("raw_info", description="Retrieve JSON info from specified *Info methods") raw_info_parser.add_argument('method', nargs='*', help="RPC method(s) to retreive info. Supported methods: {}".format(", ".join(INFO_METHODS.keys()))) raw_info_parser.set_defaults(func=ctl.raw_info) print_info_parser = subparsers.add_parser("info", description="Print generic information about node in a human readable form") print_info_parser.set_defaults(func=ctl.print_info) args = parser.parse_args() if hasattr(args, "func"): try: args.func(args) except urllib.error.URLError: print("Error: I2PControl URL is unavailable. Check your i2pd settings and network connection.") exit(1) else: parser.print_help() if __name__ == "__main__": main()