mirror of
				https://github.com/luanti-org/luanti.git
				synced 2025-10-31 15:35:21 +01:00 
			
		
		
		
	Move network protocol implementation behind an interface
This commit is contained in:
		
							
								
								
									
										1667
									
								
								src/network/mtp/impl.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1667
									
								
								src/network/mtp/impl.cpp
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										325
									
								
								src/network/mtp/impl.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								src/network/mtp/impl.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,325 @@ | ||||
| /*
 | ||||
| Minetest | ||||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||||
| 
 | ||||
| This program is free software; you can redistribute it and/or modify | ||||
| it under the terms of the GNU Lesser General Public License as published by | ||||
| the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Lesser General Public License along | ||||
| with this program; if not, write to the Free Software Foundation, Inc., | ||||
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||||
| */ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "network/connection.h" | ||||
| #include "network/socket.h" | ||||
| #include "constants.h" | ||||
| #include "util/pointer.h" | ||||
| #include "util/container.h" | ||||
| #include "util/numeric.h" | ||||
| #include "porting.h" | ||||
| #include "network/networkprotocol.h" | ||||
| #include <iostream> | ||||
| #include <vector> | ||||
| #include <map> | ||||
| 
 | ||||
| namespace con | ||||
| { | ||||
| 
 | ||||
| class ConnectionReceiveThread; | ||||
| class ConnectionSendThread; | ||||
| 
 | ||||
| class Peer; | ||||
| 
 | ||||
| // FIXME: Peer refcounting should generally be replaced by std::shared_ptr
 | ||||
| class PeerHelper | ||||
| { | ||||
| public: | ||||
| 	PeerHelper() = default; | ||||
| 	inline PeerHelper(Peer *peer) { *this = peer; } | ||||
| 	~PeerHelper(); | ||||
| 
 | ||||
| 	PeerHelper& operator=(Peer *peer); | ||||
| 	inline Peer* operator->() const { return m_peer; } | ||||
| 	inline Peer* operator&() const { return m_peer; } | ||||
| 
 | ||||
| 	inline bool operator!() { return !m_peer; } | ||||
| 	inline bool operator!=(std::nullptr_t) { return !!m_peer; } | ||||
| 
 | ||||
| private: | ||||
| 	Peer *m_peer = nullptr; | ||||
| }; | ||||
| 
 | ||||
| /*
 | ||||
| 	Connection | ||||
| */ | ||||
| 
 | ||||
| enum ConnectionEventType { | ||||
| 	CONNEVENT_NONE, | ||||
| 	CONNEVENT_DATA_RECEIVED, | ||||
| 	CONNEVENT_PEER_ADDED, | ||||
| 	CONNEVENT_PEER_REMOVED, | ||||
| 	CONNEVENT_BIND_FAILED, | ||||
| }; | ||||
| 
 | ||||
| struct ConnectionEvent; | ||||
| typedef std::shared_ptr<ConnectionEvent> ConnectionEventPtr; | ||||
| 
 | ||||
| // This is very similar to ConnectionCommand
 | ||||
| struct ConnectionEvent | ||||
| { | ||||
| 	const ConnectionEventType type; | ||||
| 	session_t peer_id = 0; | ||||
| 	Buffer<u8> data; | ||||
| 	bool timeout = false; | ||||
| 	Address address; | ||||
| 
 | ||||
| 	// We don't want to copy "data"
 | ||||
| 	DISABLE_CLASS_COPY(ConnectionEvent); | ||||
| 
 | ||||
| 	static ConnectionEventPtr create(ConnectionEventType type); | ||||
| 	static ConnectionEventPtr dataReceived(session_t peer_id, const Buffer<u8> &data); | ||||
| 	static ConnectionEventPtr peerAdded(session_t peer_id, Address address); | ||||
| 	static ConnectionEventPtr peerRemoved(session_t peer_id, bool is_timeout, Address address); | ||||
| 	static ConnectionEventPtr bindFailed(); | ||||
| 
 | ||||
| 	const char *describe() const; | ||||
| 
 | ||||
| private: | ||||
| 	ConnectionEvent(ConnectionEventType type_) : | ||||
| 		type(type_) {} | ||||
| }; | ||||
| 
 | ||||
| struct ConnectionCommand; | ||||
| typedef std::shared_ptr<ConnectionCommand> ConnectionCommandPtr; | ||||
| 
 | ||||
| struct BufferedPacket; | ||||
| typedef std::shared_ptr<BufferedPacket> BufferedPacketPtr; | ||||
| 
 | ||||
| class Connection; | ||||
| class PeerHandler; | ||||
| 
 | ||||
| class Peer : public IPeer { | ||||
| 	public: | ||||
| 		friend class PeerHelper; | ||||
| 
 | ||||
| 		virtual ~Peer() { | ||||
| 			MutexAutoLock usage_lock(m_exclusive_access_mutex); | ||||
| 			FATAL_ERROR_IF(m_usage != 0, "Reference counting failure"); | ||||
| 		} | ||||
| 
 | ||||
| 		void Drop(); | ||||
| 
 | ||||
| 		virtual void PutReliableSendCommand(ConnectionCommandPtr &c, | ||||
| 						unsigned int max_packet_size) {}; | ||||
| 
 | ||||
| 		bool isPendingDeletion() const { | ||||
| 			MutexAutoLock lock(m_exclusive_access_mutex); | ||||
| 			return m_pending_deletion; | ||||
| 		} | ||||
| 		void ResetTimeout() { | ||||
| 			MutexAutoLock lock(m_exclusive_access_mutex); | ||||
| 			m_timeout_counter = 0; | ||||
| 		} | ||||
| 
 | ||||
| 		bool isHalfOpen() const { | ||||
| 			MutexAutoLock lock(m_exclusive_access_mutex); | ||||
| 			return m_half_open; | ||||
| 		} | ||||
| 		void SetFullyOpen() { | ||||
| 			MutexAutoLock lock(m_exclusive_access_mutex); | ||||
| 			m_half_open = false; | ||||
| 		} | ||||
| 
 | ||||
| 		virtual bool isTimedOut(float timeout, std::string &reason); | ||||
| 
 | ||||
| 		unsigned int m_increment_packets_remaining = 0; | ||||
| 
 | ||||
| 		virtual u16 getNextSplitSequenceNumber(u8 channel) { return 0; }; | ||||
| 		virtual void setNextSplitSequenceNumber(u8 channel, u16 seqnum) {}; | ||||
| 		virtual SharedBuffer<u8> addSplitPacket(u8 channel, BufferedPacketPtr &toadd, | ||||
| 				bool reliable) | ||||
| 		{ | ||||
| 			FATAL_ERROR("unimplemented in abstract class"); | ||||
| 		} | ||||
| 
 | ||||
| 		virtual bool Ping(float dtime, SharedBuffer<u8>& data) { return false; }; | ||||
| 
 | ||||
| 		virtual float getStat(rtt_stat_type type) const { | ||||
| 			switch (type) { | ||||
| 				case MIN_RTT: | ||||
| 					return m_rtt.min_rtt; | ||||
| 				case MAX_RTT: | ||||
| 					return m_rtt.max_rtt; | ||||
| 				case AVG_RTT: | ||||
| 					return m_rtt.avg_rtt; | ||||
| 				case MIN_JITTER: | ||||
| 					return m_rtt.jitter_min; | ||||
| 				case MAX_JITTER: | ||||
| 					return m_rtt.jitter_max; | ||||
| 				case AVG_JITTER: | ||||
| 					return m_rtt.jitter_avg; | ||||
| 			} | ||||
| 			return -1; | ||||
| 		} | ||||
| 
 | ||||
| 	protected: | ||||
| 		Peer(session_t id, const Address &address, Connection *connection) : | ||||
| 			IPeer(id), | ||||
| 			m_connection(connection), | ||||
| 			address(address), | ||||
| 			m_last_timeout_check(porting::getTimeMs()) | ||||
| 		{ | ||||
| 		} | ||||
| 
 | ||||
| 		virtual void reportRTT(float rtt) {}; | ||||
| 
 | ||||
| 		void RTTStatistics(float rtt, | ||||
| 							const std::string &profiler_id = "", | ||||
| 							unsigned int num_samples = 1000); | ||||
| 
 | ||||
| 		bool IncUseCount(); | ||||
| 		void DecUseCount(); | ||||
| 
 | ||||
| 		mutable std::mutex m_exclusive_access_mutex; | ||||
| 
 | ||||
| 		bool m_pending_deletion = false; | ||||
| 
 | ||||
| 		Connection *m_connection; | ||||
| 
 | ||||
| 		// Address of the peer
 | ||||
| 		Address address; | ||||
| 
 | ||||
| 		// Ping timer
 | ||||
| 		float m_ping_timer = 0.0f; | ||||
| 
 | ||||
| 	private: | ||||
| 		struct rttstats { | ||||
| 			float jitter_min = FLT_MAX; | ||||
| 			float jitter_max = 0.0f; | ||||
| 			float jitter_avg = -1.0f; | ||||
| 			float min_rtt = FLT_MAX; | ||||
| 			float max_rtt = 0.0f; | ||||
| 			float avg_rtt = -1.0f; | ||||
| 		}; | ||||
| 
 | ||||
| 		rttstats m_rtt; | ||||
| 		float m_last_rtt = -1.0f; | ||||
| 
 | ||||
| 		/*
 | ||||
| 			Until the peer has communicated with us using their assigned peer id | ||||
| 			the connection is considered half-open. | ||||
| 			During this time we inhibit re-sending any reliables or pings. This | ||||
| 			is to avoid spending too many resources on a potential DoS attack | ||||
| 			and to make sure Minetest servers are not useful for UDP amplificiation. | ||||
| 		*/ | ||||
| 		bool m_half_open = true; | ||||
| 
 | ||||
| 		// current usage count
 | ||||
| 		unsigned int m_usage = 0; | ||||
| 
 | ||||
| 		// Seconds from last receive
 | ||||
| 		float m_timeout_counter = 0.0f; | ||||
| 
 | ||||
| 		u64 m_last_timeout_check; | ||||
| }; | ||||
| 
 | ||||
| class UDPPeer; | ||||
| 
 | ||||
| class Connection : public IConnection | ||||
| { | ||||
| public: | ||||
| 	friend class ConnectionSendThread; | ||||
| 	friend class ConnectionReceiveThread; | ||||
| 
 | ||||
| 	Connection(u32 max_packet_size, float timeout, bool ipv6, | ||||
| 			PeerHandler *peerhandler); | ||||
| 	~Connection(); | ||||
| 
 | ||||
| 	/* Interface */ | ||||
| 	ConnectionEventPtr waitEvent(u32 timeout_ms); | ||||
| 
 | ||||
| 	void putCommand(ConnectionCommandPtr c); | ||||
| 
 | ||||
| 	void SetTimeoutMs(u32 timeout) { m_bc_receive_timeout = timeout; } | ||||
| 	void Serve(Address bind_addr); | ||||
| 	void Connect(Address address); | ||||
| 	bool Connected(); | ||||
| 	void Disconnect(); | ||||
| 	bool ReceiveTimeoutMs(NetworkPacket *pkt, u32 timeout_ms); | ||||
| 	void Receive(NetworkPacket *pkt); | ||||
| 	void Send(session_t peer_id, u8 channelnum, NetworkPacket *pkt, bool reliable); | ||||
| 	session_t GetPeerID() const { return m_peer_id; } | ||||
| 	Address GetPeerAddress(session_t peer_id); | ||||
| 	float getPeerStat(session_t peer_id, rtt_stat_type type); | ||||
| 	float getLocalStat(rate_stat_type type); | ||||
| 	u32 GetProtocolID() const { return m_protocol_id; }; | ||||
| 	const std::string getDesc(); | ||||
| 	void DisconnectPeer(session_t peer_id); | ||||
| 
 | ||||
| protected: | ||||
| 	PeerHelper getPeerNoEx(session_t peer_id); | ||||
| 	session_t   lookupPeer(const Address& sender); | ||||
| 
 | ||||
| 	session_t createPeer(const Address& sender, int fd); | ||||
| 	UDPPeer*  createServerPeer(const Address& sender); | ||||
| 	bool deletePeer(session_t peer_id, bool timeout); | ||||
| 
 | ||||
| 	void SetPeerID(session_t id) { m_peer_id = id; } | ||||
| 
 | ||||
| 	void doResendOne(session_t peer_id); | ||||
| 
 | ||||
| 	void sendAck(session_t peer_id, u8 channelnum, u16 seqnum); | ||||
| 
 | ||||
| 	std::vector<session_t> getPeerIDs() | ||||
| 	{ | ||||
| 		MutexAutoLock peerlock(m_peers_mutex); | ||||
| 		return m_peer_ids; | ||||
| 	} | ||||
| 
 | ||||
| 	u32 getActiveCount(); | ||||
| 
 | ||||
| 	UDPSocket m_udpSocket; | ||||
| 	// Command queue: user -> SendThread
 | ||||
| 	MutexedQueue<ConnectionCommandPtr> m_command_queue; | ||||
| 
 | ||||
| 	void putEvent(ConnectionEventPtr e); | ||||
| 
 | ||||
| 	void TriggerSend(); | ||||
| 
 | ||||
| 	bool ConnectedToServer() | ||||
| 	{ | ||||
| 		return getPeerNoEx(PEER_ID_SERVER) != nullptr; | ||||
| 	} | ||||
| private: | ||||
| 	// Event queue: ReceiveThread -> user
 | ||||
| 	MutexedQueue<ConnectionEventPtr> m_event_queue; | ||||
| 
 | ||||
| 	session_t m_peer_id = 0; | ||||
| 	u32 m_protocol_id; | ||||
| 
 | ||||
| 	std::map<session_t, Peer *> m_peers; | ||||
| 	std::vector<session_t> m_peer_ids; | ||||
| 	std::mutex m_peers_mutex; | ||||
| 
 | ||||
| 	std::unique_ptr<ConnectionSendThread> m_sendThread; | ||||
| 	std::unique_ptr<ConnectionReceiveThread> m_receiveThread; | ||||
| 
 | ||||
| 	mutable std::mutex m_info_mutex; | ||||
| 
 | ||||
| 	// Backwards compatibility
 | ||||
| 	PeerHandler *m_bc_peerhandler; | ||||
| 	u32 m_bc_receive_timeout = 0; | ||||
| 
 | ||||
| 	bool m_shutting_down = false; | ||||
| }; | ||||
| 
 | ||||
| } // namespace
 | ||||
							
								
								
									
										525
									
								
								src/network/mtp/internal.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										525
									
								
								src/network/mtp/internal.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,525 @@ | ||||
| /*
 | ||||
| Minetest | ||||
| Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> | ||||
| 
 | ||||
| This program is free software; you can redistribute it and/or modify | ||||
| it under the terms of the GNU Lesser General Public License as published by | ||||
| the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Lesser General Public License along | ||||
| with this program; if not, write to the Free Software Foundation, Inc., | ||||
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||||
| */ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "network/mtp/impl.h" | ||||
| 
 | ||||
| // Constant that differentiates the protocol from random data and other protocols
 | ||||
| #define PROTOCOL_ID 0x4f457403 | ||||
| 
 | ||||
| #define MAX_UDP_PEERS 65535 | ||||
| 
 | ||||
| /*
 | ||||
| === NOTES === | ||||
| 
 | ||||
| A packet is sent through a channel to a peer with a basic header: | ||||
| 	Header (7 bytes): | ||||
| 	[0] u32 protocol_id | ||||
| 	[4] session_t sender_peer_id | ||||
| 	[6] u8 channel | ||||
| sender_peer_id: | ||||
| 	Unique to each peer. | ||||
| 	value 0 (PEER_ID_INEXISTENT) is reserved for making new connections | ||||
| 	value 1 (PEER_ID_SERVER) is reserved for server | ||||
| 	these constants are defined in constants.h | ||||
| channel: | ||||
| 	Channel numbers have no intrinsic meaning. Currently only 0, 1, 2 exist. | ||||
| */ | ||||
| #define BASE_HEADER_SIZE 7 | ||||
| #define CHANNEL_COUNT 3 | ||||
| 
 | ||||
| /*
 | ||||
| Packet types: | ||||
| 
 | ||||
| CONTROL: This is a packet used by the protocol. | ||||
| - When this is processed, nothing is handed to the user. | ||||
| 	Header (2 byte): | ||||
| 	[0] u8 type | ||||
| 	[1] u8 controltype | ||||
| controltype and data description: | ||||
| 	CONTROLTYPE_ACK | ||||
| 		[2] u16 seqnum | ||||
| 	CONTROLTYPE_SET_PEER_ID | ||||
| 		[2] session_t peer_id_new | ||||
| 	CONTROLTYPE_PING | ||||
| 	- There is no actual reply, but this can be sent in a reliable | ||||
| 	  packet to get a reply | ||||
| 	CONTROLTYPE_DISCO | ||||
| */ | ||||
| enum ControlType : u8 { | ||||
| 	CONTROLTYPE_ACK = 0, | ||||
| 	CONTROLTYPE_SET_PEER_ID = 1, | ||||
| 	CONTROLTYPE_PING = 2, | ||||
| 	CONTROLTYPE_DISCO = 3, | ||||
| }; | ||||
| 
 | ||||
| /*
 | ||||
| ORIGINAL: This is a plain packet with no control and no error | ||||
| checking at all. | ||||
| - When this is processed, it is directly handed to the user. | ||||
| 	Header (1 byte): | ||||
| 	[0] u8 type | ||||
| */ | ||||
| //#define TYPE_ORIGINAL 1
 | ||||
| #define ORIGINAL_HEADER_SIZE 1 | ||||
| 
 | ||||
| /*
 | ||||
| SPLIT: These are sequences of packets forming one bigger piece of | ||||
| data. | ||||
| - When processed and all the packet_nums 0...packet_count-1 are | ||||
|   present (this should be buffered), the resulting data shall be | ||||
|   directly handed to the user. | ||||
| - If the data fails to come up in a reasonable time, the buffer shall | ||||
|   be silently discarded. | ||||
| - These can be sent as-is or atop of a RELIABLE packet stream. | ||||
| 	Header (7 bytes): | ||||
| 	[0] u8 type | ||||
| 	[1] u16 seqnum | ||||
| 	[3] u16 chunk_count | ||||
| 	[5] u16 chunk_num | ||||
| */ | ||||
| //#define TYPE_SPLIT 2
 | ||||
| 
 | ||||
| /*
 | ||||
| RELIABLE: Delivery of all RELIABLE packets shall be forced by ACKs, | ||||
| and they shall be delivered in the same order as sent. This is done | ||||
| with a buffer in the receiving and transmitting end. | ||||
| - When this is processed, the contents of each packet is recursively | ||||
|   processed as packets. | ||||
| 	Header (3 bytes): | ||||
| 	[0] u8 type | ||||
| 	[1] u16 seqnum | ||||
| 
 | ||||
| */ | ||||
| //#define TYPE_RELIABLE 3
 | ||||
| #define RELIABLE_HEADER_SIZE 3 | ||||
| #define SEQNUM_INITIAL 65500 | ||||
| #define SEQNUM_MAX 65535 | ||||
| 
 | ||||
| namespace con | ||||
| { | ||||
| 
 | ||||
| 
 | ||||
| enum PacketType : u8 { | ||||
| 	PACKET_TYPE_CONTROL = 0, | ||||
| 	PACKET_TYPE_ORIGINAL = 1, | ||||
| 	PACKET_TYPE_SPLIT = 2, | ||||
| 	PACKET_TYPE_RELIABLE = 3, | ||||
| 	PACKET_TYPE_MAX | ||||
| }; | ||||
| 
 | ||||
| inline bool seqnum_higher(u16 totest, u16 base) | ||||
| { | ||||
| 	if (totest > base) | ||||
| 	{ | ||||
| 		if ((totest - base) > (SEQNUM_MAX/2)) | ||||
| 			return false; | ||||
| 
 | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	if ((base - totest) > (SEQNUM_MAX/2)) | ||||
| 		return true; | ||||
| 
 | ||||
| 	return false; | ||||
| } | ||||
| 
 | ||||
| inline bool seqnum_in_window(u16 seqnum, u16 next,u16 window_size) | ||||
| { | ||||
| 	u16 window_start = next; | ||||
| 	u16 window_end   = ( next + window_size ) % (SEQNUM_MAX+1); | ||||
| 
 | ||||
| 	if (window_start < window_end) { | ||||
| 		return ((seqnum >= window_start) && (seqnum < window_end)); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	return ((seqnum < window_end) || (seqnum >= window_start)); | ||||
| } | ||||
| 
 | ||||
| inline float CALC_DTIME(u64 lasttime, u64 curtime) | ||||
| { | ||||
| 	float value = (curtime - lasttime) / 1000.0f; | ||||
| 	return MYMAX(MYMIN(value, 0.1f), 0.0f); | ||||
| } | ||||
| 
 | ||||
| /* Exceptions */ | ||||
| 
 | ||||
| class NotFoundException : public BaseException | ||||
| { | ||||
| public: | ||||
| 	NotFoundException(const char *s) : BaseException(s) {} | ||||
| }; | ||||
| 
 | ||||
| class ProcessedSilentlyException : public BaseException | ||||
| { | ||||
| public: | ||||
| 	ProcessedSilentlyException(const char *s) : BaseException(s) {} | ||||
| }; | ||||
| 
 | ||||
| class ProcessedQueued : public BaseException | ||||
| { | ||||
| public: | ||||
| 	ProcessedQueued(const char *s) : BaseException(s) {} | ||||
| }; | ||||
| 
 | ||||
| class IncomingDataCorruption : public BaseException | ||||
| { | ||||
| public: | ||||
| 	IncomingDataCorruption(const char *s) : BaseException(s) {} | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| /*
 | ||||
| 	Struct for all kinds of packets. Includes following data: | ||||
| 		BASE_HEADER | ||||
| 		u8[] packet data (usually copied from SharedBuffer<u8>) | ||||
| */ | ||||
| struct BufferedPacket { | ||||
| 	BufferedPacket(u32 a_size) | ||||
| 	{ | ||||
| 		m_data.resize(a_size); | ||||
| 		data = &m_data[0]; | ||||
| 	} | ||||
| 
 | ||||
| 	DISABLE_CLASS_COPY(BufferedPacket) | ||||
| 
 | ||||
| 	u16 getSeqnum() const; | ||||
| 
 | ||||
| 	inline size_t size() const { return m_data.size(); } | ||||
| 
 | ||||
| 	u8 *data; // Direct memory access
 | ||||
| 	float time = 0.0f; // Seconds from buffering the packet or re-sending
 | ||||
| 	float totaltime = 0.0f; // Seconds from buffering the packet
 | ||||
| 	u64 absolute_send_time = -1; | ||||
| 	Address address; // Sender or destination
 | ||||
| 	unsigned int resend_count = 0; | ||||
| 
 | ||||
| private: | ||||
| 	std::vector<u8> m_data; // Data of the packet, including headers
 | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| // This adds the base headers to the data and makes a packet out of it
 | ||||
| BufferedPacketPtr makePacket(const Address &address, const SharedBuffer<u8> &data, | ||||
| 		u32 protocol_id, session_t sender_peer_id, u8 channel); | ||||
| 
 | ||||
| // Depending on size, make a TYPE_ORIGINAL or TYPE_SPLIT packet
 | ||||
| // Increments split_seqnum if a split packet is made
 | ||||
| void makeAutoSplitPacket(const SharedBuffer<u8> &data, u32 chunksize_max, | ||||
| 		u16 &split_seqnum, std::list<SharedBuffer<u8>> *list); | ||||
| 
 | ||||
| // Add the TYPE_RELIABLE header to the data
 | ||||
| SharedBuffer<u8> makeReliablePacket(const SharedBuffer<u8> &data, u16 seqnum); | ||||
| 
 | ||||
| struct IncomingSplitPacket | ||||
| { | ||||
| 	IncomingSplitPacket(u32 cc, bool r): | ||||
| 		chunk_count(cc), reliable(r) {} | ||||
| 
 | ||||
| 	IncomingSplitPacket() = delete; | ||||
| 
 | ||||
| 	float time = 0.0f; // Seconds from adding
 | ||||
| 	u32 chunk_count; | ||||
| 	bool reliable; // If true, isn't deleted on timeout
 | ||||
| 
 | ||||
| 	bool allReceived() const | ||||
| 	{ | ||||
| 		return (chunks.size() == chunk_count); | ||||
| 	} | ||||
| 	bool insert(u32 chunk_num, SharedBuffer<u8> &chunkdata); | ||||
| 	SharedBuffer<u8> reassemble(); | ||||
| 
 | ||||
| private: | ||||
| 	// Key is chunk number, value is data without headers
 | ||||
| 	std::map<u16, SharedBuffer<u8>> chunks; | ||||
| }; | ||||
| 
 | ||||
| /*
 | ||||
| 	A buffer which stores reliable packets and sorts them internally | ||||
| 	for fast access to the smallest one. | ||||
| */ | ||||
| 
 | ||||
| class ReliablePacketBuffer | ||||
| { | ||||
| public: | ||||
| 	bool getFirstSeqnum(u16 &result); | ||||
| 
 | ||||
| 	BufferedPacketPtr popFirst(); | ||||
| 	BufferedPacketPtr popSeqnum(u16 seqnum); | ||||
| 	void insert(BufferedPacketPtr &p_ptr, u16 next_expected); | ||||
| 
 | ||||
| 	void incrementTimeouts(float dtime); | ||||
| 	u32 getTimedOuts(float timeout); | ||||
| 	// timeout relative to last resend
 | ||||
| 	std::vector<ConstSharedPtr<BufferedPacket>> getResend(float timeout, u32 max_packets); | ||||
| 
 | ||||
| 	void print(); | ||||
| 	bool empty(); | ||||
| 	u32 size(); | ||||
| 
 | ||||
| 
 | ||||
| private: | ||||
| 	typedef std::list<BufferedPacketPtr>::iterator FindResult; | ||||
| 
 | ||||
| 	FindResult findPacketNoLock(u16 seqnum); | ||||
| 
 | ||||
| 	std::list<BufferedPacketPtr> m_list; | ||||
| 
 | ||||
| 	u16 m_oldest_non_answered_ack; | ||||
| 
 | ||||
| 	std::mutex m_list_mutex; | ||||
| }; | ||||
| 
 | ||||
| /*
 | ||||
| 	A buffer for reconstructing split packets | ||||
| */ | ||||
| 
 | ||||
| class IncomingSplitBuffer | ||||
| { | ||||
| public: | ||||
| 	~IncomingSplitBuffer(); | ||||
| 
 | ||||
| 	/*
 | ||||
| 		Returns a reference counted buffer of length != 0 when a full split | ||||
| 		packet is constructed. If not, returns one of length 0. | ||||
| 	*/ | ||||
| 	SharedBuffer<u8> insert(BufferedPacketPtr &p_ptr, bool reliable); | ||||
| 
 | ||||
| 	void removeUnreliableTimedOuts(float dtime, float timeout); | ||||
| 
 | ||||
| private: | ||||
| 	// Key is seqnum
 | ||||
| 	std::map<u16, IncomingSplitPacket*> m_buf; | ||||
| 
 | ||||
| 	std::mutex m_map_mutex; | ||||
| }; | ||||
| 
 | ||||
| enum ConnectionCommandType{ | ||||
| 	CONNCMD_NONE, | ||||
| 	CONNCMD_SERVE, | ||||
| 	CONNCMD_CONNECT, | ||||
| 	CONNCMD_DISCONNECT, | ||||
| 	CONNCMD_DISCONNECT_PEER, | ||||
| 	CONNCMD_SEND, | ||||
| 	CONNCMD_SEND_TO_ALL, | ||||
| 	CONCMD_ACK, | ||||
| 	CONCMD_CREATE_PEER, | ||||
| 	CONNCMD_RESEND_ONE | ||||
| }; | ||||
| 
 | ||||
| // This is very similar to ConnectionEvent
 | ||||
| struct ConnectionCommand | ||||
| { | ||||
| 	const ConnectionCommandType type; | ||||
| 	Address address; | ||||
| 	session_t peer_id = PEER_ID_INEXISTENT; | ||||
| 	u8 channelnum = 0; | ||||
| 	Buffer<u8> data; | ||||
| 	bool reliable = false; | ||||
| 	bool raw = false; | ||||
| 
 | ||||
| 	DISABLE_CLASS_COPY(ConnectionCommand); | ||||
| 
 | ||||
| 	static ConnectionCommandPtr serve(Address address); | ||||
| 	static ConnectionCommandPtr connect(Address address); | ||||
| 	static ConnectionCommandPtr disconnect(); | ||||
| 	static ConnectionCommandPtr disconnect_peer(session_t peer_id); | ||||
| 	static ConnectionCommandPtr resend_one(session_t peer_id); | ||||
| 	static ConnectionCommandPtr send(session_t peer_id, u8 channelnum, NetworkPacket *pkt, bool reliable); | ||||
| 	static ConnectionCommandPtr ack(session_t peer_id, u8 channelnum, const Buffer<u8> &data); | ||||
| 	static ConnectionCommandPtr createPeer(session_t peer_id, const Buffer<u8> &data); | ||||
| 
 | ||||
| private: | ||||
| 	ConnectionCommand(ConnectionCommandType type_) : | ||||
| 		type(type_) {} | ||||
| 
 | ||||
| 	static ConnectionCommandPtr create(ConnectionCommandType type); | ||||
| }; | ||||
| 
 | ||||
| /* maximum window size to use, 0xFFFF is theoretical maximum. don't think about
 | ||||
|  * touching it, the less you're away from it the more likely data corruption | ||||
|  * will occur | ||||
|  */ | ||||
| #define MAX_RELIABLE_WINDOW_SIZE 0x8000 | ||||
| /* starting value for window size */ | ||||
| #define START_RELIABLE_WINDOW_SIZE 0x400 | ||||
| /* minimum value for window size */ | ||||
| #define MIN_RELIABLE_WINDOW_SIZE 0x40 | ||||
| 
 | ||||
| class Channel | ||||
| { | ||||
| 
 | ||||
| public: | ||||
| 	u16 readNextIncomingSeqNum(); | ||||
| 	u16 incNextIncomingSeqNum(); | ||||
| 
 | ||||
| 	u16 getOutgoingSequenceNumber(bool& successful); | ||||
| 	u16 readOutgoingSequenceNumber(); | ||||
| 	bool putBackSequenceNumber(u16); | ||||
| 
 | ||||
| 	u16 readNextSplitSeqNum(); | ||||
| 	void setNextSplitSeqNum(u16 seqnum); | ||||
| 
 | ||||
| 	// This is for buffering the incoming packets that are coming in
 | ||||
| 	// the wrong order
 | ||||
| 	ReliablePacketBuffer incoming_reliables; | ||||
| 	// This is for buffering the sent packets so that the sender can
 | ||||
| 	// re-send them if no ACK is received
 | ||||
| 	ReliablePacketBuffer outgoing_reliables_sent; | ||||
| 
 | ||||
| 	//queued reliable packets
 | ||||
| 	std::queue<BufferedPacketPtr> queued_reliables; | ||||
| 
 | ||||
| 	//queue commands prior splitting to packets
 | ||||
| 	std::deque<ConnectionCommandPtr> queued_commands; | ||||
| 
 | ||||
| 	IncomingSplitBuffer incoming_splits; | ||||
| 
 | ||||
| 	Channel() = default; | ||||
| 	~Channel() = default; | ||||
| 
 | ||||
| 	void UpdatePacketLossCounter(unsigned int count); | ||||
| 	void UpdatePacketTooLateCounter(); | ||||
| 	void UpdateBytesSent(unsigned int bytes,unsigned int packages=1); | ||||
| 	void UpdateBytesLost(unsigned int bytes); | ||||
| 	void UpdateBytesReceived(unsigned int bytes); | ||||
| 
 | ||||
| 	void UpdateTimers(float dtime); | ||||
| 
 | ||||
| 	float getCurrentDownloadRateKB() | ||||
| 		{ MutexAutoLock lock(m_internal_mutex); return cur_kbps; }; | ||||
| 	float getMaxDownloadRateKB() | ||||
| 		{ MutexAutoLock lock(m_internal_mutex); return max_kbps; }; | ||||
| 
 | ||||
| 	float getCurrentLossRateKB() | ||||
| 		{ MutexAutoLock lock(m_internal_mutex); return cur_kbps_lost; }; | ||||
| 	float getMaxLossRateKB() | ||||
| 		{ MutexAutoLock lock(m_internal_mutex); return max_kbps_lost; }; | ||||
| 
 | ||||
| 	float getCurrentIncomingRateKB() | ||||
| 		{ MutexAutoLock lock(m_internal_mutex); return cur_incoming_kbps; }; | ||||
| 	float getMaxIncomingRateKB() | ||||
| 		{ MutexAutoLock lock(m_internal_mutex); return max_incoming_kbps; }; | ||||
| 
 | ||||
| 	float getAvgDownloadRateKB() | ||||
| 		{ MutexAutoLock lock(m_internal_mutex); return avg_kbps; }; | ||||
| 	float getAvgLossRateKB() | ||||
| 		{ MutexAutoLock lock(m_internal_mutex); return avg_kbps_lost; }; | ||||
| 	float getAvgIncomingRateKB() | ||||
| 		{ MutexAutoLock lock(m_internal_mutex); return avg_incoming_kbps; }; | ||||
| 
 | ||||
| 	u16 getWindowSize() const { return m_window_size; }; | ||||
| 
 | ||||
| 	void setWindowSize(long size) | ||||
| 	{ | ||||
| 		m_window_size = (u16)rangelim(size, MIN_RELIABLE_WINDOW_SIZE, MAX_RELIABLE_WINDOW_SIZE); | ||||
| 	} | ||||
| 
 | ||||
| private: | ||||
| 	std::mutex m_internal_mutex; | ||||
| 	u16 m_window_size = MIN_RELIABLE_WINDOW_SIZE; | ||||
| 
 | ||||
| 	u16 next_incoming_seqnum = SEQNUM_INITIAL; | ||||
| 
 | ||||
| 	u16 next_outgoing_seqnum = SEQNUM_INITIAL; | ||||
| 	u16 next_outgoing_split_seqnum = SEQNUM_INITIAL; | ||||
| 
 | ||||
| 	unsigned int current_packet_loss = 0; | ||||
| 	unsigned int current_packet_too_late = 0; | ||||
| 	unsigned int current_packet_successful = 0; | ||||
| 	float packet_loss_counter = 0.0f; | ||||
| 
 | ||||
| 	unsigned int current_bytes_transfered = 0; | ||||
| 	unsigned int current_bytes_received = 0; | ||||
| 	unsigned int current_bytes_lost = 0; | ||||
| 	float max_kbps = 0.0f; | ||||
| 	float cur_kbps = 0.0f; | ||||
| 	float avg_kbps = 0.0f; | ||||
| 	float max_incoming_kbps = 0.0f; | ||||
| 	float cur_incoming_kbps = 0.0f; | ||||
| 	float avg_incoming_kbps = 0.0f; | ||||
| 	float max_kbps_lost = 0.0f; | ||||
| 	float cur_kbps_lost = 0.0f; | ||||
| 	float avg_kbps_lost = 0.0f; | ||||
| 	float bpm_counter = 0.0f; | ||||
| 
 | ||||
| 	unsigned int rate_samples = 0; | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| class UDPPeer final : public Peer | ||||
| { | ||||
| public: | ||||
| 
 | ||||
| 	friend class PeerHelper; | ||||
| 	friend class ConnectionReceiveThread; | ||||
| 	friend class ConnectionSendThread; | ||||
| 	friend class Connection; | ||||
| 
 | ||||
| 	UDPPeer(session_t id, const Address &address, Connection *connection); | ||||
| 	virtual ~UDPPeer() = default; | ||||
| 
 | ||||
| 	void PutReliableSendCommand(ConnectionCommandPtr &c, | ||||
| 							unsigned int max_packet_size) override; | ||||
| 
 | ||||
| 	virtual const Address &getAddress() const override { | ||||
| 		return address; | ||||
| 	} | ||||
| 
 | ||||
| 	u16 getNextSplitSequenceNumber(u8 channel) override; | ||||
| 	void setNextSplitSequenceNumber(u8 channel, u16 seqnum) override; | ||||
| 
 | ||||
| 	SharedBuffer<u8> addSplitPacket(u8 channel, BufferedPacketPtr &toadd, | ||||
| 		bool reliable) override; | ||||
| 
 | ||||
| 	bool isTimedOut(float timeout, std::string &reason) override; | ||||
| 
 | ||||
| protected: | ||||
| 	/*
 | ||||
| 		Calculates avg_rtt and resend_timeout. | ||||
| 		rtt=-1 only recalculates resend_timeout | ||||
| 	*/ | ||||
| 	void reportRTT(float rtt) override; | ||||
| 
 | ||||
| 	void RunCommandQueues( | ||||
| 					unsigned int max_packet_size, | ||||
| 					unsigned int maxtransfer); | ||||
| 
 | ||||
| 	float getResendTimeout() | ||||
| 		{ MutexAutoLock lock(m_exclusive_access_mutex); return resend_timeout; } | ||||
| 
 | ||||
| 	void setResendTimeout(float timeout) | ||||
| 		{ MutexAutoLock lock(m_exclusive_access_mutex); resend_timeout = timeout; } | ||||
| 
 | ||||
| 	bool Ping(float dtime, SharedBuffer<u8>& data) override; | ||||
| 
 | ||||
| 	Channel channels[CHANNEL_COUNT]; | ||||
| 	bool m_pending_disconnect = false; | ||||
| private: | ||||
| 	// This is changed dynamically
 | ||||
| 	float resend_timeout = 0.5; | ||||
| 
 | ||||
| 	bool processReliableSendCommand( | ||||
| 					ConnectionCommandPtr &c_ptr, | ||||
| 					unsigned int max_packet_size); | ||||
| }; | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										1394
									
								
								src/network/mtp/threads.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1394
									
								
								src/network/mtp/threads.cpp
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										190
									
								
								src/network/mtp/threads.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/network/mtp/threads.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| /*
 | ||||
| Minetest | ||||
| Copyright (C) 2013-2017 celeron55, Perttu Ahola <celeron55@gmail.com> | ||||
| Copyright (C) 2017 celeron55, Loic Blot <loic.blot@unix-experience.fr> | ||||
| 
 | ||||
| This program is free software; you can redistribute it and/or modify | ||||
| it under the terms of the GNU Lesser General Public License as published by | ||||
| the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Lesser General Public License along | ||||
| with this program; if not, write to the Free Software Foundation, Inc., | ||||
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||||
| */ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| /********************************************/ | ||||
| /* may only be included from in src/network */ | ||||
| /********************************************/ | ||||
| 
 | ||||
| #include <cassert> | ||||
| #include "threading/thread.h" | ||||
| #include "network/mtp/internal.h" | ||||
| 
 | ||||
| namespace con | ||||
| { | ||||
| 
 | ||||
| class Connection; | ||||
| 
 | ||||
| struct OutgoingPacket | ||||
| { | ||||
| 	session_t peer_id; | ||||
| 	u8 channelnum; | ||||
| 	SharedBuffer<u8> data; | ||||
| 	bool reliable; | ||||
| 	bool ack; | ||||
| 
 | ||||
| 	OutgoingPacket(session_t peer_id_, u8 channelnum_, const SharedBuffer<u8> &data_, | ||||
| 			bool reliable_,bool ack_=false): | ||||
| 		peer_id(peer_id_), | ||||
| 		channelnum(channelnum_), | ||||
| 		data(data_), | ||||
| 		reliable(reliable_), | ||||
| 		ack(ack_) | ||||
| 	{ | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| class ConnectionSendThread : public Thread | ||||
| { | ||||
| 
 | ||||
| public: | ||||
| 	friend class UDPPeer; | ||||
| 
 | ||||
| 	ConnectionSendThread(unsigned int max_packet_size, float timeout); | ||||
| 
 | ||||
| 	void *run(); | ||||
| 
 | ||||
| 	void Trigger(); | ||||
| 
 | ||||
| 	void setParent(Connection *parent) | ||||
| 	{ | ||||
| 		assert(parent != NULL); // Pre-condition
 | ||||
| 		m_connection = parent; | ||||
| 	} | ||||
| 
 | ||||
| 	void setPeerTimeout(float peer_timeout) { m_timeout = peer_timeout; } | ||||
| 
 | ||||
| private: | ||||
| 	void runTimeouts(float dtime, u32 peer_packet_quota); | ||||
| 	void resendReliable(Channel &channel, const BufferedPacket *k, float resend_timeout); | ||||
| 	void rawSend(const BufferedPacket *p); | ||||
| 	bool rawSendAsPacket(session_t peer_id, u8 channelnum, | ||||
| 			const SharedBuffer<u8> &data, bool reliable); | ||||
| 
 | ||||
| 	void processReliableCommand(ConnectionCommandPtr &c); | ||||
| 	void processNonReliableCommand(ConnectionCommandPtr &c); | ||||
| 	void serve(Address bind_address); | ||||
| 	void connect(Address address); | ||||
| 	void disconnect(); | ||||
| 	void disconnect_peer(session_t peer_id); | ||||
| 	void send(session_t peer_id, u8 channelnum, const SharedBuffer<u8> &data); | ||||
| 	void sendReliable(ConnectionCommandPtr &c); | ||||
| 	void sendToAll(u8 channelnum, const SharedBuffer<u8> &data); | ||||
| 	void sendToAllReliable(ConnectionCommandPtr &c); | ||||
| 
 | ||||
| 	void sendPackets(float dtime, u32 peer_packet_quota); | ||||
| 
 | ||||
| 	void sendAsPacket(session_t peer_id, u8 channelnum, const SharedBuffer<u8> &data, | ||||
| 			bool ack = false); | ||||
| 
 | ||||
| 	void sendAsPacketReliable(BufferedPacketPtr &p, Channel *channel); | ||||
| 
 | ||||
| 	bool packetsQueued(); | ||||
| 
 | ||||
| 	Connection *m_connection = nullptr; | ||||
| 	unsigned int m_max_packet_size; | ||||
| 	float m_timeout; | ||||
| 	std::queue<OutgoingPacket> m_outgoing_queue; | ||||
| 	Semaphore m_send_sleep_semaphore; | ||||
| 
 | ||||
| 	unsigned int m_iteration_packets_avaialble; | ||||
| 	unsigned int m_max_data_packets_per_iteration; | ||||
| 	unsigned int m_max_packets_requeued = 256; | ||||
| }; | ||||
| 
 | ||||
| class ConnectionReceiveThread : public Thread | ||||
| { | ||||
| public: | ||||
| 	ConnectionReceiveThread(); | ||||
| 
 | ||||
| 	void *run(); | ||||
| 
 | ||||
| 	void setParent(Connection *parent) | ||||
| 	{ | ||||
| 		assert(parent); // Pre-condition
 | ||||
| 		m_connection = parent; | ||||
| 	} | ||||
| 
 | ||||
| private: | ||||
| 	void receive(SharedBuffer<u8> &packetdata, bool &packet_queued); | ||||
| 
 | ||||
| 	// Returns next data from a buffer if possible
 | ||||
| 	// If found, returns true; if not, false.
 | ||||
| 	// If found, sets peer_id and dst
 | ||||
| 	bool getFromBuffers(session_t &peer_id, SharedBuffer<u8> &dst); | ||||
| 
 | ||||
| 	bool checkIncomingBuffers( | ||||
| 			Channel *channel, session_t &peer_id, SharedBuffer<u8> &dst); | ||||
| 
 | ||||
| 	/*
 | ||||
| 		Processes a packet with the basic header stripped out. | ||||
| 		Parameters: | ||||
| 			packetdata: Data in packet (with no base headers) | ||||
| 			peer_id: peer id of the sender of the packet in question | ||||
| 			channelnum: channel on which the packet was sent | ||||
| 			reliable: true if recursing into a reliable packet | ||||
| 	*/ | ||||
| 	SharedBuffer<u8> processPacket(Channel *channel, | ||||
| 			const SharedBuffer<u8> &packetdata, session_t peer_id, | ||||
| 			u8 channelnum, bool reliable); | ||||
| 
 | ||||
| 	SharedBuffer<u8> handlePacketType_Control(Channel *channel, | ||||
| 			const SharedBuffer<u8> &packetdata, Peer *peer, u8 channelnum, | ||||
| 			bool reliable); | ||||
| 	SharedBuffer<u8> handlePacketType_Original(Channel *channel, | ||||
| 			const SharedBuffer<u8> &packetdata, Peer *peer, u8 channelnum, | ||||
| 			bool reliable); | ||||
| 	SharedBuffer<u8> handlePacketType_Split(Channel *channel, | ||||
| 			const SharedBuffer<u8> &packetdata, Peer *peer, u8 channelnum, | ||||
| 			bool reliable); | ||||
| 	SharedBuffer<u8> handlePacketType_Reliable(Channel *channel, | ||||
| 			const SharedBuffer<u8> &packetdata, Peer *peer, u8 channelnum, | ||||
| 			bool reliable); | ||||
| 
 | ||||
| 	struct PacketTypeHandler | ||||
| 	{ | ||||
| 		SharedBuffer<u8> (ConnectionReceiveThread::*handler)(Channel *channel, | ||||
| 				const SharedBuffer<u8> &packet, Peer *peer, u8 channelnum, | ||||
| 				bool reliable); | ||||
| 	}; | ||||
| 
 | ||||
| 	struct RateLimitHelper { | ||||
| 		u64 time = 0; | ||||
| 		int counter = 0; | ||||
| 		bool logged = false; | ||||
| 
 | ||||
| 		void tick() { | ||||
| 			u64 now = porting::getTimeS(); | ||||
| 			if (time != now) { | ||||
| 				time = now; | ||||
| 				counter = 0; | ||||
| 				logged = false; | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	static const PacketTypeHandler packetTypeRouter[PACKET_TYPE_MAX]; | ||||
| 
 | ||||
| 	Connection *m_connection = nullptr; | ||||
| 
 | ||||
| 	RateLimitHelper m_new_peer_ratelimit; | ||||
| }; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user