Orchid 1.0.0 from website:

http://www.subgraph.com/orchid.html ,
"Orchid is licensed under a three-clause BSD license."
No modifications.

Selected Apache XMLRPC 3.1.3 jars from:
http://ws.apache.org/xmlrpc/download.html
Apache 2.0 license

Todo:
https, socks
Set data directory
Logging to router logs?
jsp status/control
This commit is contained in:
zzz
2014-01-05 00:33:59 +00:00
commit 7341a2a4e9
251 changed files with 25679 additions and 0 deletions

3
LICENSE.txt Normal file
View File

@ -0,0 +1,3 @@
I2P license: TBD
Bundled software: see plugins/licenses

3
README.txt Normal file
View File

@ -0,0 +1,3 @@
This is a bundling of the orchid Java Tor client into an I2P plugin.
See http://www.subgraph.com/orchid.html for more information.

80
build.xml Normal file
View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project basedir="." default="all" name="orchid">
<target name="all" depends="clean,plugin" />
<property name="xmlrpc.version" value="3.1.3" />
<target name="war" >
<ant dir="src" target="build" />
</target>
<target name="plugin" depends="war">
<delete file="plugin/i2ptunnel.config" />
<!-- get version number -->
<buildnumber file="scripts/build.number" />
<property name="release.number" value="1.0.0-0.1" />
<!-- make the update xpi2p -->
<copy file="LICENSE.txt" todir="plugin/" overwrite="true" />
<copy file="README.txt" todir="plugin/" overwrite="true" />
<copy file="scripts/plugin.config" todir="plugin/" overwrite="true" />
<exec executable="echo" osfamily="unix" failonerror="true" output="plugin/plugin.config" append="true">
<arg value="update-only=true" />
</exec>
<exec executable="echo" osfamily="unix" failonerror="true" output="plugin/plugin.config" append="true">
<arg value="version=${release.number}-b${build.number}" />
</exec>
<exec executable="pack200" failonerror="true">
<arg value="-g" />
<arg value="plugin/lib/orchid.jar.pack" />
<arg value="src/build/orchid.jar" />
</exec>
<exec executable="pack200" failonerror="true">
<arg value="-g" />
<arg value="plugin/console/webapps/orchid.war.pack" />
<arg value="src/build/orchid.war.jar" />
</exec>
<exec executable="pack200" failonerror="true">
<arg value="-g" />
<arg value="plugin/lib/xmlrpc-client.jar.pack" />
<arg value="lib/xmlrpc-client-${xmlrpc.version}.jar" />
</exec>
<exec executable="pack200" failonerror="true">
<arg value="-g" />
<arg value="plugin/lib/xmlrpc-common.jar.pack" />
<arg value="lib/xmlrpc-common-${xmlrpc.version}.jar" />
</exec>
<exec executable="scripts/makeplugin.sh" failonerror="true" >
<arg value="plugin" />
</exec>
<move file="orchid.xpi2p" tofile="orchid-update.xpi2p" overwrite="true" />
<!-- make the install xpi2p -->
<copy file="scripts/i2ptunnel.config" todir="plugin/" overwrite="true" />
<copy file="scripts/plugin.config" todir="plugin/" overwrite="true" />
<exec executable="echo" osfamily="unix" failonerror="true" output="plugin/plugin.config" append="true">
<arg value="version=${release.number}-b${build.number}" />
</exec>
<exec executable="scripts/makeplugin.sh" failonerror="true" >
<arg value="plugin" />
</exec>
</target>
<target name="distclean" depends="clean" />
<target name="clean" >
<ant dir="src" target="clean" />
<delete file="plugin/i2ptunnel.config" />
<delete file="plugin/plugin.config" />
<delete file="plugin/lib/orchid.jar.pack" />
<delete file="plugin/lib/xmlrpc-common.jar.pack" />
<delete file="plugin/lib/xmlrpc-client.jar.pack" />
<delete file="plugin/console/webapps/orchid.war.pack" />
<delete file="plugin/LICENSE.txt" />
<delete file="plugin/README.txt" />
<delete file="orchid.xpi2p" />
<delete file="orchid-update.xpi2p" />
</target>
</project>

BIN
lib/xmlrpc-client-3.1.3.jar Normal file

Binary file not shown.

BIN
lib/xmlrpc-common-3.1.3.jar Normal file

Binary file not shown.

6
plugin/clients.config Normal file
View File

@ -0,0 +1,6 @@
clientApp.0.main=net.i2p.orchid.OrchidController
clientApp.0.name=Orchid
clientApp.0.args=$PLUGIN
clientApp.0.delay=60
clientApp.0.startOnLoad=true
clientApp.0.classpath=$PLUGIN/lib/orchid.jar,$PLUGIN/lib/xmlrpc-commons.jar,$PLUGIND/lib/xmlrpc-client.jar

View File

@ -0,0 +1,3 @@
According to http://www.subgraph.com/orchid.html ,
"Orchid is licensed under a three-clause BSD license."

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@ -0,0 +1,5 @@
Apache XML-RPC
Copyright 1999-2009 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).

1
plugin/webapps.config Normal file
View File

@ -0,0 +1 @@
webapps.orchid.classpath=$I2P/lib/orchid.jar,$I2P/lib/xmlrpc-commons.jar,$PLUGIN/lib/xmlrpc-client.jar

104
scripts/makeplugin.sh Executable file
View File

@ -0,0 +1,104 @@
#!/bin/sh
#
# basic packaging up of a plugin
#
# usage: makeplugin.sh plugindir
#
# zzz 2010-02
#
PUBKEYDIR=$HOME/.i2p-plugin-keys
PUBKEYFILE=$PUBKEYDIR/plugin-public-signing.key
PRIVKEYFILE=$PUBKEYDIR/plugin-private-signing.key
B64KEYFILE=$PUBKEYDIR/plugin-public-signing.txt
export I2P=../i2p/pkg-temp
PLUGINDIR=${1:-plugin}
PC=plugin.config
PCT=${PC}.tmp
if [ ! -f $PRIVKEYFILE ]
then
mkdir -p $PUBKEYDIR
java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate keygen $PUBKEYFILE $PRIVKEYFILE || exit 1
java -cp $I2P/lib/i2p.jar net.i2p.data.Base64 encode $PUBKEYFILE $B64KEYFILE || exit 1
rm -rf logs/
chmod 444 $PUBKEYFILE $B64KEYFILE
chmod 400 $PRIVKEYFILE
echo "Created new keys: $PUBKEYFILE $PRIVKEYFILE"
fi
rm -f plugin.zip
if [ ! -d $PLUGINDIR ]
then
echo "You must have a $PLUGINDIR directory"
exit 1
fi
OPWD=$PWD
cd $PLUGINDIR
if [ ! -f $PC ]
then
echo "You must have a $PC file"
exit 1
fi
grep -q '^signer=' $PC
if [ "$?" -ne "0" ]
then
echo "You must have a signer in $PC"
echo 'For example signer=joe@mail.i2p'
exit 1
fi
grep -q '^name=' $PC
if [ "$?" -ne "0" ]
then
echo "You must have a plugin name in $PC"
echo 'For example name=foo'
exit 1
fi
grep -q '^version=' $PC
if [ "$?" -ne "0" ]
then
echo "You must have a version in $PC"
echo 'For example version=0.1.2'
exit 1
fi
# update the date
grep -v '^date=' $PC > $PCT
DATE=`date '+%s000'`
echo "date=$DATE" >> $PCT
mv $PCT $PC
# add our Base64 key
grep -v '^key=' $PC > $PCT
B64KEY=`cat $B64KEYFILE`
echo "key=$B64KEY" >> $PCT || exit 1
mv $PCT $PC
# zip it
zip -r $OPWD/plugin.zip * -x \*.jar || exit 1
# get the version and use it for the sud header
VERSION=`grep '^version=' $PC | cut -f 2 -d '='`
# get the name and use it for the file name
NAME=`grep '^name=' $PC | cut -f 2 -d '='`
XPI2P=${NAME}.xpi2p
cd $OPWD
# sign it
java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate sign plugin.zip $XPI2P $PRIVKEYFILE $VERSION || exit 1
rm -f plugin.zip
# verify
echo 'Verifying. ...'
java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate showversion $XPI2P || exit 1
java -cp $I2P/lib/i2p.jar -Drouter.trustedUpdateKeys=$B64KEY net.i2p.crypto.TrustedUpdate verifysig $XPI2P || exit 1
rm -rf logs/
echo -n 'Plugin created: '
wc -c $XPI2P

14
scripts/plugin.config Normal file
View File

@ -0,0 +1,14 @@
name=orchid
signer=zzz-plugin@mail.i2p
consoleLinkName=Orchid
consoleLinkURL=/orchid
description=Tor Outproxy
author=zzz
updateURL=http://stats.i2p/i2p/plugins/orchid-update.xpi2p
websiteURL=http://zzz.i2p/forums/16
license=BSD
min-jetty-version=7
# TODO higher for hook
min-i2p-version=0.9.9
# ???
min-java-version=1.5

1
scripts/runtor Executable file
View File

@ -0,0 +1 @@
java -cp ../src/build/orchid.jar com.subgraph.orchid.TorClient

99
src/build.xml Normal file
View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="all" name="i2psnark">
<property name="i2pbase" value="../../i2p.i2p.zzz.outproxy"/>
<property name="i2plib" value="${i2pbase}/build"/>
<property name="jettylib" value="${i2pbase}/apps/jetty/jettylib"/>
<path id="cp">
<pathelement path="${java.class.path}" />
<pathelement location="${i2plib}/i2p.jar" />
<pathelement location="${ant.home}/lib/ant.jar"/>
<pathelement location="${jettylib}/org.mortbay.jetty.jar"/>
<pathelement location="${jettylib}/jasper-runtime.jar" />
<pathelement location="${jettylib}/javax.servlet.jar" />
<pathelement location="${jettylib}/jetty-util.jar" />
<pathelement location="${jettylib}/jetty-xml.jar" />
<pathelement location="${jettylib}/commons-logging.jar" />
<pathelement location="${jettylib}/commons-el.jar" />
<pathelement location="../lib/xmlrpc-client-${xmlrpc.version}.jar" />
<pathelement location="../lib/xmlrpc-common-${xmlrpc.version}.jar" />
</path>
<target name="all" depends="clean, build" />
<target name="build" depends="jar, war" />
<target name="builddep">
</target>
<property name="javac.compilerargs" value="" />
<target name="compile">
<mkdir dir="./build" />
<mkdir dir="./build/obj" />
<javac
srcdir="./java"
debug="true" deprecation="on" source="1.5" target="1.5"
destdir="./build/obj"
includeAntRuntime="false"
classpath="${i2plib}/i2p.jar:${i2plib}/i2ptunnel.jar:${i2plib}/i2psnark.jar:${i2plib}/mstreaming.jar:${i2plib}/systray.jar:${jettylib}/org.mortbay.jetty.jar:${jettylib}/jetty-util.jar:${jettylib}/jetty-xml.jar:../lib/xmlrpc-client-${xmlrpc.version}.jar:../lib/xmlrpc-common-${xmlrpc.version}.jar" >
<compilerarg line="${javac.compilerargs}" />
</javac>
</target>
<target name="jar" depends="builddep, compile">
<jar destfile="build/orchid.jar" basedir="./build/obj" includes="**/*.class" >
</jar>
</target>
<target name="precompilejsp" depends="compile" >
<mkdir dir="build" />
<mkdir dir="build/war/WEB-INF/classes" />
<path id="jspcp">
<path refid="cp" />
<pathelement location="build/obj" />
</path>
<java classname="org.apache.jasper.JspC" fork="true" classpathref="jspcp" failonerror="true">
<arg value="-d" />
<arg value="build/jspjava" />
<arg value="-v" />
<arg value="-p" />
<arg value="net.i2p.orchid" />
<arg value="-webinc" />
<arg value="build/web-fragment.xml" />
<arg value="-webapp" />
<arg value="jsp/" />
</java>
<javac
debug="true"
deprecation="on"
source="1.5" target="1.5"
destdir="build/war/WEB-INF/classes"
srcdir="./build/jspjava"
includes="**/*.java"
includeAntRuntime="false"
classpathref="jspcp"
failonerror="true" />
<copy file="jsp/WEB-INF/web.xml" tofile="build/web.xml" />
<loadfile property="jspc.web.fragment" srcfile="build/web-fragment.xml" />
<replace file="build/web.xml">
<replacefilter token="&lt;!-- precompiled servlets --&gt;" value="${jspc.web.fragment}" />
</replace>
</target>
<target name="war" depends="precompilejsp">
<copy file="jsp/index.html" todir="build/war" />
<war destfile="build/orchid.war.jar" webxml="build/web.xml">
<fileset dir="build/war" />
</war>
</target>
<target name="clean">
<delete dir="./build" />
</target>
<target name="cleandep" depends="clean">
</target>
<target name="distclean" depends="clean">
</target>
</project>

View File

@ -0,0 +1,8 @@
package com.subgraph.orchid;
import com.subgraph.orchid.data.HexDigest;
public interface BridgeRouter extends Router {
void setIdentity(HexDigest identity);
void setDescriptor(RouterDescriptor descriptor);
}

View File

@ -0,0 +1,230 @@
package com.subgraph.orchid;
public interface Cell {
/** Command constant for a PADDING type cell. */
final static int PADDING = 0;
/** Command constant for a CREATE type cell. */
final static int CREATE = 1;
/** Command constant for a CREATED type cell. */
final static int CREATED = 2;
/** Command constant for a RELAY type cell. */
final static int RELAY = 3;
/** Command constant for a DESTROY type cell. */
final static int DESTROY = 4;
/** Command constant for a CREATE_FAST type cell. */
final static int CREATE_FAST = 5;
/** Command constant for a CREATED_FAST type cell. */
final static int CREATED_FAST = 6;
/** Command constant for a VERSIONS type cell. */
final static int VERSIONS = 7;
/** Command constant for a NETINFO type cell. */
final static int NETINFO = 8;
/** Command constant for a RELAY_EARLY type cell. */
final static int RELAY_EARLY = 9;
final static int VPADDING = 128;
final static int CERTS = 129;
final static int AUTH_CHALLENGE = 130;
final static int AUTHENTICATE = 131;
final static int AUTHORIZE = 132;
final static int ERROR_NONE = 0;
final static int ERROR_PROTOCOL = 1;
final static int ERROR_INTERNAL = 2;
final static int ERROR_REQUESTED = 3;
final static int ERROR_HIBERNATING = 4;
final static int ERROR_RESOURCELIMIT = 5;
final static int ERROR_CONNECTFAILED = 6;
final static int ERROR_OR_IDENTITY = 7;
final static int ERROR_OR_CONN_CLOSED = 8;
final static int ERROR_FINISHED = 9;
final static int ERROR_TIMEOUT = 10;
final static int ERROR_DESTROYED = 11;
final static int ERROR_NOSUCHSERVICE = 12;
final static int ADDRESS_TYPE_HOSTNAME = 0x00;
final static int ADDRESS_TYPE_IPV4 = 0x04;
final static int ADRESS_TYPE_IPV6 = 0x06;
/**
* The fixed size of a standard cell.
*/
final static int CELL_LEN = 512;
/**
* The length of a standard cell header.
*/
final static int CELL_HEADER_LEN = 3;
/**
* The header length for a variable length cell (ie: VERSIONS)
*/
final static int CELL_VAR_HEADER_LEN = 5;
/**
* The length of the payload space in a standard cell.
*/
final static int CELL_PAYLOAD_LEN = CELL_LEN - CELL_HEADER_LEN;
/**
* Return the circuit id field from this cell.
*
* @return The circuit id field of this cell.
*/
int getCircuitId();
/**
* Return the command field from this cell.
*
* @return The command field of this cell.
*/
int getCommand();
/**
* Set the internal pointer to the first byte after the cell header.
*/
void resetToPayload();
/**
* Return the next byte from the cell and increment the internal pointer by one byte.
*
* @return The byte at the current pointer location.
*/
int getByte();
/**
* Return the byte at the specified offset into the cell.
*
* @param index The cell offset.
* @return The byte at the specified offset.
*/
int getByteAt(int index);
/**
* Return the next 16-bit big endian value from the cell and increment the internal pointer by two bytes.
*
* @return The 16-bit short value at the current pointer location.
*/
int getShort();
/**
* Return the 16-bit big endian value at the specified offset into the cell.
*
* @param index The cell offset.
* @return The 16-bit short value at the specified offset.
*/
int getShortAt(int index);
/**
* Return the next 32-bit big endian value from the cell and increment the internal pointer by four bytes.
*
* @return The 32-bit integer value at the current pointer location.
*/
int getInt();
/**
* Copy <code>buffer.length</code> bytes from the cell into <code>buffer</code>. The data is copied starting
* from the current internal pointer location and afterwards the internal pointer is incremented by <code>buffer.length</code>
* bytes.
*
* @param buffer The array of bytes to copy the cell data into.
*/
void getByteArray(byte[] buffer);
/**
* Return the number of bytes already packed (for outgoing cells) or unpacked (for incoming cells). This is
* equivalent to the internal pointer position.
*
* @return The number of bytes already consumed from this cell.
*/
int cellBytesConsumed();
/**
* Return the number of bytes remaining between the current internal pointer and the end of the cell. If fields
* are being added to a new cell for transmission then this value indicates the remaining space in bytes for
* adding new data. If fields are being read from a received cell then this value describes the number of bytes
* which can be read without overflowing the cell.
*
* @return The number of payload bytes remaining in this cell.
*/
int cellBytesRemaining();
/**
* Store a byte at the current pointer location and increment the pointer by one byte.
*
* @param value The byte value to store.
*/
void putByte(int value);
/**
* Store a byte at the specified offset into the cell.
*
* @param index The offset in bytes into the cell.
* @param value The byte value to store.
*/
void putByteAt(int index, int value);
/**
* Store a 16-bit short value in big endian order at the current pointer location and
* increment the pointer by two bytes.
*
* @param value The 16-bit short value to store.
*/
void putShort(int value);
/**
* Store a 16-bit short value in big endian byte order at the specified offset into the cell
* and increment the pointer by two bytes.
*
* @param index The offset in bytes into the cell.
* @param value The 16-bit short value to store.
*/
void putShortAt(int index, int value);
/**
* Store a 32-bit integer value in big endian order at the current pointer location and
* increment the pointer by 4 bytes.
*
* @param value The 32-bit integer value to store.
*/
void putInt(int value);
/**
* Store the entire array <code>data</code> at the current pointer location and increment
* the pointer by <code>data.length</code> bytes.
*
* @param data The array of bytes to store in the cell.
*/
void putByteArray(byte[] data);
/**
* Store <code>length</code> bytes of the byte array <code>data</code> starting from
* <code>offset</code> into the array at the current pointer location and increment
* the pointer by <code>length</code> bytes.
*
* @param data The source array of bytes.
* @param offset The offset into the source array.
* @param length The number of bytes from the source array to store.
*/
void putByteArray(byte[] data, int offset, int length);
/**
* Return the entire cell data as a raw array of bytes. For all cells except
* <code>VERSIONS</code>, this array will be exactly <code>CELL_LEN</code> bytes long.
*
* @return The cell data as an array of bytes.
*/
byte[] getCellBytes();
void putString(String string);
}

View File

@ -0,0 +1,95 @@
package com.subgraph.orchid;
import java.util.List;
/**
* A Circuit represents a logical path through multiple ORs. Circuits are described in
* section 5 of tor-spec.txt.
*
*/
public interface Circuit {
/**
* Return <code>true</code> if the circuit is presently in the connected state or
* <code>false</code> otherwise.
*
* @return Returns <code>true</code> if the circuit is presently connected, or
* <code>false</code> otherwise.
*/
boolean isConnected();
boolean isPending();
boolean isClean();
boolean isMarkedForClose();
int getSecondsDirty();
/**
* Returns the entry router <code>Connection</code> object of this Circuit. Throws
* a TorException if the circuit is not currently open.
*
* @return The Connection object for the network connection to the entry router of this
* circuit.
* @throws TorException If this circuit is not currently connected.
*/
Connection getConnection();
/**
* Returns the curcuit id value for this circuit.
*
* @return The circuit id value for this circuit.
*/
int getCircuitId();
/**
* Create a new relay cell which is configured for delivery to the specified
* circuit <code>targetNode</code> with command value <code>relayCommand</code>
* and a stream id value of <code>streamId</code>. The returned <code>RelayCell</code>
* can then be used to populate the payload of the cell before delivering it.
*
* @param relayCommand The command value to send in the relay cell header.
* @param streamId The stream id value to send in the relay cell header.
* @param targetNode The target circuit node to encrypt this cell for.
* @return A newly created relay cell object.
*/
RelayCell createRelayCell(int relayCommand, int streamId, CircuitNode targetNode);
/**
* Returns the next relay response cell received on this circuit. If no response is
* received within <code>CIRCUIT_RELAY_RESPONSE_TIMEOUT</code> milliseconds, <code>null</code>
* is returned.
*
* @return The next relay response cell received on this circuit or <code>null</code> if
* a timeout is reached before the next relay cell arrives.
*/
RelayCell receiveRelayCell();
/**
* Encrypt and deliver the relay cell <code>cell</code>.
*
* @param cell The relay cell to deliver over this circuit.
*/
void sendRelayCell(RelayCell cell);
/**
* Return the last node or 'hop' in this circuit.
*
* @return The final 'hop' or node of this circuit.
*/
CircuitNode getFinalCircuitNode();
void destroyCircuit();
void deliverRelayCell(Cell cell);
void deliverControlCell(Cell cell);
List<Stream> getActiveStreams();
void markForClose();
void appendNode(CircuitNode node);
}

View File

@ -0,0 +1,60 @@
package com.subgraph.orchid;
/**
* This callback interface is used for reporting progress when
* opening a new circuit. An instance of this interface is passed
* to the {@link Circuit#openCircuit(java.util.List, CircuitBuildHandler)}
* method.
*
* The normal sequence of callbacks which are fired when a circuit is opened
* successfully is {@link #connectionCompleted(Connection)} for the initial
* connection to the entry router, followed by one or more
* {@link #nodeAdded(CircuitNode)} as the circuit is extended with new nodes.
* When all requested nodes in the path have been added successfully to the
* circuit {@link #circuitBuildCompleted(Circuit)} is called and passed the
* newly constructed circuit.
*
* @see Circuit#openCircuit()
*
*/
public interface CircuitBuildHandler {
/**
* Called when a network connection to the entry node has completed
* successfully or if a network connection to the specified entry router
* already exists.
*
* @param connection The completed connection instance.
*/
void connectionCompleted(Connection connection);
/**
* The circuit build has failed because the network connection to the
* entry node failed. No further callback methods will be called after
* this failure has been reported.
*
* @param reason A description of the reason for failing to connect to
* the entry node.
*/
void connectionFailed(String reason);
/**
* A node or 'hop' has been added to the circuit which is being created.
*
* @param node The newly added circuit node.
*/
void nodeAdded(CircuitNode node);
/**
* The circuit has been successfully built and is ready for use.
*
* @param circuit The newly constructed circuit.
*/
void circuitBuildCompleted(Circuit circuit);
/**
* Called if the circuit build fails after connecting to the entry node.
*
* @param reason A description of the reason the circuit build has failed.
*/
void circuitBuildFailed(String reason);
}

View File

@ -0,0 +1,51 @@
package com.subgraph.orchid;
import java.util.List;
import java.util.concurrent.TimeoutException;
import com.subgraph.orchid.data.IPv4Address;
public interface CircuitManager {
static int DIRECTORY_PURPOSE_CONSENSUS = 1;
static int DIRECTORY_PURPOSE_CERTIFICATES = 2;
static int DIRECTORY_PURPOSE_DESCRIPTORS = 3;
/**
* Begin automatically building new circuits in the background.
*/
void startBuildingCircuits();
void stopBuildingCircuits(boolean killCircuits);
/**
* Attempt to open an exit stream to the specified destination <code>hostname</code> and
* <code>port</code>.
*
* @param hostname The name of the host to open an exit connection to.
* @param port The port to open an exit connection to.
* @return The status response result of attempting to open the exit connection.
*/
Stream openExitStreamTo(String hostname, int port) throws InterruptedException, TimeoutException, OpenFailedException;
/**
* Attempt to open an exit stream to the destination specified by <code>address</code> and
* <code>port</code>.
*
* @param address The address to open an exit connection to.
* @param port The port to open an exit connection to.
* @return The status response result of attempting the open the exit connection.
*/
Stream openExitStreamTo(IPv4Address address, int port) throws InterruptedException, TimeoutException, OpenFailedException;
Stream openDirectoryStream(int purpose) throws InterruptedException, TimeoutException, OpenFailedException;
Stream openDirectoryStream() throws InterruptedException, TimeoutException, OpenFailedException;
DirectoryCircuit openDirectoryCircuit() throws OpenFailedException;
Circuit getCleanInternalCircuit() throws InterruptedException;
ExitCircuit openExitCircuitTo(List<Router> path) throws OpenFailedException;
InternalCircuit openInternalCircuitTo(List<Router> path) throws OpenFailedException;
DirectoryCircuit openDirectoryCircuitTo(List<Router> path) throws OpenFailedException;
}

View File

@ -0,0 +1,90 @@
package com.subgraph.orchid;
/**
* Represents the state of a single onion router hop in a connected or connecting {@link Circuit}
*/
public interface CircuitNode {
/**
* Return the {@link Router} associated with this node.
*
* @return The {@link Router} for this hop of the circuit chain.
*/
Router getRouter();
/**
* Update the 'forward' cryptographic digest state for this
* node with the contents of <code>cell</code>
*
* @param cell The {@link RelayCell} to add to the digest.
*/
void updateForwardDigest(RelayCell cell);
/**
* Return the current 'forward' running digest value for this
* node as an array of <code>TOR_DIGEST_SIZE</code> bytes.
*
* @return The current 'forward' running digest value for this node.
*/
byte[] getForwardDigestBytes();
/**
* Encrypt a {@link RelayCell} for this node with the current
* 'forward' cipher state.
*
* @param cell The {@link RelayCell} to encrypt.
*/
void encryptForwardCell(RelayCell cell);
/**
* Return the {@link CircuitNode} which immediately preceeds this
* one in the circuit node chain or <code>null</code> if this is
* the first hop.
*
* @return The previous {@link CircuitNode} in the chain or <code>
* null</code> if this is the first node.
*/
CircuitNode getPreviousNode();
/**
* Return immediately if the packaging window for this node is open (ie: greater than 0), otherwise
* block until the circuit is destroyed or the window is incremented by receiving a RELAY_SENDME cell
* from this node.
*/
void waitForSendWindow();
/**
* If the packaging window for this node is open (ie: greater than 0) this method
* decrements the packaging window by 1 and returns immediately, otherwise it will
* block until the circuit is destroyed or the window is incremented by receiving
* a RELAY_SENDME cell from this node. This method will always decrement the packaging
* window before returning unless the circuit has been destroyed.
*/
void waitForSendWindowAndDecrement();
/**
* This method is called to signal that a RELAY_SENDME cell has been received from this
* node and the packaging window should be incremented. This will also wake up any threads
* that are waiting for the packaging window to open.
*/
void incrementSendWindow();
/**
* This method is called when a RELAY_DATA cell is received from this node to decrement
* the deliver window counter.
*/
void decrementDeliverWindow();
/**
* Examines the delivery window and determines if it would be an appropriate time to
* send a RELAY_SENDME cell. If this method returns true, it increments the delivery
* window assuming that a RELAY_SENDME cell will be transmitted.
*
* @return Returns true if the deliver window is small enough that sending a RELAY_SENDME
* cell would be appropriate.
*/
boolean considerSendingSendme();
boolean decryptBackwardCell(Cell cell);
}

View File

@ -0,0 +1,47 @@
package com.subgraph.orchid;
/**
* A network connection to a Tor onion router.
*/
public interface Connection {
/**
* Return the {@link Router} associated with this connection.
*
* @return The entry router this connection represents.
*/
Router getRouter();
/**
* Return <code>true</code> if the socket for this connection has been closed. Otherwise, <code>false</code>.
*
* @return <code>true</code> if this connection is closed or <code>false</code> otherwise.
*/
boolean isClosed();
/**
* Send a protocol {@link Cell} on this connection.
*
* @param cell The {@link Cell} to transfer.
* @throws ConnectionIOException If the cell could not be send because the connection is not connected
* or if an error occured while sending the cell data.
*/
void sendCell(Cell cell) throws ConnectionIOException;
/**
* Remove a Circuit which has been bound to this Connection by a previous call to {@link #bindCircuit(Circuit) bindCircuit}.
* After removing a Circuit, any further received incoming cells for the Circuit will be discarded.
*
* @param circuit The Circuit to remove.
*/
void removeCircuit(Circuit circuit);
/**
* Choose an available circuit id value and bind this Circuit to that id value, returning the id value.
* Once bound, any incoming relay cells will be delivered to the Circuit with {@link Circuit#deliverRelayCell(Cell)}
* and other cells will be delivered with {@link Circuit#deliverControlCell(Cell)}.
*
* @param circuit The Circuit to bind to this connection.
* @return the circuit id value for this binding.
*/
int bindCircuit(Circuit circuit);
}

View File

@ -0,0 +1,21 @@
package com.subgraph.orchid;
public interface ConnectionCache {
/**
* Returns a completed connection to the specified router. If an open connection
* to the requested router already exists it is returned, otherwise a new connection
* is opened.
*
* @param router The router to which a connection is requested.
* @param isDirectoryConnection Is this going to be used as a directory connection.
* @return a completed connection to the specified router.
* @throws InterruptedException if thread is interrupted while waiting for connection to complete.
* @throws ConnectionTimeoutException if timeout expires before connection completes.
* @throws ConnectionFailedException if connection fails due to I/O error
* @throws ConnectionHandshakeException if connection fails because an error occurred during handshake phase
*/
Connection getConnectionTo(Router router, boolean isDirectoryConnection) throws InterruptedException, ConnectionTimeoutException, ConnectionFailedException, ConnectionHandshakeException;
void close();
}

View File

@ -0,0 +1,11 @@
package com.subgraph.orchid;
public class ConnectionFailedException extends ConnectionIOException {
private static final long serialVersionUID = -4484347156587613574L;
public ConnectionFailedException(String message) {
super(message);
}
}

View File

@ -0,0 +1,10 @@
package com.subgraph.orchid;
public class ConnectionHandshakeException extends ConnectionIOException {
private static final long serialVersionUID = -2544633445932967966L;
public ConnectionHandshakeException(String message) {
super(message);
}
}

View File

@ -0,0 +1,14 @@
package com.subgraph.orchid;
public class ConnectionIOException extends Exception {
private static final long serialVersionUID = -5537650738995969203L;
public ConnectionIOException() {
super();
}
public ConnectionIOException(String message) {
super(message);
}
}

View File

@ -0,0 +1,14 @@
package com.subgraph.orchid;
public class ConnectionTimeoutException extends ConnectionIOException {
private static final long serialVersionUID = -6098661610150140151L;
public ConnectionTimeoutException() {
super();
}
public ConnectionTimeoutException(String message) {
super(message);
}
}

View File

@ -0,0 +1,44 @@
package com.subgraph.orchid;
import java.util.List;
import java.util.Set;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.Timestamp;
public interface ConsensusDocument extends Document {
enum ConsensusFlavor { NS, MICRODESC };
enum SignatureStatus { STATUS_VERIFIED, STATUS_FAILED, STATUS_NEED_CERTS };
interface RequiredCertificate {
int getDownloadFailureCount();
void incrementDownloadFailureCount();
HexDigest getAuthorityIdentity();
HexDigest getSigningKey();
}
ConsensusFlavor getFlavor();
Timestamp getValidAfterTime();
Timestamp getFreshUntilTime();
Timestamp getValidUntilTime();
int getConsensusMethod();
int getVoteSeconds();
int getDistSeconds();
Set<String> getClientVersions();
Set<String> getServerVersions();
boolean isLive();
List<RouterStatus> getRouterStatusEntries();
SignatureStatus verifySignatures();
Set<RequiredCertificate> getRequiredCertificates();
HexDigest getSigningHash();
HexDigest getSigningHash256();
int getCircWindowParameter();
int getWeightScaleParameter();
int getBandwidthWeight(String tag);
boolean getUseNTorHandshake();
}

View File

@ -0,0 +1,65 @@
package com.subgraph.orchid;
import java.util.Set;
import com.subgraph.orchid.crypto.TorPublicKey;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.IPv4Address;
public interface Descriptor extends Document {
enum CacheLocation { NOT_CACHED, CACHED_CACHEFILE, CACHED_JOURNAL }
HexDigest getDescriptorDigest();
void setLastListed(long timestamp);
long getLastListed();
void setCacheLocation(CacheLocation location);
CacheLocation getCacheLocation();
int getBodyLength();
/**
* Return the public key used to encrypt EXTEND cells while establishing
* a circuit through this router.
*
* @return The onion routing protocol key for this router.
*/
TorPublicKey getOnionKey();
byte[] getNTorOnionKey();
/**
* Return the IPv4 address of this router.
*
* @return The IPv4 address of this router.
*/
IPv4Address getAddress();
/**
* Return the port on which this node accepts TLS connections
* for the main OR protocol, or 0 if no router service is advertised.
*
* @return The onion routing port, or 0 if not a router.
*/
int getRouterPort();
Set<String> getFamilyMembers();
/**
* Return true if the exit policy of this router permits connections
* to the specified destination endpoint.
*
* @param address The IPv4 address of the destination.
* @param port The destination port.
*
* @return True if an exit connection to the specified destination is allowed
* or false otherwise.
*/
boolean exitPolicyAccepts(IPv4Address address, int port);
/**
* Return true if the exit policy of this router accepts most connections
* to the specified destination port.
*
* @param port The destination port.
* @return True if an exit connection to the specified destination port is generally allowed
* or false otherwise.
*/
boolean exitPolicyAccepts(int port);
}

View File

@ -0,0 +1,48 @@
package com.subgraph.orchid;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import com.subgraph.orchid.ConsensusDocument.RequiredCertificate;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.events.EventHandler;
/**
*
* Main interface for accessing directory information and interacting
* with directory authorities and caches.
*
*/
public interface Directory {
boolean haveMinimumRouterInfo();
void loadFromStore();
void close();
void waitUntilLoaded();
void storeCertificates();
Collection<DirectoryServer> getDirectoryAuthorities();
DirectoryServer getRandomDirectoryAuthority();
void addCertificate(KeyCertificate certificate);
Set<RequiredCertificate> getRequiredCertificates();
void addRouterMicrodescriptors(List<RouterMicrodescriptor> microdescriptors);
void addRouterDescriptors(List<RouterDescriptor> descriptors);
void addConsensusDocument(ConsensusDocument consensus, boolean fromCache);
ConsensusDocument getCurrentConsensusDocument();
boolean hasPendingConsensus();
void registerConsensusChangedHandler(EventHandler handler);
void unregisterConsensusChangedHandler(EventHandler handler);
Router getRouterByName(String name);
Router getRouterByIdentity(HexDigest identity);
List<Router> getRouterListByNames(List<String> names);
List<Router> getRoutersWithDownloadableDescriptors();
List<Router> getAllRouters();
RouterMicrodescriptor getMicrodescriptorFromCache(HexDigest descriptorDigest);
RouterDescriptor getBasicDescriptorFromCache(HexDigest descriptorDigest);
GuardEntry createGuardEntryFor(Router router);
List<GuardEntry> getGuardEntries();
void removeGuardEntry(GuardEntry entry);
void addGuardEntry(GuardEntry entry);
}

View File

@ -0,0 +1,16 @@
package com.subgraph.orchid;
import java.util.concurrent.TimeoutException;
public interface DirectoryCircuit extends Circuit {
/**
* Open an anonymous connection to the directory service running on the
* final node in this circuit.
*
* @param timeout in milliseconds
* @param autoclose if set to true, closing stream also marks this circuit for close
*
* @return The status response returned by trying to open the stream.
*/
Stream openDirectoryStream(long timeout, boolean autoclose) throws InterruptedException, TimeoutException, StreamConnectFailedException;
}

View File

@ -0,0 +1,27 @@
package com.subgraph.orchid;
import java.util.List;
import java.util.Set;
import com.subgraph.orchid.ConsensusDocument.RequiredCertificate;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.directory.downloader.DirectoryRequestFailedException;
public interface DirectoryDownloader {
void start(Directory directory);
void stop();
RouterDescriptor downloadBridgeDescriptor(Router bridge) throws DirectoryRequestFailedException;
ConsensusDocument downloadCurrentConsensus(boolean useMicrodescriptors) throws DirectoryRequestFailedException;
ConsensusDocument downloadCurrentConsensus(boolean useMicrodescriptors, DirectoryCircuit circuit) throws DirectoryRequestFailedException;
List<KeyCertificate> downloadKeyCertificates(Set<RequiredCertificate> required) throws DirectoryRequestFailedException;
List<KeyCertificate> downloadKeyCertificates(Set<RequiredCertificate> required, DirectoryCircuit circuit) throws DirectoryRequestFailedException;
List<RouterDescriptor> downloadRouterDescriptors(Set<HexDigest> fingerprints) throws DirectoryRequestFailedException;
List<RouterDescriptor> downloadRouterDescriptors(Set<HexDigest> fingerprints, DirectoryCircuit circuit) throws DirectoryRequestFailedException;
List<RouterMicrodescriptor> downloadRouterMicrodescriptors(Set<HexDigest> fingerprints) throws DirectoryRequestFailedException;
List<RouterMicrodescriptor> downloadRouterMicrodescriptors(Set<HexDigest> fingerprints, DirectoryCircuit circuit) throws DirectoryRequestFailedException;
}

View File

@ -0,0 +1,22 @@
package com.subgraph.orchid;
import java.util.List;
import com.subgraph.orchid.data.HexDigest;
/**
* Represents a directory authority server or a directory cache.
*/
public interface DirectoryServer extends Router {
int getDirectoryPort();
boolean isV2Authority();
boolean isV3Authority();
HexDigest getV3Identity();
boolean isHiddenServiceAuthority();
boolean isBridgeAuthority();
boolean isExtraInfoCache();
KeyCertificate getCertificateByFingerprint(HexDigest fingerprint);
List<KeyCertificate> getCertificates();
void addCertificate(KeyCertificate certificate);
}

View File

@ -0,0 +1,36 @@
package com.subgraph.orchid;
import java.nio.ByteBuffer;
import java.util.List;
public interface DirectoryStore {
enum CacheFile {
CERTIFICATES("certificates"),
CONSENSUS("consensus"),
CONSENSUS_MICRODESC("consensus-microdesc"),
MICRODESCRIPTOR_CACHE("cached-microdescs"),
MICRODESCRIPTOR_JOURNAL("cached-microdescs.new"),
DESCRIPTOR_CACHE("cached-descriptors"),
DESCRIPTOR_JOURNAL("cached-descriptors.new"),
STATE("state");
final private String filename;
CacheFile(String filename) {
this.filename = filename;
}
public String getFilename() {
return filename;
}
}
ByteBuffer loadCacheFile(CacheFile cacheFile);
void writeData(CacheFile cacheFile, ByteBuffer data);
void writeDocument(CacheFile cacheFile, Document document);
void writeDocumentList(CacheFile cacheFile, List<? extends Document> documents);
void appendDocumentList(CacheFile cacheFile, List<? extends Document> documents);
void removeCacheFile(CacheFile cacheFile);
void removeAllCacheFiles();
}

View File

@ -0,0 +1,9 @@
package com.subgraph.orchid;
import java.nio.ByteBuffer;
public interface Document {
ByteBuffer getRawDocumentBytes();
String getRawDocumentData();
boolean isValidDocument();
}

View File

@ -0,0 +1,50 @@
package com.subgraph.orchid;
import java.util.concurrent.TimeoutException;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
public interface ExitCircuit extends Circuit {
/**
* Open an exit stream from the final node in this circuit to the
* specified target address and port.
*
* @param address The network address of the exit target.
* @param port The port of the exit target.
* @return The status response returned by trying to open the stream.
*/
Stream openExitStream(IPv4Address address, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException;
/**
* Open an exit stream from the final node in this circuit to the
* specified target hostname and port.
*
* @param hostname The network hostname of the exit target.
* @param port The port of the exit target.
* @return The status response returned by trying to open the stream.
*/
Stream openExitStream(String hostname, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException;
/**
* Return true if the final node of this circuit is believed to be able to connect to
* the specified <code>ExitTarget</code>. Returns false if the target destination is
* not permitted by the exit policy of the final node in this circuit or if the target
* has been previously recorded to have failed through this circuit.
*
* @param target The exit destination.
* @return Return true if is likely that the final node of this circuit can connect to the specified exit target.
*/
boolean canHandleExitTo(ExitTarget target);
boolean canHandleExitToPort(int port);
/**
* Records the specified <code>ExitTarget</code> as a failed connection so that {@link #canHandleExitTo(ExitTarget)} will
* no longer return true for this exit destination.
*
* @param target The <code>ExitTarget</code> to which a connection has failed through this circuit.
*/
public void recordFailedExitTarget(ExitTarget target);
}

View File

@ -0,0 +1,18 @@
package com.subgraph.orchid;
import java.util.Date;
public interface GuardEntry {
boolean isAdded();
void markAsDown();
void clearDownSince();
String getNickname();
String getIdentity();
String getVersion();
Date getCreatedTime();
Date getDownSince();
Date getLastConnectAttempt();
Date getUnlistedSince();
boolean testCurrentlyUsable();
Router getRouterForEntry();
}

View File

@ -0,0 +1,8 @@
package com.subgraph.orchid;
import java.util.concurrent.TimeoutException;
public interface HiddenServiceCircuit extends Circuit {
Stream openStream(int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException;
}

View File

@ -0,0 +1,7 @@
package com.subgraph.orchid;
public interface InternalCircuit extends Circuit {
DirectoryCircuit cannibalizeToDirectory(Router target);
Circuit cannibalizeToIntroductionPoint(Router target);
HiddenServiceCircuit connectHiddenService(CircuitNode node);
}

View File

@ -0,0 +1,78 @@
package com.subgraph.orchid;
import com.subgraph.orchid.crypto.TorPublicKey;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.data.Timestamp;
/**
* This class represents a key certificate document as specified in
* dir-spec.txt (section 3.1). These documents are published by
* directory authorities and bind a long-term identity key to a
* more temporary signing key.
*/
public interface KeyCertificate extends Document {
/**
* Return the network address of this directory authority
* or <code>null</code> if no address was specified in the certificate.
*
* @return The network address of the directory authority this certificate
* belongs to, or <code>null</code> if not available.
*/
IPv4Address getDirectoryAddress();
/**
* Return the port on which this directory authority answers
* directory requests or 0 if no port was specified in the certificate.
*
* @return The port of this directory authority listens on or 0 if
* no port was specified in the certificate.
*/
int getDirectoryPort();
/**
* Return fingerprint of the authority identity key as specified in
* the certificate.
*
* @return The authority identity key fingerprint.
*/
HexDigest getAuthorityFingerprint();
/**
* Return the authority identity public key from the certificate.
*
* @return The authority identity public key.
*/
TorPublicKey getAuthorityIdentityKey();
/**
* Return the authority signing public key from the certificate.
*
* @return The authority signing public key.
*/
TorPublicKey getAuthoritySigningKey();
/**
* Return the time when this document and corresponding keys were
* generated.
*
* @return The time this document was generated and published.
*/
Timestamp getKeyPublishedTime();
/**
* Return the time after which this document and signing key are
* no longer valid.
*
* @return The expiry time of this document and signing key.
*/
Timestamp getKeyExpiryTime();
/**
* Return <code>true</code> if the current time is past the key
* expiry time of this certificate.
*
* @return True if this certificate is currently expired.
*/
boolean isExpired();
}

View File

@ -0,0 +1,13 @@
package com.subgraph.orchid;
public class OpenFailedException extends Exception {
private static final long serialVersionUID = 1989001056577214666L;
public OpenFailedException() {
}
public OpenFailedException(String message) {
super(message);
}
}

View File

@ -0,0 +1,65 @@
package com.subgraph.orchid;
import java.nio.ByteBuffer;
public interface RelayCell extends Cell {
final static int LENGTH_OFFSET = 12;
final static int RECOGNIZED_OFFSET = 4;
final static int DIGEST_OFFSET = 8;
final static int HEADER_SIZE = 14;
final static int RELAY_BEGIN = 1;
final static int RELAY_DATA = 2;
final static int RELAY_END = 3;
final static int RELAY_CONNECTED = 4;
final static int RELAY_SENDME = 5;
final static int RELAY_EXTEND = 6;
final static int RELAY_EXTENDED = 7;
final static int RELAY_TRUNCATE = 8;
final static int RELAY_TRUNCATED = 9;
final static int RELAY_DROP = 10;
final static int RELAY_RESOLVE = 11;
final static int RELAY_RESOLVED = 12;
final static int RELAY_BEGIN_DIR = 13;
final static int RELAY_EXTEND2 = 14;
final static int RELAY_EXTENDED2 = 15;
final static int RELAY_COMMAND_ESTABLISH_INTRO = 32;
final static int RELAY_COMMAND_ESTABLISH_RENDEZVOUS = 33;
final static int RELAY_COMMAND_INTRODUCE1 = 34;
final static int RELAY_COMMAND_INTRODUCE2 = 35;
final static int RELAY_COMMAND_RENDEZVOUS1 = 36;
final static int RELAY_COMMAND_RENDEZVOUS2 = 37;
final static int RELAY_COMMAND_INTRO_ESTABLISHED = 38;
final static int RELAY_COMMAND_RENDEZVOUS_ESTABLISHED = 39;
final static int RELAY_COMMAND_INTRODUCE_ACK = 40;
final static int REASON_MISC = 1;
final static int REASON_RESOLVEFAILED = 2;
final static int REASON_CONNECTREFUSED = 3;
final static int REASON_EXITPOLICY = 4;
final static int REASON_DESTROY = 5;
final static int REASON_DONE = 6;
final static int REASON_TIMEOUT = 7;
final static int REASON_NOROUTE = 8;
final static int REASON_HIBERNATING = 9;
final static int REASON_INTERNAL = 10;
final static int REASON_RESOURCELIMIT = 11;
final static int REASON_CONNRESET = 12;
final static int REASON_TORPROTOCOL = 13;
final static int REASON_NOTDIRECTORY = 14;
int getStreamId();
int getRelayCommand();
/**
* Return the circuit node this cell was received from for outgoing cells or the destination circuit node
* for outgoing cells.
*/
CircuitNode getCircuitNode();
ByteBuffer getPayloadBuffer();
void setLength();
void setDigest(byte[] digest);
}

View File

@ -0,0 +1,35 @@
package com.subgraph.orchid;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class Revision {
private final static String REVISION_FILE_PATH = "/build-revision";
public static String getBuildRevision() {
final InputStream input = tryResourceOpen();
if(input == null) {
return "";
}
try {
return readFirstLine(input);
} catch (IOException e) {
return "";
}
}
private static InputStream tryResourceOpen() {
return Revision.class.getResourceAsStream(REVISION_FILE_PATH);
}
private static String readFirstLine(InputStream input) throws IOException {
try {
final BufferedReader reader = new BufferedReader(new InputStreamReader(input));
return reader.readLine();
} finally {
input.close();
}
}
}

View File

@ -0,0 +1,47 @@
package com.subgraph.orchid;
import java.util.Set;
import com.subgraph.orchid.crypto.TorPublicKey;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.IPv4Address;
public interface Router {
String getNickname();
String getCountryCode();
IPv4Address getAddress();
int getOnionPort();
int getDirectoryPort();
TorPublicKey getIdentityKey();
HexDigest getIdentityHash();
boolean isDescriptorDownloadable();
String getVersion();
Descriptor getCurrentDescriptor();
HexDigest getDescriptorDigest();
HexDigest getMicrodescriptorDigest();
TorPublicKey getOnionKey();
byte[] getNTorOnionKey();
boolean hasBandwidth();
int getEstimatedBandwidth();
int getMeasuredBandwidth();
Set<String> getFamilyMembers();
int getAverageBandwidth();
int getBurstBandwidth();
int getObservedBandwidth();
boolean isHibernating();
boolean isRunning();
boolean isValid();
boolean isBadExit();
boolean isPossibleGuard();
boolean isExit();
boolean isFast();
boolean isStable();
boolean isHSDirectory();
boolean exitPolicyAccepts(IPv4Address address, int port);
boolean exitPolicyAccepts(int port);
}

View File

@ -0,0 +1,164 @@
package com.subgraph.orchid;
import com.subgraph.orchid.crypto.TorPublicKey;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.Timestamp;
import com.subgraph.orchid.data.exitpolicy.ExitPolicy;
/**
* Directory information about a single onion router. This interface
* provides access to the fields of a router descriptor document which
* has been published through to Tor directory system.
*/
public interface RouterDescriptor extends Descriptor {
/**
* Returns the nickname of this router.
*
* @return The nickname of this router.
*/
String getNickname();
/**
* Return the port on which this router provides directory related
* HTTP connections, or 0 if this node does not provide directory
* services.
*
* @return The directory service port, or 0 if not a directory server.
*/
int getDirectoryPort();
/**
* Returns the volume of traffic in bytes per second that this router
* is willing to sustain over long periods.
*
* @return The average bandwidth of this router in bytes per second.
*/
int getAverageBandwidth();
/**
* Returns the volume of traffic in bytes per second that this router
* is willing to sustain in very short intervals.
*
* @return The burst bandwidth of this router in bytes per second.
*/
int getBurstBandwidth();
/**
* Returns the volume of traffic in bytes per second that this router
* is estimated to be able to sustain.
*
* @return The observed bandwidth capacity of this router in bytes per second.
*/
int getObservedBandwidth();
/**
* Return a human-readable string describing the system on which this router
* is running, including possibly the operating system version and Tor
* implementation version.
*
* @return A string describing the platform this router is running on.
*/
String getPlatform();
/**
* Return the time this descriptor was generated.
*
* @return The time this descriptor was generated.
*/
Timestamp getPublishedTime();
/**
* Return a fingerprint of the public key of this router. The fingerprint
* is an optional field, so this method may return null if the descriptor
* of the router did not include the 'fingerprint' field.
*
* @return The fingerprint of this router, or null if no fingerprint is available.
*/
HexDigest getFingerprint();
/**
* Return the number of seconds this router has been running.
*
* @return The number of seconds this router has been running.
*/
int getUptime();
/**
* Return the long-term identity and signing public key for this
* router.
*
* @return The long-term identity and signing public key for this router.
*/
TorPublicKey getIdentityKey();
/**
* Return a string which describes how to contact the server's administrator.
* This is an optional field, so this method will return null if the descriptor
* of this router did not include the 'contact' field.
*
* @return The contact information for this router, or null if not available.
*/
String getContact();
/**
* Return true if this router is currently hibernating and not suitable for
* building new circuits.
*
* @return True if this router is currently hibernating.
*/
boolean isHibernating();
/**
* Returns true if this router stores and serves hidden service descriptors.
*
* @return True if this router is a hidden service directory.
*/
boolean isHiddenServiceDirectory();
/**
* Return true if this router is running a version of Tor which supports the
* newer enhanced DNS logic. If false, this router should be used for reverse
* hostname lookups.
*
* @return True if this router supports newer enhanced DNS logic.
*/
boolean supportsEventDNS();
/**
* Returns true if this router is a directory cache that provides extra-info
* documents.
*
* @return True if this router provides an extra-info document directory service.
*/
boolean cachesExtraInfo();
/**
* Return a digest of this router's extra-info document, or null if not
* available. This is an optional field and will only be present if the
* 'extra-info-digest' field was present in the original router descriptor.
*
* @return The digest of the router extra-info-document, or null if not available.
*/
HexDigest getExtraInfoDigest();
/**
* Return true if this router allows single-hop circuits to make exit connections.
*
* @return True if this router allows single-hop circuits to make exit connections.
*/
boolean allowsSingleHopExits();
/**
* Compare two router descriptors and return true if this router descriptor was published
* at a later time than the <code>other</code> descriptor.
*
* @param other Another router descriptor to compare.
* @return True if this descriptor was published later than <code>other</code>
*/
boolean isNewerThan(RouterDescriptor other);
ExitPolicy getExitPolicy();
}

View File

@ -0,0 +1,6 @@
package com.subgraph.orchid;
public interface RouterMicrodescriptor extends Descriptor {
}

View File

@ -0,0 +1,24 @@
package com.subgraph.orchid;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.data.Timestamp;
import com.subgraph.orchid.data.exitpolicy.ExitPorts;
public interface RouterStatus {
String getNickname();
HexDigest getIdentity();
HexDigest getDescriptorDigest();
HexDigest getMicrodescriptorDigest();
Timestamp getPublicationTime();
IPv4Address getAddress();
int getRouterPort();
boolean isDirectory();
int getDirectoryPort();
boolean hasFlag(String flag);
String getVersion();
boolean hasBandwidth();
int getEstimatedBandwidth();
int getMeasuredBandwidth();
ExitPorts getExitPorts();
}

View File

@ -0,0 +1,6 @@
package com.subgraph.orchid;
public interface SocksPortListener {
void addListeningPort(int port);
void stop();
}

View File

@ -0,0 +1,49 @@
package com.subgraph.orchid;
import java.io.InputStream;
import java.io.OutputStream;
public interface Stream {
/**
* Returns the {@link Circuit} this stream belongs to.
*
* @return The {@link Circuit} this stream belongs to.
*/
Circuit getCircuit();
/**
* Returns the stream id value of this stream.
*
* @return The stream id value of this stream.
*/
int getStreamId();
CircuitNode getTargetNode();
/**
* Close this stream.
*/
void close();
/**
* Returns an {@link InputStream} for sending data on this stream.
*
* @return An {@link InputStream} for transferring data on this stream.
*/
InputStream getInputStream();
/**
* Returns an {@link OutputStream} for receiving data from this stream.
*
* @return An {@link OutputStream} for receiving data from this stream.
*/
OutputStream getOutputStream();
/**
* If the circuit and stream level packaging windows are open for this stream
* this method returns immediately, otherwise it blocks until both windows are
* open or the stream is closed.
*/
void waitForSendWindow();
}

View File

@ -0,0 +1,35 @@
package com.subgraph.orchid;
public class StreamConnectFailedException extends Exception {
private static final long serialVersionUID = 8103571310659595097L;
private final int reason;
public StreamConnectFailedException(int reason) {
this.reason = reason;
}
public int getReason() {
return reason;
}
public boolean isReasonRetryable() {
return isRetryableReason(reason);
}
/* Copied from edge_reason_is_retriable() since this is not specified */
private static boolean isRetryableReason(int reasonCode) {
switch(reasonCode) {
case RelayCell.REASON_HIBERNATING:
case RelayCell.REASON_RESOURCELIMIT:
case RelayCell.REASON_RESOLVEFAILED:
case RelayCell.REASON_EXITPOLICY:
case RelayCell.REASON_MISC:
case RelayCell.REASON_NOROUTE:
return true;
default:
return false;
}
}
}

View File

@ -0,0 +1,167 @@
package com.subgraph.orchid;
import java.lang.reflect.Proxy;
import java.nio.charset.Charset;
import java.util.logging.Logger;
import com.subgraph.orchid.circuits.CircuitManagerImpl;
import com.subgraph.orchid.circuits.TorInitializationTracker;
import com.subgraph.orchid.config.TorConfigProxy;
import com.subgraph.orchid.connections.ConnectionCacheImpl;
import com.subgraph.orchid.directory.DirectoryImpl;
import com.subgraph.orchid.directory.downloader.DirectoryDownloaderImpl;
import com.subgraph.orchid.socks.SocksPortListenerImpl;
/**
* The <code>Tor</code> class is a collection of static methods for instantiating
* various subsystem modules.
*/
public class Tor {
private final static Logger logger = Logger.getLogger(Tor.class.getName());
public final static int BOOTSTRAP_STATUS_STARTING = 0;
public final static int BOOTSTRAP_STATUS_CONN_DIR = 5;
public final static int BOOTSTRAP_STATUS_HANDSHAKE_DIR = 10;
public final static int BOOTSTRAP_STATUS_ONEHOP_CREATE = 15;
public final static int BOOTSTRAP_STATUS_REQUESTING_STATUS = 20;
public final static int BOOTSTRAP_STATUS_LOADING_STATUS = 25;
public final static int BOOTSTRAP_STATUS_REQUESTING_KEYS = 35;
public final static int BOOTSTRAP_STATUS_LOADING_KEYS = 40;
public final static int BOOTSTRAP_STATUS_REQUESTING_DESCRIPTORS = 45;
public final static int BOOTSTRAP_STATUS_LOADING_DESCRIPTORS = 50;
public final static int BOOTSTRAP_STATUS_CONN_OR = 80;
public final static int BOOTSTRAP_STATUS_HANDSHAKE_OR = 85;
public final static int BOOTSTRAP_STATUS_CIRCUIT_CREATE = 90;
public final static int BOOTSTRAP_STATUS_DONE = 100;
private final static String implementation = "Orchid";
private final static String version = "1.0.0";
private final static Charset defaultCharset = createDefaultCharset();
private static Charset createDefaultCharset() {
return Charset.forName("ISO-8859-1");
}
public static Charset getDefaultCharset() {
return defaultCharset;
}
public static String getBuildRevision() {
return Revision.getBuildRevision();
}
public static String getImplementation() {
return implementation;
}
public static String getFullVersion() {
final String revision = getBuildRevision();
if(revision == null || revision.isEmpty()) {
return getVersion();
} else {
return getVersion() + "." + revision;
}
}
/**
* Return a string describing the version of this software.
*
* @return A string representation of the software version.
*/
public static String getVersion() {
return version;
}
/**
* Determine if running on Android by inspecting java.runtime.name property.
*
* @return True if running on Android.
*/
public static boolean isAndroidRuntime() {
final String runtime = System.getProperty("java.runtime.name");
return runtime != null && runtime.equals("Android Runtime");
}
/**
* Create and return a new <code>TorConfig</code> instance.
*
* @param logManager This is a required dependency. You must create a <code>LogManager</code>
* before calling this method to create a <code>TorConfig</code>
* @return A new <code>TorConfig</code> instance.
* @see TorConfig
*/
static public TorConfig createConfig() {
final TorConfig config = (TorConfig) Proxy.newProxyInstance(TorConfigProxy.class.getClassLoader(), new Class[] { TorConfig.class }, new TorConfigProxy());
if(isAndroidRuntime()) {
logger.warning("Android Runtime detected, disabling V2 Link protocol");
config.setHandshakeV2Enabled(false);
}
return config;
}
static public TorInitializationTracker createInitalizationTracker() {
return new TorInitializationTracker();
}
/**
* Create and return a new <code>Directory</code> instance.
*
* @param logManager This is a required dependency. You must create a <code>LogManager</code>
* before creating a <code>Directory</code>.
* @param config This is a required dependency. You must create a <code>TorConfig</code> before
* calling this method to create a <code>Directory</code>
* @return A new <code>Directory</code> instance.
* @see Directory
*/
static public Directory createDirectory(TorConfig config, DirectoryStore customDirectoryStore) {
return new DirectoryImpl(config, customDirectoryStore);
}
static public ConnectionCache createConnectionCache(TorConfig config, TorInitializationTracker tracker) {
return new ConnectionCacheImpl(config, tracker);
}
/**
* Create and return a new <code>CircuitManager</code> instance.
*
* @return A new <code>CircuitManager</code> instance.
* @see CircuitManager
*/
static public CircuitManager createCircuitManager(TorConfig config, DirectoryDownloaderImpl directoryDownloader, Directory directory, ConnectionCache connectionCache, TorInitializationTracker tracker) {
return new CircuitManagerImpl(config, directoryDownloader, directory, connectionCache, tracker);
}
/**
* Create and return a new <code>SocksPortListener</code> instance.
*
* @param logManager This is a required dependency. You must create a <code>LogManager</code>
* before calling this method to create a <code>SocksPortListener</code>.
* @param circuitManager This is a required dependency. You must create a <code>CircuitManager</code>
* before calling this method to create a <code>SocksPortListener</code>.
* @return A new <code>SocksPortListener</code> instance.
* @see SocksPortListener
*/
static public SocksPortListener createSocksPortListener(TorConfig config, CircuitManager circuitManager) {
return new SocksPortListenerImpl(config, circuitManager);
}
/**
* Create and return a new <code>DirectoryDownloader</code> instance.
*
* @param logManager This is a required dependency. You must create a <code>LogManager</code>
* before calling this method to create a <code>DirectoryDownloader</code>.
* @param directory This is a required dependency. You must create a <code>Directory</code>
* before calling this method to create a <code>DirectoryDownloader</code>
*
* @param circuitManager This is a required dependency. You must create a <code>CircuitManager</code>
* before calling this method to create a <code>DirectoryDownloader</code>.
*
* @return A new <code>DirectoryDownloader</code> instance.
* @see DirectoryDownloaderImpl
*/
static public DirectoryDownloaderImpl createDirectoryDownloader(TorConfig config, TorInitializationTracker initializationTracker) {
return new DirectoryDownloaderImpl(config, initializationTracker);
}
}

View File

@ -0,0 +1,221 @@
package com.subgraph.orchid;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import javax.net.SocketFactory;
import com.subgraph.orchid.circuits.TorInitializationTracker;
import com.subgraph.orchid.crypto.PRNGFixes;
import com.subgraph.orchid.dashboard.Dashboard;
import com.subgraph.orchid.directory.downloader.DirectoryDownloaderImpl;
import com.subgraph.orchid.sockets.OrchidSocketFactory;
/**
* This class is the main entry-point for running a Tor proxy
* or client.
*/
public class TorClient {
private final static Logger logger = Logger.getLogger(TorClient.class.getName());
private final TorConfig config;
private final Directory directory;
private final TorInitializationTracker initializationTracker;
private final ConnectionCache connectionCache;
private final CircuitManager circuitManager;
private final SocksPortListener socksListener;
private final DirectoryDownloaderImpl directoryDownloader;
private final Dashboard dashboard;
private boolean isStarted = false;
private boolean isStopped = false;
private final CountDownLatch readyLatch;
public TorClient() {
this(null);
}
public TorClient(DirectoryStore customDirectoryStore) {
if(Tor.isAndroidRuntime()) {
PRNGFixes.apply();
}
config = Tor.createConfig();
directory = Tor.createDirectory(config, customDirectoryStore);
initializationTracker = Tor.createInitalizationTracker();
initializationTracker.addListener(createReadyFlagInitializationListener());
connectionCache = Tor.createConnectionCache(config, initializationTracker);
directoryDownloader = Tor.createDirectoryDownloader(config, initializationTracker);
circuitManager = Tor.createCircuitManager(config, directoryDownloader, directory, connectionCache, initializationTracker);
socksListener = Tor.createSocksPortListener(config, circuitManager);
readyLatch = new CountDownLatch(1);
dashboard = new Dashboard();
dashboard.addRenderables(circuitManager, directoryDownloader, socksListener);
}
public TorConfig getConfig() {
return config;
}
public SocketFactory getSocketFactory() {
return new OrchidSocketFactory(this);
}
/**
* Start running the Tor client service.
*/
public synchronized void start() {
if(isStarted) {
return;
}
if(isStopped) {
throw new IllegalStateException("Cannot restart a TorClient instance. Create a new instance instead.");
}
logger.info("Starting Orchid (version: "+ Tor.getFullVersion() +")");
verifyUnlimitedStrengthPolicyInstalled();
directoryDownloader.start(directory);
circuitManager.startBuildingCircuits();
if(dashboard.isEnabledByProperty()) {
dashboard.startListening();
}
isStarted = true;
}
public synchronized void stop() {
if(!isStarted || isStopped) {
return;
}
try {
socksListener.stop();
if(dashboard.isListening()) {
dashboard.stopListening();
}
directoryDownloader.stop();
circuitManager.stopBuildingCircuits(true);
directory.close();
connectionCache.close();
} catch (Exception e) {
logger.log(Level.WARNING, "Unexpected exception while shutting down TorClient instance: "+ e, e);
} finally {
isStopped = true;
}
}
public Directory getDirectory() {
return directory;
}
public ConnectionCache getConnectionCache() {
return connectionCache;
}
public CircuitManager getCircuitManager() {
return circuitManager;
}
public DirectoryDownloader getDirectoryDownloader() {
return directoryDownloader;
}
public void waitUntilReady() throws InterruptedException {
readyLatch.await();
}
public void waitUntilReady(long timeout) throws InterruptedException, TimeoutException {
if(!readyLatch.await(timeout, TimeUnit.MILLISECONDS)) {
throw new TimeoutException();
}
}
public Stream openExitStreamTo(String hostname, int port) throws InterruptedException, TimeoutException, OpenFailedException {
ensureStarted();
return circuitManager.openExitStreamTo(hostname, port);
}
private synchronized void ensureStarted() {
if(!isStarted) {
throw new IllegalStateException("Must call start() first");
}
}
public void enableSocksListener(int port) {
socksListener.addListeningPort(port);
}
public void enableSocksListener() {
enableSocksListener(9150);
}
public void enableDashboard() {
if(!dashboard.isListening()) {
dashboard.startListening();
}
}
public void enableDashboard(int port) {
dashboard.setListeningPort(port);
enableDashboard();
}
public void disableDashboard() {
if(dashboard.isListening()) {
dashboard.stopListening();
}
}
public void addInitializationListener(TorInitializationListener listener) {
initializationTracker.addListener(listener);
}
public void removeInitializationListener(TorInitializationListener listener) {
initializationTracker.removeListener(listener);
}
private TorInitializationListener createReadyFlagInitializationListener() {
return new TorInitializationListener() {
public void initializationProgress(String message, int percent) {}
public void initializationCompleted() {
readyLatch.countDown();
}
};
}
public static void main(String[] args) {
final TorClient client = new TorClient();
client.addInitializationListener(createInitalizationListner());
client.start();
client.enableSocksListener();
}
private static TorInitializationListener createInitalizationListner() {
return new TorInitializationListener() {
public void initializationProgress(String message, int percent) {
System.out.println(">>> [ "+ percent + "% ]: "+ message);
}
public void initializationCompleted() {
System.out.println("Tor is ready to go!");
}
};
}
private void verifyUnlimitedStrengthPolicyInstalled() {
try {
if(Cipher.getMaxAllowedKeyLength("AES") < 256) {
final String message = "Unlimited Strength Jurisdiction Policy Files are required but not installed.";
logger.severe(message);
throw new TorException(message);
}
} catch (NoSuchAlgorithmException e) {
logger.log(Level.SEVERE, "No AES provider found");
throw new TorException(e);
} catch (NoSuchMethodError e) {
logger.info("Skipped check for Unlimited Strength Jurisdiction Policy Files");
}
}
}

View File

@ -0,0 +1,151 @@
package com.subgraph.orchid;
import java.io.File;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.subgraph.orchid.circuits.hs.HSDescriptorCookie;
import com.subgraph.orchid.config.TorConfigBridgeLine;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.IPv4Address;
public interface TorConfig {
@ConfigVar(type=ConfigVarType.PATH, defaultValue="~/.orchid")
File getDataDirectory();
void setDataDirectory(File directory);
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="60 seconds")
long getCircuitBuildTimeout();
void setCircuitBuildTimeout(long time, TimeUnit unit);
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="0")
long getCircuitStreamTimeout();
void setCircuitStreamTimeout(long time, TimeUnit unit);
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="1 hour")
long getCircuitIdleTimeout();
void setCircuitIdleTimeout(long time, TimeUnit unit);
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="30 seconds")
long getNewCircuitPeriod();
void setNewCircuitPeriod(long time, TimeUnit unit);
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="10 minutes")
long getMaxCircuitDirtiness();
void setMaxCircuitDirtiness(long time, TimeUnit unit);
@ConfigVar(type=ConfigVarType.INTEGER, defaultValue="32")
int getMaxClientCircuitsPending();
void setMaxClientCircuitsPending(int value);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
boolean getEnforceDistinctSubnets();
void setEnforceDistinctSubnets(boolean value);
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="2 minutes")
long getSocksTimeout();
void setSocksTimeout(long value);
@ConfigVar(type=ConfigVarType.INTEGER, defaultValue="3")
int getNumEntryGuards();
void setNumEntryGuards(int value);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
boolean getUseEntryGuards();
void setUseEntryGuards(boolean value);
@ConfigVar(type=ConfigVarType.PORTLIST, defaultValue="21,22,706,1863,5050,5190,5222,5223,6523,6667,6697,8300")
List<Integer> getLongLivedPorts();
void setLongLivedPorts(List<Integer> ports);
@ConfigVar(type=ConfigVarType.STRINGLIST)
List<String> getExcludeNodes();
void setExcludeNodes(List<String> nodes);
@ConfigVar(type=ConfigVarType.STRINGLIST)
List<String> getExcludeExitNodes();
void setExcludeExitNodes(List<String> nodes);
@ConfigVar(type=ConfigVarType.STRINGLIST)
List<String> getExitNodes();
void setExitNodes(List<String> nodes);
@ConfigVar(type=ConfigVarType.STRINGLIST)
List<String> getEntryNodes();
void setEntryNodes(List<String> nodes);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="false")
boolean getStrictNodes();
void setStrictNodes(boolean value);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="false")
boolean getFascistFirewall();
void setFascistFirewall(boolean value);
@ConfigVar(type=ConfigVarType.PORTLIST, defaultValue="80,443")
List<Integer> getFirewallPorts();
void setFirewallPorts(List<Integer> ports);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="false")
boolean getSafeSocks();
void setSafeSocks(boolean value);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
boolean getSafeLogging();
void setSafeLogging(boolean value);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
boolean getWarnUnsafeSocks();
void setWarnUnsafeSocks(boolean value);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
boolean getClientRejectInternalAddress();
void setClientRejectInternalAddress(boolean value);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
boolean getHandshakeV3Enabled();
void setHandshakeV3Enabled(boolean value);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
boolean getHandshakeV2Enabled();
void setHandshakeV2Enabled(boolean value);
@ConfigVar(type=ConfigVarType.HS_AUTH)
HSDescriptorCookie getHidServAuth(String key);
void addHidServAuth(String key, String value);
@ConfigVar(type=ConfigVarType.AUTOBOOL, defaultValue="auto")
AutoBoolValue getUseNTorHandshake();
void setUseNTorHandshake(AutoBoolValue value);
@ConfigVar(type=ConfigVarType.AUTOBOOL, defaultValue="auto")
AutoBoolValue getUseMicrodescriptors();
void setUseMicrodescriptors(AutoBoolValue value);
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="false")
boolean getUseBridges();
void setUseBridges(boolean value);
@ConfigVar(type=ConfigVarType.BRIDGE_LINE)
List<TorConfigBridgeLine> getBridges();
void addBridge(IPv4Address address, int port);
void addBridge(IPv4Address address, int port, HexDigest fingerprint);
enum ConfigVarType { INTEGER, STRING, HS_AUTH, BOOLEAN, INTERVAL, PORTLIST, STRINGLIST, PATH, AUTOBOOL, BRIDGE_LINE };
enum AutoBoolValue { TRUE, FALSE, AUTO }
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ConfigVar {
ConfigVarType type();
String defaultValue() default "";
}
}

View File

@ -0,0 +1,22 @@
package com.subgraph.orchid;
public class TorException extends RuntimeException {
private static final long serialVersionUID = 2462760291055303580L;
public TorException() {
super();
}
public TorException(String message) {
super(message);
}
public TorException(String message, Throwable ex) {
super(message, ex);
}
public TorException(Throwable ex) {
super(ex);
}
}

View File

@ -0,0 +1,6 @@
package com.subgraph.orchid;
public interface TorInitializationListener {
void initializationProgress(String message, int percent);
void initializationCompleted();
}

View File

@ -0,0 +1,14 @@
package com.subgraph.orchid;
public class TorParsingException extends TorException {
public TorParsingException(String string) {
super(string);
}
public TorParsingException(String string, Throwable ex) {
super(string, ex);
}
private static final long serialVersionUID = -4997757416476363399L;
}

View File

@ -0,0 +1,19 @@
package com.subgraph.orchid;
import java.util.List;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.directory.consensus.DirectorySignature;
public interface VoteAuthorityEntry {
String getNickname();
HexDigest getIdentity();
String getHostname();
IPv4Address getAddress();
int getDirectoryPort();
int getRouterPort();
String getContact();
HexDigest getVoteDigest();
List<DirectorySignature> getSignatures();
}

View File

@ -0,0 +1,127 @@
package com.subgraph.orchid.circuits;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.Connection;
import com.subgraph.orchid.ConnectionCache;
import com.subgraph.orchid.ConnectionFailedException;
import com.subgraph.orchid.ConnectionHandshakeException;
import com.subgraph.orchid.ConnectionTimeoutException;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.Tor;
import com.subgraph.orchid.TorException;
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
public class CircuitBuildTask implements Runnable {
private final static Logger logger = Logger.getLogger(CircuitBuildTask.class.getName());
private final CircuitCreationRequest creationRequest;
private final ConnectionCache connectionCache;
private final TorInitializationTracker initializationTracker;
private final CircuitImpl circuit;
private final CircuitExtender extender;
private Connection connection = null;
public CircuitBuildTask(CircuitCreationRequest request, ConnectionCache connectionCache, boolean ntorEnabled) {
this(request, connectionCache, ntorEnabled, null);
}
public CircuitBuildTask(CircuitCreationRequest request, ConnectionCache connectionCache, boolean ntorEnabled, TorInitializationTracker initializationTracker) {
this.creationRequest = request;
this.connectionCache = connectionCache;
this.initializationTracker = initializationTracker;
this.circuit = request.getCircuit();
this.extender = new CircuitExtender(request.getCircuit(), ntorEnabled);
}
public void run() {
Router firstRouter = null;
try {
circuit.notifyCircuitBuildStart();
creationRequest.choosePath();
if(logger.isLoggable(Level.FINE)) {
logger.fine("Opening a new circuit to "+ pathToString(creationRequest));
}
firstRouter = creationRequest.getPathElement(0);
openEntryNodeConnection(firstRouter);
buildCircuit(firstRouter);
circuit.notifyCircuitBuildCompleted();
} catch (ConnectionTimeoutException e) {
connectionFailed("Timeout connecting to "+ firstRouter);
} catch (ConnectionFailedException e) {
connectionFailed("Connection failed to "+ firstRouter + " : " + e.getMessage());
} catch (ConnectionHandshakeException e) {
connectionFailed("Handshake error connecting to "+ firstRouter + " : " + e.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
circuitBuildFailed("Circuit building thread interrupted");
} catch(PathSelectionFailedException e) {
circuitBuildFailed(e.getMessage());
} catch (TorException e) {
circuitBuildFailed(e.getMessage());
} catch(Exception e) {
circuitBuildFailed("Unexpected exception: "+ e);
logger.log(Level.WARNING, "Unexpected exception while building circuit: "+ e, e);
}
}
private String pathToString(CircuitCreationRequest ccr) {
final StringBuilder sb = new StringBuilder();
sb.append("[");
for(Router r: ccr.getPath()) {
if(sb.length() > 1)
sb.append(",");
sb.append(r.getNickname());
}
sb.append("]");
return sb.toString();
}
private void connectionFailed(String message) {
creationRequest.connectionFailed(message);
circuit.notifyCircuitBuildFailed();
}
private void circuitBuildFailed(String message) {
creationRequest.circuitBuildFailed(message);
circuit.notifyCircuitBuildFailed();
if(connection != null) {
connection.removeCircuit(circuit);
}
}
private void openEntryNodeConnection(Router firstRouter) throws ConnectionTimeoutException, ConnectionFailedException, ConnectionHandshakeException, InterruptedException {
connection = connectionCache.getConnectionTo(firstRouter, creationRequest.isDirectoryCircuit());
circuit.bindToConnection(connection);
creationRequest.connectionCompleted(connection);
}
private void buildCircuit(Router firstRouter) throws TorException {
notifyInitialization();
final CircuitNode firstNode = extender.createFastTo(firstRouter);
creationRequest.nodeAdded(firstNode);
for(int i = 1; i < creationRequest.getPathLength(); i++) {
final CircuitNode extendedNode = extender.extendTo(creationRequest.getPathElement(i));
creationRequest.nodeAdded(extendedNode);
}
creationRequest.circuitBuildCompleted(circuit);
notifyDone();
}
private void notifyInitialization() {
if(initializationTracker != null) {
final int event = creationRequest.isDirectoryCircuit() ?
Tor.BOOTSTRAP_STATUS_ONEHOP_CREATE : Tor.BOOTSTRAP_STATUS_CIRCUIT_CREATE;
initializationTracker.notifyEvent(event);
}
}
private void notifyDone() {
if(initializationTracker != null && !creationRequest.isDirectoryCircuit()) {
initializationTracker.notifyEvent(Tor.BOOTSTRAP_STATUS_DONE);
}
}
}

View File

@ -0,0 +1,91 @@
package com.subgraph.orchid.circuits;
import java.util.Collections;
import java.util.List;
import com.subgraph.orchid.Circuit;
import com.subgraph.orchid.CircuitBuildHandler;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.Connection;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
public class CircuitCreationRequest implements CircuitBuildHandler {
private final CircuitImpl circuit;
private final CircuitPathChooser pathChooser;
private final CircuitBuildHandler buildHandler;
private final boolean isDirectoryCircuit;
private List<Router> path;
public CircuitCreationRequest(CircuitPathChooser pathChooser, Circuit circuit, CircuitBuildHandler buildHandler, boolean isDirectoryCircuit) {
this.pathChooser = pathChooser;
this.circuit = (CircuitImpl) circuit;
this.buildHandler = buildHandler;
this.path = Collections.emptyList();
this.isDirectoryCircuit = isDirectoryCircuit;
}
void choosePath() throws InterruptedException, PathSelectionFailedException {
if(!(circuit instanceof CircuitImpl)) {
throw new IllegalArgumentException();
}
path = ((CircuitImpl)circuit).choosePath(pathChooser);
}
CircuitImpl getCircuit() {
return circuit;
}
List<Router> getPath() {
return path;
}
int getPathLength() {
return path.size();
}
Router getPathElement(int idx) {
return path.get(idx);
}
CircuitBuildHandler getBuildHandler() {
return buildHandler;
}
boolean isDirectoryCircuit() {
return isDirectoryCircuit;
}
public void connectionCompleted(Connection connection) {
if(buildHandler != null) {
buildHandler.connectionCompleted(connection);
}
}
public void connectionFailed(String reason) {
if(buildHandler != null) {
buildHandler.connectionFailed(reason);
}
}
public void nodeAdded(CircuitNode node) {
if(buildHandler != null) {
buildHandler.nodeAdded(node);
}
}
public void circuitBuildCompleted(Circuit circuit) {
if(buildHandler != null) {
buildHandler.circuitBuildCompleted(circuit);
}
}
public void circuitBuildFailed(String reason) {
if(buildHandler != null) {
buildHandler.circuitBuildFailed(reason);
}
}
}

View File

@ -0,0 +1,292 @@
package com.subgraph.orchid.circuits;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.subgraph.orchid.Circuit;
import com.subgraph.orchid.CircuitBuildHandler;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.Connection;
import com.subgraph.orchid.ConnectionCache;
import com.subgraph.orchid.Directory;
import com.subgraph.orchid.ExitCircuit;
import com.subgraph.orchid.InternalCircuit;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.TorConfig;
import com.subgraph.orchid.circuits.CircuitManagerImpl.CircuitFilter;
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
public class CircuitCreationTask implements Runnable {
private final static Logger logger = Logger.getLogger(CircuitCreationTask.class.getName());
private final static int MAX_CIRCUIT_DIRTINESS = 300; // seconds
private final static int MAX_PENDING_CIRCUITS = 4;
private final TorConfig config;
private final Directory directory;
private final ConnectionCache connectionCache;
private final CircuitManagerImpl circuitManager;
private final TorInitializationTracker initializationTracker;
private final CircuitPathChooser pathChooser;
private final Executor executor;
private final CircuitBuildHandler buildHandler;
private final CircuitBuildHandler internalBuildHandler;
// To avoid obnoxiously printing a warning every second
private int notEnoughDirectoryInformationWarningCounter = 0;
private final CircuitPredictor predictor;
private final AtomicLong lastNewCircuit;
CircuitCreationTask(TorConfig config, Directory directory, ConnectionCache connectionCache, CircuitPathChooser pathChooser, CircuitManagerImpl circuitManager, TorInitializationTracker initializationTracker) {
this.config = config;
this.directory = directory;
this.connectionCache = connectionCache;
this.circuitManager = circuitManager;
this.initializationTracker = initializationTracker;
this.pathChooser = pathChooser;
this.executor = Executors.newCachedThreadPool();
this.buildHandler = createCircuitBuildHandler();
this.internalBuildHandler = createInternalCircuitBuildHandler();
this.predictor = new CircuitPredictor();
this.lastNewCircuit = new AtomicLong();
}
CircuitPredictor getCircuitPredictor() {
return predictor;
}
public void run() {
expireOldCircuits();
assignPendingStreamsToActiveCircuits();
checkExpiredPendingCircuits();
checkCircuitsForCreation();
}
void predictPort(int port) {
predictor.addExitPortRequest(port);
}
private void assignPendingStreamsToActiveCircuits() {
final List<StreamExitRequest> pendingExitStreams = circuitManager.getPendingExitStreams();
if(pendingExitStreams.isEmpty())
return;
for(ExitCircuit c: circuitManager.getRandomlyOrderedListOfExitCircuits()) {
final Iterator<StreamExitRequest> it = pendingExitStreams.iterator();
while(it.hasNext()) {
if(attemptHandleStreamRequest(c, it.next()))
it.remove();
}
}
}
private boolean attemptHandleStreamRequest(ExitCircuit c, StreamExitRequest request) {
if(c.canHandleExitTo(request)) {
if(request.reserveRequest()) {
launchExitStreamTask(c, request);
}
// else request is reserved meaning another circuit is already trying to handle it
return true;
}
return false;
}
private void launchExitStreamTask(ExitCircuit circuit, StreamExitRequest exitRequest) {
final OpenExitStreamTask task = new OpenExitStreamTask(circuit, exitRequest);
executor.execute(task);
}
private void expireOldCircuits() {
final Set<Circuit> circuits = circuitManager.getCircuitsByFilter(new CircuitFilter() {
public boolean filter(Circuit circuit) {
return !circuit.isMarkedForClose() && circuit.getSecondsDirty() > MAX_CIRCUIT_DIRTINESS;
}
});
for(Circuit c: circuits) {
logger.fine("Closing idle dirty circuit: "+ c);
((CircuitImpl)c).markForClose();
}
}
private void checkExpiredPendingCircuits() {
// TODO Auto-generated method stub
}
private void checkCircuitsForCreation() {
if(!directory.haveMinimumRouterInfo()) {
if(notEnoughDirectoryInformationWarningCounter % 20 == 0)
logger.info("Cannot build circuits because we don't have enough directory information");
notEnoughDirectoryInformationWarningCounter++;
return;
}
if(lastNewCircuit.get() != 0) {
final long now = System.currentTimeMillis();
if((now - lastNewCircuit.get()) < config.getNewCircuitPeriod()) {
// return;
}
}
buildCircuitIfNeeded();
maybeBuildInternalCircuit();
}
private void buildCircuitIfNeeded() {
final List<StreamExitRequest> pendingExitStreams = circuitManager.getPendingExitStreams();
final List<PredictedPortTarget> predictedPorts = predictor.getPredictedPortTargets();
final List<ExitTarget> exitTargets = new ArrayList<ExitTarget>();
for(StreamExitRequest streamRequest: pendingExitStreams) {
if(!streamRequest.isReserved() && countCircuitsSupportingTarget(streamRequest, false) == 0) {
exitTargets.add(streamRequest);
}
}
for(PredictedPortTarget ppt: predictedPorts) {
if(countCircuitsSupportingTarget(ppt, true) < 2) {
exitTargets.add(ppt);
}
}
buildCircuitToHandleExitTargets(exitTargets);
}
private void maybeBuildInternalCircuit() {
final int needed = circuitManager.getNeededCleanCircuitCount(predictor.isInternalPredicted());
if(needed > 0) {
launchBuildTaskForInternalCircuit();
}
}
private void launchBuildTaskForInternalCircuit() {
logger.fine("Launching new internal circuit");
final InternalCircuitImpl circuit = new InternalCircuitImpl(circuitManager);
final CircuitCreationRequest request = new CircuitCreationRequest(pathChooser, circuit, internalBuildHandler, false);
final CircuitBuildTask task = new CircuitBuildTask(request, connectionCache, circuitManager.isNtorEnabled());
executor.execute(task);
circuitManager.incrementPendingInternalCircuitCount();
}
private int countCircuitsSupportingTarget(final ExitTarget target, final boolean needClean) {
final CircuitFilter filter = new CircuitFilter() {
public boolean filter(Circuit circuit) {
if(!(circuit instanceof ExitCircuit)) {
return false;
}
final ExitCircuit ec = (ExitCircuit) circuit;
final boolean pendingOrConnected = circuit.isPending() || circuit.isConnected();
final boolean isCleanIfNeeded = !(needClean && !circuit.isClean());
return pendingOrConnected && isCleanIfNeeded && ec.canHandleExitTo(target);
}
};
return circuitManager.getCircuitsByFilter(filter).size();
}
private void buildCircuitToHandleExitTargets(List<ExitTarget> exitTargets) {
if(exitTargets.isEmpty()) {
return;
}
if(!directory.haveMinimumRouterInfo())
return;
if(circuitManager.getPendingCircuitCount() >= MAX_PENDING_CIRCUITS)
return;
if(logger.isLoggable(Level.FINE)) {
logger.fine("Building new circuit to handle "+ exitTargets.size() +" pending streams and predicted ports");
}
launchBuildTaskForTargets(exitTargets);
}
private void launchBuildTaskForTargets(List<ExitTarget> exitTargets) {
final Router exitRouter = pathChooser.chooseExitNodeForTargets(exitTargets);
if(exitRouter == null) {
logger.warning("Failed to select suitable exit node for targets");
return;
}
final Circuit circuit = circuitManager.createNewExitCircuit(exitRouter);
final CircuitCreationRequest request = new CircuitCreationRequest(pathChooser, circuit, buildHandler, false);
final CircuitBuildTask task = new CircuitBuildTask(request, connectionCache, circuitManager.isNtorEnabled(), initializationTracker);
executor.execute(task);
}
private CircuitBuildHandler createCircuitBuildHandler() {
return new CircuitBuildHandler() {
public void circuitBuildCompleted(Circuit circuit) {
logger.fine("Circuit completed to: "+ circuit);
circuitOpenedHandler(circuit);
lastNewCircuit.set(System.currentTimeMillis());
}
public void circuitBuildFailed(String reason) {
logger.fine("Circuit build failed: "+ reason);
buildCircuitIfNeeded();
}
public void connectionCompleted(Connection connection) {
logger.finer("Circuit connection completed to "+ connection);
}
public void connectionFailed(String reason) {
logger.fine("Circuit connection failed: "+ reason);
buildCircuitIfNeeded();
}
public void nodeAdded(CircuitNode node) {
logger.finer("Node added to circuit: "+ node);
}
};
}
private void circuitOpenedHandler(Circuit circuit) {
if(!(circuit instanceof ExitCircuit)) {
return;
}
final ExitCircuit ec = (ExitCircuit) circuit;
final List<StreamExitRequest> pendingExitStreams = circuitManager.getPendingExitStreams();
for(StreamExitRequest req: pendingExitStreams) {
if(ec.canHandleExitTo(req) && req.reserveRequest()) {
launchExitStreamTask(ec, req);
}
}
}
private CircuitBuildHandler createInternalCircuitBuildHandler() {
return new CircuitBuildHandler() {
public void nodeAdded(CircuitNode node) {
logger.finer("Node added to internal circuit: "+ node);
}
public void connectionFailed(String reason) {
logger.fine("Circuit connection failed: "+ reason);
circuitManager.decrementPendingInternalCircuitCount();
}
public void connectionCompleted(Connection connection) {
logger.finer("Circuit connection completed to "+ connection);
}
public void circuitBuildFailed(String reason) {
logger.fine("Circuit build failed: "+ reason);
circuitManager.decrementPendingInternalCircuitCount();
}
public void circuitBuildCompleted(Circuit circuit) {
logger.fine("Internal circuit build completed: "+ circuit);
lastNewCircuit.set(System.currentTimeMillis());
circuitManager.addCleanInternalCircuit((InternalCircuit) circuit);
}
};
}
}

View File

@ -0,0 +1,155 @@
package com.subgraph.orchid.circuits;
import java.util.logging.Logger;
import com.subgraph.orchid.Cell;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.TorException;
import com.subgraph.orchid.circuits.cells.CellImpl;
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
import com.subgraph.orchid.crypto.TorCreateFastKeyAgreement;
import com.subgraph.orchid.crypto.TorKeyAgreement;
import com.subgraph.orchid.crypto.TorMessageDigest;
import com.subgraph.orchid.crypto.TorStreamCipher;
public class CircuitExtender {
private final static Logger logger = Logger.getLogger(CircuitExtender.class.getName());
private final static int DH_BYTES = 1024 / 8;
private final static int PKCS1_OAEP_PADDING_OVERHEAD = 42;
private final static int CIPHER_KEY_LEN = TorStreamCipher.KEY_LEN;
final static int TAP_ONIONSKIN_LEN = PKCS1_OAEP_PADDING_OVERHEAD + CIPHER_KEY_LEN + DH_BYTES;
final static int TAP_ONIONSKIN_REPLY_LEN = DH_BYTES + TorMessageDigest.TOR_DIGEST_SIZE;
private final CircuitImpl circuit;
private final boolean ntorEnabled;
CircuitExtender(CircuitImpl circuit, boolean ntorEnabled) {
this.circuit = circuit;
this.ntorEnabled = ntorEnabled;
}
CircuitNode createFastTo(Router targetRouter) {
logger.fine("Creating 'fast' to "+ targetRouter);
final TorCreateFastKeyAgreement kex = new TorCreateFastKeyAgreement();
sendCreateFastCell(kex);
return receiveAndProcessCreateFastResponse(targetRouter, kex);
}
private void sendCreateFastCell(TorCreateFastKeyAgreement kex) {
final Cell cell = CellImpl.createCell(circuit.getCircuitId(), Cell.CREATE_FAST);
cell.putByteArray(kex.createOnionSkin());
circuit.sendCell(cell);
}
private CircuitNode receiveAndProcessCreateFastResponse(Router targetRouter, TorKeyAgreement kex) {
final Cell cell = circuit.receiveControlCellResponse();
if(cell == null) {
throw new TorException("Timeout building circuit waiting for CREATE_FAST response from "+ targetRouter);
}
return processCreatedFastCell(targetRouter, cell, kex);
}
private CircuitNode processCreatedFastCell(Router targetRouter, Cell cell, TorKeyAgreement kex) {
final byte[] payload = new byte[TorMessageDigest.TOR_DIGEST_SIZE * 2];
final byte[] keyMaterial = new byte[CircuitNodeCryptoState.KEY_MATERIAL_SIZE];
final byte[] verifyHash = new byte[TorMessageDigest.TOR_DIGEST_SIZE];
cell.getByteArray(payload);
if(!kex.deriveKeysFromHandshakeResponse(payload, keyMaterial, verifyHash)) {
// XXX
return null;
}
final CircuitNode node = CircuitNodeImpl.createFirstHop(targetRouter, keyMaterial, verifyHash);
circuit.appendNode(node);
return node;
}
CircuitNode extendTo(Router targetRouter) {
if(circuit.getCircuitLength() == 0) {
throw new TorException("Cannot EXTEND an empty circuit");
}
if(useNtor(targetRouter)) {
final NTorCircuitExtender nce = new NTorCircuitExtender(this, targetRouter);
return nce.extendTo();
} else {
final TapCircuitExtender tce = new TapCircuitExtender(this, targetRouter);
return tce.extendTo();
}
}
private boolean useNtor(Router targetRouter) {
return ntorEnabled && targetRouter.getNTorOnionKey() != null;
}
private void logProtocolViolation(String sourceName, Router targetRouter) {
final String version = (targetRouter == null) ? "(none)" : targetRouter.getVersion();
final String targetName = (targetRouter == null) ? "(none)" : targetRouter.getNickname();
logger.warning("Protocol error extending circuit from ("+ sourceName +") to ("+ targetName +") [version: "+ version +"]");
}
private String nodeToName(CircuitNode node) {
if(node == null || node.getRouter() == null) {
return "(null)";
}
final Router router = node.getRouter();
return router.getNickname();
}
public void sendRelayCell(RelayCell cell) {
circuit.sendRelayCell(cell);
}
public RelayCell receiveRelayResponse(int expectedCommand, Router extendTarget) {
final RelayCell cell = circuit.receiveRelayCell();
if(cell == null) {
throw new TorException("Timeout building circuit");
}
final int command = cell.getRelayCommand();
if(command == RelayCell.RELAY_TRUNCATED) {
final int code = cell.getByte() & 0xFF;
final String msg = CellImpl.errorToDescription(code);
final String source = nodeToName(cell.getCircuitNode());
if(code == Cell.ERROR_PROTOCOL) {
logProtocolViolation(source, extendTarget);
}
throw new TorException("Error from ("+ source +") while extending to ("+ extendTarget.getNickname() + "): "+ msg);
} else if(command != expectedCommand) {
final String expected = RelayCellImpl.commandToDescription(expectedCommand);
final String received = RelayCellImpl.commandToDescription(command);
throw new TorException("Received incorrect extend response, expecting "+ expected + " but received "+ received);
} else {
return cell;
}
}
public CircuitNode createNewNode(Router r, byte[] keyMaterial, byte[] verifyDigest) {
final CircuitNode node = CircuitNodeImpl.createNode(r, circuit.getFinalCircuitNode(), keyMaterial, verifyDigest);
logger.fine("Adding new circuit node for "+ r.getNickname());
circuit.appendNode(node);
return node;
}
public RelayCell createRelayCell(int command) {
return new RelayCellImpl(circuit.getFinalCircuitNode(), circuit.getCircuitId(), 0, command, true);
}
Router getFinalRouter() {
final CircuitNode node = circuit.getFinalCircuitNode();
if(node != null) {
return node.getRouter();
} else {
return null;
}
}
}

View File

@ -0,0 +1,320 @@
package com.subgraph.orchid.circuits;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.subgraph.orchid.Cell;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.Connection;
import com.subgraph.orchid.ConnectionIOException;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.TorException;
import com.subgraph.orchid.circuits.cells.CellImpl;
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
import com.subgraph.orchid.dashboard.DashboardRenderable;
import com.subgraph.orchid.dashboard.DashboardRenderer;
public class CircuitIO implements DashboardRenderable {
private static final Logger logger = Logger.getLogger(CircuitIO.class.getName());
private final static long CIRCUIT_BUILD_TIMEOUT_MS = 30 * 1000;
private final static long CIRCUIT_RELAY_RESPONSE_TIMEOUT = 20 * 1000;
private final CircuitImpl circuit;
private final Connection connection;
private final int circuitId;
private final BlockingQueue<RelayCell> relayCellResponseQueue;
private final BlockingQueue<Cell> controlCellResponseQueue;
private final Map<Integer, StreamImpl> streamMap;
private final Object relaySendLock = new Object();
private boolean isMarkedForClose;
private boolean isClosed;
CircuitIO(CircuitImpl circuit, Connection connection, int circuitId) {
this.circuit = circuit;
this.connection = connection;
this.circuitId = circuitId;
this.relayCellResponseQueue = new LinkedBlockingQueue<RelayCell>();
this.controlCellResponseQueue = new LinkedBlockingQueue<Cell>();
this.streamMap = new HashMap<Integer, StreamImpl>();
}
Connection getConnection() {
return connection;
}
int getCircuitId() {
return circuitId;
}
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(CircuitNode node: circuit.getNodeList()) {
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(circuit.getStatus().isBuilding())
return remainingBuildTime();
else
return CIRCUIT_RELAY_RESPONSE_TIMEOUT;
}
private long remainingBuildTime() {
final long elapsed = circuit.getStatus().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, CREATED_FAST, or DESTROY).
*/
void deliverControlCell(Cell cell) {
if(cell.getCommand() == Cell.DESTROY) {
processDestroyCell(cell.getByte());
} else {
controlCellResponseQueue.add(cell);
}
}
private void processDestroyCell(int reason) {
logger.fine("DESTROY cell received ("+ CellImpl.errorToDescription(reason) +") on "+ circuit);
destroyCircuit();
}
/* This is called by the cell reading thread in ConnectionImpl to deliver RELAY cells. */
void deliverRelayCell(Cell cell) {
circuit.getStatus().updateDirtyTimestamp();
final RelayCell relayCell = decryptRelayCell(cell);
logRelayCell("Dispatching: ", relayCell);
switch(relayCell.getRelayCommand()) {
case RelayCell.RELAY_EXTENDED:
case RelayCell.RELAY_EXTENDED2:
case RelayCell.RELAY_RESOLVED:
case RelayCell.RELAY_TRUNCATED:
case RelayCell.RELAY_COMMAND_RENDEZVOUS_ESTABLISHED:
case RelayCell.RELAY_COMMAND_INTRODUCE_ACK:
case RelayCell.RELAY_COMMAND_RENDEZVOUS2:
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());
sendRelayCellTo(sendme, sendme.getCircuitNode());
}
}
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);
}
}
}
RelayCell createRelayCell(int relayCommand, int streamId, CircuitNode targetNode) {
return new RelayCellImpl(targetNode, circuitId, streamId, relayCommand);
}
void sendRelayCellTo(RelayCell cell, CircuitNode targetNode) {
synchronized(relaySendLock) {
logRelayCell("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);
}
}
private void logRelayCell(String message, RelayCell cell) {
final Level level = getLogLevelForCell(cell);
if(!logger.isLoggable(level)) {
return;
}
logger.log(level, message + cell);
}
private Level getLogLevelForCell(RelayCell cell) {
switch(cell.getRelayCommand()) {
case RelayCell.RELAY_DATA:
case RelayCell.RELAY_SENDME:
return Level.FINEST;
default:
return Level.FINER;
}
}
void sendCell(Cell cell) {
final CircuitStatus status = circuit.getStatus();
if(!(status.isConnected() || status.isBuilding()))
return;
try {
status.updateDirtyTimestamp();
connection.sendCell(cell);
} catch (ConnectionIOException e) {
destroyCircuit();
}
}
void markForClose() {
synchronized (streamMap) {
if(isMarkedForClose) {
return;
}
isMarkedForClose = true;
if(streamMap.isEmpty()) {
closeCircuit();
}
}
}
boolean isMarkedForClose() {
return isMarkedForClose;
}
private void closeCircuit() {
logger.fine("Closing circuit "+ circuit);
sendDestroyCell(Cell.ERROR_NONE);
connection.removeCircuit(circuit);
circuit.setStateDestroyed();
isClosed = true;
}
void sendDestroyCell(int reason) {
Cell destroy = CellImpl.createCell(circuitId, Cell.DESTROY);
destroy.putByte(reason);
try {
connection.sendCell(destroy);
} catch (ConnectionIOException e) {
logger.warning("Connection IO error sending DESTROY cell: "+ e.getMessage());
}
}
private void processCircuitSendme(RelayCell cell) {
cell.getCircuitNode().incrementSendWindow();
}
void destroyCircuit() {
synchronized(streamMap) {
if(isClosed) {
return;
}
circuit.setStateDestroyed();
connection.removeCircuit(circuit);
final List<StreamImpl> tmpList = new ArrayList<StreamImpl>(streamMap.values());
for(StreamImpl s: tmpList) {
s.close();
}
isClosed = true;
}
}
StreamImpl createNewStream(boolean autoclose) {
synchronized(streamMap) {
final int streamId = circuit.getStatus().nextStreamId();
final StreamImpl stream = new StreamImpl(circuit, circuit.getFinalCircuitNode(), streamId, autoclose);
streamMap.put(streamId, stream);
return stream;
}
}
void removeStream(StreamImpl stream) {
synchronized(streamMap) {
streamMap.remove(stream.getStreamId());
if(streamMap.isEmpty() && isMarkedForClose) {
closeCircuit();
}
}
}
List<Stream> getActiveStreams() {
synchronized (streamMap) {
return new ArrayList<Stream>(streamMap.values());
}
}
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException {
if((flags & DASHBOARD_STREAMS) == 0) {
return;
}
for(Stream s: getActiveStreams()) {
renderer.renderComponent(writer, flags, s);
}
}
}

View File

@ -0,0 +1,286 @@
package com.subgraph.orchid.circuits;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import com.subgraph.orchid.Cell;
import com.subgraph.orchid.Circuit;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.Connection;
import com.subgraph.orchid.DirectoryCircuit;
import com.subgraph.orchid.ExitCircuit;
import com.subgraph.orchid.InternalCircuit;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.TorException;
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
import com.subgraph.orchid.dashboard.DashboardRenderable;
import com.subgraph.orchid.dashboard.DashboardRenderer;
/**
* This class represents an established circuit through the Tor network.
*
*/
public abstract class CircuitImpl implements Circuit, DashboardRenderable {
protected final static Logger logger = Logger.getLogger(CircuitImpl.class.getName());
static ExitCircuit createExitCircuit(CircuitManagerImpl circuitManager, Router exitRouter) {
return new ExitCircuitImpl(circuitManager, exitRouter);
}
static ExitCircuit createExitCircuitTo(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
return new ExitCircuitImpl(circuitManager, prechosenPath);
}
static DirectoryCircuit createDirectoryCircuit(CircuitManagerImpl circuitManager) {
return new DirectoryCircuitImpl(circuitManager, null);
}
static DirectoryCircuit createDirectoryCircuitTo(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
return new DirectoryCircuitImpl(circuitManager, prechosenPath);
}
static InternalCircuit createInternalCircuitTo(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
return new InternalCircuitImpl(circuitManager, prechosenPath);
}
private final CircuitManagerImpl circuitManager;
protected final List<Router> prechosenPath;
private final List<CircuitNode> nodeList;
private final CircuitStatus status;
private CircuitIO io;
protected CircuitImpl(CircuitManagerImpl circuitManager) {
this(circuitManager, null);
}
protected CircuitImpl(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
nodeList = new ArrayList<CircuitNode>();
this.circuitManager = circuitManager;
this.prechosenPath = prechosenPath;
status = new CircuitStatus();
}
List<Router> choosePath(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException {
if(prechosenPath != null) {
return new ArrayList<Router>(prechosenPath);
} else {
return choosePathForCircuit(pathChooser);
}
}
protected abstract List<Router> choosePathForCircuit(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException;
void bindToConnection(Connection connection) {
if(io != null) {
throw new IllegalStateException("Circuit already bound to a connection");
}
final int id = connection.bindCircuit(this);
io = new CircuitIO(this, connection, id);
}
public void markForClose() {
if(io != null) {
io.markForClose();
}
}
public boolean isMarkedForClose() {
if(io == null) {
return false;
} else {
return io.isMarkedForClose();
}
}
CircuitStatus getStatus() {
return status;
}
public boolean isConnected() {
return status.isConnected();
}
public boolean isPending() {
return status.isBuilding();
}
public boolean isClean() {
return !status.isDirty();
}
public int getSecondsDirty() {
return (int) (status.getMillisecondsDirty() / 1000);
}
void notifyCircuitBuildStart() {
if(!status.isUnconnected()) {
throw new IllegalStateException("Can only connect UNCONNECTED circuits");
}
status.updateCreatedTimestamp();
status.setStateBuilding();
circuitManager.addActiveCircuit(this);
}
void notifyCircuitBuildFailed() {
status.setStateFailed();
circuitManager.removeActiveCircuit(this);
}
void notifyCircuitBuildCompleted() {
status.setStateOpen();
status.updateCreatedTimestamp();
}
public Connection getConnection() {
if(!isConnected())
throw new TorException("Circuit is not connected.");
return io.getConnection();
}
public int getCircuitId() {
if(io == null) {
return 0;
} else {
return io.getCircuitId();
}
}
public void sendRelayCell(RelayCell cell) {
io.sendRelayCellTo(cell, cell.getCircuitNode());
}
public void sendRelayCellToFinalNode(RelayCell cell) {
io.sendRelayCellTo(cell, getFinalCircuitNode());
}
public void appendNode(CircuitNode node) {
nodeList.add(node);
}
List<CircuitNode> getNodeList() {
return nodeList;
}
int getCircuitLength() {
return nodeList.size();
}
public CircuitNode 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 io.createRelayCell(relayCommand, streamId, targetNode);
}
public RelayCell receiveRelayCell() {
return io.dequeueRelayResponseCell();
}
void sendCell(Cell cell) {
io.sendCell(cell);
}
Cell receiveControlCellResponse() {
return io.receiveControlCellResponse();
}
/*
* This is called by the cell reading thread in ConnectionImpl to deliver control cells
* associated with this circuit (CREATED or CREATED_FAST).
*/
public void deliverControlCell(Cell cell) {
io.deliverControlCell(cell);
}
/* This is called by the cell reading thread in ConnectionImpl to deliver RELAY cells. */
public void deliverRelayCell(Cell cell) {
io.deliverRelayCell(cell);
}
protected StreamImpl createNewStream(boolean autoclose) {
return io.createNewStream(autoclose);
}
protected StreamImpl createNewStream() {
return createNewStream(false);
}
void setStateDestroyed() {
status.setStateDestroyed();
circuitManager.removeActiveCircuit(this);
}
public void destroyCircuit() {
io.destroyCircuit();
circuitManager.removeActiveCircuit(this);
}
public void removeStream(StreamImpl stream) {
io.removeStream(stream);
}
protected Stream processStreamOpenException(Exception e) throws InterruptedException, TimeoutException, StreamConnectFailedException {
if(e instanceof InterruptedException) {
throw (InterruptedException) e;
} else if(e instanceof TimeoutException) {
throw(TimeoutException) e;
} else if(e instanceof StreamConnectFailedException) {
throw(StreamConnectFailedException) e;
} else {
throw new IllegalStateException();
}
}
protected abstract String getCircuitTypeLabel();
public String toString() {
return " Circuit ("+ getCircuitTypeLabel() + ") id="+ getCircuitId() +" state=" + status.getStateAsString() +" "+ pathToString();
}
protected 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();
}
public List<Stream> getActiveStreams() {
if(io == null) {
return Collections.emptyList();
} else {
return io.getActiveStreams();
}
}
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException {
if(io != null) {
writer.println(toString());
renderer.renderComponent(writer, flags, io);
}
}
}

View File

@ -0,0 +1,395 @@
package com.subgraph.orchid.circuits;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.subgraph.orchid.Circuit;
import com.subgraph.orchid.CircuitBuildHandler;
import com.subgraph.orchid.CircuitManager;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.Connection;
import com.subgraph.orchid.ConnectionCache;
import com.subgraph.orchid.ConsensusDocument;
import com.subgraph.orchid.Directory;
import com.subgraph.orchid.DirectoryCircuit;
import com.subgraph.orchid.ExitCircuit;
import com.subgraph.orchid.InternalCircuit;
import com.subgraph.orchid.OpenFailedException;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.Tor;
import com.subgraph.orchid.TorConfig;
import com.subgraph.orchid.circuits.guards.EntryGuards;
import com.subgraph.orchid.circuits.hs.HiddenServiceManager;
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
import com.subgraph.orchid.crypto.TorRandom;
import com.subgraph.orchid.dashboard.DashboardRenderable;
import com.subgraph.orchid.dashboard.DashboardRenderer;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.directory.downloader.DirectoryDownloaderImpl;
public class CircuitManagerImpl implements CircuitManager, DashboardRenderable {
private final static int OPEN_DIRECTORY_STREAM_RETRY_COUNT = 5;
private final static int OPEN_DIRECTORY_STREAM_TIMEOUT = 10 * 1000;
interface CircuitFilter {
boolean filter(Circuit circuit);
}
private final TorConfig config;
private final Directory directory;
private final ConnectionCache connectionCache;
private final Set<CircuitImpl> activeCircuits;
private final Queue<InternalCircuit> cleanInternalCircuits;
private int requestedInternalCircuitCount = 0;
private int pendingInternalCircuitCount = 0;
private final TorRandom random;
private final PendingExitStreams pendingExitStreams;
private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
private final CircuitCreationTask circuitCreationTask;
private final TorInitializationTracker initializationTracker;
private final CircuitPathChooser pathChooser;
private final HiddenServiceManager hiddenServiceManager;
public CircuitManagerImpl(TorConfig config, DirectoryDownloaderImpl directoryDownloader, Directory directory, ConnectionCache connectionCache, TorInitializationTracker initializationTracker) {
this.config = config;
this.directory = directory;
this.connectionCache = connectionCache;
this.pathChooser = CircuitPathChooser.create(config, directory);
if(config.getUseEntryGuards() || config.getUseBridges()) {
this.pathChooser.enableEntryGuards(new EntryGuards(config, connectionCache, directoryDownloader, directory));
}
this.pendingExitStreams = new PendingExitStreams(config);
this.circuitCreationTask = new CircuitCreationTask(config, directory, connectionCache, pathChooser, this, initializationTracker);
this.activeCircuits = new HashSet<CircuitImpl>();
this.cleanInternalCircuits = new LinkedList<InternalCircuit>();
this.random = new TorRandom();
this.initializationTracker = initializationTracker;
this.hiddenServiceManager = new HiddenServiceManager(config, directory, this);
directoryDownloader.setCircuitManager(this);
}
public void startBuildingCircuits() {
scheduledExecutor.scheduleAtFixedRate(circuitCreationTask, 0, 1000, TimeUnit.MILLISECONDS);
}
public synchronized void stopBuildingCircuits(boolean killCircuits) {
scheduledExecutor.shutdownNow();
if(killCircuits) {
List<CircuitImpl> circuits = new ArrayList<CircuitImpl>(activeCircuits);
for(CircuitImpl c: circuits) {
c.destroyCircuit();
}
}
}
public ExitCircuit createNewExitCircuit(Router exitRouter) {
return CircuitImpl.createExitCircuit(this, exitRouter);
}
void addActiveCircuit(CircuitImpl circuit) {
synchronized (activeCircuits) {
activeCircuits.add(circuit);
activeCircuits.notifyAll();
}
}
void removeActiveCircuit(CircuitImpl circuit) {
synchronized (activeCircuits) {
activeCircuits.remove(circuit);
}
}
synchronized int getActiveCircuitCount() {
return activeCircuits.size();
}
Set<Circuit> getPendingCircuits() {
return getCircuitsByFilter(new CircuitFilter() {
public boolean filter(Circuit circuit) {
return circuit.isPending();
}
});
}
synchronized int getPendingCircuitCount() {
return getPendingCircuits().size();
}
Set<Circuit> getCircuitsByFilter(CircuitFilter filter) {
final Set<Circuit> result = new HashSet<Circuit>();
synchronized (activeCircuits) {
for(CircuitImpl c: activeCircuits) {
if(filter == null || filter.filter(c)) {
result.add(c);
}
}
}
return result;
}
List<ExitCircuit> getRandomlyOrderedListOfExitCircuits() {
final Set<Circuit> notDirectory = getCircuitsByFilter(new CircuitFilter() {
public boolean filter(Circuit circuit) {
final boolean exitType = circuit instanceof ExitCircuit;
return exitType && !circuit.isMarkedForClose() && circuit.isConnected();
}
});
final ArrayList<ExitCircuit> ac = new ArrayList<ExitCircuit>();
for(Circuit c: notDirectory) {
if(c instanceof ExitCircuit) {
ac.add((ExitCircuit) c);
}
}
final int sz = ac.size();
for(int i = 0; i < sz; i++) {
final ExitCircuit tmp = ac.get(i);
final int swapIdx = random.nextInt(sz);
ac.set(i, ac.get(swapIdx));
ac.set(swapIdx, tmp);
}
return ac;
}
public Stream openExitStreamTo(String hostname, int port)
throws InterruptedException, TimeoutException, OpenFailedException {
if(hostname.endsWith(".onion")) {
return hiddenServiceManager.getStreamTo(hostname, port);
}
validateHostname(hostname);
circuitCreationTask.predictPort(port);
return pendingExitStreams.openExitStream(hostname, port);
}
private void validateHostname(String hostname) throws OpenFailedException {
maybeRejectInternalAddress(hostname);
if(hostname.toLowerCase().endsWith(".onion")) {
throw new OpenFailedException("Hidden services not supported");
} else if(hostname.toLowerCase().endsWith(".exit")) {
throw new OpenFailedException(".exit addresses are not supported");
}
}
private void maybeRejectInternalAddress(String hostname) throws OpenFailedException {
if(IPv4Address.isValidIPv4AddressString(hostname)) {
maybeRejectInternalAddress(IPv4Address.createFromString(hostname));
}
}
private void maybeRejectInternalAddress(IPv4Address address) throws OpenFailedException {
final InetAddress inetAddress = address.toInetAddress();
if(inetAddress.isSiteLocalAddress() && config.getClientRejectInternalAddress()) {
throw new OpenFailedException("Rejecting stream target with internal address: "+ address);
}
}
public Stream openExitStreamTo(IPv4Address address, int port)
throws InterruptedException, TimeoutException, OpenFailedException {
maybeRejectInternalAddress(address);
circuitCreationTask.predictPort(port);
return pendingExitStreams.openExitStream(address, port);
}
public List<StreamExitRequest> getPendingExitStreams() {
return pendingExitStreams.getUnreservedPendingRequests();
}
public Stream openDirectoryStream() throws OpenFailedException, InterruptedException, TimeoutException {
return openDirectoryStream(0);
}
public Stream openDirectoryStream(int purpose) throws OpenFailedException, InterruptedException {
final int requestEventCode = purposeToEventCode(purpose, false);
final int loadingEventCode = purposeToEventCode(purpose, true);
int failCount = 0;
while(failCount < OPEN_DIRECTORY_STREAM_RETRY_COUNT) {
final DirectoryCircuit circuit = openDirectoryCircuit();
if(requestEventCode > 0) {
initializationTracker.notifyEvent(requestEventCode);
}
try {
final Stream stream = circuit.openDirectoryStream(OPEN_DIRECTORY_STREAM_TIMEOUT, true);
if(loadingEventCode > 0) {
initializationTracker.notifyEvent(loadingEventCode);
}
return stream;
} catch (StreamConnectFailedException e) {
circuit.markForClose();
failCount += 1;
} catch (TimeoutException e) {
circuit.markForClose();
}
}
throw new OpenFailedException("Retry count exceeded opening directory stream");
}
public DirectoryCircuit openDirectoryCircuit() throws OpenFailedException {
int failCount = 0;
while(failCount < OPEN_DIRECTORY_STREAM_RETRY_COUNT) {
final DirectoryCircuit circuit = CircuitImpl.createDirectoryCircuit(this);
if(tryOpenCircuit(circuit, true, true)) {
return circuit;
}
failCount += 1;
}
throw new OpenFailedException("Could not create circuit for directory stream");
}
private int purposeToEventCode(int purpose, boolean getLoadingEvent) {
switch(purpose) {
case DIRECTORY_PURPOSE_CONSENSUS:
return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_STATUS : Tor.BOOTSTRAP_STATUS_REQUESTING_STATUS;
case DIRECTORY_PURPOSE_CERTIFICATES:
return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_KEYS : Tor.BOOTSTRAP_STATUS_REQUESTING_KEYS;
case DIRECTORY_PURPOSE_DESCRIPTORS:
return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_DESCRIPTORS : Tor.BOOTSTRAP_STATUS_REQUESTING_DESCRIPTORS;
default:
return 0;
}
}
private static class DirectoryCircuitResult implements CircuitBuildHandler {
private boolean isFailed;
public void connectionCompleted(Connection connection) {}
public void nodeAdded(CircuitNode node) {}
public void circuitBuildCompleted(Circuit circuit) {}
public void connectionFailed(String reason) {
isFailed = true;
}
public void circuitBuildFailed(String reason) {
isFailed = true;
}
boolean isSuccessful() {
return !isFailed;
}
}
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException {
if((flags & DASHBOARD_CIRCUITS) == 0) {
return;
}
renderer.renderComponent(writer, flags, connectionCache);
renderer.renderComponent(writer, flags, circuitCreationTask.getCircuitPredictor());
writer.println("[Circuit Manager]");
writer.println();
for(Circuit c: getCircuitsByFilter(null)) {
renderer.renderComponent(writer, flags, c);
}
}
public InternalCircuit getCleanInternalCircuit() throws InterruptedException {
synchronized(cleanInternalCircuits) {
try {
requestedInternalCircuitCount += 1;
while(cleanInternalCircuits.isEmpty()) {
cleanInternalCircuits.wait();
}
return cleanInternalCircuits.remove();
} finally {
requestedInternalCircuitCount -= 1;
}
}
}
int getNeededCleanCircuitCount(boolean isPredicted) {
synchronized (cleanInternalCircuits) {
final int predictedCount = (isPredicted) ? 2 : 0;
final int needed = Math.max(requestedInternalCircuitCount, predictedCount) - (pendingInternalCircuitCount + cleanInternalCircuits.size());
if(needed < 0) {
return 0;
} else {
return needed;
}
}
}
void incrementPendingInternalCircuitCount() {
synchronized (cleanInternalCircuits) {
pendingInternalCircuitCount += 1;
}
}
void decrementPendingInternalCircuitCount() {
synchronized (cleanInternalCircuits) {
pendingInternalCircuitCount -= 1;
}
}
void addCleanInternalCircuit(InternalCircuit circuit) {
synchronized(cleanInternalCircuits) {
pendingInternalCircuitCount -= 1;
cleanInternalCircuits.add(circuit);
cleanInternalCircuits.notifyAll();
}
}
boolean isNtorEnabled() {
switch(config.getUseNTorHandshake()) {
case AUTO:
return isNtorEnabledInConsensus();
case FALSE:
return false;
case TRUE:
return true;
default:
throw new IllegalArgumentException("getUseNTorHandshake() returned "+ config.getUseNTorHandshake());
}
}
boolean isNtorEnabledInConsensus() {
ConsensusDocument consensus = directory.getCurrentConsensusDocument();
return (consensus != null) && (consensus.getUseNTorHandshake());
}
public DirectoryCircuit openDirectoryCircuitTo(List<Router> path) throws OpenFailedException {
final DirectoryCircuit circuit = CircuitImpl.createDirectoryCircuitTo(this, path);
if(!tryOpenCircuit(circuit, true, false)) {
throw new OpenFailedException("Could not create directory circuit for path");
}
return circuit;
}
public ExitCircuit openExitCircuitTo(List<Router> path) throws OpenFailedException {
final ExitCircuit circuit = CircuitImpl.createExitCircuitTo(this, path);
if(!tryOpenCircuit(circuit, false, false)) {
throw new OpenFailedException("Could not create exit circuit for path");
}
return circuit;
}
public InternalCircuit openInternalCircuitTo(List<Router> path) throws OpenFailedException {
final InternalCircuit circuit = CircuitImpl.createInternalCircuitTo(this, path);
if(!tryOpenCircuit(circuit, false, false)) {
throw new OpenFailedException("Could not create internal circuit for path");
}
return circuit;
}
private boolean tryOpenCircuit(Circuit circuit, boolean isDirectory, boolean trackInitialization) {
final DirectoryCircuitResult result = new DirectoryCircuitResult();
final CircuitCreationRequest req = new CircuitCreationRequest(pathChooser, circuit, result, isDirectory);
final CircuitBuildTask task = new CircuitBuildTask(req, connectionCache, isNtorEnabled(), (trackInitialization) ? (initializationTracker) : (null));
task.run();
return result.isSuccessful();
}
}

View File

@ -0,0 +1,102 @@
package com.subgraph.orchid.circuits;
import com.subgraph.orchid.Cell;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.crypto.TorMessageDigest;
import com.subgraph.orchid.crypto.TorStreamCipher;
import com.subgraph.orchid.data.HexDigest;
public class CircuitNodeCryptoState {
public final static int KEY_MATERIAL_SIZE = TorMessageDigest.TOR_DIGEST_SIZE * 2 + TorStreamCipher.KEY_LEN * 2;
public static CircuitNodeCryptoState createFromKeyMaterial(byte[] keyMaterial, byte[] verifyDigest) {
return new CircuitNodeCryptoState(keyMaterial, verifyDigest);
}
private final HexDigest checksumDigest;
private final TorMessageDigest forwardDigest;
private final TorMessageDigest backwardDigest;
private final TorStreamCipher forwardCipher;
private final TorStreamCipher backwardCipher;
static private byte[] extractDigestBytes(byte[] keyMaterial, int offset) {
final byte[] digestBytes = new byte[TorMessageDigest.TOR_DIGEST_SIZE];
System.arraycopy(keyMaterial, offset, digestBytes, 0, TorMessageDigest.TOR_DIGEST_SIZE);
return digestBytes;
}
static private byte[] extractCipherKey(byte[] keyMaterial, int offset) {
final byte[] keyBytes = new byte[TorStreamCipher.KEY_LEN];
System.arraycopy(keyMaterial, offset, keyBytes, 0, TorStreamCipher.KEY_LEN);
return keyBytes;
}
private CircuitNodeCryptoState(byte[] keyMaterial, byte[] verifyDigest) {
checksumDigest = HexDigest.createFromDigestBytes(verifyDigest);
int offset = 0;
forwardDigest = new TorMessageDigest();
forwardDigest.update(extractDigestBytes(keyMaterial, offset));
offset += TorMessageDigest.TOR_DIGEST_SIZE;
backwardDigest = new TorMessageDigest();
backwardDigest.update(extractDigestBytes(keyMaterial, offset));
offset += TorMessageDigest.TOR_DIGEST_SIZE;
forwardCipher = TorStreamCipher.createFromKeyBytes(extractCipherKey(keyMaterial, offset));
offset += TorStreamCipher.KEY_LEN;
backwardCipher = TorStreamCipher.createFromKeyBytes(extractCipherKey(keyMaterial, offset));
}
boolean verifyPacketDigest(HexDigest packetDigest) {
return checksumDigest.equals(packetDigest);
}
void encryptForwardCell(Cell cell) {
forwardCipher.encrypt(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
}
boolean decryptBackwardCell(Cell cell) {
backwardCipher.encrypt(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
return isRecognizedCell(cell);
}
void updateForwardDigest(Cell cell) {
forwardDigest.update(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
}
byte[] getForwardDigestBytes() {
return forwardDigest.getDigestBytes();
}
private boolean isRecognizedCell(Cell cell) {
if(cell.getShortAt(RelayCell.RECOGNIZED_OFFSET) != 0)
return false;
final byte[] digest = extractRelayDigest(cell);
final byte[] peek = backwardDigest.peekDigest(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
for(int i = 0; i < 4; i++)
if(digest[i] != peek[i]) {
replaceRelayDigest(cell, digest);
return false;
}
backwardDigest.update(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
replaceRelayDigest(cell, digest);
return true;
}
private byte[] extractRelayDigest(Cell cell) {
final byte[] digest = new byte[4];
for(int i = 0; i < 4; i++) {
digest[i] = (byte) cell.getByteAt(i + RelayCell.DIGEST_OFFSET);
cell.putByteAt(i + RelayCell.DIGEST_OFFSET, 0);
}
return digest;
}
private void replaceRelayDigest(Cell cell, byte[] digest) {
for(int i = 0; i < 4; i++)
cell.putByteAt(i + RelayCell.DIGEST_OFFSET, digest[i] & 0xFF);
}
}

View File

@ -0,0 +1,121 @@
package com.subgraph.orchid.circuits;
import com.subgraph.orchid.Cell;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.TorException;
public class CircuitNodeImpl implements CircuitNode {
public static CircuitNode createAnonymous(CircuitNode previous, byte[] keyMaterial, byte[] verifyDigest) {
return createNode(null, previous, keyMaterial, verifyDigest);
}
public static CircuitNode createFirstHop(Router r, byte[] keyMaterial, byte[] verifyDigest) {
return createNode(r, null, keyMaterial, verifyDigest);
}
public static CircuitNode createNode(Router r, CircuitNode previous, byte[] keyMaterial, byte[] verifyDigest) {
final CircuitNodeCryptoState cs = CircuitNodeCryptoState.createFromKeyMaterial(keyMaterial, verifyDigest);
return new CircuitNodeImpl(r, previous, cs);
}
private final static int CIRCWINDOW_START = 1000;
private final static int CIRCWINDOW_INCREMENT = 100;
private final Router router;
private final CircuitNodeCryptoState cryptoState;
private final CircuitNode previousNode;
private final Object windowLock;
private int packageWindow;
private int deliverWindow;
private CircuitNodeImpl(Router router, CircuitNode previous, CircuitNodeCryptoState cryptoState) {
previousNode = previous;
this.router = router;
this.cryptoState = cryptoState;
windowLock = new Object();
packageWindow = CIRCWINDOW_START;
deliverWindow = CIRCWINDOW_START;
}
public Router getRouter() {
return router;
}
public CircuitNode getPreviousNode() {
return previousNode;
}
public void encryptForwardCell(RelayCell cell) {
cryptoState.encryptForwardCell(cell);
}
public boolean decryptBackwardCell(Cell cell) {
return cryptoState.decryptBackwardCell(cell);
}
public void updateForwardDigest(RelayCell cell) {
cryptoState.updateForwardDigest(cell);
}
public byte[] getForwardDigestBytes() {
return cryptoState.getForwardDigestBytes();
}
public String toString() {
if(router != null) {
return "|"+ router.getNickname() + "|";
} else {
return "|()|";
}
}
public void decrementDeliverWindow() {
synchronized(windowLock) {
deliverWindow--;
}
}
public boolean considerSendingSendme() {
synchronized(windowLock) {
if(deliverWindow <= (CIRCWINDOW_START - CIRCWINDOW_INCREMENT)) {
deliverWindow += CIRCWINDOW_INCREMENT;
return true;
}
return false;
}
}
public void waitForSendWindow() {
waitForSendWindow(false);
}
public void waitForSendWindowAndDecrement() {
waitForSendWindow(true);
}
private void waitForSendWindow(boolean decrement) {
synchronized(windowLock) {
while(packageWindow == 0) {
try {
windowLock.wait();
} catch (InterruptedException e) {
throw new TorException("Thread interrupted while waiting for circuit send window");
}
}
if(decrement)
packageWindow--;
}
}
public void incrementSendWindow() {
synchronized(windowLock) {
packageWindow += CIRCWINDOW_INCREMENT;
windowLock.notifyAll();
}
}
}

View File

@ -0,0 +1,99 @@
package com.subgraph.orchid.circuits;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.subgraph.orchid.dashboard.DashboardRenderable;
import com.subgraph.orchid.dashboard.DashboardRenderer;
public class CircuitPredictor implements DashboardRenderable {
private final static Integer INTERNAL_CIRCUIT_PORT_VALUE = 0;
private final static long TIMEOUT_MS = 60 * 60 * 1000; // One hour
private final Map<Integer, Long> portsSeen;
public CircuitPredictor() {
portsSeen = new HashMap<Integer,Long>();
addExitPortRequest(80);
addInternalRequest();
}
void addExitPortRequest(int port) {
synchronized (portsSeen) {
portsSeen.put(port, System.currentTimeMillis());
}
}
void addInternalRequest() {
addExitPortRequest(INTERNAL_CIRCUIT_PORT_VALUE);
}
private boolean isEntryExpired(Entry<Integer, Long> e, long now) {
return (now - e.getValue()) > TIMEOUT_MS;
}
private void removeExpiredPorts() {
final long now = System.currentTimeMillis();
final Iterator<Entry<Integer, Long>> it = portsSeen.entrySet().iterator();
while(it.hasNext()) {
if(isEntryExpired(it.next(), now)) {
it.remove();
}
}
}
boolean isInternalPredicted() {
synchronized (portsSeen) {
removeExpiredPorts();
return portsSeen.containsKey(INTERNAL_CIRCUIT_PORT_VALUE);
}
}
Set<Integer> getPredictedPorts() {
synchronized (portsSeen) {
removeExpiredPorts();
final Set<Integer> result = new HashSet<Integer>(portsSeen.keySet());
result.remove(INTERNAL_CIRCUIT_PORT_VALUE);
return result;
}
}
List<PredictedPortTarget> getPredictedPortTargets() {
final List<PredictedPortTarget> targets = new ArrayList<PredictedPortTarget>();
for(int p: getPredictedPorts()) {
targets.add(new PredictedPortTarget(p));
}
return targets;
}
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags)
throws IOException {
if((flags & DASHBOARD_PREDICTED_PORTS) == 0) {
return;
}
writer.println("[Predicted Ports] ");
for(int port : portsSeen.keySet()) {
writer.write(" "+ port);
Long lastSeen = portsSeen.get(port);
if(lastSeen != null) {
long now = System.currentTimeMillis();
long ms = now - lastSeen;
writer.write(" (last seen "+ TimeUnit.MINUTES.convert(ms, TimeUnit.MILLISECONDS) +" minutes ago)");
}
writer.println();
}
writer.println();
}
}

View File

@ -0,0 +1,115 @@
package com.subgraph.orchid.circuits;
import com.subgraph.orchid.crypto.TorRandom;
public class CircuitStatus {
enum CircuitState {
UNCONNECTED("Unconnected"),
BUILDING("Building"),
FAILED("Failed"),
OPEN("Open"),
DESTROYED("Destroyed");
String name;
CircuitState(String name) { this.name = name; }
public String toString() { return name; }
}
private long timestampCreated;
private long timestampDirty;
private int currentStreamId;
private Object streamIdLock = new Object();
private volatile CircuitState state = CircuitState.UNCONNECTED;
CircuitStatus() {
initializeCurrentStreamId();
}
private void initializeCurrentStreamId() {
final TorRandom random = new TorRandom();
currentStreamId = random.nextInt(0xFFFF) + 1;
}
synchronized void updateCreatedTimestamp() {
timestampCreated = System.currentTimeMillis();
timestampDirty = 0;
}
synchronized void updateDirtyTimestamp() {
if(timestampDirty == 0 && state != CircuitState.BUILDING) {
timestampDirty = System.currentTimeMillis();
}
}
synchronized long getMillisecondsElapsedSinceCreated() {
return millisecondsElapsedSince(timestampCreated);
}
synchronized long getMillisecondsDirty() {
return millisecondsElapsedSince(timestampDirty);
}
private static long millisecondsElapsedSince(long then) {
if(then == 0) {
return 0;
}
final long now = System.currentTimeMillis();
return now - then;
}
synchronized boolean isDirty() {
return timestampDirty != 0;
}
void setStateBuilding() {
state = CircuitState.BUILDING;
}
void setStateFailed() {
state = CircuitState.FAILED;
}
void setStateOpen() {
state = CircuitState.OPEN;
}
void setStateDestroyed() {
state = CircuitState.DESTROYED;
}
boolean isBuilding() {
return state == CircuitState.BUILDING;
}
boolean isConnected() {
return state == CircuitState.OPEN;
}
boolean isUnconnected() {
return state == CircuitState.UNCONNECTED;
}
String getStateAsString() {
if(state == CircuitState.OPEN) {
return state.toString() + " ["+ getDirtyString() + "]";
}
return state.toString();
}
private String getDirtyString() {
if(!isDirty()) {
return "Clean";
} else {
return "Dirty "+ (getMillisecondsDirty() / 1000) +"s";
}
}
int nextStreamId() {
synchronized(streamIdLock) {
currentStreamId++;
if(currentStreamId > 0xFFFF)
currentStreamId = 1;
return currentStreamId;
}
}
}

View File

@ -0,0 +1,42 @@
package com.subgraph.orchid.circuits;
import java.util.List;
import java.util.concurrent.TimeoutException;
import com.subgraph.orchid.DirectoryCircuit;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
public class DirectoryCircuitImpl extends CircuitImpl implements DirectoryCircuit {
protected DirectoryCircuitImpl(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
super(circuitManager, prechosenPath);
}
public Stream openDirectoryStream(long timeout, boolean autoclose) throws InterruptedException, TimeoutException, StreamConnectFailedException {
final StreamImpl stream = createNewStream(autoclose);
try {
stream.openDirectory(timeout);
return stream;
} catch (Exception e) {
removeStream(stream);
return processStreamOpenException(e);
}
}
@Override
protected List<Router> choosePathForCircuit(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException {
if(prechosenPath != null) {
return prechosenPath;
}
return pathChooser.chooseDirectoryPath();
}
@Override
protected String getCircuitTypeLabel() {
return "Directory";
}
}

View File

@ -0,0 +1,87 @@
package com.subgraph.orchid.circuits;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import com.subgraph.orchid.ExitCircuit;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
public class ExitCircuitImpl extends CircuitImpl implements ExitCircuit {
private final Router exitRouter;
private final Set<ExitTarget> failedExitRequests;
ExitCircuitImpl(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
super(circuitManager, prechosenPath);
this.exitRouter = prechosenPath.get(prechosenPath.size() - 1);
this.failedExitRequests = new HashSet<ExitTarget>();
}
ExitCircuitImpl(CircuitManagerImpl circuitManager, Router exitRouter) {
super(circuitManager);
this.exitRouter = exitRouter;
this.failedExitRequests = new HashSet<ExitTarget>();
}
public Stream openExitStream(IPv4Address address, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
return openExitStream(address.toString(), port, timeout);
}
public Stream openExitStream(String target, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
final StreamImpl stream = createNewStream();
try {
stream.openExit(target, port, timeout);
return stream;
} catch (Exception e) {
removeStream(stream);
return processStreamOpenException(e);
}
}
public void recordFailedExitTarget(ExitTarget target) {
synchronized(failedExitRequests) {
failedExitRequests.add(target);
}
}
public boolean canHandleExitTo(ExitTarget target) {
synchronized(failedExitRequests) {
if(failedExitRequests.contains(target)) {
return false;
}
}
if(isMarkedForClose()) {
return false;
}
if(target.isAddressTarget()) {
return exitRouter.exitPolicyAccepts(target.getAddress(), target.getPort());
} else {
return exitRouter.exitPolicyAccepts(target.getPort());
}
}
public boolean canHandleExitToPort(int port) {
return exitRouter.exitPolicyAccepts(port);
}
@Override
protected List<Router> choosePathForCircuit(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException {
return pathChooser.choosePathWithExit(exitRouter);
}
@Override
protected String getCircuitTypeLabel() {
return "Exit";
}
}

View File

@ -0,0 +1,118 @@
package com.subgraph.orchid.circuits;
import java.util.List;
import java.util.concurrent.TimeoutException;
import com.subgraph.orchid.Circuit;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.DirectoryCircuit;
import com.subgraph.orchid.HiddenServiceCircuit;
import com.subgraph.orchid.InternalCircuit;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
public class InternalCircuitImpl extends CircuitImpl implements InternalCircuit, DirectoryCircuit, HiddenServiceCircuit {
private enum InternalType { UNUSED, HS_INTRODUCTION, HS_DIRECTORY, HS_CIRCUIT }
private InternalType type;
private boolean ntorEnabled;
InternalCircuitImpl(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
super(circuitManager, prechosenPath);
this.type = InternalType.UNUSED;
this.ntorEnabled = circuitManager.isNtorEnabled();
}
protected InternalCircuitImpl(CircuitManagerImpl circuitManager) {
this(circuitManager, null);
}
@Override
protected List<Router> choosePathForCircuit(CircuitPathChooser pathChooser)
throws InterruptedException, PathSelectionFailedException {
return pathChooser.chooseInternalPath();
}
public Circuit cannibalizeToIntroductionPoint(Router target) {
cannibalizeTo(target);
type = InternalType.HS_INTRODUCTION;
return this;
}
private void cannibalizeTo(Router target) {
if(type != InternalType.UNUSED) {
throw new IllegalStateException("Cannot cannibalize internal circuit with type "+ type);
}
final CircuitExtender extender = new CircuitExtender(this, ntorEnabled);
extender.extendTo(target);
}
public Stream openDirectoryStream(long timeout, boolean autoclose) throws InterruptedException, TimeoutException, StreamConnectFailedException {
if(type != InternalType.HS_DIRECTORY) {
throw new IllegalStateException("Cannot open directory stream on internal circuit with type "+ type);
}
final StreamImpl stream = createNewStream();
try {
stream.openDirectory(timeout);
return stream;
} catch (Exception e) {
removeStream(stream);
return processStreamOpenException(e);
}
}
public DirectoryCircuit cannibalizeToDirectory(Router target) {
cannibalizeTo(target);
type = InternalType.HS_DIRECTORY;
return this;
}
public HiddenServiceCircuit connectHiddenService(CircuitNode node) {
if(type != InternalType.UNUSED) {
throw new IllegalStateException("Cannot connect hidden service from internal circuit type "+ type);
}
appendNode(node);
type = InternalType.HS_CIRCUIT;
return this;
}
public Stream openStream(int port, long timeout)
throws InterruptedException, TimeoutException, StreamConnectFailedException {
if(type != InternalType.HS_CIRCUIT) {
throw new IllegalStateException("Cannot open stream to hidden service from internal circuit type "+ type);
}
final StreamImpl stream = createNewStream();
try {
stream.openExit("", port, timeout);
return stream;
} catch (Exception e) {
removeStream(stream);
return processStreamOpenException(e);
}
}
@Override
protected String getCircuitTypeLabel() {
switch(type) {
case HS_CIRCUIT:
return "Hidden Service";
case HS_DIRECTORY:
return "HS Directory";
case HS_INTRODUCTION:
return "HS Introduction";
case UNUSED:
return "Internal";
default:
return "(null)";
}
}
}

View File

@ -0,0 +1,114 @@
package com.subgraph.orchid.circuits;
import java.util.logging.Logger;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.TorException;
import com.subgraph.orchid.crypto.TorMessageDigest;
import com.subgraph.orchid.crypto.TorNTorKeyAgreement;
public class NTorCircuitExtender {
private final static Logger logger = Logger.getLogger(NTorCircuitExtender.class.getName());
private final CircuitExtender extender;
private final Router router;
private final TorNTorKeyAgreement kex;
public NTorCircuitExtender(CircuitExtender extender, Router router) {
this.extender = extender;
this.router = router;
this.kex = new TorNTorKeyAgreement(router.getIdentityHash(), router.getNTorOnionKey());
}
CircuitNode extendTo() {
final byte[] onion = kex.createOnionSkin();
if(finalRouterSupportsExtend2()) {
logger.fine("Extending circuit to "+ router.getNickname() + " with NTor inside RELAY_EXTEND2");
return extendWithExtend2(onion);
} else {
logger.fine("Extending circuit to "+ router.getNickname() + " with NTor inside RELAY_EXTEND");
return extendWithTunneledExtend(onion);
}
}
private CircuitNode extendWithExtend2(byte[] onion) {
final RelayCell cell = createExtend2Cell(onion);
extender.sendRelayCell(cell);
final RelayCell response = extender.receiveRelayResponse(RelayCell.RELAY_EXTENDED2, router);
return processExtended2(response);
}
private CircuitNode extendWithTunneledExtend(byte[] onion) {
final RelayCell cell = createExtendCell(onion, kex.getNtorCreateMagic());
extender.sendRelayCell(cell);
final RelayCell response = extender.receiveRelayResponse(RelayCell.RELAY_EXTENDED, router);
return processExtended(response);
}
private boolean finalRouterSupportsExtend2() {
return extender.getFinalRouter().getNTorOnionKey() != null;
}
private RelayCell createExtend2Cell(byte[] ntorOnionskin) {
final RelayCell cell = extender.createRelayCell(RelayCell.RELAY_EXTEND2);
cell.putByte(2);
cell.putByte(0);
cell.putByte(6);
cell.putByteArray(router.getAddress().getAddressDataBytes());
cell.putShort(router.getOnionPort());
cell.putByte(2);
cell.putByte(20);
cell.putByteArray(router.getIdentityHash().getRawBytes());
cell.putShort(0x0002);
cell.putShort(ntorOnionskin.length);
cell.putByteArray(ntorOnionskin);
return cell;
}
private RelayCell createExtendCell(byte[] ntorOnionskin, byte[] ntorMagic) {
final RelayCell cell = extender.createRelayCell(RelayCell.RELAY_EXTEND);
cell.putByteArray(router.getAddress().getAddressDataBytes());
cell.putShort(router.getOnionPort());
final int paddingLength = CircuitExtender.TAP_ONIONSKIN_LEN - (ntorOnionskin.length + ntorMagic.length);
final byte[] padding = new byte[paddingLength];
cell.putByteArray(ntorMagic);
cell.putByteArray(ntorOnionskin);
cell.putByteArray(padding);
cell.putByteArray(router.getIdentityHash().getRawBytes());
return cell;
}
private CircuitNode processExtended(RelayCell cell) {
byte[] payload = new byte[CircuitExtender.TAP_ONIONSKIN_REPLY_LEN];
cell.getByteArray(payload);
return processPayload(payload);
}
private CircuitNode processExtended2(RelayCell cell) {
final int payloadLength = cell.getShort();
if(payloadLength > cell.cellBytesRemaining()) {
throw new TorException("Incorrect payload length value in RELAY_EXTENED2 cell");
}
byte[] payload = new byte[payloadLength];
cell.getByteArray(payload);
return processPayload(payload);
}
private CircuitNode processPayload(byte[] payload) {
final byte[] keyMaterial = new byte[CircuitNodeCryptoState.KEY_MATERIAL_SIZE];
final byte[] verifyDigest = new byte[TorMessageDigest.TOR_DIGEST_SIZE];
if(!kex.deriveKeysFromHandshakeResponse(payload, keyMaterial, verifyDigest)) {
return null;
}
return extender.createNewNode(router, keyMaterial, verifyDigest);
}
}

View File

@ -0,0 +1,50 @@
package com.subgraph.orchid.circuits;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import com.subgraph.orchid.ExitCircuit;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
public class OpenExitStreamTask implements Runnable {
private final static Logger logger = Logger.getLogger(OpenExitStreamTask.class.getName());
private final ExitCircuit circuit;
private final StreamExitRequest exitRequest;
OpenExitStreamTask(ExitCircuit circuit, StreamExitRequest exitRequest) {
this.circuit = circuit;
this.exitRequest = exitRequest;
}
public void run() {
logger.fine("Attempting to open stream to "+ exitRequest);
try {
exitRequest.setCompletedSuccessfully(tryOpenExitStream());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
exitRequest.setInterrupted();
} catch (TimeoutException e) {
circuit.markForClose();
exitRequest.setCompletedTimeout();
} catch (StreamConnectFailedException e) {
if(!e.isReasonRetryable()) {
exitRequest.setExitFailed();
circuit.recordFailedExitTarget(exitRequest);
} else {
circuit.markForClose();
exitRequest.setStreamOpenFailure(e.getReason());
}
}
}
private Stream tryOpenExitStream() throws InterruptedException, TimeoutException, StreamConnectFailedException {
if(exitRequest.isAddressTarget()) {
return circuit.openExitStream(exitRequest.getAddress(), exitRequest.getPort(), exitRequest.getStreamTimeout());
} else {
return circuit.openExitStream(exitRequest.getHostname(), exitRequest.getPort(), exitRequest.getStreamTimeout());
}
}
}

View File

@ -0,0 +1,77 @@
package com.subgraph.orchid.circuits;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import com.subgraph.orchid.OpenFailedException;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.TorConfig;
import com.subgraph.orchid.data.IPv4Address;
public class PendingExitStreams {
private final Set<StreamExitRequest> pendingRequests;
private final Object lock = new Object();
private final TorConfig config;
PendingExitStreams(TorConfig config) {
this.config = config;
pendingRequests = new HashSet<StreamExitRequest>();
}
Stream openExitStream(IPv4Address address, int port) throws InterruptedException, OpenFailedException {
final StreamExitRequest request = new StreamExitRequest(lock, address, port);
return openExitStreamByRequest(request);
}
Stream openExitStream(String hostname, int port) throws InterruptedException, OpenFailedException {
final StreamExitRequest request = new StreamExitRequest(lock, hostname, port);
return openExitStreamByRequest(request);
}
private Stream openExitStreamByRequest(StreamExitRequest request) throws InterruptedException, OpenFailedException {
if(config.getCircuitStreamTimeout() != 0) {
request.setStreamTimeout(config.getCircuitStreamTimeout());
}
synchronized(lock) {
pendingRequests.add(request);
try {
return handleRequest(request);
} finally {
pendingRequests.remove(request);
}
}
}
private Stream handleRequest(StreamExitRequest request) throws InterruptedException, OpenFailedException {
while(true) {
while(!request.isCompleted()) {
lock.wait();
}
try {
return request.getStream();
} catch (TimeoutException e) {
request.resetForRetry();
} catch (StreamConnectFailedException e) {
request.resetForRetry();
}
}
}
List<StreamExitRequest> getUnreservedPendingRequests() {
final List<StreamExitRequest> result = new ArrayList<StreamExitRequest>();
synchronized (lock) {
for(StreamExitRequest request: pendingRequests) {
if(!request.isReserved()) {
result.add(request);
}
}
}
return result;
}
}

View File

@ -0,0 +1,29 @@
package com.subgraph.orchid.circuits;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
public class PredictedPortTarget implements ExitTarget {
final int port;
public PredictedPortTarget(int port) {
this.port = port;
}
public boolean isAddressTarget() {
return false;
}
public IPv4Address getAddress() {
return new IPv4Address(0);
}
public String getHostname() {
return "";
}
public int getPort() {
return port;
}
}

View File

@ -0,0 +1,170 @@
package com.subgraph.orchid.circuits;
import java.util.concurrent.TimeoutException;
import com.subgraph.orchid.OpenFailedException;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
import com.subgraph.orchid.misc.GuardedBy;
public class StreamExitRequest implements ExitTarget {
private enum CompletionStatus {NOT_COMPLETED, SUCCESS, TIMEOUT, STREAM_OPEN_FAILURE, EXIT_FAILURE, INTERRUPTED};
private final boolean isAddress;
private final IPv4Address address;
private final String hostname;
private final int port;
private final Object requestCompletionLock;
@GuardedBy("requestCompletionLock") private CompletionStatus completionStatus;
@GuardedBy("requestCompletionLock") private Stream stream;
@GuardedBy("requestCompletionLock") private int streamOpenFailReason;
@GuardedBy("this") private boolean isReserved;
@GuardedBy("this") private int retryCount;
@GuardedBy("this") private long specificTimeout;
StreamExitRequest(Object requestCompletionLock, IPv4Address address, int port) {
this(requestCompletionLock, true, "", address, port);
}
StreamExitRequest(Object requestCompletionLock, String hostname, int port) {
this(requestCompletionLock, false, hostname, null, port);
}
private StreamExitRequest(Object requestCompletionLock, boolean isAddress, String hostname, IPv4Address address, int port) {
this.requestCompletionLock = requestCompletionLock;
this.isAddress = isAddress;
this.hostname = hostname;
this.address = address;
this.port = port;
this.completionStatus = CompletionStatus.NOT_COMPLETED;
}
public boolean isAddressTarget() {
return isAddress;
}
public IPv4Address getAddress() {
return address;
}
public String getHostname() {
return hostname;
}
public int getPort() {
return port;
}
public synchronized void setStreamTimeout(long timeout) {
specificTimeout = timeout;
}
public synchronized long getStreamTimeout() {
if(specificTimeout > 0) {
return specificTimeout;
} else if(retryCount < 2) {
return 10 * 1000;
} else {
return 15 * 1000;
}
}
void setCompletedTimeout() {
synchronized (requestCompletionLock) {
newStatus(CompletionStatus.TIMEOUT);
}
}
void setExitFailed() {
synchronized (requestCompletionLock) {
newStatus(CompletionStatus.EXIT_FAILURE);
}
}
void setStreamOpenFailure(int reason) {
synchronized (requestCompletionLock) {
streamOpenFailReason = reason;
newStatus(CompletionStatus.STREAM_OPEN_FAILURE);
}
}
void setCompletedSuccessfully(Stream stream) {
synchronized (requestCompletionLock) {
this.stream = stream;
newStatus(CompletionStatus.SUCCESS);
}
}
void setInterrupted() {
synchronized (requestCompletionLock) {
newStatus(CompletionStatus.INTERRUPTED);
}
}
private void newStatus(CompletionStatus newStatus) {
if(completionStatus != CompletionStatus.NOT_COMPLETED) {
throw new IllegalStateException("Attempt to set completion state to " + newStatus +" while status is "+ completionStatus);
}
completionStatus = newStatus;
requestCompletionLock.notifyAll();
}
Stream getStream() throws OpenFailedException, TimeoutException, StreamConnectFailedException, InterruptedException {
synchronized(requestCompletionLock) {
switch(completionStatus) {
case NOT_COMPLETED:
throw new IllegalStateException("Request not completed");
case EXIT_FAILURE:
throw new OpenFailedException("Failure at exit node");
case TIMEOUT:
throw new TimeoutException();
case STREAM_OPEN_FAILURE:
throw new StreamConnectFailedException(streamOpenFailReason);
case INTERRUPTED:
throw new InterruptedException();
case SUCCESS:
return stream;
default:
throw new IllegalStateException("Unknown completion status");
}
}
}
synchronized void resetForRetry() {
synchronized (requestCompletionLock) {
streamOpenFailReason = 0;
completionStatus = CompletionStatus.NOT_COMPLETED;
}
retryCount += 1;
isReserved = false;
}
boolean isCompleted() {
synchronized (requestCompletionLock) {
return completionStatus != CompletionStatus.NOT_COMPLETED;
}
}
synchronized boolean reserveRequest() {
if(isReserved) return false;
isReserved = true;
return true;
}
synchronized boolean isReserved() {
return isReserved;
}
public String toString() {
if(isAddress)
return address + ":"+ port;
else
return hostname + ":"+ port;
}
}

View File

@ -0,0 +1,219 @@
package com.subgraph.orchid.circuits;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import com.subgraph.orchid.Circuit;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.TorException;
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
import com.subgraph.orchid.dashboard.DashboardRenderable;
import com.subgraph.orchid.dashboard.DashboardRenderer;
public class StreamImpl implements Stream, DashboardRenderable {
private final static Logger logger = Logger.getLogger(StreamImpl.class.getName());
private final static int STREAMWINDOW_START = 500;
private final static int STREAMWINDOW_INCREMENT = 50;
private final static int STREAMWINDOW_MAX_UNFLUSHED = 10;
private final CircuitImpl circuit;
private final int streamId;
private final boolean autoclose;
private final CircuitNode targetNode;
private final TorInputStream inputStream;
private final TorOutputStream outputStream;
private boolean isClosed;
private boolean relayEndReceived;
private int relayEndReason;
private boolean relayConnectedReceived;
private final Object waitConnectLock = new Object();
private final Object windowLock = new Object();
private int packageWindow;
private int deliverWindow;
private String streamTarget = "";
StreamImpl(CircuitImpl circuit, CircuitNode targetNode, int streamId, boolean autoclose) {
this.circuit = circuit;
this.targetNode = targetNode;
this.streamId = streamId;
this.autoclose = autoclose;
this.inputStream = new TorInputStream(this);
this.outputStream = new TorOutputStream(this);
packageWindow = STREAMWINDOW_START;
deliverWindow = STREAMWINDOW_START;
}
void addInputCell(RelayCell cell) {
if(isClosed)
return;
if(cell.getRelayCommand() == RelayCell.RELAY_END) {
synchronized(waitConnectLock) {
relayEndReason = cell.getByte();
relayEndReceived = true;
inputStream.addEndCell(cell);
waitConnectLock.notifyAll();
}
} else if(cell.getRelayCommand() == RelayCell.RELAY_CONNECTED) {
synchronized(waitConnectLock) {
relayConnectedReceived = true;
waitConnectLock.notifyAll();
}
} else if(cell.getRelayCommand() == RelayCell.RELAY_SENDME) {
synchronized(windowLock) {
packageWindow += STREAMWINDOW_INCREMENT;
windowLock.notifyAll();
}
}
else {
inputStream.addInputCell(cell);
synchronized(windowLock) {
deliverWindow--;
if(deliverWindow < 0)
throw new TorException("Stream has negative delivery window");
}
considerSendingSendme();
}
}
private void considerSendingSendme() {
synchronized(windowLock) {
if(deliverWindow > (STREAMWINDOW_START - STREAMWINDOW_INCREMENT))
return;
if(inputStream.unflushedCellCount() >= STREAMWINDOW_MAX_UNFLUSHED)
return;
final RelayCell sendme = circuit.createRelayCell(RelayCell.RELAY_SENDME, streamId, targetNode);
circuit.sendRelayCell(sendme);
deliverWindow += STREAMWINDOW_INCREMENT;
}
}
public int getStreamId() {
return streamId;
}
public Circuit getCircuit() {
return circuit;
}
public CircuitNode getTargetNode() {
return targetNode;
}
public void close() {
if(isClosed)
return;
logger.fine("Closing stream "+ this);
isClosed = true;
inputStream.close();
outputStream.close();
circuit.removeStream(this);
if(autoclose) {
circuit.markForClose();
}
if(!relayEndReceived) {
final RelayCell cell = new RelayCellImpl(circuit.getFinalCircuitNode(), circuit.getCircuitId(), streamId, RelayCell.RELAY_END);
cell.putByte(RelayCell.REASON_DONE);
circuit.sendRelayCellToFinalNode(cell);
}
}
public void openDirectory(long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
streamTarget = "[Directory]";
final RelayCell cell = new RelayCellImpl(circuit.getFinalCircuitNode(), circuit.getCircuitId(), streamId, RelayCell.RELAY_BEGIN_DIR);
circuit.sendRelayCellToFinalNode(cell);
waitForRelayConnected(timeout);
}
void openExit(String target, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
streamTarget = target + ":"+ port;
final RelayCell cell = new RelayCellImpl(circuit.getFinalCircuitNode(), circuit.getCircuitId(), streamId, RelayCell.RELAY_BEGIN);
cell.putString(target + ":"+ port);
circuit.sendRelayCellToFinalNode(cell);
waitForRelayConnected(timeout);
}
private void waitForRelayConnected(long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
final long start = System.currentTimeMillis();
long elapsed = 0;
synchronized(waitConnectLock) {
while(!relayConnectedReceived) {
if(relayEndReceived) {
throw new StreamConnectFailedException(relayEndReason);
}
if(elapsed >= timeout) {
throw new TimeoutException();
}
waitConnectLock.wait(timeout - elapsed);
elapsed = System.currentTimeMillis() - start;
}
}
}
public InputStream getInputStream() {
return inputStream;
}
public OutputStream getOutputStream() {
return outputStream;
}
public void waitForSendWindowAndDecrement() {
waitForSendWindow(true);
}
public void waitForSendWindow() {
waitForSendWindow(false);
}
public void waitForSendWindow(boolean decrement) {
synchronized(windowLock) {
while(packageWindow == 0) {
try {
windowLock.wait();
} catch (InterruptedException e) {
throw new TorException("Thread interrupted while waiting for stream package window");
}
}
if(decrement)
packageWindow--;
}
targetNode.waitForSendWindow();
}
public String toString() {
return "[Stream stream_id="+ streamId + " circuit="+ circuit +" target="+ streamTarget +"]";
}
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException {
writer.print(" ");
writer.print("[Stream stream_id="+ streamId + " cid="+ circuit.getCircuitId());
if(relayConnectedReceived) {
writer.print(" sent="+outputStream.getBytesSent() + " recv="+ inputStream.getBytesReceived());
} else {
writer.print(" (waiting connect)");
}
writer.print(" target="+ streamTarget);
writer.println("]");
}
}

View File

@ -0,0 +1,55 @@
package com.subgraph.orchid.circuits;
import java.util.logging.Logger;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.crypto.TorMessageDigest;
import com.subgraph.orchid.crypto.TorTapKeyAgreement;
public class TapCircuitExtender {
private final static Logger logger = Logger.getLogger(TapCircuitExtender.class.getName());
private final CircuitExtender extender;
private final TorTapKeyAgreement kex;
private final Router router;
public TapCircuitExtender(CircuitExtender extender, Router router) {
this.extender = extender;
this.router = router;
this.kex = new TorTapKeyAgreement(router.getOnionKey());
}
public CircuitNode extendTo() {
logger.fine("Extending to "+ router.getNickname() + " with TAP");
final RelayCell cell = createRelayExtendCell();
extender.sendRelayCell(cell);
final RelayCell response = extender.receiveRelayResponse(RelayCell.RELAY_EXTENDED, router);
if(response == null) {
return null;
}
return processExtendResponse(response);
}
private CircuitNode processExtendResponse(RelayCell response) {
final byte[] handshakeResponse = new byte[TorTapKeyAgreement.DH_LEN + TorMessageDigest.TOR_DIGEST_SIZE];
response.getByteArray(handshakeResponse);
final byte[] keyMaterial = new byte[CircuitNodeCryptoState.KEY_MATERIAL_SIZE];
final byte[] verifyDigest = new byte[TorMessageDigest.TOR_DIGEST_SIZE];
if(!kex.deriveKeysFromHandshakeResponse(handshakeResponse, keyMaterial, verifyDigest)) {
return null;
}
return extender.createNewNode(router, keyMaterial, verifyDigest);
}
private RelayCell createRelayExtendCell() {
final RelayCell cell = extender.createRelayCell(RelayCell.RELAY_EXTEND);
cell.putByteArray(router.getAddress().getAddressDataBytes());
cell.putShort(router.getOnionPort());
cell.putByteArray(kex.createOnionSkin());
cell.putByteArray(router.getIdentityHash().getRawBytes());
return cell;
}
}

View File

@ -0,0 +1,103 @@
package com.subgraph.orchid.circuits;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.subgraph.orchid.Tor;
import com.subgraph.orchid.TorInitializationListener;
public class TorInitializationTracker {
private final static Logger logger = Logger.getLogger(TorInitializationTracker.class.getName());
private final static Map<Integer, String> messageMap = new HashMap<Integer, String>();
static {
messageMap.put(Tor.BOOTSTRAP_STATUS_STARTING, "Starting");
messageMap.put(Tor.BOOTSTRAP_STATUS_CONN_DIR, "Connecting to directory server");
messageMap.put(Tor.BOOTSTRAP_STATUS_HANDSHAKE_DIR, "Finishing handshake with directory server");
messageMap.put(Tor.BOOTSTRAP_STATUS_ONEHOP_CREATE, "Establishing an encrypted directory connection");
messageMap.put(Tor.BOOTSTRAP_STATUS_REQUESTING_STATUS, "Asking for networkstatus consensus");
messageMap.put(Tor.BOOTSTRAP_STATUS_LOADING_STATUS, "Loading networkstatus consensus");
messageMap.put(Tor.BOOTSTRAP_STATUS_REQUESTING_KEYS, "Asking for authority key certs");
messageMap.put(Tor.BOOTSTRAP_STATUS_LOADING_KEYS, "Loading authority key certs");
messageMap.put(Tor.BOOTSTRAP_STATUS_REQUESTING_DESCRIPTORS, "Asking for relay descriptors");
messageMap.put(Tor.BOOTSTRAP_STATUS_LOADING_DESCRIPTORS, "Loading relay descriptors");
messageMap.put(Tor.BOOTSTRAP_STATUS_CONN_OR, "Connecting to the Tor network");
messageMap.put(Tor.BOOTSTRAP_STATUS_HANDSHAKE_OR, "Finished Handshake with first hop");
messageMap.put(Tor.BOOTSTRAP_STATUS_CIRCUIT_CREATE, "Establishing a Tor circuit");
messageMap.put(Tor.BOOTSTRAP_STATUS_DONE, "Done");
}
private final List<TorInitializationListener> listeners = new ArrayList<TorInitializationListener>();
private final Object stateLock = new Object();
private int bootstrapState = Tor.BOOTSTRAP_STATUS_STARTING;
public void addListener(TorInitializationListener listener) {
synchronized(listeners) {
if(!listeners.contains(listener)) {
listeners.add(listener);
}
}
}
public void removeListener(TorInitializationListener listener) {
synchronized(listeners) {
listeners.remove(listener);
}
}
public int getBootstrapState() {
return bootstrapState;
}
public void start() {
synchronized (stateLock) {
bootstrapState = Tor.BOOTSTRAP_STATUS_STARTING;
notifyListeners(Tor.BOOTSTRAP_STATUS_STARTING);
}
}
public void notifyEvent(int eventCode) {
synchronized(stateLock) {
if(eventCode <= bootstrapState || eventCode > 100) {
return;
}
bootstrapState = eventCode;
notifyListeners(eventCode);
}
}
private void notifyListeners(int code) {
final String message = getMessageForCode(code);
for(TorInitializationListener listener: getListeners()) {
try {
listener.initializationProgress(message, code);
if(code >= 100) {
listener.initializationCompleted();
}
} catch(Exception e) {
logger.log(Level.SEVERE, "Exception occurred in TorInitializationListener callback: "+ e.getMessage(), e);
}
}
}
private String getMessageForCode(int code) {
if(messageMap.containsKey(code)) {
return messageMap.get(code);
} else {
return "Unknown state";
}
}
private List<TorInitializationListener> getListeners() {
synchronized (listeners) {
return new ArrayList<TorInitializationListener>(listeners);
}
}
}

View File

@ -0,0 +1,228 @@
package com.subgraph.orchid.circuits;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Queue;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
import com.subgraph.orchid.misc.GuardedBy;
import com.subgraph.orchid.misc.ThreadSafe;
@ThreadSafe
public class TorInputStream extends InputStream {
private final static RelayCell CLOSE_SENTINEL = new RelayCellImpl(null, 0, 0, 0);
private final static ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
private final Stream stream;
private final Object lock = new Object();
/** Queue of RelayCells that have been received on this stream */
@GuardedBy("lock") private final Queue<RelayCell> incomingCells;
/** Number of unread data bytes in current buffer and in RELAY_DATA cells on queue */
@GuardedBy("lock") private int availableBytes;
/** Total number of data bytes received in RELAY_DATA cells on this stream */
@GuardedBy("lock") private long bytesReceived;
/** Bytes of data from the RELAY_DATA cell currently being consumed */
@GuardedBy("lock") private ByteBuffer currentBuffer;
/** Set when a RELAY_END cell is received */
@GuardedBy("lock") private boolean isEOF;
/** Set when close() is called on this stream */
@GuardedBy("lock") private boolean isClosed;
TorInputStream(Stream stream) {
this.stream = stream;
this.incomingCells = new LinkedList<RelayCell>();
this.currentBuffer = EMPTY_BUFFER;
}
long getBytesReceived() {
synchronized (lock) {
return bytesReceived;
}
}
@Override
public int read() throws IOException {
synchronized (lock) {
if(isClosed) {
throw new IOException("Stream closed");
}
refillBufferIfNeeded();
if(isEOF) {
return -1;
}
availableBytes -= 1;
return currentBuffer.get() & 0xFF;
}
}
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
public synchronized int read(byte[] b, int off, int len) throws IOException {
synchronized (lock) {
if(isClosed) {
throw new IOException("Stream closed");
}
checkReadArguments(b, off, len);
if(len == 0) {
return 0;
}
refillBufferIfNeeded();
if(isEOF) {
return -1;
}
int bytesRead = 0;
int bytesRemaining = len;
while(bytesRemaining > 0 && !isEOF) {
refillBufferIfNeeded();
bytesRead += readFromCurrentBuffer(b, off + bytesRead, len - bytesRead);
bytesRemaining = len - bytesRead;
if(availableBytes == 0) {
return bytesRead;
}
}
return bytesRead;
}
}
@GuardedBy("lock")
private int readFromCurrentBuffer(byte[] b, int off, int len) {
final int readLength = (currentBuffer.remaining() >= len) ? (len) : (currentBuffer.remaining());
currentBuffer.get(b, off, readLength);
availableBytes -= readLength;
return readLength;
}
private void checkReadArguments(byte[] b, int off, int len) {
if(b == null) {
throw new NullPointerException();
}
if( (off < 0) || (off >= b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
}
}
public int available() {
synchronized(lock) {
return availableBytes;
}
}
public void close() {
synchronized (lock) {
if(isClosed) {
return;
}
isClosed = true;
incomingCells.add(CLOSE_SENTINEL);
lock.notifyAll();
}
stream.close();
}
void addEndCell(RelayCell cell) {
synchronized (lock) {
if(isClosed) {
return;
}
incomingCells.add(cell);
lock.notifyAll();
}
}
void addInputCell(RelayCell cell) {
synchronized (lock) {
if(isClosed) {
return;
}
incomingCells.add(cell);
bytesReceived += cell.cellBytesRemaining();
availableBytes += cell.cellBytesRemaining();
lock.notifyAll();
}
}
@GuardedBy("lock")
// When this method (or fillBuffer()) returns either isEOF is set or currentBuffer has at least one byte to read
private void refillBufferIfNeeded() throws IOException {
if(!isEOF) {
if(currentBuffer.hasRemaining()) {
return;
}
fillBuffer();
}
}
@GuardedBy("lock")
private void fillBuffer() throws IOException {
while(true) {
processIncomingCell(getNextCell());
if(isEOF || currentBuffer.hasRemaining()) {
return;
}
}
}
@GuardedBy("lock")
private void processIncomingCell(RelayCell nextCell) throws IOException {
if(isClosed || nextCell == CLOSE_SENTINEL) {
throw new IOException("Input stream closed");
}
switch(nextCell.getRelayCommand()) {
case RelayCell.RELAY_DATA:
currentBuffer = nextCell.getPayloadBuffer();
break;
case RelayCell.RELAY_END:
currentBuffer = EMPTY_BUFFER;
isEOF = true;
break;
default:
throw new IOException("Unexpected RelayCell command type in TorInputStream queue: "+ nextCell.getRelayCommand());
}
}
@GuardedBy("lock")
private RelayCell getNextCell() throws IOException {
try {
while(incomingCells.isEmpty()) {
lock.wait();
}
return incomingCells.remove();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Read interrupted");
}
}
int unflushedCellCount() {
synchronized (lock) {
return incomingCells.size();
}
}
public String toString() {
return "TorInputStream stream="+ stream.getStreamId() +" node="+ stream.getTargetNode();
}
}

View File

@ -0,0 +1,85 @@
package com.subgraph.orchid.circuits;
import java.io.IOException;
import java.io.OutputStream;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
public class TorOutputStream extends OutputStream {
private final StreamImpl stream;
private RelayCell currentOutputCell;
private volatile boolean isClosed;
private long bytesSent;
TorOutputStream(StreamImpl stream) {
this.stream = stream;
this.bytesSent = 0;
}
private void flushCurrentOutputCell() {
if(currentOutputCell != null && currentOutputCell.cellBytesConsumed() > RelayCell.HEADER_SIZE) {
stream.waitForSendWindowAndDecrement();
stream.getCircuit().sendRelayCell(currentOutputCell);
bytesSent += (currentOutputCell.cellBytesConsumed() - RelayCell.HEADER_SIZE);
}
currentOutputCell = new RelayCellImpl(stream.getTargetNode(), stream.getCircuit().getCircuitId(),
stream.getStreamId(), RelayCell.RELAY_DATA);
}
long getBytesSent() {
return bytesSent;
}
@Override
public synchronized void write(int b) throws IOException {
checkOpen();
if(currentOutputCell == null || currentOutputCell.cellBytesRemaining() == 0)
flushCurrentOutputCell();
currentOutputCell.putByte(b);
}
public synchronized void write(byte[] data, int offset, int length) throws IOException {
checkOpen();
if(currentOutputCell == null || currentOutputCell.cellBytesRemaining() == 0)
flushCurrentOutputCell();
while(length > 0) {
if(length < currentOutputCell.cellBytesRemaining()) {
currentOutputCell.putByteArray(data, offset, length);
return;
}
final int writeCount = currentOutputCell.cellBytesRemaining();
currentOutputCell.putByteArray(data, offset, writeCount);
flushCurrentOutputCell();
offset += writeCount;
length -= writeCount;
}
}
private void checkOpen() throws IOException {
if(isClosed)
throw new IOException("Output stream is closed");
}
public synchronized void flush() {
if(isClosed)
return;
flushCurrentOutputCell();
}
public synchronized void close() {
if(isClosed)
return;
flush();
isClosed = true;
currentOutputCell = null;
stream.close();
}
public String toString() {
return "TorOutputStream stream="+ stream.getStreamId() +" node="+ stream.getTargetNode();
}
}

View File

@ -0,0 +1,215 @@
package com.subgraph.orchid.circuits.cells;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import com.subgraph.orchid.Cell;
public class CellImpl implements Cell {
public static CellImpl createCell(int circuitId, int command) {
return new CellImpl(circuitId, command);
}
public static CellImpl createVarCell(int circuitId, int command, int payloadLength) {
return new CellImpl(circuitId, command, payloadLength);
}
public static CellImpl readFromInputStream(InputStream input) throws IOException {
final ByteBuffer header = readHeaderFromInputStream(input);
final int circuitId = header.getShort() & 0xFFFF;
final int command = header.get() & 0xFF;
if(command == VERSIONS || command > 127) {
return readVarCell(circuitId, command, input);
}
final CellImpl cell = new CellImpl(circuitId, command);
readAll(input, cell.getCellBytes(), CELL_HEADER_LEN, CELL_PAYLOAD_LEN);
return cell;
}
private static ByteBuffer readHeaderFromInputStream(InputStream input) throws IOException {
final byte[] cellHeader = new byte[CELL_HEADER_LEN];
readAll(input, cellHeader);
return ByteBuffer.wrap(cellHeader);
}
private static CellImpl readVarCell(int circuitId, int command, InputStream input) throws IOException {
final byte[] lengthField = new byte[2];
readAll(input, lengthField);
final int length = ((lengthField[0] & 0xFF) << 8) | (lengthField[1] & 0xFF);
CellImpl cell = new CellImpl(circuitId, command, length);
readAll(input, cell.getCellBytes(), CELL_VAR_HEADER_LEN, length);
return cell;
}
private static void readAll(InputStream input, byte[] buffer) throws IOException {
readAll(input, buffer, 0, buffer.length);
}
private static void readAll(InputStream input, byte[] buffer, int offset, int length) throws IOException {
int bytesRead = 0;
while(bytesRead < length) {
final int n = input.read(buffer, offset + bytesRead, length - bytesRead);
if(n == -1)
throw new EOFException();
bytesRead += n;
}
}
private final int circuitId;
private final int command;
protected final ByteBuffer cellBuffer;
/* Variable length cell constructor (ie: VERSIONS cells only) */
private CellImpl(int circuitId, int command, int payloadLength) {
this.circuitId = circuitId;
this.command = command;
this.cellBuffer = ByteBuffer.wrap(new byte[CELL_VAR_HEADER_LEN + payloadLength]);
cellBuffer.putShort((short)circuitId);
cellBuffer.put((byte)command);
cellBuffer.putShort((short) payloadLength);
cellBuffer.mark();
}
/* Fixed length cell constructor */
protected CellImpl(int circuitId, int command) {
this.circuitId = circuitId;
this.command = command;
this.cellBuffer = ByteBuffer.wrap(new byte[CELL_LEN]);
cellBuffer.putShort((short) circuitId);
cellBuffer.put((byte) command);
cellBuffer.mark();
}
protected CellImpl(byte[] rawCell) {
this.cellBuffer = ByteBuffer.wrap(rawCell);
this.circuitId = cellBuffer.getShort() & 0xFFFF;
this.command = cellBuffer.get() & 0xFF;
cellBuffer.mark();
}
public int getCircuitId() {
return circuitId;
}
public int getCommand() {
return command;
}
public void resetToPayload() {
cellBuffer.reset();
}
public int getByte() {
return cellBuffer.get() & 0xFF;
}
public int getByteAt(int index) {
return cellBuffer.get(index) & 0xFF;
}
public int getShort() {
return cellBuffer.getShort() & 0xFFFF;
}
public int getInt() {
return cellBuffer.getInt();
}
public int getShortAt(int index) {
return cellBuffer.getShort(index) & 0xFFFF;
}
public void getByteArray(byte[] buffer) {
cellBuffer.get(buffer);
}
public int cellBytesConsumed() {
return cellBuffer.position();
}
public int cellBytesRemaining() {
return cellBuffer.remaining();
}
public void putByte(int value) {
cellBuffer.put((byte) value);
}
public void putByteAt(int index, int value) {
cellBuffer.put(index, (byte) value);
}
public void putShort(int value) {
cellBuffer.putShort((short) value);
}
public void putShortAt(int index, int value) {
cellBuffer.putShort(index, (short) value);
}
public void putInt(int value) {
cellBuffer.putInt(value);
}
public void putString(String string) {
final byte[] bytes = new byte[string.length() + 1];
for(int i = 0; i < string.length(); i++)
bytes[i] = (byte) string.charAt(i);
putByteArray(bytes);
}
public void putByteArray(byte[] data) {
cellBuffer.put(data);
}
public void putByteArray(byte[] data, int offset, int length) {
cellBuffer.put(data, offset, length);
}
public byte[] getCellBytes() {
return cellBuffer.array();
}
public String toString() {
return "Cell: circuit_id="+ circuitId +" command="+ command +" payload_len="+ cellBuffer.position();
}
public static String errorToDescription(int errorCode) {
switch(errorCode) {
case ERROR_NONE:
return "No error reason given";
case ERROR_PROTOCOL:
return "Tor protocol violation";
case ERROR_INTERNAL:
return "Internal error";
case ERROR_REQUESTED:
return "Response to a TRUNCATE command sent from client";
case ERROR_HIBERNATING:
return "Not currently operating; trying to save bandwidth.";
case ERROR_RESOURCELIMIT:
return "Out of memory, sockets, or circuit IDs.";
case ERROR_CONNECTFAILED:
return "Unable to reach server.";
case ERROR_OR_IDENTITY:
return "Connected to server, but its OR identity was not as expected.";
case ERROR_OR_CONN_CLOSED:
return "The OR connection that was carrying this circuit died.";
case ERROR_FINISHED:
return "The circuit has expired for being dirty or old.";
case ERROR_TIMEOUT:
return "Circuit construction took too long.";
case ERROR_DESTROYED:
return "The circuit was destroyed without client TRUNCATE";
case ERROR_NOSUCHSERVICE:
return "Request for unknown hidden service";
default:
return "Error code "+ errorCode;
}
}
}

View File

@ -0,0 +1,180 @@
package com.subgraph.orchid.circuits.cells;
import java.nio.ByteBuffer;
import com.subgraph.orchid.Cell;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.RelayCell;
import com.subgraph.orchid.TorException;
public class RelayCellImpl extends CellImpl implements RelayCell {
public static RelayCell createFromCell(CircuitNode node, Cell cell) {
if(cell.getCommand() != Cell.RELAY)
throw new TorException("Attempted to create RelayCell from Cell type: "+ cell.getCommand());
return new RelayCellImpl(node, cell.getCellBytes());
}
private final int streamId;
private final int relayCommand;
private final CircuitNode circuitNode;
private final boolean isOutgoing;
/*
* The payload of each unencrypted RELAY cell consists of:
* Relay command [1 byte]
* 'Recognized' [2 bytes]
* StreamID [2 bytes]
* Digest [4 bytes]
* Length [2 bytes]
* Data [CELL_LEN-14 bytes]
*/
public RelayCellImpl(CircuitNode node, int circuit, int stream, int relayCommand) {
this(node, circuit, stream, relayCommand, false);
}
public RelayCellImpl(CircuitNode node, int circuit, int stream, int relayCommand, boolean isRelayEarly) {
super(circuit, (isRelayEarly) ? (Cell.RELAY_EARLY) : (Cell.RELAY));
this.circuitNode = node;
this.relayCommand = relayCommand;
this.streamId = stream;
this.isOutgoing = true;
putByte(relayCommand); // Command
putShort(0); // 'Recognized'
putShort(stream); // Stream
putInt(0); // Digest
putShort(0); // Length
}
private RelayCellImpl(CircuitNode node, byte[] rawCell) {
super(rawCell);
this.circuitNode = node;
this.relayCommand = getByte();
getShort();
this.streamId = getShort();
this.isOutgoing = false;
getInt();
int payloadLength = getShort();
cellBuffer.mark(); // End of header
if(RelayCell.HEADER_SIZE + payloadLength > rawCell.length)
throw new TorException("Header length field exceeds total size of cell");
cellBuffer.limit(RelayCell.HEADER_SIZE + payloadLength);
}
public int getStreamId() {
return streamId;
}
public int getRelayCommand() {
return relayCommand;
}
public void setLength() {
putShortAt(LENGTH_OFFSET, (short) (cellBytesConsumed() - HEADER_SIZE));
}
public void setDigest(byte[] digest) {
for(int i = 0; i < 4; i++)
putByteAt(DIGEST_OFFSET + i, digest[i]);
}
public ByteBuffer getPayloadBuffer() {
final ByteBuffer dup = cellBuffer.duplicate();
dup.reset();
return dup.slice();
}
public CircuitNode getCircuitNode() {
return circuitNode;
}
public String toString() {
if(isOutgoing)
return "["+ commandToDescription(relayCommand) +" stream="+ streamId +" payload_len="+ (cellBytesConsumed() - HEADER_SIZE) +" dest="+ circuitNode +"]";
else
return "["+ commandToString() + " stream="+ streamId + " payload_len="+ cellBuffer.remaining() +" source="+ circuitNode + "]";
}
public String commandToString() {
if(relayCommand == RELAY_TRUNCATED) {
final int code = getByteAt(HEADER_SIZE);
return commandToDescription(relayCommand) + " ("+ CellImpl.errorToDescription(code) +")";
} else if(relayCommand == RELAY_END) {
final int code = getByteAt(HEADER_SIZE);
return commandToDescription(relayCommand) +" ("+ reasonToDescription(code) +")";
}
else
return commandToDescription(relayCommand);
}
public static String reasonToDescription(int reasonCode) {
switch(reasonCode) {
case REASON_MISC:
return "Unlisted reason";
case REASON_RESOLVEFAILED:
return "Couldn't look up hostname";
case REASON_CONNECTREFUSED:
return "Remote host refused connection";
case REASON_EXITPOLICY:
return "OR refuses to connect to host or port";
case REASON_DESTROY:
return "Circuit is being destroyed";
case REASON_DONE:
return "Anonymized TCP connection was closed";
case REASON_TIMEOUT:
return "Connection timed out, or OR timed out while connecting";
case REASON_HIBERNATING:
return "OR is temporarily hibernating";
case REASON_INTERNAL:
return "Internal error at the OR";
case REASON_RESOURCELIMIT:
return "OR has no resources to fulfill request";
case REASON_CONNRESET:
return "Connection was unexpectedly reset";
case REASON_TORPROTOCOL:
return "Tor protocol violation";
case REASON_NOTDIRECTORY:
return "Client sent RELAY_BEGIN_DIR to a non-directory server.";
default:
return "Reason code "+ reasonCode;
}
}
public static String commandToDescription(int command) {
switch(command) {
case RELAY_BEGIN:
return "RELAY_BEGIN";
case RELAY_DATA:
return "RELAY_DATA";
case RELAY_END:
return "RELAY_END";
case RELAY_CONNECTED:
return "RELAY_CONNECTED";
case RELAY_SENDME:
return "RELAY_SENDME";
case RELAY_EXTEND:
return "RELAY_EXTEND";
case RELAY_EXTENDED:
return "RELAY_EXTENDED";
case RELAY_TRUNCATE:
return "RELAY_TRUNCATE";
case RELAY_TRUNCATED:
return "RELAY_TRUNCATED";
case RELAY_DROP:
return "RELAY_DROP";
case RELAY_RESOLVE:
return "RELAY_RESOLVE";
case RELAY_RESOLVED:
return "RELAY_RESOLVED";
case RELAY_BEGIN_DIR:
return "RELAY_BEGIN_DIR";
case RELAY_EXTEND2:
return "RELAY_EXTEND2";
case RELAY_EXTENDED2:
return "RELAY_EXTENDED2";
default:
return "Relay command = "+ command;
}
}
}

View File

@ -0,0 +1,226 @@
package com.subgraph.orchid.circuits.guards;
import java.util.Collections;
import java.util.Set;
import com.subgraph.orchid.BridgeRouter;
import com.subgraph.orchid.Descriptor;
import com.subgraph.orchid.RouterDescriptor;
import com.subgraph.orchid.crypto.TorPublicKey;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.geoip.CountryCodeService;
public class BridgeRouterImpl implements BridgeRouter {
private final IPv4Address address;
private final int port;
private HexDigest identity;
private Descriptor descriptor;
private volatile String cachedCountryCode;
BridgeRouterImpl(IPv4Address address, int port) {
this.address = address;
this.port = port;
}
public IPv4Address getAddress() {
return address;
}
public HexDigest getIdentity() {
return identity;
}
public void setIdentity(HexDigest identity) {
this.identity = identity;
}
public void setDescriptor(RouterDescriptor descriptor) {
this.descriptor = descriptor;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((address == null) ? 0 : address.hashCode());
result = prime * result + port;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
BridgeRouterImpl other = (BridgeRouterImpl) obj;
if (address == null) {
if (other.address != null) {
return false;
}
} else if (!address.equals(other.address)) {
return false;
}
if (port != other.port) {
return false;
}
return true;
}
public String getNickname() {
return toString();
}
public String getCountryCode() {
String cc = cachedCountryCode;
if(cc == null) {
cc = CountryCodeService.getInstance().getCountryCodeForAddress(getAddress());
cachedCountryCode = cc;
}
return cc;
}
public int getOnionPort() {
return port;
}
public int getDirectoryPort() {
return 0;
}
public TorPublicKey getIdentityKey() {
return null;
}
public HexDigest getIdentityHash() {
return identity;
}
public boolean isDescriptorDownloadable() {
return false;
}
public String getVersion() {
return "";
}
public Descriptor getCurrentDescriptor() {
return descriptor;
}
public HexDigest getDescriptorDigest() {
return null;
}
public HexDigest getMicrodescriptorDigest() {
return null;
}
public TorPublicKey getOnionKey() {
if(descriptor != null) {
return descriptor.getOnionKey();
} else {
return null;
}
}
public byte[] getNTorOnionKey() {
if(descriptor != null) {
return descriptor.getNTorOnionKey();
} else {
return null;
}
}
public boolean hasBandwidth() {
return false;
}
public int getEstimatedBandwidth() {
return 0;
}
public int getMeasuredBandwidth() {
return 0;
}
public Set<String> getFamilyMembers() {
if(descriptor != null) {
return descriptor.getFamilyMembers();
} else {
return Collections.emptySet();
}
}
public int getAverageBandwidth() {
return 0;
}
public int getBurstBandwidth() {
return 0;
}
public int getObservedBandwidth() {
return 0;
}
public boolean isHibernating() {
if(descriptor instanceof RouterDescriptor) {
return ((RouterDescriptor)descriptor).isHibernating();
} else {
return false;
}
}
public boolean isRunning() {
return true;
}
public boolean isValid() {
return true;
}
public boolean isBadExit() {
return false;
}
public boolean isPossibleGuard() {
return true;
}
public boolean isExit() {
return false;
}
public boolean isFast() {
return true;
}
public boolean isStable() {
return true;
}
public boolean isHSDirectory() {
return false;
}
public boolean exitPolicyAccepts(IPv4Address address, int port) {
return false;
}
public boolean exitPolicyAccepts(int port) {
return false;
}
public String toString() {
return "[Bridge "+ address + ":"+ port + "]";
}
}

View File

@ -0,0 +1,163 @@
package com.subgraph.orchid.circuits.guards;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import com.subgraph.orchid.BridgeRouter;
import com.subgraph.orchid.DirectoryDownloader;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.RouterDescriptor;
import com.subgraph.orchid.TorConfig;
import com.subgraph.orchid.config.TorConfigBridgeLine;
import com.subgraph.orchid.crypto.TorRandom;
import com.subgraph.orchid.directory.downloader.DirectoryRequestFailedException;
public class Bridges {
private static final Logger logger = Logger.getLogger(Bridges.class.getName());
private class DescriptorDownloader implements Runnable {
private final BridgeRouterImpl target;
DescriptorDownloader(BridgeRouterImpl target) {
this.target = target;
}
public void run() {
try {
downloadDescriptor();
} finally {
decrementOutstandingTasks();
}
}
private void downloadDescriptor() {
logger.fine("Downloading descriptor for bridge: "+ target);
try {
final RouterDescriptor descriptor = directoryDownloader.downloadBridgeDescriptor(target);
if(descriptor != null) {
logger.fine("Descriptor received for bridge "+ target +". Adding to list of usable bridges");
target.setDescriptor(descriptor);
synchronized(lock) {
bridgeRouters.add(target);
lock.notifyAll();
}
}
} catch (DirectoryRequestFailedException e) {
logger.warning("Failed to download descriptor for bridge: "+ e.getMessage());
}
}
private void decrementOutstandingTasks() {
if(outstandingDownloadTasks.decrementAndGet() == 0) {
logger.fine("Initial descriptor fetch complete");
synchronized(lock) {
bridgesInitialized = true;
lock.notifyAll();
}
}
}
}
private final TorConfig config;
private final DirectoryDownloader directoryDownloader;
private final Set<BridgeRouterImpl> bridgeRouters;
private final TorRandom random;
private final Object lock;
/** Initialization started */
private boolean bridgesInitializing;
/** Initialization completed */
private boolean bridgesInitialized;
private AtomicInteger outstandingDownloadTasks;
Bridges(TorConfig config, DirectoryDownloader directoryDownloader) {
this.config = config;
this.directoryDownloader = directoryDownloader;
this.bridgeRouters = new HashSet<BridgeRouterImpl>();
this.random = new TorRandom();
this.lock = new Object();
this.outstandingDownloadTasks = new AtomicInteger();
}
BridgeRouter chooseRandomBridge(Set<Router> excluded) throws InterruptedException {
synchronized(lock) {
if(!bridgesInitialized && !bridgesInitializing) {
initializeBridges();
}
while(!bridgesInitialized && !hasCandidates(excluded)) {
lock.wait();
}
final List<BridgeRouter> candidates = getCandidates(excluded);
if(candidates.isEmpty()) {
logger.warning("Bridges enabled but no usable bridges configured");
return null;
}
return candidates.get(random.nextInt(candidates.size()));
}
}
private boolean hasCandidates(Set<Router> excluded) {
return !(getCandidates(excluded).isEmpty());
}
private List<BridgeRouter> getCandidates(Set<Router> excluded) {
if(bridgeRouters.isEmpty()) {
return Collections.emptyList();
}
final List<BridgeRouter> candidates = new ArrayList<BridgeRouter>(bridgeRouters.size());
for(BridgeRouter br: bridgeRouters) {
if(!excluded.contains(br)) {
candidates.add(br);
}
}
return candidates;
}
private void initializeBridges() {
logger.fine("Initializing bridges...");
synchronized(lock) {
if(bridgesInitializing || bridgesInitialized) {
return;
}
if(directoryDownloader == null) {
throw new IllegalStateException("Cannot download bridge descriptors because DirectoryDownload instance not initialized");
}
bridgesInitializing = true;
startAllDownloadTasks();
}
}
private List<Runnable> createDownloadTasks() {
final List<Runnable> tasks = new ArrayList<Runnable>();
for(TorConfigBridgeLine line: config.getBridges()) {
tasks.add(new DescriptorDownloader(createBridgeFromLine(line)));
}
return tasks;
}
private void startAllDownloadTasks() {
final List<Runnable> tasks = createDownloadTasks();
outstandingDownloadTasks.set(tasks.size());
for(Runnable r: tasks) {
final Thread thread = new Thread(r);
thread.start();
}
}
private BridgeRouterImpl createBridgeFromLine(TorConfigBridgeLine line) {
final BridgeRouterImpl bridge = new BridgeRouterImpl(line.getAddress(), line.getPort());
if(line.getFingerprint() != null) {
bridge.setIdentity(line.getFingerprint());
}
return bridge;
}
}

View File

@ -0,0 +1,305 @@
package com.subgraph.orchid.circuits.guards;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import com.subgraph.orchid.ConnectionCache;
import com.subgraph.orchid.Directory;
import com.subgraph.orchid.DirectoryDownloader;
import com.subgraph.orchid.GuardEntry;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.TorConfig;
import com.subgraph.orchid.circuits.path.CircuitNodeChooser;
import com.subgraph.orchid.circuits.path.CircuitNodeChooser.WeightRule;
import com.subgraph.orchid.circuits.path.RouterFilter;
import com.subgraph.orchid.crypto.TorRandom;
public class EntryGuards {
private final static Logger logger = Logger.getLogger(EntryGuards.class.getName());
private final static int MIN_USABLE_GUARDS = 2;
private final static int NUM_ENTRY_GUARDS = 3;
private final TorConfig config;
private final TorRandom random;
private final CircuitNodeChooser nodeChooser;
private final ConnectionCache connectionCache;
private final Directory directory;
private final Set<GuardEntry> pendingProbes;
private final Bridges bridges;
private final Object lock;
private final Executor executor;
public EntryGuards(TorConfig config, ConnectionCache connectionCache, DirectoryDownloader directoryDownloader, Directory directory) {
this.config = config;
this.random = new TorRandom();
this.nodeChooser = new CircuitNodeChooser(config, directory);
this.connectionCache = connectionCache;
this.directory = directory;
this.pendingProbes = new HashSet<GuardEntry>();
this.bridges = new Bridges(config, directoryDownloader);
this.lock = new Object();
this.executor = Executors.newCachedThreadPool();
}
public boolean isUsingBridges() {
return config.getUseBridges();
}
public Router chooseRandomGuard(Set<Router> excluded) throws InterruptedException {
if(config.getUseBridges()) {
return bridges.chooseRandomBridge(excluded);
}
/*
* path-spec 5.
*
* When choosing the first hop of a circuit, Tor chooses at random from among the first
* NumEntryGuards (default 3) usable guards on the list. If there are not at least 2
* usable guards on the list, Tor adds routers until there are, or until there are no
* more usable routers to add.
*/
final List<Router> usableGuards = getMinimumUsableGuards(excluded, MIN_USABLE_GUARDS);
final int n = Math.min(usableGuards.size(), NUM_ENTRY_GUARDS);
return usableGuards.get(random.nextInt(n));
}
private List<Router> getMinimumUsableGuards(Set<Router> excluded, int minSize) throws InterruptedException {
synchronized(lock) {
testStatusOfAllGuards();
while(true) {
List<Router> usableGuards = getUsableGuardRouters(excluded);
if(usableGuards.size() >= minSize) {
return usableGuards;
} else {
maybeChooseNew(usableGuards.size(), minSize, getExcludedForChooseNew(excluded, usableGuards));
}
lock.wait(5000);
}
}
}
void probeConnectionSucceeded(GuardEntry entry) {
synchronized (lock) {
pendingProbes.remove(entry);
if(entry.isAdded()) {
retestProbeSucceeded(entry);
} else {
initialProbeSucceeded(entry);
}
}
}
void probeConnectionFailed(GuardEntry entry) {
synchronized (lock) {
pendingProbes.remove(entry);
if(entry.isAdded()) {
retestProbeFailed(entry);
}
lock.notifyAll();
}
}
/* all methods below called holding 'lock' */
private void retestProbeSucceeded(GuardEntry entry) {
entry.clearDownSince();
}
private void initialProbeSucceeded(GuardEntry entry) {
logger.fine("Probe connection to "+ entry.getRouterForEntry() + " succeeded. Adding it as a new entry guard.");
directory.addGuardEntry(entry);
retestAllUnreachable();
}
private void retestProbeFailed(GuardEntry entry) {
entry.markAsDown();
}
/*
* path-spec 5.
*
* Additionally, Tor retries unreachable guards the first time it adds a new
* guard to the list, since it is possible that the old guards were only marked
* as unreachable because the network was unreachable or down.
*/
private void retestAllUnreachable() {
for(GuardEntry e: directory.getGuardEntries()) {
if(e.getDownSince() != null) {
launchEntryProbe(e);
}
}
}
private void testStatusOfAllGuards() {
for(GuardEntry entry: directory.getGuardEntries()) {
if(isPermanentlyUnlisted(entry) || isExpired(entry)) {
directory.removeGuardEntry(entry);
} else if(needsUnreachableTest(entry)) {
launchEntryProbe(entry);
}
}
}
private List<Router> getUsableGuardRouters(Set<Router> excluded) {
List<Router> usableRouters = new ArrayList<Router>();
for(GuardEntry entry: directory.getGuardEntries()) {
addRouterIfUsableAndNotExcluded(entry, excluded, usableRouters);
}
return usableRouters;
}
private void addRouterIfUsableAndNotExcluded(GuardEntry entry, Set<Router> excluded, List<Router> routers) {
if(entry.testCurrentlyUsable() && entry.getDownSince() == null) {
final Router r = entry.getRouterForEntry();
if(r != null && !excluded.contains(r)) {
routers.add(r);
}
}
}
private Set<Router> getExcludedForChooseNew(Set<Router> excluded, List<Router> usable) {
final Set<Router> set = new HashSet<Router>();
set.addAll(excluded);
set.addAll(usable);
addPendingInitialConnections(set);
return set;
}
private void addPendingInitialConnections(Set<Router> routerSet) {
for(GuardEntry entry: pendingProbes) {
if(!entry.isAdded()) {
Router r = entry.getRouterForEntry();
if(r != null) {
routerSet.add(r);
}
}
}
}
private void maybeChooseNew(int usableSize, int minSize, Set<Router> excluded) {
int sz = usableSize + countPendingInitialProbes();
while(sz < minSize) {
Router newGuard = chooseNewGuard(excluded);
if(newGuard == null) {
logger.warning("Need to add entry guards but no suitable guard routers are available");
return;
}
logger.fine("Testing "+ newGuard + " as a new guard since we only have "+ usableSize + " usable guards");
final GuardEntry entry = directory.createGuardEntryFor(newGuard);
launchEntryProbe(entry);
sz += 1;
}
}
private int countPendingInitialProbes() {
int count = 0;
for(GuardEntry entry: pendingProbes) {
if(!entry.isAdded()) {
count += 1;
}
}
return count;
}
private Router chooseNewGuard(final Set<Router> excluded) {
return nodeChooser.chooseRandomNode(WeightRule.WEIGHT_FOR_GUARD, new RouterFilter() {
public boolean filter(Router router) {
return router.isValid() && router.isPossibleGuard() && router.isRunning() && !excluded.contains(router);
}
});
}
private void launchEntryProbe(GuardEntry entry) {
if(!entry.testCurrentlyUsable() || pendingProbes.contains(entry)) {
return;
}
pendingProbes.add(entry);
executor.execute(new GuardProbeTask(connectionCache, this, entry));
}
/*
* path-spec 5.
*
* If the guard is excluded because of its status in the networkstatuses for
* over 30 days, Tor removes it from the list entirely, preserving order.
*/
private boolean isPermanentlyUnlisted(GuardEntry entry) {
final Date unlistedSince = entry.getUnlistedSince();
if(unlistedSince == null || pendingProbes.contains(entry)) {
return false;
}
final Date now = new Date();
final long unlistedTime = now.getTime() - unlistedSince.getTime();
return unlistedTime > THIRTY_DAYS;
}
/*
* Expire guards after 60 days since creation time.
*/
private boolean isExpired(GuardEntry entry) {
final Date createdAt = entry.getCreatedTime();
final Date now = new Date();
final long createdAgo = now.getTime() - createdAt.getTime();
return createdAgo > SIXTY_DAYS;
}
private boolean needsUnreachableTest(GuardEntry entry) {
final Date downSince = entry.getDownSince();
if(downSince == null || !entry.testCurrentlyUsable()) {
return false;
}
final Date now = new Date();
final Date lastConnect = entry.getLastConnectAttempt();
final long timeDown = now.getTime() - downSince.getTime();
final long timeSinceLastRetest = (lastConnect == null) ? timeDown : (now.getTime() - lastConnect.getTime());
return timeSinceLastRetest > getRetestInterval(timeDown);
}
private final static long ONE_HOUR = hoursToMs(1);
private final static long FOUR_HOURS = hoursToMs(4);
private final static long SIX_HOURS = hoursToMs(6);
private final static long EIGHTEEN_HOURS = hoursToMs(18);
private final static long THIRTYSIX_HOURS = hoursToMs(36);
private final static long THREE_DAYS = daysToMs(3);
private final static long SEVEN_DAYS = daysToMs(7);
private final static long THIRTY_DAYS = daysToMs(30);
private final static long SIXTY_DAYS = daysToMs(60);
private static long hoursToMs(long n) {
return TimeUnit.MILLISECONDS.convert(n, TimeUnit.HOURS);
}
private static long daysToMs(long n) {
return TimeUnit.MILLISECONDS.convert(n, TimeUnit.DAYS);
}
/*
* path-spec 5.
*
* If Tor fails to connect to an otherwise usable guard, it retries
* periodically: every hour for six hours, every 4 hours for 3 days, every
* 18 hours for a week, and every 36 hours thereafter.
*/
private long getRetestInterval(long timeDown) {
if(timeDown < SIX_HOURS) {
return ONE_HOUR;
} else if(timeDown < THREE_DAYS) {
return FOUR_HOURS;
} else if(timeDown < SEVEN_DAYS) {
return EIGHTEEN_HOURS;
} else {
return THIRTYSIX_HOURS;
}
}
}

View File

@ -0,0 +1,42 @@
package com.subgraph.orchid.circuits.guards;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.subgraph.orchid.ConnectionCache;
import com.subgraph.orchid.ConnectionIOException;
import com.subgraph.orchid.GuardEntry;
import com.subgraph.orchid.Router;
public class GuardProbeTask implements Runnable{
private final static Logger logger = Logger.getLogger(GuardProbeTask.class.getName());
private final ConnectionCache connectionCache;
private final EntryGuards entryGuards;
private final GuardEntry entry;
public GuardProbeTask(ConnectionCache connectionCache, EntryGuards entryGuards, GuardEntry entry) {
this.connectionCache = connectionCache;
this.entryGuards = entryGuards;
this.entry = entry;
}
public void run() {
final Router router = entry.getRouterForEntry();
if(router == null) {
entryGuards.probeConnectionFailed(entry);
return;
}
try {
connectionCache.getConnectionTo(router, false);
entryGuards.probeConnectionSucceeded(entry);
return;
} catch (ConnectionIOException e) {
logger.fine("IO exception probing entry guard "+ router + " : "+ e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch(Exception e) {
logger.log(Level.WARNING, "Unexpected exception probing entry guard: "+ e, e);
}
entryGuards.probeConnectionFailed(entry);
}
}

View File

@ -0,0 +1,120 @@
package com.subgraph.orchid.circuits.hs;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.subgraph.orchid.TorParsingException;
import com.subgraph.orchid.circuits.hs.HSDescriptorCookie.CookieType;
import com.subgraph.orchid.crypto.TorMessageDigest;
import com.subgraph.orchid.crypto.TorStreamCipher;
public class HSAuthentication {
private final static int BASIC_ID_LENGTH = 4;
private final HSDescriptorCookie cookie;
public HSAuthentication(HSDescriptorCookie cookie) {
this.cookie = cookie;
}
public byte[] decryptIntroductionPoints(byte[] content) throws HSAuthenticationException {
final ByteBuffer buffer = ByteBuffer.wrap(content);
final int firstByte = buffer.get() & 0xFF;
if(firstByte == 1) {
return decryptIntroductionPointsWithBasicAuth(buffer);
} else if(firstByte == 2) {
return decryptIntroductionPointsWithStealthAuth(buffer);
} else {
throw new HSAuthenticationException("Introduction points section begins with unrecognized byte ("+ firstByte +")");
}
}
private static class BasicAuthEntry {
final byte[] id;
final byte[] skey;
BasicAuthEntry(byte[] id, byte[] skey) {
this.id = id;
this.skey = skey;
}
}
private BasicAuthEntry createEntry(ByteBuffer bb) {
final byte[] id = new byte[BASIC_ID_LENGTH];
final byte[] skey = new byte[TorStreamCipher.KEY_LEN];
bb.get(id);
bb.get(skey);
return new BasicAuthEntry(id, skey);
}
private byte[] decryptIntroductionPointsWithBasicAuth(ByteBuffer buffer) throws HSAuthenticationException {
if(cookie == null || cookie.getType() != CookieType.COOKIE_BASIC) {
throw new TorParsingException("Introduction points encrypted with 'basic' authentication and no cookie available to decrypt");
}
final List<BasicAuthEntry> entries = readBasicEntries(buffer);
final byte[] iv = readAuthIV(buffer);
final byte[] id = generateAuthId(iv);
final byte[] k = findKeyInAuthEntries(entries, id);
return decryptRemaining(buffer, k, iv);
}
private List<BasicAuthEntry> readBasicEntries(ByteBuffer b) {
final int blockCount = b.get() & 0xFF;
final int entryCount = blockCount * 16;
final List<BasicAuthEntry> entries = new ArrayList<BasicAuthEntry>(entryCount);
for(int i = 0; i < entryCount; i++) {
entries.add( createEntry(b) );
}
return entries;
}
private byte[] readAuthIV(ByteBuffer b) {
final byte[] iv = new byte[16];
b.get(iv);
return iv;
}
private byte[] generateAuthId(byte[] iv) {
final TorMessageDigest md = new TorMessageDigest();
md.update(cookie.getValue());
md.update(iv);
final byte[] digest = md.getDigestBytes();
final byte[] id = new byte[BASIC_ID_LENGTH];
System.arraycopy(digest, 0, id, 0, BASIC_ID_LENGTH);
return id;
}
private byte[] findKeyInAuthEntries(List<BasicAuthEntry> entries, byte[] id) throws HSAuthenticationException {
for(BasicAuthEntry e: entries) {
if(Arrays.equals(id, e.id)) {
return decryptAuthEntry(e);
}
}
throw new HSAuthenticationException("Could not find matching cookie id for basic authentication");
}
private byte[] decryptAuthEntry(BasicAuthEntry entry) throws HSAuthenticationException {
TorStreamCipher cipher = TorStreamCipher.createFromKeyBytes(cookie.getValue());
cipher.encrypt(entry.skey);
return entry.skey;
}
private byte[] decryptRemaining(ByteBuffer buffer, byte[] key, byte[] iv) {
TorStreamCipher streamCipher = TorStreamCipher.createFromKeyBytesWithIV(key, iv);
final byte[] remaining = new byte[buffer.remaining()];
buffer.get(remaining);
streamCipher.encrypt(remaining);
return remaining;
}
private byte[] decryptIntroductionPointsWithStealthAuth(ByteBuffer buffer) {
if(cookie == null || cookie.getType() != CookieType.COOKIE_STEALTH) {
throw new TorParsingException("Introduction points encrypted with 'stealth' authentication and no cookie available to descrypt");
}
final byte[] iv = readAuthIV(buffer);
return decryptRemaining(buffer, cookie.getValue(), iv);
}
}

View File

@ -0,0 +1,14 @@
package com.subgraph.orchid.circuits.hs;
public class HSAuthenticationException extends Exception {
private static final long serialVersionUID = 1L;
HSAuthenticationException(String message) {
super(message);
}
HSAuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,109 @@
package com.subgraph.orchid.circuits.hs;
import java.util.ArrayList;
import java.util.List;
import com.subgraph.orchid.crypto.TorPublicKey;
import com.subgraph.orchid.crypto.TorRandom;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.Timestamp;
public class HSDescriptor {
private final static long MS_24_HOURS = (24 * 60 * 60 * 1000);
private final HiddenService hiddenService;
private HexDigest descriptorId;
private Timestamp publicationTime;
private HexDigest secretIdPart;
private TorPublicKey permanentKey;
private int[] protocolVersions;
private List<IntroductionPoint> introductionPoints;
public HSDescriptor(HiddenService hiddenService) {
this.hiddenService = hiddenService;
introductionPoints = new ArrayList<IntroductionPoint>();
}
HiddenService getHiddenService() {
return hiddenService;
}
void setPublicationTime(Timestamp ts) {
this.publicationTime = ts;
}
void setSecretIdPart(HexDigest secretIdPart) {
this.secretIdPart = secretIdPart;
}
void setDescriptorId(HexDigest descriptorId) {
this.descriptorId = descriptorId;
}
void setPermanentKey(TorPublicKey permanentKey) {
this.permanentKey = permanentKey;
}
void setProtocolVersions(int[] protocolVersions) {
this.protocolVersions = protocolVersions;
}
void addIntroductionPoint(IntroductionPoint ip) {
introductionPoints.add(ip);
}
HexDigest getDescriptorId() {
return descriptorId;
}
int getVersion() {
return 2;
}
TorPublicKey getPermanentKey() {
return permanentKey;
}
HexDigest getSecretIdPart() {
return secretIdPart;
}
Timestamp getPublicationTime() {
return publicationTime;
}
int[] getProtocolVersions() {
return protocolVersions;
}
boolean isExpired() {
final long now = System.currentTimeMillis();
final long then = publicationTime.getTime();
return (now - then) > MS_24_HOURS;
}
List<IntroductionPoint> getIntroductionPoints() {
return new ArrayList<IntroductionPoint>(introductionPoints);
}
List<IntroductionPoint> getShuffledIntroductionPoints() {
return shuffle(getIntroductionPoints());
}
private List<IntroductionPoint> shuffle(List<IntroductionPoint> list) {
final TorRandom r = new TorRandom();
final int sz = list.size();
for(int i = 0; i < sz; i++) {
swap(list, i, r.nextInt(sz));
}
return list;
}
private void swap(List<IntroductionPoint> list, int a, int b) {
if(a == b) {
return;
}
final IntroductionPoint tmp = list.get(a);
list.set(a, list.get(b));
list.set(b, tmp);
}
}

View File

@ -0,0 +1,33 @@
package com.subgraph.orchid.circuits.hs;
public class HSDescriptorCookie {
public enum CookieType { COOKIE_BASIC, COOKIE_STEALTH };
private final CookieType type;
private final byte[] value;
public HSDescriptorCookie(CookieType type, byte[] value) {
this.type = type;
this.value = value;
}
public byte getAuthTypeByte() {
switch(type) {
case COOKIE_BASIC:
return 1;
case COOKIE_STEALTH:
return 2;
default:
throw new IllegalStateException();
}
}
public CookieType getType() {
return type;
}
public byte[] getValue() {
return value;
}
}

View File

@ -0,0 +1,28 @@
package com.subgraph.orchid.circuits.hs;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.data.HexDigest;
public class HSDescriptorDirectory {
private final HexDigest descriptorId;
private final Router directory;
HSDescriptorDirectory(HexDigest descriptorId, Router directory) {
this.descriptorId = descriptorId;
this.directory = directory;
}
Router getDirectory() {
return directory;
}
HexDigest getDescriptorId() {
return descriptorId;
}
public String toString() {
return descriptorId + " : " + directory;
}
}

View File

@ -0,0 +1,135 @@
package com.subgraph.orchid.circuits.hs;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import com.subgraph.orchid.DirectoryCircuit;
import com.subgraph.orchid.InternalCircuit;
import com.subgraph.orchid.OpenFailedException;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.TorException;
import com.subgraph.orchid.circuits.CircuitManagerImpl;
import com.subgraph.orchid.directory.DocumentFieldParserImpl;
import com.subgraph.orchid.directory.downloader.DirectoryRequestFailedException;
import com.subgraph.orchid.directory.downloader.HttpConnection;
import com.subgraph.orchid.directory.parsing.DocumentParsingResultHandler;
public class HSDescriptorDownloader {
private final static Logger logger = Logger.getLogger(HSDescriptorDirectory.class.getName());
private final HiddenService hiddenService;
private final CircuitManagerImpl circuitManager;
private final List<HSDescriptorDirectory> directories;
public HSDescriptorDownloader(HiddenService hiddenService, CircuitManagerImpl circuitManager, List<HSDescriptorDirectory> directories) {
this.hiddenService = hiddenService;
this.circuitManager = circuitManager;
this.directories = directories;
}
public HSDescriptor downloadDescriptor() {
for(HSDescriptorDirectory d: directories) {
HSDescriptor descriptor = downloadDescriptorFrom(d);
if(descriptor != null) {
return descriptor;
}
}
// All directories failed
return null;
}
private HSDescriptor downloadDescriptorFrom(HSDescriptorDirectory dd) {
logger.fine("Downloading descriptor from "+ dd.getDirectory());
Stream stream = null;
try {
stream = openHSDirectoryStream(dd.getDirectory());
HttpConnection http = new HttpConnection(stream);
http.sendGetRequest("/tor/rendezvous2/"+ dd.getDescriptorId().toBase32());
http.readResponse();
if(http.getStatusCode() == 200) {
return readDocument(dd, http.getMessageBody());
} else {
logger.fine("HS descriptor download for "+ hiddenService.getOnionAddressForLogging() + " failed with status "+ http.getStatusCode());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} catch (TimeoutException e) {
logger.fine("Timeout downloading HS descriptor from "+ dd.getDirectory());
e.printStackTrace();
return null;
} catch (IOException e) {
logger.info("IOException downloading HS descriptor from "+ dd.getDirectory() +" : "+ e);
return null;
} catch (OpenFailedException e) {
logger.info("Failed to open stream to HS directory "+ dd.getDirectory() +" : "+ e.getMessage());
return null;
} catch (DirectoryRequestFailedException e) {
logger.info("Directory request to HS directory "+ dd.getDirectory() + " failed "+ e.getMessage());
return null;
} finally {
if(stream != null) {
stream.close();
stream.getCircuit().markForClose();
}
}
return null;
}
private Stream openHSDirectoryStream(Router directory) throws TimeoutException, InterruptedException, OpenFailedException {
final InternalCircuit circuit = circuitManager.getCleanInternalCircuit();
try {
final DirectoryCircuit dc = circuit.cannibalizeToDirectory(directory);
return dc.openDirectoryStream(10000, true);
} catch (StreamConnectFailedException e) {
circuit.markForClose();
throw new OpenFailedException("Failed to open directory stream");
} catch (TorException e) {
circuit.markForClose();
throw new OpenFailedException("Failed to extend circuit to HS directory: "+ e.getMessage());
}
}
private HSDescriptor readDocument(HSDescriptorDirectory dd, ByteBuffer body) {
DocumentFieldParserImpl fieldParser = new DocumentFieldParserImpl(body);
HSDescriptorParser parser = new HSDescriptorParser(hiddenService, fieldParser, hiddenService.getAuthenticationCookie());
DescriptorParseResult result = new DescriptorParseResult(dd);
parser.parse(result);
return result.getDescriptor();
}
private static class DescriptorParseResult implements DocumentParsingResultHandler<HSDescriptor> {
HSDescriptorDirectory dd;
HSDescriptor descriptor;
public DescriptorParseResult(HSDescriptorDirectory dd) {
this.dd = dd;
}
HSDescriptor getDescriptor() {
return descriptor;
}
public void documentParsed(HSDescriptor document) {
this.descriptor = document;
}
public void documentInvalid(HSDescriptor document, String message) {
logger.info("Invalid HS descriptor document received from "+ dd.getDirectory() + " for descriptor "+ dd.getDescriptorId());
}
public void parsingError(String message) {
logger.info("Failed to parse HS descriptor document received from "+ dd.getDirectory() + " for descriptor "+ dd.getDescriptorId() + " : " + message);
}
}
}

View File

@ -0,0 +1,38 @@
package com.subgraph.orchid.circuits.hs;
public enum HSDescriptorKeyword {
RENDEZVOUS_SERVICE_DESCRIPTOR("rendezvous-service-descriptor", 1),
VERSION("version", 1),
PERMANENT_KEY("permanent-key", 0),
SECRET_ID_PART("secret-id-part", 1),
PUBLICATION_TIME("publication-time", 2),
PROTOCOL_VERSIONS("protocol-versions", 2),
INTRODUCTION_POINTS("introduction-points", 0),
SIGNATURE("signature", 0),
UNKNOWN_KEYWORD("KEYWORD NOT FOUND", 0);
private final String keyword;
private final int argumentCount;
HSDescriptorKeyword(String keyword, int argumentCount) {
this.keyword = keyword;
this.argumentCount = argumentCount;
}
String getKeyword() {
return keyword;
}
int getArgumentCount() {
return argumentCount;
}
static HSDescriptorKeyword findKeyword(String keyword) {
for(HSDescriptorKeyword k: values()) {
if(k.getKeyword().equals(keyword)) {
return k;
}
}
return UNKNOWN_KEYWORD;
}
}

View File

@ -0,0 +1,160 @@
package com.subgraph.orchid.circuits.hs;
import java.nio.ByteBuffer;
import java.util.logging.Logger;
import com.subgraph.orchid.TorParsingException;
import com.subgraph.orchid.crypto.TorSignature;
import com.subgraph.orchid.directory.DocumentFieldParserImpl;
import com.subgraph.orchid.directory.parsing.BasicDocumentParsingResult;
import com.subgraph.orchid.directory.parsing.DocumentFieldParser;
import com.subgraph.orchid.directory.parsing.DocumentObject;
import com.subgraph.orchid.directory.parsing.DocumentParser;
import com.subgraph.orchid.directory.parsing.DocumentParsingHandler;
import com.subgraph.orchid.directory.parsing.DocumentParsingResult;
import com.subgraph.orchid.directory.parsing.DocumentParsingResultHandler;
import com.subgraph.orchid.encoders.Base64;
public class HSDescriptorParser implements DocumentParser<HSDescriptor>{
private static final Logger logger = Logger.getLogger(HSDescriptor.class.getName());
private final DocumentFieldParser fieldParser;
private final HSDescriptor descriptor;
private final HSAuthentication authentication;
private DocumentParsingResultHandler<HSDescriptor> resultHandler;
public HSDescriptorParser(HiddenService hiddenService, DocumentFieldParser fieldParser) {
this(hiddenService, fieldParser, null);
}
public HSDescriptorParser(HiddenService hiddenService, DocumentFieldParser fieldParser, HSDescriptorCookie cookie) {
this.fieldParser = fieldParser;
this.fieldParser.setHandler(createParsingHandler());
this.descriptor = new HSDescriptor(hiddenService);
this.authentication = new HSAuthentication(cookie);
}
private DocumentParsingHandler createParsingHandler() {
return new DocumentParsingHandler() {
public void parseKeywordLine() {
processKeywordLine();
}
public void endOfDocument() {
}
};
}
public boolean parse(DocumentParsingResultHandler<HSDescriptor> resultHandler) {
this.resultHandler = resultHandler;
fieldParser.startSignedEntity();
try {
fieldParser.processDocument();
return true;
} catch(TorParsingException e) {
resultHandler.parsingError(e.getMessage());
return false;
}
}
public DocumentParsingResult<HSDescriptor> parse() {
final BasicDocumentParsingResult<HSDescriptor> result = new BasicDocumentParsingResult<HSDescriptor>();
parse(result);
return result;
}
private void processKeywordLine() {
final HSDescriptorKeyword keyword = HSDescriptorKeyword.findKeyword(fieldParser.getCurrentKeyword());
if(!keyword.equals(HSDescriptorKeyword.UNKNOWN_KEYWORD)) {
processKeyword(keyword);
}
}
private void processKeyword(HSDescriptorKeyword keyword) {
switch(keyword) {
case RENDEZVOUS_SERVICE_DESCRIPTOR:
descriptor.setDescriptorId(fieldParser.parseBase32Digest());
break;
case VERSION:
if(fieldParser.parseInteger() != 2) {
throw new TorParsingException("Unexpected Descriptor version");
}
break;
case PERMANENT_KEY:
descriptor.setPermanentKey(fieldParser.parsePublicKey());
break;
case SECRET_ID_PART:
descriptor.setSecretIdPart(fieldParser.parseBase32Digest());
break;
case PUBLICATION_TIME:
descriptor.setPublicationTime(fieldParser.parseTimestamp());
break;
case PROTOCOL_VERSIONS:
descriptor.setProtocolVersions(fieldParser.parseIntegerList());
break;
case INTRODUCTION_POINTS:
processIntroductionPoints();
break;
case SIGNATURE:
processSignature();
break;
case UNKNOWN_KEYWORD:
break;
}
}
private void processIntroductionPoints() {
final DocumentObject ob = fieldParser.parseObject();
final ByteBuffer buffer = createIntroductionPointBuffer(ob);
final IntroductionPointParser parser = new IntroductionPointParser(new DocumentFieldParserImpl(buffer));
parser.parse(new DocumentParsingResultHandler<IntroductionPoint>() {
public void documentParsed(IntroductionPoint document) {
logger.fine("adding intro point "+ document.getIdentity());
descriptor.addIntroductionPoint(document);
}
public void documentInvalid(IntroductionPoint document, String message) {
logger.info("Invalid introduction point received");
}
public void parsingError(String message) {
logger.info("Error parsing introduction points: "+ message);
}
});
}
private ByteBuffer createIntroductionPointBuffer(DocumentObject ob) {
final byte[] content = Base64.decode(ob.getContent(false));
if(content[0] == 'i') {
return ByteBuffer.wrap(content);
} else {
try {
byte[] decrypted = authentication.decryptIntroductionPoints(content);
return ByteBuffer.wrap(decrypted);
} catch (HSAuthenticationException e) {
throw new TorParsingException("Failed to decrypt introduction points: "+ e.getMessage());
}
}
}
private void processSignature() {
fieldParser.endSignedEntity();
final TorSignature signature = fieldParser.parseSignature();
if(!fieldParser.verifySignedEntity(descriptor.getPermanentKey(), signature)) {
resultHandler.documentInvalid(descriptor, "Signature verification failed");
fieldParser.logWarn("Signature failed for descriptor: "+ descriptor.getDescriptorId().toBase32());
return;
}
resultHandler.documentParsed(descriptor);
}
}

View File

@ -0,0 +1,116 @@
package com.subgraph.orchid.circuits.hs;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.subgraph.orchid.ConsensusDocument;
import com.subgraph.orchid.Directory;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.crypto.TorRandom;
import com.subgraph.orchid.data.HexDigest;
public class HSDirectories {
private final static int DIR_CLUSTER_SZ = 3;
private final Directory directory;
private final TorRandom random;
private ConsensusDocument currentConsensus;
private List<Router> hsDirectories;
HSDirectories(Directory directory) {
this.directory = directory;
this.hsDirectories = new ArrayList<Router>();
this.random = new TorRandom();
}
List<HSDescriptorDirectory> getDirectoriesForHiddenService(HiddenService hs) {
final List<HSDescriptorDirectory> dirs = new ArrayList<HSDescriptorDirectory>(2 * DIR_CLUSTER_SZ);
for(HexDigest id: hs.getAllCurrentDescriptorIds()) {
for(Router r: getDirectoriesForDescriptorId(id)) {
dirs.add(new HSDescriptorDirectory(id, r));
}
}
return dirs;
}
private List<Router> getDirectoriesForDescriptorId(HexDigest descriptorId) {
final String hexId = descriptorId.toString();
refreshFromDirectory();
final int idx = getIndexForDescriptorId(hexId);
return selectDirectoriesAtIndex(idx);
}
private int getIndexForDescriptorId(String hexId) {
for(int i = 0; i < hsDirectories.size(); i++) {
String routerId = getHexIdForIndex(i);
if(routerId.compareTo(hexId) > 0) {
return i;
}
}
return 0;
}
private String getHexIdForIndex(int idx) {
final Router r = hsDirectories.get(idx);
return r.getIdentityHash().toString();
}
private List<Router> selectDirectoriesAtIndex(int idx) {
if(idx < 0 || idx >= hsDirectories.size()) {
throw new IllegalArgumentException("idx = "+ idx);
}
if(hsDirectories.size() < DIR_CLUSTER_SZ) {
throw new IllegalStateException();
}
final List<Router> dirs = new ArrayList<Router>(DIR_CLUSTER_SZ);
for(int i = 0; i < DIR_CLUSTER_SZ; i++) {
dirs.add(hsDirectories.get(idx));
idx += 1;
if(idx == hsDirectories.size()) {
idx = 0;
}
}
randomShuffle(dirs);
return dirs;
}
private void refreshFromDirectory() {
ConsensusDocument consensus = directory.getCurrentConsensusDocument();
if(currentConsensus == consensus) {
return;
}
currentConsensus = consensus;
hsDirectories.clear();
for(Router r: directory.getAllRouters()) {
if(r.isHSDirectory()) {
hsDirectories.add(r);
}
}
Collections.sort(hsDirectories, new Comparator<Router>() {
public int compare(Router r1, Router r2) {
final String s1 = r1.getIdentityHash().toString();
final String s2 = r2.getIdentityHash().toString();
return s1.compareTo(s2);
}
});
}
private void randomShuffle(List<Router> dirs) {
for(int i = 0; i < dirs.size(); i++) {
swap(dirs, i, random.nextInt(dirs.size()));
}
}
private void swap(List<Router> dirs, int idx1, int idx2) {
if(idx1 != idx2) {
final Router r1 = dirs.get(idx1);
final Router r2 = dirs.get(idx2);
dirs.set(idx1, r2);
dirs.set(idx2, r1);
}
}
}

View File

@ -0,0 +1,139 @@
package com.subgraph.orchid.circuits.hs;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.subgraph.orchid.HiddenServiceCircuit;
import com.subgraph.orchid.TorConfig;
import com.subgraph.orchid.circuits.hs.HSDescriptorCookie.CookieType;
import com.subgraph.orchid.crypto.TorMessageDigest;
import com.subgraph.orchid.data.Base32;
import com.subgraph.orchid.data.HexDigest;
public class HiddenService {
private final TorConfig config;
private final byte[] permanentId;
private HSDescriptor descriptor;
private HiddenServiceCircuit circuit;
static byte[] decodeOnion(String onionAddress) {
final int idx = onionAddress.indexOf(".onion");
if(idx == -1) {
return Base32.base32Decode(onionAddress);
} else {
return Base32.base32Decode(onionAddress.substring(0, idx));
}
}
HiddenService(TorConfig config, byte[] permanentId) {
this.config = config;
this.permanentId = permanentId;
}
String getOnionAddressForLogging() {
if(config.getSafeLogging()) {
return "[scrubbed]";
} else {
return getOnionAddress();
}
}
String getOnionAddress() {
return Base32.base32Encode(permanentId) + ".onion";
}
boolean hasCurrentDescriptor() {
return (descriptor != null && !descriptor.isExpired());
}
HSDescriptor getDescriptor() {
return descriptor;
}
void setDescriptor(HSDescriptor descriptor) {
this.descriptor = descriptor;
}
HiddenServiceCircuit getCircuit() {
return circuit;
}
void setCircuit(HiddenServiceCircuit circuit) {
this.circuit = circuit;
}
HSDescriptorCookie getAuthenticationCookie() {
return config.getHidServAuth(getOnionAddress());
}
List<HexDigest> getAllCurrentDescriptorIds() {
final List<HexDigest> ids = new ArrayList<HexDigest>();
ids.add(getCurrentDescriptorId(0));
ids.add(getCurrentDescriptorId(1));
return ids;
}
HexDigest getCurrentDescriptorId(int replica) {
final TorMessageDigest digest = new TorMessageDigest();
digest.update(permanentId);
digest.update(getCurrentSecretId(replica));
return digest.getHexDigest();
}
byte[] getCurrentSecretId(int replica) {
final TorMessageDigest digest = new TorMessageDigest();
digest.update(getCurrentTimePeriod());
final HSDescriptorCookie cookie = getAuthenticationCookie();
if(cookie != null && cookie.getType() == CookieType.COOKIE_STEALTH) {
digest.update(cookie.getValue());
}
digest.update(new byte[] { (byte) replica });
return digest.getDigestBytes();
}
byte[] getCurrentTimePeriod() {
final long now = System.currentTimeMillis() / 1000;
final int idByte = permanentId[0] & 0xFF;
return calculateTimePeriod(now, idByte);
}
static byte[] calculateTimePeriod(long currentTime, int idByte) {
final long t = (currentTime + (idByte * 86400L / 256)) / 86400L;
return toNetworkBytes(t);
}
static byte[] toNetworkBytes(long value) {
final byte[] result = new byte[4];
for(int i = 3; i >= 0; i--) {
result[i] = (byte) (value & 0xFF);
value >>= 8;
}
return result;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(permanentId);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
HiddenService other = (HiddenService) obj;
if (!Arrays.equals(permanentId, other.permanentId))
return false;
return true;
}
}

View File

@ -0,0 +1,121 @@
package com.subgraph.orchid.circuits.hs;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import com.subgraph.orchid.Directory;
import com.subgraph.orchid.HiddenServiceCircuit;
import com.subgraph.orchid.OpenFailedException;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.TorConfig;
import com.subgraph.orchid.TorException;
import com.subgraph.orchid.circuits.CircuitManagerImpl;
public class HiddenServiceManager {
private final static int RENDEZVOUS_RETRY_COUNT = 5;
private final static int HS_STREAM_TIMEOUT = 20000;
private final static Logger logger = Logger.getLogger(HiddenServiceManager.class.getName());
private final Map<String, HiddenService> hiddenServices;
private final TorConfig config;
private final Directory directory;
private final HSDirectories hsDirectories;
private final CircuitManagerImpl circuitManager;
public HiddenServiceManager(TorConfig config, Directory directory, CircuitManagerImpl circuitManager) {
this.config = config;
this.directory = directory;
this.hiddenServices = new HashMap<String, HiddenService>();
this.hsDirectories = new HSDirectories(directory);
this.circuitManager = circuitManager;
}
public Stream getStreamTo(String onion, int port) throws OpenFailedException, InterruptedException, TimeoutException {
final HiddenService hs = getHiddenServiceForOnion(onion);
final HiddenServiceCircuit circuit = getCircuitTo(hs);
try {
return circuit.openStream(port, HS_STREAM_TIMEOUT);
} catch (StreamConnectFailedException e) {
throw new OpenFailedException("Failed to open stream to hidden service "+ hs.getOnionAddressForLogging() + " reason "+ e.getReason());
}
}
private synchronized HiddenServiceCircuit getCircuitTo(HiddenService hs) throws OpenFailedException {
if(hs.getCircuit() == null) {
final HiddenServiceCircuit c = openCircuitTo(hs);
if(c == null) {
throw new OpenFailedException("Failed to open circuit to "+ hs.getOnionAddressForLogging());
}
hs.setCircuit(c);
}
return hs.getCircuit();
}
private HiddenServiceCircuit openCircuitTo(HiddenService hs) throws OpenFailedException {
HSDescriptor descriptor = getDescriptorFor(hs);
for(int i = 0; i < RENDEZVOUS_RETRY_COUNT; i++) {
final HiddenServiceCircuit c = openRendezvousCircuit(hs, descriptor);
if(c != null) {
return c;
}
}
throw new OpenFailedException("Failed to open circuit to "+ hs.getOnionAddressForLogging());
}
HSDescriptor getDescriptorFor(HiddenService hs) throws OpenFailedException {
if(hs.hasCurrentDescriptor()) {
return hs.getDescriptor();
}
final HSDescriptor descriptor = downloadDescriptorFor(hs);
if(descriptor == null) {
final String msg = "Failed to download HS descriptor for "+ hs.getOnionAddressForLogging();
logger.info(msg);
throw new OpenFailedException(msg);
}
hs.setDescriptor(descriptor);
return descriptor;
}
private HSDescriptor downloadDescriptorFor(HiddenService hs) {
logger.fine("Downloading HS descriptor for "+ hs.getOnionAddressForLogging());
final List<HSDescriptorDirectory> dirs = hsDirectories.getDirectoriesForHiddenService(hs);
final HSDescriptorDownloader downloader = new HSDescriptorDownloader(hs, circuitManager, dirs);
return downloader.downloadDescriptor();
}
HiddenService getHiddenServiceForOnion(String onion) throws OpenFailedException {
final String key = onion.endsWith(".onion") ? onion.substring(0, onion.length() - 6) : onion;
synchronized(hiddenServices) {
if(!hiddenServices.containsKey(key)) {
hiddenServices.put(key, createHiddenServiceFor(key));
}
return hiddenServices.get(key);
}
}
private HiddenService createHiddenServiceFor(String key) throws OpenFailedException {
try {
byte[] decoded = HiddenService.decodeOnion(key);
return new HiddenService(config, decoded);
} catch (TorException e) {
final String target = config.getSafeLogging() ? "[scrubbed]" : (key + ".onion");
throw new OpenFailedException("Failed to decode onion address "+ target + " : "+ e.getMessage());
}
}
private HiddenServiceCircuit openRendezvousCircuit(HiddenService hs, HSDescriptor descriptor) {
final RendezvousCircuitBuilder builder = new RendezvousCircuitBuilder(directory, circuitManager, hs, descriptor);
try {
return builder.call();
} catch (Exception e) {
return null;
}
}
}

View File

@ -0,0 +1,58 @@
package com.subgraph.orchid.circuits.hs;
import com.subgraph.orchid.crypto.TorPublicKey;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.IPv4Address;
public class IntroductionPoint {
private HexDigest identity;
private IPv4Address address;
private int onionPort;
private TorPublicKey onionKey;
private TorPublicKey serviceKey;
IntroductionPoint(HexDigest identity) {
this.identity = identity;
}
void setAddress(IPv4Address address) {
this.address = address;
}
void setOnionPort(int onionPort) {
this.onionPort = onionPort;
}
void setOnionKey(TorPublicKey onionKey) {
this.onionKey = onionKey;
}
void setServiceKey(TorPublicKey serviceKey) {
this.serviceKey = serviceKey;
}
boolean isValidDocument() {
return identity != null && address != null && onionPort != 0 && onionKey != null && serviceKey != null;
}
public HexDigest getIdentity() {
return identity;
}
public IPv4Address getAddress() {
return address;
}
public int getPort() {
return onionPort;
}
public TorPublicKey getOnionKey() {
return onionKey;
}
public TorPublicKey getServiceKey() {
return serviceKey;
}
}

View File

@ -0,0 +1,37 @@
package com.subgraph.orchid.circuits.hs;
public enum IntroductionPointKeyword {
SERVICE_AUTHENTICATION("service-authentication", 2),
INTRODUCTION_POINT("introduction-point", 1),
IP_ADDRESS("ip-address", 1),
ONION_PORT("onion-port", 1),
ONION_KEY("onion-key", 0),
SERVICE_KEY("service-key", 0),
INTRO_AUTHENTICATION("intro-authentication", 2),
UNKNOWN_KEYWORD("KEYWORD NOT FOUND", 0);
private final String keyword;
private final int argumentCount;
IntroductionPointKeyword(String keyword, int argumentCount) {
this.keyword = keyword;
this.argumentCount = argumentCount;
}
String getKeyword() {
return keyword;
}
int getArgumentCount() {
return argumentCount;
}
static IntroductionPointKeyword findKeyword(String keyword) {
for(IntroductionPointKeyword k: values()) {
if(k.getKeyword().equals(keyword)) {
return k;
}
}
return UNKNOWN_KEYWORD;
}
}

View File

@ -0,0 +1,118 @@
package com.subgraph.orchid.circuits.hs;
import com.subgraph.orchid.TorParsingException;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.directory.parsing.BasicDocumentParsingResult;
import com.subgraph.orchid.directory.parsing.DocumentFieldParser;
import com.subgraph.orchid.directory.parsing.DocumentParser;
import com.subgraph.orchid.directory.parsing.DocumentParsingHandler;
import com.subgraph.orchid.directory.parsing.DocumentParsingResult;
import com.subgraph.orchid.directory.parsing.DocumentParsingResultHandler;
public class IntroductionPointParser implements DocumentParser<IntroductionPoint>{
private final DocumentFieldParser fieldParser;
private DocumentParsingResultHandler<IntroductionPoint> resultHandler;
private IntroductionPoint currentIntroductionPoint;
public IntroductionPointParser(DocumentFieldParser fieldParser) {
this.fieldParser = fieldParser;
this.fieldParser.setHandler(createParsingHandler());
}
public boolean parse(DocumentParsingResultHandler<IntroductionPoint> resultHandler) {
this.resultHandler = resultHandler;
try {
fieldParser.processDocument();
return true;
} catch(TorParsingException e) {
resultHandler.parsingError(e.getMessage());
return false;
}
}
public DocumentParsingResult<IntroductionPoint> parse() {
final BasicDocumentParsingResult<IntroductionPoint> result = new BasicDocumentParsingResult<IntroductionPoint>();
parse(result);
return result;
}
private DocumentParsingHandler createParsingHandler() {
return new DocumentParsingHandler() {
public void parseKeywordLine() {
processKeywordLine();
}
public void endOfDocument() {
validateAndReportIntroductionPoint(currentIntroductionPoint);
}
};
}
private void resetIntroductionPoint(HexDigest identity) {
validateAndReportIntroductionPoint(currentIntroductionPoint);
currentIntroductionPoint = new IntroductionPoint(identity);
}
private void validateAndReportIntroductionPoint(IntroductionPoint introductionPoint) {
if(introductionPoint == null) {
return;
}
if(introductionPoint.isValidDocument()) {
resultHandler.documentParsed(introductionPoint);
} else {
resultHandler.documentInvalid(introductionPoint, "Invalid introduction point");
}
}
private void processKeywordLine() {
final IntroductionPointKeyword keyword = IntroductionPointKeyword.findKeyword(fieldParser.getCurrentKeyword());
if(!keyword.equals(IntroductionPointKeyword.UNKNOWN_KEYWORD)) {
processKeyword(keyword);
}
}
private void processKeyword(IntroductionPointKeyword keyword) {
switch(keyword) {
case INTRO_AUTHENTICATION:
break;
case INTRODUCTION_POINT:
resetIntroductionPoint(fieldParser.parseBase32Digest());
break;
case IP_ADDRESS:
if(currentIntroductionPoint != null) {
currentIntroductionPoint.setAddress(fieldParser.parseAddress());
}
break;
case ONION_KEY:
if(currentIntroductionPoint != null) {
currentIntroductionPoint.setOnionKey(fieldParser.parsePublicKey());
}
break;
case ONION_PORT:
if(currentIntroductionPoint != null) {
currentIntroductionPoint.setOnionPort(fieldParser.parsePort());
}
break;
case SERVICE_KEY:
if(currentIntroductionPoint != null) {
currentIntroductionPoint.setServiceKey(fieldParser.parsePublicKey());
}
break;
case SERVICE_AUTHENTICATION:
break;
case UNKNOWN_KEYWORD:
break;
}
}
}

Some files were not shown because too many files have changed in this diff Show More