/*
* $Id$
*
* Copyright (C) 2003-2014 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.net.ipv4.tcp;
import java.net.ConnectException;
import java.net.SocketException;
import java.util.LinkedList;
import org.apache.log4j.Logger;
import org.jnode.net.SocketBuffer;
import org.jnode.net.ipv4.IPv4Address;
import org.jnode.net.ipv4.IPv4Constants;
import org.jnode.net.ipv4.IPv4ControlBlock;
import org.jnode.net.ipv4.IPv4Header;
import org.jnode.util.NumberUtils;
import org.jnode.util.TimeoutException;
/**
* @author Ewout Prangsma (epr@users.sourceforge.net)
*/
public class TCPControlBlock extends IPv4ControlBlock implements TCPConstants {
private static final boolean DEBUG = false;
/**
* My logger
*/
private static final Logger log = Logger.getLogger(TCPControlBlock.class);
/**
* The outgoing channel
*/
private final TCPOutChannel outChannel;
/**
* The incoming channel
*/
private final TCPInChannel inChannel;
/** Last incoming sequence number */
// private int lastInSeqNr;
/**
* Window size of the outgoing connection
*/
private int outWindowSize;
/**
* The current state
*/
private int curState;
/**
* My listening parent
*/
private final TCPControlBlock parent;
/**
* List of connections that are established, but have not been "accepted"
*/
private LinkedList<TCPControlBlock> readyToAcceptList = new LinkedList<TCPControlBlock>();
/**
* Has this connection be reset?
*/
private boolean reset;
/**
* Has this connection been refused?
*/
private boolean refused;
/**
* Timeout for blocking operations
*/
private int timeout = TCP_DEFAULT_TIMEOUT;
/**
* Create a new instance
*
* @param list
* @param parent
* @param tcp
* @param isn The initial outgoing sequence number
*/
public TCPControlBlock(TCPControlBlockList list, TCPControlBlock parent, TCPProtocol tcp,
int isn) {
super(list, IPv4Constants.IPPROTO_TCP, TCP_DEFAULT_TTL);
this.parent = parent;
this.outChannel = new TCPOutChannel(tcp, this, isn);
this.inChannel = new TCPInChannel(this);
// this.tcp = tcp;
this.curState = TCPS_CLOSED;
this.outWindowSize = TCP_MAXWIN;
this.reset = false;
this.refused = false;
}
// ------------------------------------------
// Receive (input) methods
// ------------------------------------------
/**
* Handle a received segment for this connection
*
* @param hdr
* @param skbuf
*/
public synchronized void receive(TCPHeader hdr, SocketBuffer skbuf) throws SocketException {
if (DEBUG) {
if (log.isDebugEnabled()) {
log.debug("receive: me=[" + this + "], hdr=[" + hdr + ']');
}
}
final IPv4Header ipHdr = (IPv4Header) skbuf.getNetworkLayerHeader();
final boolean ack = hdr.isFlagAcknowledgeSet();
final boolean rst = hdr.isFlagResetSet();
if (rst) {
receiveProcessReset(ipHdr, hdr, skbuf);
return;
}
if (ack) {
outChannel.processAck(hdr.getAckNr());
}
switch (curState) {
case TCPS_LISTEN:
receiveListen(ipHdr, hdr, skbuf);
break;
case TCPS_SYN_RECV:
receiveSynRecv(ipHdr, hdr, skbuf);
break;
case TCPS_SYN_SENT:
receiveSynSend(ipHdr, hdr, skbuf);
break;
case TCPS_ESTABLISHED:
receiveEstablished(ipHdr, hdr, skbuf);
break;
case TCPS_FIN_WAIT_1:
receiveFinWait1(ipHdr, hdr, skbuf);
break;
case TCPS_FIN_WAIT_2:
receiveFinWait2(ipHdr, hdr, skbuf);
break;
case TCPS_LAST_ACK:
receiveLastAck(ipHdr, hdr, skbuf);
break;
case TCPS_CLOSING:
receiveClosing(ipHdr, hdr, skbuf);
break;
case TCPS_TIME_WAIT:
receiveTimeWait(ipHdr, hdr, skbuf);
break;
default:
if (DEBUG) {
log.debug("Unhandled state in receive (" + getStateName() + ')');
}
break;
}
}
/**
* Process a reset segment
*
* @param ipHdr
* @param hdr
* @param skbuf
*/
private void receiveProcessReset(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
final boolean ack = hdr.isFlagAcknowledgeSet();
final boolean syn = hdr.isFlagSynchronizeSet();
switch (curState) {
case TCPS_SYN_RECV:
case TCPS_SYN_SENT:
notifyConnectionRefused();
setState(TCPS_CLOSED);
drop(ipHdr, hdr, "connection refused");
break;
case TCPS_ESTABLISHED:
case TCPS_FIN_WAIT_1:
case TCPS_FIN_WAIT_2:
case TCPS_CLOSE_WAIT:
notifyConnectionReset();
setState(TCPS_CLOSED);
drop(ipHdr, hdr, "connection reset");
break;
case TCPS_CLOSING:
case TCPS_LAST_ACK:
case TCPS_TIME_WAIT:
setState(TCPS_CLOSED);
drop(ipHdr, hdr, "connection reset");
break;
}
if (syn) {
notifyConnectionReset();
sendRST();
drop(ipHdr, hdr, "connection reset");
}
if (!ack) {
drop(ipHdr, hdr, "ACK expected together with RST");
}
}
/**
* Current state is LISTEN. If a SYN segment is received, send a SYN&ACK,
* and set the state to SYN_RECV
*
* @param hdr
* @param skbuf
*/
private void receiveListen(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
if (DEBUG) {
log.debug("receiveListen");
}
final boolean ack = hdr.isFlagAcknowledgeSet();
final boolean syn = hdr.isFlagSynchronizeSet();
if (ack) {
// Drop this segment with a RST reply
sendRST();
drop(ipHdr, hdr, "unexpected ACK segment");
} else if (syn) {
// Drop if broadcast or multicast
final IPv4Address dst = ipHdr.getDestination();
if (dst.isBroadcast() || dst.isMulticast()) {
// Drop this segment
drop(ipHdr, hdr, "broadcast or multicast destination");
} else {
// Process the SYN request
final TCPControlBlock copy =
(TCPControlBlock) copyAndConnect(dst, ipHdr.getSource(), hdr.getSrcPort());
copy.listenSynReceivedOnCopy(hdr, skbuf);
}
} else {
// Invalid segment
drop(ipHdr, hdr, "unexpected segment; SYN expected");
}
}
/**
* Current state is 0, SYN segment received. This method is called on a copy
* of the listening control block. Send a SYN&ACK, and set the state to
* SYN_RECV
*
* @param hdr
* @param skbuf
*/
private void listenSynReceivedOnCopy(TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
if (DEBUG) {
log.debug("listenSynReceivedOnCopy");
}
// Save the foreign seq nr
inChannel.initISN(hdr);
// Send the SYN&ACK TCP reply
sendACK(TCPF_SYN, hdr.getSequenceNr() + 1);
// Update the state
setState(TCPS_SYN_RECV);
}
/**
* Current state is SYN_RECV.
*/
private void receiveSynRecv(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
final boolean ack = hdr.isFlagAcknowledgeSet();
if (ack) {
setState(TCPS_ESTABLISHED);
parent.notifyChildEstablished(this);
} else {
// Invalid segment
drop(ipHdr, hdr, "ACK expected");
}
}
/**
* Current state is SYN_SEND.
*/
private void receiveSynSend(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
final boolean syn = hdr.isFlagSynchronizeSet();
if (!syn) {
// Invalid segment
drop(ipHdr, hdr, "SYN expected");
} else {
// Active open , go to ESTABLISHED
inChannel.initISN(hdr);
sendACK(0, hdr.getSequenceNr() + 1);
setState(TCPS_ESTABLISHED);
}
}
/**
* Current state is ESTABLISHED, FIN segment received. Send a ACK, and set
* the state to CLOSE_WAIT
*
* @param hdr
* @param skbuf
*/
private void receiveEstablished(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
final boolean fin = hdr.isFlagFinishedSet();
// Process the data
inChannel.processData(ipHdr, hdr, skbuf);
// FIN received, then change state
if (fin) {
setState(TCPS_CLOSE_WAIT);
}
}
/**
* State is FIN_WAIT_1, any segment received
*
* @param hdr
* @param skbuf
*/
private void receiveFinWait1(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
final boolean fin = hdr.isFlagFinishedSet();
final boolean ack = hdr.isFlagAcknowledgeSet();
// Process the data
inChannel.processData(ipHdr, hdr, skbuf);
// Update state (if required)
if (fin && ack) {
setState(TCPS_TIME_WAIT);
} else if (fin) {
setState(TCPS_CLOSING);
} else if (ack) {
setState(TCPS_FIN_WAIT_2);
} else {
// Invalid segment
drop(ipHdr, hdr, "FIN and/or ACK expected");
}
}
/**
* State is FIN_WAIT_2, any segment received
*
* @param hdr
* @param skbuf
*/
private void receiveFinWait2(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
final boolean fin = hdr.isFlagFinishedSet();
// Process the data
inChannel.processData(ipHdr, hdr, skbuf);
// Update state
if (fin) {
setState(TCPS_TIME_WAIT);
// wait for 2MSL (2 maximum segment life time)
final long start = System.currentTimeMillis();
int timeout = 400;
long now;
while (true) {
now = System.currentTimeMillis();
if ((start + timeout <= now)) {
// We have a timeout
break;
}
try {
wait(10);
} catch (InterruptedException ex) {
// Ignore
}
}
setState(TCPS_CLOSED);
} else {
// Invalid segment
drop(ipHdr, hdr, "FIN expected");
}
}
/**
* State is LAST_ACK, any segment received
*
* @param hdr
* @param skbuf
*/
private void receiveLastAck(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
final boolean ack = hdr.isFlagAcknowledgeSet();
if (ack) {
setState(TCPS_CLOSED);
} else {
// Invalid segment
drop(ipHdr, hdr, "ACK expected");
}
}
/**
* State is CLOSING, any segment received
*
* @param hdr
* @param skbuf
*/
private void receiveClosing(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
final boolean ack = hdr.isFlagAcknowledgeSet();
if (ack) {
setState(TCPS_TIME_WAIT);
} else {
// Invalid segment
drop(ipHdr, hdr, "ACK expected");
}
}
/**
* State is TIME_WAIT, discard any segments
*
* @param hdr
* @param skbuf
*/
private void receiveTimeWait(IPv4Header ipHdr, TCPHeader hdr, SocketBuffer skbuf)
throws SocketException {
setState(TCPS_CLOSED);
drop(ipHdr, hdr, "discard all in TIME_WAIT state");
}
// ------------------------------------------
// Timeout methods
// ------------------------------------------
/**
* Process timeout handling
*/
public void timeout() {
try {
outChannel.timeout();
} catch (SocketException ex) {
log.error("Error in timeout of " + this, ex);
}
}
// ------------------------------------------
// Utility methods
// ------------------------------------------
/**
* Notify a segment drop to the debug log
*/
private void drop(IPv4Header ipHdr, TCPHeader hdr, String reason) {
if (DEBUG) {
log.debug("Dropping segment due to: " + reason);
}
}
/**
* Send a ACK segment
*/
protected final void sendACK(int extraFlags, int ackNr) throws SocketException {
if (DEBUG) {
log.debug("sendACK(0x" + NumberUtils.hex(extraFlags, 4) + ", " + (ackNr & 0xFFFFFFFFL) + ')');
}
// Create the FIN TCP reply
final TCPHeader replyHdr = createOutgoingTCPHeader(extraFlags | TCPF_ACK, ackNr);
// ACK takes 0 seq-nrs, so don't increment snd_next
// Create the IP reply header
final IPv4Header replyIp = createOutgoingIPv4Header();
// Send the reply
outChannel.send(replyIp, replyHdr);
}
/**
* Send a FIN segment
*/
private void sendFIN() throws SocketException {
if (DEBUG) {
log.debug("sendFIN");
}
// Create the FIN TCP reply
final TCPHeader replyHdr =
createOutgoingTCPHeader(TCPF_FIN | TCPF_ACK, inChannel.getRcvNext());
// Create the IP reply header
final IPv4Header replyIp = createOutgoingIPv4Header();
// Send the reply
outChannel.send(replyIp, replyHdr);
}
/**
* Send a RST segment
*/
private void sendRST() throws SocketException {
if (DEBUG) {
log.debug("sendRST");
}
// Create the RST TCP reply
final TCPHeader replyHdr = createOutgoingTCPHeader(TCPF_RST, 0);
// RST takes 0 seg-nrs TODO is this correct????
// Create the IP reply header
final IPv4Header replyIp = createOutgoingIPv4Header();
// Send the reply
outChannel.send(replyIp, replyHdr);
}
/**
* Send a SYN segment
*/
private void sendSYN() throws SocketException {
if (DEBUG) {
log.debug("sendSYN");
}
// Create the SYN TCP
final TCPHeader hdr = createOutgoingTCPHeader(TCPF_SYN, 0);
// Create the IP reply header
final IPv4Header ipHdr = createOutgoingIPv4Header();
// Send the reply
outChannel.send(ipHdr, hdr);
}
/**
* Notify this listening parent that one of my children have established a
* connection.
*
* @param child
*/
private synchronized void notifyChildEstablished(TCPControlBlock child) throws SocketException {
if (isState(TCPS_LISTEN)) {
// Put on the waiting list for accept to handle and notify blocked
// threads.
readyToAcceptList.add(child);
notifyAll();
} else {
// I'm not listening anymore, close the connection.
child.appClose();
}
}
/**
* Notify a connection reset
*/
private synchronized void notifyConnectionReset() {
this.reset = true;
inChannel.notifyConnectionReset();
outChannel.notifyConnectionReset();
notifyAll();
}
/**
* Notify a connection refused
*/
private void notifyConnectionRefused() {
this.refused = true;
notifyAll();
}
/**
* Is the current state equal to the given state?
*
* @param state
* @return
*/
private boolean isState(int state) {
return (this.curState == state);
}
/**
* Update the state and notify any waiting threads
*
* @param state
*/
private synchronized void setState(int state) throws SocketException {
// System.out.println("state = " + state);
if (this.curState != state) {
this.curState = state;
if (state == TCPS_CLOSED) {
super.removeFromList();
}
notifyAll();
}
}
/**
* Wait until the state is equal to the given state or the connection is
* reset or refused.
*/
private synchronized void waitUntilState(int state, long timeout) throws TimeoutException {
final long start = System.currentTimeMillis();
while (!isState(state) && !isReset() && !isRefused()) {
final long now = System.currentTimeMillis();
if ((start + timeout <= now) && (timeout > 0)) {
// We have a timeout
throw new TimeoutException();
}
try {
// Thread.currentThread().sleep(Math.max(1, timeout - (now -
// start)));
wait(Math.max(1, timeout - (now - start)));
} catch (InterruptedException ex) {
// Ignore
}
}
}
/**
* Create a TCP header for outgoing trafic
*
* @param options
* @return The created TCP header
*/
protected TCPHeader createOutgoingTCPHeader(int options, int ackNr) {
final TCPHeader hdr =
new TCPHeader(getLocalPort(), getForeignPort(), 0, 0, ackNr, outWindowSize, 0);
hdr.setFlags(options);
return hdr;
}
// ------------------------------------------
// Application methods
// ------------------------------------------
/**
* Wait for incoming requests
*
* @throws SocketException
*/
public synchronized void appListen() throws SocketException {
if (!isState(TCPS_CLOSED)) {
throw new SocketException("Invalid connection state " + getStateName());
}
setState(TCPS_LISTEN);
}
/**
* Active connect to a foreign address. This method blocks until the
* connection has been established.
*
* @throws SocketException
*/
public synchronized void appConnect(IPv4Address fAddr, int fPort) throws SocketException {
if (!isState(TCPS_CLOSED)) {
throw new SocketException("Invalid connection state " + getStateName());
}
super.connect(getLocalAddress(), fAddr, fPort);
for (int attempt = 0; attempt < TCP_MAXCONNECT; attempt++) {
try {
// Send the SYN
sendSYN();
// Update the state
setState(TCPS_SYN_SENT);
// Wait for an ESTABLISHED state
waitUntilState(TCPS_ESTABLISHED, timeout);
// Check for reset condition
if (isRefused()) {
throw new ConnectException("Connection refused");
}
if (DEBUG) {
log.debug("Connected to " + fAddr + ':' + fPort);
}
return;
} catch (TimeoutException ex) {
// Ignore and just try again
}
}
// Not succeeded to connect
throw new ConnectException("Connection request timeout");
}
/**
* Wait for an established connection.
*
* @return The accepted connection
*/
public synchronized TCPControlBlock appAccept() {
while (true) {
if (!readyToAcceptList.isEmpty()) {
final TCPControlBlock child = (TCPControlBlock) readyToAcceptList.getFirst();
readyToAcceptList.remove(child);
return child;
} else {
try {
wait();
} catch (InterruptedException ex) {
// Ignore
}
}
}
}
/**
* Active close the connection by the application.
*/
public/* synchronized */void appClose() throws SocketException {
if (DEBUG) {
if (log.isDebugEnabled()) {
log.debug("active close state=" + getStateName());
}
}
try {
switch (curState) {
case TCPS_SYN_RECV:
case TCPS_ESTABLISHED:
sendFIN();
setState(TCPS_FIN_WAIT_1);
// this is blocking the closing of the socket/output stream
// - Martin Husted Hartvig 01/03/2005
// the waitUntilState have to rethinked
// waitUntilState(TCPS_CLOSED, 0);
break;
case TCPS_SYN_SENT:
case TCPS_LISTEN:
setState(TCPS_CLOSED);
break;
case TCPS_CLOSE_WAIT:
sendFIN();
setState(TCPS_LAST_ACK);
waitUntilState(TCPS_CLOSED, 0);
break;
case TCPS_CLOSED:
// Ignore
break;
default:
throw new SocketException("Illegal state in close (" + getStateName() + ')');
}
} catch (TimeoutException ex) {
throw (SocketException) new SocketException("Timeout").initCause(ex);
}
if (isReset()) {
throw new SocketException("Connection reset");
}
}
/**
* Send data to the foreign side. This method can split-up the data in
* chunks and blocks until there is space in the send buffer to hold the
* data.
*
* @param data
* @param offset
* @param length
* @throws SocketException
*/
public void appSendData(byte[] data, int offset, int length) throws SocketException {
if (DEBUG) {
log.debug("appSendData(data, " + offset + ", " + length + ')');
}
if (!isState(TCPS_ESTABLISHED) && !isState(TCPS_CLOSE_WAIT)) {
throw new SocketException("Illegal state to send data: " + getStateName());
}
if (offset < 0) {
throw new IllegalArgumentException("offset " + offset);
}
if (length < 0) {
throw new IllegalArgumentException("length " + length);
}
final int mss = outChannel.getMss();
while (length > 0) {
final int chunk = Math.min(length, mss);
// Create the TCP header
final TCPHeader hdr = createOutgoingTCPHeader(TCPF_ACK, inChannel.getRcvNext());
// Create the IP header
final IPv4Header ipHdr = createOutgoingIPv4Header();
// Send the chunk of data
outChannel.send(ipHdr, hdr, data, offset, chunk);
// Update length & offset
offset += chunk;
length -= chunk;
}
}
/**
* Return the number of available bytes in the input buffer.
*/
public int appAvailable() {
return inChannel.available();
}
/**
* Read data from the input buffer up to len bytes long. Block until there
* is data available.
*
* @param dst
* @param off
* @param len
* @return The number of bytes read
*/
public int appRead(byte[] dst, int off, int len) throws SocketException {
return inChannel.read(dst, off, len);
}
/**
* @return Returns the state.
*/
public final int getState() {
return this.curState;
}
/**
* @return Returns the name of the current state.
*/
public final String getStateName() {
return TCP_STATE_NAMES[this.curState];
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
return super.toString() + ", state " + TCP_STATE_NAMES[curState];
}
/**
* Has this connection been reset
*
* @return Returns the reset.
*/
public final boolean isReset() {
return this.reset;
}
/**
* @return Returns the refused.
*/
public final boolean isRefused() {
return this.refused;
}
public int getReceiveBufferSize() {
return inChannel.getBufferSize();
}
public int getSendBufferSize() {
return outChannel.getBufferSize();
}
}