diff --git a/hyperglass/command/construct.py b/hyperglass/command/construct.py index a3b6aa7..a8dba84 100644 --- a/hyperglass/command/construct.py +++ b/hyperglass/command/construct.py @@ -89,6 +89,7 @@ class Construct: query = [] query_protocol = f"ipv{ipaddress.ip_network(self.query_target).version}" afi = getattr(self.device_vrf, query_protocol) + cmd_type = self.get_cmd_type(query_protocol, self.query_vrf) if self.transport == "rest": query.append( @@ -96,14 +97,13 @@ class Construct: { "query_type": "ping", "vrf": afi.vrf_name, - "afi": query_protocol, + "afi": cmd_type, "source": afi.source_address.compressed, "target": self.query_target, } ) ) elif self.transport == "scrape": - cmd_type = self.get_cmd_type(query_protocol, self.query_vrf) cmd = self.device_commands(self.device.commands, cmd_type, "ping") query.append( cmd.format( @@ -130,6 +130,7 @@ class Construct: query = [] query_protocol = f"ipv{ipaddress.ip_network(self.query_target).version}" afi = getattr(self.device_vrf, query_protocol) + cmd_type = self.get_cmd_type(query_protocol, self.query_vrf) if self.transport == "rest": query.append( @@ -137,14 +138,13 @@ class Construct: { "query_type": "traceroute", "vrf": afi.vrf_name, - "afi": query_protocol, + "afi": cmd_type, "source": afi.source_address.compressed, "target": self.query_target, } ) ) elif self.transport == "scrape": - cmd_type = self.get_cmd_type(query_protocol, self.query_vrf) cmd = self.device_commands(self.device.commands, cmd_type, "traceroute") query.append( cmd.format( @@ -168,6 +168,7 @@ class Construct: query = [] query_protocol = f"ipv{ipaddress.ip_network(self.query_target).version}" afi = getattr(self.device_vrf, query_protocol) + cmd_type = self.get_cmd_type(query_protocol, self.query_vrf) if self.transport == "rest": query.append( @@ -175,14 +176,13 @@ class Construct: { "query_type": "bgp_route", "vrf": afi.vrf_name, - "afi": query_protocol, - "source": afi.source_address.compressed, + "afi": cmd_type, + "source": None, "target": self.format_target(self.query_target), } ) ) elif self.transport == "scrape": - cmd_type = self.get_cmd_type(query_protocol, self.query_vrf) cmd = self.device_commands(self.device.commands, cmd_type, "bgp_route") query.append( cmd.format( @@ -218,19 +218,20 @@ class Construct: for afi in afis: afi_attr = getattr(self.device_vrf, afi) + cmd_type = self.get_cmd_type(afi, self.query_vrf) if self.transport == "rest": query.append( json.dumps( { "query_type": "bgp_community", "vrf": afi_attr.vrf_name, - "afi": afi, + "afi": cmd_type, "target": self.query_target, + "source": None, } ) ) elif self.transport == "scrape": - cmd_type = self.get_cmd_type(afi, self.query_vrf) cmd = self.device_commands( self.device.commands, cmd_type, "bgp_community" ) @@ -267,19 +268,20 @@ class Construct: for afi in afis: afi_attr = getattr(self.device_vrf, afi) + cmd_type = self.get_cmd_type(afi, self.query_vrf) if self.transport == "rest": query.append( json.dumps( { "query_type": "bgp_aspath", "vrf": afi_attr.vrf_name, - "afi": afi, + "afi": cmd_type, "target": self.query_target, + "source": None, } ) ) elif self.transport == "scrape": - cmd_type = self.get_cmd_type(afi, self.query_vrf) cmd = self.device_commands(self.device.commands, cmd_type, "bgp_aspath") query.append( cmd.format( diff --git a/hyperglass/command/encode.py b/hyperglass/command/encode.py new file mode 100644 index 0000000..4261a93 --- /dev/null +++ b/hyperglass/command/encode.py @@ -0,0 +1,30 @@ +# Standard Library Imports +import datetime + +# Third Party Imports +import jwt + +# Project Imports +from hyperglass.exceptions import RestError + + +async def jwt_decode(payload, secret): + """Decode & validate an encoded JSON Web Token (JWT)""" + try: + decoded = jwt.decode(payload, secret, algorithm="HS256") + decoded = decoded["payload"] + return decoded + except (KeyError, jwt.PyJWTError) as exp: + raise RestError(str(exp)) from None + + +async def jwt_encode(payload, secret, duration): + """Encode a query to a JSON Web Token (JWT)""" + token = { + "payload": payload, + "nbf": datetime.datetime.utcnow(), + "iat": datetime.datetime.utcnow(), + "exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=duration), + } + encoded = jwt.encode(token, secret, algorithm="HS256").decode("utf-8") + return encoded diff --git a/hyperglass/command/execute.py b/hyperglass/command/execute.py index 6175137..fbefff1 100644 --- a/hyperglass/command/execute.py +++ b/hyperglass/command/execute.py @@ -21,6 +21,7 @@ from netmiko import NetMikoTimeoutException # Project Imports from hyperglass.command.construct import Construct from hyperglass.command.validate import Validate +from hyperglass.command.encode import jwt_decode, jwt_encode from hyperglass.configuration import devices from hyperglass.configuration import logzero_config # noqa: F401 from hyperglass.configuration import params @@ -218,53 +219,51 @@ class Connect: """Sends HTTP POST to router running a hyperglass API agent""" log.debug(f"Query parameters: {self.query}") - uri = Supported.map_rest(self.device.nos) - headers = { - "Content-Type": "application/json", - "X-API-Key": self.device.credential.password.get_secret_value(), - } - http_protocol = protocol_map.get(self.device.port, "http") - endpoint = "{protocol}://{addr}:{port}/{uri}".format( - protocol=http_protocol, - addr=self.device.address, - port=self.device.port, - uri=uri, + # uri = Supported.map_rest(self.device.nos) + headers = {"Content-Type": "application/json"} + http_protocol = protocol_map.get(self.device.port, "https") + endpoint = "{protocol}://{addr}:{port}/query".format( + protocol=http_protocol, addr=self.device.address, port=self.device.port ) log.debug(f"HTTP Headers: {headers}") log.debug(f"URL endpoint: {endpoint}") try: - http_client = httpx.AsyncClient() - responses = [] - for query in self.query: - raw_response = await http_client.post( - endpoint, headers=headers, json=query, timeout=7 - ) - log.debug(f"HTTP status code: {raw_response.status_code}") + async with httpx.Client() as http_client: + responses = [] + for query in self.query: + encoded_query = await jwt_encode( + payload=query, + secret=self.device.credential.password.get_secret_value(), + duration=params.general.request_timeout, + ) + log.debug(f"Encoded JWT: {encoded_query}") + raw_response = await http_client.post( + endpoint, + headers=headers, + json={"encoded": encoded_query}, + timeout=params.general.request_timeout, + ) + log.debug(f"HTTP status code: {raw_response.status_code}") - raw = raw_response.text - responses.append(raw) + raw = raw_response.text + log.debug(f"Raw Response: {raw}") + + if raw_response.status_code == 200: + decoded = await jwt_decode( + payload=raw_response.json()["encoded"], + secret=self.device.credential.password.get_secret_value(), + ) + log.debug(f"Decoded Response: {decoded}") + + responses.append(decoded) + else: + log.error(raw_response.text) response = "\n\n".join(responses) log.debug(f"Output for query {self.query}:\n{response}") - except ( - httpx.exceptions.ConnectTimeout, - httpx.exceptions.CookieConflict, - httpx.exceptions.DecodingError, - httpx.exceptions.InvalidURL, - httpx.exceptions.PoolTimeout, - httpx.exceptions.ProtocolError, - httpx.exceptions.ReadTimeout, - httpx.exceptions.RedirectBodyUnavailable, - httpx.exceptions.RedirectLoop, - httpx.exceptions.ResponseClosed, - httpx.exceptions.ResponseNotRead, - httpx.exceptions.StreamConsumed, - httpx.exceptions.Timeout, - httpx.exceptions.TooManyRedirects, - httpx.exceptions.WriteTimeout, - ) as rest_error: + except httpx.exceptions.HTTPError as rest_error: rest_msg = " ".join( re.findall(r"[A-Z][^A-Z]*", rest_error.__class__.__name__) ) diff --git a/requirements.txt b/requirements.txt index 47b1bc9..1f89c8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ aredis==1.1.5 click==7.0 +cryptography==2.8 hiredis==1.0.0 -httpx==0.6.8 +httpx==0.9.* jinja2==2.10.1 logzero==1.5.0 markdown2==2.3.8 @@ -9,6 +10,7 @@ netmiko==2.4.1 passlib==1.7.1 prometheus_client==0.7.1 pydantic==0.32.2 +pyjwt==1.7.1 pyyaml==5.1.1 redis==3.2.1 sanic-limiter==0.1.3