lightyear_netcode/auth.rs
1use bevy_ecs::resource::Resource;
2
3use crate::{ConnectToken, Error, Key, generate_key};
4use core::net::SocketAddr;
5use core::str::FromStr;
6
7#[derive(Resource, Default, Clone)]
8#[allow(clippy::large_enum_variant)]
9/// Struct used to authenticate with the server when using the Netcode connection.
10///
11/// Netcode is a standard to establish secure connections between clients and game servers on top of
12/// an unreliable unordered transport such as UDP.
13/// You can read more about it here: `<https://github.com/mas-bandwidth/netcode/blob/main/STANDARD.md>`
14///
15/// The client sends a [`ConnectToken`] to the game server to start the connection process.
16///
17/// There are several ways to obtain a [`ConnectToken`]:
18/// - the client can request a [`ConnectToken`] via a secure (e.g. HTTPS) connection from a backend server.
19///   The server must use the same `protocol_id` and `private_key` as the game servers.
20///   The backend server could be a dedicated webserver; or the game server itself, if it has a way to
21///   establish secure connection.
22/// - when testing, it can be convenient for the client to create its own [`ConnectToken`] manually.
23///   You can use `Authentication::Manual` for those cases.
24pub enum Authentication {
25    /// Use a [`ConnectToken`] to authenticate with the game server.
26    ///
27    /// The client must have already received the [`ConnectToken`] from the backend.
28    /// (The backend will generate a new `client_id` for the user, and use that to generate the
29    /// [`ConnectToken`])
30    Token(ConnectToken),
31    /// The client can build a [`ConnectToken`] manually.
32    ///
33    /// This is only useful for testing purposes. In production, the client should not have access
34    /// to the `private_key`.
35    Manual {
36        server_addr: SocketAddr,
37        client_id: u64,
38        private_key: Key,
39        protocol_id: u64,
40    },
41    #[default]
42    /// The client has no [`ConnectToken`], so it cannot connect to the game server yet.
43    ///
44    /// This is provided so that you can still build a Client while waiting
45    /// to receive a [`ConnectToken`] from the backend.
46    None,
47}
48
49impl Authentication {
50    /// Returns true if the Authentication contains a [`ConnectToken`] that can be used to
51    /// connect to the game server
52    pub fn has_token(&self) -> bool {
53        matches!(self, Authentication::Token(..))
54    }
55
56    pub fn get_token(
57        self,
58        client_timeout_secs: i32,
59        token_expire_secs: i32,
60    ) -> Result<ConnectToken, Error> {
61        Ok(match self {
62            Authentication::Token(token) => token,
63            Authentication::Manual {
64                server_addr,
65                client_id,
66                private_key,
67                protocol_id,
68            } => ConnectToken::build(server_addr, protocol_id, client_id, private_key)
69                .timeout_seconds(client_timeout_secs)
70                .expire_seconds(token_expire_secs)
71                .generate()?,
72            Authentication::None => {
73                // create a fake connect token so that we can build a NetcodeClient
74                ConnectToken::build(
75                    SocketAddr::from_str("0.0.0.0:0").unwrap(),
76                    0,
77                    0,
78                    generate_key(),
79                )
80                .timeout_seconds(client_timeout_secs)
81                .generate()?
82            }
83        })
84    }
85}
86
87impl core::fmt::Debug for Authentication {
88    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
89        match self {
90            Authentication::Token(_) => write!(f, "Token(<connect_token>)"),
91            Authentication::Manual {
92                server_addr,
93                client_id,
94                private_key,
95                protocol_id,
96            } => f
97                .debug_struct("Manual")
98                .field("server_addr", server_addr)
99                .field("client_id", client_id)
100                .field("private_key", private_key)
101                .field("protocol_id", protocol_id)
102                .finish(),
103            Authentication::None => write!(f, "None"),
104        }
105    }
106}