* I2PSOCKSIRCTunnel:
- New, for filtering IRC client traffic when using SOCKS * I2PTunnelIRCClient: - Make filter classes static and public for use by SOCKS - Eliminate redundant case conversion - Pass ISON message through (jIRCii uses it for pings) - Switch back to StringBuffer since it's used by 2 threads - Set daemon on filter threads * SOCKS5Server: - Fix handling of multiple authentication methods
This commit is contained in:
@ -61,6 +61,7 @@ import net.i2p.data.Base64;
|
|||||||
import net.i2p.data.DataFormatException;
|
import net.i2p.data.DataFormatException;
|
||||||
import net.i2p.data.DataHelper;
|
import net.i2p.data.DataHelper;
|
||||||
import net.i2p.data.Destination;
|
import net.i2p.data.Destination;
|
||||||
|
import net.i2p.i2ptunnel.socks.I2PSOCKSIRCTunnel;
|
||||||
import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel;
|
import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel;
|
||||||
import net.i2p.i2ptunnel.streamr.StreamrConsumer;
|
import net.i2p.i2ptunnel.streamr.StreamrConsumer;
|
||||||
import net.i2p.i2ptunnel.streamr.StreamrProducer;
|
import net.i2p.i2ptunnel.streamr.StreamrProducer;
|
||||||
@ -895,6 +896,39 @@ public class I2PTunnel implements Logging, EventDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run an SOCKS IRC tunnel on the given port number
|
||||||
|
* @since 0.7.12
|
||||||
|
*/
|
||||||
|
public void runSOCKSIRCTunnel(String args[], Logging l) {
|
||||||
|
if (args.length >= 1 && args.length <= 2) {
|
||||||
|
int _port = -1;
|
||||||
|
try {
|
||||||
|
_port = Integer.parseInt(args[0]);
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
l.log("invalid port");
|
||||||
|
_log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
|
||||||
|
notifyEvent("sockstunnelTaskId", Integer.valueOf(-1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isShared = false;
|
||||||
|
if (args.length > 1)
|
||||||
|
isShared = "true".equalsIgnoreCase(args[1].trim());
|
||||||
|
|
||||||
|
ownDest = !isShared;
|
||||||
|
I2PTunnelTask task;
|
||||||
|
task = new I2PSOCKSIRCTunnel(_port, l, ownDest, (EventDispatcher) this, this);
|
||||||
|
addtask(task);
|
||||||
|
notifyEvent("sockstunnelTaskId", Integer.valueOf(task.getId()));
|
||||||
|
} else {
|
||||||
|
l.log("sockstunnel <port>");
|
||||||
|
l.log(" creates a tunnel that distributes SOCKS requests.");
|
||||||
|
notifyEvent("sockstunnelTaskId", Integer.valueOf(-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streamr client
|
* Streamr client
|
||||||
*
|
*
|
||||||
|
@ -82,10 +82,10 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
try {
|
try {
|
||||||
i2ps = createI2PSocket(clientDest);
|
i2ps = createI2PSocket(clientDest);
|
||||||
i2ps.setReadTimeout(readTimeout);
|
i2ps.setReadTimeout(readTimeout);
|
||||||
StringBuilder expectedPong = new StringBuilder();
|
StringBuffer expectedPong = new StringBuffer();
|
||||||
Thread in = new I2PAppThread(new IrcInboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " in");
|
Thread in = new I2PAppThread(new IrcInboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " in", true);
|
||||||
in.start();
|
in.start();
|
||||||
Thread out = new I2PAppThread(new IrcOutboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " out");
|
Thread out = new I2PAppThread(new IrcOutboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " out", true);
|
||||||
out.start();
|
out.start();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
if (_log.shouldLog(Log.ERROR))
|
if (_log.shouldLog(Log.ERROR))
|
||||||
@ -117,13 +117,13 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
/*************************************************************************
|
/*************************************************************************
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
private class IrcInboundFilter implements Runnable {
|
public static class IrcInboundFilter implements Runnable {
|
||||||
|
|
||||||
private Socket local;
|
private Socket local;
|
||||||
private I2PSocket remote;
|
private I2PSocket remote;
|
||||||
private StringBuilder expectedPong;
|
private StringBuffer expectedPong;
|
||||||
|
|
||||||
IrcInboundFilter(Socket _local, I2PSocket _remote, StringBuilder pong) {
|
public IrcInboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong) {
|
||||||
local=_local;
|
local=_local;
|
||||||
remote=_remote;
|
remote=_remote;
|
||||||
expectedPong=pong;
|
expectedPong=pong;
|
||||||
@ -191,13 +191,13 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
/*************************************************************************
|
/*************************************************************************
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
private class IrcOutboundFilter implements Runnable {
|
public static class IrcOutboundFilter implements Runnable {
|
||||||
|
|
||||||
private Socket local;
|
private Socket local;
|
||||||
private I2PSocket remote;
|
private I2PSocket remote;
|
||||||
private StringBuilder expectedPong;
|
private StringBuffer expectedPong;
|
||||||
|
|
||||||
IrcOutboundFilter(Socket _local, I2PSocket _remote, StringBuilder pong) {
|
public IrcOutboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong) {
|
||||||
local=_local;
|
local=_local;
|
||||||
remote=_remote;
|
remote=_remote;
|
||||||
expectedPong=pong;
|
expectedPong=pong;
|
||||||
@ -266,7 +266,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public String inboundFilter(String s, StringBuilder expectedPong) {
|
public static String inboundFilter(String s, StringBuffer expectedPong) {
|
||||||
|
|
||||||
String field[]=s.split(" ",4);
|
String field[]=s.split(" ",4);
|
||||||
String command;
|
String command;
|
||||||
@ -353,7 +353,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String outboundFilter(String s, StringBuilder expectedPong) {
|
public static String outboundFilter(String s, StringBuffer expectedPong) {
|
||||||
|
|
||||||
String field[]=s.split(" ",3);
|
String field[]=s.split(" ",3);
|
||||||
String command;
|
String command;
|
||||||
@ -378,7 +378,8 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
"KICK",
|
"KICK",
|
||||||
"HELPME",
|
"HELPME",
|
||||||
"RULES",
|
"RULES",
|
||||||
"TOPIC"
|
"TOPIC",
|
||||||
|
"ISON" // jIRCii uses this for a ping (response is 303)
|
||||||
};
|
};
|
||||||
|
|
||||||
if(field[0].length()==0)
|
if(field[0].length()==0)
|
||||||
@ -390,7 +391,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
|
|
||||||
command = field[0].toUpperCase();
|
command = field[0].toUpperCase();
|
||||||
|
|
||||||
if ("PING".equalsIgnoreCase(command)) {
|
if ("PING".equals(command)) {
|
||||||
// Most clients just send a PING and are happy with any old PONG. Others,
|
// Most clients just send a PING and are happy with any old PONG. Others,
|
||||||
// like BitchX, actually expect certain behavior. It sends two different pings:
|
// like BitchX, actually expect certain behavior. It sends two different pings:
|
||||||
// "PING :irc.freshcoffee.i2p" and "PING 1234567890 127.0.0.1" (where the IP is the proxy)
|
// "PING :irc.freshcoffee.i2p" and "PING 1234567890 127.0.0.1" (where the IP is the proxy)
|
||||||
@ -426,19 +427,19 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
|
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
if ("PONG".equalsIgnoreCase(command))
|
if ("PONG".equals(command))
|
||||||
return "PONG 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works
|
return "PONG 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works
|
||||||
|
|
||||||
// Allow all allowedCommands
|
// Allow all allowedCommands
|
||||||
for(int i=0;i<allowedCommands.length;i++)
|
for(int i=0;i<allowedCommands.length;i++)
|
||||||
{
|
{
|
||||||
if(allowedCommands[i].equalsIgnoreCase(command))
|
if(allowedCommands[i].equals(command))
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mIRC sends "NOTICE user :DCC Send file (IP)"
|
// mIRC sends "NOTICE user :DCC Send file (IP)"
|
||||||
// in addition to the CTCP version
|
// in addition to the CTCP version
|
||||||
if("NOTICE".equalsIgnoreCase(command))
|
if("NOTICE".equals(command))
|
||||||
{
|
{
|
||||||
String msg = field[2];
|
String msg = field[2];
|
||||||
if(msg.startsWith(":DCC "))
|
if(msg.startsWith(":DCC "))
|
||||||
@ -447,7 +448,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allow PRIVMSG, but block CTCP (except ACTION).
|
// Allow PRIVMSG, but block CTCP (except ACTION).
|
||||||
if("PRIVMSG".equalsIgnoreCase(command) || "NOTICE".equalsIgnoreCase(command))
|
if("PRIVMSG".equals(command) || "NOTICE".equals(command))
|
||||||
{
|
{
|
||||||
String msg;
|
String msg;
|
||||||
msg = field[2];
|
msg = field[2];
|
||||||
@ -465,14 +466,16 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
if("USER".equalsIgnoreCase(command)) {
|
if("USER".equals(command)) {
|
||||||
int idx = field[2].lastIndexOf(":");
|
int idx = field[2].lastIndexOf(":");
|
||||||
if(idx<0)
|
if(idx<0)
|
||||||
return "USER user hostname localhost :realname";
|
return "USER user hostname localhost :realname";
|
||||||
String realname = field[2].substring(idx+1);
|
String realname = field[2].substring(idx+1);
|
||||||
String ret = "USER "+field[1]+" hostname localhost :"+realname;
|
String ret = "USER "+field[1]+" hostname localhost :"+realname;
|
||||||
return ret;
|
return ret;
|
||||||
} else if ("QUIT".equalsIgnoreCase(command)) {
|
}
|
||||||
|
|
||||||
|
if ("QUIT".equals(command)) {
|
||||||
return "QUIT :leaving";
|
return "QUIT :leaving";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,6 +144,8 @@ public class TunnelController implements Logging {
|
|||||||
startIrcClient();
|
startIrcClient();
|
||||||
} else if("sockstunnel".equals(type)) {
|
} else if("sockstunnel".equals(type)) {
|
||||||
startSocksClient();
|
startSocksClient();
|
||||||
|
} else if("socksirctunnel".equals(type)) {
|
||||||
|
startSocksIRCClient();
|
||||||
} else if("connectclient".equals(type)) {
|
} else if("connectclient".equals(type)) {
|
||||||
startConnectClient();
|
startConnectClient();
|
||||||
} else if ("client".equals(type)) {
|
} else if ("client".equals(type)) {
|
||||||
@ -211,6 +213,14 @@ public class TunnelController implements Logging {
|
|||||||
_tunnel.runSOCKSTunnel(new String[] { listenPort, sharedClient }, this);
|
_tunnel.runSOCKSTunnel(new String[] { listenPort, sharedClient }, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @since 0.7.12 */
|
||||||
|
private void startSocksIRCClient() {
|
||||||
|
setListenOn();
|
||||||
|
String listenPort = getListenPort();
|
||||||
|
String sharedClient = getSharedClient();
|
||||||
|
_tunnel.runSOCKSIRCTunnel(new String[] { listenPort, sharedClient }, this);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Streamr client is a UDP server, use the listenPort field for targetPort
|
* Streamr client is a UDP server, use the listenPort field for targetPort
|
||||||
* and the listenOnInterface field for the targetHost
|
* and the listenOnInterface field for the targetHost
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
/* I2PSOCKSTunnel is released under the terms of the GNU GPL,
|
||||||
|
* with an additional exception. For further details, see the
|
||||||
|
* licensing terms in I2PTunnel.java.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2004 by human
|
||||||
|
*/
|
||||||
|
package net.i2p.i2ptunnel.socks;
|
||||||
|
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
import net.i2p.I2PAppContext;
|
||||||
|
import net.i2p.client.streaming.I2PSocket;
|
||||||
|
import net.i2p.i2ptunnel.I2PTunnel;
|
||||||
|
import net.i2p.i2ptunnel.I2PTunnelIRCClient;
|
||||||
|
import net.i2p.i2ptunnel.Logging;
|
||||||
|
import net.i2p.util.EventDispatcher;
|
||||||
|
import net.i2p.util.I2PAppThread;
|
||||||
|
import net.i2p.util.Log;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Pipe SOCKS IRC connections through I2PTunnelIRCClient filtering,
|
||||||
|
* to get the best of both worlds:
|
||||||
|
*
|
||||||
|
* - SOCKS lets you specify the host so you don't have to set up
|
||||||
|
* a tunnel for each IRC server in advance
|
||||||
|
* - IRC filtering for security
|
||||||
|
*
|
||||||
|
* @since 0.7.12
|
||||||
|
* @author zzz
|
||||||
|
*/
|
||||||
|
public class I2PSOCKSIRCTunnel extends I2PSOCKSTunnel {
|
||||||
|
|
||||||
|
private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(I2PSOCKSIRCTunnel.class);
|
||||||
|
private static final int __clientId = 0;
|
||||||
|
|
||||||
|
public I2PSOCKSIRCTunnel(int localPort, Logging l, boolean ownDest, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||||
|
super(localPort, l, ownDest, notifyThis, tunnel);
|
||||||
|
setName(getLocalPort() + " -> SOCKSIRCTunnel");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as in I2PSOCKSTunnel, but run the filters from I2PTunnelIRCClient
|
||||||
|
* instead of I2PTunnelRunner
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void clientConnectionRun(Socket s) {
|
||||||
|
try {
|
||||||
|
_log.error("SOCKS IRC Tunnel Start");
|
||||||
|
SOCKSServer serv = SOCKSServerFactory.createSOCKSServer(s);
|
||||||
|
Socket clientSock = serv.getClientSocket();
|
||||||
|
I2PSocket destSock = serv.getDestinationI2PSocket(this);
|
||||||
|
StringBuffer expectedPong = new StringBuffer();
|
||||||
|
Thread in = new I2PAppThread(new I2PTunnelIRCClient.IrcInboundFilter(clientSock, destSock, expectedPong), "SOCKS IRC Client " + (++__clientId) + " in", true);
|
||||||
|
in.start();
|
||||||
|
Thread out = new I2PAppThread(new I2PTunnelIRCClient.IrcOutboundFilter(clientSock, destSock, expectedPong), "SOCKS IRC Client " + __clientId + " out", true);
|
||||||
|
out.start();
|
||||||
|
} catch (SOCKSException e) {
|
||||||
|
_log.error("Error from SOCKS connection", e);
|
||||||
|
closeSocket(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -89,10 +89,10 @@ public class SOCKS5Server extends SOCKSServer {
|
|||||||
int method = Method.NO_ACCEPTABLE_METHODS;
|
int method = Method.NO_ACCEPTABLE_METHODS;
|
||||||
|
|
||||||
for (int i = 0; i < nMethods; ++i) {
|
for (int i = 0; i < nMethods; ++i) {
|
||||||
method = in.readByte() & 0xff;
|
int meth = in.readByte() & 0xff;
|
||||||
if (method == Method.NO_AUTH_REQUIRED) {
|
if (meth == Method.NO_AUTH_REQUIRED) {
|
||||||
// That's fine, we do support this method
|
// That's fine, we do support this method
|
||||||
break;
|
method = meth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ public class SOCKS5Server extends SOCKSServer {
|
|||||||
int socksVer = in.readByte() & 0xff;
|
int socksVer = in.readByte() & 0xff;
|
||||||
if (socksVer != SOCKS_VERSION_5) {
|
if (socksVer != SOCKS_VERSION_5) {
|
||||||
_log.debug("error in SOCKS5 request (protocol != 5? wtf?)");
|
_log.debug("error in SOCKS5 request (protocol != 5? wtf?)");
|
||||||
throw new SOCKSException("Invalid protocol version in request");
|
throw new SOCKSException("Invalid protocol version in request: " + socksVer);
|
||||||
}
|
}
|
||||||
|
|
||||||
int command = in.readByte() & 0xff;
|
int command = in.readByte() & 0xff;
|
||||||
|
@ -349,6 +349,7 @@ public class IndexBean {
|
|||||||
return ( ("client".equals(type)) ||
|
return ( ("client".equals(type)) ||
|
||||||
("httpclient".equals(type)) ||
|
("httpclient".equals(type)) ||
|
||||||
("sockstunnel".equals(type)) ||
|
("sockstunnel".equals(type)) ||
|
||||||
|
("socksirctunnel".equals(type)) ||
|
||||||
("connectclient".equals(type)) ||
|
("connectclient".equals(type)) ||
|
||||||
("streamrclient".equals(type)) ||
|
("streamrclient".equals(type)) ||
|
||||||
("ircclient".equals(type)));
|
("ircclient".equals(type)));
|
||||||
@ -385,6 +386,7 @@ public class IndexBean {
|
|||||||
else if ("server".equals(internalType)) return _("Standard server");
|
else if ("server".equals(internalType)) return _("Standard server");
|
||||||
else if ("httpserver".equals(internalType)) return _("HTTP server");
|
else if ("httpserver".equals(internalType)) return _("HTTP server");
|
||||||
else if ("sockstunnel".equals(internalType)) return _("SOCKS 4/4a/5 proxy");
|
else if ("sockstunnel".equals(internalType)) return _("SOCKS 4/4a/5 proxy");
|
||||||
|
else if ("socksirctunnel".equals(internalType)) return _("SOCKS IRC proxy");
|
||||||
else if ("connectclient".equals(internalType)) return _("CONNECT/SSL/HTTPS proxy");
|
else if ("connectclient".equals(internalType)) return _("CONNECT/SSL/HTTPS proxy");
|
||||||
else if ("ircserver".equals(internalType)) return _("IRC server");
|
else if ("ircserver".equals(internalType)) return _("IRC server");
|
||||||
else if ("streamrclient".equals(internalType)) return _("Streamr client");
|
else if ("streamrclient".equals(internalType)) return _("Streamr client");
|
||||||
|
@ -250,7 +250,8 @@
|
|||||||
}
|
}
|
||||||
%></div>
|
%></div>
|
||||||
|
|
||||||
<% if (!"sockstunnel".equals(indexBean.getInternalType(curClient))) { %>
|
<% if (!("sockstunnel".equals(indexBean.getInternalType(curClient)) ||
|
||||||
|
"socksirctunnel".equals(indexBean.getInternalType(curClient)))) { %>
|
||||||
<div class="destinationField rowItem">
|
<div class="destinationField rowItem">
|
||||||
<label>
|
<label>
|
||||||
<% if ("httpclient".equals(indexBean.getInternalType(curClient)) || "connectclient".equals(indexBean.getInternalType(curClient))) { %>
|
<% if ("httpclient".equals(indexBean.getInternalType(curClient)) || "connectclient".equals(indexBean.getInternalType(curClient))) { %>
|
||||||
@ -288,6 +289,7 @@
|
|||||||
<option value="httpclient">HTTP</option>
|
<option value="httpclient">HTTP</option>
|
||||||
<option value="ircclient">IRC</option>
|
<option value="ircclient">IRC</option>
|
||||||
<option value="sockstunnel">SOCKS 4/4a/5</option>
|
<option value="sockstunnel">SOCKS 4/4a/5</option>
|
||||||
|
<option value="socksirctunnel">SOCKS IRC</option>
|
||||||
<option value="connectclient">CONNECT</option>
|
<option value="connectclient">CONNECT</option>
|
||||||
<option value="streamrclient">Streamr</option>
|
<option value="streamrclient">Streamr</option>
|
||||||
</select>
|
</select>
|
||||||
|
12
history.txt
12
history.txt
@ -1,3 +1,15 @@
|
|||||||
|
2010-03-05 zzz
|
||||||
|
* I2PSOCKSIRCTunnel:
|
||||||
|
- New, for filtering IRC client traffic when using SOCKS
|
||||||
|
* I2PTunnelIRCClient:
|
||||||
|
- Make filter classes static and public for use by SOCKS
|
||||||
|
- Eliminate redundant case conversion
|
||||||
|
- Pass ISON message through (jIRCii uses it for pings)
|
||||||
|
- Switch back to StringBuffer since it's used by 2 threads
|
||||||
|
- Set daemon on filter threads
|
||||||
|
* SOCKS5Server:
|
||||||
|
- Fix handling of multiple authentication methods
|
||||||
|
|
||||||
2010-03-02 zzz
|
2010-03-02 zzz
|
||||||
* Console:
|
* Console:
|
||||||
- Add link to jobs.jsp on configservice.jsp
|
- Add link to jobs.jsp on configservice.jsp
|
||||||
|
@ -18,7 +18,7 @@ public class RouterVersion {
|
|||||||
/** deprecated */
|
/** deprecated */
|
||||||
public final static String ID = "Monotone";
|
public final static String ID = "Monotone";
|
||||||
public final static String VERSION = CoreVersion.VERSION;
|
public final static String VERSION = CoreVersion.VERSION;
|
||||||
public final static long BUILD = 8;
|
public final static long BUILD = 9;
|
||||||
|
|
||||||
/** for example "-test" */
|
/** for example "-test" */
|
||||||
public final static String EXTRA = "";
|
public final static String EXTRA = "";
|
||||||
|
Reference in New Issue
Block a user