forked from I2P_Developers/i2p.i2p
SAM: Abort tunnel build if control socket closes
Command error handling and message improvements Implement simple HELP command bump -1
This commit is contained in:
@@ -53,6 +53,7 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
private volatile boolean stolenSocket;
|
private volatile boolean stolenSocket;
|
||||||
private volatile boolean streamForwardingSocket;
|
private volatile boolean streamForwardingSocket;
|
||||||
private final boolean sendPorts;
|
private final boolean sendPorts;
|
||||||
|
private final Object socketRLock = new Object();
|
||||||
private long _lastPing;
|
private long _lastPing;
|
||||||
private static final int FIRST_READ_TIMEOUT = 60*1000;
|
private static final int FIRST_READ_TIMEOUT = 60*1000;
|
||||||
private static final int READ_TIMEOUT = 3*60*1000;
|
private static final int READ_TIMEOUT = 3*60*1000;
|
||||||
@@ -234,8 +235,10 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
buf.setLength(0);
|
buf.setLength(0);
|
||||||
// first time, set a timeout
|
// first time, set a timeout
|
||||||
try {
|
try {
|
||||||
ReadLine.readLine(socket, buf, gotFirstLine ? 0 : FIRST_READ_TIMEOUT);
|
synchronized(socketRLock) {
|
||||||
socket.setSoTimeout(0);
|
ReadLine.readLine(socket, buf, gotFirstLine ? 0 : FIRST_READ_TIMEOUT);
|
||||||
|
socket.setSoTimeout(0);
|
||||||
|
}
|
||||||
} catch (SocketTimeoutException ste) {
|
} catch (SocketTimeoutException ste) {
|
||||||
writeString(SESSION_ERROR, "command timeout, bye");
|
writeString(SESSION_ERROR, "command timeout, bye");
|
||||||
break;
|
break;
|
||||||
@@ -266,6 +269,9 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
} else if (domain.equals("PONG")) {
|
} else if (domain.equals("PONG")) {
|
||||||
execPongMessage(opcode);
|
execPongMessage(opcode);
|
||||||
continue;
|
continue;
|
||||||
|
} else if (domain.equals("HELP")) {
|
||||||
|
writeString("HELP STATUS RESULT=OK MESSAGE=https://geti2p.net/en/docs/api/samv3\n");
|
||||||
|
continue;
|
||||||
} else if (domain.equals("QUIT") || domain.equals("STOP") ||
|
} else if (domain.equals("QUIT") || domain.equals("STOP") ||
|
||||||
domain.equals("EXIT")) {
|
domain.equals("EXIT")) {
|
||||||
writeString(domain + " STATUS RESULT=OK MESSAGE=bye\n");
|
writeString(domain + " STATUS RESULT=OK MESSAGE=bye\n");
|
||||||
@@ -274,7 +280,7 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
|
|
||||||
if (opcode == null) {
|
if (opcode == null) {
|
||||||
// This is not a correct message, for sure
|
// This is not a correct message, for sure
|
||||||
if (writeString(domain + " STATUS RESULT=I2P_ERROR", "command not specified"))
|
if (writeString(domain + " STATUS RESULT=I2P_ERROR", "missing subcommand, enter HELP for help"))
|
||||||
continue;
|
continue;
|
||||||
else
|
else
|
||||||
break;
|
break;
|
||||||
@@ -299,10 +305,7 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
} else if (domain.equals("AUTH")) {
|
} else if (domain.equals("AUTH")) {
|
||||||
canContinue = execAuthMessage(opcode, props);
|
canContinue = execAuthMessage(opcode, props);
|
||||||
} else {
|
} else {
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
canContinue = writeString(domain + " STATUS RESULT=I2P_ERROR", "unsupported command, enter HELP for help");
|
||||||
_log.debug("Unrecognized message domain: \""
|
|
||||||
+ domain + "\"");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canContinue) {
|
if (!canContinue) {
|
||||||
@@ -406,7 +409,8 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
if (style == null && !opcode.equals("REMOVE"))
|
if (style == null && !opcode.equals("REMOVE"))
|
||||||
return writeString(SESSION_ERROR, "No SESSION STYLE specified");
|
return writeString(SESSION_ERROR, "No SESSION STYLE specified");
|
||||||
|
|
||||||
try{
|
SocketCloseDetector detector = null;
|
||||||
|
try {
|
||||||
if (opcode.equals("CREATE")) {
|
if (opcode.equals("CREATE")) {
|
||||||
if ((this.getRawSession()!= null) || (this.getDatagramSession() != null)
|
if ((this.getRawSession()!= null) || (this.getDatagramSession() != null)
|
||||||
|| (this.getStreamSession() != null)) {
|
|| (this.getStreamSession() != null)) {
|
||||||
@@ -483,25 +487,33 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
|
|
||||||
|
|
||||||
// Create the session
|
// Create the session
|
||||||
|
// We block in the session constructors while tunnels are built.
|
||||||
|
// If the client times out and closes the socket, we won't know it
|
||||||
|
// without a separate socket monitor.
|
||||||
|
detector = new SocketCloseDetector();
|
||||||
|
|
||||||
if (style.equals("RAW")) {
|
if (style.equals("RAW")) {
|
||||||
|
detector.start();
|
||||||
SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props);
|
SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props);
|
||||||
SAMv3RawSession v3 = new SAMv3RawSession(nick, dgs);
|
SAMv3RawSession v3 = new SAMv3RawSession(nick, dgs);
|
||||||
rawSession = v3;
|
rawSession = v3;
|
||||||
this.session = v3;
|
this.session = v3;
|
||||||
v3.start();
|
v3.start();
|
||||||
} else if (style.equals("DATAGRAM")) {
|
} else if (style.equals("DATAGRAM")) {
|
||||||
|
detector.start();
|
||||||
SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props);
|
SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props);
|
||||||
SAMv3DatagramSession v3 = new SAMv3DatagramSession(nick, dgs);
|
SAMv3DatagramSession v3 = new SAMv3DatagramSession(nick, dgs);
|
||||||
datagramSession = v3;
|
datagramSession = v3;
|
||||||
this.session = v3;
|
this.session = v3;
|
||||||
v3.start();
|
v3.start();
|
||||||
} else if (style.equals("STREAM")) {
|
} else if (style.equals("STREAM")) {
|
||||||
|
detector.start();
|
||||||
SAMv3StreamSession v3 = newSAMStreamSession(nick);
|
SAMv3StreamSession v3 = newSAMStreamSession(nick);
|
||||||
streamSession = v3;
|
streamSession = v3;
|
||||||
this.session = v3;
|
this.session = v3;
|
||||||
v3.start();
|
v3.start();
|
||||||
} else if (style.equals("PRIMARY") || style.equals("MASTER")) {
|
} else if (style.equals("PRIMARY") || style.equals("MASTER")) {
|
||||||
|
detector.start();
|
||||||
SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props);
|
SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props);
|
||||||
PrimarySession v3 = new PrimarySession(nick, dgs, this, allProps);
|
PrimarySession v3 = new PrimarySession(nick, dgs, this, allProps);
|
||||||
streamSession = v3;
|
streamSession = v3;
|
||||||
@@ -511,12 +523,21 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
v3.start();
|
v3.start();
|
||||||
} else {
|
} else {
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Unrecognized SESSION STYLE: \"" + style +"\"");
|
_log.debug("Unsupported SESSION STYLE: \"" + style +"\"");
|
||||||
return writeString(SESSION_ERROR, "Unrecognized SESSION STYLE");
|
return writeString(SESSION_ERROR, "Unrecognized SESSION STYLE");
|
||||||
}
|
}
|
||||||
ok = true ;
|
// kill the detector
|
||||||
return writeString("SESSION STATUS RESULT=OK DESTINATION="
|
detector.done = true;
|
||||||
+ dest + "\n");
|
synchronized(socketRLock) {
|
||||||
|
detector.interrupt();
|
||||||
|
}
|
||||||
|
String ignoredCommand = detector.ignoredCommand;
|
||||||
|
detector = null;
|
||||||
|
ok = true;
|
||||||
|
boolean rv = writeString("SESSION STATUS RESULT=OK DESTINATION=" + dest + '\n');
|
||||||
|
if (rv && ignoredCommand != null)
|
||||||
|
rv = writeString(ignoredCommand + " STATUS RESULT=I2P_ERROR", "invalid state");
|
||||||
|
return rv;
|
||||||
} else if (opcode.equals("ADD") || opcode.equals("REMOVE")) {
|
} else if (opcode.equals("ADD") || opcode.equals("REMOVE")) {
|
||||||
// prevent trouble in finally block
|
// prevent trouble in finally block
|
||||||
ok = true;
|
ok = true;
|
||||||
@@ -552,6 +573,16 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
_log.error("Failed to start SAM session", e);
|
_log.error("Failed to start SAM session", e);
|
||||||
return writeString(SESSION_ERROR, e.getMessage());
|
return writeString(SESSION_ERROR, e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
|
if (detector != null) {
|
||||||
|
// kill the detector
|
||||||
|
detector.done = true;
|
||||||
|
synchronized(socketRLock) {
|
||||||
|
detector.interrupt();
|
||||||
|
}
|
||||||
|
String ignoredCommand = detector.ignoredCommand;
|
||||||
|
if (ignoredCommand != null)
|
||||||
|
writeString(ignoredCommand + " STATUS RESULT=I2P_ERROR", "invalid state");
|
||||||
|
}
|
||||||
// unregister the session if it has not been created
|
// unregister the session if it has not been created
|
||||||
if ( !ok && nick!=null ) {
|
if ( !ok && nick!=null ) {
|
||||||
sSessionsHash.del(nick) ;
|
sSessionsHash.del(nick) ;
|
||||||
@@ -560,6 +591,88 @@ class SAMv3Handler extends SAMv1Handler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for socket close while tunnels are being built,
|
||||||
|
* by doing what is hopefully a dummy read for the next command.
|
||||||
|
* Interrupt the handler if it happens.
|
||||||
|
* After tunnel build success or failure, the handler will interrupt us.
|
||||||
|
*
|
||||||
|
* If the command is QUIT or equivalent, do that.
|
||||||
|
* If it's anything else, set ignoredCommand, and execSessionMessage() will deal with it.
|
||||||
|
*
|
||||||
|
* @since 0.9.58
|
||||||
|
*/
|
||||||
|
private class SocketCloseDetector extends I2PAppThread {
|
||||||
|
private final Thread _handler = Thread.currentThread();
|
||||||
|
public volatile String ignoredCommand;
|
||||||
|
public volatile boolean done;
|
||||||
|
|
||||||
|
public SocketCloseDetector() {
|
||||||
|
super("SAM control socket close detector");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
try {
|
||||||
|
Socket s = socket.socket();
|
||||||
|
InputStream in = s.getInputStream();
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
//_log.debug("sleeping...");
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Only read under lock
|
||||||
|
// And execSessionMessage() must lock before interrupting us
|
||||||
|
// Because interrupting a SocketChannel read will close the socket
|
||||||
|
synchronized(socketRLock) {
|
||||||
|
s.setSoTimeout(20);
|
||||||
|
try {
|
||||||
|
// we could use ReadLine here, but let's keep it simple
|
||||||
|
while (true) {
|
||||||
|
int c = in.read();
|
||||||
|
if (c < 0)
|
||||||
|
throw new IOException("Socket closed");
|
||||||
|
if (c == '\n') {
|
||||||
|
String line = buf.toString();
|
||||||
|
buf.setLength(0);
|
||||||
|
try {
|
||||||
|
Properties props = SAMUtils.parseParams(line);
|
||||||
|
String domain = props.getProperty(SAMUtils.COMMAND);
|
||||||
|
if (domain == null)
|
||||||
|
continue; // empty line
|
||||||
|
if (domain.equals("QUIT") || domain.equals("STOP") || domain.equals("EXIT")) {
|
||||||
|
_log.error("SAM socket closed while waiting for tunnels to build");
|
||||||
|
writeString(SESSION_ERROR, "Tunnel build interrupted");
|
||||||
|
writeString(domain + " STATUS RESULT=OK", "bye");
|
||||||
|
try { closeClientSocket(); } catch (IOException ioe) {}
|
||||||
|
_handler.interrupt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ignoredCommand = domain;
|
||||||
|
} catch (SAMException e) {
|
||||||
|
ignoredCommand = "SESSION";
|
||||||
|
}
|
||||||
|
if (_log.shouldWarn())
|
||||||
|
_log.warn("Ignoring SAM command during tunnel build: " + line);
|
||||||
|
}
|
||||||
|
buf.append((char) c);
|
||||||
|
}
|
||||||
|
} catch (SocketTimeoutException ste) {}
|
||||||
|
s.setSoTimeout(0);
|
||||||
|
}
|
||||||
|
} while (!done);
|
||||||
|
if (_log.shouldWarn())
|
||||||
|
_log.warn("Detector exit after tunnel build");
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
_log.error("SAM socket closed while waiting for tunnels to build", ioe);
|
||||||
|
_handler.interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws NPE if login nickname is not registered
|
* @throws NPE if login nickname is not registered
|
||||||
*/
|
*/
|
||||||
|
@@ -1,3 +1,9 @@
|
|||||||
|
2023-01-11 zzz
|
||||||
|
* SAM: Abort tunnel build if control socket closes
|
||||||
|
|
||||||
|
2023-01-11 zzz
|
||||||
|
* Console: Add ability to search netdb for a range of ports
|
||||||
|
|
||||||
2023-01-10 2.1.0 (API 0.9.57) released
|
2023-01-10 2.1.0 (API 0.9.57) released
|
||||||
|
|
||||||
2023-01-08 zzz
|
2023-01-08 zzz
|
||||||
|
@@ -18,7 +18,7 @@ public class RouterVersion {
|
|||||||
/** deprecated */
|
/** deprecated */
|
||||||
public final static String ID = "Git";
|
public final static String ID = "Git";
|
||||||
public final static String VERSION = CoreVersion.VERSION;
|
public final static String VERSION = CoreVersion.VERSION;
|
||||||
public final static long BUILD = 0;
|
public final static long BUILD = 1;
|
||||||
|
|
||||||
/** for example "-test" */
|
/** for example "-test" */
|
||||||
public final static String EXTRA = "";
|
public final static String EXTRA = "";
|
||||||
|
Reference in New Issue
Block a user