mirror of
https://github.com/servalproject/serval-dna.git
synced 2025-01-02 19:36:48 +00:00
Refactor Java API
Add stateful class to represent the state of the serval daemon, shifting some functions from BatPhone Add convenience functions to get pre-configured MDP and HTTP Restful client classes
This commit is contained in:
parent
47f051917d
commit
68dbaef38d
@ -12,8 +12,8 @@ public abstract class AbstractMdpProtocol<T> extends ChannelSelector.Handler {
|
|||||||
protected final MdpSocket socket;
|
protected final MdpSocket socket;
|
||||||
protected final AsyncResult<T> results;
|
protected final AsyncResult<T> results;
|
||||||
|
|
||||||
public AbstractMdpProtocol(ChannelSelector selector, AsyncResult<T> results) throws IOException {
|
public AbstractMdpProtocol(ChannelSelector selector, int loopbackMdpPort, AsyncResult<T> results) throws IOException {
|
||||||
socket = new MdpSocket();
|
this.socket = new MdpSocket(loopbackMdpPort);
|
||||||
socket.bind();
|
socket.bind();
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
this.results = results;
|
this.results = results;
|
||||||
|
@ -7,8 +7,8 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
public class MdpDnaLookup extends AbstractMdpProtocol<ServalDCommand.LookupResult> {
|
public class MdpDnaLookup extends AbstractMdpProtocol<ServalDCommand.LookupResult> {
|
||||||
|
|
||||||
public MdpDnaLookup(ChannelSelector selector, AsyncResult<ServalDCommand.LookupResult> results) throws IOException {
|
public MdpDnaLookup(ChannelSelector selector, int loopbackMdpPort, AsyncResult<ServalDCommand.LookupResult> results) throws IOException {
|
||||||
super(selector, results);
|
super(selector, loopbackMdpPort, results);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendRequest(SubscriberId destination, String did) throws IOException {
|
public void sendRequest(SubscriberId destination, String did) throws IOException {
|
||||||
|
@ -21,8 +21,8 @@ public class MdpServiceLookup extends AbstractMdpProtocol<MdpServiceLookup.Servi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MdpServiceLookup(ChannelSelector selector, AsyncResult<ServiceResult> results) throws IOException {
|
public MdpServiceLookup(ChannelSelector selector, int loopbackMdpPort, AsyncResult<ServiceResult> results) throws IOException {
|
||||||
super(selector, results);
|
super(selector, loopbackMdpPort, results);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendRequest(SubscriberId destination, String pattern) throws IOException {
|
public void sendRequest(SubscriberId destination, String pattern) throws IOException {
|
||||||
|
@ -17,10 +17,11 @@ public class MdpSocket{
|
|||||||
private int port;
|
private int port;
|
||||||
|
|
||||||
private static final InetAddress loopback;
|
private static final InetAddress loopback;
|
||||||
public static int loopbackMdpPort =0;
|
private final int loopbackMdpPort;
|
||||||
static {
|
static {
|
||||||
InetAddress local=null;
|
InetAddress local=null;
|
||||||
try {
|
try {
|
||||||
|
// can't trust Inet4Address.getLocalHost() as some implementations can fail to resolve the name "loopback"
|
||||||
local = Inet4Address.getByAddress(new byte[]{127, 0, 0, 1});
|
local = Inet4Address.getByAddress(new byte[]{127, 0, 0, 1});
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@ -29,12 +30,15 @@ public class MdpSocket{
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Create an unbound socket, may be used for other information requests before binding */
|
/* Create an unbound socket, may be used for other information requests before binding */
|
||||||
public MdpSocket() throws IOException {
|
public MdpSocket(int loopbackMdpPort) throws IOException {
|
||||||
|
this.loopbackMdpPort = loopbackMdpPort;
|
||||||
}
|
}
|
||||||
public MdpSocket(int port) throws IOException {
|
public MdpSocket(int loopbackMdpPort, int port) throws IOException {
|
||||||
|
this(loopbackMdpPort);
|
||||||
bind(SubscriberId.ANY, port);
|
bind(SubscriberId.ANY, port);
|
||||||
}
|
}
|
||||||
public MdpSocket(SubscriberId sid, int port) throws IOException {
|
public MdpSocket(int loopbackMdpPort, SubscriberId sid, int port) throws IOException {
|
||||||
|
this(loopbackMdpPort);
|
||||||
bind(sid, port);
|
bind(sid, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,72 +20,33 @@
|
|||||||
|
|
||||||
package org.servalproject.servaldna;
|
package org.servalproject.servaldna;
|
||||||
|
|
||||||
|
import org.servalproject.codec.Base64;
|
||||||
|
import org.servalproject.servaldna.meshms.MeshMSConversationList;
|
||||||
|
import org.servalproject.servaldna.meshms.MeshMSException;
|
||||||
|
import org.servalproject.servaldna.meshms.MeshMSMessageList;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import org.servalproject.codec.Base64;
|
|
||||||
import org.servalproject.servaldna.SubscriberId;
|
|
||||||
import org.servalproject.servaldna.ServalDCommand;
|
|
||||||
import org.servalproject.servaldna.ServalDInterfaceException;
|
|
||||||
import org.servalproject.servaldna.meshms.MeshMSConversationList;
|
|
||||||
import org.servalproject.servaldna.meshms.MeshMSMessageList;
|
|
||||||
import org.servalproject.servaldna.meshms.MeshMSException;
|
|
||||||
|
|
||||||
public class ServalDClient implements ServalDHttpConnectionFactory
|
public class ServalDClient implements ServalDHttpConnectionFactory
|
||||||
{
|
{
|
||||||
|
private final int httpPort;
|
||||||
|
private final String restfulUsername;
|
||||||
|
private final String restfulPassword;
|
||||||
|
|
||||||
private static final String restfulUsername = "ServalDClient";
|
public ServalDClient(int httpPort, String restfulUsername, String restfulPassword) throws ServalDInterfaceException {
|
||||||
private static final String restfulPasswordDefault = "u6ng^ues%@@SabLEEEE8";
|
if (httpPort < 1 || httpPort > 65535)
|
||||||
private static String restfulPassword;
|
throw new ServalDInterfaceException("invalid HTTP port number: " + httpPort);
|
||||||
protected boolean connected;
|
if (restfulUsername == null)
|
||||||
int httpPort;
|
throw new ServalDInterfaceException("invalid HTTP username");
|
||||||
|
if (restfulPassword == null)
|
||||||
public static ServalDClient newServalDClient()
|
throw new ServalDInterfaceException("invalid HTTP password");
|
||||||
{
|
this.httpPort = httpPort;
|
||||||
return new ServalDClient();
|
this.restfulUsername = restfulUsername;
|
||||||
}
|
this.restfulPassword = restfulPassword;
|
||||||
|
|
||||||
protected ServalDClient()
|
|
||||||
{
|
|
||||||
restfulPassword = null;
|
|
||||||
connected = false;
|
|
||||||
httpPort = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void connect() throws ServalDInterfaceException
|
|
||||||
{
|
|
||||||
ensureServerRunning();
|
|
||||||
if (!connected) {
|
|
||||||
if (!fetchRestfulAuthorization())
|
|
||||||
createRestfulAuthorization();
|
|
||||||
connected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureServerRunning() throws ServalDInterfaceException
|
|
||||||
{
|
|
||||||
ServalDCommand.Status s = ServalDCommand.serverStatus();
|
|
||||||
if (!s.status.equals("running"))
|
|
||||||
throw new ServalDInterfaceException("server is not running");
|
|
||||||
if (s.httpPort < 1 || s.httpPort > 65535)
|
|
||||||
throw new ServalDInterfaceException("invalid HTTP port number: " + s.httpPort);
|
|
||||||
httpPort = s.httpPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean fetchRestfulAuthorization() throws ServalDInterfaceException
|
|
||||||
{
|
|
||||||
restfulPassword = ServalDCommand.getConfigItem("rhizome.api.restful.users." + restfulUsername + ".password");
|
|
||||||
return restfulPassword != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createRestfulAuthorization() throws ServalDInterfaceException
|
|
||||||
{
|
|
||||||
ServalDCommand.setConfigItem("rhizome.api.restful.users." + restfulUsername + ".password", restfulPasswordDefault);
|
|
||||||
ServalDCommand.configSync();
|
|
||||||
if (!fetchRestfulAuthorization())
|
|
||||||
throw new ServalDInterfaceException("restful password not set");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException, MeshMSException
|
public MeshMSConversationList meshmsListConversations(SubscriberId sid) throws ServalDInterfaceException, IOException, MeshMSException
|
||||||
@ -105,9 +66,6 @@ public class ServalDClient implements ServalDHttpConnectionFactory
|
|||||||
// interface ServalDHttpConnectionFactory
|
// interface ServalDHttpConnectionFactory
|
||||||
public HttpURLConnection newServalDHttpConnection(String path) throws ServalDInterfaceException, IOException
|
public HttpURLConnection newServalDHttpConnection(String path) throws ServalDInterfaceException, IOException
|
||||||
{
|
{
|
||||||
connect();
|
|
||||||
assert restfulPassword != null;
|
|
||||||
assert httpPort != 0;
|
|
||||||
URL url = new URL("http", "localhost", httpPort, path);
|
URL url = new URL("http", "localhost", httpPort, path);
|
||||||
URLConnection uconn = url.openConnection();
|
URLConnection uconn = url.openConnection();
|
||||||
HttpURLConnection conn;
|
HttpURLConnection conn;
|
||||||
@ -117,7 +75,6 @@ public class ServalDClient implements ServalDHttpConnectionFactory
|
|||||||
catch (ClassCastException e) {
|
catch (ClassCastException e) {
|
||||||
throw new ServalDInterfaceException("URL.openConnection() returned a " + uconn.getClass().getName() + ", expecting a HttpURLConnection", e);
|
throw new ServalDInterfaceException("URL.openConnection() returned a " + uconn.getClass().getName() + ", expecting a HttpURLConnection", e);
|
||||||
}
|
}
|
||||||
int status = 0;
|
|
||||||
conn.setAllowUserInteraction(false);
|
conn.setAllowUserInteraction(false);
|
||||||
try {
|
try {
|
||||||
conn.addRequestProperty("Authorization", "Basic " + Base64.encode((restfulUsername + ":" + restfulPassword).getBytes("US-ASCII")));
|
conn.addRequestProperty("Authorization", "Basic " + Base64.encode((restfulUsername + ":" + restfulPassword).getBytes("US-ASCII")));
|
||||||
|
109
java/org/servalproject/servaldna/ServerControl.java
Normal file
109
java/org/servalproject/servaldna/ServerControl.java
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package org.servalproject.servaldna;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by jeremy on 20/06/14.
|
||||||
|
*/
|
||||||
|
public class ServerControl {
|
||||||
|
private String instancePath;
|
||||||
|
private final String execPath;
|
||||||
|
private int loopbackMdpPort;
|
||||||
|
private int httpPort=0;
|
||||||
|
private int pid;
|
||||||
|
private static final String restfulUsername="ServalDClient";
|
||||||
|
private ServalDClient client;
|
||||||
|
|
||||||
|
public ServerControl(){
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
public ServerControl(String execPath){
|
||||||
|
this.execPath = execPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstancePath(){
|
||||||
|
return instancePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStatus(ServalDCommand.Status result){
|
||||||
|
loopbackMdpPort = result.mdpInetPort;
|
||||||
|
pid = result.pid;
|
||||||
|
httpPort = result.httpPort;
|
||||||
|
instancePath = result.instancePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearStatus(){
|
||||||
|
loopbackMdpPort = 0;
|
||||||
|
pid = 0;
|
||||||
|
httpPort = 0;
|
||||||
|
client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() throws ServalDFailureException {
|
||||||
|
if (execPath==null)
|
||||||
|
setStatus(ServalDCommand.serverStart());
|
||||||
|
else
|
||||||
|
setStatus(ServalDCommand.serverStart(execPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() throws ServalDFailureException {
|
||||||
|
try{
|
||||||
|
ServalDCommand.serverStop();
|
||||||
|
}finally{
|
||||||
|
clearStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void restart() throws ServalDFailureException {
|
||||||
|
try {
|
||||||
|
stop();
|
||||||
|
} catch (ServalDFailureException e) {
|
||||||
|
// ignore failures, at least we tried...
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRunning() throws ServalDFailureException {
|
||||||
|
ServalDCommand.Status s = ServalDCommand.serverStatus();
|
||||||
|
|
||||||
|
if (s.status.equals("running")) {
|
||||||
|
setStatus(s);
|
||||||
|
}else{
|
||||||
|
clearStatus();
|
||||||
|
}
|
||||||
|
return pid!=0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MdpServiceLookup getMdpService(ChannelSelector selector, AsyncResult<MdpServiceLookup.ServiceResult> results) throws ServalDInterfaceException, IOException {
|
||||||
|
if (!isRunning())
|
||||||
|
throw new ServalDInterfaceException("server is not running");
|
||||||
|
return new MdpServiceLookup(selector, this.loopbackMdpPort, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MdpDnaLookup getMdpDnaLookup(ChannelSelector selector, AsyncResult<ServalDCommand.LookupResult> results) throws ServalDInterfaceException, IOException {
|
||||||
|
if (!isRunning())
|
||||||
|
throw new ServalDInterfaceException("server is not running");
|
||||||
|
return new MdpDnaLookup(selector, this.loopbackMdpPort, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServalDClient getRestfulClient() throws ServalDInterfaceException {
|
||||||
|
if (!isRunning())
|
||||||
|
throw new ServalDInterfaceException("server is not running");
|
||||||
|
if (client==null) {
|
||||||
|
String restfulPassword = ServalDCommand.getConfigItem("rhizome.api.restful.users." + restfulUsername + ".password");
|
||||||
|
if (restfulPassword == null) {
|
||||||
|
String pwd = new BigInteger(130, new SecureRandom()).toString(32);
|
||||||
|
ServalDCommand.setConfigItem("rhizome.api.restful.users." + restfulUsername + ".password", pwd);
|
||||||
|
ServalDCommand.configSync();
|
||||||
|
restfulPassword = ServalDCommand.getConfigItem("rhizome.api.restful.users." + restfulUsername + ".password");
|
||||||
|
if (restfulPassword == null)
|
||||||
|
throw new ServalDInterfaceException("Failed to set restful password");
|
||||||
|
}
|
||||||
|
client = new ServalDClient(this.httpPort, restfulUsername, restfulPassword);
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
@ -4,10 +4,11 @@ import org.servalproject.servaldna.AsyncResult;
|
|||||||
import org.servalproject.servaldna.ChannelSelector;
|
import org.servalproject.servaldna.ChannelSelector;
|
||||||
import org.servalproject.servaldna.MdpDnaLookup;
|
import org.servalproject.servaldna.MdpDnaLookup;
|
||||||
import org.servalproject.servaldna.MdpServiceLookup;
|
import org.servalproject.servaldna.MdpServiceLookup;
|
||||||
import org.servalproject.servaldna.MdpSocket;
|
|
||||||
import org.servalproject.servaldna.ResultList;
|
import org.servalproject.servaldna.ResultList;
|
||||||
import org.servalproject.servaldna.ServalDCommand;
|
import org.servalproject.servaldna.ServalDCommand;
|
||||||
import org.servalproject.servaldna.ServalDFailureException;
|
import org.servalproject.servaldna.ServalDFailureException;
|
||||||
|
import org.servalproject.servaldna.ServalDInterfaceException;
|
||||||
|
import org.servalproject.servaldna.ServerControl;
|
||||||
import org.servalproject.servaldna.SubscriberId;
|
import org.servalproject.servaldna.SubscriberId;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -34,14 +35,8 @@ public class CommandLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void lookup(String did) throws IOException, InterruptedException, ServalDFailureException {
|
static void lookup(String did) throws IOException, InterruptedException, ServalDInterfaceException {
|
||||||
ServalDCommand.Status s = ServalDCommand.serverStatus();
|
MdpDnaLookup lookup = new ServerControl().getMdpDnaLookup(new ChannelSelector(), new AsyncResult<ServalDCommand.LookupResult>() {
|
||||||
System.out.println(s);
|
|
||||||
if (s.getResult()!=0)
|
|
||||||
throw new ServalDFailureException("Serval daemon isn't running");
|
|
||||||
MdpSocket.loopbackMdpPort = s.mdpInetPort;
|
|
||||||
ChannelSelector selector = new ChannelSelector();
|
|
||||||
MdpDnaLookup lookup = new MdpDnaLookup(selector, new AsyncResult<ServalDCommand.LookupResult>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void result(ServalDCommand.LookupResult nextResult) {
|
public void result(ServalDCommand.LookupResult nextResult) {
|
||||||
System.out.println(nextResult.toString());
|
System.out.println(nextResult.toString());
|
||||||
@ -52,14 +47,8 @@ public class CommandLine {
|
|||||||
lookup.close();
|
lookup.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void service(String pattern) throws IOException, InterruptedException, ServalDFailureException {
|
static void service(String pattern) throws IOException, InterruptedException, ServalDInterfaceException {
|
||||||
ServalDCommand.Status s = ServalDCommand.serverStatus();
|
MdpServiceLookup lookup = new ServerControl().getMdpService(new ChannelSelector(), new AsyncResult<MdpServiceLookup.ServiceResult>() {
|
||||||
System.out.println(s);
|
|
||||||
if (s.getResult()!=0)
|
|
||||||
throw new ServalDFailureException("Serval daemon isn't running");
|
|
||||||
MdpSocket.loopbackMdpPort = s.mdpInetPort;
|
|
||||||
ChannelSelector selector = new ChannelSelector();
|
|
||||||
MdpServiceLookup lookup = new MdpServiceLookup(selector, new AsyncResult<MdpServiceLookup.ServiceResult>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void result(MdpServiceLookup.ServiceResult nextResult) {
|
public void result(MdpServiceLookup.ServiceResult nextResult) {
|
||||||
System.out.println(nextResult.toString());
|
System.out.println(nextResult.toString());
|
||||||
|
@ -20,23 +20,23 @@
|
|||||||
|
|
||||||
package org.servalproject.test;
|
package org.servalproject.test;
|
||||||
|
|
||||||
import java.lang.System;
|
|
||||||
import java.io.IOException;
|
|
||||||
import org.servalproject.servaldna.SubscriberId;
|
|
||||||
|
|
||||||
import org.servalproject.servaldna.ServalDClient;
|
import org.servalproject.servaldna.ServalDClient;
|
||||||
import org.servalproject.servaldna.ServalDInterfaceException;
|
import org.servalproject.servaldna.ServalDInterfaceException;
|
||||||
import org.servalproject.servaldna.meshms.MeshMSConversationList;
|
import org.servalproject.servaldna.ServerControl;
|
||||||
|
import org.servalproject.servaldna.SubscriberId;
|
||||||
import org.servalproject.servaldna.meshms.MeshMSConversation;
|
import org.servalproject.servaldna.meshms.MeshMSConversation;
|
||||||
import org.servalproject.servaldna.meshms.MeshMSMessageList;
|
import org.servalproject.servaldna.meshms.MeshMSConversationList;
|
||||||
import org.servalproject.servaldna.meshms.MeshMSMessage;
|
|
||||||
import org.servalproject.servaldna.meshms.MeshMSException;
|
import org.servalproject.servaldna.meshms.MeshMSException;
|
||||||
|
import org.servalproject.servaldna.meshms.MeshMSMessage;
|
||||||
|
import org.servalproject.servaldna.meshms.MeshMSMessageList;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
public class Meshms {
|
public class Meshms {
|
||||||
|
|
||||||
static void meshms_list_conversations(SubscriberId sid) throws ServalDInterfaceException, IOException, InterruptedException
|
static void meshms_list_conversations(SubscriberId sid) throws ServalDInterfaceException, IOException, InterruptedException
|
||||||
{
|
{
|
||||||
ServalDClient client = ServalDClient.newServalDClient();
|
ServalDClient client = new ServerControl().getRestfulClient();
|
||||||
MeshMSConversationList list = null;
|
MeshMSConversationList list = null;
|
||||||
try {
|
try {
|
||||||
list = client.meshmsListConversations(sid);
|
list = client.meshmsListConversations(sid);
|
||||||
@ -64,7 +64,7 @@ public class Meshms {
|
|||||||
|
|
||||||
static void meshms_list_messages(SubscriberId sid1, SubscriberId sid2) throws ServalDInterfaceException, IOException, InterruptedException
|
static void meshms_list_messages(SubscriberId sid1, SubscriberId sid2) throws ServalDInterfaceException, IOException, InterruptedException
|
||||||
{
|
{
|
||||||
ServalDClient client = ServalDClient.newServalDClient();
|
ServalDClient client = new ServerControl().getRestfulClient();
|
||||||
MeshMSMessageList list = null;
|
MeshMSMessageList list = null;
|
||||||
try {
|
try {
|
||||||
list = client.meshmsListMessages(sid1, sid2);
|
list = client.meshmsListMessages(sid1, sid2);
|
||||||
|
@ -32,11 +32,6 @@ setup() {
|
|||||||
set log.console.level debug \
|
set log.console.level debug \
|
||||||
set debug.httpd on
|
set debug.httpd on
|
||||||
create_identities 4
|
create_identities 4
|
||||||
configure_servald_server() {
|
|
||||||
add_servald_interface
|
|
||||||
executeOk_servald config \
|
|
||||||
set rhizome.api.restful.users.joe.password bloggs
|
|
||||||
}
|
|
||||||
start_servald_server
|
start_servald_server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user