From 981f73a8e991b06e0215a51b3e0f2d501b4da722 Mon Sep 17 00:00:00 2001 From: Stefan Tatschner Date: Tue, 25 Oct 2022 10:43:17 +0200 Subject: [PATCH] docs: Add public API and remove autogenerated docs --- Makefile | 1 - docs/.gitignore | 1 - docs/Makefile | 6 ---- docs/api.md | 11 ++++++ docs/conf.py | 1 + docs/index.md | 2 +- docs/transports.md | 2 ++ src/gallia/transports/base.py | 67 ++++++++++++++++++++++++++++++++++- 8 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 docs/api.md diff --git a/Makefile b/Makefile index bea686e5c..e498f1d78 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,6 @@ fmt: .PHONY: docs docs: - $(MAKE) -C docs api-stubs $(MAKE) -C docs html .PHONY: test diff --git a/docs/.gitignore b/docs/.gitignore index 87831a4dd..ee8922b27 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -3,4 +3,3 @@ # SPDX-License-Identifier: CC0-1.0 /_build -/api diff --git a/docs/Makefile b/docs/Makefile index 0e4f88a0e..b1e294baf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -18,12 +18,6 @@ help: .PHONY: help Makefile -.PHONY: api-stubs -api-stubs: - sphinx-apidoc -o api ../src - sed -i "1 s|.*|Modules|" api/modules.rst - sed -i "2 s|.*|=======|" api/modules.rst - # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 000000000..6433378cd --- /dev/null +++ b/docs/api.md @@ -0,0 +1,11 @@ +# Public API + +## gallia.transports.base + +All available transports are documented in {doc}`../transports`). + +```{eval-rst} +.. automodule:: gallia.transports.base + :members: + :show-inheritance: +``` diff --git a/docs/conf.py b/docs/conf.py index acdcec9f2..f24f8742a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,6 +44,7 @@ "myst_parser", ] +myst_heading_anchors = 2 myst_enable_extensions = ["deflist"] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/index.md b/docs/index.md index 9240f7b8f..df5c5e54d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,7 +52,7 @@ Several concepts and ideas are implemented in `gallia` in order to provide compr :maxdepth: 1 :caption: API -api/modules +api ``` `gallia` is designed as a pentesting framework where each test produces a lot of data. diff --git a/docs/transports.md b/docs/transports.md index 08627b2e6..cbb0b7ff6 100644 --- a/docs/transports.md +++ b/docs/transports.md @@ -25,6 +25,8 @@ The relevant transport protocol is specified in the scheme. ### isotp +ISO-TP (ISO 15765-2) as provided by the Linux [socket API](https://www.kernel.org/doc/html/latest/networking/can.html). + The can interface is specified as a host, e.g. `can0`. The following parameters are available (these are ISOTP transport settings): diff --git a/src/gallia/transports/base.py b/src/gallia/transports/base.py index daf024f2c..612b1d8db 100644 --- a/src/gallia/transports/base.py +++ b/src/gallia/transports/base.py @@ -15,6 +15,14 @@ class TargetURI: + """TargetURI represents a target to which gallia can connect. + The target string must conform to a URI is specified by RFC3986. + + Basically, this is a wrapper around Python's `urlparse()` and + `parse_qs()` methods. TargetURI provides frequently used properties + for a more userfriendly usage. Instances are meant to be passed to + the `connect()` method of transports. + """ def __init__(self, raw: str) -> None: self.raw = raw self.url = urlparse(raw) @@ -28,32 +36,47 @@ def from_parts( port: int | None, args: dict[str, Any], ) -> TargetURI: + """Constructs a instance of TargetURI with the given arguments. + The `args` dict is used for the query string. + """ netloc = host if port is None else join_host_port(host, port) return TargetURI(urlunparse((scheme, netloc, "", "", urlencode(args), ""))) @property def scheme(self) -> str: + """The URI scheme""" return self.url.scheme @property def hostname(self) -> str | None: + """The hostname (without port)""" return self.url.hostname @property def port(self) -> int | None: + """The port number""" return self.url.port @property def netloc(self) -> str: + """The hostname and the portnumber, separated by a colon.""" return self.url.netloc @property def location(self) -> str: - assert self.scheme != "", "url scheme is empty" + """A URI string which only consists of the relevant scheme, + the host and the port. + """ return f"{self.scheme}://{self.url.netloc}" @property def qs_flat(self) -> dict[str, str]: + """A dict which contains the query string's key/value pairs. + In case a key appears multiple times, this variant only + contains the first found key/value pair. In contrast to + `self.qs`, this variant avoids lists and might be easier + to use for some cases. + """ d = {} for k, v in self.qs.items(): d[k] = v[0] @@ -68,7 +91,26 @@ def __str__(self) -> str: class BaseTransport(ABC): + """BaseTransport is the base class providing the required + interface for all transports used by gallia. + + A transport usually is some kind of network protocol which + carries an application level protocol. A good example is + DoIP carrying UDS requests which acts as a minimal middleware + on top of TCP. + + This class is to be used as a subclass with all abstractmethods + implemented and the SCHEME property filled. + + A few methods provide a `tags` argument. The debug logs of these + calls include these tags in the `tags` property of the relevant + `gallia.log.PenlogRecord`. + """ + + #: The scheme for the implemented protocol, e.g. "doip". SCHEME: str = "" + #: The buffersize of the transport. Might be used in read() calls. + #: Defaults to `io.DEFAULT_BUFFER_SIZE`. BUFSIZE: int = io.DEFAULT_BUFFER_SIZE def __init__(self, target: TargetURI) -> None: @@ -90,6 +132,7 @@ def __init_subclass__( @classmethod def check_scheme(cls, target: TargetURI) -> None: + """Checks if the provided URI has the correct scheme.""" if target.scheme != cls.SCHEME: raise ValueError(f"invalid scheme: {target.scheme}; expected: {cls.SCHEME}") @@ -100,13 +143,22 @@ async def connect( target: str | TargetURI, timeout: float | None = None, ) -> TransportT: + """Classmethod to connect the transport to a relevant target. + The target argument is a URI, such as `doip://192.0.2.2:13400?src_addr=0xf4&dst_addr=0x1d"` + An instance of the relevant transport class is returned. + """ ... @abstractmethod async def close(self) -> None: + """Terminates the connection and clean up all allocated ressources.""" ... async def reconnect(self: TransportT, timeout: float | None = None) -> TransportT: + """Closes the connection to the target and reconnects. A new + instance of this class is returned rendering the old one + obsolete. This method is safe for concurrent use. + """ async with self.mutex: await self.close() return await self.connect(self.target) @@ -117,6 +169,10 @@ async def read( timeout: float | None = None, tags: list[str] | None = None, ) -> bytes: + """Reads one message and returns its raw byte representation. + An example for one message is 'one line, terminated by \\n' for + a TCP transport yielding lines. + """ ... @abstractmethod @@ -126,6 +182,7 @@ async def write( timeout: float | None = None, tags: list[str] | None = None, ) -> int: + """Writes one message and return the number of written bytes.""" ... async def request( @@ -134,6 +191,10 @@ async def request( timeout: float | None = None, tags: list[str] | None = None, ) -> bytes: + """Chains a `self.write()` call with a `self.read()` call. + The call is protected by a mutex and is thus safe for concurrent + use. + """ async with self.mutex: return await self.request_unsafe(data, timeout, tags) @@ -143,5 +204,9 @@ async def request_unsafe( timeout: float | None = None, tags: list[str] | None = None, ) -> bytes: + """Chains a `self.write()` call with a `self.read()` call. + The call is **not** protected by a mutex. Only use this method + when you know what you are doing. + """ await self.write(data, timeout, tags) return await self.read(timeout, tags)