# encoding: utf-8
# pysap - Python library for crafting SAP's network protocols packets
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# Author:
# Martin Gallo (@martingalloar)
# Code contributed by SecureAuth to the OWASP CBAS project
#
# Standard imports
import re
import logging
from socket import error as SocketError
# External imports
from scapy.layers.inet import TCP
from scapy.packet import Packet, bind_layers, Raw
from scapy.supersocket import socket, StreamSocket
from scapy.fields import (ByteField, ShortField, ConditionalField, StrField,
IntField, StrNullField, PacketListField,
FieldLenField, FieldListField, SignedIntEnumField,
StrFixedLenField, PacketField, BitField, LongField,
ByteEnumKeysField)
# Custom imports
from pysap.SAPSNC import SAPSNCFrame
from pysap.SAPNI import (SAPNI, SAPNIStreamSocket, SAPNIProxy,
SAPNIProxyHandler)
from pysap.utils.fields import (PacketNoPadded, StrNullFixedLenField)
# Create a logger for the SAPRouter layer
log_saprouter = logging.getLogger("pysap.saprouter")
# Router Opcode values
router_control_opcodes = {
0: "Error information",
1: "Version request",
2: "Version response",
5: "Send Handle (5)", # TODO: Check this opcodes
6: "Send Handle (6)", # TODO: Check this opcodes
8: "Send Handle (8)", # TODO: Check this opcodes
70: "SNC request",
71: "SNC handshake complete",
}
"""Router Opcode values"""
# Router Return Code values (as per SAP Note 63342 https://launchpad.support.sap.com/#/notes/63342)
router_return_codes = {
-1: "NI-internal error (NIEINTERN)",
-2: "Host name unknown (NIEHOST_UNKNOWN)",
-3: "Service unknown (NIESERV_UNKNOWN)",
-4: "Service already used (NIESERV_USED)",
-5: "Time limit reached (NIETIMEOUT)",
-6: "Connection to partner broken (NIECONN_BROKEN)",
-7: "Data range too small (NIETOO_SMALL)",
-8: "Invalid parameters (NIEINVAL)",
-9: "Wake-Up (without data) (NIEWAKEUP)",
-10: "Connection setup failed (NIECONN_REFUSED)",
-11: "PING/PONG signal received (NIEPING)",
-12: "Connection to partner via NiRouter not yet set up (NIECONN_PENDING)",
-13: "Invalid version (NIEVERSION)",
-14: "Local hostname cannot be found (NIEMYHOSTNAME)",
-15: "No free port in range (NIENOFREEPORT)",
-16: "Local hostname invalid (NIEMYHOST_VERIFY)",
-17: "Error in the SNC shift in the saprouter ==> (NIESNC_FAILURE)",
-18: "Opcode received (NIEOPCODE)",
-19: "queue limit reached, next package not accepted (NIEQUE_FULL)",
-20: "Requested package too large (NIETOO_BIG)",
-90: "Host name unknown (NIEROUT_HOST_UNKNOWN)",
-91: "Service unknown (NIEROUT_SERV_UNKNOWN)",
-92: "Connection setup failed (NIEROUT_CONN_REFUSED)",
-93: "NI-internal errors (NIEROUT_INTERN)",
-94: "Connect from source to destination not allowed (NIEROUT_PERM_DENIED)",
-95: "Connection terminated (NIEROUT_CONN_BROKEN)",
-96: "Invalid client version (NIEROUT_VERSION)",
-97: "Connection cancelled by administrator (NIEROUT_CANCELED)",
-98: "saprouter shutdown (NIEROUT_SHUTDOWN)",
-99: "Information request refused (NIEROUT_INFO_DENIED)",
-100: "Max. number of clients reached (NIEROUT_OVERFLOW)",
-101: "Talkmode not allowed (NIEROUT_MODE_DENIED)",
-102: "Client not available (NIEROUT_NOCLIENT)",
-103: "Error in external library (NIEROUT_EXTERN)",
-104: "Error in the SNC shift (NIEROUT_SNC_FAILURE)",
}
"""Router Return Code values"""
# Router Administration Command values
router_adm_commands = {
2: "Information Request",
3: "New Route Table Request",
4: "Toggle Trace Request",
5: "Stop Request",
6: "Cancel Route Request",
7: "Dump Buffers Request",
8: "Flush Buffers Request",
9: "Soft Shutdown Request",
10: "Set Trace Peer",
11: "Clear Trace Peer",
12: "Trace Connection",
13: "Trace Connection",
14: "Hide Error Information Request",
}
"""Router Administration Command values"""
ROUTER_TALK_MODE_NI_MSG_IO = 0
ROUTER_TALK_MODE_NI_RAW_IO = 1
ROUTER_TALK_MODE_NI_ROUT_IO = 2
# Router NI Talk mode values
router_ni_talk_mode_values = {
ROUTER_TALK_MODE_NI_MSG_IO: "NI_MSG_IO",
ROUTER_TALK_MODE_NI_RAW_IO: "NI_RAW_IO",
ROUTER_TALK_MODE_NI_ROUT_IO: "NI_ROUT_IO",
}
"""Router NI Talk mode values"""
[docs]class SAPRouterRouteHop(PacketNoPadded):
"""SAP Router Protocol Route Hop
This packet is used to describe a hop in a route using the SAP Router.
"""
name = "SAP Router Route Hop"
fields_desc = [
StrNullField("hostname", None),
StrNullField("port", None),
StrNullField("password", None),
]
regex = re.compile(r"""
(/[hH]/(?P<hostname>[\w\.\-]+) # Hostname, FQDN or IP addresss
(/[sS]/(?P<port>[\w]+))? # Optional port/service
(/[pwPW]/(?P<password>[\w.]+))? # Optional password
)
""", re.VERBOSE)
""" :cvar: Regular expression for matching route strings
:type: regex
"""
[docs] @classmethod
def from_string(cls, route_string):
"""Build a list of route hops from a route string. The format of a
route string is:
(/H/host/S/service/W/pass)*
or for older versions (<4.0):
(/H/host/S/service/P/pass)*
:param route_string: route string
:type route_string: C{string}
:return: route hops in the route string
:rtype: ``list`` of :class:`SAPRouterRouteHop`
"""
result = []
for route_hop in [x.groupdict() for x in cls.regex.finditer(route_string)]:
result.append(cls(hostname=route_hop["hostname"],
port=route_hop["port"],
password=route_hop["password"]))
return result
[docs] @classmethod
def from_hops(cls, route_hops):
"""Build a route string from a list of route hops.
:param route_hops: route hops
:type route_hops: ``list`` of :class:`SAPRouterRouteHop`
:return: route string
:rtype: C{string}
"""
result = ""
for route_hop in route_hops:
result += "/H/{}".format(route_hop.hostname)
if route_hop.port:
result += "/S/{}".format(route_hop.port)
if route_hop.password:
result += "/W/{}".format(route_hop.password)
return result
[docs]class SAPRouterInfoClient(PacketNoPadded):
"""SAP Router Protocol Information Request Client info
This packet is used to return the information of a connected client.
"""
name = "SAP Router Client Info"
fields_desc = [ # 137 bytes length
IntField("id", 1),
BitField("flag_XXX1", 0, 1),
BitField("flag_XXX2", 0, 1),
BitField("flag_XXX3", 0, 1),
BitField("flag_XXX4", 0, 1),
BitField("flag_XXX5", 0, 1),
BitField("flag_traced", 0, 1),
BitField("flag_connected", 0, 1),
BitField("flag_routed", 0, 1),
LongField("connected_on", 0),
StrNullFixedLenField("address", None, length=45),
StrNullFixedLenField("partner", None, length=45),
StrNullFixedLenField("service", None, length=27),
StrFixedLenField("XXX3", None, length=7),
]
[docs]class SAPRouterInfoClients(PacketNoPadded):
"""SAP Router Protocol Information Request Client info list
This packet is used to return the list of current connected clients.
"""
name = "SAP Router Client Info List"
fields_desc = [
PacketListField("clients", None, SAPRouterInfoClient)
]
[docs]class SAPRouterInfoServer(PacketNoPadded):
"""SAP Router Protocol Information Request Server info
This packet is used to return information about the SAP Router
"""
name = "SAP Router Server Info"
fields_desc = [
IntField("pid", 0),
IntField("ppid", 0),
LongField("started_on", 0),
ShortField("port", 0),
ShortField("pport", 0),
]
[docs]class SAPRouterError(PacketNoPadded):
"""SAP Router Protocol Error Text
This packet is used to describe an error returned by SAP Router.
"""
name = "SAP Router Error Text"
fields_desc = [
StrNullField("eyecatcher", "*ERR*"),
StrNullField("counter", "1"),
StrNullField("error", ""),
StrNullField("return_code", ""),
StrNullField("component", "NI (network interface)"),
StrNullField("release", ""),
StrNullField("version", ""),
StrNullField("module", "nirout.cpp"),
StrNullField("line", ""),
StrNullField("detail", ""),
StrNullField("error_time", ""),
StrNullField("system_call", ""),
StrNullField("errorno", ""),
StrNullField("errorno_text", ""),
StrNullField("error_count", ""),
StrNullField("location", ""),
StrNullField("XXX5", ""),
StrNullField("XXX6", ""),
StrNullField("XXX7", ""),
StrNullField("XXX8", ""),
StrNullField("eyecatcher", "*ERR*"),
]
time_format = "%a %b %d %H:%M:%S %Y"
""" :cvar: Format to use when building the time field
:type: C{string}
"""
[docs]def router_is_route(pkt):
"""Returns if the packet is a Route packet.
:param pkt: packet to look at
:type pkt: :class:`SAPRouter`
:return: if the type of the packet is Route
:rtype: ``bool``
"""
return pkt.type == SAPRouter.SAPROUTER_ROUTE
[docs]def router_is_admin(pkt):
"""Returns if the packet is a Admin packet.
:param pkt: packet to look at
:type pkt: :class:`SAPRouter`
:return: if the type of the packet is Admin
:rtype: ``bool``
"""
return pkt.type == SAPRouter.SAPROUTER_ADMIN
[docs]def router_is_error(pkt):
"""Returns if the packet is a Error Information packet.
:param pkt: packet to look at
:type pkt: :class:`SAPRouter`
:return: if the type of the packet is Error
:rtype: ``bool``
"""
return pkt.type == SAPRouter.SAPROUTER_ERROR
[docs]def router_is_control(pkt):
"""Returns if the packet is a Control packet.
:param pkt: packet to look at
:type pkt: :class:`SAPRouter`
:return: if the type of the packet is Control
:rtype: ``bool``
"""
return pkt.type == SAPRouter.SAPROUTER_CONTROL
[docs]def router_is_pong(pkt):
"""Returns if the packet is a Pong (route accepted) packet.
:param pkt: packet to look at
:type pkt: :class:`SAPRouter`
:return: if the type of the packet is Pong
:rtype: ``bool``
"""
return pkt.type == SAPRouter.SAPROUTER_PONG
[docs]def router_is_known_type(pkt):
"""Returns if the packet is of a known type (Admin, Route, Error or Pong).
:param pkt: packet to look at
:type pkt: :class:`SAPRouter`
:return: if the type of the packet is known
:rtype: ``bool``
"""
return pkt.type in SAPRouter.router_type_values
[docs]class SAPRouter(Packet):
"""SAP Router packet
This packet is used for general SAP Router packets. There are (at least)
five types of SAP Router packets:
1. Route packets. For requesting the routing of a connection to a
remote hosts. The packet contains some general information and a
connection string with a list of routing hops (:class:`SAPRouterRouteHop`).
2. Administration packets. This packet is used for the SAP Router to
send administrative commands. It's suppose to be used only from the
hosts running the SAP Router or when an specific route is included in
the routing table. Generally administration packets are not accepted
from the external binding.
3. Error Information packets. Packets sent when an error occurred.
4. Control Message packets. Used to perform some control activities,
like retrieving the current SAPRouter version or to perform the SNC
handshake. They have the same structure that error information
packets.
5. Route accepted packet. Used to acknowledge a route request
("NI_PONG").
Routed packets and some responses doesn't fill in these five packet
types. For identifying those cases, you should check the type using the
function :class:`router_is_known_type`.
NI Versions found (unconfirmed):
- 30: Release 40C
- 36: Release <6.20
- 38: Release 7.00/7.10
- 39: Release 7.11
- 40: Release 7.20/7.21
"""
# Default router version to use
SAPROUTER_DEFAULT_VERSION = 40
# Constants for router types
SAPROUTER_ROUTE = "NI_ROUTE"
""" :cvar: Constant for route packets
:type: C{string} """
SAPROUTER_ADMIN = "ROUTER_ADM"
""" :cvar: Constant for administration packets
:type: C{string} """
SAPROUTER_ERROR = "NI_RTERR"
""" :cvar: Constant for error information packets
:type: C{string} """
SAPROUTER_CONTROL = "NI_RTERR"
""" :cvar: Constant for control messages packets
:type: C{string} """
SAPROUTER_PONG = "NI_PONG"
""" :cvar: Constant for route accepted packets
:type: C{string} """
router_type_values = [
SAPROUTER_ADMIN,
SAPROUTER_ERROR,
SAPROUTER_CONTROL,
SAPROUTER_ROUTE,
SAPROUTER_PONG,
]
""" :cvar: List of known packet types
:type: ``list`` of C{string} """
name = "SAP Router"
fields_desc = [
# General fields present in all SAP Router packets
StrNullField("type", SAPROUTER_ROUTE),
ConditionalField(ByteField("version", 2), lambda pkt:router_is_known_type(pkt) and not router_is_pong(pkt)),
# Route packets
ConditionalField(ByteField("route_ni_version", SAPROUTER_DEFAULT_VERSION), router_is_route),
ConditionalField(ByteField("route_entries", 0), router_is_route),
ConditionalField(ByteEnumKeysField("route_talk_mode", ROUTER_TALK_MODE_NI_MSG_IO, router_ni_talk_mode_values), router_is_route),
ConditionalField(ShortField("route_padd", 0), router_is_route),
ConditionalField(ByteField("route_rest_nodes", 0), router_is_route),
ConditionalField(FieldLenField("route_length", 0, length_of="route_string", fmt="I"), router_is_route),
ConditionalField(IntField("route_offset", 0), router_is_route),
ConditionalField(PacketListField("route_string", None, SAPRouterRouteHop,
length_from=lambda pkt:pkt.route_length), router_is_route),
# Admin packets
ConditionalField(ByteEnumKeysField("adm_command", 0x02, router_adm_commands), router_is_admin),
ConditionalField(ShortField("adm_unused", 0x00), lambda pkt:router_is_admin(pkt) and pkt.adm_command not in [10, 11, 12, 13]),
# Info Request fields
ConditionalField(StrNullFixedLenField("adm_password", "", 19), lambda pkt:router_is_admin(pkt) and pkt.adm_command in [2]),
# Cancel Route fields
ConditionalField(FieldLenField("adm_client_count", None, count_of="adm_client_ids", fmt="H"), lambda pkt:router_is_admin(pkt) and pkt.adm_command in [6]),
# Trace Connection fields
ConditionalField(FieldLenField("adm_client_count", None, count_of="adm_client_ids", fmt="I"), lambda pkt:router_is_admin(pkt) and pkt.adm_command in [12, 13]),
# Cancel Route or Trace Connection fields
ConditionalField(FieldListField("adm_client_ids", [0x00], IntField("", 0), count_from=lambda pkt:pkt.adm_client_count), lambda pkt:router_is_admin(pkt) and pkt.adm_command in [6, 12, 13]),
# Set/Clear Peer Trace fields # TODO: Check whether this field should be a IPv6 address or another proper field
ConditionalField(StrFixedLenField("adm_address_mask", "", 32), lambda pkt:router_is_admin(pkt) and pkt.adm_command in [10, 11]),
# Error Information/Control Messages fields
ConditionalField(ByteEnumKeysField("opcode", 0, router_control_opcodes), lambda pkt: router_is_error(pkt) or router_is_control(pkt)),
ConditionalField(ByteField("opcode_padd", 0), lambda pkt: router_is_error(pkt) or router_is_control(pkt)),
ConditionalField(SignedIntEnumField("return_code", 0, router_return_codes), lambda pkt: router_is_error(pkt) or router_is_control(pkt)),
# Error Information fields
ConditionalField(FieldLenField("err_text_length", None, length_of="err_text_value", fmt="!I"), lambda pkt: router_is_error(pkt) and pkt.opcode == 0),
ConditionalField(PacketField("err_text_value", SAPRouterError(), SAPRouterError), lambda pkt: router_is_error(pkt) and pkt.opcode == 0 and pkt.err_text_length > 0),
ConditionalField(IntField("err_text_unknown", 0), lambda pkt: router_is_error(pkt) and pkt.opcode == 0),
# Control Message fields
ConditionalField(IntField("control_text_length", 0), lambda pkt: router_is_control(pkt) and pkt.opcode != 0),
ConditionalField(StrField("control_text_value", "*ERR"), lambda pkt: router_is_control(pkt) and pkt.opcode != 0),
# SNC Frame fields
ConditionalField(PacketField("snc_frame", None, SAPSNCFrame), lambda pkt: router_is_control(pkt) and pkt.opcode in [70, 71])
]
# Retrieve the version of the remote SAP Router
[docs]def get_router_version(connection):
"""Helper function to retrieve the version of a remote SAP Router. It
uses a control packet with the 'version request' operation code. The version
is obtained either from a valid 'version response' packet or from the error
message packet if something happened.
:param connection: connection with the SAP Router
:type connection: :class:`SAPNIStreamSocket`
:return: version
"""
response = connection.sr(SAPRouter(type=SAPRouter.SAPROUTER_CONTROL,
version=SAPRouter.SAPROUTER_DEFAULT_VERSION,
opcode=1))
response.decode_payload_as(SAPRouter)
return response.version
[docs]class SAPRouteException(Exception):
"""Exception for SAP Router routing errors"""
[docs]class SAPRoutedStreamSocket(SAPNIStreamSocket):
"""Stream socket implementation for a connection routed through a SAP
Router server. It works by wrapping a :class:`SAPNIStreamSocket` and connecting
first to the SAP Router given a route string or list of :class:`SAPRouterRouteHop`.
"""
desc = "NI Stream socket routed trough a SAP Router"
def __init__(self, sock, route, talk_mode=None, router_version=None,
keep_alive=True, base_cls=None):
"""Initialize the routed stream socket. It should receive a socket
connected with the SAP Router, and a route to specify to it. After
initialization and if the route is accepted all calls to send() and
recv() would be made to the target host/service trough the SAP
Router.
:param sock: a socket connected to the SAP Router
:type sock: C{socket}
:param route: a route to specify to the SAP Router
:type route: ``list`` of :class:`SAPRouterRouteHop`
:param talk_mode: the talk mode to use when routing
:type talk_mode: ``int``
:param router_version: the router version to use for requesting the
route. If no router version is provided, it will be obtained from
the SAP Router by means of a control packet.
:type router_version: ``int``
:param keep_alive: if true, the socket will automatically respond to
keep-alive request messages. Otherwise, the keep-alive messages
are passed to the caller in :class:`recv` and :class:`sr` calls.
:type keep_alive: ``bool``
:param base_cls: the base class to use when receiving packets, it uses
SAPNI as default if no class specified
:type base_cls: :class:`Packet` class
"""
self.routed = False
self.talk_mode = talk_mode
self.router_version = router_version
# Connect to the SAP Router
SAPNIStreamSocket.__init__(self, sock, keep_alive=keep_alive,
base_cls=base_cls)
# Now that we've a NIStreamSocket, retrieve the router version if
# was not specified
if self.router_version is None:
self.router_version = get_router_version(self)
self.route_to(route, talk_mode)
[docs] def route_to(self, route, talk_mode):
"""Make the route request to the target host/service.
:param route: a route to specify to the SAP Router
:type route: ``list`` of :class:`SAPRouterRouteHop`
:param talk_mode: the talk mode to use when routing
:type talk_mode: ``int``
:raise SAPRouteException: if the route request to the target host/port
was not accepted by the SAP Router
:raise socket.error: if the connection to the target host/port failed
or the SAP Router returned an error
"""
# Build the route request packet
talk_mode = talk_mode or ROUTER_TALK_MODE_NI_MSG_IO
router_strings = list(map(str, route))
target = "%s:%d" % (route[-1].hostname, int(route[-1].port))
router_strings_lens = list(map(len, router_strings))
route_request = SAPRouter(type=SAPRouter.SAPROUTER_ROUTE,
route_ni_version=self.router_version,
route_entries=len(route),
route_talk_mode=talk_mode,
route_rest_nodes=len(route) - 1,
route_length=sum(router_strings_lens),
route_offset=router_strings_lens[0],
route_string=route)
log_saprouter.debug("Requesting route to %s using mode %d (%s)",
target, talk_mode, router_ni_talk_mode_values[talk_mode])
# Send the request and grab the response
response = self.sr(route_request)
response.decode_payload_as(SAPRouter)
if SAPRouter in response:
response = response[SAPRouter]
if router_is_pong(response):
self.routed = True
log_saprouter.debug("Route to %s accepted", target)
elif router_is_error(response) and response.return_code == -94:
log_saprouter.debug("Route to %s denied", target)
raise SAPRouteException("Route request not accepted")
else:
log_saprouter.warning("Error requesting route to %s", target)
raise Exception("Router error:", response.err_text_value)
else:
log_saprouter.warning("Error requesting route to %s", target)
raise Exception("Wrong response received")
[docs] def recv(self):
"""Receive a packet from the target host. If the talk mode in use is
native and we've already set the route, the packet received is a raw
packet. Otherwise, the packet received is a NI layer packet in the same
way the :class:`SAPNIStreamSocket` works.
"""
# If we're working on native mode and the route was accepted, we don't
# need the NI layer anymore. Just use the plain socket inside the
# NIStreamSockets.
if self.routed and self.talk_mode == ROUTER_TALK_MODE_NI_RAW_IO:
return StreamSocket.recv(self)
# If the route was not accepted yet or we're working on non-native talk
# mode, we need the NI layer.
return SAPNIStreamSocket.recv(self)
[docs] def send(self, packet):
"""Send a packet. If the talk mode in use is native the packet sent is
a raw packet. Otherwise, the packet is a NI layer packet in the same way
the :class:`SAPNIStreamSocket` works.
:param packet: packet to send
:type packet: Packet
"""
# If we're working on native mode and the route was accepted, we don't
# need the NI layer anymore. Just use the plain socket inside the
# NIStreamSockets.
if self.routed and self.talk_mode == ROUTER_TALK_MODE_NI_RAW_IO:
return StreamSocket.send(self, packet)
# If the route was not accepted yet or we're working on non-native talk
# mode, we need the NI layer.
return SAPNIStreamSocket.send(self, packet)
[docs] @classmethod
def get_nisocket(cls, host=None, port=None, route=None, password=None,
talk_mode=None, router_version=None, **kwargs):
"""Helper function to obtain a :class:`SAPRoutedStreamSocket`. If no
route is specified, it returns a plain `SAPNIStreamSocket`. If no
route is specified and the talk mode is raw, it returns a plain
`StreamSocket` as it's assumed that the NI layer is not desired.
:param host: target host to connect to if not specified in the route
:type host: C{string}
:param port: target port to connect to if not specified in the route
:type port: ``int``
:param route: route to use for determining the SAP Router to connect
:type route: C{string} or ``list`` of :class:`SAPRouterRouteHop`
:param password: target password if not specified in the route
:type password: C{string}
:param talk_mode: the talk mode to use for requesting the route
:type talk_mode: ``int``
:param router_version: the router version to use for requesting the
route
:type router_version: ``int``
:keyword kwargs: arguments to pass to :class:`SAPRoutedStreamSocket`
constructor
:return: connected socket through the specified route
:rtype: :class:`SAPRoutedStreamSocket`
:raise SAPRouteException: if the route request to the target host/port
was not accepted by the SAP Router
:raise socket.error: if the connection to the target host/port failed
or the SAP Router returned an error
"""
# If no route was provided, check the talk mode
if route is None:
# If talk mode is raw, create a new StreamSocket and get rid of the
# NI layer completely and force the base class to Raw.
if talk_mode == ROUTER_TALK_MODE_NI_RAW_IO:
sock = socket.create_connection((host, port))
if "base_cls" in kwargs:
kwargs["basecls"] = Raw
del(kwargs["base_cls"])
return StreamSocket(sock, **kwargs)
# Otherwise use the standard SAPNIStreamSocket get_nisocket method
else:
return SAPNIStreamSocket.get_nisocket(host, port, **kwargs)
# If the route was provided using a route string, convert it to a
# list of hops
if isinstance(route, str):
route = SAPRouterRouteHop.from_string(route)
# If the host and port were specified, we need to add a new hop to
# the route
if host is not None and port is not None:
route.append(SAPRouterRouteHop(hostname=host,
port=port,
password=password))
# Connect to the first hop in the route (it should be the SAP Router)
sock = socket.create_connection((route[0].hostname, int(route[0].port)))
# Create a SAPRoutedStreamSocket instance specifying the route
return cls(sock, route, talk_mode, router_version, **kwargs)
[docs]class SAPRouterNativeProxy(SAPNIProxy):
"""SAP Router Native Proxy
Proxy implementation that routes traffic through a remote SAP Router server
to a target host/port. It works by binding a :class:`SAPNIStreamSocket` and
requesting the SAP Router a route to the target location. If the route is
accepted it keeps the listener open for connections and spawn a new
:class:`SAPRouterNativeRouterHandler` instance for each client.
Example usage::
proxy = SAPRouterNativeProxy(local_host, local_port,
remote_host, remote_port,
SAPRouterNativeRouterHandler,
target_address=target_address,
target_post=target_port,
target_pass=target_pass)
proxy.handle_connection()
"""
def __init__(self, bind_address, bind_port, remote_address, remote_port,
handler, target_address, target_port, target_pass=None,
talk_mode=ROUTER_TALK_MODE_NI_MSG_IO, backlog=5, keep_alive=True, options=None):
"""Create the proxy binding a socket in the giving port, requesting the
route to the target address/port and setting the handler for the
incoming connections.
:param bind_address: address to bind the listener socket
:type bind_address: C{string}
:param bind_port: port to bind the listener socket
:type bind_port: ``int``
:param remote_address: remote address to connect to
:param remote_address: C{string}
:param remote_port: remote port to connect to
:type remote_port: ``int``
:param handler: handler class
:type handler: :class:`SAPNIProxyHandler` class
:param target_address: target address to connect to
:param target_address: C{string}
:param target_port: target port to connect to
:type target_port: ``int``
:param target_pass: target password to use when requesting the route
:param target_pass: C{string}
:param talk_mode: talk mode to use when requesting the route
:type talk_mode: ``int``
:param backlog: backlog parameter to set in the listener socket
:type backlog: ``int``
:param keep_alive: if true, the proxy will handle the keep-alive
requests and responses. Otherwise, keep-alive messages are passed
to the handler as regular packets.
:type keep_alive: ``bool``
:param options: options to pass to the handler instance
:type options: ``dict``
:raise SAPRouteException: if the route request is denied
:raise Exception: if an error occurred when requesting the route
"""
super(SAPRouterNativeProxy, self).__init__(bind_address, bind_port,
remote_address, remote_port,
handler or SAPRouterNativeRouterHandler,
backlog, keep_alive, options)
self.target_address = target_address
self.target_port = target_port
self.target_pass = target_pass
self.talk_mode = talk_mode
self.routed = False
self.route()
[docs] def handle_connection(self):
"""Block until a connection is received from the listener, request
a route to forward the traffic through the remote SAP Router and
handle the client using the provided handler class.
:return: the handler instance handling the request
:rtype: :class:`SAPNIProxyHandler`
"""
# Accept a client connection
(client, __) = self.listener.ins.accept()
# Creates a remote socket
router = self.route()
# Create the NI Stream Socket and handle it
proxy = self.handler(SAPNIStreamSocket(client, self.keep_alive),
router,
self.options)
return proxy
[docs] def route(self):
"""Requests a route to forward the traffic through the remote SAP
Router.
:raise SAPRouteException: if the route request is denied
:raise Exception: if an error occurred when requesting the route
"""
log_saprouter.debug("Routing to %s:%d", self.target_address, self.target_port)
# Creates the connection with the SAP Router
(remote_address, remote_port) = self.remote_host
router = SAPNIStreamSocket.get_nisocket(remote_address, remote_port,
keep_alive=self.keep_alive)
# Build the Route request packet
if self.options.target_route_string is None:
router_string = [SAPRouterRouteHop(hostname=remote_address,
port=remote_port),
SAPRouterRouteHop(hostname=self.target_address,
port=self.target_port,
password=self.target_pass)]
else:
router_string = SAPRouterRouteHop.from_string(self.options.target_route_string)
router_string_lens = list(map(len, list(map(str, router_string))))
p = SAPRouter(type=SAPRouter.SAPROUTER_ROUTE,
route_entries=len(router_string),
route_talk_mode=self.talk_mode,
route_rest_nodes=len(router_string) - 1,
route_length=sum(router_string_lens),
route_offset=router_string_lens[0],
route_string=router_string)
# Send the request and grab the response
response = router.sr(p)
if SAPRouter in response:
response = response[SAPRouter]
if router_is_pong(response):
log_saprouter.debug("Route request to %s:%d accepted by %s:%d",
self.target_address, self.target_port,
remote_address, remote_port)
self.routed = True
elif router_is_error(response) and response.return_code == -94:
log_saprouter.debug("Route request to %s:%d not accepted by %s:%d",
self.target_address, self.target_port,
remote_address, remote_port)
raise SAPRouteException("Route request not accepted")
else:
log_saprouter.error("Router send error: %s", response.err_text_value)
raise Exception("Router error: %s", response.err_text_value)
else:
log_saprouter.error("Wrong response received")
raise Exception("Wrong response received")
return router
[docs]class SAPRouterNativeRouterHandler(SAPNIProxyHandler):
"""SAP Router Native Proxy Handler
Handles packets routed through a remote SAP Router. It works by bypassing
the SAP NI layer in order to allow native traffic.
"""
def __init__(self, client, server, options=None):
self.options = options
self.mtu = 2048
super(SAPRouterNativeRouterHandler, self).__init__(client, server, options)
[docs] def recv_send(self, local, remote, process):
"""Receives data from one socket connection, process it and send to the
remote connection.
:param local: the local socket
:type local: :class:`SAPNIStreamSocket`
:param remote: the remote socket
:type remote: :class:`SAPNIStreamSocket`
:param process: the function that process the incoming data
:type process: function
"""
# Receive a native packet (not SAP NI)
packet = local.ins.recv(self.mtu)
log_saprouter.debug("Received %d native bytes", len(packet))
# Handle close connection
if len(packet) == 0:
local.close()
raise SocketError((100, "Underlying stream socket tore down"))
# Send the packet to the remote peer
remote.ins.sendall(packet)
log_saprouter.debug("Sent %d native bytes", len(packet))
# Bind SAP NI with the SAP Router port
bind_layers(TCP, SAPNI, dport=3299)