mirror of
https://github.com/wichtounet/thor-os.git
synced 2025-09-09 12:31:06 -04:00
Basic nslookup support
This commit is contained in:
parent
9f27ad8420
commit
eabca269b0
@ -115,6 +115,8 @@ std::expected<size_t> wait_for_packet(char* buffer, socket_fd_t socket_fd);
|
||||
*/
|
||||
std::expected<size_t> wait_for_packet(char* buffer, socket_fd_t socket_fd, size_t ms);
|
||||
|
||||
void propagate_packet(const ethernet::packet& packet, socket_protocol protocol);
|
||||
|
||||
} // end of network namespace
|
||||
|
||||
#endif
|
||||
|
@ -180,6 +180,8 @@ void network::dns::decode(network::interface_descriptor& /*interface*/, network:
|
||||
logging::logf(logging::log_level::TRACE, "dns: Refused\n");
|
||||
}
|
||||
}
|
||||
|
||||
network::propagate_packet(packet, network::socket_protocol::DNS);
|
||||
}
|
||||
|
||||
std::expected<network::ethernet::packet> network::dns::prepare_packet_query(network::interface_descriptor& interface, network::ip::address target_ip, uint16_t source_port, uint16_t identification, size_t payload_size) {
|
||||
|
@ -111,23 +111,7 @@ void network::icmp::decode(network::interface_descriptor& interface, network::et
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO Need something better for this
|
||||
|
||||
for(size_t pid = 0; pid < scheduler::MAX_PROCESS; ++pid){
|
||||
auto state = scheduler::get_process_state(pid);
|
||||
if(state != scheduler::process_state::EMPTY && state != scheduler::process_state::NEW && state != scheduler::process_state::KILLED){
|
||||
for(auto& socket : scheduler::get_sockets(pid)){
|
||||
if(socket.listen && socket.protocol == network::socket_protocol::ICMP){
|
||||
auto copy = packet;
|
||||
copy.payload = new char[copy.payload_size];
|
||||
std::copy_n(packet.payload, packet.payload_size, copy.payload);
|
||||
|
||||
socket.listen_packets.push(copy);
|
||||
socket.listen_queue.wake_up();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
network::propagate_packet(packet, network::socket_protocol::ICMP);
|
||||
}
|
||||
|
||||
std::expected<network::ethernet::packet> network::icmp::prepare_packet(network::interface_descriptor& interface, network::ip::address target_ip, size_t payload_size, type t, size_t code){
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include "tlib/errors.hpp"
|
||||
|
||||
#include "net/icmp_layer.hpp"
|
||||
#include "net/dns_layer.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@ -218,7 +219,7 @@ std::expected<network::socket_fd_t> network::open(network::socket_domain domain,
|
||||
return std::make_expected_from_error<network::socket_fd_t>(std::ERROR_SOCKET_INVALID_TYPE);
|
||||
}
|
||||
|
||||
if(protocol != socket_protocol::ICMP){
|
||||
if(protocol != socket_protocol::ICMP && protocol != socket_protocol::DNS){
|
||||
return std::make_expected_from_error<network::socket_fd_t>(std::ERROR_SOCKET_INVALID_PROTOCOL);
|
||||
}
|
||||
|
||||
@ -242,19 +243,37 @@ std::tuple<size_t, size_t> network::prepare_packet(socket_fd_t socket_fd, void*
|
||||
|
||||
auto& socket = scheduler::get_socket(socket_fd);
|
||||
|
||||
switch(socket.protocol){
|
||||
case network::socket_protocol::ICMP:
|
||||
auto return_from_packet = [&socket](std::expected<network::ethernet::packet>& packet) -> std::tuple<size_t, size_t> {
|
||||
if (packet) {
|
||||
auto fd = socket.register_packet(*packet);
|
||||
|
||||
return {fd, packet->index};
|
||||
} else {
|
||||
return {-packet.error(), 0};
|
||||
}
|
||||
};
|
||||
|
||||
switch (socket.protocol) {
|
||||
case network::socket_protocol::ICMP: {
|
||||
auto descriptor = static_cast<network::icmp::packet_descriptor*>(desc);
|
||||
auto& interface = select_interface(descriptor->target_ip);
|
||||
auto packet = network::icmp::prepare_packet(buffer, interface, descriptor->target_ip, descriptor->payload_size, descriptor->type, descriptor->code);
|
||||
auto packet = network::icmp::prepare_packet(buffer, interface, descriptor->target_ip, descriptor->payload_size, descriptor->type, descriptor->code);
|
||||
|
||||
if(packet){
|
||||
auto fd = socket.register_packet(*packet);
|
||||
return return_from_packet(packet);
|
||||
}
|
||||
|
||||
return {fd, packet->index};
|
||||
case network::socket_protocol::DNS: {
|
||||
auto descriptor = static_cast<network::dns::packet_descriptor*>(desc);
|
||||
auto& interface = select_interface(descriptor->target_ip);
|
||||
|
||||
if(descriptor->query){
|
||||
auto packet = network::dns::prepare_packet_query(buffer, interface, descriptor->target_ip, descriptor->source_port, descriptor->identification, descriptor->payload_size);
|
||||
|
||||
return return_from_packet(packet);
|
||||
} else {
|
||||
return {-packet.error(), 0};
|
||||
return {-std::ERROR_SOCKET_INVALID_PACKET_DESCRIPTOR, 0};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {-std::ERROR_SOCKET_UNIMPLEMENTED, 0};
|
||||
@ -279,6 +298,12 @@ std::expected<void> network::finalize_packet(socket_fd_t socket_fd, size_t packe
|
||||
network::icmp::finalize_packet(interface, packet);
|
||||
socket.erase_packet(packet_fd);
|
||||
|
||||
return std::make_expected();
|
||||
|
||||
case network::socket_protocol::DNS:
|
||||
network::dns::finalize_packet(interface, packet);
|
||||
socket.erase_packet(packet_fd);
|
||||
|
||||
return std::make_expected();
|
||||
}
|
||||
|
||||
@ -358,3 +383,23 @@ std::expected<size_t> network::wait_for_packet(char* buffer, socket_fd_t socket_
|
||||
|
||||
return {packet.index};
|
||||
}
|
||||
|
||||
void network::propagate_packet(const ethernet::packet& packet, socket_protocol protocol){
|
||||
// TODO Need something better for this
|
||||
|
||||
for(size_t pid = 0; pid < scheduler::MAX_PROCESS; ++pid){
|
||||
auto state = scheduler::get_process_state(pid);
|
||||
if(state != scheduler::process_state::EMPTY && state != scheduler::process_state::NEW && state != scheduler::process_state::KILLED){
|
||||
for(auto& socket : scheduler::get_sockets(pid)){
|
||||
if(socket.listen && socket.protocol == protocol){
|
||||
auto copy = packet;
|
||||
copy.payload = new char[copy.payload_size];
|
||||
std::copy_n(packet.payload, packet.payload_size, copy.payload);
|
||||
|
||||
socket.listen_packets.push(copy);
|
||||
socket.listen_queue.wake_up();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
programs/nslookup/Makefile
Normal file
14
programs/nslookup/Makefile
Normal file
@ -0,0 +1,14 @@
|
||||
.PHONY: default clean
|
||||
|
||||
EXEC_NAME=nslookup
|
||||
|
||||
default: link
|
||||
|
||||
include ../../cpp.mk
|
||||
|
||||
$(eval $(call program_compile_cpp_folder,src))
|
||||
$(eval $(call program_link_executable,$(EXEC_NAME)))
|
||||
|
||||
clean:
|
||||
@ echo -e "Remove compiled files"
|
||||
@ rm -rf debug
|
207
programs/nslookup/src/main.cpp
Normal file
207
programs/nslookup/src/main.cpp
Normal file
@ -0,0 +1,207 @@
|
||||
//=======================================================================
|
||||
// Copyright Baptiste Wicht 2013-2016.
|
||||
// Distributed under the terms of the MIT License.
|
||||
// (See accompanying file LICENSE or copy at
|
||||
// http://www.opensource.org/licenses/MIT)
|
||||
//=======================================================================
|
||||
|
||||
#include <tlib/system.hpp>
|
||||
#include <tlib/errors.hpp>
|
||||
#include <tlib/print.hpp>
|
||||
#include <tlib/net.hpp>
|
||||
#include <tlib/dns.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
static constexpr const size_t retries = 10;
|
||||
static constexpr const size_t timeout_ms = 2000;
|
||||
|
||||
bool send_request(tlib::socket& sock, const std::string& domain){
|
||||
auto parts = std::split(domain, '.');
|
||||
|
||||
size_t characters = domain.size() - (parts.size() - 1); // The dots are not included
|
||||
size_t labels = parts.size();
|
||||
|
||||
tlib::dns::packet_descriptor desc;
|
||||
desc.payload_size = labels + characters + 1 + 2 * 2;
|
||||
desc.target_ip = tlib::ip::make_address(10, 0, 2, 3);
|
||||
desc.source_port = 3456;
|
||||
desc.identification = 0x666;
|
||||
desc.query = true;
|
||||
|
||||
auto packet = sock.prepare_packet(&desc);
|
||||
|
||||
if (!sock) {
|
||||
tlib::printf("nslookup: prepare_packet error: %s\n", std::error_message(sock.error()));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* payload = reinterpret_cast<char*>(packet.payload + packet.index);
|
||||
|
||||
size_t i = 0;
|
||||
for (auto& part : parts) {
|
||||
payload[i++] = part.size();
|
||||
|
||||
for (size_t j = 0; j < part.size(); ++j) {
|
||||
payload[i++] = part[j];
|
||||
}
|
||||
}
|
||||
|
||||
payload[i++] = 0;
|
||||
|
||||
auto* q_type = reinterpret_cast<uint16_t*>(packet.payload + packet.index + i);
|
||||
*q_type = 0x0100; // A Record
|
||||
|
||||
auto* q_class = reinterpret_cast<uint16_t*>(packet.payload + packet.index + i + 2);
|
||||
*q_class = 0x0100; // IN (internet)
|
||||
|
||||
sock.finalize_packet(packet);
|
||||
|
||||
if (!sock) {
|
||||
tlib::printf("nslookup: finalize_packet error: %s\n", std::error_message(sock.error()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // end of anonymous namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc != 2) {
|
||||
tlib::print_line("usage: nslookup domain");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string domain(argv[1]);
|
||||
|
||||
tlib::socket sock(tlib::socket_domain::AF_INET, tlib::socket_type::RAW, tlib::socket_protocol::DNS);
|
||||
|
||||
if (!sock) {
|
||||
tlib::printf("nslookup: socket error: %s\n", std::error_message(sock.error()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
sock.listen(true);
|
||||
|
||||
if (!sock) {
|
||||
tlib::printf("nslookup: socket error: %s\n", std::error_message(sock.error()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t tries = 0;
|
||||
|
||||
if(!send_request(sock, domain)){
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto before = tlib::ms_time();
|
||||
auto after = before;
|
||||
|
||||
while (true) {
|
||||
// Make sure we don't wait for more than the timeout
|
||||
if (after > before + timeout_ms) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto remaining = timeout_ms - (after - before);
|
||||
|
||||
auto p = sock.wait_for_packet(remaining);
|
||||
if (!sock) {
|
||||
tlib::printf("nslookup: wait_for_packet error: %s\n", std::error_message(sock.error()));
|
||||
return 1;
|
||||
} else {
|
||||
auto* dns_header = reinterpret_cast<tlib::dns::header*>(p.payload + p.index);
|
||||
|
||||
auto identification = tlib::switch_endian_16(dns_header->identification);
|
||||
|
||||
// Only handle packet with the correct identification
|
||||
if (identification == 0x666) {
|
||||
auto questions = tlib::switch_endian_16(dns_header->questions);
|
||||
auto answers = tlib::switch_endian_16(dns_header->answers);
|
||||
|
||||
auto flags = dns_header->flags;
|
||||
auto qr = flags >> 15;
|
||||
|
||||
// Only handle Response
|
||||
if (qr) {
|
||||
auto rcode = flags & 0xF;
|
||||
|
||||
if (rcode == 0x0 && answers > 0) {
|
||||
auto* payload = p.payload + p.index + sizeof(tlib::dns::header);
|
||||
|
||||
// Decode the questions (simply wrap around it)
|
||||
|
||||
for (size_t i = 0; i < questions; ++i) {
|
||||
size_t length;
|
||||
auto domain = tlib::dns::decode_domain(payload, length);
|
||||
|
||||
payload += length;
|
||||
payload += 4;
|
||||
|
||||
tlib::printf("DNS Question %s\n", domain.c_str());
|
||||
}
|
||||
|
||||
tlib::printf("DNS Answers:\n");
|
||||
|
||||
for (size_t i = 0; i < answers; ++i) {
|
||||
auto label = static_cast<uint8_t>(*payload);
|
||||
|
||||
std::string domain;
|
||||
if (label > 64) {
|
||||
// This is a pointer
|
||||
auto pointer = tlib::switch_endian_16(*reinterpret_cast<uint16_t*>(payload));
|
||||
auto offset = pointer & (0xFFFF >> 2);
|
||||
|
||||
payload += 2;
|
||||
|
||||
size_t ignored;
|
||||
domain = tlib::dns::decode_domain(p.payload + p.index + offset, ignored);
|
||||
} else {
|
||||
tlib::printf("nslookup: Cannot read DNS response\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto rr_type = tlib::switch_endian_16(*reinterpret_cast<uint16_t*>(payload));
|
||||
payload += 2;
|
||||
|
||||
auto rr_class = tlib::switch_endian_16(*reinterpret_cast<uint16_t*>(payload));
|
||||
payload += 2;
|
||||
|
||||
auto ttl = tlib::switch_endian_32(*reinterpret_cast<uint32_t*>(payload));
|
||||
payload += 4;
|
||||
|
||||
auto rd_length = tlib::switch_endian_16(*reinterpret_cast<uint16_t*>(payload));
|
||||
payload += 2;
|
||||
|
||||
if (rr_type == 0x1 && rr_class == 0x1) {
|
||||
auto ip = reinterpret_cast<uint8_t*>(payload);
|
||||
|
||||
tlib::printf(" Answer %u Domain %s Type %u Class %u TTL %u IP: %u.%u.%u.%u\n",
|
||||
i, domain.c_str(), rr_type, rr_class, ttl, ip[3], ip[2], ip[1], ip[0]);
|
||||
} else {
|
||||
tlib::printf("nslookup: Cannot read DNS response\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
payload += rd_length;
|
||||
}
|
||||
|
||||
break;
|
||||
} else {
|
||||
// There was an error, retry
|
||||
if(++tries == retries || !send_request(sock, domain)){
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
after = tlib::ms_time();
|
||||
}
|
||||
|
||||
sock.listen(false);
|
||||
|
||||
return 0;
|
||||
}
|
27
tlib/include/tlib/dns.hpp
Normal file
27
tlib/include/tlib/dns.hpp
Normal file
@ -0,0 +1,27 @@
|
||||
//=======================================================================
|
||||
// Copyright Baptiste Wicht 2013-2016.
|
||||
// Distributed under the terms of the MIT License.
|
||||
// (See accompanying file LICENSE or copy at
|
||||
// http://www.opensource.org/licenses/MIT)
|
||||
//=======================================================================
|
||||
|
||||
#ifndef TLIB_NET_DNS_H
|
||||
#define TLIB_NET_DNS_H
|
||||
|
||||
#include <string.hpp>
|
||||
|
||||
#include "tlib/net_constants.hpp"
|
||||
|
||||
ASSERT_ONLY_THOR_PROGRAM
|
||||
|
||||
namespace tlib {
|
||||
|
||||
namespace dns {
|
||||
|
||||
std::string decode_domain(char* payload, size_t& offset);
|
||||
|
||||
} // end of namespace dns
|
||||
|
||||
} // end of namespace tlib
|
||||
|
||||
#endif
|
@ -12,34 +12,35 @@
|
||||
|
||||
namespace std {
|
||||
|
||||
constexpr const size_t ERROR_NOT_EXISTS = 1;
|
||||
constexpr const size_t ERROR_NOT_EXECUTABLE = 2;
|
||||
constexpr const size_t ERROR_FAILED_EXECUTION = 3;
|
||||
constexpr const size_t ERROR_NOTHING_MOUNTED = 4;
|
||||
constexpr const size_t ERROR_INVALID_FILE_PATH = 5;
|
||||
constexpr const size_t ERROR_DIRECTORY = 6;
|
||||
constexpr const size_t ERROR_INVALID_FILE_DESCRIPTOR = 7;
|
||||
constexpr const size_t ERROR_FAILED = 8;
|
||||
constexpr const size_t ERROR_EXISTS = 9;
|
||||
constexpr const size_t ERROR_BUFFER_SMALL = 10;
|
||||
constexpr const size_t ERROR_INVALID_FILE_SYSTEM = 11;
|
||||
constexpr const size_t ERROR_DISK_FULL = 12;
|
||||
constexpr const size_t ERROR_PERMISSION_DENIED = 13;
|
||||
constexpr const size_t ERROR_INVALID_OFFSET = 14;
|
||||
constexpr const size_t ERROR_UNSUPPORTED = 15;
|
||||
constexpr const size_t ERROR_INVALID_COUNT = 16;
|
||||
constexpr const size_t ERROR_INVALID_REQUEST = 17;
|
||||
constexpr const size_t ERROR_INVALID_DEVICE = 18;
|
||||
constexpr const size_t ERROR_ALREADY_MOUNTED = 19;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_DOMAIN = 20;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_TYPE = 21;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_PROTOCOL = 22;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_FD = 23;
|
||||
constexpr const size_t ERROR_SOCKET_UNIMPLEMENTED = 24;
|
||||
constexpr const size_t ERROR_SOCKET_NO_INTERFACE = 25;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_PACKET_FD = 26;
|
||||
constexpr const size_t ERROR_SOCKET_NOT_LISTEN = 27;
|
||||
constexpr const size_t ERROR_SOCKET_TIMEOUT = 28;
|
||||
constexpr const size_t ERROR_NOT_EXISTS = 1;
|
||||
constexpr const size_t ERROR_NOT_EXECUTABLE = 2;
|
||||
constexpr const size_t ERROR_FAILED_EXECUTION = 3;
|
||||
constexpr const size_t ERROR_NOTHING_MOUNTED = 4;
|
||||
constexpr const size_t ERROR_INVALID_FILE_PATH = 5;
|
||||
constexpr const size_t ERROR_DIRECTORY = 6;
|
||||
constexpr const size_t ERROR_INVALID_FILE_DESCRIPTOR = 7;
|
||||
constexpr const size_t ERROR_FAILED = 8;
|
||||
constexpr const size_t ERROR_EXISTS = 9;
|
||||
constexpr const size_t ERROR_BUFFER_SMALL = 10;
|
||||
constexpr const size_t ERROR_INVALID_FILE_SYSTEM = 11;
|
||||
constexpr const size_t ERROR_DISK_FULL = 12;
|
||||
constexpr const size_t ERROR_PERMISSION_DENIED = 13;
|
||||
constexpr const size_t ERROR_INVALID_OFFSET = 14;
|
||||
constexpr const size_t ERROR_UNSUPPORTED = 15;
|
||||
constexpr const size_t ERROR_INVALID_COUNT = 16;
|
||||
constexpr const size_t ERROR_INVALID_REQUEST = 17;
|
||||
constexpr const size_t ERROR_INVALID_DEVICE = 18;
|
||||
constexpr const size_t ERROR_ALREADY_MOUNTED = 19;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_DOMAIN = 20;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_TYPE = 21;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_PROTOCOL = 22;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_FD = 23;
|
||||
constexpr const size_t ERROR_SOCKET_UNIMPLEMENTED = 24;
|
||||
constexpr const size_t ERROR_SOCKET_NO_INTERFACE = 25;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_PACKET_FD = 26;
|
||||
constexpr const size_t ERROR_SOCKET_NOT_LISTEN = 27;
|
||||
constexpr const size_t ERROR_SOCKET_TIMEOUT = 28;
|
||||
constexpr const size_t ERROR_SOCKET_INVALID_PACKET_DESCRIPTOR = 29;
|
||||
|
||||
inline const char* error_message(size_t error){
|
||||
switch(error){
|
||||
@ -99,6 +100,8 @@ inline const char* error_message(size_t error){
|
||||
return "The socket is not configured to listen";
|
||||
case ERROR_SOCKET_TIMEOUT:
|
||||
return "Network timeout";
|
||||
case ERROR_SOCKET_INVALID_PACKET_DESCRIPTOR:
|
||||
return "The packet descriptor for the packet to send is invalid";
|
||||
default:
|
||||
return "Unknonwn error";
|
||||
}
|
||||
|
@ -92,6 +92,17 @@ private:
|
||||
size_t error_code; ///< The error code
|
||||
};
|
||||
|
||||
inline uint16_t switch_endian_16(uint16_t nb) {
|
||||
return (nb >> 8) | (nb << 8);
|
||||
}
|
||||
|
||||
inline uint32_t switch_endian_32(uint32_t nb) {
|
||||
return ((nb >> 24) & 0xff) |
|
||||
((nb << 8) & 0xff0000) |
|
||||
((nb >> 8) & 0xff00) |
|
||||
((nb << 24) & 0xff000000);
|
||||
}
|
||||
|
||||
} // end of namespace tlib
|
||||
|
||||
#endif
|
||||
|
@ -103,6 +103,7 @@ struct packet_descriptor {
|
||||
ip::address target_ip;
|
||||
uint16_t source_port;
|
||||
uint16_t identification;
|
||||
bool query;
|
||||
};
|
||||
|
||||
} // end of dns namespace
|
||||
|
35
tlib/src/dns.cpp
Normal file
35
tlib/src/dns.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
//=======================================================================
|
||||
// Copyright Baptiste Wicht 2013-2016.
|
||||
// Distributed under the terms of the MIT License.
|
||||
// (See accompanying file LICENSE or copy at
|
||||
// http://www.opensource.org/licenses/MIT)
|
||||
//=======================================================================
|
||||
|
||||
#include "tlib/dns.hpp"
|
||||
#include "tlib/malloc.hpp"
|
||||
|
||||
std::string tlib::dns::decode_domain(char* payload, size_t& offset) {
|
||||
std::string domain;
|
||||
|
||||
offset = 0;
|
||||
|
||||
while (true) {
|
||||
auto label_size = static_cast<uint8_t>(*(payload + offset));
|
||||
++offset;
|
||||
|
||||
if (!label_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!domain.empty()) {
|
||||
domain += '.';
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < label_size; ++i) {
|
||||
domain += *(payload + offset);
|
||||
++offset;
|
||||
}
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user