am 62de7421: Merge change I4fe2a47a into eclair
Merge commit '62de742125d8f5fd1d236f720b2de3bf5cf76174' into eclair-plus-aosp * commit '62de742125d8f5fd1d236f720b2de3bf5cf76174': Fix stopping all vpn daemons before connect and more.
This commit is contained in:
@@ -31,24 +31,17 @@ class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> {
|
||||
protected void connect(String serverIp, String username, String password)
|
||||
throws IOException {
|
||||
L2tpIpsecPskProfile p = getProfile();
|
||||
VpnDaemons daemons = getDaemons();
|
||||
|
||||
// IPSEC
|
||||
DaemonProxy ipsec = startDaemon(IPSEC);
|
||||
ipsec.sendCommand(serverIp, L2tpService.L2TP_PORT, p.getPresharedKey());
|
||||
ipsec.closeControlSocket();
|
||||
daemons.startIpsecForL2tp(serverIp, p.getPresharedKey())
|
||||
.closeControlSocket();
|
||||
|
||||
sleep(2000); // 2 seconds
|
||||
|
||||
// L2TP
|
||||
MtpdHelper.sendCommand(this, L2tpService.L2TP_DAEMON, serverIp,
|
||||
L2tpService.L2TP_PORT,
|
||||
daemons.startL2tp(serverIp,
|
||||
(p.isSecretEnabled() ? p.getSecretString() : null),
|
||||
username, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopPreviouslyRunDaemons() {
|
||||
stopDaemon(IPSEC);
|
||||
stopDaemon(MtpdHelper.MTPD);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,10 @@ class L2tpIpsecService extends VpnService<L2tpIpsecProfile> {
|
||||
protected void connect(String serverIp, String username, String password)
|
||||
throws IOException {
|
||||
L2tpIpsecProfile p = getProfile();
|
||||
VpnDaemons daemons = getDaemons();
|
||||
|
||||
// IPSEC
|
||||
DaemonProxy ipsec = startDaemon(IPSEC);
|
||||
ipsec.sendCommand(serverIp, L2tpService.L2TP_PORT,
|
||||
DaemonProxy ipsec = daemons.startIpsecForL2tp(serverIp,
|
||||
Credentials.USER_PRIVATE_KEY + p.getUserCertificate(),
|
||||
Credentials.USER_CERTIFICATE + p.getUserCertificate(),
|
||||
Credentials.CA_CERTIFICATE + p.getCaCertificate());
|
||||
@@ -42,15 +43,8 @@ class L2tpIpsecService extends VpnService<L2tpIpsecProfile> {
|
||||
sleep(2000); // 2 seconds
|
||||
|
||||
// L2TP
|
||||
MtpdHelper.sendCommand(this, L2tpService.L2TP_DAEMON, serverIp,
|
||||
L2tpService.L2TP_PORT,
|
||||
daemons.startL2tp(serverIp,
|
||||
(p.isSecretEnabled() ? p.getSecretString() : null),
|
||||
username, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopPreviouslyRunDaemons() {
|
||||
stopDaemon(IPSEC);
|
||||
stopDaemon(MtpdHelper.MTPD);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,20 +24,12 @@ import java.io.IOException;
|
||||
* The service that manages the L2TP VPN connection.
|
||||
*/
|
||||
class L2tpService extends VpnService<L2tpProfile> {
|
||||
static final String L2TP_DAEMON = "l2tp";
|
||||
static final String L2TP_PORT = "1701";
|
||||
|
||||
@Override
|
||||
protected void connect(String serverIp, String username, String password)
|
||||
throws IOException {
|
||||
L2tpProfile p = getProfile();
|
||||
MtpdHelper.sendCommand(this, L2TP_DAEMON, serverIp, L2TP_PORT,
|
||||
getDaemons().startL2tp(serverIp,
|
||||
(p.isSecretEnabled() ? p.getSecretString() : null),
|
||||
username, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopPreviouslyRunDaemons() {
|
||||
stopDaemon(MtpdHelper.MTPD);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2009, The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.vpn;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A helper class for sending commands to the MTP daemon (mtpd).
|
||||
*/
|
||||
class MtpdHelper {
|
||||
static final String MTPD = "mtpd";
|
||||
private static final String VPN_LINKNAME = "vpn";
|
||||
private static final String PPP_ARGS_SEPARATOR = "";
|
||||
|
||||
static void sendCommand(VpnService<?> vpnService, String protocol,
|
||||
String serverIp, String port, String secret, String username,
|
||||
String password) throws IOException {
|
||||
sendCommand(vpnService, protocol, serverIp, port, secret, username,
|
||||
password, false);
|
||||
}
|
||||
|
||||
static void sendCommand(VpnService<?> vpnService, String protocol,
|
||||
String serverIp, String port, String secret, String username,
|
||||
String password, boolean encryption) throws IOException {
|
||||
ArrayList<String> args = new ArrayList<String>();
|
||||
args.addAll(Arrays.asList(protocol, serverIp, port));
|
||||
if (secret != null) args.add(secret);
|
||||
args.add(PPP_ARGS_SEPARATOR);
|
||||
addPppArguments(args, serverIp, username, password, encryption);
|
||||
|
||||
DaemonProxy mtpd = vpnService.startDaemon(MTPD);
|
||||
mtpd.sendCommand(args.toArray(new String[args.size()]));
|
||||
}
|
||||
|
||||
private static void addPppArguments(ArrayList<String> args, String serverIp,
|
||||
String username, String password, boolean encryption)
|
||||
throws IOException {
|
||||
args.addAll(Arrays.asList(
|
||||
"linkname", VPN_LINKNAME,
|
||||
"name", username,
|
||||
"password", password,
|
||||
"refuse-eap", "nodefaultroute", "usepeerdns",
|
||||
"idle", "1800",
|
||||
"mtu", "1400",
|
||||
"mru", "1400"));
|
||||
if (encryption) {
|
||||
args.add("+mppe");
|
||||
}
|
||||
}
|
||||
|
||||
private MtpdHelper() {
|
||||
}
|
||||
}
|
||||
@@ -24,19 +24,11 @@ import java.io.IOException;
|
||||
* The service that manages the PPTP VPN connection.
|
||||
*/
|
||||
class PptpService extends VpnService<PptpProfile> {
|
||||
static final String PPTP_DAEMON = "pptp";
|
||||
static final String PPTP_PORT = "1723";
|
||||
|
||||
@Override
|
||||
protected void connect(String serverIp, String username, String password)
|
||||
throws IOException {
|
||||
PptpProfile p = getProfile();
|
||||
MtpdHelper.sendCommand(this, PPTP_DAEMON, serverIp, PPTP_PORT, null,
|
||||
username, password, p.isEncryptionEnabled());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopPreviouslyRunDaemons() {
|
||||
stopDaemon(MtpdHelper.MTPD);
|
||||
getDaemons().startPptp(serverIp, username, password,
|
||||
p.isEncryptionEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
147
packages/VpnServices/src/com/android/server/vpn/VpnDaemons.java
Normal file
147
packages/VpnServices/src/com/android/server/vpn/VpnDaemons.java
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) 2009, The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.vpn;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A helper class for managing native VPN daemons.
|
||||
*/
|
||||
class VpnDaemons implements Serializable {
|
||||
static final long serialVersionUID = 1L;
|
||||
private final String TAG = VpnDaemons.class.getSimpleName();
|
||||
|
||||
private static final String MTPD = "mtpd";
|
||||
private static final String IPSEC = "racoon";
|
||||
|
||||
private static final String L2TP = "l2tp";
|
||||
private static final String L2TP_PORT = "1701";
|
||||
|
||||
private static final String PPTP = "pptp";
|
||||
private static final String PPTP_PORT = "1723";
|
||||
|
||||
private static final String VPN_LINKNAME = "vpn";
|
||||
private static final String PPP_ARGS_SEPARATOR = "";
|
||||
|
||||
private List<DaemonProxy> mDaemonList = new ArrayList<DaemonProxy>();
|
||||
|
||||
public DaemonProxy startL2tp(String serverIp, String secret,
|
||||
String username, String password) throws IOException {
|
||||
return startMtpd(L2TP, serverIp, L2TP_PORT, secret, username, password,
|
||||
false);
|
||||
}
|
||||
|
||||
public DaemonProxy startPptp(String serverIp, String username,
|
||||
String password, boolean encryption) throws IOException {
|
||||
return startMtpd(PPTP, serverIp, PPTP_PORT, null, username, password,
|
||||
encryption);
|
||||
}
|
||||
|
||||
public DaemonProxy startIpsecForL2tp(String serverIp, String pskKey)
|
||||
throws IOException {
|
||||
DaemonProxy ipsec = startDaemon(IPSEC);
|
||||
ipsec.sendCommand(serverIp, L2TP_PORT, pskKey);
|
||||
return ipsec;
|
||||
}
|
||||
|
||||
public DaemonProxy startIpsecForL2tp(String serverIp, String userKeyKey,
|
||||
String userCertKey, String caCertKey) throws IOException {
|
||||
DaemonProxy ipsec = startDaemon(IPSEC);
|
||||
ipsec.sendCommand(serverIp, L2TP_PORT, userKeyKey, userCertKey,
|
||||
caCertKey);
|
||||
return ipsec;
|
||||
}
|
||||
|
||||
public synchronized void stopAll() {
|
||||
new DaemonProxy(MTPD).stop();
|
||||
new DaemonProxy(IPSEC).stop();
|
||||
}
|
||||
|
||||
public synchronized void closeSockets() {
|
||||
for (DaemonProxy s : mDaemonList) s.closeControlSocket();
|
||||
}
|
||||
|
||||
public synchronized boolean anyDaemonStopped() {
|
||||
for (DaemonProxy s : mDaemonList) {
|
||||
if (s.isStopped()) {
|
||||
Log.w(TAG, " VPN daemon gone: " + s.getName());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public synchronized int getSocketError() {
|
||||
for (DaemonProxy s : mDaemonList) {
|
||||
int errCode = getResultFromSocket(s);
|
||||
if (errCode != 0) return errCode;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private synchronized DaemonProxy startDaemon(String daemonName)
|
||||
throws IOException {
|
||||
DaemonProxy daemon = new DaemonProxy(daemonName);
|
||||
mDaemonList.add(daemon);
|
||||
daemon.start();
|
||||
return daemon;
|
||||
}
|
||||
|
||||
private int getResultFromSocket(DaemonProxy s) {
|
||||
try {
|
||||
return s.getResultFromSocket();
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private DaemonProxy startMtpd(String protocol,
|
||||
String serverIp, String port, String secret, String username,
|
||||
String password, boolean encryption) throws IOException {
|
||||
ArrayList<String> args = new ArrayList<String>();
|
||||
args.addAll(Arrays.asList(protocol, serverIp, port));
|
||||
if (secret != null) args.add(secret);
|
||||
args.add(PPP_ARGS_SEPARATOR);
|
||||
addPppArguments(args, serverIp, username, password, encryption);
|
||||
|
||||
DaemonProxy mtpd = startDaemon(MTPD);
|
||||
mtpd.sendCommand(args.toArray(new String[args.size()]));
|
||||
return mtpd;
|
||||
}
|
||||
|
||||
private static void addPppArguments(ArrayList<String> args, String serverIp,
|
||||
String username, String password, boolean encryption)
|
||||
throws IOException {
|
||||
args.addAll(Arrays.asList(
|
||||
"linkname", VPN_LINKNAME,
|
||||
"name", username,
|
||||
"password", password,
|
||||
"refuse-eap", "nodefaultroute", "usepeerdns",
|
||||
"idle", "1800",
|
||||
"mtu", "1400",
|
||||
"mru", "1400"));
|
||||
if (encryption) {
|
||||
args.add("+mppe");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,18 +30,15 @@ import android.util.Log;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The service base class for managing a type of VPN connection.
|
||||
*/
|
||||
abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
protected static final long serialVersionUID = 1L;
|
||||
static final long serialVersionUID = 1L;
|
||||
private static final boolean DBG = true;
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
|
||||
@@ -75,8 +72,8 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
|
||||
private long mStartTime; // VPN connection start time
|
||||
|
||||
// for helping managing multiple daemons
|
||||
private DaemonHelper mDaemonHelper = new DaemonHelper();
|
||||
// for helping managing daemons
|
||||
private VpnDaemons mDaemons = new VpnDaemons();
|
||||
|
||||
// for helping showing, updating notification
|
||||
private transient NotificationHelper mNotification;
|
||||
@@ -87,21 +84,11 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
protected abstract void connect(String serverIp, String username,
|
||||
String password) throws IOException;
|
||||
|
||||
protected abstract void stopPreviouslyRunDaemons();
|
||||
|
||||
/**
|
||||
* Starts a VPN daemon.
|
||||
* Returns the daemons management class for this service object.
|
||||
*/
|
||||
protected DaemonProxy startDaemon(String daemonName)
|
||||
throws IOException {
|
||||
return mDaemonHelper.startDaemon(daemonName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a VPN daemon.
|
||||
*/
|
||||
protected void stopDaemon(String daemonName) {
|
||||
new DaemonProxy(daemonName).stop();
|
||||
protected VpnDaemons getDaemons() {
|
||||
return mDaemons;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +128,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
try {
|
||||
setState(VpnState.CONNECTING);
|
||||
|
||||
stopPreviouslyRunDaemons();
|
||||
mDaemons.stopAll();
|
||||
String serverIp = getIp(getProfile().getServerName());
|
||||
saveLocalIpAndInterface(serverIp);
|
||||
onBeforeConnect();
|
||||
@@ -160,7 +147,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
setState(VpnState.DISCONNECTING);
|
||||
mNotification.showDisconnect();
|
||||
|
||||
mDaemonHelper.stopAll();
|
||||
mDaemons.stopAll();
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "onDisconnect()", e);
|
||||
} finally {
|
||||
@@ -206,7 +193,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
onConnected();
|
||||
return;
|
||||
} else {
|
||||
int err = mDaemonHelper.getSocketError();
|
||||
int err = mDaemons.getSocketError();
|
||||
if (err != 0) {
|
||||
onError(err);
|
||||
return;
|
||||
@@ -223,7 +210,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
private synchronized void onConnected() throws IOException {
|
||||
if (DBG) Log.d(TAG, "onConnected()");
|
||||
|
||||
mDaemonHelper.closeSockets();
|
||||
mDaemons.closeSockets();
|
||||
saveOriginalDns();
|
||||
saveAndSetDomainSuffices();
|
||||
|
||||
@@ -341,15 +328,20 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
public void run() {
|
||||
Log.i(TAG, "VPN connectivity monitor running");
|
||||
try {
|
||||
for (;;) {
|
||||
for (int i = 10; ; i--) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
boolean heavyCheck = i == 0;
|
||||
synchronized (VpnService.this) {
|
||||
if ((mState != VpnState.CONNECTED)
|
||||
|| !checkConnectivity()) {
|
||||
break;
|
||||
if (mState != VpnState.CONNECTED) break;
|
||||
mNotification.update(now);
|
||||
|
||||
if (heavyCheck) {
|
||||
i = 10;
|
||||
if (checkConnectivity()) checkDns();
|
||||
}
|
||||
mNotification.update();
|
||||
checkDns();
|
||||
VpnService.this.wait(1000); // 1 second
|
||||
long t = 1000L - System.currentTimeMillis() + now;
|
||||
if (t > 100L) VpnService.this.wait(t);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
@@ -378,7 +370,7 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
|
||||
// returns false if vpn connectivity is broken
|
||||
private boolean checkConnectivity() {
|
||||
if (mDaemonHelper.anyDaemonStopped() || isLocalIpChanged()) {
|
||||
if (mDaemons.anyDaemonStopped() || isLocalIpChanged()) {
|
||||
onError(new IOException("Connectivity lost"));
|
||||
return false;
|
||||
} else {
|
||||
@@ -421,60 +413,17 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
}
|
||||
|
||||
private class DaemonHelper implements Serializable {
|
||||
private List<DaemonProxy> mDaemonList =
|
||||
new ArrayList<DaemonProxy>();
|
||||
|
||||
synchronized DaemonProxy startDaemon(String daemonName)
|
||||
throws IOException {
|
||||
DaemonProxy daemon = new DaemonProxy(daemonName);
|
||||
mDaemonList.add(daemon);
|
||||
daemon.start();
|
||||
return daemon;
|
||||
}
|
||||
|
||||
synchronized void stopAll() {
|
||||
for (DaemonProxy s : mDaemonList) s.stop();
|
||||
}
|
||||
|
||||
synchronized void closeSockets() {
|
||||
for (DaemonProxy s : mDaemonList) s.closeControlSocket();
|
||||
}
|
||||
|
||||
synchronized boolean anyDaemonStopped() {
|
||||
for (DaemonProxy s : mDaemonList) {
|
||||
if (s.isStopped()) {
|
||||
Log.w(TAG, " VPN daemon gone: " + s.getName());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int getResultFromSocket(DaemonProxy s) {
|
||||
try {
|
||||
return s.getResultFromSocket();
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
synchronized int getSocketError() {
|
||||
for (DaemonProxy s : mDaemonList) {
|
||||
int errCode = getResultFromSocket(s);
|
||||
if (errCode != 0) return errCode;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper class for showing, updating notification.
|
||||
private class NotificationHelper {
|
||||
void update() {
|
||||
void update(long now) {
|
||||
String title = getNotificationTitle(true);
|
||||
Notification n = new Notification(R.drawable.vpn_connected, title,
|
||||
mStartTime);
|
||||
n.setLatestEventInfo(mContext, title,
|
||||
getNotificationMessage(true), prepareNotificationIntent());
|
||||
getConnectedNotificationMessage(now),
|
||||
prepareNotificationIntent());
|
||||
n.flags |= Notification.FLAG_NO_CLEAR;
|
||||
n.flags |= Notification.FLAG_ONGOING_EVENT;
|
||||
enableNotification(n);
|
||||
@@ -485,7 +434,8 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
Notification n = new Notification(R.drawable.vpn_disconnected,
|
||||
title, System.currentTimeMillis());
|
||||
n.setLatestEventInfo(mContext, title,
|
||||
getNotificationMessage(false), prepareNotificationIntent());
|
||||
getDisconnectedNotificationMessage(),
|
||||
prepareNotificationIntent());
|
||||
n.flags |= Notification.FLAG_AUTO_CANCEL;
|
||||
disableNotification();
|
||||
enableNotification(n);
|
||||
@@ -515,8 +465,8 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
return String.format(formatString, mProfile.getName());
|
||||
}
|
||||
|
||||
private String getFormattedTime(long duration) {
|
||||
long hours = duration / 3600;
|
||||
private String getFormattedTime(int duration) {
|
||||
int hours = duration / 3600;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (hours > 0) sb.append(hours).append(':');
|
||||
sb.append(String.format("%02d:%02d", (duration % 3600 / 60),
|
||||
@@ -524,14 +474,13 @@ abstract class VpnService<E extends VpnProfile> implements Serializable {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String getNotificationMessage(boolean connected) {
|
||||
if (connected) {
|
||||
long time = (System.currentTimeMillis() - mStartTime) / 1000;
|
||||
return getFormattedTime(time);
|
||||
} else {
|
||||
return mContext.getString(
|
||||
R.string.vpn_notification_hint_disconnected);
|
||||
}
|
||||
private String getConnectedNotificationMessage(long now) {
|
||||
return getFormattedTime((int) (now - mStartTime) / 1000);
|
||||
}
|
||||
|
||||
private String getDisconnectedNotificationMessage() {
|
||||
return mContext.getString(
|
||||
R.string.vpn_notification_hint_disconnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user