Skip to content

client

Protocol factory -- client.

ClientFactory

ClientFactory(motd: bytes, blacklist: List[str], metar_manager: MetarManager, plugin_manager: PluginManager, db_engine: AsyncEngine)

Factory of ClientProtocol.

Attributes:

  • clients (Dict[bytes, Client]) –

    All clients, Dict[callsign(bytes), Client]

  • heartbeat_task (Task[NoReturn] | None) –

    Task to send heartbeat to clients.

  • motd (List[bytes]) –

    The Message Of The Day.

  • blacklist (List[str]) –

    IP blacklist.

  • metar_manager (MetarManager) –

    The metar manager.

  • plugin_manager (PluginManager) –

    The plugin manager.

  • db_engine (AsyncEngine) –

    Async sqlalchemy engine.

  • password_hasher (PasswordHasher) –

    Argon2 password hasher.

Source code in src/pyfsd/factory/client.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def __init__(
    self,
    motd: bytes,
    blacklist: List[str],
    metar_manager: "MetarManager",
    plugin_manager: "PluginManager",
    db_engine: "AsyncEngine",
) -> None:
    """Create a ClientFactory instance."""
    self.clients = {}
    self.heartbeat_task = None
    self.motd = motd.splitlines()
    self.blacklist = blacklist
    self.metar_manager = metar_manager
    self.plugin_manager = plugin_manager
    self.db_engine = db_engine
    self.password_hasher = PasswordHasher()

__call__

__call__() -> ClientProtocol

Create a ClientProtocol instance.

Source code in src/pyfsd/factory/client.py
115
116
117
def __call__(self) -> ClientProtocol:
    """Create a ClientProtocol instance."""
    return ClientProtocol(self)

broadcast

broadcast(*lines: bytes, check_func: BroadcastChecker = lambda , : True, auto_newline: bool = True, from_client: Optional[Client] = None) -> bool

Broadcast a message.

Parameters:

  • lines (bytes, default: () ) –

    Lines to be broadcasted.

  • check_func (BroadcastChecker, default: lambda , : True ) –

    Function to check if message should be sent to a client.

  • auto_newline (bool, default: True ) –

    Auto put newline marker between lines or not.

  • from_client (Optional[Client], default: None ) –

    Where the message from.

Return

Lines sent to at least client or not.

Source code in src/pyfsd/factory/client.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def broadcast(
    self,
    *lines: bytes,
    check_func: "BroadcastChecker" = lambda _, __: True,
    auto_newline: bool = True,
    from_client: Optional["Client"] = None,
) -> bool:
    """Broadcast a message.

    Args:
        lines: Lines to be broadcasted.
        check_func: Function to check if message should be sent to a client.
        auto_newline: Auto put newline marker between lines or not.
        from_client: Where the message from.

    Return:
        Lines sent to at least client or not.
    """
    have_one = False
    data = join_lines(*lines, newline=auto_newline)
    for client in self.clients.values():
        if client == from_client:
            continue
        if not check_func(from_client, client):
            continue
        have_one = True
        if not client.transport.is_closing():
            client.transport.write(data)
    return have_one

check_auth async

check_auth(username: str, password: str) -> Optional[int]

Check if password and username is correct.

Source code in src/pyfsd/factory/client.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
async def check_auth(self, username: str, password: str) -> Optional[int]:
    """Check if password and username is correct."""

    # This function updates hash (argon2)
    async def update_hashed(new_hashed: str) -> None:
        async with self.db_engine.begin() as conn:
            await conn.execute(
                update(users_table)
                .where(users_table.c.callsign == username)
                .values(password=new_hashed)
            )

    # Fetch current hashed password and rating
    async with self.db_engine.begin() as conn:
        infos = tuple(
            await conn.execute(
                select(users_table.c.password, users_table.c.rating).where(
                    users_table.c.callsign == username
                )
            )
        )
    if len(infos) == 0:  # User not found
        return None
    if len(infos) != 1:  # User duplicated
        raise RuntimeError(f"Duplicated callsign in users database: {username}")
    hashed, rating = cast(Tuple[str, int], infos[0])

    # =============== Check if hash is sha256
    if len(hashed) == 64:  # hash is sha256
        if sha256(password.encode()).hexdigest() == hashed:  # correct
            # Now we have the plain password, save it as argon2
            new_hashed = self.password_hasher.hash(password)
            await update_hashed(new_hashed)
            return rating
        return None  # incorrect
    # =============== Check argon2
    try:
        self.password_hasher.verify(hashed, password)
    except exceptions.VerifyMismatchError:  # Incorrect
        return None
    except exceptions.InvalidHashError:
        await logger.aerror(f"Invalid hash found in users table: {hashed}")
        return None
    except BaseException:  # What happened?
        await logger.aexception("Uncaught exception when vaildating password")
        return None
    # Check if need rehash
    if self.password_hasher.check_needs_rehash(hashed):
        await update_hashed(self.password_hasher.hash(password))
    return rating

get_heartbeat_task

get_heartbeat_task() -> Task[NoReturn]

Get heartbeat task.

Source code in src/pyfsd/factory/client.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def get_heartbeat_task(self) -> "Task[NoReturn]":
    """Get heartbeat task."""
    if self.heartbeat_task is not None:
        return self.heartbeat_task

    async def heartbeater() -> NoReturn:
        while True:
            await asleep(70)
            self.heartbeat()

    self.heartbeat_task = create_task(heartbeater())
    return self.heartbeat_task

heartbeat

heartbeat() -> None

Send heartbeat to clients.

Source code in src/pyfsd/factory/client.py
103
104
105
106
107
108
109
110
111
112
113
def heartbeat(self) -> None:
    """Send heartbeat to clients."""
    random_int: int = randint(-214743648, 2147483647)  # noqa: S311
    self.broadcast(
        make_packet(
            FSDClientCommand.WIND_DELTA + "SERVER",
            "*",
            f"{random_int % 11 - 5}",
            f"{random_int % 21 - 10}",
        ).encode("ascii"),
    )

remove_all_clients

remove_all_clients() -> None

Remove all clients.

Source code in src/pyfsd/factory/client.py
220
221
222
223
def remove_all_clients(self) -> None:
    """Remove all clients."""
    for client in self.clients.values():
        client.transport.close()

send_to

send_to(callsign: bytes, *lines: bytes, auto_newline: bool = True) -> bool

Send lines to a specified client.

Parameters:

  • callsign (bytes) –

    The client's callsign.

  • lines (bytes, default: () ) –

    Lines to be broadcasted.

  • auto_newline (bool, default: True ) –

    Auto put newline marker between lines or not.

Returns:

  • bool

    Is there a client called {callsign} (and is message sent or not).

Source code in src/pyfsd/factory/client.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def send_to(
    self, callsign: bytes, *lines: bytes, auto_newline: bool = True
) -> bool:
    """Send lines to a specified client.

    Args:
        callsign: The client's callsign.
        lines: Lines to be broadcasted.
        auto_newline: Auto put newline marker between lines or not.

    Returns:
        Is there a client called {callsign} (and is message sent or not).
    """
    data = join_lines(*lines, newline=auto_newline)
    try:
        self.clients[callsign].transport.write(data)
        return True
    except KeyError:
        return False