Skip to content

Commit d9f79bd

Browse files
bjoernricksgreenbonebot
authored andcommitted
Add: Add first draft of a core GMP protocol implementation
Implement the GMP as an IO independent software stack. With this core protocol it will be possible to implement all kind of stacks for GMP easily.
1 parent 3f3652a commit d9f79bd

File tree

9 files changed

+730
-0
lines changed

9 files changed

+730
-0
lines changed

gvm/protocols/gmp/core/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# SPDX-FileCopyrightText: 2024 Greenbone AG
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from ._connection import Connection
6+
from ._request import Request
7+
from ._response import Response, StatusError
8+
9+
__all__ = (
10+
"Connection",
11+
"Request",
12+
"Response",
13+
"StatusError",
14+
)

gvm/protocols/gmp/core/_connection.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# SPDX-FileCopyrightText: 2024 Greenbone AG
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from typing import AnyStr, Optional, Protocol
6+
7+
from lxml import etree
8+
9+
from gvm.errors import GvmError
10+
11+
from ._request import Request
12+
from ._response import Response
13+
14+
15+
class XmlReader:
16+
"""
17+
Read a XML command until its closing element
18+
"""
19+
20+
def start_xml(self) -> None:
21+
self._first_element: Optional[etree._Element] = None
22+
# act on start and end element events and
23+
# allow huge text data (for report content)
24+
self._parser = etree.XMLPullParser(
25+
events=("start", "end"), huge_tree=True
26+
)
27+
28+
def is_end_xml(self) -> bool:
29+
for action, obj in self._parser.read_events():
30+
if not self._first_element and action in "start":
31+
self._first_element = obj.tag # type: ignore
32+
33+
if (
34+
self._first_element
35+
and action in "end"
36+
and str(self._first_element) == str(obj.tag) # type: ignore
37+
):
38+
return True
39+
return False
40+
41+
def feed_xml(self, data: AnyStr) -> None:
42+
try:
43+
self._parser.feed(data)
44+
except etree.ParseError as e:
45+
raise GvmError(
46+
f"Cannot parse XML response. Response data read {data!r}",
47+
e,
48+
) from None
49+
50+
51+
class InvalidStateError(GvmError):
52+
def __init__(self, message: str = "Invalid State", *args):
53+
super().__init__(message, *args)
54+
55+
56+
class State(Protocol):
57+
def __set_context__(self, context: "Context") -> None: ...
58+
def send(self, request: Request) -> bytes: ...
59+
def receive_data(self, data: bytes) -> Optional[Response]: ...
60+
def close(self) -> None: ...
61+
62+
63+
class Context(Protocol):
64+
def __set_state__(self, state: State) -> None: ...
65+
66+
67+
class AbstractState:
68+
_context: Context
69+
70+
def __set_context__(self, context: Context) -> None:
71+
self._context = context
72+
73+
def set_next_state(self, next_state: State) -> None:
74+
self._context.__set_state__(next_state)
75+
76+
77+
class InitialState(AbstractState):
78+
def send(self, request: Request) -> bytes:
79+
self.set_next_state(AwaitingResponseState(request))
80+
return bytes(request)
81+
82+
def receive_data(self, data: bytes) -> Optional[Response]:
83+
raise InvalidStateError()
84+
85+
def close(self) -> None:
86+
# nothing to do
87+
return
88+
89+
90+
class AwaitingResponseState(AbstractState):
91+
def __init__(self, request: Request) -> None:
92+
self._request = request
93+
94+
def send(self, request: Request) -> bytes:
95+
raise InvalidStateError()
96+
97+
def close(self) -> None:
98+
self.set_next_state(InitialState())
99+
100+
def receive_data(self, data: bytes) -> Optional[Response]:
101+
next_state = ReceivingDataState(self._request)
102+
self.set_next_state(next_state)
103+
return next_state.receive_data(data)
104+
105+
106+
class ErrorState(AbstractState):
107+
message = (
108+
"The connection is in an error state. Please close the connection."
109+
)
110+
111+
def send(self, request: Request) -> bytes:
112+
raise InvalidStateError(self.message)
113+
114+
def close(self) -> None:
115+
self.set_next_state(InitialState())
116+
117+
def receive_data(self, data: bytes) -> Optional[Response]:
118+
raise InvalidStateError(self.message)
119+
120+
121+
class ReceivingDataState(AbstractState):
122+
def __init__(self, request: Request) -> None:
123+
self._request = request
124+
self._data = bytearray()
125+
self._reader = XmlReader()
126+
self._reader.start_xml()
127+
128+
def send(self, request: Request) -> bytes:
129+
raise InvalidStateError()
130+
131+
def close(self) -> None:
132+
self.set_next_state(InitialState())
133+
134+
def receive_data(self, data: bytes) -> Optional[Response]:
135+
self._data += data
136+
try:
137+
self._reader.feed_xml(data)
138+
except GvmError as e:
139+
self.set_next_state(ErrorState())
140+
raise e
141+
142+
if not self._reader.is_end_xml():
143+
return None
144+
145+
self.set_next_state(InitialState())
146+
return Response(data=bytes(self._data), request=self._request)
147+
148+
149+
class Connection:
150+
"""
151+
This is a [SansIO]() connection and not a socket connection
152+
153+
It is responsible for
154+
"""
155+
156+
def __init__(self) -> None:
157+
self.__set_state__(InitialState())
158+
159+
def send(self, request: Request) -> bytes:
160+
return self._state.send(request)
161+
162+
def receive_data(self, data: bytes) -> Optional[Response]:
163+
return self._state.receive_data(data)
164+
165+
def close(self) -> None:
166+
return self._state.close()
167+
168+
def __set_state__(self, state: State) -> None:
169+
self._state = state
170+
self._state.__set_context__(self)

gvm/protocols/gmp/core/_request.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SPDX-FileCopyrightText: 2024 Greenbone AG
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from typing import Protocol, runtime_checkable
6+
7+
8+
@runtime_checkable
9+
class Request(Protocol):
10+
def __bytes__(self) -> bytes: ...
11+
def __str__(self) -> str: ...

gvm/protocols/gmp/core/_response.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# SPDX-FileCopyrightText: 2024 Greenbone AG
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from functools import cached_property
6+
from typing import Optional
7+
8+
from typing_extensions import Self
9+
10+
from gvm.errors import GvmError
11+
from gvm.xml import Element, parse_xml
12+
13+
from ._request import Request
14+
15+
16+
class StatusError(GvmError):
17+
def __init__(self, message: str | None, *args, response: "Response"):
18+
super().__init__(message, *args)
19+
self.response = response
20+
self.request = response.request
21+
22+
23+
class Response:
24+
def __init__(self, *, request: Request, data: bytes) -> None:
25+
self._request = request
26+
self._data = data
27+
self.__xml: Optional[Element] = None
28+
29+
def __root_element(self) -> Element:
30+
if self.__xml is None:
31+
self.__xml = self.xml()
32+
return self.__xml
33+
34+
def xml(self) -> Element:
35+
return parse_xml(self.data)
36+
37+
@property
38+
def data(self) -> bytes:
39+
return self._data
40+
41+
@property
42+
def request(self) -> Request:
43+
return self._request
44+
45+
@cached_property
46+
def status_code(self) -> Optional[int]:
47+
root = self.__root_element()
48+
try:
49+
status = root.attrib["status"]
50+
return int(status)
51+
except (KeyError, ValueError):
52+
return None
53+
54+
@property
55+
def is_success(self) -> bool:
56+
status = self.status_code
57+
return status is not None and 200 <= status <= 299
58+
59+
def raise_for_status(self) -> Self:
60+
if self.is_success:
61+
return self
62+
raise StatusError(
63+
f"Invalid status code {self.status_code}", response=self
64+
)
65+
66+
def __bytes__(self) -> bytes:
67+
return self._data
68+
69+
def __str__(self) -> str:
70+
return self._data.decode()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# SPDX-FileCopyrightText: 2024 Greenbone AG
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from ._auth import Authentication
6+
from ._port_list import PortList, PortRangeType
7+
from ._resource_names import ResourceNames, ResourceType
8+
from ._version import Version
9+
10+
__all__ = (
11+
"Authentication",
12+
"PortList",
13+
"PortRangeType",
14+
"Version",
15+
"ResourceNames",
16+
"ResourceType",
17+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# SPDX-FileCopyrightText: 2024 Greenbone AG
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from gvm.errors import RequiredArgument
6+
from gvm.xml import XmlCommand
7+
8+
from .._request import Request
9+
10+
11+
class Authentication:
12+
13+
@classmethod
14+
def authenticate(cls, username: str, password: str) -> Request:
15+
"""Authenticate to gvmd.
16+
17+
The generated authenticate command will be send to server.
18+
Afterwards the response is read, transformed and returned.
19+
20+
Args:
21+
username: Username
22+
password: Password
23+
"""
24+
cmd = XmlCommand("authenticate")
25+
26+
if not username:
27+
raise RequiredArgument(
28+
function=cls.authenticate.__name__, argument="username"
29+
)
30+
31+
if not password:
32+
raise RequiredArgument(
33+
function=cls.authenticate.__name__, argument="password"
34+
)
35+
36+
credentials = cmd.add_element("credentials")
37+
credentials.add_element("username", username)
38+
credentials.add_element("password", password)
39+
return cmd
40+
41+
@staticmethod
42+
def describe_auth() -> Request:
43+
"""Describe authentication methods
44+
45+
Returns a list of all used authentication methods if such a list is
46+
available.
47+
"""
48+
return XmlCommand("describe_auth")
49+
50+
@classmethod
51+
def modify_auth(
52+
cls, group_name: str, auth_conf_settings: dict[str, str]
53+
) -> Request:
54+
"""Modifies an existing authentication.
55+
56+
Arguments:
57+
group_name: Name of the group to be modified.
58+
auth_conf_settings: The new auth config.
59+
"""
60+
if not group_name:
61+
raise RequiredArgument(
62+
function=cls.modify_auth.__name__, argument="group_name"
63+
)
64+
if not auth_conf_settings:
65+
raise RequiredArgument(
66+
function=cls.modify_auth.__name__,
67+
argument="auth_conf_settings",
68+
)
69+
70+
cmd = XmlCommand("modify_auth")
71+
group = cmd.add_element("group", attrs={"name": str(group_name)})
72+
73+
for key, value in auth_conf_settings.items():
74+
auth_conf = group.add_element("auth_conf_setting")
75+
auth_conf.add_element("key", key)
76+
auth_conf.add_element("value", value)
77+
78+
return cmd

0 commit comments

Comments
 (0)