package org.torproject.jtor.circuits.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.torproject.jtor.TorException;
import org.torproject.jtor.circuits.Circuit;
import org.torproject.jtor.circuits.CircuitBuildHandler;
import org.torproject.jtor.circuits.CircuitNode;
import org.torproject.jtor.circuits.Connection;
import org.torproject.jtor.circuits.ConnectionClosedException;
import org.torproject.jtor.circuits.OpenStreamResponse;
import org.torproject.jtor.circuits.cells.Cell;
import org.torproject.jtor.circuits.cells.RelayCell;
import org.torproject.jtor.data.IPv4Address;
import org.torproject.jtor.data.exitpolicy.ExitTarget;
import org.torproject.jtor.directory.Router;
import org.torproject.jtor.logging.Logger;
/**
* This class represents an established circuit through the Tor network.
*
*/
public class CircuitImpl implements Circuit {
static CircuitImpl create(CircuitManagerImpl circuitManager, ConnectionManagerImpl connectionManager, Logger logger) {
return new CircuitImpl(circuitManager, connectionManager, logger);
}
private final static long CIRCUIT_BUILD_TIMEOUT_MS = 30 * 1000;
private final static long CIRCUIT_RELAY_RESPONSE_TIMEOUT = 20 * 1000;
private ConnectionImpl entryConnection;
private int circuitId;
private final CircuitManagerImpl circuitManager;
private final Logger logger;
private final List<CircuitNodeImpl> nodeList;
private final BlockingQueue<RelayCell> relayCellResponseQueue;
private final BlockingQueue<Cell> controlCellResponseQueue;
private final Map<Integer, StreamImpl> streamMap;
private final Set<ExitTarget> failedExitRequests;
private final CircuitBuilder circuitBuilder;
private final CircuitStatus status;
private final Object relaySendLock = new Object();
private CircuitImpl(CircuitManagerImpl circuitManager, ConnectionManagerImpl connectionManager, Logger logger) {
nodeList = new ArrayList<CircuitNodeImpl>();
this.circuitManager = circuitManager;
this.logger = logger;
this.relayCellResponseQueue = new LinkedBlockingQueue<RelayCell>();
this.controlCellResponseQueue = new LinkedBlockingQueue<Cell>();
this.streamMap = new HashMap<Integer, StreamImpl>();
this.failedExitRequests = new HashSet<ExitTarget>();
status = new CircuitStatus();
circuitBuilder = new CircuitBuilder(this, connectionManager, logger);
}
void initializeConnectingCircuit(ConnectionImpl entryConnection, int circuitId) {
this.circuitId = circuitId;
this.entryConnection = entryConnection;
}
public boolean isConnected() {
return status.isConnected();
}
void setConnected() {
status.setStateConnected();
}
public void openCircuit(List<Router> circuitPath, CircuitBuildHandler handler) {
startCircuitOpen(circuitPath);
if(circuitBuilder.openCircuit(circuitPath, handler))
circuitOpenSucceeded();
else
circuitOpenFailed();
}
private void startCircuitOpen(List<Router> circuitPath) {
if(!status.isUnconnected())
throw new IllegalStateException("Can only connect UNCONNECTED circuits");
status.updateCreatedTimestamp();
status.setStateBuilding(circuitPath);
circuitManager.circuitStartConnect(this);
}
private void circuitOpenFailed() {
status.setStateFailed();
circuitManager.circuitInactive(this);
}
private void circuitOpenSucceeded() {
status.setStateOpen();
circuitManager.circuitConnected(this);
}
public void extendCircuit(Router router) {
if(!isConnected())
throw new TorException("Cannot extend an unconnected circuit");
circuitBuilder.extendTo(router);
}
public Connection getConnection() {
if(!isConnected())
throw new TorException("Circuit is not connected.");
return entryConnection;
}
public int getCircuitId() {
return circuitId;
}
public void sendRelayCell(RelayCell cell) {
sendRelayCellTo(cell, cell.getCircuitNode());
}
public void sendRelayCellToFinalNode(RelayCell cell) {
sendRelayCellTo(cell, getFinalCircuitNode());
}
private void sendRelayCellTo(RelayCell cell, CircuitNode targetNode) {
synchronized(relaySendLock) {
logger.debug("Sending: "+ cell);
cell.setLength();
targetNode.updateForwardDigest(cell);
cell.setDigest(targetNode.getForwardDigestBytes());
for(CircuitNode node = targetNode; node != null; node = node.getPreviousNode())
node.encryptForwardCell(cell);
if(cell.getRelayCommand() == RelayCell.RELAY_DATA)
targetNode.waitForSendWindowAndDecrement();
sendCell(cell);
}
}
public void sendCell(Cell cell) {
if(!(status.isConnected() || status.isBuilding()))
return;
try {
status.updateDirtyTimestamp();
entryConnection.sendCell(cell);
} catch (ConnectionClosedException e) {
destroyCircuit();
}
}
void appendNode(CircuitNodeImpl node) {
nodeList.add(node);
}
int getCircuitLength() {
return nodeList.size();
}
public CircuitNodeImpl getFinalCircuitNode() {
if(nodeList.isEmpty())
throw new TorException("getFinalCircuitNode() called on empty circuit");
return nodeList.get( getCircuitLength() - 1);
}
public RelayCell createRelayCell(int relayCommand, int streamId, CircuitNode targetNode) {
return new RelayCellImpl(targetNode, circuitId, streamId, relayCommand);
}
public RelayCell receiveRelayCell() {
return dequeueRelayResponseCell();
}
private RelayCell dequeueRelayResponseCell() {
try {
final long timeout = getReceiveTimeout();
return relayCellResponseQueue.poll(timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
private RelayCell decryptRelayCell(Cell cell) {
for(CircuitNodeImpl node: nodeList) {
if(node.decryptBackwardCell(cell)) {
return RelayCellImpl.createFromCell(node, cell);
}
}
destroyCircuit();
throw new TorException("Could not decrypt relay cell");
}
// Return null on timeout
Cell receiveControlCellResponse() {
try {
final long timeout = getReceiveTimeout();
return controlCellResponseQueue.poll(timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
private long getReceiveTimeout() {
if(status.isBuilding())
return remainingBuildTime();
else
return CIRCUIT_RELAY_RESPONSE_TIMEOUT;
}
private long remainingBuildTime() {
final long elapsed = status.getMillisecondsElapsedSinceCreated();
if(elapsed == 0 || elapsed >= CIRCUIT_BUILD_TIMEOUT_MS)
return 0;
return CIRCUIT_BUILD_TIMEOUT_MS - elapsed;
}
/*
* This is called by the cell reading thread in ConnectionImpl to deliver control cells
* associated with this circuit (CREATED or CREATED_FAST).
*/
void deliverControlCell(Cell cell) {
status.updateDirtyTimestamp();
controlCellResponseQueue.add(cell);
}
/* This is called by the cell reading thread in ConnectionImpl to deliver RELAY cells. */
void deliverRelayCell(Cell cell) {
status.updateDirtyTimestamp();
final RelayCell relayCell = decryptRelayCell(cell);
logger.debug("Dispatching: "+ relayCell);
switch(relayCell.getRelayCommand()) {
case RelayCell.RELAY_EXTENDED:
case RelayCell.RELAY_RESOLVED:
case RelayCell.RELAY_TRUNCATED:
relayCellResponseQueue.add(relayCell);
break;
case RelayCell.RELAY_DATA:
case RelayCell.RELAY_END:
case RelayCell.RELAY_CONNECTED:
processRelayDataCell(relayCell);
break;
case RelayCell.RELAY_SENDME:
if(relayCell.getStreamId() != 0)
processRelayDataCell(relayCell);
else
processCircuitSendme(relayCell);
break;
case RelayCell.RELAY_BEGIN:
case RelayCell.RELAY_BEGIN_DIR:
case RelayCell.RELAY_EXTEND:
case RelayCell.RELAY_RESOLVE:
case RelayCell.RELAY_TRUNCATE:
destroyCircuit();
throw new TorException("Unexpected 'forward' direction relay cell type: "+ relayCell.getRelayCommand());
}
}
/* Runs in the context of the connection cell reading thread */
private void processRelayDataCell(RelayCell cell) {
if(cell.getRelayCommand() == RelayCell.RELAY_DATA) {
cell.getCircuitNode().decrementDeliverWindow();
if(cell.getCircuitNode().considerSendingSendme()) {
final RelayCell sendme = createRelayCell(RelayCell.RELAY_SENDME, 0, cell.getCircuitNode());
sendRelayCell(sendme);
}
}
synchronized(streamMap) {
final StreamImpl stream = streamMap.get(cell.getStreamId());
// It's not unusual for the stream to not be found. For example, if a RELAY_CONNECTED arrives after
// the client has stopped waiting for it, the stream will never be tracked and eventually the edge node
// will send a RELAY_END for this stream.
if(stream != null)
stream.addInputCell(cell);
else
logger.debug("Stream not found for stream id="+ cell.getStreamId());
}
}
private void processCircuitSendme(RelayCell cell) {
cell.getCircuitNode().incrementSendWindow();
}
public OpenStreamResponse openDirectoryStream() {
return null;
}
public OpenStreamResponse openExitStream(IPv4Address address, int port) {
return openExitStream(address.toString(), port);
}
public OpenStreamResponse openExitStream(String target, int port) {
final StreamImpl stream = createNewStream();
final OpenStreamResponse response = stream.openExit(target, port);
switch(response.getStatus()) {
case STATUS_STREAM_TIMEOUT:
logger.info("Timeout opening stream: "+ stream);
if(status.countStreamTimeout()) {
// XXX do something
}
removeStream(stream);
break;
case STATUS_STREAM_ERROR:
logger.info("Error opening stream: "+ stream +" reason: "+ response.getErrorCodeMessage());
removeStream(stream);
break;
}
return response;
}
boolean isFinalNodeDirectory() {
return getFinalCircuitNode().getRouter().getDirectoryPort() != 0;
}
void destroyCircuit() {
status.setStateDestroyed();
entryConnection.removeCircuit(this);
synchronized(streamMap) {
final List<StreamImpl> tmpList = new ArrayList<StreamImpl>(streamMap.values());
for(StreamImpl s: tmpList)
s.close();
}
circuitManager.circuitInactive(this);
}
private StreamImpl createNewStream() {
synchronized(streamMap) {
final int streamId = status.nextStreamId();
final StreamImpl stream = new StreamImpl(this, getFinalCircuitNode(), streamId);
streamMap.put(streamId, stream);
return stream;
}
}
void removeStream(StreamImpl stream) {
synchronized(streamMap) {
streamMap.remove(stream.getStreamId());
}
}
public void recordFailedExitTarget(ExitTarget target) {
synchronized(failedExitRequests) {
failedExitRequests.add(target);
}
}
public boolean canHandleExitTo(ExitTarget target) {
synchronized(failedExitRequests) {
if(failedExitRequests.contains(target))
return false;
}
final Router lastRouter = status.getFinalRouter();
if(target.isAddressTarget())
return lastRouter.exitPolicyAccepts(target.getAddress(), target.getPort());
else
return lastRouter.exitPolicyAccepts(target.getPort());
}
public String toString() {
return "Circuit id="+ circuitId +" state=" + status.getStateAsString() +" "+ pathToString();
}
private String pathToString() {
final StringBuilder sb = new StringBuilder();
sb.append("[");
for(CircuitNode node: nodeList) {
if(sb.length() > 1)
sb.append(",");
sb.append(node.toString());
}
sb.append("]");
return sb.toString();
}
}