mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +01:00
Debug proxy cpp example
This commit is contained in:
133
cpp/examples/debug_proxy.cpp
Normal file
133
cpp/examples/debug_proxy.cpp
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <idevice++/core_device_proxy.hpp>
|
||||||
|
#include <idevice++/debug_proxy.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
#include <idevice++/rsd.hpp>
|
||||||
|
#include <idevice++/usbmuxd.hpp>
|
||||||
|
|
||||||
|
static void die(const char* msg, const IdeviceFFI::FfiError& e) {
|
||||||
|
std::cerr << msg << ": " << e.message << "\n";
|
||||||
|
std::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> split_args(const std::string& line) {
|
||||||
|
std::istringstream iss(line);
|
||||||
|
std::vector<std::string> toks;
|
||||||
|
std::string tok;
|
||||||
|
while (iss >> tok)
|
||||||
|
toks.push_back(tok);
|
||||||
|
return toks;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
IdeviceFFI::FfiError err;
|
||||||
|
|
||||||
|
// 1) usbmuxd → pick first device
|
||||||
|
auto mux = IdeviceFFI::UsbmuxdConnection::default_new(/*tag*/ 0, err);
|
||||||
|
if (!mux)
|
||||||
|
die("failed to connect to usbmuxd", err);
|
||||||
|
|
||||||
|
auto devices = mux->get_devices(err);
|
||||||
|
if (!devices)
|
||||||
|
die("failed to list devices", err);
|
||||||
|
if (devices->empty()) {
|
||||||
|
std::cerr << "no devices connected\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& dev = (*devices)[0];
|
||||||
|
auto udid = dev.get_udid();
|
||||||
|
if (!udid) {
|
||||||
|
std::cerr << "device has no UDID\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
auto mux_id = dev.get_id();
|
||||||
|
if (!mux_id) {
|
||||||
|
std::cerr << "device has no mux id\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Provider via default usbmuxd addr
|
||||||
|
auto addr = IdeviceFFI::UsbmuxdAddr::default_new();
|
||||||
|
|
||||||
|
const uint32_t tag = 0;
|
||||||
|
const std::string label = "debug-proxy-jkcoxson";
|
||||||
|
auto provider =
|
||||||
|
IdeviceFFI::Provider::usbmuxd_new(std::move(addr), tag, *udid, *mux_id, label, err);
|
||||||
|
if (!provider)
|
||||||
|
die("failed to create provider", err);
|
||||||
|
|
||||||
|
// 3) CoreDeviceProxy
|
||||||
|
auto cdp = IdeviceFFI::CoreDeviceProxy::connect(*provider, err);
|
||||||
|
if (!cdp)
|
||||||
|
die("failed CoreDeviceProxy connect", err);
|
||||||
|
|
||||||
|
auto rsd_port = cdp->get_server_rsd_port(err);
|
||||||
|
if (!rsd_port)
|
||||||
|
die("failed to get RSD port", err);
|
||||||
|
|
||||||
|
// 4) Software tunnel → stream
|
||||||
|
auto adapter = std::move(*cdp).create_tcp_adapter(err);
|
||||||
|
if (!adapter)
|
||||||
|
die("failed to create software tunnel adapter", err);
|
||||||
|
|
||||||
|
auto stream = adapter->connect(*rsd_port, err);
|
||||||
|
if (!stream)
|
||||||
|
die("failed to connect RSD stream", err);
|
||||||
|
|
||||||
|
// 5) RSD handshake
|
||||||
|
auto rsd = IdeviceFFI::RsdHandshake::from_socket(std::move(*stream), err);
|
||||||
|
if (!rsd)
|
||||||
|
die("failed RSD handshake", err);
|
||||||
|
|
||||||
|
// 6) DebugProxy over RSD
|
||||||
|
auto dbg = IdeviceFFI::DebugProxy::connect_rsd(*adapter, *rsd, err);
|
||||||
|
if (!dbg)
|
||||||
|
die("failed to connect DebugProxy", err);
|
||||||
|
|
||||||
|
std::cout << "Shell connected! Type 'exit' to quit.\n";
|
||||||
|
for (;;) {
|
||||||
|
std::cout << "> " << std::flush;
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
if (!std::getline(std::cin, line))
|
||||||
|
break;
|
||||||
|
// trim
|
||||||
|
auto first = line.find_first_not_of(" \t\r\n");
|
||||||
|
if (first == std::string::npos)
|
||||||
|
continue;
|
||||||
|
auto last = line.find_last_not_of(" \t\r\n");
|
||||||
|
line = line.substr(first, last - first + 1);
|
||||||
|
|
||||||
|
if (line == "exit")
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Interpret: first token = command name, rest = argv
|
||||||
|
auto toks = split_args(line);
|
||||||
|
if (toks.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
std::string name = toks.front();
|
||||||
|
std::vector<std::string> argv(toks.begin() + 1, toks.end());
|
||||||
|
|
||||||
|
auto res = dbg->send_command(name, argv, err);
|
||||||
|
if (!res && err) {
|
||||||
|
std::cerr << "send_command failed: " << err.message << "\n";
|
||||||
|
// clear error for next loop
|
||||||
|
err = IdeviceFFI::FfiError{};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (res && !res->empty()) {
|
||||||
|
std::cout << *res << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -66,7 +66,6 @@ class AppService {
|
|||||||
connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err);
|
connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err);
|
||||||
|
|
||||||
// Factory: from socket Box<dyn ReadWrite> (consumes it).
|
// Factory: from socket Box<dyn ReadWrite> (consumes it).
|
||||||
// Only use if you actually obtain such a pointer from C.
|
|
||||||
static std::optional<AppService> from_readwrite_ptr(ReadWriteOpaque* consumed, FfiError& err);
|
static std::optional<AppService> from_readwrite_ptr(ReadWriteOpaque* consumed, FfiError& err);
|
||||||
|
|
||||||
// nice ergonomic overload: consume a C++ ReadWrite by releasing it
|
// nice ergonomic overload: consume a C++ ReadWrite by releasing it
|
||||||
|
|||||||
120
cpp/include/idevice++/debug_proxy.hpp
Normal file
120
cpp/include/idevice++/debug_proxy.hpp
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Bring in the global C ABI (all C structs/functions are global)
|
||||||
|
#include <idevice++/core_device_proxy.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/rsd.hpp>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
class DebugProxy {
|
||||||
|
public:
|
||||||
|
DebugProxy() = default;
|
||||||
|
DebugProxy(const DebugProxy&) = delete;
|
||||||
|
DebugProxy& operator=(const DebugProxy&) = delete;
|
||||||
|
|
||||||
|
DebugProxy(DebugProxy&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; }
|
||||||
|
DebugProxy& operator=(DebugProxy&& other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
reset();
|
||||||
|
handle_ = other.handle_;
|
||||||
|
other.handle_ = nullptr;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
~DebugProxy() { reset(); }
|
||||||
|
|
||||||
|
// Factory: connect over RSD (borrows adapter & handshake; does not consume them)
|
||||||
|
static std::optional<DebugProxy>
|
||||||
|
connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err);
|
||||||
|
|
||||||
|
// Factory: consume a ReadWrite stream (fat pointer)
|
||||||
|
static std::optional<DebugProxy> from_readwrite_ptr(::ReadWriteOpaque* consumed, FfiError& err);
|
||||||
|
|
||||||
|
// Convenience: consume a C++ ReadWrite wrapper by releasing it into the ABI
|
||||||
|
static std::optional<DebugProxy> from_readwrite(ReadWrite&& rw, FfiError& err);
|
||||||
|
|
||||||
|
// API
|
||||||
|
std::optional<std::string>
|
||||||
|
send_command(const std::string& name, const std::vector<std::string>& argv, FfiError& err);
|
||||||
|
|
||||||
|
std::optional<std::string> read_response(FfiError& err);
|
||||||
|
|
||||||
|
bool send_raw(const std::vector<uint8_t>& data, FfiError& err);
|
||||||
|
|
||||||
|
// Reads up to `len` bytes; ABI returns a heap C string (we treat as bytes → string)
|
||||||
|
std::optional<std::string> read(std::size_t len, FfiError& err);
|
||||||
|
|
||||||
|
// Sets argv, returns textual reply (OK/echo/etc)
|
||||||
|
std::optional<std::string> set_argv(const std::vector<std::string>& argv, FfiError& err);
|
||||||
|
|
||||||
|
bool send_ack(FfiError& err);
|
||||||
|
bool send_nack(FfiError& err);
|
||||||
|
|
||||||
|
// No error object in ABI; immediate effect
|
||||||
|
void set_ack_mode(bool enabled) { ::debug_proxy_set_ack_mode(handle_, enabled ? 1 : 0); }
|
||||||
|
|
||||||
|
::DebugProxyHandle* raw() const { return handle_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit DebugProxy(::DebugProxyHandle* h) : handle_(h) {}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
if (handle_) {
|
||||||
|
::debug_proxy_free(handle_);
|
||||||
|
handle_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::DebugProxyHandle* handle_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Small helper that owns a DebugserverCommandHandle
|
||||||
|
class DebugCommand {
|
||||||
|
public:
|
||||||
|
DebugCommand() = default;
|
||||||
|
DebugCommand(const DebugCommand&) = delete;
|
||||||
|
DebugCommand& operator=(const DebugCommand&) = delete;
|
||||||
|
|
||||||
|
DebugCommand(DebugCommand&& other) noexcept : handle_(other.handle_) {
|
||||||
|
other.handle_ = nullptr;
|
||||||
|
}
|
||||||
|
DebugCommand& operator=(DebugCommand&& other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
reset();
|
||||||
|
handle_ = other.handle_;
|
||||||
|
other.handle_ = nullptr;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
~DebugCommand() { reset(); }
|
||||||
|
|
||||||
|
static std::optional<DebugCommand> make(const std::string& name,
|
||||||
|
const std::vector<std::string>& argv);
|
||||||
|
|
||||||
|
::DebugserverCommandHandle* raw() const { return handle_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit DebugCommand(::DebugserverCommandHandle* h) : handle_(h) {}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
if (handle_) {
|
||||||
|
::debugserver_command_free(handle_);
|
||||||
|
handle_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::DebugserverCommandHandle* handle_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
141
cpp/src/debug_proxy.cpp
Normal file
141
cpp/src/debug_proxy.cpp
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <idevice++/debug_proxy.hpp>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
static std::optional<std::string> take_cstring(char* p) {
|
||||||
|
if (!p)
|
||||||
|
return std::nullopt;
|
||||||
|
std::string s(p);
|
||||||
|
::idevice_string_free(p);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DebugCommand ----
|
||||||
|
std::optional<DebugCommand> DebugCommand::make(const std::string& name,
|
||||||
|
const std::vector<std::string>& argv) {
|
||||||
|
std::vector<const char*> c_argv;
|
||||||
|
c_argv.reserve(argv.size());
|
||||||
|
for (auto& a : argv)
|
||||||
|
c_argv.push_back(a.c_str());
|
||||||
|
|
||||||
|
auto* h = ::debugserver_command_new(
|
||||||
|
name.c_str(),
|
||||||
|
c_argv.empty() ? nullptr : const_cast<const char* const*>(c_argv.data()),
|
||||||
|
c_argv.size());
|
||||||
|
if (!h)
|
||||||
|
return std::nullopt;
|
||||||
|
return DebugCommand(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DebugProxy factories ----
|
||||||
|
std::optional<DebugProxy>
|
||||||
|
DebugProxy::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) {
|
||||||
|
::DebugProxyHandle* out = nullptr;
|
||||||
|
if (IdeviceFfiError* e = ::debug_proxy_connect_rsd(adapter.raw(), rsd.raw(), &out)) {
|
||||||
|
err = FfiError(e);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return DebugProxy(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<DebugProxy> DebugProxy::from_readwrite_ptr(::ReadWriteOpaque* consumed,
|
||||||
|
FfiError& err) {
|
||||||
|
::DebugProxyHandle* out = nullptr;
|
||||||
|
if (IdeviceFfiError* e = ::debug_proxy_new(consumed, &out)) {
|
||||||
|
err = FfiError(e);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return DebugProxy(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<DebugProxy> DebugProxy::from_readwrite(ReadWrite&& rw, FfiError& err) {
|
||||||
|
// Rust consumes the pointer regardless of outcome; release before calling
|
||||||
|
return from_readwrite_ptr(rw.release(), err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DebugProxy API ----
|
||||||
|
std::optional<std::string> DebugProxy::send_command(const std::string& name,
|
||||||
|
const std::vector<std::string>& argv,
|
||||||
|
FfiError& err) {
|
||||||
|
auto cmd = DebugCommand::make(name, argv);
|
||||||
|
if (!cmd) {
|
||||||
|
// treat as invalid arg
|
||||||
|
err.code = -1;
|
||||||
|
err.message = "debugserver_command_new failed";
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* resp_c = nullptr;
|
||||||
|
if (IdeviceFfiError* e = ::debug_proxy_send_command(handle_, cmd->raw(), &resp_c)) {
|
||||||
|
err = FfiError(e);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return take_cstring(resp_c); // may be null → std::nullopt
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> DebugProxy::read_response(FfiError& err) {
|
||||||
|
char* resp_c = nullptr;
|
||||||
|
if (IdeviceFfiError* e = ::debug_proxy_read_response(handle_, &resp_c)) {
|
||||||
|
err = FfiError(e);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return take_cstring(resp_c);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DebugProxy::send_raw(const std::vector<uint8_t>& data, FfiError& err) {
|
||||||
|
if (IdeviceFfiError* e = ::debug_proxy_send_raw(handle_, data.data(), data.size())) {
|
||||||
|
err = FfiError(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> DebugProxy::read(std::size_t len, FfiError& err) {
|
||||||
|
char* resp_c = nullptr;
|
||||||
|
if (IdeviceFfiError* e = ::debug_proxy_read(handle_, len, &resp_c)) {
|
||||||
|
err = FfiError(e);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return take_cstring(resp_c);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> DebugProxy::set_argv(const std::vector<std::string>& argv,
|
||||||
|
FfiError& err) {
|
||||||
|
std::vector<const char*> c_argv;
|
||||||
|
c_argv.reserve(argv.size());
|
||||||
|
for (auto& a : argv)
|
||||||
|
c_argv.push_back(a.c_str());
|
||||||
|
|
||||||
|
char* resp_c = nullptr;
|
||||||
|
if (IdeviceFfiError* e = ::debug_proxy_set_argv(
|
||||||
|
handle_,
|
||||||
|
c_argv.empty() ? nullptr : const_cast<const char* const*>(c_argv.data()),
|
||||||
|
c_argv.size(),
|
||||||
|
&resp_c)) {
|
||||||
|
err = FfiError(e);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return take_cstring(resp_c);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DebugProxy::send_ack(FfiError& err) {
|
||||||
|
if (IdeviceFfiError* e = ::debug_proxy_send_ack(handle_)) {
|
||||||
|
err = FfiError(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DebugProxy::send_nack(FfiError& err) {
|
||||||
|
if (IdeviceFfiError* e = ::debug_proxy_send_nack(handle_)) {
|
||||||
|
err = FfiError(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
@@ -12,7 +12,6 @@ std::optional<Lockdown> Lockdown::connect(Provider& provider, FfiError& err) {
|
|||||||
|
|
||||||
if (IdeviceFfiError* e = ::lockdownd_connect(provider.raw(), &out)) {
|
if (IdeviceFfiError* e = ::lockdownd_connect(provider.raw(), &out)) {
|
||||||
// Rust freed the provider on error -> abandon our ownership to avoid double free.
|
// Rust freed the provider on error -> abandon our ownership to avoid double free.
|
||||||
// Your Provider wrapper should expose release().
|
|
||||||
provider.release();
|
provider.release();
|
||||||
err = FfiError(e);
|
err = FfiError(e);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ std::optional<uint32_t> UsbmuxdDevice::get_id() const {
|
|||||||
std::optional<UsbmuxdConnectionType> UsbmuxdDevice::get_connection_type() const {
|
std::optional<UsbmuxdConnectionType> UsbmuxdDevice::get_connection_type() const {
|
||||||
uint8_t t = idevice_usbmuxd_device_get_connection_type(handle_.get());
|
uint8_t t = idevice_usbmuxd_device_get_connection_type(handle_.get());
|
||||||
if (t == 0)
|
if (t == 0)
|
||||||
return std::nullopt; // adjust to your API contract
|
return std::nullopt;
|
||||||
return UsbmuxdConnectionType(t);
|
return UsbmuxdConnectionType(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use idevice::{IdeviceError, ReadWrite, RsdService};
|
|||||||
|
|
||||||
use crate::core_device_proxy::AdapterHandle;
|
use crate::core_device_proxy::AdapterHandle;
|
||||||
use crate::rsd::RsdHandshakeHandle;
|
use crate::rsd::RsdHandshakeHandle;
|
||||||
use crate::{IdeviceFfiError, RUNTIME, ffi_err};
|
use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err};
|
||||||
|
|
||||||
/// Opaque handle to a DebugProxyClient
|
/// Opaque handle to a DebugProxyClient
|
||||||
pub struct DebugProxyHandle(pub DebugProxyClient<Box<dyn ReadWrite>>);
|
pub struct DebugProxyHandle(pub DebugProxyClient<Box<dyn ReadWrite>>);
|
||||||
@@ -170,7 +170,7 @@ pub unsafe extern "C" fn debug_proxy_connect_rsd(
|
|||||||
/// `handle` must be a valid pointer to a location where the handle will be stored
|
/// `handle` must be a valid pointer to a location where the handle will be stored
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn debug_proxy_new(
|
pub unsafe extern "C" fn debug_proxy_new(
|
||||||
socket: *mut Box<dyn ReadWrite>,
|
socket: *mut ReadWriteOpaque,
|
||||||
handle: *mut *mut DebugProxyHandle,
|
handle: *mut *mut DebugProxyHandle,
|
||||||
) -> *mut IdeviceFfiError {
|
) -> *mut IdeviceFfiError {
|
||||||
if socket.is_null() || handle.is_null() {
|
if socket.is_null() || handle.is_null() {
|
||||||
@@ -178,7 +178,7 @@ pub unsafe extern "C" fn debug_proxy_new(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let socket = unsafe { Box::from_raw(socket) };
|
let socket = unsafe { Box::from_raw(socket) };
|
||||||
let client = DebugProxyClient::new(*socket);
|
let client = DebugProxyClient::new(socket.inner.unwrap());
|
||||||
let new_handle = DebugProxyHandle(client);
|
let new_handle = DebugProxyHandle(client);
|
||||||
|
|
||||||
unsafe { *handle = Box::into_raw(Box::new(new_handle)) };
|
unsafe { *handle = Box::into_raw(Box::new(new_handle)) };
|
||||||
|
|||||||
Reference in New Issue
Block a user