From 7e609a24093e3bd0b0141db815f5052bfce3fae1 Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Mon, 14 Nov 2016 20:48:19 +0100 Subject: [PATCH] Remove Orchid forked subproject and support for connecting via Tor. --- .travis.yml | 4 +- core/pom.xml | 5 - .../java/org/bitcoinj/core/PeerGroup.java | 103 +- .../java/org/bitcoinj/kits/WalletAppKit.java | 21 +- .../bitcoinj/net/discovery/TorDiscovery.java | 279 --- orchid/.gitignore | 5 - orchid/LICENSE | 25 - orchid/README | 0 orchid/build.xml | 116 - orchid/data/GeoIP.dat | Bin 583720 -> 0 bytes orchid/data/README | 3 - orchid/doc/spec/address-spec.txt | 58 - orchid/doc/spec/bridges-spec.txt | 249 -- orchid/doc/spec/control-spec.txt | 1853 -------------- orchid/doc/spec/dir-spec.txt | 2132 ----------------- orchid/doc/spec/path-spec.txt | 437 ---- orchid/doc/spec/rend-spec.txt | 751 ------ orchid/doc/spec/socks-extensions.txt | 78 - orchid/doc/spec/tor-spec.txt | 992 -------- orchid/logging.properties | 8 - .../orchid/xmlrpc/OrchidXmlRpcTransport.java | 309 --- .../xmlrpc/OrchidXmlRpcTransportFactory.java | 30 - orchid/pom.xml | 156 -- .../src/com/subgraph/orchid/BridgeRouter.java | 8 - orchid/src/com/subgraph/orchid/Cell.java | 230 -- orchid/src/com/subgraph/orchid/Circuit.java | 95 - .../subgraph/orchid/CircuitBuildHandler.java | 60 - .../com/subgraph/orchid/CircuitManager.java | 51 - .../src/com/subgraph/orchid/CircuitNode.java | 90 - .../src/com/subgraph/orchid/Connection.java | 47 - .../com/subgraph/orchid/ConnectionCache.java | 23 - .../orchid/ConnectionFailedException.java | 11 - .../orchid/ConnectionHandshakeException.java | 10 - .../orchid/ConnectionIOException.java | 14 - .../orchid/ConnectionTimeoutException.java | 14 - .../subgraph/orchid/ConsensusDocument.java | 44 - .../src/com/subgraph/orchid/Descriptor.java | 65 - orchid/src/com/subgraph/orchid/Directory.java | 48 - .../com/subgraph/orchid/DirectoryCircuit.java | 16 - .../subgraph/orchid/DirectoryDownloader.java | 27 - .../com/subgraph/orchid/DirectoryServer.java | 22 - .../com/subgraph/orchid/DirectoryStore.java | 36 - orchid/src/com/subgraph/orchid/Document.java | 9 - .../src/com/subgraph/orchid/ExitCircuit.java | 50 - .../src/com/subgraph/orchid/GuardEntry.java | 18 - .../subgraph/orchid/HiddenServiceCircuit.java | 8 - .../com/subgraph/orchid/InternalCircuit.java | 7 - .../com/subgraph/orchid/KeyCertificate.java | 78 - .../subgraph/orchid/OpenFailedException.java | 13 - orchid/src/com/subgraph/orchid/RelayCell.java | 65 - orchid/src/com/subgraph/orchid/Revision.java | 35 - orchid/src/com/subgraph/orchid/Router.java | 47 - .../com/subgraph/orchid/RouterDescriptor.java | 164 -- .../orchid/RouterMicrodescriptor.java | 6 - .../src/com/subgraph/orchid/RouterStatus.java | 24 - .../subgraph/orchid/SocksPortListener.java | 6 - orchid/src/com/subgraph/orchid/Stream.java | 49 - .../orchid/StreamConnectFailedException.java | 35 - orchid/src/com/subgraph/orchid/Threading.java | 71 - orchid/src/com/subgraph/orchid/Tor.java | 167 -- orchid/src/com/subgraph/orchid/TorClient.java | 217 -- orchid/src/com/subgraph/orchid/TorConfig.java | 151 -- .../src/com/subgraph/orchid/TorException.java | 22 - .../orchid/TorInitializationListener.java | 6 - .../subgraph/orchid/TorParsingException.java | 14 - .../subgraph/orchid/VoteAuthorityEntry.java | 19 - .../orchid/circuits/CircuitBuildTask.java | 127 - .../circuits/CircuitCreationRequest.java | 91 - .../orchid/circuits/CircuitCreationTask.java | 297 --- .../orchid/circuits/CircuitExtender.java | 155 -- .../subgraph/orchid/circuits/CircuitIO.java | 351 --- .../subgraph/orchid/circuits/CircuitImpl.java | 289 --- .../orchid/circuits/CircuitManagerImpl.java | 443 ---- .../circuits/CircuitNodeCryptoState.java | 102 - .../orchid/circuits/CircuitNodeImpl.java | 121 - .../orchid/circuits/CircuitPredictor.java | 99 - .../orchid/circuits/CircuitStatus.java | 115 - .../orchid/circuits/DirectoryCircuitImpl.java | 42 - .../orchid/circuits/ExitCircuitImpl.java | 87 - .../orchid/circuits/InternalCircuitImpl.java | 118 - .../orchid/circuits/NTorCircuitExtender.java | 114 - .../orchid/circuits/OpenExitStreamTask.java | 50 - .../orchid/circuits/PendingExitStreams.java | 77 - .../orchid/circuits/PredictedPortTarget.java | 29 - .../orchid/circuits/StreamExitRequest.java | 170 -- .../subgraph/orchid/circuits/StreamImpl.java | 219 -- .../orchid/circuits/TapCircuitExtender.java | 55 - .../circuits/TorInitializationTracker.java | 103 - .../orchid/circuits/TorInputStream.java | 228 -- .../orchid/circuits/TorOutputStream.java | 85 - .../orchid/circuits/cells/CellImpl.java | 215 -- .../orchid/circuits/cells/RelayCellImpl.java | 180 -- .../circuits/guards/BridgeRouterImpl.java | 226 -- .../orchid/circuits/guards/Bridges.java | 163 -- .../orchid/circuits/guards/EntryGuards.java | 305 --- .../circuits/guards/GuardProbeTask.java | 42 - .../orchid/circuits/hs/HSAuthentication.java | 120 - .../hs/HSAuthenticationException.java | 14 - .../orchid/circuits/hs/HSDescriptor.java | 109 - .../circuits/hs/HSDescriptorCookie.java | 33 - .../circuits/hs/HSDescriptorDirectory.java | 28 - .../circuits/hs/HSDescriptorDownloader.java | 135 -- .../circuits/hs/HSDescriptorKeyword.java | 38 - .../circuits/hs/HSDescriptorParser.java | 160 -- .../orchid/circuits/hs/HSDirectories.java | 116 - .../orchid/circuits/hs/HiddenService.java | 139 -- .../circuits/hs/HiddenServiceManager.java | 121 - .../orchid/circuits/hs/IntroductionPoint.java | 58 - .../circuits/hs/IntroductionPointKeyword.java | 37 - .../circuits/hs/IntroductionPointParser.java | 118 - .../circuits/hs/IntroductionProcessor.java | 105 - .../circuits/hs/RendezvousCircuitBuilder.java | 99 - .../circuits/hs/RendezvousProcessor.java | 96 - .../path/BandwidthWeightedRouters.java | 184 -- .../circuits/path/CircuitNodeChooser.java | 186 -- .../CircuitNodeChooserWeightParameters.java | 149 -- .../circuits/path/CircuitPathChooser.java | 202 -- .../circuits/path/ConfigNodeFilter.java | 201 -- .../path/PathSelectionFailedException.java | 11 - .../orchid/circuits/path/RouterFilter.java | 7 - .../circuits/path/TorConfigNodeFilter.java | 70 - .../orchid/config/TorConfigBridgeLine.java | 29 - .../orchid/config/TorConfigHSAuth.java | 57 - .../orchid/config/TorConfigInterval.java | 111 - .../orchid/config/TorConfigParser.java | 75 - .../orchid/config/TorConfigProxy.java | 199 -- .../connections/ConnectionCacheImpl.java | 204 -- .../connections/ConnectionHandshake.java | 158 -- .../connections/ConnectionHandshakeV2.java | 90 - .../connections/ConnectionHandshakeV3.java | 195 -- .../orchid/connections/ConnectionImpl.java | 374 --- .../connections/ConnectionSocketFactory.java | 76 - .../subgraph/orchid/crypto/ASN1Parser.java | 146 -- .../subgraph/orchid/crypto/Curve25519.java | 469 ---- .../orchid/crypto/HybridEncryption.java | 150 -- .../com/subgraph/orchid/crypto/PRNGFixes.java | 360 --- .../subgraph/orchid/crypto/RSAKeyEncoder.java | 131 - .../crypto/TorCreateFastKeyAgreement.java | 54 - .../orchid/crypto/TorKeyAgreement.java | 6 - .../orchid/crypto/TorKeyDerivation.java | 40 - .../orchid/crypto/TorMessageDigest.java | 128 - .../orchid/crypto/TorNTorKeyAgreement.java | 160 -- .../subgraph/orchid/crypto/TorPrivateKey.java | 48 - .../subgraph/orchid/crypto/TorPublicKey.java | 133 - .../crypto/TorRFC5869KeyDerivation.java | 79 - .../com/subgraph/orchid/crypto/TorRandom.java | 56 - .../subgraph/orchid/crypto/TorSignature.java | 75 - .../orchid/crypto/TorStreamCipher.java | 127 - .../orchid/crypto/TorTapKeyAgreement.java | 195 -- .../subgraph/orchid/dashboard/Dashboard.java | 186 -- .../orchid/dashboard/DashboardConnection.java | 130 - .../orchid/dashboard/DashboardRenderable.java | 15 - .../orchid/dashboard/DashboardRenderer.java | 8 - .../orchid/data/BandwidthHistory.java | 29 - .../src/com/subgraph/orchid/data/Base32.java | 88 - .../com/subgraph/orchid/data/HexDigest.java | 140 -- .../com/subgraph/orchid/data/IPv4Address.java | 102 - .../com/subgraph/orchid/data/RandomSet.java | 72 - .../com/subgraph/orchid/data/Timestamp.java | 50 - .../orchid/data/exitpolicy/ExitPolicy.java | 54 - .../orchid/data/exitpolicy/ExitPorts.java | 54 - .../orchid/data/exitpolicy/ExitTarget.java | 10 - .../orchid/data/exitpolicy/Network.java | 47 - .../orchid/data/exitpolicy/PolicyRule.java | 69 - .../orchid/data/exitpolicy/PortRange.java | 72 - .../orchid/directory/DescriptorCache.java | 206 -- .../orchid/directory/DescriptorCacheData.java | 88 - .../directory/DirectoryAuthorityStatus.java | 102 - .../orchid/directory/DirectoryImpl.java | 513 ---- .../orchid/directory/DirectoryServerImpl.java | 147 -- .../orchid/directory/DirectoryStoreFile.java | 226 -- .../orchid/directory/DirectoryStoreImpl.java | 58 - .../directory/DocumentFieldParserImpl.java | 422 ---- .../directory/DocumentParserFactoryImpl.java | 36 - .../orchid/directory/GuardEntryImpl.java | 274 --- .../subgraph/orchid/directory/RouterImpl.java | 260 -- .../subgraph/orchid/directory/StateFile.java | 296 --- .../orchid/directory/TrustedAuthorities.java | 138 -- .../certificate/KeyCertificateImpl.java | 93 - .../certificate/KeyCertificateKeyword.java | 42 - .../certificate/KeyCertificateParser.java | 151 -- .../consensus/AuthoritySectionParser.java | 64 - .../consensus/ConsensusDocumentImpl.java | 344 --- .../consensus/ConsensusDocumentParser.java | 106 - .../ConsensusDocumentSectionParser.java | 38 - .../consensus/DirectorySignature.java | 35 - .../directory/consensus/DocumentKeyword.java | 103 - .../consensus/FooterSectionParser.java | 85 - .../consensus/PreambleSectionParser.java | 131 - .../consensus/RequiredCertificateImpl.java | 69 - .../directory/consensus/RouterStatusImpl.java | 106 - .../consensus/RouterStatusSectionParser.java | 141 -- .../consensus/VoteAuthorityEntryImpl.java | 67 - .../downloader/BridgeDescriptorFetcher.java | 19 - .../downloader/CertificateFetcher.java | 40 - .../downloader/ConsensusFetcher.java | 29 - .../downloader/DescriptorProcessor.java | 112 - .../DirectoryDocumentRequestor.java | 125 - .../downloader/DirectoryDownloadTask.java | 228 -- .../downloader/DirectoryDownloaderImpl.java | 144 -- .../DirectoryRequestFailedException.java | 15 - .../directory/downloader/DocumentFetcher.java | 52 - .../directory/downloader/HttpConnection.java | 247 -- .../downloader/MicrodescriptorFetcher.java | 44 - .../downloader/RouterDescriptorFetcher.java | 43 - .../parsing/BasicDocumentParsingResult.java | 70 - .../parsing/DocumentFieldParser.java | 341 --- .../directory/parsing/DocumentObject.java | 43 - .../directory/parsing/DocumentParser.java | 7 - .../parsing/DocumentParserFactory.java | 18 - .../parsing/DocumentParsingHandler.java | 6 - .../parsing/DocumentParsingResult.java | 13 - .../parsing/DocumentParsingResultHandler.java | 8 - .../parsing/NameIntegerParameter.java | 24 - .../router/MicrodescriptorCacheLocation.java | 25 - .../router/RouterDescriptorImpl.java | 317 --- .../router/RouterDescriptorKeyword.java | 63 - .../router/RouterDescriptorParser.java | 223 -- .../router/RouterMicrodescriptorImpl.java | 157 -- .../router/RouterMicrodescriptorKeyword.java | 41 - .../router/RouterMicrodescriptorParser.java | 130 - .../com/subgraph/orchid/encoders/Base64.java | 121 - .../orchid/encoders/Base64Encoder.java | 328 --- .../orchid/encoders/DecoderException.java | 20 - .../com/subgraph/orchid/encoders/Encoder.java | 17 - .../orchid/encoders/EncoderException.java | 20 - .../src/com/subgraph/orchid/encoders/Hex.java | 131 - .../subgraph/orchid/encoders/HexEncoder.java | 187 -- .../src/com/subgraph/orchid/events/Event.java | 3 - .../subgraph/orchid/events/EventHandler.java | 5 - .../subgraph/orchid/events/EventManager.java | 34 - .../orchid/geoip/CountryCodeService.java | 172 -- .../com/subgraph/orchid/misc/GuardedBy.java | 45 - .../com/subgraph/orchid/misc/Immutable.java | 41 - .../subgraph/orchid/misc/NotThreadSafe.java | 34 - .../com/subgraph/orchid/misc/ThreadSafe.java | 33 - .../src/com/subgraph/orchid/misc/Utils.java | 14 - .../sockets/AndroidSSLSocketFactory.java | 66 - .../orchid/sockets/AndroidSocket.java | 69 - .../orchid/sockets/OrchidSocketFactory.java | 80 - .../orchid/sockets/OrchidSocketImpl.java | 179 -- .../sockets/OrchidSocketImplFactory.java | 18 - .../sslengine/HandshakeCallbackHandler.java | 5 - .../sslengine/SSLEngineInputStream.java | 57 - .../sockets/sslengine/SSLEngineManager.java | 343 --- .../sslengine/SSLEngineOutputStream.java | 46 - .../sockets/sslengine/SSLEngineSSLSocket.java | 315 --- .../subgraph/orchid/socks/Socks4Request.java | 70 - .../subgraph/orchid/socks/Socks5Request.java | 145 -- .../orchid/socks/SocksClientTask.java | 122 - .../orchid/socks/SocksPortListenerImpl.java | 116 - .../subgraph/orchid/socks/SocksRequest.java | 179 -- .../orchid/socks/SocksRequestException.java | 15 - .../orchid/socks/SocksStreamConnection.java | 138 -- .../com/subgraph/orchid/TorConfigTest.java | 116 - .../orchid/circuits/TorInputStreamTest.java | 184 -- .../circuits/hs/HSDescriptorParserTest.java | 166 -- .../circuits/path/ConfigNodeFilterTest.java | 73 - .../orchid/crypto/ASN1ParserTest.java | 65 - .../orchid/crypto/RSAKeyEncoderTest.java | 53 - .../orchid/geoip/CountryCodeServiceTest.java | 37 - .../java/org/bitcoinj/tools/WalletTool.java | 11 - .../src/main/java/wallettemplate/Main.java | 4 - .../java/wallettemplate/MainController.java | 30 +- 264 files changed, 6 insertions(+), 32917 deletions(-) delete mode 100644 core/src/main/java/org/bitcoinj/net/discovery/TorDiscovery.java delete mode 100644 orchid/.gitignore delete mode 100644 orchid/LICENSE delete mode 100644 orchid/README delete mode 100644 orchid/build.xml delete mode 100644 orchid/data/GeoIP.dat delete mode 100644 orchid/data/README delete mode 100644 orchid/doc/spec/address-spec.txt delete mode 100644 orchid/doc/spec/bridges-spec.txt delete mode 100644 orchid/doc/spec/control-spec.txt delete mode 100644 orchid/doc/spec/dir-spec.txt delete mode 100644 orchid/doc/spec/path-spec.txt delete mode 100644 orchid/doc/spec/rend-spec.txt delete mode 100644 orchid/doc/spec/socks-extensions.txt delete mode 100644 orchid/doc/spec/tor-spec.txt delete mode 100644 orchid/logging.properties delete mode 100644 orchid/opt/xmlrpc/com/subgraph/orchid/xmlrpc/OrchidXmlRpcTransport.java delete mode 100644 orchid/opt/xmlrpc/com/subgraph/orchid/xmlrpc/OrchidXmlRpcTransportFactory.java delete mode 100644 orchid/pom.xml delete mode 100644 orchid/src/com/subgraph/orchid/BridgeRouter.java delete mode 100644 orchid/src/com/subgraph/orchid/Cell.java delete mode 100644 orchid/src/com/subgraph/orchid/Circuit.java delete mode 100644 orchid/src/com/subgraph/orchid/CircuitBuildHandler.java delete mode 100644 orchid/src/com/subgraph/orchid/CircuitManager.java delete mode 100644 orchid/src/com/subgraph/orchid/CircuitNode.java delete mode 100644 orchid/src/com/subgraph/orchid/Connection.java delete mode 100644 orchid/src/com/subgraph/orchid/ConnectionCache.java delete mode 100644 orchid/src/com/subgraph/orchid/ConnectionFailedException.java delete mode 100644 orchid/src/com/subgraph/orchid/ConnectionHandshakeException.java delete mode 100644 orchid/src/com/subgraph/orchid/ConnectionIOException.java delete mode 100644 orchid/src/com/subgraph/orchid/ConnectionTimeoutException.java delete mode 100644 orchid/src/com/subgraph/orchid/ConsensusDocument.java delete mode 100644 orchid/src/com/subgraph/orchid/Descriptor.java delete mode 100644 orchid/src/com/subgraph/orchid/Directory.java delete mode 100644 orchid/src/com/subgraph/orchid/DirectoryCircuit.java delete mode 100644 orchid/src/com/subgraph/orchid/DirectoryDownloader.java delete mode 100644 orchid/src/com/subgraph/orchid/DirectoryServer.java delete mode 100644 orchid/src/com/subgraph/orchid/DirectoryStore.java delete mode 100644 orchid/src/com/subgraph/orchid/Document.java delete mode 100644 orchid/src/com/subgraph/orchid/ExitCircuit.java delete mode 100644 orchid/src/com/subgraph/orchid/GuardEntry.java delete mode 100644 orchid/src/com/subgraph/orchid/HiddenServiceCircuit.java delete mode 100644 orchid/src/com/subgraph/orchid/InternalCircuit.java delete mode 100644 orchid/src/com/subgraph/orchid/KeyCertificate.java delete mode 100644 orchid/src/com/subgraph/orchid/OpenFailedException.java delete mode 100644 orchid/src/com/subgraph/orchid/RelayCell.java delete mode 100644 orchid/src/com/subgraph/orchid/Revision.java delete mode 100644 orchid/src/com/subgraph/orchid/Router.java delete mode 100644 orchid/src/com/subgraph/orchid/RouterDescriptor.java delete mode 100644 orchid/src/com/subgraph/orchid/RouterMicrodescriptor.java delete mode 100644 orchid/src/com/subgraph/orchid/RouterStatus.java delete mode 100644 orchid/src/com/subgraph/orchid/SocksPortListener.java delete mode 100644 orchid/src/com/subgraph/orchid/Stream.java delete mode 100644 orchid/src/com/subgraph/orchid/StreamConnectFailedException.java delete mode 100644 orchid/src/com/subgraph/orchid/Threading.java delete mode 100644 orchid/src/com/subgraph/orchid/Tor.java delete mode 100644 orchid/src/com/subgraph/orchid/TorClient.java delete mode 100644 orchid/src/com/subgraph/orchid/TorConfig.java delete mode 100644 orchid/src/com/subgraph/orchid/TorException.java delete mode 100644 orchid/src/com/subgraph/orchid/TorInitializationListener.java delete mode 100644 orchid/src/com/subgraph/orchid/TorParsingException.java delete mode 100644 orchid/src/com/subgraph/orchid/VoteAuthorityEntry.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitBuildTask.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitCreationRequest.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitCreationTask.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitExtender.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitIO.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitManagerImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitNodeCryptoState.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitNodeImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitPredictor.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/CircuitStatus.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/DirectoryCircuitImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/ExitCircuitImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/InternalCircuitImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/NTorCircuitExtender.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/OpenExitStreamTask.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/PendingExitStreams.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/PredictedPortTarget.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/StreamExitRequest.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/StreamImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/TapCircuitExtender.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/TorInitializationTracker.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/TorInputStream.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/TorOutputStream.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/cells/CellImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/cells/RelayCellImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/guards/BridgeRouterImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/guards/Bridges.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/guards/EntryGuards.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/guards/GuardProbeTask.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HSAuthentication.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HSAuthenticationException.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptor.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorCookie.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorDirectory.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorDownloader.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorKeyword.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorParser.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HSDirectories.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HiddenService.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/HiddenServiceManager.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPoint.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPointKeyword.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPointParser.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/IntroductionProcessor.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/RendezvousCircuitBuilder.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/hs/RendezvousProcessor.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/path/BandwidthWeightedRouters.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/path/CircuitNodeChooser.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/path/CircuitNodeChooserWeightParameters.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/path/CircuitPathChooser.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/path/ConfigNodeFilter.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/path/PathSelectionFailedException.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/path/RouterFilter.java delete mode 100644 orchid/src/com/subgraph/orchid/circuits/path/TorConfigNodeFilter.java delete mode 100644 orchid/src/com/subgraph/orchid/config/TorConfigBridgeLine.java delete mode 100644 orchid/src/com/subgraph/orchid/config/TorConfigHSAuth.java delete mode 100644 orchid/src/com/subgraph/orchid/config/TorConfigInterval.java delete mode 100644 orchid/src/com/subgraph/orchid/config/TorConfigParser.java delete mode 100644 orchid/src/com/subgraph/orchid/config/TorConfigProxy.java delete mode 100644 orchid/src/com/subgraph/orchid/connections/ConnectionCacheImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/connections/ConnectionHandshake.java delete mode 100644 orchid/src/com/subgraph/orchid/connections/ConnectionHandshakeV2.java delete mode 100644 orchid/src/com/subgraph/orchid/connections/ConnectionHandshakeV3.java delete mode 100644 orchid/src/com/subgraph/orchid/connections/ConnectionImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/connections/ConnectionSocketFactory.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/ASN1Parser.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/Curve25519.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/HybridEncryption.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/PRNGFixes.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/RSAKeyEncoder.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorCreateFastKeyAgreement.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorKeyAgreement.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorKeyDerivation.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorMessageDigest.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorNTorKeyAgreement.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorPrivateKey.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorPublicKey.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorRFC5869KeyDerivation.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorRandom.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorSignature.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorStreamCipher.java delete mode 100644 orchid/src/com/subgraph/orchid/crypto/TorTapKeyAgreement.java delete mode 100644 orchid/src/com/subgraph/orchid/dashboard/Dashboard.java delete mode 100644 orchid/src/com/subgraph/orchid/dashboard/DashboardConnection.java delete mode 100644 orchid/src/com/subgraph/orchid/dashboard/DashboardRenderable.java delete mode 100644 orchid/src/com/subgraph/orchid/dashboard/DashboardRenderer.java delete mode 100644 orchid/src/com/subgraph/orchid/data/BandwidthHistory.java delete mode 100644 orchid/src/com/subgraph/orchid/data/Base32.java delete mode 100644 orchid/src/com/subgraph/orchid/data/HexDigest.java delete mode 100644 orchid/src/com/subgraph/orchid/data/IPv4Address.java delete mode 100644 orchid/src/com/subgraph/orchid/data/RandomSet.java delete mode 100644 orchid/src/com/subgraph/orchid/data/Timestamp.java delete mode 100644 orchid/src/com/subgraph/orchid/data/exitpolicy/ExitPolicy.java delete mode 100644 orchid/src/com/subgraph/orchid/data/exitpolicy/ExitPorts.java delete mode 100644 orchid/src/com/subgraph/orchid/data/exitpolicy/ExitTarget.java delete mode 100644 orchid/src/com/subgraph/orchid/data/exitpolicy/Network.java delete mode 100644 orchid/src/com/subgraph/orchid/data/exitpolicy/PolicyRule.java delete mode 100644 orchid/src/com/subgraph/orchid/data/exitpolicy/PortRange.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/DescriptorCache.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/DescriptorCacheData.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/DirectoryAuthorityStatus.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/DirectoryImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/DirectoryServerImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/DirectoryStoreFile.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/DirectoryStoreImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/DocumentFieldParserImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/DocumentParserFactoryImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/GuardEntryImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/RouterImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/StateFile.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/TrustedAuthorities.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateKeyword.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/AuthoritySectionParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentSectionParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/DirectorySignature.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/DocumentKeyword.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/FooterSectionParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/PreambleSectionParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/RequiredCertificateImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/RouterStatusImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/RouterStatusSectionParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/consensus/VoteAuthorityEntryImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/BridgeDescriptorFetcher.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/CertificateFetcher.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/ConsensusFetcher.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/DescriptorProcessor.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDocumentRequestor.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDownloadTask.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDownloaderImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/DirectoryRequestFailedException.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/DocumentFetcher.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/HttpConnection.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/MicrodescriptorFetcher.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/downloader/RouterDescriptorFetcher.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/parsing/BasicDocumentParsingResult.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/parsing/DocumentFieldParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/parsing/DocumentObject.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/parsing/DocumentParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/parsing/DocumentParserFactory.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingHandler.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingResult.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingResultHandler.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/parsing/NameIntegerParameter.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/router/MicrodescriptorCacheLocation.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorKeyword.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorParser.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorKeyword.java delete mode 100644 orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorParser.java delete mode 100644 orchid/src/com/subgraph/orchid/encoders/Base64.java delete mode 100644 orchid/src/com/subgraph/orchid/encoders/Base64Encoder.java delete mode 100644 orchid/src/com/subgraph/orchid/encoders/DecoderException.java delete mode 100644 orchid/src/com/subgraph/orchid/encoders/Encoder.java delete mode 100644 orchid/src/com/subgraph/orchid/encoders/EncoderException.java delete mode 100644 orchid/src/com/subgraph/orchid/encoders/Hex.java delete mode 100644 orchid/src/com/subgraph/orchid/encoders/HexEncoder.java delete mode 100644 orchid/src/com/subgraph/orchid/events/Event.java delete mode 100644 orchid/src/com/subgraph/orchid/events/EventHandler.java delete mode 100644 orchid/src/com/subgraph/orchid/events/EventManager.java delete mode 100644 orchid/src/com/subgraph/orchid/geoip/CountryCodeService.java delete mode 100644 orchid/src/com/subgraph/orchid/misc/GuardedBy.java delete mode 100644 orchid/src/com/subgraph/orchid/misc/Immutable.java delete mode 100644 orchid/src/com/subgraph/orchid/misc/NotThreadSafe.java delete mode 100644 orchid/src/com/subgraph/orchid/misc/ThreadSafe.java delete mode 100644 orchid/src/com/subgraph/orchid/misc/Utils.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/AndroidSSLSocketFactory.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/AndroidSocket.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/OrchidSocketFactory.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/OrchidSocketImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/OrchidSocketImplFactory.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/sslengine/HandshakeCallbackHandler.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineInputStream.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineManager.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineOutputStream.java delete mode 100644 orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineSSLSocket.java delete mode 100644 orchid/src/com/subgraph/orchid/socks/Socks4Request.java delete mode 100644 orchid/src/com/subgraph/orchid/socks/Socks5Request.java delete mode 100644 orchid/src/com/subgraph/orchid/socks/SocksClientTask.java delete mode 100644 orchid/src/com/subgraph/orchid/socks/SocksPortListenerImpl.java delete mode 100644 orchid/src/com/subgraph/orchid/socks/SocksRequest.java delete mode 100644 orchid/src/com/subgraph/orchid/socks/SocksRequestException.java delete mode 100644 orchid/src/com/subgraph/orchid/socks/SocksStreamConnection.java delete mode 100644 orchid/test/com/subgraph/orchid/TorConfigTest.java delete mode 100644 orchid/test/com/subgraph/orchid/circuits/TorInputStreamTest.java delete mode 100644 orchid/test/com/subgraph/orchid/circuits/hs/HSDescriptorParserTest.java delete mode 100644 orchid/test/com/subgraph/orchid/circuits/path/ConfigNodeFilterTest.java delete mode 100644 orchid/test/com/subgraph/orchid/crypto/ASN1ParserTest.java delete mode 100644 orchid/test/com/subgraph/orchid/crypto/RSAKeyEncoderTest.java delete mode 100644 orchid/test/com/subgraph/orchid/geoip/CountryCodeServiceTest.java diff --git a/.travis.yml b/.travis.yml index 2ead96e7..b5b39f91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,7 @@ install: true # remove default script: - mvn -q clean install -Pno-network - jdk_switcher use openjdk6 - - cd orchid - - mvn -q clean package - - cd ../core + - cd core - mvn -q clean package -Pno-network after_success: diff --git a/core/pom.xml b/core/pom.xml index 03c8cccf..a6c5b2b5 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -426,11 +426,6 @@ 1.8 true - - org.bitcoinj - orchid - 1.2.1 - com.squareup.okhttp okhttp diff --git a/core/src/main/java/org/bitcoinj/core/PeerGroup.java b/core/src/main/java/org/bitcoinj/core/PeerGroup.java index 40b49880..4f5dc2fd 100644 --- a/core/src/main/java/org/bitcoinj/core/PeerGroup.java +++ b/core/src/main/java/org/bitcoinj/core/PeerGroup.java @@ -23,11 +23,8 @@ import com.google.common.collect.*; import com.google.common.net.*; import com.google.common.primitives.*; import com.google.common.util.concurrent.*; -import com.squareup.okhttp.*; -import com.subgraph.orchid.*; import net.jcip.annotations.*; import org.bitcoinj.core.listeners.*; -import org.bitcoinj.crypto.*; import org.bitcoinj.net.*; import org.bitcoinj.net.discovery.*; import org.bitcoinj.script.*; @@ -84,7 +81,6 @@ public class PeerGroup implements TransactionBroadcaster { * get through. */ public static final int DEFAULT_CONNECTIONS = 12; - private static final int TOR_TIMEOUT_SECONDS = 60; private volatile int vMaxPeersToDiscoverCount = 100; private static final long DEFAULT_PEER_DISCOVERY_TIMEOUT_MILLIS = 5000; private volatile long vPeerDiscoveryTimeoutMillis = DEFAULT_PEER_DISCOVERY_TIMEOUT_MILLIS; @@ -113,7 +109,6 @@ public class PeerGroup implements TransactionBroadcaster { // Currently connecting peers. private final CopyOnWriteArrayList pendingPeers; private final ClientConnectionManager channels; - @Nullable private final TorClient torClient; // The peer that has been selected for the purposes of downloading announced data. @GuardedBy("lock") private Peer downloadPeer; @@ -314,95 +309,22 @@ public class PeerGroup implements TransactionBroadcaster { this(context, chain, new NioClientManager()); } - /** See {@link #newWithTor(Context, AbstractBlockChain, TorClient)} */ - public static PeerGroup newWithTor(NetworkParameters params, @Nullable AbstractBlockChain chain, TorClient torClient) throws TimeoutException { - return newWithTor(Context.getOrCreate(params), chain, torClient); - } - - /** - *

Creates a PeerGroup that accesses the network via the Tor network. The provided TorClient is used so you can - * preconfigure it beforehand. It should not have been already started. You can just use "new TorClient()" if - * you don't have any particular configuration requirements.

- * - *

Peer discovery is automatically configured to use DNS seeds resolved via a random selection of exit nodes. - * If running on the Oracle JDK the unlimited strength jurisdiction checks will also be overridden, - * as they no longer apply anyway and can cause startup failures due to the requirement for AES-256.

- * - *

The user does not need any additional software for this: it's all pure Java. As of April 2014 this mode - * is experimental.

- * - * @throws TimeoutException if Tor fails to start within 20 seconds. - */ - public static PeerGroup newWithTor(Context context, @Nullable AbstractBlockChain chain, TorClient torClient) throws TimeoutException { - return newWithTor(context, chain, torClient, true); - } - - /** - *

Creates a PeerGroup that accesses the network via the Tor network. The provided TorClient is used so you can - * preconfigure it beforehand. It should not have been already started. You can just use "new TorClient()" if - * you don't have any particular configuration requirements.

- * - *

If running on the Oracle JDK the unlimited strength jurisdiction checks will also be overridden, - * as they no longer apply anyway and can cause startup failures due to the requirement for AES-256.

- * - *

The user does not need any additional software for this: it's all pure Java. As of April 2014 this mode - * is experimental.

- * - * @param doDiscovery if true, DNS or HTTP peer discovery will be performed via Tor: this is almost always what you want. - * @throws java.util.concurrent.TimeoutException if Tor fails to start within 20 seconds. - */ - public static PeerGroup newWithTor(Context context, @Nullable AbstractBlockChain chain, TorClient torClient, boolean doDiscovery) throws TimeoutException { - checkNotNull(torClient); - DRMWorkaround.maybeDisableExportControls(); - BlockingClientManager manager = new BlockingClientManager(torClient.getSocketFactory()); - final int CONNECT_TIMEOUT_MSEC = TOR_TIMEOUT_SECONDS * 1000; - manager.setConnectTimeoutMillis(CONNECT_TIMEOUT_MSEC); - PeerGroup result = new PeerGroup(context, chain, manager, torClient); - result.setConnectTimeoutMillis(CONNECT_TIMEOUT_MSEC); - - if (doDiscovery) { - NetworkParameters params = context.getParams(); - HttpDiscovery.Details[] httpSeeds = params.getHttpSeeds(); - if (httpSeeds.length > 0) { - // Use HTTP discovery when Tor is active and there is a Cartographer seed, for a much needed speed boost. - OkHttpClient httpClient = new OkHttpClient(); - httpClient.setSocketFactory(torClient.getSocketFactory()); - List discoveries = Lists.newArrayList(); - for (HttpDiscovery.Details httpSeed : httpSeeds) - discoveries.add(new HttpDiscovery(params, httpSeed, httpClient)); - result.addPeerDiscovery(new MultiplexingDiscovery(params, discoveries)); - } else { - result.addPeerDiscovery(new TorDiscovery(params, torClient)); - } - } - return result; - } - /** See {@link #PeerGroup(Context, AbstractBlockChain, ClientConnectionManager)} */ public PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain, ClientConnectionManager connectionManager) { - this(Context.getOrCreate(params), chain, connectionManager, null); + this(Context.getOrCreate(params), chain, connectionManager); } /** * Creates a new PeerGroup allowing you to specify the {@link ClientConnectionManager} which is used to create new * connections and keep track of existing ones. */ - public PeerGroup(Context context, @Nullable AbstractBlockChain chain, ClientConnectionManager connectionManager) { - this(context, chain, connectionManager, null); - } - - /** - * Creates a new PeerGroup allowing you to specify the {@link ClientConnectionManager} which is used to create new - * connections and keep track of existing ones. - */ - private PeerGroup(Context context, @Nullable AbstractBlockChain chain, ClientConnectionManager connectionManager, @Nullable TorClient torClient) { + private PeerGroup(Context context, @Nullable AbstractBlockChain chain, ClientConnectionManager connectionManager) { checkNotNull(context); this.params = context.getParams(); this.chain = chain; fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds(); wallets = new CopyOnWriteArrayList(); peerFilterProviders = new CopyOnWriteArrayList(); - this.torClient = torClient; executor = createPrivateExecutor(); @@ -1137,16 +1059,6 @@ public class PeerGroup implements TransactionBroadcaster { public void run() { try { log.info("Starting ..."); - if (torClient != null) { - log.info("Starting Tor/Orchid ..."); - torClient.start(); - try { - torClient.waitUntilReady(TOR_TIMEOUT_SECONDS * 1000); - } catch (Exception e) { - throw new RuntimeException(e); - } - log.info("Tor ready"); - } channels.startAsync(); channels.awaitRunning(); triggerConnections(); @@ -1183,9 +1095,6 @@ public class PeerGroup implements TransactionBroadcaster { for (PeerDiscovery peerDiscovery : peerDiscoverers) { peerDiscovery.shutdown(); } - if (torClient != null) { - torClient.stop(); - } vRunning = false; log.info("Stopped."); } catch (Throwable e) { @@ -2327,14 +2236,6 @@ public class PeerGroup implements TransactionBroadcaster { } } - /** - * Returns the {@link com.subgraph.orchid.TorClient} object for this peer group, if Tor is in use, null otherwise. - */ - @Nullable - public TorClient getTorClient() { - return torClient; - } - /** * Returns the maximum number of {@link Peer}s to discover. This maximum is checked after * each {@link PeerDiscovery} so this max number can be surpassed. diff --git a/core/src/main/java/org/bitcoinj/kits/WalletAppKit.java b/core/src/main/java/org/bitcoinj/kits/WalletAppKit.java index 628e0220..90bafe8e 100644 --- a/core/src/main/java/org/bitcoinj/kits/WalletAppKit.java +++ b/core/src/main/java/org/bitcoinj/kits/WalletAppKit.java @@ -19,7 +19,6 @@ package org.bitcoinj.kits; import com.google.common.collect.*; import com.google.common.util.concurrent.*; -import com.subgraph.orchid.*; import org.bitcoinj.core.listeners.*; import org.bitcoinj.core.*; import org.bitcoinj.net.discovery.*; @@ -78,7 +77,6 @@ public class WalletAppKit extends AbstractIdleService { protected boolean autoStop = true; protected InputStream checkpoints; protected boolean blockingStartup = true; - protected boolean useTor = false; // Perhaps in future we can change this to true. protected String userAgent, version; protected WalletProtobufSerializer.WalletFactory walletFactory; @Nullable protected DeterministicSeed restoreFromSeed; @@ -178,15 +176,6 @@ public class WalletAppKit extends AbstractIdleService { return this; } - /** - * If called, then an embedded Tor client library will be used to connect to the P2P network. The user does not need - * any additional software for this: it's all pure Java. As of April 2014 this mode is experimental. - */ - public WalletAppKit useTor() { - this.useTor = true; - return this; - } - /** * If a seed is set here then any existing wallet that matches the file name will be renamed to a backup name, * the chain file will be deleted, and the wallet object will be instantiated with the given seed instead of @@ -318,7 +307,7 @@ public class WalletAppKit extends AbstractIdleService { for (PeerAddress addr : peerAddresses) vPeerGroup.addAddress(addr); vPeerGroup.setMaxConnections(peerAddresses.length); peerAddresses = null; - } else if (!params.getId().equals(NetworkParameters.ID_REGTEST) && !useTor) { + } else if (!params.getId().equals(NetworkParameters.ID_REGTEST)) { vPeerGroup.addPeerDiscovery(discovery != null ? discovery : new DnsDiscovery(params)); } vChain.addWallet(vWallet); @@ -457,13 +446,7 @@ public class WalletAppKit extends AbstractIdleService { protected PeerGroup createPeerGroup() throws TimeoutException { - if (useTor) { - TorClient torClient = new TorClient(); - torClient.getConfig().setDataDirectory(directory); - return PeerGroup.newWithTor(params, vChain, torClient); - } - else - return new PeerGroup(params, vChain); + return new PeerGroup(params, vChain); } private void installShutdownHook() { diff --git a/core/src/main/java/org/bitcoinj/net/discovery/TorDiscovery.java b/core/src/main/java/org/bitcoinj/net/discovery/TorDiscovery.java deleted file mode 100644 index e17100a6..00000000 --- a/core/src/main/java/org/bitcoinj/net/discovery/TorDiscovery.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2014 Miron Cuperman - * Copyright 2015 Andreas Schildbach - * - * 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. - */ - -package org.bitcoinj.net.discovery; - -import com.google.common.collect.*; -import com.google.common.util.concurrent.*; -import com.subgraph.orchid.*; -import com.subgraph.orchid.circuits.path.*; -import com.subgraph.orchid.data.*; -import com.subgraph.orchid.data.exitpolicy.*; -import org.bitcoinj.core.*; -import org.bitcoinj.utils.*; -import org.slf4j.*; - -import java.net.*; -import java.util.*; -import java.util.concurrent.*; - -import static com.google.common.base.Preconditions.*; -import static java.util.Collections.*; - -/** - *

Supports peer discovery through Tor.

- * - *

Failure to obtain at least four different peers through different exit nodes will cause - * a PeerDiscoveryException will be thrown during getPeers(). - *

- * - *

DNS seeds do not attempt to enumerate every peer on the network. If you want more peers - * to connect to, you need to discover them via other means (like addr broadcasts).

- */ -public class TorDiscovery implements PeerDiscovery { - private static final Logger log = LoggerFactory.getLogger(TorDiscovery.class); - public static final int MINIMUM_ROUTER_COUNT = 4; - public static final int ROUTER_LOOKUP_COUNT = 10; - public static final int MINIMUM_ROUTER_LOOKUP_COUNT = 6; - public static final int RECEIVE_RETRIES = 3; - public static final int RESOLVE_STREAM_ID = 0x1000; // An arbitrary stream ID - public static final int RESOLVE_CNAME = 0x00; - public static final int RESOLVE_ERROR = 0xf0; - public static final int RESOLVE_IPV4 = 0x04; - public static final int RESOLVE_IPV6 = 0x06; - - private final String[] hostNames; - private final NetworkParameters netParams; - private final CircuitPathChooser pathChooser; - private final TorClient torClient; - private ListeningExecutorService threadPool; - - /** - * Supports finding peers through Tor. Community run DNS entry points will be used. - * - * @param netParams Network parameters to be used for port information. - */ - public TorDiscovery(NetworkParameters netParams, TorClient torClient) { - this(netParams.getDnsSeeds(), netParams, torClient); - } - - /** - * Supports finding peers through Tor. - * - * @param hostNames Host names to be examined for seed addresses. - * @param netParams Network parameters to be used for port information. - * @param torClient an already-started Tor client. - */ - public TorDiscovery(String[] hostNames, NetworkParameters netParams, TorClient torClient) { - this.hostNames = hostNames; - this.netParams = netParams; - - this.torClient = torClient; - this.pathChooser = CircuitPathChooser.create(torClient.getConfig(), torClient.getDirectory()); - } - - private static class Lookup { - final Router router; - final InetAddress address; - - Lookup(Router router, InetAddress address) { - this.router = router; - this.address = address; - } - } - - @Override - public InetSocketAddress[] getPeers(long services, long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException { - if (hostNames == null) - throw new PeerDiscoveryException("Unable to find any peers via DNS"); - if (services != 0) - throw new PeerDiscoveryException("DNS seeds cannot filter by services: " + services); - - Set routers = Sets.newHashSet(); - ArrayList dummyTargets = Lists.newArrayList(); - - // Collect exit nodes until we have enough - while (routers.size() < ROUTER_LOOKUP_COUNT) { - Router router = pathChooser.chooseExitNodeForTargets(dummyTargets); - routers.add(router); - } - - try { - List circuits = - getCircuits(torClient.getConfig().getCircuitBuildTimeout(), TimeUnit.MILLISECONDS, routers); - if (circuits.isEmpty()) - throw new PeerDiscoveryException("Failed to open any circuit within " + - String.valueOf(timeoutValue) + " " + timeoutUnit); - - Collection addresses = lookupAddresses(timeoutValue, timeoutUnit, circuits); - - if (addresses.size() < MINIMUM_ROUTER_COUNT) - throw new PeerDiscoveryException("Unable to find enough peers via Tor - got " + addresses.size()); - ArrayList addressList = Lists.newArrayList(); - addressList.addAll(addresses); - Collections.shuffle(addressList); - return addressList.toArray(new InetSocketAddress[addressList.size()]); - } catch (InterruptedException e) { - throw new PeerDiscoveryException(e); - } - } - - private List getCircuits(long timeoutValue, TimeUnit timeoutUnit, Set routers) throws InterruptedException { - checkArgument(routers.size() >= MINIMUM_ROUTER_LOOKUP_COUNT, "Set of {} routers is smaller than required minimum {}", - routers.size(), MINIMUM_ROUTER_LOOKUP_COUNT); - createThreadPool(routers.size()); - - try { - List> circuitFutures = Lists.newArrayList(); - final CountDownLatch doneSignal = new CountDownLatch(MINIMUM_ROUTER_LOOKUP_COUNT); - for (final Router router : routers) { - ListenableFuture openCircuit = threadPool.submit(new Callable() { - @Override - public Circuit call() throws Exception { - return torClient.getCircuitManager().openInternalCircuitTo(Lists.newArrayList(router)); - } - }); - Futures.addCallback(openCircuit, new FutureCallback() { - @Override - public void onSuccess(Circuit circuit) { - doneSignal.countDown(); - } - @Override - public void onFailure(Throwable thrown) { - doneSignal.countDown(); - } - }); - circuitFutures.add(openCircuit); - } - - boolean countedDown = doneSignal.await(timeoutValue, timeoutUnit); - - try { - List circuits = new ArrayList(Futures.successfulAsList(circuitFutures).get()); - // Any failures will result in null entries. Remove them. - circuits.removeAll(singleton(null)); - int failures = routers.size() - circuits.size(); - if (failures > 0) log.warn("{} failures " + (countedDown ? "" : "(including timeout) ") + - "opening DNS lookup circuits", failures); - return circuits; - } catch (ExecutionException e) { - // Cannot happen, successfulAsList accepts failures - throw new RuntimeException(e); - } - } finally { - shutdownThreadPool(); - } - } - - private Collection lookupAddresses(long timeoutValue, TimeUnit timeoutUnit, List circuits) throws InterruptedException { - createThreadPool(circuits.size() * hostNames.length); - - try { - List> lookupFutures = Lists.newArrayList(); - for (final Circuit circuit : circuits) { - for (final String seed : hostNames) { - lookupFutures.add(threadPool.submit(new Callable() { - @Override - public Lookup call() throws Exception { - return new Lookup(circuit.getFinalCircuitNode().getRouter(), lookup(circuit, seed)); - } - })); - } - } - - threadPool.awaitTermination(timeoutValue, timeoutUnit); - int timeouts = 0; - for (ListenableFuture future : lookupFutures) { - if (!future.isDone()) { - timeouts++; - future.cancel(true); - } - } - if (timeouts > 0) - log.warn("{} DNS lookups timed out", timeouts); - - try { - List lookups = new ArrayList(Futures.successfulAsList(lookupFutures).get()); - // Any failures will result in null entries. Remove them. - lookups.removeAll(singleton(null)); - - // Use a map to enforce one result per exit node - // TODO: randomize result selection better - Map lookupMap = Maps.newHashMap(); - - for (Lookup lookup : lookups) { - InetSocketAddress address = new InetSocketAddress(lookup.address, netParams.getPort()); - lookupMap.put(lookup.router.getIdentityHash(), address); - } - - return lookupMap.values(); - } catch (ExecutionException e) { - // Cannot happen, successfulAsList accepts failures - throw new RuntimeException(e); - } - } finally { - shutdownThreadPool(); - } - } - - private synchronized void shutdownThreadPool() { - threadPool.shutdownNow(); - threadPool = null; - } - - private synchronized void createThreadPool(int size) { - threadPool = MoreExecutors.listeningDecorator( - Executors.newFixedThreadPool(size, new ContextPropagatingThreadFactory("Tor DNS discovery"))); - } - - private InetAddress lookup(Circuit circuit, String seed) throws UnknownHostException { - // Send a resolve cell to the exit node - RelayCell cell = circuit.createRelayCell(RelayCell.RELAY_RESOLVE, RESOLVE_STREAM_ID, circuit.getFinalCircuitNode()); - cell.putString(seed); - circuit.sendRelayCell(cell); - - // Wait a few cell timeout periods (3 * 20 sec) for replies, in case the path is slow - for (int i = 0 ; i < RECEIVE_RETRIES; i++) { - RelayCell res = circuit.receiveRelayCell(); - if (res != null) { - while (res.cellBytesRemaining() > 0) { - int type = res.getByte(); - int len = res.getByte(); - byte[] value = new byte[len]; - res.getByteArray(value); - int ttl = res.getInt(); - - if (type == RESOLVE_CNAME || type >= RESOLVE_ERROR) { - // TODO handle .onion CNAME replies - throw new RuntimeException(new String(value)); - } else if (type == RESOLVE_IPV4 || type == RESOLVE_IPV6) { - return InetAddress.getByAddress(value); - } - } - break; - } - } - throw new RuntimeException("Could not look up " + seed); - } - - @Override - public synchronized void shutdown() { - if (threadPool != null) { - shutdownThreadPool(); - } - } -} diff --git a/orchid/.gitignore b/orchid/.gitignore deleted file mode 100644 index f58915aa..00000000 --- a/orchid/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -bin/ -orchid-*.jar -orchid-*.zip -build-revision -lib/xmlrpc-* diff --git a/orchid/LICENSE b/orchid/LICENSE deleted file mode 100644 index 2738761a..00000000 --- a/orchid/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2009-2011, Bruce Leidl -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the author nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/orchid/README b/orchid/README deleted file mode 100644 index e69de29b..00000000 diff --git a/orchid/build.xml b/orchid/build.xml deleted file mode 100644 index 8aa956c1..00000000 --- a/orchid/build.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/orchid/data/GeoIP.dat b/orchid/data/GeoIP.dat deleted file mode 100644 index fe563297c33efb0401c24f3abb9019709adbca3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 583720 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 z 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 diff --git a/orchid/data/README b/orchid/data/README deleted file mode 100644 index 3eccbba5..00000000 --- a/orchid/data/README +++ /dev/null @@ -1,3 +0,0 @@ -GeoIP.dat GeoLite Country database downloaded September, 2013 - - http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz diff --git a/orchid/doc/spec/address-spec.txt b/orchid/doc/spec/address-spec.txt deleted file mode 100644 index 2e1aff2b..00000000 --- a/orchid/doc/spec/address-spec.txt +++ /dev/null @@ -1,58 +0,0 @@ - - Special Hostnames in Tor - Nick Mathewson - -1. Overview - - Most of the time, Tor treats user-specified hostnames as opaque: When - the user connects to www.torproject.org, Tor picks an exit node and uses - that node to connect to "www.torproject.org". Some hostnames, however, - can be used to override Tor's default behavior and circuit-building - rules. - - These hostnames can be passed to Tor as the address part of a SOCKS4a or - SOCKS5 request. If the application is connected to Tor using an IP-only - method (such as SOCKS4, TransPort, or NatdPort), these hostnames can be - substituted for certain IP addresses using the MapAddress configuration - option or the MAPADDRESS control command. - -2. .exit - - SYNTAX: [hostname].[name-or-digest].exit - [name-or-digest].exit - - Hostname is a valid hostname; [name-or-digest] is either the nickname of a - Tor node or the hex-encoded digest of that node's public key. - - When Tor sees an address in this format, it uses the specified hostname as - the exit node. If no "hostname" component is given, Tor defaults to the - published IPv4 address of the exit node. - - It is valid to try to resolve hostnames, and in fact upon success Tor - will cache an internal mapaddress of the form - "www.google.com.foo.exit=64.233.161.99.foo.exit" to speed subsequent - lookups. - - The .exit notation is disabled by default as of Tor 0.2.2.1-alpha, due - to potential application-level attacks. - - EXAMPLES: - www.example.com.exampletornode.exit - - Connect to www.example.com from the node called "exampletornode". - - exampletornode.exit - - Connect to the published IP address of "exampletornode" using - "exampletornode" as the exit. - -3. .onion - - SYNTAX: [digest].onion - - The digest is the first eighty bits of a SHA1 hash of the identity key for - a hidden service, encoded in base32. - - When Tor sees an address in this format, it tries to look up and connect to - the specified hidden service. See rend-spec.txt for full details. - diff --git a/orchid/doc/spec/bridges-spec.txt b/orchid/doc/spec/bridges-spec.txt deleted file mode 100644 index 64711881..00000000 --- a/orchid/doc/spec/bridges-spec.txt +++ /dev/null @@ -1,249 +0,0 @@ - - Tor bridges specification - -0. Preface - - This document describes the design decisions around support for bridge - users, bridge relays, and bridge authorities. It acts as an overview - of the bridge design and deployment for developers, and it also tries - to point out limitations in the current design and implementation. - - For more details on what all of these mean, look at blocking.tex in - /doc/design-paper/ - -1. Bridge relays - - Bridge relays are just like normal Tor relays except they don't publish - their server descriptors to the main directory authorities. - -1.1. PublishServerDescriptor - - To configure your relay to be a bridge relay, just add - BridgeRelay 1 - PublishServerDescriptor bridge - to your torrc. This will cause your relay to publish its descriptor - to the bridge authorities rather than to the default authorities. - - Alternatively, you can say - BridgeRelay 1 - PublishServerDescriptor 0 - which will cause your relay to not publish anywhere. This could be - useful for private bridges. - -1.2. Recommendations. - - Bridge relays should use an exit policy of "reject *:*". This is - because they only need to relay traffic between the bridge users - and the rest of the Tor network, so there's no need to let people - exit directly from them. - - We invented the RelayBandwidth* options for this situation: Tor clients - who want to allow relaying too. See proposal 111 for details. Relay - operators should feel free to rate-limit their relayed traffic. - -1.3. Implementation note. - - Vidalia 0.0.15 has turned its "Relay" settings page into a tri-state - "Don't relay" / "Relay for the Tor network" / "Help censored users". - - If you click the third choice, it forces your exit policy to reject *:*. - - If all the bridges end up on port 9001, that's not so good. On the - other hand, putting the bridges on a low-numbered port in the Unix - world requires jumping through extra hoops. The current compromise is - that Vidalia makes the ORPort default to 443 on Windows, and 9001 on - other platforms. - - At the bottom of the relay config settings window, Vidalia displays - the bridge identifier to the operator (see Section 3.1) so he can pass - it on to bridge users. - -2. Bridge authorities. - - Bridge authorities are like normal v3 directory authorities, except - they don't create their own network-status documents or votes. So if - you ask a bridge authority for a network-status document or consensus, - they behave like a directory mirror: they give you one from one of - the main authorities. But if you ask the bridge authority for the - descriptor corresponding to a particular identity fingerprint, it will - happily give you the latest descriptor for that fingerprint. - - To become a bridge authority, add these lines to your torrc: - AuthoritativeDirectory 1 - BridgeAuthoritativeDir 1 - - Right now there's one bridge authority, running on the Tonga relay. - -2.1. Exporting bridge-purpose descriptors - - We've added a new purpose for server descriptors: the "bridge" - purpose. With the new router-descriptors file format that includes - annotations, it's easy to look through it and find the bridge-purpose - descriptors. - - Currently we export the bridge descriptors from Tonga to the - BridgeDB server, so it can give them out according to the policies - in blocking.pdf. - -2.2. Reachability/uptime testing - - Right now the bridge authorities do active reachability testing of - bridges, so we know which ones to recommend for users. - - But in the design document, we suggested that bridges should publish - anonymously (i.e. via Tor) to the bridge authority, so somebody watching - the bridge authority can't just enumerate all the bridges. But if we're - doing active measurement, the game is up. Perhaps we should back off on - this goal, or perhaps we should do our active measurement anonymously? - - Answering this issue is scheduled for 0.2.1.x. - -2.3. Future work: migrating to multiple bridge authorities - - Having only one bridge authority is both a trust bottleneck (if you - break into one place you learn about every single bridge we've got) - and a robustness bottleneck (when it's down, bridge users become sad). - - Right now if we put up a second bridge authority, all the bridges would - publish to it, and (assuming the code works) bridge users would query - a random bridge authority. This resolves the robustness bottleneck, - but makes the trust bottleneck even worse. - - In 0.2.2.x and later we should think about better ways to have multiple - bridge authorities. - -3. Bridge users. - - Bridge users are like ordinary Tor users except they use encrypted - directory connections by default, and they use bridge relays as both - entry guards (their first hop) and directory guards (the source of - all their directory information). - - To become a bridge user, add the following line to your torrc: - UseBridges 1 - - and then add at least one "Bridge" line to your torrc based on the - format below. - -3.1. Format of the bridge identifier. - - The canonical format for a bridge identifier contains an IP address, - an ORPort, and an identity fingerprint: - bridge 128.31.0.34:9009 4C17 FB53 2E20 B2A8 AC19 9441 ECD2 B017 7B39 E4B1 - - However, the identity fingerprint can be left out, in which case the - bridge user will connect to that relay and use it as a bridge regardless - of what identity key it presents: - bridge 128.31.0.34:9009 - This might be useful for cases where only short bridge identifiers - can be communicated to bridge users. - - In a future version we may also support bridge identifiers that are - only a key fingerprint: - bridge 4C17 FB53 2E20 B2A8 AC19 9441 ECD2 B017 7B39 E4B1 - and the bridge user can fetch the latest descriptor from the bridge - authority (see Section 3.4). - -3.2. Bridges as entry guards - - For now, bridge users add their bridge relays to their list of "entry - guards" (see path-spec.txt for background on entry guards). They are - managed by the entry guard algorithms exactly as if they were a normal - entry guard -- their keys and timing get cached in the "state" file, - etc. This means that when the Tor user starts up with "UseBridges" - disabled, he will skip past the bridge entries since they won't be - listed as up and usable in his networkstatus consensus. But to be clear, - the "entry_guards" list doesn't currently distinguish guards by purpose. - - Internally, each bridge user keeps a smartlist of "bridge_info_t" - that reflects the "bridge" lines from his torrc along with a download - schedule (see Section 3.5 below). When he starts Tor, he attempts - to fetch a descriptor for each configured bridge (see Section 3.4 - below). When he succeeds at getting a descriptor for one of the bridges - in his list, he adds it directly to the entry guard list using the - normal add_an_entry_guard() interface. Once a bridge descriptor has - been added, should_delay_dir_fetches() will stop delaying further - directory fetches, and the user begins to bootstrap his directory - information from that bridge (see Section 3.3). - - Currently bridge users cache their bridge descriptors to the - "cached-descriptors" file (annotated with purpose "bridge"), but - they don't make any attempt to reuse descriptors they find in this - file. The theory is that either the bridge is available now, in which - case you can get a fresh descriptor, or it's not, in which case an - old descriptor won't do you much good. - - We could disable writing out the bridge lines to the state file, if - we think this is a problem. - - As an exception, if we get an application request when we have one - or more bridge descriptors but we believe none of them are running, - we mark them all as running again. This is similar to the exception - already in place to help long-idle Tor clients realize they should - fetch fresh directory information rather than just refuse requests. - -3.3. Bridges as directory guards - - In addition to using bridges as the first hop in their circuits, bridge - users also use them to fetch directory updates. Other than initial - bootstrapping to find a working bridge descriptor (see Section 3.4 - below), all further non-anonymized directory fetches will be redirected - to the bridge. - - This means that bridge relays need to have cached answers for all - questions the bridge user might ask. This makes the upgrade path - tricky --- for example, if we migrate to a v4 directory design, the - bridge user would need to keep using v3 so long as his bridge relays - only knew how to answer v3 queries. - - In a future design, for cases where the user has enough information - to build circuits yet the chosen bridge doesn't know how to answer a - given query, we might teach bridge users to make an anonymized request - to a more suitable directory server. - -3.4. How bridge users get their bridge descriptor - - Bridge users can fetch bridge descriptors in two ways: by going directly - to the bridge and asking for "/tor/server/authority", or by going to - the bridge authority and asking for "/tor/server/fp/ID". By default, - they will only try the direct queries. If the user sets - UpdateBridgesFromAuthority 1 - in his config file, then he will try querying the bridge authority - first for bridges where he knows a digest (if he only knows an IP - address and ORPort, then his only option is a direct query). - - If the user has at least one working bridge, then he will do further - queries to the bridge authority through a full three-hop Tor circuit. - But when bootstrapping, he will make a direct begin_dir-style connection - to the bridge authority. - - As of Tor 0.2.0.10-alpha, if the user attempts to fetch a descriptor - from the bridge authority and it returns a 404 not found, the user - will automatically fall back to trying a direct query. Therefore it is - recommended that bridge users always set UpdateBridgesFromAuthority, - since at worst it will delay their fetches a little bit and notify - the bridge authority of the identity fingerprint (but not location) - of their intended bridges. - -3.5. Bridge descriptor retry schedule - - Bridge users try to fetch a descriptor for each bridge (using the - steps in Section 3.4 above) on startup. Whenever they receive a - bridge descriptor, they reschedule a new descriptor download for 1 - hour from then. - - If on the other hand it fails, they try again after 15 minutes for the - first attempt, after 15 minutes for the second attempt, and after 60 - minutes for subsequent attempts. - - In 0.2.2.x we should come up with some smarter retry schedules. - -3.6. Implementation note. - - Vidalia 0.1.0 has a new checkbox in its Network config window called - "My ISP blocks connections to the Tor network." Users who click that - box change their configuration to: - UseBridges 1 - UpdateBridgesFromAuthority 1 - and should add at least one bridge identifier. - diff --git a/orchid/doc/spec/control-spec.txt b/orchid/doc/spec/control-spec.txt deleted file mode 100644 index 1a463afc..00000000 --- a/orchid/doc/spec/control-spec.txt +++ /dev/null @@ -1,1853 +0,0 @@ - - TC: A Tor control protocol (Version 1) - -0. Scope - - This document describes an implementation-specific protocol that is used - for other programs (such as frontend user-interfaces) to communicate with a - locally running Tor process. It is not part of the Tor onion routing - protocol. - - This protocol replaces version 0 of TC, which is now deprecated. For - reference, TC is described in "control-spec-v0.txt". Implementors are - recommended to avoid using TC directly, but instead to use a library that - can easily be updated to use the newer protocol. (Version 0 is used by Tor - versions 0.1.0.x; the protocol in this document only works with Tor - versions in the 0.1.1.x series and later.) - -1. Protocol outline - - TC is a bidirectional message-based protocol. It assumes an underlying - stream for communication between a controlling process (the "client" - or "controller") and a Tor process (or "server"). The stream may be - implemented via TCP, TLS-over-TCP, a Unix-domain socket, or so on, - but it must provide reliable in-order delivery. For security, the - stream should not be accessible by untrusted parties. - - In TC, the client and server send typed messages to each other over the - underlying stream. The client sends "commands" and the server sends - "replies". - - By default, all messages from the server are in response to messages from - the client. Some client requests, however, will cause the server to send - messages to the client indefinitely far into the future. Such - "asynchronous" replies are marked as such. - - Servers respond to messages in the order messages are received. - -2. Message format - -2.1. Description format - - The message formats listed below use ABNF as described in RFC 2234. - The protocol itself is loosely based on SMTP (see RFC 2821). - - We use the following nonterminals from RFC 2822: atom, qcontent - - We define the following general-use nonterminals: - - String = DQUOTE *qcontent DQUOTE - - There are explicitly no limits on line length. All 8-bit characters are - permitted unless explicitly disallowed. - - Wherever CRLF is specified to be accepted from the controller, Tor MAY also - accept LF. Tor, however, MUST NOT generate LF instead of CRLF. - Controllers SHOULD always send CRLF. - -2.2. Commands from controller to Tor - - Command = Keyword Arguments CRLF / "+" Keyword Arguments CRLF Data - Keyword = 1*ALPHA - Arguments = *(SP / VCHAR) - - Specific commands and their arguments are described below in section 3. - -2.3. Replies from Tor to the controller - - Reply = SyncReply / AsyncReply - SyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine - AsyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine - - MidReplyLine = StatusCode "-" ReplyLine - DataReplyLine = StatusCode "+" ReplyLine Data - EndReplyLine = StatusCode SP ReplyLine - ReplyLine = [ReplyText] CRLF - ReplyText = XXXX - StatusCode = 3DIGIT - - Specific replies are mentioned below in section 3, and described more fully - in section 4. - - [Compatibility note: versions of Tor before 0.2.0.3-alpha sometimes - generate AsyncReplies of the form "*(MidReplyLine / DataReplyLine)". - This is incorrect, but controllers that need to work with these - versions of Tor should be prepared to get multi-line AsyncReplies with - the final line (usually "650 OK") omitted.] - -2.4. General-use tokens - - ; Identifiers for servers. - ServerID = Nickname / Fingerprint - - Nickname = 1*19 NicknameChar - NicknameChar = "a"-"z" / "A"-"Z" / "0" - "9" - Fingerprint = "$" 40*HEXDIG - - ; A "=" indicates that the given nickname is canonical; a "~" indicates - ; that the given nickname is not canonical. If no nickname is given at - ; all, Tor does not even have a guess for what this router calls itself. - LongName = Fingerprint [ ( "=" / "~" ) Nickname ] - - ; How a controller tells Tor about a particular OR. There are four - ; possible formats: - ; $Digest -- The router whose identity key hashes to the given digest. - ; This is the preferred way to refer to an OR. - ; $Digest~Name -- The router whose identity key hashes to the given - ; digest, but only if the router has the given nickname. - ; $Digest=Name -- The router whose identity key hashes to the given - ; digest, but only if the router is Named and has the given - ; nickname. - ; Name -- The Named router with the given nickname, or, if no such - ; router exists, any router whose nickname matches the one given. - ; This is not a safe way to refer to routers, since Named status - ; could under some circumstances change over time. - ServerSpec = LongName / Nickname - - ; Unique identifiers for streams or circuits. Currently, Tor only - ; uses digits, but this may change - StreamID = 1*16 IDChar - CircuitID = 1*16 IDChar - IDChar = ALPHA / DIGIT - - Address = ip4-address / ip6-address / hostname (XXXX Define these) - - ; A "Data" section is a sequence of octets concluded by the terminating - ; sequence CRLF "." CRLF. The terminating sequence may not appear in the - ; body of the data. Leading periods on lines in the data are escaped with - ; an additional leading period as in RFC 2821 section 4.5.2. - Data = *DataLine "." CRLF - DataLine = CRLF / "." 1*LineItem CRLF / NonDotItem *LineItem CRLF - LineItem = NonCR / 1*CR NonCRLF - NonDotItem = NonDotCR / 1*CR NonCRLF - -3. Commands - - All commands are case-insensitive, but most keywords are case-sensitive. - -3.1. SETCONF - - Change the value of one or more configuration variables. The syntax is: - - "SETCONF" 1*(SP keyword ["=" value]) CRLF - value = String / QuotedString - - Tor behaves as though it had just read each of the key-value pairs - from its configuration file. Keywords with no corresponding values have - their configuration values reset to 0 or NULL (use RESETCONF if you want - to set it back to its default). SETCONF is all-or-nothing: if there - is an error in any of the configuration settings, Tor sets none of them. - - Tor responds with a "250 configuration values set" reply on success. - If some of the listed keywords can't be found, Tor replies with a - "552 Unrecognized option" message. Otherwise, Tor responds with a - "513 syntax error in configuration values" reply on syntax error, or a - "553 impossible configuration setting" reply on a semantic error. - - When a configuration option takes multiple values, or when multiple - configuration keys form a context-sensitive group (see GETCONF below), then - setting _any_ of the options in a SETCONF command is taken to reset all of - the others. For example, if two ORBindAddress values are configured, and a - SETCONF command arrives containing a single ORBindAddress value, the new - command's value replaces the two old values. - - Sometimes it is not possible to change configuration options solely by - issuing a series of SETCONF commands, because the value of one of the - configuration options depends on the value of another which has not yet - been set. Such situations can be overcome by setting multiple configuration - options with a single SETCONF command (e.g. SETCONF ORPort=443 - ORListenAddress=9001). - -3.2. RESETCONF - - Remove all settings for a given configuration option entirely, assign - its default value (if any), and then assign the String provided. - Typically the String is left empty, to simply set an option back to - its default. The syntax is: - - "RESETCONF" 1*(SP keyword ["=" String]) CRLF - - Otherwise it behaves like SETCONF above. - -3.3. GETCONF - - Request the value of a configuration variable. The syntax is: - - "GETCONF" 1*(SP keyword) CRLF - - If all of the listed keywords exist in the Tor configuration, Tor replies - with a series of reply lines of the form: - 250 keyword=value - If any option is set to a 'default' value semantically different from an - empty string, Tor may reply with a reply line of the form: - 250 keyword - - Value may be a raw value or a quoted string. Tor will try to use - unquoted values except when the value could be misinterpreted through - not being quoted. - - If some of the listed keywords can't be found, Tor replies with a - "552 unknown configuration keyword" message. - - If an option appears multiple times in the configuration, all of its - key-value pairs are returned in order. - - Some options are context-sensitive, and depend on other options with - different keywords. These cannot be fetched directly. Currently there - is only one such option: clients should use the "HiddenServiceOptions" - virtual keyword to get all HiddenServiceDir, HiddenServicePort, - HiddenServiceNodes, and HiddenServiceExcludeNodes option settings. - -3.4. SETEVENTS - - Request the server to inform the client about interesting events. The - syntax is: - - "SETEVENTS" [SP "EXTENDED"] *(SP EventCode) CRLF - - EventCode = "CIRC" / "STREAM" / "ORCONN" / "BW" / "DEBUG" / - "INFO" / "NOTICE" / "WARN" / "ERR" / "NEWDESC" / "ADDRMAP" / - "AUTHDIR_NEWDESCS" / "DESCCHANGED" / "STATUS_GENERAL" / - "STATUS_CLIENT" / "STATUS_SERVER" / "GUARD" / "NS" / "STREAM_BW" / - "CLIENTS_SEEN" / "NEWCONSENSUS" - - Any events *not* listed in the SETEVENTS line are turned off; thus, sending - SETEVENTS with an empty body turns off all event reporting. - - The server responds with a "250 OK" reply on success, and a "552 - Unrecognized event" reply if one of the event codes isn't recognized. (On - error, the list of active event codes isn't changed.) - - If the flag string "EXTENDED" is provided, Tor may provide extra - information with events for this connection; see 4.1 for more information. - NOTE: All events on a given connection will be provided in extended format, - or none. - NOTE: "EXTENDED" is only supported in Tor 0.1.1.9-alpha or later. - - Each event is described in more detail in Section 4.1. - -3.5. AUTHENTICATE - - Sent from the client to the server. The syntax is: - "AUTHENTICATE" [ SP 1*HEXDIG / QuotedString ] CRLF - - The server responds with "250 OK" on success or "515 Bad authentication" if - the authentication cookie is incorrect. Tor closes the connection on an - authentication failure. - - The format of the 'cookie' is implementation-dependent; see 5.1 below for - information on how the standard Tor implementation handles it. - - Before the client has authenticated, no command other than PROTOCOLINFO, - AUTHENTICATE, or QUIT is valid. If the controller sends any other command, - or sends a malformed command, or sends an unsuccessful AUTHENTICATE - command, or sends PROTOCOLINFO more than once, Tor sends an error reply and - closes the connection. - - To prevent some cross-protocol attacks, the AUTHENTICATE command is still - required even if all authentication methods in Tor are disabled. In this - case, the controller should just send "AUTHENTICATE" CRLF. - - (Versions of Tor before 0.1.2.16 and 0.2.0.4-alpha did not close the - connection after an authentication failure.) - -3.6. SAVECONF - - Sent from the client to the server. The syntax is: - "SAVECONF" CRLF - - Instructs the server to write out its config options into its torrc. Server - returns "250 OK" if successful, or "551 Unable to write configuration - to disk" if it can't write the file or some other error occurs. - -3.7. SIGNAL - - Sent from the client to the server. The syntax is: - - "SIGNAL" SP Signal CRLF - - Signal = "RELOAD" / "SHUTDOWN" / "DUMP" / "DEBUG" / "HALT" / - "HUP" / "INT" / "USR1" / "USR2" / "TERM" / "NEWNYM" / - "CLEARDNSCACHE" - - The meaning of the signals are: - - RELOAD -- Reload: reload config items, refetch directory. (like HUP) - SHUTDOWN -- Controlled shutdown: if server is an OP, exit immediately. - If it's an OR, close listeners and exit after 30 seconds. - (like INT) - DUMP -- Dump stats: log information about open connections and - circuits. (like USR1) - DEBUG -- Debug: switch all open logs to loglevel debug. (like USR2) - HALT -- Immediate shutdown: clean up and exit now. (like TERM) - CLEARDNSCACHE -- Forget the client-side cached IPs for all hostnames. - NEWNYM -- Switch to clean circuits, so new application requests - don't share any circuits with old ones. Also clears - the client-side DNS cache. (Tor MAY rate-limit its - response to this signal.) - - The server responds with "250 OK" if the signal is recognized (or simply - closes the socket if it was asked to close immediately), or "552 - Unrecognized signal" if the signal is unrecognized. - -3.8. MAPADDRESS - - Sent from the client to the server. The syntax is: - - "MAPADDRESS" 1*(Address "=" Address SP) CRLF - - The first address in each pair is an "original" address; the second is a - "replacement" address. The client sends this message to the server in - order to tell it that future SOCKS requests for connections to the original - address should be replaced with connections to the specified replacement - address. If the addresses are well-formed, and the server is able to - fulfill the request, the server replies with a 250 message: - 250-OldAddress1=NewAddress1 - 250 OldAddress2=NewAddress2 - - containing the source and destination addresses. If request is - malformed, the server replies with "512 syntax error in command - argument". If the server can't fulfill the request, it replies with - "451 resource exhausted". - - The client may decline to provide a body for the original address, and - instead send a special null address ("0.0.0.0" for IPv4, "::0" for IPv6, or - "." for hostname), signifying that the server should choose the original - address itself, and return that address in the reply. The server - should ensure that it returns an element of address space that is unlikely - to be in actual use. If there is already an address mapped to the - destination address, the server may reuse that mapping. - - If the original address is already mapped to a different address, the old - mapping is removed. If the original address and the destination address - are the same, the server removes any mapping in place for the original - address. - - Example: - C: MAPADDRESS 0.0.0.0=torproject.org 1.2.3.4=tor.freehaven.net - S: 250-127.192.10.10=torproject.org - S: 250 1.2.3.4=tor.freehaven.net - - {Note: This feature is designed to be used to help Tor-ify applications - that need to use SOCKS4 or hostname-less SOCKS5. There are three - approaches to doing this: - 1. Somehow make them use SOCKS4a or SOCKS5-with-hostnames instead. - 2. Use tor-resolve (or another interface to Tor's resolve-over-SOCKS - feature) to resolve the hostname remotely. This doesn't work - with special addresses like x.onion or x.y.exit. - 3. Use MAPADDRESS to map an IP address to the desired hostname, and then - arrange to fool the application into thinking that the hostname - has resolved to that IP. - This functionality is designed to help implement the 3rd approach.} - - Mappings set by the controller last until the Tor process exits: - they never expire. If the controller wants the mapping to last only - a certain time, then it must explicitly un-map the address when that - time has elapsed. - -3.9. GETINFO - - Sent from the client to the server. The syntax is as for GETCONF: - "GETINFO" 1*(SP keyword) CRLF - one or more NL-terminated strings. The server replies with an INFOVALUE - message, or a 551 or 552 error. - - Unlike GETCONF, this message is used for data that are not stored in the Tor - configuration file, and that may be longer than a single line. On success, - one ReplyLine is sent for each requested value, followed by a final 250 OK - ReplyLine. If a value fits on a single line, the format is: - 250-keyword=value - If a value must be split over multiple lines, the format is: - 250+keyword= - value - . - Recognized keys and their values include: - - "version" -- The version of the server's software, including the name - of the software. (example: "Tor 0.0.9.4") - - "config-file" -- The location of Tor's configuration file ("torrc"). - - ["exit-policy/prepend" -- The default exit policy lines that Tor will - *prepend* to the ExitPolicy config option. - -- Never implemented. Useful?] - - "exit-policy/default" -- The default exit policy lines that Tor will - *append* to the ExitPolicy config option. - - "desc/id/" or "desc/name/" -- the latest - server descriptor for a given OR, NUL-terminated. - - "desc-annotations/id/" -- outputs the annotations string - (source, timestamp of arrival, purpose, etc) for the corresponding - descriptor. [First implemented in 0.2.0.13-alpha.] - - "extra-info/digest/" -- the extrainfo document whose digest (in - hex) is . Only available if we're downloading extra-info - documents. - - "ns/id/" or "ns/name/" -- the latest router - status info (v2 directory style) for a given OR. Router status - info is as given in - dir-spec.txt, and reflects the current beliefs of this Tor about the - router in question. Like directory clients, controllers MUST - tolerate unrecognized flags and lines. The published date and - descriptor digest are those believed to be best by this Tor, - not necessarily those for a descriptor that Tor currently has. - [First implemented in 0.1.2.3-alpha.] - - "ns/all" -- Router status info (v2 directory style) for all ORs we - have an opinion about, joined by newlines. [First implemented - in 0.1.2.3-alpha.] - - "ns/purpose/" -- Router status info (v2 directory style) - for all ORs of this purpose. Mostly designed for /ns/purpose/bridge - queries. [First implemented in 0.2.0.13-alpha.] - - "desc/all-recent" -- the latest server descriptor for every router that - Tor knows about. - - "network-status" -- a space-separated list (v1 directory style) - of all known OR identities. This is in the same format as the - router-status line in v1 directories; see dir-spec-v1.txt section - 3 for details. (If VERBOSE_NAMES is enabled, the output will - not conform to dir-spec-v1.txt; instead, the result will be a - space-separated list of LongName, each preceded by a "!" if it is - believed to be not running.) This option is deprecated; use - "ns/all" instead. - - "address-mappings/all" - "address-mappings/config" - "address-mappings/cache" - "address-mappings/control" -- a \r\n-separated list of address - mappings, each in the form of "from-address to-address expiry". - The 'config' key returns those address mappings set in the - configuration; the 'cache' key returns the mappings in the - client-side DNS cache; the 'control' key returns the mappings set - via the control interface; the 'all' target returns the mappings - set through any mechanism. - Expiry is formatted as with ADDRMAP events, except that "expiry" is - always a time in GMT or the string "NEVER"; see section 4.1.7. - First introduced in 0.2.0.3-alpha. - - "addr-mappings/*" -- as for address-mappings/*, but without the - expiry portion of the value. Use of this value is deprecated - since 0.2.0.3-alpha; use address-mappings instead. - - "address" -- the best guess at our external IP address. If we - have no guess, return a 551 error. (Added in 0.1.2.2-alpha) - - "fingerprint" -- the contents of the fingerprint file that Tor - writes as a server, or a 551 if we're not a server currently. - (Added in 0.1.2.3-alpha) - - "circuit-status" - A series of lines as for a circuit status event. Each line is of - the form: - CircuitID SP CircStatus [SP Path] CRLF - - "stream-status" - A series of lines as for a stream status event. Each is of the form: - StreamID SP StreamStatus SP CircID SP Target CRLF - - "orconn-status" - A series of lines as for an OR connection status event. Each is of the - form: - ServerID SP ORStatus CRLF - - "entry-guards" - A series of lines listing the currently chosen entry guards, if any. - Each is of the form: - ServerID2 SP Status [SP ISOTime] CRLF - - Status-with-time = ("unlisted") SP ISOTime - Status = ("up" / "never-connected" / "down" / - "unusable" / "unlisted" ) - - ServerID2 = Nickname / 40*HEXDIG - - [From 0.1.1.4-alpha to 0.1.1.10-alpha, this was called "helper-nodes". - Tor still supports calling it that for now, but support will be - removed in 0.1.3.x.] - - [Older versions of Tor (before 0.1.2.x-final) generated 'down' instead - of unlisted/unusable. Current Tors never generate 'down'.] - - [XXXX ServerID2 differs from ServerID in not prefixing fingerprints - with a $. This is an implementation error. It would be nice to add - the $ back in if we can do so without breaking compatibility.] - - "accounting/enabled" - "accounting/hibernating" - "accounting/bytes" - "accounting/bytes-left" - "accounting/interval-start" - "accounting/interval-wake" - "accounting/interval-end" - Information about accounting status. If accounting is enabled, - "enabled" is 1; otherwise it is 0. The "hibernating" field is "hard" - if we are accepting no data; "soft" if we're accepting no new - connections, and "awake" if we're not hibernating at all. The "bytes" - and "bytes-left" fields contain (read-bytes SP write-bytes), for the - start and the rest of the interval respectively. The 'interval-start' - and 'interval-end' fields are the borders of the current interval; the - 'interval-wake' field is the time within the current interval (if any) - where we plan[ned] to start being active. The times are GMT. - - "config/names" - A series of lines listing the available configuration options. Each is - of the form: - OptionName SP OptionType [ SP Documentation ] CRLF - OptionName = Keyword - OptionType = "Integer" / "TimeInterval" / "DataSize" / "Float" / - "Boolean" / "Time" / "CommaList" / "Dependant" / "Virtual" / - "String" / "LineList" - Documentation = Text - - "info/names" - A series of lines listing the available GETINFO options. Each is of - one of these forms: - OptionName SP Documentation CRLF - OptionPrefix SP Documentation CRLF - OptionPrefix = OptionName "/*" - - "events/names" - A space-separated list of all the events supported by this version of - Tor's SETEVENTS. - - "features/names" - A space-separated list of all the events supported by this version of - Tor's USEFEATURE. - - "ip-to-country/*" - Maps IP addresses to 2-letter country codes. For example, - "GETINFO ip-to-country/18.0.0.1" should give "US". - - "next-circuit/IP:port" - XXX todo. - - "dir/status-vote/current/consensus" [added in Tor 0.2.1.6-alpha] - "dir/status/authority" - "dir/status/fp/" - "dir/status/fp/++" - "dir/status/all" - "dir/server/fp/" - "dir/server/fp/++" - "dir/server/d/" - "dir/server/d/++" - "dir/server/authority" - "dir/server/all" - A series of lines listing directory contents, provided according to the - specification for the URLs listed in Section 4.4 of dir-spec.txt. Note - that Tor MUST NOT provide private information, such as descriptors for - routers not marked as general-purpose. When asked for 'authority' - information for which this Tor is not authoritative, Tor replies with - an empty string. - - "status/circuit-established" - "status/enough-dir-info" - "status/good-server-descriptor" - "status/accepted-server-descriptor" - "status/..." - These provide the current internal Tor values for various Tor - states. See Section 4.1.10 for explanations. (Only a few of the - status events are available as getinfo's currently. Let us know if - you want more exposed.) - "status/reachability-succeeded/or" - 0 or 1, depending on whether we've found our ORPort reachable. - "status/reachability-succeeded/dir" - 0 or 1, depending on whether we've found our DirPort reachable. - "status/reachability-succeeded" - "OR=" ("0"/"1") SP "DIR=" ("0"/"1") - Combines status/reachability-succeeded/*; controllers MUST ignore - unrecognized elements in this entry. - "status/bootstrap-phase" - Returns the most recent bootstrap phase status event - sent. Specifically, it returns a string starting with either - "NOTICE BOOTSTRAP ..." or "WARN BOOTSTRAP ...". Controllers should - use this getinfo when they connect or attach to Tor to learn its - current bootstrap state. - "status/version/recommended" - List of currently recommended versions. - "status/version/current" - Status of the current version. One of: new, old, unrecommended, - recommended, new in series, obsolete. - "status/clients-seen" - A summary of which countries we've seen clients from recently, - formatted the same as the CLIENTS_SEEN status event described in - Section 4.1.14. This GETINFO option is currently available only - for bridge relays. - - Examples: - C: GETINFO version desc/name/moria1 - S: 250+desc/name/moria= - S: [Descriptor for moria] - S: . - S: 250-version=Tor 0.1.1.0-alpha-cvs - S: 250 OK - -3.10. EXTENDCIRCUIT - - Sent from the client to the server. The format is: - "EXTENDCIRCUIT" SP CircuitID SP - ServerSpec *("," ServerSpec) - [SP "purpose=" Purpose] CRLF - - This request takes one of two forms: either the CircuitID is zero, in - which case it is a request for the server to build a new circuit according - to the specified path, or the CircuitID is nonzero, in which case it is a - request for the server to extend an existing circuit with that ID according - to the specified path. - - If CircuitID is 0 and "purpose=" is specified, then the circuit's - purpose is set. Two choices are recognized: "general" and - "controller". If not specified, circuits are created as "general". - - If the request is successful, the server sends a reply containing a - message body consisting of the CircuitID of the (maybe newly created) - circuit. The syntax is "250" SP "EXTENDED" SP CircuitID CRLF. - -3.11. SETCIRCUITPURPOSE - - Sent from the client to the server. The format is: - "SETCIRCUITPURPOSE" SP CircuitID SP Purpose CRLF - - This changes the circuit's purpose. See EXTENDCIRCUIT above for details. - -3.12. SETROUTERPURPOSE - - Sent from the client to the server. The format is: - "SETROUTERPURPOSE" SP NicknameOrKey SP Purpose CRLF - - This changes the descriptor's purpose. See +POSTDESCRIPTOR below - for details. - - NOTE: This command was disabled and made obsolete as of Tor - 0.2.0.8-alpha. It doesn't exist anymore, and is listed here only for - historical interest. - -3.13. ATTACHSTREAM - - Sent from the client to the server. The syntax is: - "ATTACHSTREAM" SP StreamID SP CircuitID [SP "HOP=" HopNum] CRLF - - This message informs the server that the specified stream should be - associated with the specified circuit. Each stream may be associated with - at most one circuit, and multiple streams may share the same circuit. - Streams can only be attached to completed circuits (that is, circuits that - have sent a circuit status 'BUILT' event or are listed as built in a - GETINFO circuit-status request). - - If the circuit ID is 0, responsibility for attaching the given stream is - returned to Tor. - - If HOP=HopNum is specified, Tor will choose the HopNumth hop in the - circuit as the exit node, rather than the last node in the circuit. - Hops are 1-indexed; generally, it is not permitted to attach to hop 1. - - Tor responds with "250 OK" if it can attach the stream, 552 if the circuit - or stream didn't exist, or 551 if the stream couldn't be attached for - another reason. - - {Implementation note: Tor will close unattached streams by itself, - roughly two minutes after they are born. Let the developers know if - that turns out to be a problem.} - - {Implementation note: By default, Tor automatically attaches streams to - circuits itself, unless the configuration variable - "__LeaveStreamsUnattached" is set to "1". Attempting to attach streams - via TC when "__LeaveStreamsUnattached" is false may cause a race between - Tor and the controller, as both attempt to attach streams to circuits.} - - {Implementation note: You can try to attachstream to a stream that - has already sent a connect or resolve request but hasn't succeeded - yet, in which case Tor will detach the stream from its current circuit - before proceeding with the new attach request.} - -3.14. POSTDESCRIPTOR - - Sent from the client to the server. The syntax is: - "+POSTDESCRIPTOR" [SP "purpose=" Purpose] [SP "cache=" Cache] - CRLF Descriptor CRLF "." CRLF - - This message informs the server about a new descriptor. If Purpose is - specified, it must be either "general", "controller", or "bridge", - else we return a 552 error. The default is "general". - - If Cache is specified, it must be either "no" or "yes", else we - return a 552 error. If Cache is not specified, Tor will decide for - itself whether it wants to cache the descriptor, and controllers - must not rely on its choice. - - The descriptor, when parsed, must contain a number of well-specified - fields, including fields for its nickname and identity. - - If there is an error in parsing the descriptor, the server must send a - "554 Invalid descriptor" reply. If the descriptor is well-formed but - the server chooses not to add it, it must reply with a 251 message - whose body explains why the server was not added. If the descriptor - is added, Tor replies with "250 OK". - -3.15. REDIRECTSTREAM - - Sent from the client to the server. The syntax is: - "REDIRECTSTREAM" SP StreamID SP Address [SP Port] CRLF - - Tells the server to change the exit address on the specified stream. If - Port is specified, changes the destination port as well. No remapping - is performed on the new provided address. - - To be sure that the modified address will be used, this event must be sent - after a new stream event is received, and before attaching this stream to - a circuit. - - Tor replies with "250 OK" on success. - -3.16. CLOSESTREAM - - Sent from the client to the server. The syntax is: - - "CLOSESTREAM" SP StreamID SP Reason *(SP Flag) CRLF - - Tells the server to close the specified stream. The reason should be one - of the Tor RELAY_END reasons given in tor-spec.txt, as a decimal. Flags is - not used currently; Tor servers SHOULD ignore unrecognized flags. Tor may - hold the stream open for a while to flush any data that is pending. - - Tor replies with "250 OK" on success, or a 512 if there aren't enough - arguments, or a 552 if it doesn't recognize the StreamID or reason. - -3.17. CLOSECIRCUIT - - The syntax is: - CLOSECIRCUIT SP CircuitID *(SP Flag) CRLF - Flag = "IfUnused" - - Tells the server to close the specified circuit. If "IfUnused" is - provided, do not close the circuit unless it is unused. - - Other flags may be defined in the future; Tor SHOULD ignore unrecognized - flags. - - Tor replies with "250 OK" on success, or a 512 if there aren't enough - arguments, or a 552 if it doesn't recognize the CircuitID. - -3.18. QUIT - - Tells the server to hang up on this controller connection. This command - can be used before authenticating. - -3.19. USEFEATURE - - The syntax is: - - "USEFEATURE" *(SP FeatureName) CRLF - FeatureName = 1*(ALPHA / DIGIT / "_" / "-") - - Sometimes extensions to the controller protocol break compatibility with - older controllers. In this case, whenever possible, the extensions are - first included in Tor disabled by default, and only enabled on a given - controller connection when the "USEFEATURE" command is given. Once a - "USEFEATURE" command is given, it applies to all subsequent interactions on - the same connection; to disable an enabled feature, a new controller - connection must be opened. - - This is a forward-compatibility mechanism; each feature will eventually - become a regular part of the control protocol in some future version of Tor. - Tor will ignore a request to use any feature that is already on by default. - Tor will give a "552" error if any requested feature is not recognized. - - Feature names are case-insensitive. - - EXTENDED_EVENTS - - Same as passing 'EXTENDED' to SETEVENTS; this is the preferred way to - request the extended event syntax. - - This feature was first used in 0.1.2.3-alpha. It is always-on in - Tor 0.2.2.1-alpha and later. - - VERBOSE_NAMES - - Instead of ServerID as specified above, the controller should - identify ORs by LongName in events and GETINFO results. This format is - strictly more informative: rather than including Nickname for - known Named routers and Fingerprint for unknown or unNamed routers, the - LongName format includes a Fingerprint, an indication of Named status, - and a Nickname (if one is known). - - This will not be always-enabled until at least two stable - releases after 0.1.2.2-alpha, the release where it was first - available. It is always-on in Tor 0.2.2.1-alpha and later. - -3.20. RESOLVE - - The syntax is - "RESOLVE" *Option *Address CRLF - Option = "mode=reverse" - Address = a hostname or IPv4 address - - This command launches a remote hostname lookup request for every specified - request (or reverse lookup if "mode=reverse" is specified). Note that the - request is done in the background: to see the answers, your controller will - need to listen for ADDRMAP events; see 4.1.7 below. - - [Added in Tor 0.2.0.3-alpha] - -3.21. PROTOCOLINFO - - The syntax is: - "PROTOCOLINFO" *(SP PIVERSION) CRLF - - The server reply format is: - "250-PROTOCOLINFO" SP PIVERSION CRLF *InfoLine "250 OK" CRLF - - InfoLine = AuthLine / VersionLine / OtherLine - - AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *(",")AuthMethod - *(SP "COOKIEFILE=" AuthCookieFile) CRLF - VersionLine = "250-VERSION" SP "Tor=" TorVersion [SP Arguments] CRLF - - AuthMethod = - "NULL" / ; No authentication is required - "HASHEDPASSWORD" / ; A controller must supply the original password - "COOKIE" / ; A controller must supply the contents of a cookie - - AuthCookieFile = QuotedString - TorVersion = QuotedString - - OtherLine = "250-" Keyword [SP Arguments] CRLF - - PIVERSION: 1*DIGIT - - Tor MAY give its InfoLines in any order; controllers MUST ignore InfoLines - with keywords they do not recognize. Controllers MUST ignore extraneous - data on any InfoLine. - - PIVERSION is there in case we drastically change the syntax one day. For - now it should always be "1". Controllers MAY provide a list of the - protocolinfo versions they support; Tor MAY select a version that the - controller does not support. - - AuthMethod is used to specify one or more control authentication - methods that Tor currently accepts. - - AuthCookieFile specifies the absolute path and filename of the - authentication cookie that Tor is expecting and is provided iff - the METHODS field contains the method "COOKIE". Controllers MUST handle - escape sequences inside this string. - - The VERSION line contains the Tor version. - - [Unlike other commands besides AUTHENTICATE, PROTOCOLINFO may be used (but - only once!) before AUTHENTICATE.] - - [PROTOCOLINFO was not supported before Tor 0.2.0.5-alpha.] - -4. Replies - - Reply codes follow the same 3-character format as used by SMTP, with the - first character defining a status, the second character defining a - subsystem, and the third designating fine-grained information. - - The TC protocol currently uses the following first characters: - - 2yz Positive Completion Reply - The command was successful; a new request can be started. - - 4yz Temporary Negative Completion reply - The command was unsuccessful but might be reattempted later. - - 5yz Permanent Negative Completion Reply - The command was unsuccessful; the client should not try exactly - that sequence of commands again. - - 6yz Asynchronous Reply - Sent out-of-order in response to an earlier SETEVENTS command. - - The following second characters are used: - - x0z Syntax - Sent in response to ill-formed or nonsensical commands. - - x1z Protocol - Refers to operations of the Tor Control protocol. - - x5z Tor - Refers to actual operations of Tor system. - - The following codes are defined: - - 250 OK - 251 Operation was unnecessary - [Tor has declined to perform the operation, but no harm was done.] - - 451 Resource exhausted - - 500 Syntax error: protocol - - 510 Unrecognized command - 511 Unimplemented command - 512 Syntax error in command argument - 513 Unrecognized command argument - 514 Authentication required - 515 Bad authentication - - 550 Unspecified Tor error - - 551 Internal error - [Something went wrong inside Tor, so that the client's - request couldn't be fulfilled.] - - 552 Unrecognized entity - [A configuration key, a stream ID, circuit ID, event, - mentioned in the command did not actually exist.] - - 553 Invalid configuration value - [The client tried to set a configuration option to an - incorrect, ill-formed, or impossible value.] - - 554 Invalid descriptor - - 555 Unmanaged entity - - 650 Asynchronous event notification - - Unless specified to have specific contents, the human-readable messages - in error replies should not be relied upon to match those in this document. - -4.1. Asynchronous events - - These replies can be sent after a corresponding SETEVENTS command has been - received. They will not be interleaved with other Reply elements, but they - can appear between a command and its corresponding reply. For example, - this sequence is possible: - - C: SETEVENTS CIRC - S: 250 OK - C: GETCONF SOCKSPORT ORPORT - S: 650 CIRC 1000 EXTENDED moria1,moria2 - S: 250-SOCKSPORT=9050 - S: 250 ORPORT=0 - - But this sequence is disallowed: - C: SETEVENTS CIRC - S: 250 OK - C: GETCONF SOCKSPORT ORPORT - S: 250-SOCKSPORT=9050 - S: 650 CIRC 1000 EXTENDED moria1,moria2 - S: 250 ORPORT=0 - - Clients MUST tolerate more arguments in an asynchonous reply than - expected, and MUST tolerate more lines in an asynchronous reply than - expected. For instance, a client that expects a CIRC message like: - 650 CIRC 1000 EXTENDED moria1,moria2 - must tolerate: - 650-CIRC 1000 EXTENDED moria1,moria2 0xBEEF - 650-EXTRAMAGIC=99 - 650 ANONYMITY=high - - If clients ask for extended events, then each event line as specified below - will be followed by additional extensions. Additional lines will be of the - form - "650" ("-"/" ") KEYWORD ["=" ARGUMENTS] CRLF - Additional arguments will be of the form - SP KEYWORD ["=" ( QuotedString / * NonSpDquote ) ] - Such clients MUST tolerate lines with keywords they do not recognize. - -4.1.1. Circuit status changed - - The syntax is: - - "650" SP "CIRC" SP CircuitID SP CircStatus [SP Path] - [SP "REASON=" Reason [SP "REMOTE_REASON=" Reason]] CRLF - - CircStatus = - "LAUNCHED" / ; circuit ID assigned to new circuit - "BUILT" / ; all hops finished, can now accept streams - "EXTENDED" / ; one more hop has been completed - "FAILED" / ; circuit closed (was not built) - "CLOSED" ; circuit closed (was built) - - Path = ServerID *("," ServerID) - - Reason = "NONE" / "TORPROTOCOL" / "INTERNAL" / "REQUESTED" / - "HIBERNATING" / "RESOURCELIMIT" / "CONNECTFAILED" / - "OR_IDENTITY" / "OR_CONN_CLOSED" / "TIMEOUT" / - "FINISHED" / "DESTROYED" / "NOPATH" / "NOSUCHSERVICE" - - The path is provided only when the circuit has been extended at least one - hop. - - The "REASON" field is provided only for FAILED and CLOSED events, and only - if extended events are enabled (see 3.19). Clients MUST accept reasons - not listed above. Reasons are as given in tor-spec.txt, except for: - - NOPATH (Not enough nodes to make circuit) - - The "REMOTE_REASON" field is provided only when we receive a DESTROY or - TRUNCATE cell, and only if extended events are enabled. It contains the - actual reason given by the remote OR for closing the circuit. Clients MUST - accept reasons not listed above. Reasons are as listed in tor-spec.txt. - -4.1.2. Stream status changed - - The syntax is: - - "650" SP "STREAM" SP StreamID SP StreamStatus SP CircID SP Target - [SP "REASON=" Reason [ SP "REMOTE_REASON=" Reason ]] - [SP "SOURCE=" Source] [ SP "SOURCE_ADDR=" Address ":" Port ] - [SP "PURPOSE=" Purpose] - CRLF - - StreamStatus = - "NEW" / ; New request to connect - "NEWRESOLVE" / ; New request to resolve an address - "REMAP" / ; Address re-mapped to another - "SENTCONNECT" / ; Sent a connect cell along a circuit - "SENTRESOLVE" / ; Sent a resolve cell along a circuit - "SUCCEEDED" / ; Received a reply; stream established - "FAILED" / ; Stream failed and not retriable - "CLOSED" / ; Stream closed - "DETACHED" ; Detached from circuit; still retriable - - Target = Address ":" Port - - The circuit ID designates which circuit this stream is attached to. If - the stream is unattached, the circuit ID "0" is given. - - Reason = "MISC" / "RESOLVEFAILED" / "CONNECTREFUSED" / - "EXITPOLICY" / "DESTROY" / "DONE" / "TIMEOUT" / - "HIBERNATING" / "INTERNAL"/ "RESOURCELIMIT" / - "CONNRESET" / "TORPROTOCOL" / "NOTDIRECTORY" / "END" - - The "REASON" field is provided only for FAILED, CLOSED, and DETACHED - events, and only if extended events are enabled (see 3.19). Clients MUST - accept reasons not listed above. Reasons are as given in tor-spec.txt, - except for: - - END (We received a RELAY_END cell from the other side of this - stream.) - [XXXX document more. -NM] - - The "REMOTE_REASON" field is provided only when we receive a RELAY_END - cell, and only if extended events are enabled. It contains the actual - reason given by the remote OR for closing the stream. Clients MUST accept - reasons not listed above. Reasons are as listed in tor-spec.txt. - - "REMAP" events include a Source if extended events are enabled: - Source = "CACHE" / "EXIT" - Clients MUST accept sources not listed above. "CACHE" is given if - the Tor client decided to remap the address because of a cached - answer, and "EXIT" is given if the remote node we queried gave us - the new address as a response. - - The "SOURCE_ADDR" field is included with NEW and NEWRESOLVE events if - extended events are enabled. It indicates the address and port - that requested the connection, and can be (e.g.) used to look up the - requesting program. - - Purpose = "DIR_FETCH" / "UPLOAD_DESC" / "DNS_REQUEST" / - "USER" / "DIRPORT_TEST" - - The "PURPOSE" field is provided only for NEW and NEWRESOLVE events, and - only if extended events are enabled (see 3.19). Clients MUST accept - purposes not listed above. - -4.1.3. OR Connection status changed - - The syntax is: - "650" SP "ORCONN" SP (ServerID / Target) SP ORStatus [ SP "REASON=" - Reason ] [ SP "NCIRCS=" NumCircuits ] CRLF - - ORStatus = "NEW" / "LAUNCHED" / "CONNECTED" / "FAILED" / "CLOSED" - - NEW is for incoming connections, and LAUNCHED is for outgoing - connections. CONNECTED means the TLS handshake has finished (in - either direction). FAILED means a connection is being closed that - hasn't finished its handshake, and CLOSED is for connections that - have handshaked. - - A ServerID is specified unless it's a NEW connection, in which - case we don't know what server it is yet, so we use Address:Port. - - If extended events are enabled (see 3.19), optional reason and - circuit counting information is provided for CLOSED and FAILED - events. - - Reason = "MISC" / "DONE" / "CONNECTREFUSED" / - "IDENTITY" / "CONNECTRESET" / "TIMEOUT" / "NOROUTE" / - "IOERROR" / "RESOURCELIMIT" - - NumCircuits counts both established and pending circuits. - -4.1.4. Bandwidth used in the last second - - The syntax is: - "650" SP "BW" SP BytesRead SP BytesWritten *(SP Type "=" Num) CRLF - BytesRead = 1*DIGIT - BytesWritten = 1*DIGIT - Type = "DIR" / "OR" / "EXIT" / "APP" / ... - Num = 1*DIGIT - - BytesRead and BytesWritten are the totals. [In a future Tor version, - we may also include a breakdown of the connection types that used - bandwidth this second (not implemented yet).] - -4.1.5. Log messages - - The syntax is: - "650" SP Severity SP ReplyText CRLF - or - "650+" Severity CRLF Data 650 SP "OK" CRLF - - Severity = "DEBUG" / "INFO" / "NOTICE" / "WARN"/ "ERR" - -4.1.6. New descriptors available - - Syntax: - "650" SP "NEWDESC" 1*(SP ServerID) CRLF - -4.1.7. New Address mapping - - Syntax: - "650" SP "ADDRMAP" SP Address SP NewAddress SP Expiry - [SP Error] SP GMTExpiry CRLF - - NewAddress = Address / "" - Expiry = DQUOTE ISOTime DQUOTE / "NEVER" - - Error = "error=" ErrorCode - ErrorCode = XXXX - GMTExpiry = "EXPIRES=" DQUOTE IsoTime DQUOTE - - Error and GMTExpiry are only provided if extended events are enabled. - - Expiry is expressed as the local time (rather than GMT). This is a bug, - left in for backward compatibility; new code should look at GMTExpiry - instead. - - These events are generated when a new address mapping is entered in the - cache, or when the answer for a RESOLVE command is found. - -4.1.8. Descriptors uploaded to us in our role as authoritative dirserver - - Syntax: - "650" "+" "AUTHDIR_NEWDESCS" CRLF Action CRLF Message CRLF - Descriptor CRLF "." CRLF "650" SP "OK" CRLF - Action = "ACCEPTED" / "DROPPED" / "REJECTED" - Message = Text - -4.1.9. Our descriptor changed - - Syntax: - "650" SP "DESCCHANGED" CRLF - - [First added in 0.1.2.2-alpha.] - -4.1.10. Status events - - Status events (STATUS_GENERAL, STATUS_CLIENT, and STATUS_SERVER) are sent - based on occurrences in the Tor process pertaining to the general state of - the program. Generally, they correspond to log messages of severity Notice - or higher. They differ from log messages in that their format is a - specified interface. - - Syntax: - "650" SP StatusType SP StatusSeverity SP StatusAction - [SP StatusArguments] CRLF - - StatusType = "STATUS_GENERAL" / "STATUS_CLIENT" / "STATUS_SERVER" - StatusSeverity = "NOTICE" / "WARN" / "ERR" - StatusAction = 1*ALPHA - StatusArguments = StatusArgument *(SP StatusArgument) - StatusArgument = StatusKeyword '=' StatusValue - StatusKeyword = 1*(ALNUM / "_") - StatusValue = 1*(ALNUM / '_') / QuotedString - - Action is a string, and Arguments is a series of keyword=value - pairs on the same line. Values may be space-terminated strings, - or quoted strings. - - These events are always produced with EXTENDED_EVENTS and - VERBOSE_NAMES; see the explanations in the USEFEATURE section - for details. - - Controllers MUST tolerate unrecognized actions, MUST tolerate - unrecognized arguments, MUST tolerate missing arguments, and MUST - tolerate arguments that arrive in any order. - - Each event description below is accompanied by a recommendation for - controllers. These recommendations are suggestions only; no controller - is required to implement them. - - Compatibility note: versions of Tor before 0.2.0.22-rc incorrectly - generated "STATUS_SERVER" as "STATUS_SEVER". To be compatible with those - versions, tools should accept both. - - Actions for STATUS_GENERAL events can be as follows: - - CLOCK_JUMPED - "TIME=NUM" - Tor spent enough time without CPU cycles that it has closed all - its circuits and will establish them anew. This typically - happens when a laptop goes to sleep and then wakes up again. It - also happens when the system is swapping so heavily that Tor is - starving. The "time" argument specifies the number of seconds Tor - thinks it was unconscious for (or alternatively, the number of - seconds it went back in time). - - This status event is sent as NOTICE severity normally, but WARN - severity if Tor is acting as a server currently. - - {Recommendation for controller: ignore it, since we don't really - know what the user should do anyway. Hm.} - - DANGEROUS_VERSION - "CURRENT=version" - "REASON=NEW/OBSOLETE/UNRECOMMENDED" - "RECOMMENDED=\"version, version, ...\"" - Tor has found that directory servers don't recommend its version of - the Tor software. RECOMMENDED is a comma-and-space-separated string - of Tor versions that are recommended. REASON is NEW if this version - of Tor is newer than any recommended version, OBSOLETE if - this version of Tor is older than any recommended version, and - UNRECOMMENDED if some recommended versions of Tor are newer and - some are older than this version. (The "OBSOLETE" reason was called - "OLD" from Tor 0.1.2.3-alpha up to and including 0.2.0.12-alpha.) - - {Controllers may want to suggest that the user upgrade OLD or - UNRECOMMENDED versions. NEW versions may be known-insecure, or may - simply be development versions.} - - TOO_MANY_CONNECTIONS - "CURRENT=NUM" - Tor has reached its ulimit -n or whatever the native limit is on file - descriptors or sockets. CURRENT is the number of sockets Tor - currently has open. The user should really do something about - this. The "current" argument shows the number of connections currently - open. - - {Controllers may recommend that the user increase the limit, or - increase it for them. Recommendations should be phrased in an - OS-appropriate way and automated when possible.} - - BUG - "REASON=STRING" - Tor has encountered a situation that its developers never expected, - and the developers would like to learn that it happened. Perhaps - the controller can explain this to the user and encourage her to - file a bug report? - - {Controllers should log bugs, but shouldn't annoy the user in case a - bug appears frequently.} - - CLOCK_SKEW - SKEW="+" / "-" SECONDS - MIN_SKEW="+" / "-" SECONDS. - SOURCE="DIRSERV:" IP ":" Port / - "NETWORKSTATUS:" IP ":" Port / - "OR:" IP ":" Port / - "CONSENSUS" - If "SKEW" is present, it's an estimate of how far we are from the - time declared in the source. (In other words, if we're an hour in - the past, the value is -3600.) "MIN_SKEW" is present, it's a lower - bound. If the source is a DIRSERV, we got the current time from a - connection to a dirserver. If the source is a NETWORKSTATUS, we - decided we're skewed because we got a v2 networkstatus from far in - the future. If the source is OR, the skew comes from a NETINFO - cell from a connection to another relay. If the source is - CONSENSUS, we decided we're skewed because we got a networkstatus - consensus from the future. - - {Tor should send this message to controllers when it thinks the - skew is so high that it will interfere with proper Tor operation. - Controllers shouldn't blindly adjust the clock, since the more - accurate source of skew info (DIRSERV) is currently - unauthenticated.} - - BAD_LIBEVENT - "METHOD=" libevent method - "VERSION=" libevent version - "BADNESS=" "BROKEN" / "BUGGY" / "SLOW" - "RECOVERED=" "NO" / "YES" - Tor knows about bugs in using the configured event method in this - version of libevent. "BROKEN" libevents won't work at all; - "BUGGY" libevents might work okay; "SLOW" libevents will work - fine, but not quickly. If "RECOVERED" is YES, Tor managed to - switch to a more reliable (but probably slower!) libevent method. - - {Controllers may want to warn the user if this event occurs, though - generally it's the fault of whoever built the Tor binary and there's - not much the user can do besides upgrade libevent or upgrade the - binary.} - - DIR_ALL_UNREACHABLE - Tor believes that none of the known directory servers are - reachable -- this is most likely because the local network is - down or otherwise not working, and might help to explain for the - user why Tor appears to be broken. - - {Controllers may want to warn the user if this event occurs; further - action is generally not possible.} - - CONSENSUS_ARRIVED - Tor has received and validated a new consensus networkstatus. - (This event can be delayed a little while after the consensus - is received, if Tor needs to fetch certificates.) - - Actions for STATUS_CLIENT events can be as follows: - - BOOTSTRAP - "PROGRESS=" num - "TAG=" Keyword - "SUMMARY=" String - ["WARNING=" String - "REASON=" Keyword - "COUNT=" num - "RECOMMENDATION=" Keyword - ] - - Tor has made some progress at establishing a connection to the - Tor network, fetching directory information, or making its first - circuit; or it has encountered a problem while bootstrapping. This - status event is especially useful for users with slow connections - or with connectivity problems. - - "Progress" gives a number between 0 and 100 for how far through - the bootstrapping process we are. "Summary" is a string that can - be displayed to the user to describe the *next* task that Tor - will tackle, i.e., the task it is working on after sending the - status event. "Tag" is a string that controllers can use to - recognize bootstrap phases, if they want to do something smarter - than just blindly displaying the summary string; see Section 5 - for the current tags that Tor issues. - - The StatusSeverity describes whether this is a normal bootstrap - phase (severity notice) or an indication of a bootstrapping - problem (severity warn). - - For bootstrap problems, we include the same progress, tag, and - summary values as we would for a normal bootstrap event, but we - also include "warning", "reason", "count", and "recommendation" - key/value combos. The "count" number tells how many bootstrap - problems there have been so far at this phase. The "reason" - string lists one of the reasons allowed in the ORCONN event. The - "warning" argument string with any hints Tor has to offer about - why it's having troubles bootstrapping. - - The "reason" values are long-term-stable controller-facing tags to - identify particular issues in a bootstrapping step. The warning - strings, on the other hand, are human-readable. Controllers - SHOULD NOT rely on the format of any warning string. Currently - the possible values for "recommendation" are either "ignore" or - "warn" -- if ignore, the controller can accumulate the string in - a pile of problems to show the user if the user asks; if warn, - the controller should alert the user that Tor is pretty sure - there's a bootstrapping problem. - - Currently Tor uses recommendation=ignore for the first - nine bootstrap problem reports for a given phase, and then - uses recommendation=warn for subsequent problems at that - phase. Hopefully this is a good balance between tolerating - occasional errors and reporting serious problems quickly. - - ENOUGH_DIR_INFO - Tor now knows enough network-status documents and enough server - descriptors that it's going to start trying to build circuits now. - - {Controllers may want to use this event to decide when to indicate - progress to their users, but should not interrupt the user's browsing - to tell them so.} - - NOT_ENOUGH_DIR_INFO - We discarded expired statuses and router descriptors to fall - below the desired threshold of directory information. We won't - try to build any circuits until ENOUGH_DIR_INFO occurs again. - - {Controllers may want to use this event to decide when to indicate - progress to their users, but should not interrupt the user's browsing - to tell them so.} - - CIRCUIT_ESTABLISHED - Tor is able to establish circuits for client use. This event will - only be sent if we just built a circuit that changed our mind -- - that is, prior to this event we didn't know whether we could - establish circuits. - - {Suggested use: controllers can notify their users that Tor is - ready for use as a client once they see this status event. [Perhaps - controllers should also have a timeout if too much time passes and - this event hasn't arrived, to give tips on how to troubleshoot. - On the other hand, hopefully Tor will send further status events - if it can identify the problem.]} - - CIRCUIT_NOT_ESTABLISHED - "REASON=" "EXTERNAL_ADDRESS" / "DIR_ALL_UNREACHABLE" / "CLOCK_JUMPED" - We are no longer confident that we can build circuits. The "reason" - keyword provides an explanation: which other status event type caused - our lack of confidence. - - {Controllers may want to use this event to decide when to indicate - progress to their users, but should not interrupt the user's browsing - to do so.} - [Note: only REASON=CLOCK_JUMPED is implemented currently.] - - DANGEROUS_PORT - "PORT=" port - "RESULT=" "REJECT" / "WARN" - A stream was initiated to a port that's commonly used for - vulnerable-plaintext protocols. If the Result is "reject", we - refused the connection; whereas if it's "warn", we allowed it. - - {Controllers should warn their users when this occurs, unless they - happen to know that the application using Tor is in fact doing so - correctly (e.g., because it is part of a distributed bundle). They - might also want some sort of interface to let the user configure - their RejectPlaintextPorts and WarnPlaintextPorts config options.} - - DANGEROUS_SOCKS - "PROTOCOL=" "SOCKS4" / "SOCKS5" - "ADDRESS=" IP:port - A connection was made to Tor's SOCKS port using one of the SOCKS - approaches that doesn't support hostnames -- only raw IP addresses. - If the client application got this address from gethostbyname(), - it may be leaking target addresses via DNS. - - {Controllers should warn their users when this occurs, unless they - happen to know that the application using Tor is in fact doing so - correctly (e.g., because it is part of a distributed bundle).} - - SOCKS_UNKNOWN_PROTOCOL - "DATA=string" - A connection was made to Tor's SOCKS port that tried to use it - for something other than the SOCKS protocol. Perhaps the user is - using Tor as an HTTP proxy? The DATA is the first few characters - sent to Tor on the SOCKS port. - - {Controllers may want to warn their users when this occurs: it - indicates a misconfigured application.} - - SOCKS_BAD_HOSTNAME - "HOSTNAME=QuotedString" - Some application gave us a funny-looking hostname. Perhaps - it is broken? In any case it won't work with Tor and the user - should know. - - {Controllers may want to warn their users when this occurs: it - usually indicates a misconfigured application.} - - Actions for STATUS_SERVER can be as follows: - - EXTERNAL_ADDRESS - "ADDRESS=IP" - "HOSTNAME=NAME" - "METHOD=CONFIGURED/DIRSERV/RESOLVED/INTERFACE/GETHOSTNAME" - Our best idea for our externally visible IP has changed to 'IP'. - If 'HOSTNAME' is present, we got the new IP by resolving 'NAME'. If the - method is 'CONFIGURED', the IP was given verbatim as a configuration - option. If the method is 'RESOLVED', we resolved the Address - configuration option to get the IP. If the method is 'GETHOSTNAME', - we resolved our hostname to get the IP. If the method is 'INTERFACE', - we got the address of one of our network interfaces to get the IP. If - the method is 'DIRSERV', a directory server told us a guess for what - our IP might be. - - {Controllers may want to record this info and display it to the user.} - - CHECKING_REACHABILITY - "ORADDRESS=IP:port" - "DIRADDRESS=IP:port" - We're going to start testing the reachability of our external OR port - or directory port. - - {This event could affect the controller's idea of server status, but - the controller should not interrupt the user to tell them so.} - - REACHABILITY_SUCCEEDED - "ORADDRESS=IP:port" - "DIRADDRESS=IP:port" - We successfully verified the reachability of our external OR port or - directory port (depending on which of ORADDRESS or DIRADDRESS is - given.) - - {This event could affect the controller's idea of server status, but - the controller should not interrupt the user to tell them so.} - - GOOD_SERVER_DESCRIPTOR - We successfully uploaded our server descriptor to at least one - of the directory authorities, with no complaints. - - {Originally, the goal of this event was to declare "every authority - has accepted the descriptor, so there will be no complaints - about it." But since some authorities might be offline, it's - harder to get certainty than we had thought. As such, this event - is equivalent to ACCEPTED_SERVER_DESCRIPTOR below. Controllers - should just look at ACCEPTED_SERVER_DESCRIPTOR and should ignore - this event for now.} - - NAMESERVER_STATUS - "NS=addr" - "STATUS=" "UP" / "DOWN" - "ERR=" message - One of our nameservers has changed status. - - {This event could affect the controller's idea of server status, but - the controller should not interrupt the user to tell them so.} - - NAMESERVER_ALL_DOWN - All of our nameservers have gone down. - - {This is a problem; if it happens often without the nameservers - coming up again, the user needs to configure more or better - nameservers.} - - DNS_HIJACKED - Our DNS provider is providing an address when it should be saying - "NOTFOUND"; Tor will treat the address as a synonym for "NOTFOUND". - - {This is an annoyance; controllers may want to tell admins that their - DNS provider is not to be trusted.} - - DNS_USELESS - Our DNS provider is giving a hijacked address instead of well-known - websites; Tor will not try to be an exit node. - - {Controllers could warn the admin if the server is running as an - exit server: the admin needs to configure a good DNS server. - Alternatively, this happens a lot in some restrictive environments - (hotels, universities, coffeeshops) when the user hasn't registered.} - - BAD_SERVER_DESCRIPTOR - "DIRAUTH=addr:port" - "REASON=string" - A directory authority rejected our descriptor. Possible reasons - include malformed descriptors, incorrect keys, highly skewed clocks, - and so on. - - {Controllers should warn the admin, and try to cope if they can.} - - ACCEPTED_SERVER_DESCRIPTOR - "DIRAUTH=addr:port" - A single directory authority accepted our descriptor. - // actually notice - - {This event could affect the controller's idea of server status, but - the controller should not interrupt the user to tell them so.} - - REACHABILITY_FAILED - "ORADDRESS=IP:port" - "DIRADDRESS=IP:port" - We failed to connect to our external OR port or directory port - successfully. - - {This event could affect the controller's idea of server status. The - controller should warn the admin and suggest reasonable steps to take.} - -4.1.11. Our set of guard nodes has changed - - Syntax: - "650" SP "GUARD" SP Type SP Name SP Status ... CRLF - Type = "ENTRY" - Name = The (possibly verbose) nickname of the guard affected. - Status = "NEW" | "UP" | "DOWN" | "BAD" | "GOOD" | "DROPPED" - - [explain states. XXX] - -4.1.12. Network status has changed - - Syntax: - "650" "+" "NS" CRLF 1*NetworkStatus "." CRLF "650" SP "OK" CRLF - - The event is used whenever our local view of a relay status changes. - This happens when we get a new v3 consensus (in which case the entries - we see are a duplicate of what we see in the NEWCONSENSUS event, - below), but it also happens when we decide to mark a relay as up or - down in our local status, for example based on connection attempts. - - [First added in 0.1.2.3-alpha] - -4.1.13. Bandwidth used on an application stream - - The syntax is: - "650" SP "STREAM_BW" SP StreamID SP BytesRead SP BytesWritten CRLF - BytesRead = 1*DIGIT - BytesWritten = 1*DIGIT - - BytesRead and BytesWritten are the number of bytes read and written since - the last STREAM_BW event on this stream. These events are generated about - once per second per stream; no events are generated for streams that have - not read or written. - - These events apply only to streams entering Tor (such as on a SOCKSPort, - TransPort, or so on). They are not generated for exiting streams. - -4.1.14. Per-country client stats - - The syntax is: - "650" SP "CLIENTS_SEEN" SP TimeStarted SP CountrySummary CRLF - - We just generated a new summary of which countries we've seen clients - from recently. The controller could display this for the user, e.g. - in their "relay" configuration window, to give them a sense that they - are actually being useful. - - Currently only bridge relays will receive this event, but once we figure - out how to sufficiently aggregate and sanitize the client counts on - main relays, we might start sending these events in other cases too. - - TimeStarted is a quoted string indicating when the reported summary - counts from (in GMT). - - The CountrySummary keyword has as its argument a comma-separated - set of "countrycode=count" pairs. For example, - 650-CLIENTS_SEEN TimeStarted="Thu Dec 25 23:50:43 EST 2008" - 650 CountrySummary=us=16,de=8,uk=8 -[XXX Matt Edman informs me that the time format above is wrong. -RD] - -4.1.15. New consensus networkstatus has arrived. - - The syntax is: - "650" "+" "NEWCONSENSUS" CRLF 1*NetworkStatus "." CRLF "650" SP - "OK" CRLF - - A new consensus networkstatus has arrived. We include NS-style lines for - every relay in the consensus. NEWCONSENSUS is a separate event from the - NS event, because the list here represents every usable relay: so any - relay *not* mentioned in this list is implicitly no longer recommended. - - [First added in 0.2.1.13-alpha] - -5. Implementation notes - -5.1. Authentication - - If the control port is open and no authentication operation is enabled, Tor - trusts any local user that connects to the control port. This is generally - a poor idea. - - If the 'CookieAuthentication' option is true, Tor writes a "magic cookie" - file named "control_auth_cookie" into its data directory. To authenticate, - the controller must send the contents of this file, encoded in hexadecimal. - - If the 'HashedControlPassword' option is set, it must contain the salted - hash of a secret password. The salted hash is computed according to the - S2K algorithm in RFC 2440 (OpenPGP), and prefixed with the s2k specifier. - This is then encoded in hexadecimal, prefixed by the indicator sequence - "16:". Thus, for example, the password 'foo' could encode to: - 16:660537E3E1CD49996044A3BF558097A981F539FEA2F9DA662B4626C1C2 - ++++++++++++++++**^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - salt hashed value - indicator - You can generate the salt of a password by calling - 'tor --hash-password ' - or by using the example code in the Python and Java controller libraries. - To authenticate under this scheme, the controller sends Tor the original - secret that was used to generate the password, either as a quoted string - or encoded in hexadecimal. - -5.2. Don't let the buffer get too big. - - If you ask for lots of events, and 16MB of them queue up on the buffer, - the Tor process will close the socket. - -5.3. Backward compatibility with v0 control protocol. - - The 'version 0' control protocol was replaced in Tor 0.1.1.x. Support - was removed in Tor 0.2.0.x. Every non-obsolete version of Tor now - supports the version 1 control protocol. - - For backward compatibility with the "version 0" control protocol, - Tor used to check whether the third octet of the first command is zero. - (If it was, Tor assumed that version 0 is in use.) - - This compatibility was removed in Tor 0.1.2.16 and 0.2.0.4-alpha. - -5.4. Tor config options for use by controllers - - Tor provides a few special configuration options for use by controllers. - These options can be set and examined by the SETCONF and GETCONF commands, - but are not saved to disk by SAVECONF. - - Generally, these options make Tor unusable by disabling a portion of Tor's - normal operations. Unless a controller provides replacement functionality - to fill this gap, Tor will not correctly handle user requests. - - __AllDirOptionsPrivate - - If true, Tor will try to launch all directory operations through - anonymous connections. (Ordinarily, Tor only tries to anonymize - requests related to hidden services.) This option will slow down - directory access, and may stop Tor from working entirely if it does not - yet have enough directory information to build circuits. - - (Boolean. Default: "0".) - - __DisablePredictedCircuits - - If true, Tor will not launch preemptive "general-purpose" circuits for - streams to attach to. (It will still launch circuits for testing and - for hidden services.) - - (Boolean. Default: "0".) - - __LeaveStreamsUnattached - - If true, Tor will not automatically attach new streams to circuits; - instead, the controller must attach them with ATTACHSTREAM. If the - controller does not attach the streams, their data will never be routed. - - (Boolean. Default: "0".) - - __HashedControlSessionPassword - - As HashedControlPassword, but is not saved to the torrc file by - SAVECONF. Added in Tor 0.2.0.20-rc. - - __ReloadTorrcOnSIGHUP - - If this option is true (the default), we reload the torrc from disk - every time we get a SIGHUP (from the controller or via a signal). - Otherwise, we don't. This option exists so that controllers can keep - their options from getting overwritten when a user sends Tor a HUP for - some other reason (for example, to rotate the logs). - - (Boolean. Default: "1") - -5.5. Phases from the Bootstrap status event. - - This section describes the various bootstrap phases currently reported - by Tor. Controllers should not assume that the percentages and tags - listed here will continue to match up, or even that the tags will stay - in the same order. Some phases might also be skipped (not reported) - if the associated bootstrap step is already complete, or if the phase - no longer is necessary. Only "starting" and "done" are guaranteed to - exist in all future versions. - - Current Tor versions enter these phases in order, monotonically. - Future Tors MAY revisit earlier stages. - - Phase 0: - tag=starting summary="Starting" - - Tor starts out in this phase. - - Phase 5: - tag=conn_dir summary="Connecting to directory mirror" - - Tor sends this event as soon as Tor has chosen a directory mirror -- - e.g. one of the authorities if bootstrapping for the first time or - after a long downtime, or one of the relays listed in its cached - directory information otherwise. - - Tor will stay at this phase until it has successfully established - a TCP connection with some directory mirror. Problems in this phase - generally happen because Tor doesn't have a network connection, or - because the local firewall is dropping SYN packets. - - Phase 10: - tag=handshake_dir summary="Finishing handshake with directory mirror" - - This event occurs when Tor establishes a TCP connection with a relay used - as a directory mirror (or its https proxy if it's using one). Tor remains - in this phase until the TLS handshake with the relay is finished. - - Problems in this phase generally happen because Tor's firewall is - doing more sophisticated MITM attacks on it, or doing packet-level - keyword recognition of Tor's handshake. - - Phase 15: - tag=onehop_create summary="Establishing one-hop circuit for dir info" - - Once TLS is finished with a relay, Tor will send a CREATE_FAST cell - to establish a one-hop circuit for retrieving directory information. - It will remain in this phase until it receives the CREATED_FAST cell - back, indicating that the circuit is ready. - - Phase 20: - tag=requesting_status summary="Asking for networkstatus consensus" - - Once we've finished our one-hop circuit, we will start a new stream - for fetching the networkstatus consensus. We'll stay in this phase - until we get the 'connected' relay cell back, indicating that we've - established a directory connection. - - Phase 25: - tag=loading_status summary="Loading networkstatus consensus" - - Once we've established a directory connection, we will start fetching - the networkstatus consensus document. This could take a while; this - phase is a good opportunity for using the "progress" keyword to indicate - partial progress. - - This phase could stall if the directory mirror we picked doesn't - have a copy of the networkstatus consensus so we have to ask another, - or it does give us a copy but we don't find it valid. - - Phase 40: - tag=loading_keys summary="Loading authority key certs" - - Sometimes when we've finished loading the networkstatus consensus, - we find that we don't have all the authority key certificates for the - keys that signed the consensus. At that point we put the consensus we - fetched on hold and fetch the keys so we can verify the signatures. - - Phase 45 - tag=requesting_descriptors summary="Asking for relay descriptors" - - Once we have a valid networkstatus consensus and we've checked all - its signatures, we start asking for relay descriptors. We stay in this - phase until we have received a 'connected' relay cell in response to - a request for descriptors. - - Phase 50: - tag=loading_descriptors summary="Loading relay descriptors" - - We will ask for relay descriptors from several different locations, - so this step will probably make up the bulk of the bootstrapping, - especially for users with slow connections. We stay in this phase until - we have descriptors for at least 1/4 of the usable relays listed in - the networkstatus consensus. This phase is also a good opportunity to - use the "progress" keyword to indicate partial steps. - - Phase 80: - tag=conn_or summary="Connecting to entry guard" - - Once we have a valid consensus and enough relay descriptors, we choose - some entry guards and start trying to build some circuits. This step - is similar to the "conn_dir" phase above; the only difference is - the context. - - If a Tor starts with enough recent cached directory information, - its first bootstrap status event will be for the conn_or phase. - - Phase 85: - tag=handshake_or summary="Finishing handshake with entry guard" - - This phase is similar to the "handshake_dir" phase, but it gets reached - if we finish a TCP connection to a Tor relay and we have already reached - the "conn_or" phase. We'll stay in this phase until we complete a TLS - handshake with a Tor relay. - - Phase 90: - tag=circuit_create summary="Establishing circuits" - - Once we've finished our TLS handshake with an entry guard, we will - set about trying to make some 3-hop circuits in case we need them soon. - - Phase 100: - tag=done summary="Done" - - A full 3-hop exit circuit has been established. Tor is ready to handle - application connections now. - diff --git a/orchid/doc/spec/dir-spec.txt b/orchid/doc/spec/dir-spec.txt deleted file mode 100644 index faa3a660..00000000 --- a/orchid/doc/spec/dir-spec.txt +++ /dev/null @@ -1,2132 +0,0 @@ - - Tor directory protocol, version 3 - -0. Scope and preliminaries - - This directory protocol is used by Tor version 0.2.0.x-alpha and later. - See dir-spec-v1.txt for information on the protocol used up to the - 0.1.0.x series, and dir-spec-v2.txt for information on the protocol - used by the 0.1.1.x and 0.1.2.x series. - - Caches and authorities must still support older versions of the - directory protocols, until the versions of Tor that require them are - finally out of commission. See Section XXXX on backward compatibility. - - This document merges and supersedes the following proposals: - - 101 Voting on the Tor Directory System - 103 Splitting identity key from regularly used signing key - 104 Long and Short Router Descriptors - - AS OF 14 JUNE 2007, THIS SPECIFICATION HAS NOT YET BEEN COMPLETELY - IMPLEMENTED, OR COMPLETELY COMPLETED. - - XXX when to download certificates. - XXX timeline - XXX fill in XXXXs - -0.1. History - - The earliest versions of Onion Routing shipped with a list of known - routers and their keys. When the set of routers changed, users needed to - fetch a new list. - - The Version 1 Directory protocol - -------------------------------- - - Early versions of Tor (0.0.2) introduced "Directory authorities": servers - that served signed "directory" documents containing a list of signed - "router descriptors", along with short summary of the status of each - router. Thus, clients could get up-to-date information on the state of - the network automatically, and be certain that the list they were getting - was attested by a trusted directory authority. - - Later versions (0.0.8) added directory caches, which download - directories from the authorities and serve them to clients. Non-caches - fetch from the caches in preference to fetching from the authorities, thus - distributing bandwidth requirements. - - Also added during the version 1 directory protocol were "router status" - documents: short documents that listed only the up/down status of the - routers on the network, rather than a complete list of all the - descriptors. Clients and caches would fetch these documents far more - frequently than they would fetch full directories. - - The Version 2 Directory Protocol - -------------------------------- - - During the Tor 0.1.1.x series, Tor revised its handling of directory - documents in order to address two major problems: - - * Directories had grown quite large (over 1MB), and most directory - downloads consisted mainly of router descriptors that clients - already had. - - * Every directory authority was a trust bottleneck: if a single - directory authority lied, it could make clients believe for a time - an arbitrarily distorted view of the Tor network. (Clients - trusted the most recent signed document they downloaded.) Thus, - adding more authorities would make the system less secure, not - more. - - To address these, we extended the directory protocol so that - authorities now published signed "network status" documents. Each - network status listed, for every router in the network: a hash of its - identity key, a hash of its most recent descriptor, and a summary of - what the authority believed about its status. Clients would download - the authorities' network status documents in turn, and believe - statements about routers iff they were attested to by more than half of - the authorities. - - Instead of downloading all router descriptors at once, clients - downloaded only the descriptors that they did not have. Descriptors - were indexed by their digests, in order to prevent malicious caches - from giving different versions of a router descriptor to different - clients. - - Routers began working harder to upload new descriptors only when their - contents were substantially changed. - - -0.2. Goals of the version 3 protocol - - Version 3 of the Tor directory protocol tries to solve the following - issues: - - * A great deal of bandwidth used to transmit router descriptors was - used by two fields that are not actually used by Tor routers - (namely read-history and write-history). We save about 60% by - moving them into a separate document that most clients do not - fetch or use. - - * It was possible under certain perverse circumstances for clients - to download an unusual set of network status documents, thus - partitioning themselves from clients who have a more recent and/or - typical set of documents. Even under the best of circumstances, - clients were sensitive to the ages of the network status documents - they downloaded. Therefore, instead of having the clients - correlate multiple network status documents, we have the - authorities collectively vote on a single consensus network status - document. - - * The most sensitive data in the entire network (the identity keys - of the directory authorities) needed to be stored unencrypted so - that the authorities can sign network-status documents on the fly. - Now, the authorities' identity keys are stored offline, and used - to certify medium-term signing keys that can be rotated. - -0.3. Some Remaining questions - - Things we could solve on a v3 timeframe: - - The SHA-1 hash is showing its age. We should do something about our - dependency on it. We could probably future-proof ourselves here in - this revision, at least so far as documents from the authorities are - concerned. - - Too many things about the authorities are hardcoded by IP. - - Perhaps we should start accepting longer identity keys for routers - too. - - Things to solve eventually: - - Requiring every client to know about every router won't scale forever. - - Requiring every directory cache to know every router won't scale - forever. - - -1. Outline - - There is a small set (say, around 5-10) of semi-trusted directory - authorities. A default list of authorities is shipped with the Tor - software. Users can change this list, but are encouraged not to do so, - in order to avoid partitioning attacks. - - Every authority has a very-secret, long-term "Authority Identity Key". - This is stored encrypted and/or offline, and is used to sign "key - certificate" documents. Every key certificate contains a medium-term - (3-12 months) "authority signing key", that is used by the authority to - sign other directory information. (Note that the authority identity - key is distinct from the router identity key that the authority uses - in its role as an ordinary router.) - - Routers periodically upload signed "routers descriptors" to the - directory authorities describing their keys, capabilities, and other - information. Routers may also upload signed "extra info documents" - containing information that is not required for the Tor protocol. - Directory authorities serve router descriptors indexed by router - identity, or by hash of the descriptor. - - Routers may act as directory caches to reduce load on the directory - authorities. They announce this in their descriptors. - - Periodically, each directory authority generates a view of - the current descriptors and status for known routers. They send a - signed summary of this view (a "status vote") to the other - authorities. The authorities compute the result of this vote, and sign - a "consensus status" document containing the result of the vote. - - Directory caches download, cache, and re-serve consensus documents. - - Clients, directory caches, and directory authorities all use consensus - documents to find out when their list of routers is out-of-date. - (Directory authorities also use vote statuses.) If it is, they download - any missing router descriptors. Clients download missing descriptors - from caches; caches and authorities download from authorities. - Descriptors are downloaded by the hash of the descriptor, not by the - server's identity key: this prevents servers from attacking clients by - giving them descriptors nobody else uses. - - All directory information is uploaded and downloaded with HTTP. - - [Authorities also generate and caches also cache documents produced and - used by earlier versions of this protocol; see section XXX for notes.] - -1.1. What's different from version 2? - - Clients used to download multiple network status documents, - corresponding roughly to "status votes" above. They would compute the - result of the vote on the client side. - - Authorities used to sign documents using the same private keys they used - for their roles as routers. This forced them to keep these extremely - sensitive keys in memory unencrypted. - - All of the information in extra-info documents used to be kept in the - main descriptors. - -1.2. Document meta-format - - Router descriptors, directories, and running-routers documents all obey the - following lightweight extensible information format. - - The highest level object is a Document, which consists of one or more - Items. Every Item begins with a KeywordLine, followed by zero or more - Objects. A KeywordLine begins with a Keyword, optionally followed by - whitespace and more non-newline characters, and ends with a newline. A - Keyword is a sequence of one or more characters in the set [A-Za-z0-9-]. - An Object is a block of encoded data in pseudo-Open-PGP-style - armor. (cf. RFC 2440) - - More formally: - - NL = The ascii LF character (hex value 0x0a). - Document ::= (Item | NL)+ - Item ::= KeywordLine Object* - KeywordLine ::= Keyword NL | Keyword WS ArgumentChar+ NL - Keyword = KeywordChar+ - KeywordChar ::= 'A' ... 'Z' | 'a' ... 'z' | '0' ... '9' | '-' - ArgumentChar ::= any printing ASCII character except NL. - WS = (SP | TAB)+ - Object ::= BeginLine Base-64-encoded-data EndLine - BeginLine ::= "-----BEGIN " Keyword "-----" NL - EndLine ::= "-----END " Keyword "-----" NL - - The BeginLine and EndLine of an Object must use the same keyword. - - When interpreting a Document, software MUST ignore any KeywordLine that - starts with a keyword it doesn't recognize; future implementations MUST NOT - require current clients to understand any KeywordLine not currently - described. - - The "opt" keyword was used until Tor 0.1.2.5-alpha for non-critical future - extensions. All implementations MUST ignore any item of the form "opt - keyword ....." when they would not recognize "keyword ....."; and MUST - treat "opt keyword ....." as synonymous with "keyword ......" when keyword - is recognized. - - Implementations before 0.1.2.5-alpha rejected any document with a - KeywordLine that started with a keyword that they didn't recognize. - When generating documents that need to be read by older versions of Tor, - implementations MUST prefix items not recognized by older versions of - Tor with an "opt" until those versions of Tor are obsolete. [Note that - key certificates, status vote documents, extra info documents, and - status consensus documents will never be read by older versions of Tor.] - - Other implementations that want to extend Tor's directory format MAY - introduce their own items. The keywords for extension items SHOULD start - with the characters "x-" or "X-", to guarantee that they will not conflict - with keywords used by future versions of Tor. - - In our document descriptions below, we tag Items with a multiplicity in - brackets. Possible tags are: - - "At start, exactly once": These items MUST occur in every instance of - the document type, and MUST appear exactly once, and MUST be the - first item in their documents. - - "Exactly once": These items MUST occur exactly one time in every - instance of the document type. - - "At end, exactly once": These items MUST occur in every instance of - the document type, and MUST appear exactly once, and MUST be the - last item in their documents. - - "At most once": These items MAY occur zero or one times in any - instance of the document type, but MUST NOT occur more than once. - - "Any number": These items MAY occur zero, one, or more times in any - instance of the document type. - - "Once or more": These items MUST occur at least once in any instance - of the document type, and MAY occur more. - -1.3. Signing documents - - Every signable document below is signed in a similar manner, using a - given "Initial Item", a final "Signature Item", a digest algorithm, and - a signing key. - - The Initial Item must be the first item in the document. - - The Signature Item has the following format: - - [arguments] NL SIGNATURE NL - - The "SIGNATURE" Object contains a signature (using the signing key) of - the PKCS1-padded digest of the entire document, taken from the - beginning of the Initial item, through the newline after the Signature - Item's keyword and its arguments. - - Unless otherwise, the digest algorithm is SHA-1. - - All documents are invalid unless signed with the correct signing key. - - The "Digest" of a document, unless stated otherwise, is its digest *as - signed by this signature scheme*. - -1.4. Voting timeline - - Every consensus document has a "valid-after" (VA) time, a "fresh-until" - (FU) time and a "valid-until" (VU) time. VA MUST precede FU, which MUST - in turn precede VU. Times are chosen so that every consensus will be - "fresh" until the next consensus becomes valid, and "valid" for a while - after. At least 3 consensuses should be valid at any given time. - - The timeline for a given consensus is as follows: - - VA-DistSeconds-VoteSeconds: The authorities exchange votes. - - VA-DistSeconds-VoteSeconds/2: The authorities try to download any - votes they don't have. - - VA-DistSeconds: The authorities calculate the consensus and exchange - signatures. - - VA-DistSeconds/2: The authorities try to download any signatures - they don't have. - - VA: All authorities have a multiply signed consensus. - - VA ... FU: Caches download the consensus. (Note that since caches have - no way of telling what VA and FU are until they have downloaded - the consensus, they assume that the present consensus's VA is - equal to the previous one's FU, and that its FU is one interval after - that.) - - FU: The consensus is no longer the freshest consensus. - - FU ... (the current consensus's VU): Clients download the consensus. - (See note above: clients guess that the next consensus's FU will be - two intervals after the current VA.) - - VU: The consensus is no longer valid. - - VoteSeconds and DistSeconds MUST each be at least 20 seconds; FU-VA and - VU-FU MUST each be at least 5 minutes. - -2. Router operation and formats - - ORs SHOULD generate a new router descriptor and a new extra-info - document whenever any of the following events have occurred: - - - A period of time (18 hrs by default) has passed since the last - time a descriptor was generated. - - - A descriptor field other than bandwidth or uptime has changed. - - - Bandwidth has changed by a factor of 2 from the last time a - descriptor was generated, and at least a given interval of time - (20 mins by default) has passed since then. - - - Its uptime has been reset (by restarting). - - [XXX this list is incomplete; see router_differences_are_cosmetic() - in routerlist.c for others] - - ORs SHOULD NOT publish a new router descriptor or extra-info document - if none of the above events have occurred and not much time has passed - (12 hours by default). - - After generating a descriptor, ORs upload them to every directory - authority they know, by posting them (in order) to the URL - - http:///tor/ - -2.1. Router descriptor format - - Router descriptors consist of the following items. For backward - compatibility, there should be an extra NL at the end of each router - descriptor. - - In lines that take multiple arguments, extra arguments SHOULD be - accepted and ignored. Many of the nonterminals below are defined in - section 2.3. - - "router" nickname address ORPort SOCKSPort DirPort NL - - [At start, exactly once.] - - Indicates the beginning of a router descriptor. "nickname" must be a - valid router nickname as specified in 2.3. "address" must be an IPv4 - address in dotted-quad format. The last three numbers indicate the - TCP ports at which this OR exposes functionality. ORPort is a port at - which this OR accepts TLS connections for the main OR protocol; - SOCKSPort is deprecated and should always be 0; and DirPort is the - port at which this OR accepts directory-related HTTP connections. If - any port is not supported, the value 0 is given instead of a port - number. (At least one of DirPort and ORPort SHOULD be set; - authorities MAY reject any descriptor with both DirPort and ORPort of - 0.) - - "bandwidth" bandwidth-avg bandwidth-burst bandwidth-observed NL - - [Exactly once] - - Estimated bandwidth for this router, in bytes per second. The - "average" bandwidth is the volume per second that the OR is willing to - sustain over long periods; the "burst" bandwidth is the volume that - the OR is willing to sustain in very short intervals. The "observed" - value is an estimate of the capacity this server can handle. The - server remembers the max bandwidth sustained output over any ten - second period in the past day, and another sustained input. The - "observed" value is the lesser of these two numbers. - - "platform" string NL - - [At most once] - - A human-readable string describing the system on which this OR is - running. This MAY include the operating system, and SHOULD include - the name and version of the software implementing the Tor protocol. - - "published" YYYY-MM-DD HH:MM:SS NL - - [Exactly once] - - The time, in GMT, when this descriptor (and its corresponding - extra-info document if any) was generated. - - "fingerprint" fingerprint NL - - [At most once] - - A fingerprint (a HASH_LEN-byte of asn1 encoded public key, encoded in - hex, with a single space after every 4 characters) for this router's - identity key. A descriptor is considered invalid (and MUST be - rejected) if the fingerprint line does not match the public key. - - [We didn't start parsing this line until Tor 0.1.0.6-rc; it should - be marked with "opt" until earlier versions of Tor are obsolete.] - - "hibernating" bool NL - - [At most once] - - If the value is 1, then the Tor server was hibernating when the - descriptor was published, and shouldn't be used to build circuits. - - [We didn't start parsing this line until Tor 0.1.0.6-rc; it should be - marked with "opt" until earlier versions of Tor are obsolete.] - - "uptime" number NL - - [At most once] - - The number of seconds that this OR process has been running. - - "onion-key" NL a public key in PEM format - - [Exactly once] - - This key is used to encrypt EXTEND cells for this OR. The key MUST be - accepted for at least 1 week after any new key is published in a - subsequent descriptor. It MUST be 1024 bits. - - "signing-key" NL a public key in PEM format - - [Exactly once] - - The OR's long-term identity key. It MUST be 1024 bits. - - "accept" exitpattern NL - "reject" exitpattern NL - - [Any number] - - These lines describe an "exit policy": the rules that an OR follows - when deciding whether to allow a new stream to a given address. The - 'exitpattern' syntax is described below. There MUST be at least one - such entry. The rules are considered in order; if no rule matches, - the address will be accepted. For clarity, the last such entry SHOULD - be accept *:* or reject *:*. - - "router-signature" NL Signature NL - - [At end, exactly once] - - The "SIGNATURE" object contains a signature of the PKCS1-padded - hash of the entire router descriptor, taken from the beginning of the - "router" line, through the newline after the "router-signature" line. - The router descriptor is invalid unless the signature is performed - with the router's identity key. - - "contact" info NL - - [At most once] - - Describes a way to contact the server's administrator, preferably - including an email address and a PGP key fingerprint. - - "family" names NL - - [At most once] - - 'Names' is a space-separated list of server nicknames or - hexdigests. If two ORs list one another in their "family" entries, - then OPs should treat them as a single OR for the purpose of path - selection. - - For example, if node A's descriptor contains "family B", and node B's - descriptor contains "family A", then node A and node B should never - be used on the same circuit. - - "read-history" YYYY-MM-DD HH:MM:SS (NSEC s) NUM,NUM,NUM,NUM,NUM... NL - [At most once] - "write-history" YYYY-MM-DD HH:MM:SS (NSEC s) NUM,NUM,NUM,NUM,NUM... NL - [At most once] - - Declare how much bandwidth the OR has used recently. Usage is divided - into intervals of NSEC seconds. The YYYY-MM-DD HH:MM:SS field - defines the end of the most recent interval. The numbers are the - number of bytes used in the most recent intervals, ordered from - oldest to newest. - - [We didn't start parsing these lines until Tor 0.1.0.6-rc; they should - be marked with "opt" until earlier versions of Tor are obsolete.] - - [See also migration notes in section 2.2.1.] - - "eventdns" bool NL - - [At most once] - - Declare whether this version of Tor is using the newer enhanced - dns logic. Versions of Tor with this field set to false SHOULD NOT - be used for reverse hostname lookups. - - [All versions of Tor before 0.1.2.2-alpha should be assumed to have - this option set to 0 if it is not present. All Tor versions at - 0.1.2.2-alpha or later should be assumed to have this option set to - 1 if it is not present. Until 0.1.2.1-alpha-dev, this option was - not generated, even when the new DNS code was in use. Versions of Tor - before 0.1.2.1-alpha-dev did not parse this option, so it should be - marked "opt". The dnsworker logic has been removed, so this option - should not be used by new server code. However, it can still be - used, and should still be recognized by new code until Tor 0.1.2.x - is obsolete.] - - "caches-extra-info" NL - - [At most once.] - - Present only if this router is a directory cache that provides - extra-info documents. - - [Versions before 0.2.0.1-alpha don't recognize this, and versions - before 0.1.2.5-alpha will reject descriptors containing it unless - it is prefixed with "opt"; it should be so prefixed until these - versions are obsolete.] - - "extra-info-digest" digest NL - - [At most once] - - "Digest" is a hex-encoded digest (using upper-case characters) of the - router's extra-info document, as signed in the router's extra-info - (that is, not including the signature). (If this field is absent, the - router is not uploading a corresponding extra-info document.) - - [Versions before 0.2.0.1-alpha don't recognize this, and versions - before 0.1.2.5-alpha will reject descriptors containing it unless - it is prefixed with "opt"; it should be so prefixed until these - versions are obsolete.] - - "hidden-service-dir" *(SP VersionNum) NL - - [At most once.] - - Present only if this router stores and serves hidden service - descriptors. If any VersionNum(s) are specified, this router - supports those descriptor versions. If none are specified, it - defaults to version 2 descriptors. - - [Versions of Tor before 0.1.2.5-alpha rejected router descriptors - with unrecognized items; the protocols line should be preceded with - an "opt" until these Tors are obsolete.] - - "protocols" SP "Link" SP LINK-VERSION-LIST SP "Circuit" SP - CIRCUIT-VERSION-LIST NL - - [At most once.] - - Both lists are space-separated sequences of numbers, to indicate which - protocols the server supports. As of 30 Mar 2008, specified - protocols are "Link 1 2 Circuit 1". See section 4.1 of tor-spec.txt - for more information about link protocol versions. - - [Versions of Tor before 0.1.2.5-alpha rejected router descriptors - with unrecognized items; the protocols line should be preceded with - an "opt" until these Tors are obsolete.] - - "allow-single-hop-exits" - - [At most once.] - - Present only if the router allows single-hop circuits to make exit - connections. Most Tor servers do not support this: this is - included for specialized controllers designed to support perspective - access and such. - - -2.2. Extra-info documents - - Extra-info documents consist of the following items: - - "extra-info" Nickname Fingerprint NL - [At start, exactly once.] - - Identifies what router this is an extra info descriptor for. - Fingerprint is encoded in hex (using upper-case letters), with - no spaces. - - "published" - - [Exactly once.] - - The time, in GMT, when this document (and its corresponding router - descriptor if any) was generated. It MUST match the published time - in the corresponding router descriptor. - - "read-history" YYYY-MM-DD HH:MM:SS (NSEC s) NUM,NUM,NUM,NUM,NUM... NL - [At most once.] - "write-history" YYYY-MM-DD HH:MM:SS (NSEC s) NUM,NUM,NUM,NUM,NUM... NL - [At most once.] - - As documented in 2.1 above. See migration notes in section 2.2.1. - - "geoip-start" YYYY-MM-DD HH:MM:SS NL - "geoip-client-origins" CC=N,CC=N,... NL - - Only generated by bridge routers (see blocking.pdf), and only - when they have been configured with a geoip database. - Non-bridges SHOULD NOT generate these fields. Contains a list - of mappings from two-letter country codes (CC) to the number - of clients that have connected to that bridge from that - country (approximate, and rounded up to the nearest multiple of 8 - in order to hamper traffic analysis). A country is included - only if it has at least one address. The time in - "geoip-start" is the time at which we began collecting geoip - statistics. - - "dirreq-stats-end" YYYY-MM-DD HH:MM:SS (NSEC s) NL - [At most once.] - - YYYY-MM-DD HH:MM:SS defines the end of the included measurement - interval of length NSEC seconds (86400 seconds by default). - - A "dirreq-stats-end" line, as well as any other "dirreq-*" line, - is only added when the relay has opened its Dir port and after 24 - hours of measuring directory requests. - - "dirreq-v2-ips" CC=N,CC=N,... NL - [At most once.] - "dirreq-v3-ips" CC=N,CC=N,... NL - [At most once.] - - List of mappings from two-letter country codes to the number of - unique IP addresses that have connected from that country to - request a v2/v3 network status, rounded up to the nearest multiple - of 8. Only those IP addresses are counted that the directory can - answer with a 200 OK status code. - - "dirreq-v2-reqs" CC=N,CC=N,... NL - [At most once.] - "dirreq-v3-reqs" CC=N,CC=N,... NL - [At most once.] - - List of mappings from two-letter country codes to the number of - requests for v2/v3 network statuses from that country, rounded up - to the nearest multiple of 8. Only those requests are counted that - the directory can answer with a 200 OK status code. - - "dirreq-v2-share" num% NL - [At most once.] - "dirreq-v3-share" num% NL - [At most once.] - - The share of v2/v3 network status requests that the directory - expects to receive from clients based on its advertised bandwidth - compared to the overall network bandwidth capacity. Shares are - formatted in percent with two decimal places. Shares are - calculated as means over the whole 24-hour interval. - - "dirreq-v2-resp" status=num,... NL - [At most once.] - "dirreq-v3-resp" status=nul,... NL - [At most once.] - - List of mappings from response statuses to the number of requests - for v2/v3 network statuses that were answered with that response - status, rounded up to the nearest multiple of 4. Only response - statuses with at least 1 response are reported. New response - statuses can be added at any time. The current list of response - statuses is as follows: - - "ok": a network status request is answered; this number - corresponds to the sum of all requests as reported in - "dirreq-v2-reqs" or "dirreq-v3-reqs", respectively, before - rounding up. - "not-enough-sigs: a version 3 network status is not signed by a - sufficient number of requested authorities. - "unavailable": a requested network status object is unavailable. - "not-found": a requested network status is not found. - "not-modified": a network status has not been modified since the - If-Modified-Since time that is included in the request. - "busy": the directory is busy. - - "dirreq-v2-direct-dl" key=val,... NL - [At most once.] - "dirreq-v3-direct-dl" key=val,... NL - [At most once.] - "dirreq-v2-tunneled-dl" key=val,... NL - [At most once.] - "dirreq-v3-tunneled-dl" key=val,... NL - [At most once.] - - List of statistics about possible failures in the download process - of v2/v3 network statuses. Requests are either "direct" - HTTP-encoded requests over the relay's directory port, or - "tunneled" requests using a BEGIN_DIR cell over the relay's OR - port. The list of possible statistics can change, and statistics - can be left out from reporting. The current list of statistics is - as follows: - - Successful downloads and failures: - - "complete": a client has finished the download successfully. - "timeout": a download did not finish within 10 minutes after - starting to send the response. - "running": a download is still running at the end of the - measurement period for less than 10 minutes after starting to - send the response. - - Download times: - - "min", "max": smallest and largest measured bandwidth in B/s. - "d[1-4,6-9]": 1st to 4th and 6th to 9th decile of measured - bandwidth in B/s. For a given decile i, i/10 of all downloads - had a smaller bandwidth than di, and (10-i)/10 of all downloads - had a larger bandwidth than di. - "q[1,3]": 1st and 3rd quartile of measured bandwidth in B/s. One - fourth of all downloads had a smaller bandwidth than q1, one - fourth of all downloads had a larger bandwidth than q3, and the - remaining half of all downloads had a bandwidth between q1 and - q3. - "md": median of measured bandwidth in B/s. Half of the downloads - had a smaller bandwidth than md, the other half had a larger - bandwidth than md. - - "entry-stats-end" YYYY-MM-DD HH:MM:SS (NSEC s) NL - [At most once.] - - YYYY-MM-DD HH:MM:SS defines the end of the included measurement - interval of length NSEC seconds (86400 seconds by default). - - An "entry-stats-end" line, as well as any other "entry-*" - line, is first added after the relay has been running for at least - 24 hours. - - "entry-ips" CC=N,CC=N,... NL - [At most once.] - - List of mappings from two-letter country codes to the number of - unique IP addresses that have connected from that country to the - relay and which are no known other relays, rounded up to the - nearest multiple of 8. - - "cell-stats-end" YYYY-MM-DD HH:MM:SS (NSEC s) NL - [At most once.] - - YYYY-MM-DD HH:MM:SS defines the end of the included measurement - interval of length NSEC seconds (86400 seconds by default). - - A "cell-stats-end" line, as well as any other "cell-*" line, - is first added after the relay has been running for at least 24 - hours. - - "cell-processed-cells" num,...,num NL - [At most once.] - - Mean number of processed cells per circuit, subdivided into - deciles of circuits by the number of cells they have processed in - descending order from loudest to quietest circuits. - - "cell-queued-cells" num,...,num NL - [At most once.] - - Mean number of cells contained in queues by circuit decile. These - means are calculated by 1) determining the mean number of cells in - a single circuit between its creation and its termination and 2) - calculating the mean for all circuits in a given decile as - determined in "cell-processed-cells". Numbers have a precision of - two decimal places. - - "cell-time-in-queue" num,...,num NL - [At most once.] - - Mean time cells spend in circuit queues in milliseconds. Times are - calculated by 1) determining the mean time cells spend in the - queue of a single circuit and 2) calculating the mean for all - circuits in a given decile as determined in - "cell-processed-cells". - - "cell-circuits-per-decile" num NL - [At most once.] - - Mean number of circuits that are included in any of the deciles, - rounded up to the next integer. - - "exit-stats-end" YYYY-MM-DD HH:MM:SS (NSEC s) NL - [At most once.] - - YYYY-MM-DD HH:MM:SS defines the end of the included measurement - interval of length NSEC seconds (86400 seconds by default). - - An "exit-stats-end" line, as well as any other "exit-*" line, is - first added after the relay has been running for at least 24 hours - and only if the relay permits exiting (where exiting to a single - port and IP address is sufficient). - - "exit-kibibytes-written" port=N,port=N,... NL - [At most once.] - "exit-kibibytes-read" port=N,port=N,... NL - [At most once.] - - List of mappings from ports to the number of kibibytes that the - relay has written to or read from exit connections to that port, - rounded up to the next full kibibyte. - - "exit-streams-opened" port=N,port=N,... NL - [At most once.] - - List of mappings from ports to the number of opened exit streams - to that port, rounded up to the nearest multiple of 4. - - "router-signature" NL Signature NL - [At end, exactly once.] - - A document signature as documented in section 1.3, using the - initial item "extra-info" and the final item "router-signature", - signed with the router's identity key. - -2.2.1. Moving history fields to extra-info documents. - - Tools that want to use the read-history and write-history values SHOULD - download extra-info documents as well as router descriptors. Such - tools SHOULD accept history values from both sources; if they appear in - both documents, the values in the extra-info documents are authoritative. - - New versions of Tor no longer generate router descriptors - containing read-history or write-history. Tools should continue to - accept read-history and write-history values in router descriptors - produced by older versions of Tor until all Tor versions earlier - than 0.2.0.x are obsolete. - -2.3. Nonterminals in router descriptors - - nickname ::= between 1 and 19 alphanumeric characters ([A-Za-z0-9]), - case-insensitive. - hexdigest ::= a '$', followed by 40 hexadecimal characters - ([A-Fa-f0-9]). [Represents a server by the digest of its identity - key.] - - exitpattern ::= addrspec ":" portspec - portspec ::= "*" | port | port "-" port - port ::= an integer between 1 and 65535, inclusive. - - [Some implementations incorrectly generate ports with value 0. - Implementations SHOULD accept this, and SHOULD NOT generate it. - Connections to port 0 are never permitted.] - - addrspec ::= "*" | ip4spec | ip6spec - ipv4spec ::= ip4 | ip4 "/" num_ip4_bits | ip4 "/" ip4mask - ip4 ::= an IPv4 address in dotted-quad format - ip4mask ::= an IPv4 mask in dotted-quad format - num_ip4_bits ::= an integer between 0 and 32 - ip6spec ::= ip6 | ip6 "/" num_ip6_bits - ip6 ::= an IPv6 address, surrounded by square brackets. - num_ip6_bits ::= an integer between 0 and 128 - - bool ::= "0" | "1" - -3. Formats produced by directory authorities. - - Every authority has two keys used in this protocol: a signing key, and - an authority identity key. (Authorities also have a router identity - key used in their role as a router and by earlier versions of the - directory protocol.) The identity key is used from time to time to - sign new key certificates using new signing keys; it is very sensitive. - The signing key is used to sign key certificates and status documents. - - There are three kinds of documents generated by directory authorities: - - Key certificates - Status votes - Status consensuses - - Each is discussed below. - -3.1. Key certificates - - Key certificates consist of the following items: - - "dir-key-certificate-version" version NL - - [At start, exactly once.] - - Determines the version of the key certificate. MUST be "3" for - the protocol described in this document. Implementations MUST - reject formats they don't understand. - - "dir-address" IPPort NL - [At most once] - - An IP:Port for this authority's directory port. - - "fingerprint" fingerprint NL - - [Exactly once.] - - Hexadecimal encoding without spaces based on the authority's - identity key. - - "dir-identity-key" NL a public key in PEM format - - [Exactly once.] - - The long-term authority identity key for this authority. This key - SHOULD be at least 2048 bits long; it MUST NOT be shorter than - 1024 bits. - - "dir-key-published" YYYY-MM-DD HH:MM:SS NL - - [Exactly once.] - - The time (in GMT) when this document and corresponding key were - last generated. - - "dir-key-expires" YYYY-MM-DD HH:MM:SS NL - - [Exactly once.] - - A time (in GMT) after which this key is no longer valid. - - "dir-signing-key" NL a key in PEM format - - [Exactly once.] - - The directory server's public signing key. This key MUST be at - least 1024 bits, and MAY be longer. - - "dir-key-crosscert" NL CrossSignature NL - - [At most once.] - - NOTE: Authorities MUST include this field in all newly generated - certificates. A future version of this specification will make - the field required. - - CrossSignature is a signature, made using the certificate's signing - key, of the digest of the PKCS1-padded hash of the certificate's - identity key. For backward compatibility with broken versions of the - parser, we wrap the base64-encoded signature in -----BEGIN ID - SIGNATURE---- and -----END ID SIGNATURE----- tags. Implementations - MUST allow the "ID " portion to be omitted, however. - - When encountering a certificate with a dir-key-crosscert entry, - implementations MUST verify that the signature is a correct signature - of the hash of the identity key using the signing key. - - "dir-key-certification" NL Signature NL - - [At end, exactly once.] - - A document signature as documented in section 1.3, using the - initial item "dir-key-certificate-version" and the final item - "dir-key-certification", signed with the authority identity key. - - Authorities MUST generate a new signing key and corresponding - certificate before the key expires. - -3.2. Vote and consensus status documents - - Votes and consensuses are more strictly formatted then other documents - in this specification, since different authorities must be able to - generate exactly the same consensus given the same set of votes. - - The procedure for deciding when to generate vote and consensus status - documents are described in section XXX below. - - Status documents contain a preamble, an authority section, a list of - router status entries, and one or more footer signature, in that order. - - Unlike other formats described above, a SP in these documents must be a - single space character (hex 20). - - Some items appear only in votes, and some items appear only in - consensuses. Unless specified, items occur in both. - - The preamble contains the following items. They MUST occur in the - order given here: - - "network-status-version" SP version NL. - - [At start, exactly once.] - - A document format version. For this specification, the version is - "3". - - "vote-status" SP type NL - - [Exactly once.] - - The status MUST be "vote" or "consensus", depending on the type of - the document. - - "consensus-methods" SP IntegerList NL - - [Exactly once for votes; does not occur in consensuses.] - - A space-separated list of supported methods for generating - consensuses from votes. See section 3.4.1 for details. Method "1" - MUST be included. - - "consensus-method" SP Integer NL - - [Exactly once for consensuses; does not occur in votes.] - - See section 3.4.1 for details. - - (Only included when the vote is generated with consensus-method 2 or - later.) - - "published" SP YYYY-MM-DD SP HH:MM:SS NL - - [Exactly once for votes; does not occur in consensuses.] - - The publication time for this status document (if a vote). - - "valid-after" SP YYYY-MM-DD SP HH:MM:SS NL - - [Exactly once.] - - The start of the Interval for this vote. Before this time, the - consensus document produced from this vote should not be used. - See 1.4 for voting timeline information. - - "fresh-until" SP YYYY-MM-DD SP HH:MM:SS NL - - [Exactly once.] - - The time at which the next consensus should be produced; before this - time, there is no point in downloading another consensus, since there - won't be a new one. See 1.4 for voting timeline information. - - "valid-until" SP YYYY-MM-DD SP HH:MM:SS NL - - [Exactly once.] - - The end of the Interval for this vote. After this time, the - consensus produced by this vote should not be used. See 1.4 for - voting timeline information. - - "voting-delay" SP VoteSeconds SP DistSeconds NL - - [Exactly once.] - - VoteSeconds is the number of seconds that we will allow to collect - votes from all authorities; DistSeconds is the number of seconds - we'll allow to collect signatures from all authorities. See 1.4 for - voting timeline information. - - "client-versions" SP VersionList NL - - [At most once.] - - A comma-separated list of recommended client versions, in - ascending order. If absent, no opinion is held about client - versions. - - "server-versions" SP VersionList NL - - [At most once.] - - A comma-separated list of recommended server versions, in - ascending order. If absent, no opinion is held about server - versions. - - "known-flags" SP FlagList NL - - [Exactly once.] - - A space-separated list of all of the flags that this document - might contain. A flag is "known" either because the authority - knows about them and might set them (if in a vote), or because - enough votes were counted for the consensus for an authoritative - opinion to have been formed about their status. - - "params" SP [Parameters] NL - - [At most once] - - Parameter ::= Keyword '=' Int32 - Int32 ::= A decimal integer between -2147483648 and 2147483647. - Parameters ::= Parameter | Parameters SP Parameter - - The parameters list, if present, contains a space-separated list of - key-value pairs, sorted in lexical order by their keyword. Each - parameter has its own meaning. - - (Only included when the vote is generated with consensus-method 7 or - later.) - - The authority section of a vote contains the following items, followed - in turn by the authority's current key certificate: - - "dir-source" SP nickname SP identity SP address SP IP SP dirport SP - orport NL - - [Exactly once, at start] - - Describes this authority. The nickname is a convenient identifier - for the authority. The identity is an uppercase hex fingerprint of - the authority's current (v3 authority) identity key. The address is - the server's hostname. The IP is the server's current IP address, - and dirport is its current directory port. XXXXorport - - "contact" SP string NL - - [At most once.] - - An arbitrary string describing how to contact the directory - server's administrator. Administrators should include at least an - email address and a PGP fingerprint. - - "legacy-key" SP FINGERPRINT NL - - [At most once] - - Lists a fingerprint for an obsolete _identity_ key still used - by this authority to keep older clients working. This option - is used to keep key around for a little while in case the - authorities need to migrate many identity keys at once. - (Generally, this would only happen because of a security - vulnerability that affected multiple authorities, like the - Debian OpenSSL RNG bug of May 2008.) - - The authority section of a consensus contains groups the following items, - in the order given, with one group for each authority that contributed to - the consensus, with groups sorted by authority identity digest: - - "dir-source" SP nickname SP identity SP address SP IP SP dirport SP - orport NL - - [Exactly once, at start] - - As in the authority section of a vote. - - "contact" SP string NL - - [At most once.] - - As in the authority section of a vote. - - "vote-digest" SP digest NL - - [Exactly once.] - - A digest of the vote from the authority that contributed to this - consensus, as signed (that is, not including the signature). - (Hex, upper-case.) - - Each router status entry contains the following items. Router status - entries are sorted in ascending order by identity digest. - - "r" SP nickname SP identity SP digest SP publication SP IP SP ORPort - SP DirPort NL - - [At start, exactly once.] - - "Nickname" is the OR's nickname. "Identity" is a hash of its - identity key, encoded in base64, with trailing equals sign(s) - removed. "Digest" is a hash of its most recent descriptor as - signed (that is, not including the signature), encoded in base64. - "Publication" is the - publication time of its most recent descriptor, in the form - YYYY-MM-DD HH:MM:SS, in GMT. "IP" is its current IP address; - ORPort is its current OR port, "DirPort" is it's current directory - port, or "0" for "none". - - "s" SP Flags NL - - [At most once.] - - A series of space-separated status flags, in alphabetical order. - Currently documented flags are: - - "Authority" if the router is a directory authority. - "BadExit" if the router is believed to be useless as an exit node - (because its ISP censors it, because it is behind a restrictive - proxy, or for some similar reason). - "BadDirectory" if the router is believed to be useless as a - directory cache (because its directory port isn't working, - its bandwidth is always throttled, or for some similar - reason). - "Exit" if the router is more useful for building - general-purpose exit circuits than for relay circuits. The - path building algorithm uses this flag; see path-spec.txt. - "Fast" if the router is suitable for high-bandwidth circuits. - "Guard" if the router is suitable for use as an entry guard. - "HSDir" if the router is considered a v2 hidden service directory. - "Named" if the router's identity-nickname mapping is canonical, - and this authority binds names. - "Stable" if the router is suitable for long-lived circuits. - "Running" if the router is currently usable. - "Unnamed" if another router has bound the name used by this - router, and this authority binds names. - "Valid" if the router has been 'validated'. - "V2Dir" if the router implements the v2 directory protocol. - "V3Dir" if the router implements this protocol. - - "v" SP version NL - - [At most once.] - - The version of the Tor protocol that this server is running. If - the value begins with "Tor" SP, the rest of the string is a Tor - version number, and the protocol is "The Tor protocol as supported - by the given version of Tor." Otherwise, if the value begins with - some other string, Tor has upgraded to a more sophisticated - protocol versioning system, and the protocol is "a version of the - Tor protocol more recent than any we recognize." - - Directory authorities SHOULD omit version strings they receive from - descriptors if they would cause "v" lines to be over 128 characters - long. - - "w" SP "Bandwidth=" INT [SP "Measured=" INT] NL - - [At most once.] - - An estimate of the bandwidth of this server, in an arbitrary - unit (currently kilobytes per second). Used to weight router - selection. - - Additionally, the Measured= keyword is present in votes by - participating bandwidth measurement authorites to indicate - a measured bandwidth currently produced by measuring stream - capacities. - - Other weighting keywords may be added later. - Clients MUST ignore keywords they do not recognize. - - "p" SP ("accept" / "reject") SP PortList NL - - [At most once.] - - PortList = PortOrRange - PortList = PortList "," PortOrRange - PortOrRange = INT "-" INT / INT - - A list of those ports that this router supports (if 'accept') - or does not support (if 'reject') for exit to "most - addresses". - - The signature section contains the following item, which appears - Exactly Once for a vote, and At Least Once for a consensus. - - "directory-signature" SP identity SP signing-key-digest NL Signature - - This is a signature of the status document, with the initial item - "network-status-version", and the signature item - "directory-signature", using the signing key. (In this case, we take - the hash through the _space_ after directory-signature, not the - newline: this ensures that all authorities sign the same thing.) - "identity" is the hex-encoded digest of the authority identity key of - the signing authority, and "signing-key-digest" is the hex-encoded - digest of the current authority signing key of the signing authority. - -3.3. Deciding how to vote. - - (This section describes how directory authorities choose which status - flags to apply to routers, as of Tor 0.2.0.0-alpha-dev. Later directory - authorities MAY do things differently, so long as clients keep working - well. Clients MUST NOT depend on the exact behaviors in this section.) - - In the below definitions, a router is considered "active" if it is - running, valid, and not hibernating. - - "Valid" -- a router is 'Valid' if it is running a version of Tor not - known to be broken, and the directory authority has not blacklisted - it as suspicious. - - "Named" -- Directory authority administrators may decide to support name - binding. If they do, then they must maintain a file of - nickname-to-identity-key mappings, and try to keep this file consistent - with other directory authorities. If they don't, they act as clients, and - report bindings made by other directory authorities (name X is bound to - identity Y if at least one binding directory lists it, and no directory - binds X to some other Y'.) A router is called 'Named' if the router - believes the given name should be bound to the given key. - - Two strategies exist on the current network for deciding on - values for the Named flag. In the original version, server - operators were asked to send nickname-identity pairs to a - mailing list of Naming directory authorities operators. The - operators were then supposed to add the pairs to their - mapping files; in practice, they didn't get to this often. - - Newer Naming authorities run a script that registers routers - in their mapping files once the routers have been online at - least two weeks, no other router has that nickname, and no - other router has wanted the nickname for a month. If a router - has not been online for six months, the router is removed. - - "Unnamed" -- Directory authorities that support naming should vote for a - router to be 'Unnamed' if its given nickname is mapped to a different - identity. - - "Running" -- A router is 'Running' if the authority managed to connect to - it successfully within the last 30 minutes. - - "Stable" -- A router is 'Stable' if it is active, and either its Weighted - MTBF is at least the median for known active routers or its Weighted MTBF - corresponds to at least 7 days. Routers are never called Stable if they are - running a version of Tor known to drop circuits stupidly. (0.1.1.10-alpha - through 0.1.1.16-rc are stupid this way.) - - To calculate weighted MTBF, compute the weighted mean of the lengths - of all intervals when the router was observed to be up, weighting - intervals by $\alpha^n$, where $n$ is the amount of time that has - passed since the interval ended, and $\alpha$ is chosen so that - measurements over approximately one month old no longer influence the - weighted MTBF much. - - [XXXX what happens when we have less than 4 days of MTBF info.] - - "Exit" -- A router is called an 'Exit' iff it allows exits to at - least two of the ports 80, 443, and 6667 and allows exits to at - least one /8 address space. - - "Fast" -- A router is 'Fast' if it is active, and its bandwidth is - either in the top 7/8ths for known active routers or at least 100KB/s. - - "Guard" -- A router is a possible 'Guard' if its Weighted Fractional - Uptime is at least the median for "familiar" active routers, and if - its bandwidth is at least median or at least 250KB/s. - If the total bandwidth of active non-BadExit Exit servers is less - than one third of the total bandwidth of all active servers, no Exit is - listed as a Guard. - - To calculate weighted fractional uptime, compute the fraction - of time that the router is up in any given day, weighting so that - downtime and uptime in the past counts less. - - A node is 'familiar' if 1/8 of all active nodes have appeared more - recently than it, OR it has been around for a few weeks. - - "Authority" -- A router is called an 'Authority' if the authority - generating the network-status document believes it is an authority. - - "V2Dir" -- A router supports the v2 directory protocol if it has an open - directory port, and it is running a version of the directory protocol that - supports the functionality clients need. (Currently, this is - 0.1.1.9-alpha or later.) - - "V3Dir" -- A router supports the v3 directory protocol if it has an open - directory port, and it is running a version of the directory protocol that - supports the functionality clients need. (Currently, this is - 0.2.0.?????-alpha or later.) - - "HSDir" -- A router is a v2 hidden service directory if it stores and - serves v2 hidden service descriptors and the authority managed to connect - to it successfully within the last 24 hours. - - Directory server administrators may label some servers or IPs as - blacklisted, and elect not to include them in their network-status lists. - - Authorities SHOULD 'disable' any servers in excess of 3 on any single IP. - When there are more than 3 to choose from, authorities should first prefer - authorities to non-authorities, then prefer Running to non-Running, and - then prefer high-bandwidth to low-bandwidth. To 'disable' a server, the - authority *should* advertise it without the Running or Valid flag. - - Thus, the network-status vote includes all non-blacklisted, - non-expired, non-superseded descriptors. - - The bandwidth in a "w" line should be taken as the best estimate - of the router's actual capacity that the authority has. For now, - this should be the lesser of the observed bandwidth and bandwidth - rate limit from the router descriptor. It is given in kilobytes - per second, and capped at some arbitrary value (currently 10 MB/s). - - The Measured= keyword on a "w" line vote is currently computed - by multiplying the previous published consensus bandwidth by the - ratio of the measured average node stream capacity to the network - average. If 3 or more authorities provide a Measured= keyword for - a router, the authorites produce a consensus containing a "w" - Bandwidth= keyword equal to the median of the Measured= votes. - - The ports listed in a "p" line should be taken as those ports for - which the router's exit policy permits 'most' addresses, ignoring any - accept not for all addresses, ignoring all rejects for private - netblocks. "Most" addresses are permitted if no more than 2^25 - IPv4 addresses (two /8 networks) were blocked. The list is encoded - as described in 3.4.2. - -3.4. Computing a consensus from a set of votes - - Given a set of votes, authorities compute the contents of the consensus - document as follows: - - The "valid-after", "valid-until", and "fresh-until" times are taken as - the median of the respective values from all the votes. - - The times in the "voting-delay" line are taken as the median of the - VoteSeconds and DistSeconds times in the votes. - - Known-flags is the union of all flags known by any voter. - - Entries are given on the "params" line for every keyword on which any - authority voted. The values given are the low-median of all votes on - that keyword. - - "client-versions" and "server-versions" are sorted in ascending - order; A version is recommended in the consensus if it is recommended - by more than half of the voting authorities that included a - client-versions or server-versions lines in their votes. - - The authority item groups (dir-source, contact, fingerprint, - vote-digest) are taken from the votes of the voting - authorities. These groups are sorted by the digests of the - authorities identity keys, in ascending order. If the consensus - method is 3 or later, a dir-source line must be included for - every vote with legacy-key entry, using the legacy-key's - fingerprint, the voter's ordinary nickname with the string - "-legacy" appended, and all other fields as from the original - vote's dir-source line. - - A router status entry: - * is included in the result if some router status entry with the same - identity is included by more than half of the authorities (total - authorities, not just those whose votes we have). - - * For any given identity, we include at most one router status entry. - - * A router entry has a flag set if that is included by more than half - of the authorities who care about that flag. - - * Two router entries are "the same" if they have the same - tuple. - We choose the tuple for a given router as whichever tuple appears - for that router in the most votes. We break ties first in favor of - the more recently published, then in favor of smaller server - descriptor digest. - - * The Named flag appears if it is included for this routerstatus by - _any_ authority, and if all authorities that list it list the same - nickname. However, if consensus-method 2 or later is in use, and - any authority calls this identity/nickname pair Unnamed, then - this routerstatus does not get the Named flag. - - * If consensus-method 2 or later is in use, the Unnamed flag is - set for a routerstatus if any authorities have voted for a different - identities to be Named with that nickname, or if any authority - lists that nickname/ID pair as Unnamed. - - (With consensus-method 1, Unnamed is set like any other flag.) - - * The version is given as whichever version is listed by the most - voters, with ties decided in favor of more recent versions. - - * If consensus-method 4 or later is in use, then routers that - do not have the Running flag are not listed at all. - - * If consensus-method 5 or later is in use, then the "w" line - is generated using a low-median of the bandwidth values from - the votes that included "w" lines for this router. - - * If consensus-method 5 or later is in use, then the "p" line - is taken from the votes that have the same policy summary - for the descriptor we are listing. (They should all be the - same. If they are not, we pick the most commonly listed - one, breaking ties in favor of the lexicographically larger - vote.) The port list is encoded as specified in 3.4.2. - - * If consensus-method 6 or later is in use and if 3 or more - authorities provide a Measured= keyword in their votes for - a router, the authorities produce a consensus containing a - Bandwidth= keyword equal to the median of the Measured= votes. - - * If consensus-method 7 or later is in use, the params line is - included in the output. - - The signatures at the end of a consensus document are sorted in - ascending order by identity digest. - - All ties in computing medians are broken in favor of the smaller or - earlier item. - -3.4.1. Forward compatibility - - Future versions of Tor will need to include new information in the - consensus documents, but it is important that all authorities (or at least - half) generate and sign the same signed consensus. - - To achieve this, authorities list in their votes their supported methods - for generating consensuses from votes. Later methods will be assigned - higher numbers. Currently recognized methods: - "1" -- The first implemented version. - "2" -- Added support for the Unnamed flag. - "3" -- Added legacy ID key support to aid in authority ID key rollovers - "4" -- No longer list routers that are not running in the consensus - "5" -- adds support for "w" and "p" lines. - "6" -- Prefers measured bandwidth values rather than advertised - - Before generating a consensus, an authority must decide which consensus - method to use. To do this, it looks for the highest version number - supported by more than 2/3 of the authorities voting. If it supports this - method, then it uses it. Otherwise, it falls back to method 1. - - (The consensuses generated by new methods must be parsable by - implementations that only understand the old methods, and must not cause - those implementations to compromise their anonymity. This is a means for - making changes in the contents of consensus; not for making - backward-incompatible changes in their format.) - -3.4.2. Encoding port lists - - Whether the summary shows the list of accepted ports or the list of - rejected ports depends on which list is shorter (has a shorter string - representation). In case of ties we choose the list of accepted - ports. As an exception to this rule an allow-all policy is - represented as "accept 1-65535" instead of "reject " and a reject-all - policy is similarly given as "reject 1-65535". - - Summary items are compressed, that is instead of "80-88,89-100" there - only is a single item of "80-100", similarly instead of "20,21" a - summary will say "20-21". - - Port lists are sorted in ascending order. - - The maximum allowed length of a policy summary (including the "accept " - or "reject ") is 1000 characters. If a summary exceeds that length we - use an accept-style summary and list as much of the port list as is - possible within these 1000 bytes. [XXXX be more specific.] - -3.5. Detached signatures - - Assuming full connectivity, every authority should compute and sign the - same consensus directory in each period. Therefore, it isn't necessary to - download the consensus computed by each authority; instead, the - authorities only push/fetch each others' signatures. A "detached - signature" document contains items as follows: - - "consensus-digest" SP Digest NL - - [At start, at most once.] - - The digest of the consensus being signed. - - "valid-after" SP YYYY-MM-DD SP HH:MM:SS NL - "fresh-until" SP YYYY-MM-DD SP HH:MM:SS NL - "valid-until" SP YYYY-MM-DD SP HH:MM:SS NL - - [As in the consensus] - - "directory-signature" - - [As in the consensus; the signature object is the same as in the - consensus document.] - - -4. Directory server operation - - All directory authorities and directory caches ("directory servers") - implement this section, except as noted. - -4.1. Accepting uploads (authorities only) - - When a router posts a signed descriptor to a directory authority, the - authority first checks whether it is well-formed and correctly - self-signed. If it is, the authority next verifies that the nickname - in question is not already assigned to a router with a different - public key. - Finally, the authority MAY check that the router is not blacklisted - because of its key, IP, or another reason. - - If the descriptor passes these tests, and the authority does not already - have a descriptor for a router with this public key, it accepts the - descriptor and remembers it. - - If the authority _does_ have a descriptor with the same public key, the - newly uploaded descriptor is remembered if its publication time is more - recent than the most recent old descriptor for that router, and either: - - There are non-cosmetic differences between the old descriptor and the - new one. - - Enough time has passed between the descriptors' publication times. - (Currently, 12 hours.) - - Differences between router descriptors are "non-cosmetic" if they would be - sufficient to force an upload as described in section 2 above. - - Note that the "cosmetic difference" test only applies to uploaded - descriptors, not to descriptors that the authority downloads from other - authorities. - - When a router posts a signed extra-info document to a directory authority, - the authority again checks it for well-formedness and correct signature, - and checks that its matches the extra-info-digest in some router - descriptor that it believes is currently useful. If so, it accepts it and - stores it and serves it as requested. If not, it drops it. - -4.2. Voting (authorities only) - - Authorities divide time into Intervals. Authority administrators SHOULD - try to all pick the same interval length, and SHOULD pick intervals that - are commonly used divisions of time (e.g., 5 minutes, 15 minutes, 30 - minutes, 60 minutes, 90 minutes). Voting intervals SHOULD be chosen to - divide evenly into a 24-hour day. - - Authorities SHOULD act according to interval and delays in the - latest consensus. Lacking a latest consensus, they SHOULD default to a - 30-minute Interval, a 5 minute VotingDelay, and a 5 minute DistDelay. - - Authorities MUST take pains to ensure that their clocks remain accurate - within a few seconds. (Running NTP is usually sufficient.) - - The first voting period of each day begins at 00:00 (midnight) GMT. If - the last period of the day would be truncated by one-half or more, it is - merged with the second-to-last period. - - An authority SHOULD publish its vote immediately at the start of each voting - period (minus VoteSeconds+DistSeconds). It does this by making it - available at - http:///tor/status-vote/next/authority.z - and sending it in an HTTP POST request to each other authority at the URL - http:///tor/post/vote - - If, at the start of the voting period, minus DistSeconds, an authority - does not have a current statement from another authority, the first - authority downloads the other's statement. - - Once an authority has a vote from another authority, it makes it available - at - http:///tor/status-vote/next/.z - where is the fingerprint of the other authority's identity key. - And at - http:///tor/status-vote/next/d/.z - where is the digest of the vote document. - - The consensus status, along with as many signatures as the server - currently knows, should be available at - http:///tor/status-vote/next/consensus.z - All of the detached signatures it knows for consensus status should be - available at: - http:///tor/status-vote/next/consensus-signatures.z - - Once there are enough signatures, or once the voting period starts, - these documents are available at - http:///tor/status-vote/current/consensus.z - and - http:///tor/status-vote/current/consensus-signatures.z - [XXX current/consensus-signatures is not currently implemented, as it - is not used in the voting protocol.] - - The other vote documents are analogously made available under - http:///tor/status-vote/current/authority.z - http:///tor/status-vote/current/.z - http:///tor/status-vote/current/d/.z - once the consensus is complete. - - Once an authority has computed and signed a consensus network status, it - should send its detached signature to each other authority in an HTTP POST - request to the URL: - http:///tor/post/consensus-signature - - [XXX Note why we support push-and-then-pull.] - - [XXX possible future features include support for downloading old - consensuses.] - -4.3. Downloading consensus status documents (caches only) - - All directory servers (authorities and caches) try to keep a recent - network-status consensus document to serve to clients. A cache ALWAYS - downloads a network-status consensus if any of the following are true: - - The cache has no consensus document. - - The cache's consensus document is no longer valid. - Otherwise, the cache downloads a new consensus document at a randomly - chosen time in the first half-interval after its current consensus - stops being fresh. (This time is chosen at random to avoid swarming - the authorities at the start of each period. The interval size is - inferred from the difference between the valid-after time and the - fresh-until time on the consensus.) - - [For example, if a cache has a consensus that became valid at 1:00, - and is fresh until 2:00, that cache will fetch a new consensus at - a random time between 2:00 and 2:30.] - -4.4. Downloading and storing router descriptors (authorities and caches) - - Periodically (currently, every 10 seconds), directory servers check - whether there are any specific descriptors that they do not have and that - they are not currently trying to download. Caches identify these - descriptors by hash in the recent network-status consensus documents; - authorities identify them by hash in vote (if publication date is more - recent than the descriptor we currently have). - - [XXXX need a way to fetch descriptors ahead of the vote? v2 status docs can - do that for now.] - - If so, the directory server launches requests to the authorities for these - descriptors, such that each authority is only asked for descriptors listed - in its most recent vote (if the requester is an authority) or in the - consensus (if the requester is a cache). If we're an authority, and more - than one authority lists the descriptor, we choose which to ask at random. - - If one of these downloads fails, we do not try to download that descriptor - from the authority that failed to serve it again unless we receive a newer - network-status (consensus or vote) from that authority that lists the same - descriptor. - - Directory servers must potentially cache multiple descriptors for each - router. Servers must not discard any descriptor listed by any recent - consensus. If there is enough space to store additional descriptors, - servers SHOULD try to hold those which clients are likely to download the - most. (Currently, this is judged based on the interval for which each - descriptor seemed newest.) -[XXXX define recent] - - Authorities SHOULD NOT download descriptors for routers that they would - immediately reject for reasons listed in 3.1. - -4.5. Downloading and storing extra-info documents - - All authorities, and any cache that chooses to cache extra-info documents, - and any client that uses extra-info documents, should implement this - section. - - Note that generally, clients don't need extra-info documents. - - Periodically, the Tor instance checks whether it is missing any extra-info - documents: in other words, if it has any router descriptors with an - extra-info-digest field that does not match any of the extra-info - documents currently held. If so, it downloads whatever extra-info - documents are missing. Caches download from authorities; non-caches try - to download from caches. We follow the same splitting and back-off rules - as in 4.4 (if a cache) or 5.3 (if a client). - -4.6. General-use HTTP URLs - - "Fingerprints" in these URLs are base-16-encoded SHA1 hashes. - - The most recent v3 consensus should be available at: - http:///tor/status-vote/current/consensus.z - - Starting with Tor version 0.2.1.1-alpha is also available at: - http:///tor/status-vote/current/consensus/++.z - - Where F1, F2, etc. are authority identity fingerprints the client trusts. - Servers will only return a consensus if more than half of the requested - authorities have signed the document, otherwise a 404 error will be sent - back. The fingerprints can be shortened to a length of any multiple of - two, using only the leftmost part of the encoded fingerprint. Tor uses - 3 bytes (6 hex characters) of the fingerprint. - - Clients SHOULD sort the fingerprints in ascending order. Server MUST - accept any order. - - Clients SHOULD use this format when requesting consensus documents from - directory authority servers and from caches running a version of Tor - that is known to support this URL format. - - A concatenated set of all the current key certificates should be available - at: - http:///tor/keys/all.z - - The key certificate for this server (if it is an authority) should be - available at: - http:///tor/keys/authority.z - - The key certificate for an authority whose authority identity fingerprint - is should be available at: - http:///tor/keys/fp/.z - - The key certificate whose signing key fingerprint is should be - available at: - http:///tor/keys/sk/.z - - The key certificate whose identity key fingerprint is and whose signing - key fingerprint is should be available at: - - http:///tor/keys/fp-sk/-.z - - (As usual, clients may request multiple certificates using: - http:///tor/keys/fp-sk/-+-.z ) - [The above fp-sk format was not supported before Tor 0.2.1.9-alpha.] - - The most recent descriptor for a server whose identity key has a - fingerprint of should be available at: - http:///tor/server/fp/.z - - The most recent descriptors for servers with identity fingerprints - ,, should be available at: - http:///tor/server/fp/++.z - - (NOTE: Implementations SHOULD NOT download descriptors by identity key - fingerprint. This allows a corrupted server (in collusion with a cache) to - provide a unique descriptor to a client, and thereby partition that client - from the rest of the network.) - - The server descriptor with (descriptor) digest (in hex) should be - available at: - http:///tor/server/d/.z - - The most recent descriptors with digests ,, should be - available at: - http:///tor/server/d/++.z - - The most recent descriptor for this server should be at: - http:///tor/server/authority.z - [Nothing in the Tor protocol uses this resource yet, but it is useful - for debugging purposes. Also, the official Tor implementations - (starting at 0.1.1.x) use this resource to test whether a server's - own DirPort is reachable.] - - A concatenated set of the most recent descriptors for all known servers - should be available at: - http:///tor/server/all.z - - Extra-info documents are available at the URLS - http:///tor/extra/d/... - http:///tor/extra/fp/... - http:///tor/extra/all[.z] - http:///tor/extra/authority[.z] - (As for /tor/server/ URLs: supports fetching extra-info - documents by their digest, by the fingerprint of their servers, - or all at once. When serving by fingerprint, we serve the - extra-info that corresponds to the descriptor we would serve by - that fingerprint. Only directory authorities of version - 0.2.0.1-alpha or later are guaranteed to support the first - three classes of URLs. Caches may support them, and MUST - support them if they have advertised "caches-extra-info".) - - For debugging, directories SHOULD expose non-compressed objects at URLs like - the above, but without the final ".z". - Clients MUST handle compressed concatenated information in two forms: - - A concatenated list of zlib-compressed objects. - - A zlib-compressed concatenated list of objects. - Directory servers MAY generate either format: the former requires less - CPU, but the latter requires less bandwidth. - - Clients SHOULD use upper case letters (A-F) when base16-encoding - fingerprints. Servers MUST accept both upper and lower case fingerprints - in requests. - -5. Client operation: downloading information - - Every Tor that is not a directory server (that is, those that do - not have a DirPort set) implements this section. - -5.1. Downloading network-status documents - - Each client maintains a list of directory authorities. Insofar as - possible, clients SHOULD all use the same list. - - Clients try to have a live consensus network-status document at all times. - A network-status document is "live" if the time in its valid-until field - has not passed. - - If a client is missing a live network-status document, it tries to fetch - it from a directory cache (or from an authority if it knows no caches). - On failure, the client waits briefly, then tries that network-status - document again from another cache. The client does not build circuits - until it has a live network-status consensus document, and it has - descriptors for more than 1/4 of the routers that it believes are running. - - (Note: clients can and should pick caches based on the network-status - information they have: once they have first fetched network-status info - from an authority, they should not need to go to the authority directly - again.) - - To avoid swarming the caches whenever a consensus expires, the - clients download new consensuses at a randomly chosen time after the - caches are expected to have a fresh consensus, but before their - consensus will expire. (This time is chosen uniformly at random from - the interval between the time 3/4 into the first interval after the - consensus is no longer fresh, and 7/8 of the time remaining after - that before the consensus is invalid.) - - [For example, if a cache has a consensus that became valid at 1:00, - and is fresh until 2:00, and expires at 4:00, that cache will fetch - a new consensus at a random time between 2:45 and 3:50, since 3/4 - of the one-hour interval is 45 minutes, and 7/8 of the remaining 75 - minutes is 65 minutes.] - -5.2. Downloading and storing router descriptors - - Clients try to have the best descriptor for each router. A descriptor is - "best" if: - * It is listed in the consensus network-status document. - - Periodically (currently every 10 seconds) clients check whether there are - any "downloadable" descriptors. A descriptor is downloadable if: - - It is the "best" descriptor for some router. - - The descriptor was published at least 10 minutes in the past. - (This prevents clients from trying to fetch descriptors that the - mirrors have probably not yet retrieved and cached.) - - The client does not currently have it. - - The client is not currently trying to download it. - - The client would not discard it immediately upon receiving it. - - The client thinks it is running and valid (see 6.1 below). - - If at least 16 known routers have downloadable descriptors, or if - enough time (currently 10 minutes) has passed since the last time the - client tried to download descriptors, it launches requests for all - downloadable descriptors, as described in 5.3 below. - - When a descriptor download fails, the client notes it, and does not - consider the descriptor downloadable again until a certain amount of time - has passed. (Currently 0 seconds for the first failure, 60 seconds for the - second, 5 minutes for the third, 10 minutes for the fourth, and 1 day - thereafter.) Periodically (currently once an hour) clients reset the - failure count. - - Clients retain the most recent descriptor they have downloaded for each - router so long as it is not too old (currently, 48 hours), OR so long as - no better descriptor has been downloaded for the same router. - - [Versions of Tor before 0.1.2.3-alpha would discard descriptors simply for - being published too far in the past.] [The code seems to discard - descriptors in all cases after they're 5 days old. True? -RD] - -5.3. Managing downloads - - When a client has no consensus network-status document, it downloads it - from a randomly chosen authority. In all other cases, the client - downloads from caches randomly chosen from among those believed to be V2 - directory servers. (This information comes from the network-status - documents; see 6 below.) - - When downloading multiple router descriptors, the client chooses multiple - mirrors so that: - - At least 3 different mirrors are used, except when this would result - in more than one request for under 4 descriptors. - - No more than 128 descriptors are requested from a single mirror. - - Otherwise, as few mirrors as possible are used. - After choosing mirrors, the client divides the descriptors among them - randomly. - - After receiving any response client MUST discard any network-status - documents and descriptors that it did not request. - -6. Using directory information - - Everyone besides directory authorities uses the approaches in this section - to decide which servers to use and what their keys are likely to be. - (Directory authorities just believe their own opinions, as in 3.1 above.) - -6.1. Choosing routers for circuits. - - Circuits SHOULD NOT be built until the client has enough directory - information: a live consensus network status [XXXX fallback?] and - descriptors for at least 1/4 of the servers believed to be running. - - A server is "listed" if it is included by the consensus network-status - document. Clients SHOULD NOT use unlisted servers. - - These flags are used as follows: - - - Clients SHOULD NOT use non-'Valid' or non-'Running' routers unless - requested to do so. - - - Clients SHOULD NOT use non-'Fast' routers for any purpose other than - very-low-bandwidth circuits (such as introduction circuits). - - - Clients SHOULD NOT use non-'Stable' routers for circuits that are - likely to need to be open for a very long time (such as those used for - IRC or SSH connections). - - - Clients SHOULD NOT choose non-'Guard' nodes when picking entry guard - nodes. - - - Clients SHOULD NOT download directory information from non-'V2Dir' - caches. - - See the "path-spec.txt" document for more details. - -6.2. Managing naming - - In order to provide human-memorable names for individual server - identities, some directory servers bind names to IDs. Clients handle - names in two ways: - - When a client encounters a name it has not mapped before: - - If the consensus lists any router with that name as "Named", or if - consensus-method 2 or later is in use and the consensus lists any - router with that name as having the "Unnamed" flag, then the name is - bound. (It's bound to the ID listed in the entry with the Named, - or to an unknown ID if no name is found.) - - When the user refers to a bound name, the implementation SHOULD provide - only the router with ID bound to that name, and no other router, even - if the router with the right ID can't be found. - - When a user tries to refer to a non-bound name, the implementation SHOULD - warn the user. After warning the user, the implementation MAY use any - router that advertises the name. - - Not every router needs a nickname. When a router doesn't configure a - nickname, it publishes with the default nickname "Unnamed". Authorities - SHOULD NOT ever mark a router with this nickname as Named; client software - SHOULD NOT ever use a router in response to a user request for a router - called "Unnamed". - -6.3. Software versions - - An implementation of Tor SHOULD warn when it has fetched a consensus - network-status, and it is running a software version not listed. - -6.4. Warning about a router's status. - - If a router tries to publish its descriptor to a Naming authority - that has its nickname mapped to another key, the router SHOULD - warn the operator that it is either using the wrong key or is using - an already claimed nickname. - - If a router has fetched a consensus document,, and the - authorities do not publish a binding for the router's nickname, the - router MAY remind the operator that the chosen nickname is not - bound to this key at the authorities, and suggest contacting the - authority operators. - - ... - -6.5. Router protocol versions - - A client should believe that a router supports a given feature if that - feature is supported by the router or protocol versions in more than half - of the live networkstatuses' "v" entries for that router. In other words, - if the "v" entries for some router are: - v Tor 0.0.8pre1 (from authority 1) - v Tor 0.1.2.11 (from authority 2) - v FutureProtocolDescription 99 (from authority 3) - then the client should believe that the router supports any feature - supported by 0.1.2.11. - - This is currently equivalent to believing the median declared version for - a router in all live networkstatuses. - -7. Standards compliance - - All clients and servers MUST support HTTP 1.0. Clients and servers MAY - support later versions of HTTP as well. - -7.1. HTTP headers - - Servers MAY set the Content-Length: header. Servers SHOULD set - Content-Encoding to "deflate" or "identity". - - Servers MAY include an X-Your-Address-Is: header, whose value is the - apparent IP address of the client connecting to them (as a dotted quad). - For directory connections tunneled over a BEGIN_DIR stream, servers SHOULD - report the IP from which the circuit carrying the BEGIN_DIR stream reached - them. [Servers before version 0.1.2.5-alpha reported 127.0.0.1 for all - BEGIN_DIR-tunneled connections.] - - Servers SHOULD disable caching of multiple network statuses or multiple - router descriptors. Servers MAY enable caching of single descriptors, - single network statuses, the list of all router descriptors, a v1 - directory, or a v1 running routers document. XXX mention times. - -7.2. HTTP status codes - - Tor delivers the following status codes. Some were chosen without much - thought; other code SHOULD NOT rely on specific status codes yet. - - 200 -- the operation completed successfully - -- the user requested statuses or serverdescs, and none of the ones we - requested were found (0.2.0.4-alpha and earlier). - - 304 -- the client specified an if-modified-since time, and none of the - requested resources have changed since that time. - - 400 -- the request is malformed, or - -- the URL is for a malformed variation of one of the URLs we support, - or - -- the client tried to post to a non-authority, or - -- the authority rejected a malformed posted document, or - - 404 -- the requested document was not found. - -- the user requested statuses or serverdescs, and none of the ones - requested were found (0.2.0.5-alpha and later). - - 503 -- we are declining the request in order to save bandwidth - -- user requested some items that we ordinarily generate or store, - but we do not have any available. - -9. Backward compatibility and migration plans - - Until Tor versions before 0.1.1.x are completely obsolete, directory - authorities should generate, and mirrors should download and cache, v1 - directories and running-routers lists, and allow old clients to download - them. These documents and the rules for retrieving, serving, and caching - them are described in dir-spec-v1.txt. - - Until Tor versions before 0.2.0.x are completely obsolete, directory - authorities should generate, mirrors should download and cache, v2 - network-status documents, and allow old clients to download them. - Additionally, all directory servers and caches should download, store, and - serve any router descriptor that is required because of v2 network-status - documents. These documents and the rules for retrieving, serving, and - caching them are described in dir-spec-v1.txt. - -A. Consensus-negotiation timeline. - - - Period begins: this is the Published time. - Everybody sends votes - Reconciliation: everybody tries to fetch missing votes. - consensus may exist at this point. - End of voting period: - everyone swaps signatures. - Now it's okay for caches to download - Now it's okay for clients to download. - - Valid-after/valid-until switchover - diff --git a/orchid/doc/spec/path-spec.txt b/orchid/doc/spec/path-spec.txt deleted file mode 100644 index 78f3b63b..00000000 --- a/orchid/doc/spec/path-spec.txt +++ /dev/null @@ -1,437 +0,0 @@ - - Tor Path Specification - - Roger Dingledine - Nick Mathewson - -Note: This is an attempt to specify Tor as currently implemented. Future -versions of Tor will implement improved algorithms. - -This document tries to cover how Tor chooses to build circuits and assign -streams to circuits. Other implementations MAY take other approaches, but -implementors should be aware of the anonymity and load-balancing implications -of their choices. - - THIS SPEC ISN'T DONE YET. - -1. General operation - - Tor begins building circuits as soon as it has enough directory - information to do so (see section 5 of dir-spec.txt). Some circuits are - built preemptively because we expect to need them later (for user - traffic), and some are built because of immediate need (for user traffic - that no current circuit can handle, for testing the network or our - reachability, and so on). - - When a client application creates a new stream (by opening a SOCKS - connection or launching a resolve request), we attach it to an appropriate - open circuit if one exists, or wait if an appropriate circuit is - in-progress. We launch a new circuit only - if no current circuit can handle the request. We rotate circuits over - time to avoid some profiling attacks. - - To build a circuit, we choose all the nodes we want to use, and then - construct the circuit. Sometimes, when we want a circuit that ends at a - given hop, and we have an appropriate unused circuit, we "cannibalize" the - existing circuit and extend it to the new terminus. - - These processes are described in more detail below. - - This document describes Tor's automatic path selection logic only; path - selection can be overridden by a controller (with the EXTENDCIRCUIT and - ATTACHSTREAM commands). Paths constructed through these means may - violate some constraints given below. - -1.1. Terminology - - A "path" is an ordered sequence of nodes, not yet built as a circuit. - - A "clean" circuit is one that has not yet been used for any traffic. - - A "fast" or "stable" or "valid" node is one that has the 'Fast' or - 'Stable' or 'Valid' flag - set respectively, based on our current directory information. A "fast" - or "stable" circuit is one consisting only of "fast" or "stable" nodes. - - In an "exit" circuit, the final node is chosen based on waiting stream - requests if any, and in any case it avoids nodes with exit policy of - "reject *:*". An "internal" circuit, on the other hand, is one where - the final node is chosen just like a middle node (ignoring its exit - policy). - - A "request" is a client-side stream or DNS resolve that needs to be - served by a circuit. - - A "pending" circuit is one that we have started to build, but which has - not yet completed. - - A circuit or path "supports" a request if it is okay to use the - circuit/path to fulfill the request, according to the rules given below. - A circuit or path "might support" a request if some aspect of the request - is unknown (usually its target IP), but we believe the path probably - supports the request according to the rules given below. - -1.1. A server's bandwidth - - Old versions of Tor did not report bandwidths in network status - documents, so clients had to learn them from the routers' advertised - server descriptors. - - For versions of Tor prior to 0.2.1.17-rc, everywhere below where we - refer to a server's "bandwidth", we mean its clipped advertised - bandwidth, computed by taking the smaller of the 'rate' and - 'observed' arguments to the "bandwidth" element in the server's - descriptor. If a router's advertised bandwidth is greater than - MAX_BELIEVABLE_BANDWIDTH (currently 10 MB/s), we clipped to that - value. - - For more recent versions of Tor, we take the bandwidth value declared - in the consensus, and fall back to the clipped advertised bandwidth - only if the consensus does not have bandwidths listed. - -2. Building circuits - -2.1. When we build - -2.1.1. Clients build circuits preemptively - - When running as a client, Tor tries to maintain at least a certain - number of clean circuits, so that new streams can be handled - quickly. To increase the likelihood of success, Tor tries to - predict what circuits will be useful by choosing from among nodes - that support the ports we have used in the recent past (by default - one hour). Specifically, on startup Tor tries to maintain one clean - fast exit circuit that allows connections to port 80, and at least - two fast clean stable internal circuits in case we get a resolve - request or hidden service request (at least three if we _run_ a - hidden service). - - After that, Tor will adapt the circuits that it preemptively builds - based on the requests it sees from the user: it tries to have two fast - clean exit circuits available for every port seen within the past hour - (each circuit can be adequate for many predicted ports -- it doesn't - need two separate circuits for each port), and it tries to have the - above internal circuits available if we've seen resolves or hidden - service activity within the past hour. If there are 12 or more clean - circuits open, it doesn't open more even if it has more predictions. - - Only stable circuits can "cover" a port that is listed in the - LongLivedPorts config option. Similarly, hidden service requests - to ports listed in LongLivedPorts make us create stable internal - circuits. - - Note that if there are no requests from the user for an hour, Tor - will predict no use and build no preemptive circuits. - - The Tor client SHOULD NOT store its list of predicted requests to a - persistent medium. - -2.1.2. Clients build circuits on demand - - Additionally, when a client request exists that no circuit (built or - pending) might support, we create a new circuit to support the request. - For exit connections, we pick an exit node that will handle the - most pending requests (choosing arbitrarily among ties), launch a - circuit to end there, and repeat until every unattached request - might be supported by a pending or built circuit. For internal - circuits, we pick an arbitrary acceptable path, repeating as needed. - - In some cases we can reuse an already established circuit if it's - clean; see Section 2.3 (cannibalizing circuits) for details. - -2.1.3. Servers build circuits for testing reachability and bandwidth - - Tor servers test reachability of their ORPort once they have - successfully built a circuit (on start and whenever their IP address - changes). They build an ordinary fast internal circuit with themselves - as the last hop. As soon as any testing circuit succeeds, the Tor - server decides it's reachable and is willing to publish a descriptor. - - We launch multiple testing circuits (one at a time), until we - have NUM_PARALLEL_TESTING_CIRC (4) such circuits open. Then we - do a "bandwidth test" by sending a certain number of relay drop - cells down each circuit: BandwidthRate * 10 / CELL_NETWORK_SIZE - total cells divided across the four circuits, but never more than - CIRCWINDOW_START (1000) cells total. This exercises both outgoing and - incoming bandwidth, and helps to jumpstart the observed bandwidth - (see dir-spec.txt). - - Tor servers also test reachability of their DirPort once they have - established a circuit, but they use an ordinary exit circuit for - this purpose. - -2.1.4. Hidden-service circuits - - See section 4 below. - -2.1.5. Rate limiting of failed circuits - - If we fail to build a circuit N times in a X second period (see Section - 2.3 for how this works), we stop building circuits until the X seconds - have elapsed. - XXXX - -2.1.6. When to tear down circuits - - XXXX - -2.2. Path selection and constraints - - We choose the path for each new circuit before we build it. We choose the - exit node first, followed by the other nodes in the circuit. All paths - we generate obey the following constraints: - - We do not choose the same router twice for the same path. - - We do not choose any router in the same family as another in the same - path. - - We do not choose more than one router in a given /16 subnet - (unless EnforceDistinctSubnets is 0). - - We don't choose any non-running or non-valid router unless we have - been configured to do so. By default, we are configured to allow - non-valid routers in "middle" and "rendezvous" positions. - - If we're using Guard nodes, the first node must be a Guard (see 5 - below) - - XXXX Choosing the length - - For circuits that do not need to be "fast", when choosing among - multiple candidates for a path element, we choose randomly. - - For "fast" circuits, we pick a given router as an exit with probability - proportional to its bandwidth. - - For non-exit positions on "fast" circuits, we pick routers as above, but - we weight the bandwidth of Exit-flagged nodes depending - on the fraction of bandwidth available from non-Exit nodes. Call the - total bandwidth for Exit nodes under consideration E, - and the total bandwidth for all nodes under - consideration T. If E..exit, the request is rewritten to a request for - , and the request is only supported by the exit whose nickname - or fingerprint is . - -2.3. Cannibalizing circuits - - If we need a circuit and have a clean one already established, in - some cases we can adapt the clean circuit for our new - purpose. Specifically, - - For hidden service interactions, we can "cannibalize" a clean internal - circuit if one is available, so we don't need to build those circuits - from scratch on demand. - - We can also cannibalize clean circuits when the client asks to exit - at a given node -- either via the ".exit" notation or because the - destination is running at the same location as an exit node. - - -2.4. Handling failure - - If an attempt to extend a circuit fails (either because the first create - failed or a subsequent extend failed) then the circuit is torn down and is - no longer pending. (XXXX really?) Requests that might have been - supported by the pending circuit thus become unsupported, and a new - circuit needs to be constructed. - - If a stream "begin" attempt fails with an EXITPOLICY error, we - decide that the exit node's exit policy is not correctly advertised, - so we treat the exit node as if it were a non-exit until we retrieve - a fresh descriptor for it. - - XXXX - -3. Attaching streams to circuits - - When a circuit that might support a request is built, Tor tries to attach - the request's stream to the circuit and sends a BEGIN, BEGIN_DIR, - or RESOLVE relay - cell as appropriate. If the request completes unsuccessfully, Tor - considers the reason given in the CLOSE relay cell. [XXX yes, and?] - - - After a request has remained unattached for SocksTimeout (2 minutes - by default), Tor abandons the attempt and signals an error to the - client as appropriate (e.g., by closing the SOCKS connection). - - XXX Timeouts and when Tor auto-retries. - * What stream-end-reasons are appropriate for retrying. - - If no reply to BEGIN/RESOLVE, then the stream will timeout and fail. - -4. Hidden-service related circuits - - XXX Tracking expected hidden service use (client-side and hidserv-side) - -5. Guard nodes - - We use Guard nodes (also called "helper nodes" in the literature) to - prevent certain profiling attacks. Here's the risk: if we choose entry and - exit nodes at random, and an attacker controls C out of N servers - (ignoring bandwidth), then the - attacker will control the entry and exit node of any given circuit with - probability (C/N)^2. But as we make many different circuits over time, - then the probability that the attacker will see a sample of about (C/N)^2 - of our traffic goes to 1. Since statistical sampling works, the attacker - can be sure of learning a profile of our behavior. - - If, on the other hand, we picked an entry node and held it fixed, we would - have probability C/N of choosing a bad entry and being profiled, and - probability (N-C)/N of choosing a good entry and not being profiled. - - When guard nodes are enabled, Tor maintains an ordered list of entry nodes - as our chosen guards, and stores this list persistently to disk. If a Guard - node becomes unusable, rather than replacing it, Tor adds new guards to the - end of the list. 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. - - A guard is unusable if any of the following hold: - - it is not marked as a Guard by the networkstatuses, - - it is not marked Valid (and the user hasn't set AllowInvalid entry) - - it is not marked Running - - Tor couldn't reach it the last time it tried to connect - - A guard is unusable for a particular circuit if any of the rules for path - selection in 2.2 are not met. In particular, if the circuit is "fast" - and the guard is not Fast, or if the circuit is "stable" and the guard is - not Stable, or if the guard has already been chosen as the exit node in - that circuit, Tor can't use it as a guard node for that circuit. - - 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. - - 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. 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. - - Tor does not add a guard persistently to the list until the first time we - have connected to it successfully. - -6. Router descriptor purposes - - There are currently three "purposes" supported for router descriptors: - general, controller, and bridge. Most descriptors are of type general - -- these are the ones listed in the consensus, and the ones fetched - and used in normal cases. - - Controller-purpose descriptors are those delivered by the controller - and labelled as such: they will be kept around (and expire like - normal descriptors), and they can be used by the controller in its - CIRCUITEXTEND commands. Otherwise they are ignored by Tor when it - chooses paths. - - Bridge-purpose descriptors are for routers that are used as bridges. See - doc/design-paper/blocking.pdf for more design explanation, or proposal - 125 for specific details. Currently bridge descriptors are used in place - of normal entry guards, for Tor clients that have UseBridges enabled. - - -X. Old notes - -X.1. Do we actually do this? - -How to deal with network down. - - While all helpers are down/unreachable and there are no established - or on-the-way testing circuits, launch a testing circuit. (Do this - periodically in the same way we try to establish normal circuits - when things are working normally.) - (Testing circuits are a special type of circuit, that streams won't - attach to by accident.) - - When a testing circuit succeeds, mark all helpers up and hold - the testing circuit open. - - If a connection to a helper succeeds, close all testing circuits. - Else mark that helper down and try another. - - If the last helper is marked down and we already have a testing - circuit established, then add the first hop of that testing circuit - to the end of our helper node list, close that testing circuit, - and go back to square one. (Actually, rather than closing the - testing circuit, can we get away with converting it to a normal - circuit and beginning to use it immediately?) - - [Do we actually do any of the above? If so, let's spec it. If not, let's - remove it. -NM] - -X.2. A thing we could do to deal with reachability. - -And as a bonus, it leads to an answer to Nick's attack ("If I pick -my helper nodes all on 18.0.0.0:*, then I move, you'll know where I -bootstrapped") -- the answer is to pick your original three helper nodes -without regard for reachability. Then the above algorithm will add some -more that are reachable for you, and if you move somewhere, it's more -likely (though not certain) that some of the originals will become useful. -Is that smart or just complex? - -X.3. Some stuff that worries me about entry guards. 2006 Jun, Nickm. - - It is unlikely for two users to have the same set of entry guards. - Observing a user is sufficient to learn its entry guards. So, as we move - around, entry guards make us linkable. If we want to change guards when - our location (IP? subnet?) changes, we have two bad options. We could - - Drop the old guards. But if we go back to our old location, - we'll not use our old guards. For a laptop that sometimes gets used - from work and sometimes from home, this is pretty fatal. - - Remember the old guards as associated with the old location, and use - them again if we ever go back to the old location. This would be - nasty, since it would force us to record where we've been. - - [Do we do any of this now? If not, this should move into 099-misc or - 098-todo. -NM] - diff --git a/orchid/doc/spec/rend-spec.txt b/orchid/doc/spec/rend-spec.txt deleted file mode 100644 index f0300926..00000000 --- a/orchid/doc/spec/rend-spec.txt +++ /dev/null @@ -1,751 +0,0 @@ - - Tor Rendezvous Specification - -0. Overview and preliminaries - - Read - https://www.torproject.org/doc/design-paper/tor-design.html#sec:rendezvous - before you read this specification. It will make more sense. - - Rendezvous points provide location-hidden services (server - anonymity) for the onion routing network. With rendezvous points, - Bob can offer a TCP service (say, a webserver) via the onion - routing network, without revealing the IP of that service. - - Bob does this by anonymously advertising a public key for his - service, along with a list of onion routers to act as "Introduction - Points" for his service. He creates forward circuits to those - introduction points, and tells them about his public key. To - connect to Bob, Alice first builds a circuit to an OR to act as - her "Rendezvous Point." She then connects to one of Bob's chosen - introduction points, optionally provides authentication or - authorization information, and asks it to tell him about her Rendezvous - Point (RP). If Bob chooses to answer, he builds a circuit to her - RP, and tells it to connect him to Alice. The RP joins their - circuits together, and begins relaying cells. Alice's 'BEGIN' - cells are received directly by Bob's OP, which passes data to - and from the local server implementing Bob's service. - - Below we describe a network-level specification of this service, - along with interfaces to make this process transparent to Alice - (so long as she is using an OP). - -0.1. Notation, conventions and prerequisites - - In the specifications below, we use the same notation and terminology - as in "tor-spec.txt". The service specified here also requires the - existence of an onion routing network as specified in that file. - - H(x) is a SHA1 digest of x. - PKSign(SK,x) is a PKCS.1-padded RSA signature of x with SK. - PKEncrypt(SK,x) is a PKCS.1-padded RSA encryption of x with SK. - Public keys are all RSA, and encoded in ASN.1. - All integers are stored in network (big-endian) order. - All symmetric encryption uses AES in counter mode, except where - otherwise noted. - - In all discussions, "Alice" will refer to a user connecting to a - location-hidden service, and "Bob" will refer to a user running a - location-hidden service. - - An OP is (as defined elsewhere) an "Onion Proxy" or Tor client. - - An OR is (as defined elsewhere) an "Onion Router" or Tor server. - - An "Introduction point" is a Tor server chosen to be Bob's medium-term - 'meeting place'. A "Rendezvous point" is a Tor server chosen by Alice to - be a short-term communication relay between her and Bob. All Tor servers - potentially act as introduction and rendezvous points. - -0.2. Protocol outline - - 1. Bob->Bob's OP: "Offer IP:Port as - public-key-name:Port". [configuration] - (We do not specify this step; it is left to the implementor of - Bob's OP.) - - 2. Bob's OP generates keypair and rendezvous service descriptor: - "Meet public-key X at introduction point A, B, or C." (signed) - - 3. Bob's OP->Introduction point via Tor: [introduction setup] - "This pk is me." - - 4. Bob's OP->directory service via Tor: publishes Bob's service - descriptor [advertisement] - - 5. Out of band, Alice receives a [x.y.]z.onion:port address. - She opens a SOCKS connection to her OP, and requests - x.y.z.onion:port. - - 6. Alice's OP retrieves Bob's descriptor via Tor. [descriptor lookup.] - - 7. Alice's OP chooses a rendezvous point, opens a circuit to that - rendezvous point, and establishes a rendezvous circuit. [rendezvous - setup.] - - 8. Alice connects to the Introduction point via Tor, and tells it about - her rendezvous point and optional authentication/authorization - information. (Encrypted to Bob.) [Introduction 1] - - 9. The Introduction point passes this on to Bob's OP via Tor, along the - introduction circuit. [Introduction 2] - - 10. Bob's OP decides whether to connect to Alice, and if so, creates a - circuit to Alice's RP via Tor. Establishes a shared circuit. - [Rendezvous.] - - 11. Alice's OP sends begin cells to Bob's OP. [Connection] - -0.3. Constants and new cell types - - Relay cell types - 32 -- RELAY_ESTABLISH_INTRO - 33 -- RELAY_ESTABLISH_RENDEZVOUS - 34 -- RELAY_INTRODUCE1 - 35 -- RELAY_INTRODUCE2 - 36 -- RELAY_RENDEZVOUS1 - 37 -- RELAY_RENDEZVOUS2 - 38 -- RELAY_INTRO_ESTABLISHED - 39 -- RELAY_RENDEZVOUS_ESTABLISHED - 40 -- RELAY_COMMAND_INTRODUCE_ACK - -0.4. Version overview - - There are several parts in the hidden service protocol that have - changed over time, each of them having its own version number, whereas - other parts remained the same. The following list of potentially - versioned protocol parts should help reduce some confusion: - - - Hidden service descriptor: the binary-based v0 was the default for - a long time, and an ascii-based v2 has been added by proposal - 114. See 1.2. - - - Hidden service descriptor propagation mechanism: currently related to - the hidden service descriptor version -- v0 publishes to the original - hs directory authorities, whereas v2 publishes to a rotating subset - of relays with the "hsdir" flag; see 1.4 and 1.6. - - - Introduction protocol for how to generate an introduction cell: - v0 specified a nickname for the rendezvous point and assumed the - relay would know about it, whereas v2 now specifies IP address, - port, and onion key so the relay doesn't need to already recognize - it. See 1.8. - -1. The Protocol - -1.1. Bob configures his local OP. - - We do not specify a format for the OP configuration file. However, - OPs SHOULD allow Bob to provide more than one advertised service - per OP, and MUST allow Bob to specify one or more virtual ports per - service. Bob provides a mapping from each of these virtual ports - to a local IP:Port pair. - -1.2. Bob's OP generates service descriptors. - - The first time the OP provides an advertised service, it generates - a public/private keypair (stored locally). - - Beginning with 0.2.0.10-alpha, Bob's OP encodes "V2" descriptors. The - format of a "V2" descriptor is as follows: - - "rendezvous-service-descriptor" descriptor-id NL - - [At start, exactly once] - - Indicates the beginning of the descriptor. "descriptor-id" is a - periodically changing identifier of 160 bits formatted as 32 base32 - chars that is calculated by the hidden service and its clients. If - the optional "descriptor-cookie" is used, this "descriptor-id" - cannot be computed by anyone else. (Everyone can verify that this - "descriptor-id" belongs to the rest of the descriptor, even without - knowing the optional "descriptor-cookie", as described below.) The - "descriptor-id" is calculated by performing the following operation: - - descriptor-id = - H(permanent-id | H(time-period | descriptor-cookie | replica)) - - "permanent-id" is the permanent identifier of the hidden service, - consisting of 80 bits. It can be calculated by computing the hash value - of the public hidden service key and truncating after the first 80 bits: - - permanent-id = H(public-key)[:10] - - "H(time-period | descriptor-cookie | replica)" is the (possibly - secret) id part that is - necessary to verify that the hidden service is the true originator - of this descriptor. It can only be created by the hidden service - and its clients, but the "signature" below can only be created by - the service. - - "descriptor-cookie" is an optional secret password of 128 bits that - is shared between the hidden service provider and its clients. - - "replica" denotes the number of the non-consecutive replica. - - (Each descriptor is replicated on a number of _consecutive_ nodes - in the identifier ring by making every storing node responsible - for the identifier intervals starting from its 3rd predecessor's - ID to its own ID. In addition to that, every service publishes - multiple descriptors with different descriptor IDs in order to - distribute them to different places on the ring. Therefore, - "replica" chooses one of the _non-consecutive_ replicas. -KL) - - The "time-period" changes periodically depending on the global time and - as a function of "permanent-id". The current value for "time-period" can - be calculated using the following formula: - - time-period = (current-time + permanent-id-byte * 86400 / 256) - / 86400 - - "current-time" contains the current system time in seconds since - 1970-01-01 00:00, e.g. 1188241957. "permanent-id-byte" is the first - (unsigned) byte of the permanent identifier (which is in network - order), e.g. 143. Adding the product of "permanent-id-byte" and - 86400 (seconds per day), divided by 256, prevents "time-period" from - changing for all descriptors at the same time of the day. The result - of the overall operation is a (network-ordered) 32-bit integer, e.g. - 13753 or 0x000035B9 with the example values given above. - - "version" version-number NL - - [Exactly once] - - The version number of this descriptor's format. In this case: 2. - - "permanent-key" NL a public key in PEM format - - [Exactly once] - - The public key of the hidden service which is required to verify the - "descriptor-id" and the "signature". - - "secret-id-part" secret-id-part NL - - [Exactly once] - - The result of the following operation as explained above, formatted as - 32 base32 chars. Using this secret id part, everyone can verify that - the signed descriptor belongs to "descriptor-id". - - secret-id-part = H(time-period | descriptor-cookie | replica) - - "publication-time" YYYY-MM-DD HH:MM:SS NL - - [Exactly once] - - A timestamp when this descriptor has been created. - - "protocol-versions" version-string NL - - [Exactly once] - - A comma-separated list of recognized and permitted version numbers - for use in INTRODUCE cells; these versions are described in section - 1.8 below. - - "introduction-points" NL encrypted-string - - [At most once] - - A list of introduction points. If the optional "descriptor-cookie" is - used, this list is encrypted with AES in CTR mode with a random - initialization vector of 128 bits that is written to - the beginning of the encrypted string, and the "descriptor-cookie" as - secret key of 128 bits length. - - The string containing the introduction point data (either encrypted - or not) is encoded in base64, and surrounded with - "-----BEGIN MESSAGE-----" and "-----END MESSAGE-----". - - The unencrypted string may begin with: - - ["service-authentication" auth-type NL auth-data ... reserved] - - [At start, any number] - - The service-specific authentication data can be used to perform - client authentication. This data is independent of the selected - introduction point as opposed to "intro-authentication" below. - - Subsequently, an arbitrary number of introduction point entries may - follow, each containing the following data: - - "introduction-point" identifier NL - - [At start, exactly once] - - The identifier of this introduction point: the base-32 encoded - hash of this introduction point's identity key. - - "ip-address" ip-address NL - - [Exactly once] - - The IP address of this introduction point. - - "onion-port" port NL - - [Exactly once] - - The TCP port on which the introduction point is listening for - incoming onion requests. - - "onion-key" NL a public key in PEM format - - [Exactly once] - - The public key that can be used to encrypt messages to this - introduction point. - - "service-key" NL a public key in PEM format - - [Exactly once] - - The public key that can be used to encrypt messages to the hidden - service. - - ["intro-authentication" auth-type NL auth-data ... reserved] - - [Any number] - - The introduction-point-specific authentication data can be used - to perform client authentication. This data depends on the - selected introduction point as opposed to "service-authentication" - above. - - (This ends the fields in the encrypted portion of the descriptor.) - - [It's ok for Bob to advertise 0 introduction points. He might want - to do that if he previously advertised some introduction points, - and now he doesn't have any. -RD] - - "signature" NL signature-string - - [At end, exactly once] - - A signature of all fields above with the private key of the hidden - service. - -1.2.1. Other descriptor formats we don't use. - - Support for the V0 descriptor format was dropped in 0.2.2.0-alpha-dev: - - KL Key length [2 octets] - PK Bob's public key [KL octets] - TS A timestamp [4 octets] - NI Number of introduction points [2 octets] - Ipt A list of NUL-terminated ORs [variable] - SIG Signature of above fields [variable] - - KL is the length of PK, in octets. - TS is the number of seconds elapsed since Jan 1, 1970. - - The members of Ipt may be either (a) nicknames, or (b) identity key - digests, encoded in hex, and prefixed with a '$'. - - The V1 descriptor format was understood and accepted from - 0.1.1.5-alpha-cvs to 0.2.0.6-alpha-dev, but no Tors generated it and - it was removed: - - V Format byte: set to 255 [1 octet] - V Version byte: set to 1 [1 octet] - KL Key length [2 octets] - PK Bob's public key [KL octets] - TS A timestamp [4 octets] - PROTO Protocol versions: bitmask [2 octets] - NI Number of introduction points [2 octets] - For each introduction point: (as in INTRODUCE2 cells) - IP Introduction point's address [4 octets] - PORT Introduction point's OR port [2 octets] - ID Introduction point identity ID [20 octets] - KLEN Length of onion key [2 octets] - KEY Introduction point onion key [KLEN octets] - SIG Signature of above fields [variable] - - A hypothetical "V1" descriptor, that has never been used but might - be useful for historical reasons, contains: - - V Format byte: set to 255 [1 octet] - V Version byte: set to 1 [1 octet] - KL Key length [2 octets] - PK Bob's public key [KL octets] - TS A timestamp [4 octets] - PROTO Rendezvous protocol versions: bitmask [2 octets] - NA Number of auth mechanisms accepted [1 octet] - For each auth mechanism: - AUTHT The auth type that is supported [2 octets] - AUTHL Length of auth data [1 octet] - AUTHD Auth data [variable] - NI Number of introduction points [2 octets] - For each introduction point: (as in INTRODUCE2 cells) - ATYPE An address type (typically 4) [1 octet] - ADDR Introduction point's IP address [4 or 16 octets] - PORT Introduction point's OR port [2 octets] - AUTHT The auth type that is supported [2 octets] - AUTHL Length of auth data [1 octet] - AUTHD Auth data [variable] - ID Introduction point identity ID [20 octets] - KLEN Length of onion key [2 octets] - KEY Introduction point onion key [KLEN octets] - SIG Signature of above fields [variable] - - AUTHT specifies which authentication/authorization mechanism is - required by the hidden service or the introduction point. AUTHD - is arbitrary data that can be associated with an auth approach. - Currently only AUTHT of [00 00] is supported, with an AUTHL of 0. - See section 2 of this document for details on auth mechanisms. - -1.3. Bob's OP establishes his introduction points. - - The OP establishes a new introduction circuit to each introduction - point. These circuits MUST NOT be used for anything but hidden service - introduction. To establish the introduction, Bob sends a - RELAY_ESTABLISH_INTRO cell, containing: - - KL Key length [2 octets] - PK Introduction public key [KL octets] - HS Hash of session info [20 octets] - SIG Signature of above information [variable] - - [XXX011, need to add auth information here. -RD] - - To prevent replay attacks, the HS field contains a SHA-1 hash based on the - shared secret KH between Bob's OP and the introduction point, as - follows: - HS = H(KH | "INTRODUCE") - That is: - HS = H(KH | [49 4E 54 52 4F 44 55 43 45]) - (KH, as specified in tor-spec.txt, is H(g^xy | [00]) .) - - Upon receiving such a cell, the OR first checks that the signature is - correct with the included public key. If so, it checks whether HS is - correct given the shared state between Bob's OP and the OR. If either - check fails, the OP discards the cell; otherwise, it associates the - circuit with Bob's public key, and dissociates any other circuits - currently associated with PK. On success, the OR sends Bob a - RELAY_INTRO_ESTABLISHED cell with an empty payload. - - Bob's OP does not include its own public key in the RELAY_ESTABLISH_INTRO - cell, but the public key of a freshly generated introduction key pair. - The OP also includes these fresh public keys in the v2 hidden service - descriptor together with the other introduction point information. The - reason is that the introduction point does not need to and therefore - should not know for which hidden service it works, so as to prevent it - from tracking the hidden service's activity. - -1.4. Bob's OP advertises his service descriptor(s). - - Bob's OP opens a stream to each directory server's directory port via Tor. - (He may re-use old circuits for this.) Over this stream, Bob's OP makes - an HTTP 'POST' request, to a URL "/tor/rendezvous/publish" relative to the - directory server's root, containing as its body Bob's service descriptor. - - Bob should upload a service descriptor for each version format that - is supported in the current Tor network. - - Upon receiving a descriptor, the directory server checks the signature, - and discards the descriptor if the signature does not match the enclosed - public key. Next, the directory server checks the timestamp. If the - timestamp is more than 24 hours in the past or more than 1 hour in the - future, or the directory server already has a newer descriptor with the - same public key, the server discards the descriptor. Otherwise, the - server discards any older descriptors with the same public key and - version format, and associates the new descriptor with the public key. - The directory server remembers this descriptor for at least 24 hours - after its timestamp. At least every 18 hours, Bob's OP uploads a - fresh descriptor. - - Bob's OP publishes v2 descriptors to a changing subset of all v2 hidden - service directories. Therefore, Bob's OP opens a stream via Tor to each - responsible hidden service directory. (He may re-use old circuits - for this.) Over this stream, Bob's OP makes an HTTP 'POST' request to a - URL "/tor/rendezvous2/publish" relative to the hidden service - directory's root, containing as its body Bob's service descriptor. - - At any time, there are 6 hidden service directories responsible for - keeping replicas of a descriptor; they consist of 2 sets of 3 hidden - service directories with consecutive onion IDs. Bob's OP learns about - the complete list of hidden service directories by filtering the - consensus status document received from the directory authorities. A - hidden service directory is deemed responsible for all descriptor IDs in - the interval from its direct predecessor, exclusive, to its own ID, - inclusive; it further holds replicas for its 2 predecessors. A - participant only trusts its own routing list and never learns about - routing information from other parties. - - Bob's OP publishes a new v2 descriptor once an hour or whenever its - content changes. V2 descriptors can be found by clients within a given - time period of 24 hours, after which they change their ID as described - under 1.2. If a published descriptor would be valid for less than 60 - minutes (= 2 x 30 minutes to allow the server to be 30 minutes behind - and the client 30 minutes ahead), Bob's OP publishes the descriptor - under the ID of both, the current and the next publication period. - -1.5. Alice receives a x.y.z.onion address. - - When Alice receives a pointer to a location-hidden service, it is as a - hostname of the form "z.onion" or "y.z.onion" or "x.y.z.onion", where - z is a base-32 encoding of a 10-octet hash of Bob's service's public - key, computed as follows: - - 1. Let H = H(PK). - 2. Let H' = the first 80 bits of H, considering each octet from - most significant bit to least significant bit. - 2. Generate a 16-character encoding of H', using base32 as defined - in RFC 3548. - - (We only use 80 bits instead of the 160 bits from SHA1 because we - don't need to worry about arbitrary collisions, and because it will - make handling the url's more convenient.) - - The string "x", if present, is the base-32 encoding of the - authentication/authorization required by the introduction point. - The string "y", if present, is the base-32 encoding of the - authentication/authorization required by the hidden service. - Omitting a string is taken to mean auth type [00 00]. - See section 2 of this document for details on auth mechanisms. - - [Yes, numbers are allowed at the beginning. See RFC 1123. -NM] - -1.6. Alice's OP retrieves a service descriptor. - - Similarly to the description in section 1.4, Alice's OP fetches a v2 - descriptor from a randomly chosen hidden service directory out of the - changing subset of 6 nodes. If the request is unsuccessful, Alice retries - the other remaining responsible hidden service directories in a random - order. Alice relies on Bob to care about a potential clock skew between - the two by possibly storing two sets of descriptors (see end of section - 1.4). - - Alice's OP opens a stream via Tor to the chosen v2 hidden service - directory. (She may re-use old circuits for this.) Over this stream, - Alice's OP makes an HTTP 'GET' request for the document - "/tor/rendezvous2/", where z is replaced with the encoding of the - descriptor ID. The directory replies with a 404 HTTP response if it does - not recognize , and otherwise returns Bob's most recently uploaded - service descriptor. - - If Alice's OP receives a 404 response, it tries the other directory - servers, and only fails the lookup if none recognize the public key hash. - - Upon receiving a service descriptor, Alice verifies with the same process - as the directory server uses, described above in section 1.4. - - The directory server gives a 400 response if it cannot understand Alice's - request. - - Alice should cache the descriptor locally, but should not use - descriptors that are more than 24 hours older than their timestamp. - [Caching may make her partitionable, but she fetched it anonymously, - and we can't very well *not* cache it. -RD] - -1.7. Alice's OP establishes a rendezvous point. - - When Alice requests a connection to a given location-hidden service, - and Alice's OP does not have an established circuit to that service, - the OP builds a rendezvous circuit. It does this by establishing - a circuit to a randomly chosen OR, and sending a - RELAY_ESTABLISH_RENDEZVOUS cell to that OR. The body of that cell - contains: - - RC Rendezvous cookie [20 octets] - - [XXX011 this looks like an auth mechanism. should we generalize here? -RD] - - The rendezvous cookie is an arbitrary 20-byte value, chosen randomly by - Alice's OP. - - Upon receiving a RELAY_ESTABLISH_RENDEZVOUS cell, the OR associates the - RC with the circuit that sent it. It replies to Alice with an empty - RELAY_RENDEZVOUS_ESTABLISHED cell to indicate success. - - Alice's OP MUST NOT use the circuit which sent the cell for any purpose - other than rendezvous with the given location-hidden service. - -1.8. Introduction: from Alice's OP to Introduction Point - - Alice builds a separate circuit to one of Bob's chosen introduction - points, and sends it a RELAY_INTRODUCE1 cell containing: - - Cleartext - PK_ID Identifier for Bob's PK [20 octets] - Encrypted to Bob's PK: (in the v0 intro protocol) - RP Rendezvous point's nickname [20 octets] - RC Rendezvous cookie [20 octets] - g^x Diffie-Hellman data, part 1 [128 octets] - OR (in the v1 intro protocol) - VER Version byte: set to 1. [1 octet] - RP Rendezvous point nick or ID [42 octets] - RC Rendezvous cookie [20 octets] - g^x Diffie-Hellman data, part 1 [128 octets] - OR (in the v2 intro protocol) - VER Version byte: set to 2. [1 octet] - IP Rendezvous point's address [4 octets] - PORT Rendezvous point's OR port [2 octets] - ID Rendezvous point identity ID [20 octets] - KLEN Length of onion key [2 octets] - KEY Rendezvous point onion key [KLEN octets] - RC Rendezvous cookie [20 octets] - g^x Diffie-Hellman data, part 1 [128 octets] - - PK_ID is the hash of Bob's public key. RP is NUL-padded and - terminated. In version 0, it must contain a nickname. In version 1, - it must contain EITHER a nickname or an identity key digest that is - encoded in hex and prefixed with a '$'. - - The hybrid encryption to Bob's PK works just like the hybrid - encryption in CREATE cells (see tor-spec). Thus the payload of the - version 0 RELAY_INTRODUCE1 cell on the wire will contain - 20+42+16+20+20+128=246 bytes, and the version 1 and version 2 - introduction formats have other sizes. - - Through Tor 0.2.0.6-alpha, clients only generated the v0 introduction - format, whereas hidden services have understood and accepted v0, - v1, and v2 since 0.1.1.x. As of Tor 0.2.0.7-alpha and 0.1.2.18, - clients switched to using the v2 intro format. - - If Alice has downloaded a v2 descriptor, she uses the contained public - key ("service-key") instead of Bob's public key to create the - RELAY_INTRODUCE1 cell as described above. - -1.8.1. Other introduction formats we don't use. - - We briefly speculated about using the following format for the - "encrypted to Bob's PK" part of the introduction, but no Tors have - ever generated these. - - VER Version byte: set to 3. [1 octet] - ATYPE An address type (typically 4) [1 octet] - ADDR Rendezvous point's IP address [4 or 16 octets] - PORT Rendezvous point's OR port [2 octets] - AUTHT The auth type that is supported [2 octets] - AUTHL Length of auth data [1 octet] - AUTHD Auth data [variable] - ID Rendezvous point identity ID [20 octets] - KLEN Length of onion key [2 octets] - KEY Rendezvous point onion key [KLEN octets] - RC Rendezvous cookie [20 octets] - g^x Diffie-Hellman data, part 1 [128 octets] - -1.9. Introduction: From the Introduction Point to Bob's OP - - If the Introduction Point recognizes PK_ID as a public key which has - established a circuit for introductions as in 1.3 above, it sends the body - of the cell in a new RELAY_INTRODUCE2 cell down the corresponding circuit. - (If the PK_ID is unrecognized, the RELAY_INTRODUCE1 cell is discarded.) - - After sending the RELAY_INTRODUCE2 cell, the OR replies to Alice with an - empty RELAY_COMMAND_INTRODUCE_ACK cell. If no RELAY_INTRODUCE2 cell can - be sent, the OR replies to Alice with a non-empty cell to indicate an - error. (The semantics of the cell body may be determined later; the - current implementation sends a single '1' byte on failure.) - - When Bob's OP receives the RELAY_INTRODUCE2 cell, it decrypts it with - the private key for the corresponding hidden service, and extracts the - rendezvous point's nickname, the rendezvous cookie, and the value of g^x - chosen by Alice. - -1.10. Rendezvous - - Bob's OP builds a new Tor circuit ending at Alice's chosen rendezvous - point, and sends a RELAY_RENDEZVOUS1 cell along this circuit, containing: - RC Rendezvous cookie [20 octets] - g^y Diffie-Hellman [128 octets] - KH Handshake digest [20 octets] - - (Bob's OP MUST NOT use this circuit for any other purpose.) - - If the RP recognizes RC, it relays the rest of the cell down the - corresponding circuit in a RELAY_RENDEZVOUS2 cell, containing: - - g^y Diffie-Hellman [128 octets] - KH Handshake digest [20 octets] - - (If the RP does not recognize the RC, it discards the cell and - tears down the circuit.) - - When Alice's OP receives a RELAY_RENDEZVOUS2 cell on a circuit which - has sent a RELAY_ESTABLISH_RENDEZVOUS cell but which has not yet received - a reply, it uses g^y and H(g^xy) to complete the handshake as in the Tor - circuit extend process: they establish a 60-octet string as - K = SHA1(g^xy | [00]) | SHA1(g^xy | [01]) | SHA1(g^xy | [02]) - and generate - KH = K[0..15] - Kf = K[16..31] - Kb = K[32..47] - - Subsequently, the rendezvous point passes relay cells, unchanged, from - each of the two circuits to the other. When Alice's OP sends - RELAY cells along the circuit, it first encrypts them with the - Kf, then with all of the keys for the ORs in Alice's side of the circuit; - and when Alice's OP receives RELAY cells from the circuit, it decrypts - them with the keys for the ORs in Alice's side of the circuit, then - decrypts them with Kb. Bob's OP does the same, with Kf and Kb - interchanged. - -1.11. Creating streams - - To open TCP connections to Bob's location-hidden service, Alice's OP sends - a RELAY_BEGIN cell along the established circuit, using the special - address "", and a chosen port. Bob's OP chooses a destination IP and - port, based on the configuration of the service connected to the circuit, - and opens a TCP stream. From then on, Bob's OP treats the stream as an - ordinary exit connection. - [ Except he doesn't include addr in the connected cell or the end - cell. -RD] - - Alice MAY send multiple RELAY_BEGIN cells along the circuit, to open - multiple streams to Bob. Alice SHOULD NOT send RELAY_BEGIN cells for any - other address along her circuit to Bob; if she does, Bob MUST reject them. - -2. Authentication and authorization. - -Foo. - -3. Hidden service directory operation - - This section has been introduced with the v2 hidden service descriptor - format. It describes all operations of the v2 hidden service descriptor - fetching and propagation mechanism that are required for the protocol - described in section 1 to succeed with v2 hidden service descriptors. - -3.1. Configuring as hidden service directory - - Every onion router that has its directory port open can decide whether it - wants to store and serve hidden service descriptors. An onion router which - is configured as such includes the "hidden-service-dir" flag in its router - descriptors that it sends to directory authorities. - - The directory authorities include a new flag "HSDir" for routers that - decided to provide storage for hidden service descriptors and that - have been running for at least 24 hours. - -3.2. Accepting publish requests - - Hidden service directory nodes accept publish requests for v2 hidden service - descriptors and store them to their local memory. (It is not necessary to - make descriptors persistent, because after restarting, the onion router - would not be accepted as a storing node anyway, because it has not been - running for at least 24 hours.) All requests and replies are formatted as - HTTP messages. Requests are initiated via BEGIN_DIR cells directed to - the router's directory port, and formatted as HTTP POST requests to the URL - "/tor/rendezvous2/publish" relative to the hidden service directory's root, - containing as its body a v2 service descriptor. - - A hidden service directory node parses every received descriptor and only - stores it when it thinks that it is responsible for storing that descriptor - based on its own routing table. See section 1.4 for more information on how - to determine responsibility for a certain descriptor ID. - -3.3. Processing fetch requests - - Hidden service directory nodes process fetch requests for hidden service - descriptors by looking them up in their local memory. (They do not need to - determine if they are responsible for the passed ID, because it does no harm - if they deliver a descriptor for which they are not (any more) responsible.) - All requests and replies are formatted as HTTP messages. Requests are - initiated via BEGIN_DIR cells directed to the router's directory port, - and formatted as HTTP GET requests for the document "/tor/rendezvous2/", - where z is replaced with the encoding of the descriptor ID. - diff --git a/orchid/doc/spec/socks-extensions.txt b/orchid/doc/spec/socks-extensions.txt deleted file mode 100644 index 62d86acd..00000000 --- a/orchid/doc/spec/socks-extensions.txt +++ /dev/null @@ -1,78 +0,0 @@ -Tor's extensions to the SOCKS protocol - -1. Overview - - The SOCKS protocol provides a generic interface for TCP proxies. Client - software connects to a SOCKS server via TCP, and requests a TCP connection - to another address and port. The SOCKS server establishes the connection, - and reports success or failure to the client. After the connection has - been established, the client application uses the TCP stream as usual. - - Tor supports SOCKS4 as defined in [1], SOCKS4A as defined in [2], and - SOCKS5 as defined in [3]. - - The stickiest issue for Tor in supporting clients, in practice, is forcing - DNS lookups to occur at the OR side: if clients do their own DNS lookup, - the DNS server can learn which addresses the client wants to reach. - SOCKS4 supports addressing by IPv4 address; SOCKS4A is a kludge on top of - SOCKS4 to allow addressing by hostname; SOCKS5 supports IPv4, IPv6, and - hostnames. - -1.1. Extent of support - - Tor supports the SOCKS4, SOCKS4A, and SOCKS5 standards, except as follows: - - BOTH: - - The BIND command is not supported. - - SOCKS4,4A: - - SOCKS4 usernames are ignored. - - SOCKS5: - - The (SOCKS5) "UDP ASSOCIATE" command is not supported. - - IPv6 is not supported in CONNECT commands. - - Only the "NO AUTHENTICATION" (SOCKS5) authentication method [00] is - supported. - -2. Name lookup - - As an extension to SOCKS4A and SOCKS5, Tor implements a new command value, - "RESOLVE" [F0]. When Tor receives a "RESOLVE" SOCKS command, it initiates - a remote lookup of the hostname provided as the target address in the SOCKS - request. The reply is either an error (if the address couldn't be - resolved) or a success response. In the case of success, the address is - stored in the portion of the SOCKS response reserved for remote IP address. - - (We support RESOLVE in SOCKS4 too, even though it is unnecessary.) - - For SOCKS5 only, we support reverse resolution with a new command value, - "RESOLVE_PTR" [F1]. In response to a "RESOLVE_PTR" SOCKS5 command with - an IPv4 address as its target, Tor attempts to find the canonical - hostname for that IPv4 record, and returns it in the "server bound - address" portion of the reply. - (This command was not supported before Tor 0.1.2.2-alpha.) - -3. Other command extensions. - - Tor 0.1.2.4-alpha added a new command value: "CONNECT_DIR" [F2]. - In this case, Tor will open an encrypted direct TCP connection to the - directory port of the Tor server specified by address:port (the port - specified should be the ORPort of the server). It uses a one-hop tunnel - and a "BEGIN_DIR" relay cell to accomplish this secure connection. - - The F2 command value was removed in Tor 0.2.0.10-alpha in favor of a - new use_begindir flag in edge_connection_t. - -4. HTTP-resistance - - Tor checks the first byte of each SOCKS request to see whether it looks - more like an HTTP request (that is, it starts with a "G", "H", or "P"). If - so, Tor returns a small webpage, telling the user that his/her browser is - misconfigured. This is helpful for the many users who mistakenly try to - use Tor as an HTTP proxy instead of a SOCKS proxy. - -References: - [1] http://archive.socks.permeo.com/protocol/socks4.protocol - [2] http://archive.socks.permeo.com/protocol/socks4a.protocol - [3] SOCKS5: RFC1928 - diff --git a/orchid/doc/spec/tor-spec.txt b/orchid/doc/spec/tor-spec.txt deleted file mode 100644 index efa6029f..00000000 --- a/orchid/doc/spec/tor-spec.txt +++ /dev/null @@ -1,992 +0,0 @@ - - Tor Protocol Specification - - Roger Dingledine - Nick Mathewson - -Note: This document aims to specify Tor as implemented in 0.2.1.x. Future -versions of Tor may implement improved protocols, and compatibility is not -guaranteed. Compatibility notes are given for versions 0.1.1.15-rc and -later; earlier versions are not compatible with the Tor network as of this -writing. - -This specification is not a design document; most design criteria -are not examined. For more information on why Tor acts as it does, -see tor-design.pdf. - -0. Preliminaries - -0.1. Notation and encoding - - PK -- a public key. - SK -- a private key. - K -- a key for a symmetric cypher. - - a|b -- concatenation of 'a' and 'b'. - - [A0 B1 C2] -- a three-byte sequence, containing the bytes with - hexadecimal values A0, B1, and C2, in that order. - - All numeric values are encoded in network (big-endian) order. - - H(m) -- a cryptographic hash of m. - -0.2. Security parameters - - Tor uses a stream cipher, a public-key cipher, the Diffie-Hellman - protocol, and a hash function. - - KEY_LEN -- the length of the stream cipher's key, in bytes. - - PK_ENC_LEN -- the length of a public-key encrypted message, in bytes. - PK_PAD_LEN -- the number of bytes added in padding for public-key - encryption, in bytes. (The largest number of bytes that can be encrypted - in a single public-key operation is therefore PK_ENC_LEN-PK_PAD_LEN.) - - DH_LEN -- the number of bytes used to represent a member of the - Diffie-Hellman group. - DH_SEC_LEN -- the number of bytes used in a Diffie-Hellman private key (x). - - HASH_LEN -- the length of the hash function's output, in bytes. - - PAYLOAD_LEN -- The longest allowable cell payload, in bytes. (509) - - CELL_LEN -- The length of a Tor cell, in bytes. - -0.3. Ciphers - - For a stream cipher, we use 128-bit AES in counter mode, with an IV of all - 0 bytes. - - For a public-key cipher, we use RSA with 1024-bit keys and a fixed - exponent of 65537. We use OAEP-MGF1 padding, with SHA-1 as its digest - function. We leave the optional "Label" parameter unset. (For OAEP - padding, see ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1.pdf) - - For Diffie-Hellman, we use a generator (g) of 2. For the modulus (p), we - use the 1024-bit safe prime from rfc2409 section 6.2 whose hex - representation is: - - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08" - "8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" - "302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" - "A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" - "49286651ECE65381FFFFFFFFFFFFFFFF" - - As an optimization, implementations SHOULD choose DH private keys (x) of - 320 bits. Implementations that do this MUST never use any DH key more - than once. - [May other implementations reuse their DH keys?? -RD] - [Probably not. Conceivably, you could get away with changing DH keys once - per second, but there are too many oddball attacks for me to be - comfortable that this is safe. -NM] - - For a hash function, we use SHA-1. - - KEY_LEN=16. - DH_LEN=128; DH_SEC_LEN=40. - PK_ENC_LEN=128; PK_PAD_LEN=42. - HASH_LEN=20. - - When we refer to "the hash of a public key", we mean the SHA-1 hash of the - DER encoding of an ASN.1 RSA public key (as specified in PKCS.1). - - All "random" values should be generated with a cryptographically strong - random number generator, unless otherwise noted. - - The "hybrid encryption" of a byte sequence M with a public key PK is - computed as follows: - 1. If M is less than PK_ENC_LEN-PK_PAD_LEN, pad and encrypt M with PK. - 2. Otherwise, generate a KEY_LEN byte random key K. - Let M1 = the first PK_ENC_LEN-PK_PAD_LEN-KEY_LEN bytes of M, - and let M2 = the rest of M. - Pad and encrypt K|M1 with PK. Encrypt M2 with our stream cipher, - using the key K. Concatenate these encrypted values. - [XXX Note that this "hybrid encryption" approach does not prevent - an attacker from adding or removing bytes to the end of M. It also - allows attackers to modify the bytes not covered by the OAEP -- - see Goldberg's PET2006 paper for details. We will add a MAC to this - scheme one day. -RD] - -0.4. Other parameter values - - CELL_LEN=512 - -1. System overview - - Tor is a distributed overlay network designed to anonymize - low-latency TCP-based applications such as web browsing, secure shell, - and instant messaging. Clients choose a path through the network and - build a ``circuit'', in which each node (or ``onion router'' or ``OR'') - in the path knows its predecessor and successor, but no other nodes in - the circuit. Traffic flowing down the circuit is sent in fixed-size - ``cells'', which are unwrapped by a symmetric key at each node (like - the layers of an onion) and relayed downstream. - -1.1. Keys and names - - Every Tor server has multiple public/private keypairs: - - - A long-term signing-only "Identity key" used to sign documents and - certificates, and used to establish server identity. - - A medium-term "Onion key" used to decrypt onion skins when accepting - circuit extend attempts. (See 5.1.) Old keys MUST be accepted for at - least one week after they are no longer advertised. Because of this, - servers MUST retain old keys for a while after they're rotated. - - A short-term "Connection key" used to negotiate TLS connections. - Tor implementations MAY rotate this key as often as they like, and - SHOULD rotate this key at least once a day. - - Tor servers are also identified by "nicknames"; these are specified in - dir-spec.txt. - -2. Connections - - Connections between two Tor servers, or between a client and a server, - use TLS/SSLv3 for link authentication and encryption. All - implementations MUST support the SSLv3 ciphersuite - "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", and SHOULD support the TLS - ciphersuite "TLS_DHE_RSA_WITH_AES_128_CBC_SHA" if it is available. - - There are three acceptable ways to perform a TLS handshake when - connecting to a Tor server: "certificates up-front", "renegotiation", and - "backwards-compatible renegotiation". ("Backwards-compatible - renegotiation" is, as the name implies, compatible with both other - handshake types.) - - Before Tor 0.2.0.21, only "certificates up-front" was supported. In Tor - 0.2.0.21 or later, "backwards-compatible renegotiation" is used. - - In "certificates up-front", the connection initiator always sends a - two-certificate chain, consisting of an X.509 certificate using a - short-term connection public key and a second, self- signed X.509 - certificate containing its identity key. The other party sends a similar - certificate chain. The initiator's ClientHello MUST NOT include any - ciphersuites other than: - TLS_DHE_RSA_WITH_AES_256_CBC_SHA - TLS_DHE_RSA_WITH_AES_128_CBC_SHA - SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA - SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA - - In "renegotiation", the connection initiator sends no certificates, and - the responder sends a single connection certificate. Once the TLS - handshake is complete, the initiator renegotiates the handshake, with each - parties sending a two-certificate chain as in "certificates up-front". - The initiator's ClientHello MUST include at least once ciphersuite not in - the list above. The responder SHOULD NOT select any ciphersuite besides - those in the list above. - [The above "should not" is because some of the ciphers that - clients list may be fake.] - - In "backwards-compatible renegotiation", the connection initiator's - ClientHello MUST include at least one ciphersuite other than those listed - above. The connection responder examines the initiator's ciphersuite list - to see whether it includes any ciphers other than those included in the - list above. If extra ciphers are included, the responder proceeds as in - "renegotiation": it sends a single certificate and does not request - client certificates. Otherwise (in the case that no extra ciphersuites - are included in the ClientHello) the responder proceeds as in - "certificates up-front": it requests client certificates, and sends a - two-certificate chain. In either case, once the responder has sent its - certificate or certificates, the initiator counts them. If two - certificates have been sent, it proceeds as in "certificates up-front"; - otherwise, it proceeds as in "renegotiation". - - All new implementations of the Tor server protocol MUST support - "backwards-compatible renegotiation"; clients SHOULD do this too. If - this is not possible, new client implementations MUST support both - "renegotiation" and "certificates up-front" and use the router's - published link protocols list (see dir-spec.txt on the "protocols" entry) - to decide which to use. - - In all of the above handshake variants, certificates sent in the clear - SHOULD NOT include any strings to identify the host as a Tor server. In - the "renegotation" and "backwards-compatible renegotiation", the - initiator SHOULD chose a list of ciphersuites and TLS extensions chosen - to mimic one used by a popular web browser. - - Responders MUST NOT select any TLS ciphersuite that lacks ephemeral keys, - or whose symmetric keys are less then KEY_LEN bits, or whose digests are - less than HASH_LEN bits. Responders SHOULD NOT select any SSLv3 - ciphersuite other than those listed above. - - Even though the connection protocol is identical, we will think of the - initiator as either an onion router (OR) if it is willing to relay - traffic for other Tor users, or an onion proxy (OP) if it only handles - local requests. Onion proxies SHOULD NOT provide long-term-trackable - identifiers in their handshakes. - - In all handshake variants, once all certificates are exchanged, all - parties receiving certificates must confirm that the identity key is as - expected. (When initiating a connection, the expected identity key is - the one given in the directory; when creating a connection because of an - EXTEND cell, the expected identity key is the one given in the cell.) If - the key is not as expected, the party must close the connection. - - When connecting to an OR, all parties SHOULD reject the connection if that - OR has a malformed or missing certificate. When accepting an incoming - connection, an OR SHOULD NOT reject incoming connections from parties with - malformed or missing certificates. (However, an OR should not believe - that an incoming connection is from another OR unless the certificates - are present and well-formed.) - - [Before version 0.1.2.8-rc, ORs rejected incoming connections from ORs and - OPs alike if their certificates were missing or malformed.] - - Once a TLS connection is established, the two sides send cells - (specified below) to one another. Cells are sent serially. All - cells are CELL_LEN bytes long. Cells may be sent embedded in TLS - records of any size or divided across TLS records, but the framing - of TLS records MUST NOT leak information about the type or contents - of the cells. - - TLS connections are not permanent. Either side MAY close a connection - if there are no circuits running over it and an amount of time - (KeepalivePeriod, defaults to 5 minutes) has passed since the last time - any traffic was transmitted over the TLS connection. Clients SHOULD - also hold a TLS connection with no circuits open, if it is likely that a - circuit will be built soon using that connection. - - (As an exception, directory servers may try to stay connected to all of - the ORs -- though this will be phased out for the Tor 0.1.2.x release.) - - To avoid being trivially distinguished from servers, client-only Tor - instances are encouraged but not required to use a two-certificate chain - as well. Clients SHOULD NOT keep using the same certificates when - their IP address changes. Clients MAY send no certificates at all. - -3. Cell Packet format - - The basic unit of communication for onion routers and onion - proxies is a fixed-width "cell". - - On a version 1 connection, each cell contains the following - fields: - - CircID [2 bytes] - Command [1 byte] - Payload (padded with 0 bytes) [PAYLOAD_LEN bytes] - - On a version 2 connection, all cells are as in version 1 connections, - except for the initial VERSIONS cell, whose format is: - - Circuit [2 octets; set to 0] - Command [1 octet; set to 7 for VERSIONS] - Length [2 octets; big-endian integer] - Payload [Length bytes] - - The CircID field determines which circuit, if any, the cell is - associated with. - - The 'Command' field holds one of the following values: - 0 -- PADDING (Padding) (See Sec 7.2) - 1 -- CREATE (Create a circuit) (See Sec 5.1) - 2 -- CREATED (Acknowledge create) (See Sec 5.1) - 3 -- RELAY (End-to-end data) (See Sec 5.5 and 6) - 4 -- DESTROY (Stop using a circuit) (See Sec 5.4) - 5 -- CREATE_FAST (Create a circuit, no PK) (See Sec 5.1) - 6 -- CREATED_FAST (Circuit created, no PK) (See Sec 5.1) - 7 -- VERSIONS (Negotiate proto version) (See Sec 4) - 8 -- NETINFO (Time and address info) (See Sec 4) - 9 -- RELAY_EARLY (End-to-end data; limited) (See sec 5.6) - - The interpretation of 'Payload' depends on the type of the cell. - PADDING: Payload is unused. - CREATE: Payload contains the handshake challenge. - CREATED: Payload contains the handshake response. - RELAY: Payload contains the relay header and relay body. - DESTROY: Payload contains a reason for closing the circuit. - (see 5.4) - Upon receiving any other value for the command field, an OR must - drop the cell. Since more cell types may be added in the future, ORs - should generally not warn when encountering unrecognized commands. - - The payload is padded with 0 bytes. - - PADDING cells are currently used to implement connection keepalive. - If there is no other traffic, ORs and OPs send one another a PADDING - cell every few minutes. - - CREATE, CREATED, and DESTROY cells are used to manage circuits; - see section 5 below. - - RELAY cells are used to send commands and data along a circuit; see - section 6 below. - - VERSIONS and NETINFO cells are used to set up connections. See section 4 - below. - -4. Negotiating and initializing connections - -4.1. Negotiating versions with VERSIONS cells - - There are multiple instances of the Tor link connection protocol. Any - connection negotiated using the "certificates up front" handshake (see - section 2 above) is "version 1". In any connection where both parties - have behaved as in the "renegotiation" handshake, the link protocol - version is 2 or higher. - - To determine the version, in any connection where the "renegotiation" - handshake was used (that is, where the server sent only one certificate - at first and where the client did not send any certificates until - renegotiation), both parties MUST send a VERSIONS cell immediately after - the renegotiation is finished, before any other cells are sent. Parties - MUST NOT send any other cells on a connection until they have received a - VERSIONS cell. - - The payload in a VERSIONS cell is a series of big-endian two-byte - integers. Both parties MUST select as the link protocol version the - highest number contained both in the VERSIONS cell they sent and in the - versions cell they received. If they have no such version in common, - they cannot communicate and MUST close the connection. - - Since the version 1 link protocol does not use the "renegotiation" - handshake, implementations MUST NOT list version 1 in their VERSIONS - cell. - -4.2. NETINFO cells - - If version 2 or higher is negotiated, each party sends the other a - NETINFO cell. The cell's payload is: - - Timestamp [4 bytes] - Other OR's address [variable] - Number of addresses [1 byte] - This OR's addresses [variable] - - The address format is a type/length/value sequence as given in section - 6.4 below. The timestamp is a big-endian unsigned integer number of - seconds since the unix epoch. - - Implementations MAY use the timestamp value to help decide if their - clocks are skewed. Initiators MAY use "other OR's address" to help - learn which address their connections are originating from, if they do - not know it. Initiators SHOULD use "this OR's address" to make sure - that they have connected to another OR at its canonical address. - - [As of 0.2.0.23-rc, implementations use none of the above values.] - - -5. Circuit management - -5.1. CREATE and CREATED cells - - Users set up circuits incrementally, one hop at a time. To create a - new circuit, OPs send a CREATE cell to the first node, with the - first half of the DH handshake; that node responds with a CREATED - cell with the second half of the DH handshake plus the first 20 bytes - of derivative key data (see section 5.2). To extend a circuit past - the first hop, the OP sends an EXTEND relay cell (see section 5) - which instructs the last node in the circuit to send a CREATE cell - to extend the circuit. - - The payload for a CREATE cell is an 'onion skin', which consists - of the first step of the DH handshake data (also known as g^x). - This value is hybrid-encrypted (see 0.3) to Bob's onion key, giving - an onion-skin of: - PK-encrypted: - Padding [PK_PAD_LEN bytes] - Symmetric key [KEY_LEN bytes] - First part of g^x [PK_ENC_LEN-PK_PAD_LEN-KEY_LEN bytes] - Symmetrically encrypted: - Second part of g^x [DH_LEN-(PK_ENC_LEN-PK_PAD_LEN-KEY_LEN) - bytes] - - The relay payload for an EXTEND relay cell consists of: - Address [4 bytes] - Port [2 bytes] - Onion skin [DH_LEN+KEY_LEN+PK_PAD_LEN bytes] - Identity fingerprint [HASH_LEN bytes] - - The port and address field denote the IPV4 address and port of the next - onion router in the circuit; the public key hash is the hash of the PKCS#1 - ASN1 encoding of the next onion router's identity (signing) key. (See 0.3 - above.) Including this hash allows the extending OR verify that it is - indeed connected to the correct target OR, and prevents certain - man-in-the-middle attacks. - - The payload for a CREATED cell, or the relay payload for an - EXTENDED cell, contains: - DH data (g^y) [DH_LEN bytes] - Derivative key data (KH) [HASH_LEN bytes] - - The CircID for a CREATE cell is an arbitrarily chosen 2-byte integer, - selected by the node (OP or OR) that sends the CREATE cell. To prevent - CircID collisions, when one node sends a CREATE cell to another, it chooses - from only one half of the possible values based on the ORs' public - identity keys: if the sending node has a lower key, it chooses a CircID with - an MSB of 0; otherwise, it chooses a CircID with an MSB of 1. - - (An OP with no public key MAY choose any CircID it wishes, since an OP - never needs to process a CREATE cell.) - - Public keys are compared numerically by modulus. - - As usual with DH, x and y MUST be generated randomly. - -5.1.1. CREATE_FAST/CREATED_FAST cells - - When initializing the first hop of a circuit, the OP has already - established the OR's identity and negotiated a secret key using TLS. - Because of this, it is not always necessary for the OP to perform the - public key operations to create a circuit. In this case, the - OP MAY send a CREATE_FAST cell instead of a CREATE cell for the first - hop only. The OR responds with a CREATED_FAST cell, and the circuit is - created. - - A CREATE_FAST cell contains: - - Key material (X) [HASH_LEN bytes] - - A CREATED_FAST cell contains: - - Key material (Y) [HASH_LEN bytes] - Derivative key data [HASH_LEN bytes] (See 5.2 below) - - The values of X and Y must be generated randomly. - - If an OR sees a circuit created with CREATE_FAST, the OR is sure to be the - first hop of a circuit. ORs SHOULD reject attempts to create streams with - RELAY_BEGIN exiting the circuit at the first hop: letting Tor be used as a - single hop proxy makes exit nodes a more attractive target for compromise. - -5.2. Setting circuit keys - - Once the handshake between the OP and an OR is completed, both can - now calculate g^xy with ordinary DH. Before computing g^xy, both client - and server MUST verify that the received g^x or g^y value is not degenerate; - that is, it must be strictly greater than 1 and strictly less than p-1 - where p is the DH modulus. Implementations MUST NOT complete a handshake - with degenerate keys. Implementations MUST NOT discard other "weak" - g^x values. - - (Discarding degenerate keys is critical for security; if bad keys - are not discarded, an attacker can substitute the server's CREATED - cell's g^y with 0 or 1, thus creating a known g^xy and impersonating - the server. Discarding other keys may allow attacks to learn bits of - the private key.) - - If CREATE or EXTEND is used to extend a circuit, the client and server - base their key material on K0=g^xy, represented as a big-endian unsigned - integer. - - If CREATE_FAST is used, the client and server base their key material on - K0=X|Y. - - From the base key material K0, they compute KEY_LEN*2+HASH_LEN*3 bytes of - derivative key data as - K = H(K0 | [00]) | H(K0 | [01]) | H(K0 | [02]) | ... - - The first HASH_LEN bytes of K form KH; the next HASH_LEN form the forward - digest Df; the next HASH_LEN 41-60 form the backward digest Db; the next - KEY_LEN 61-76 form Kf, and the final KEY_LEN form Kb. Excess bytes from K - are discarded. - - KH is used in the handshake response to demonstrate knowledge of the - computed shared key. Df is used to seed the integrity-checking hash - for the stream of data going from the OP to the OR, and Db seeds the - integrity-checking hash for the data stream from the OR to the OP. Kf - is used to encrypt the stream of data going from the OP to the OR, and - Kb is used to encrypt the stream of data going from the OR to the OP. - -5.3. Creating circuits - - When creating a circuit through the network, the circuit creator - (OP) performs the following steps: - - 1. Choose an onion router as an exit node (R_N), such that the onion - router's exit policy includes at least one pending stream that - needs a circuit (if there are any). - - 2. Choose a chain of (N-1) onion routers - (R_1...R_N-1) to constitute the path, such that no router - appears in the path twice. - - 3. If not already connected to the first router in the chain, - open a new connection to that router. - - 4. Choose a circID not already in use on the connection with the - first router in the chain; send a CREATE cell along the - connection, to be received by the first onion router. - - 5. Wait until a CREATED cell is received; finish the handshake - and extract the forward key Kf_1 and the backward key Kb_1. - - 6. For each subsequent onion router R (R_2 through R_N), extend - the circuit to R. - - To extend the circuit by a single onion router R_M, the OP performs - these steps: - - 1. Create an onion skin, encrypted to R_M's public onion key. - - 2. Send the onion skin in a relay EXTEND cell along - the circuit (see section 5). - - 3. When a relay EXTENDED cell is received, verify KH, and - calculate the shared keys. The circuit is now extended. - - When an onion router receives an EXTEND relay cell, it sends a CREATE - cell to the next onion router, with the enclosed onion skin as its - payload. As special cases, if the extend cell includes a digest of - all zeroes, or asks to extend back to the relay that sent the extend - cell, the circuit will fail and be torn down. The initiating onion - router chooses some circID not yet used on the connection between the - two onion routers. (But see section 5.1. above, concerning choosing - circIDs based on lexicographic order of nicknames.) - - When an onion router receives a CREATE cell, if it already has a - circuit on the given connection with the given circID, it drops the - cell. Otherwise, after receiving the CREATE cell, it completes the - DH handshake, and replies with a CREATED cell. Upon receiving a - CREATED cell, an onion router packs it payload into an EXTENDED relay - cell (see section 5), and sends that cell up the circuit. Upon - receiving the EXTENDED relay cell, the OP can retrieve g^y. - - (As an optimization, OR implementations may delay processing onions - until a break in traffic allows time to do so without harming - network latency too greatly.) - -5.3.1. Canonical connections - - It is possible for an attacker to launch a man-in-the-middle attack - against a connection by telling OR Alice to extend to OR Bob at some - address X controlled by the attacker. The attacker cannot read the - encrypted traffic, but the attacker is now in a position to count all - bytes sent between Alice and Bob (assuming Alice was not already - connected to Bob.) - - To prevent this, when an OR we gets an extend request, it SHOULD use an - existing OR connection if the ID matches, and ANY of the following - conditions hold: - - The IP matches the requested IP. - - The OR knows that the IP of the connection it's using is canonical - because it was listed in the NETINFO cell. - - The OR knows that the IP of the connection it's using is canonical - because it was listed in the server descriptor. - - [This is not implemented in Tor 0.2.0.23-rc.] - -5.4. Tearing down circuits - - Circuits are torn down when an unrecoverable error occurs along - the circuit, or when all streams on a circuit are closed and the - circuit's intended lifetime is over. Circuits may be torn down - either completely or hop-by-hop. - - To tear down a circuit completely, an OR or OP sends a DESTROY - cell to the adjacent nodes on that circuit, using the appropriate - direction's circID. - - Upon receiving an outgoing DESTROY cell, an OR frees resources - associated with the corresponding circuit. If it's not the end of - the circuit, it sends a DESTROY cell for that circuit to the next OR - in the circuit. If the node is the end of the circuit, then it tears - down any associated edge connections (see section 6.1). - - After a DESTROY cell has been processed, an OR ignores all data or - destroy cells for the corresponding circuit. - - To tear down part of a circuit, the OP may send a RELAY_TRUNCATE cell - signaling a given OR (Stream ID zero). That OR sends a DESTROY - cell to the next node in the circuit, and replies to the OP with a - RELAY_TRUNCATED cell. - - When an unrecoverable error occurs along one connection in a - circuit, the nodes on either side of the connection should, if they - are able, act as follows: the node closer to the OP should send a - RELAY_TRUNCATED cell towards the OP; the node farther from the OP - should send a DESTROY cell down the circuit. - - The payload of a RELAY_TRUNCATED or DESTROY cell contains a single octet, - describing why the circuit is being closed or truncated. When sending a - TRUNCATED or DESTROY cell because of another TRUNCATED or DESTROY cell, - the error code should be propagated. The origin of a circuit always sets - this error code to 0, to avoid leaking its version. - - The error codes are: - 0 -- NONE (No reason given.) - 1 -- PROTOCOL (Tor protocol violation.) - 2 -- INTERNAL (Internal error.) - 3 -- REQUESTED (A client sent a TRUNCATE command.) - 4 -- HIBERNATING (Not currently operating; trying to save bandwidth.) - 5 -- RESOURCELIMIT (Out of memory, sockets, or circuit IDs.) - 6 -- CONNECTFAILED (Unable to reach server.) - 7 -- OR_IDENTITY (Connected to server, but its OR identity was not - as expected.) - 8 -- OR_CONN_CLOSED (The OR connection that was carrying this circuit - died.) - 9 -- FINISHED (The circuit has expired for being dirty or old.) - 10 -- TIMEOUT (Circuit construction took too long) - 11 -- DESTROYED (The circuit was destroyed w/o client TRUNCATE) - 12 -- NOSUCHSERVICE (Request for unknown hidden service) - -5.5. Routing relay cells - - When an OR receives a RELAY or RELAY_EARLY cell, it checks the cell's - circID and determines whether it has a corresponding circuit along that - connection. If not, the OR drops the cell. - - Otherwise, if the OR is not at the OP edge of the circuit (that is, - either an 'exit node' or a non-edge node), it de/encrypts the payload - with the stream cipher, as follows: - 'Forward' relay cell (same direction as CREATE): - Use Kf as key; decrypt. - 'Back' relay cell (opposite direction from CREATE): - Use Kb as key; encrypt. - Note that in counter mode, decrypt and encrypt are the same operation. - - The OR then decides whether it recognizes the relay cell, by - inspecting the payload as described in section 6.1 below. If the OR - recognizes the cell, it processes the contents of the relay cell. - Otherwise, it passes the decrypted relay cell along the circuit if - the circuit continues. If the OR at the end of the circuit - encounters an unrecognized relay cell, an error has occurred: the OR - sends a DESTROY cell to tear down the circuit. - - When a relay cell arrives at an OP, the OP decrypts the payload - with the stream cipher as follows: - OP receives data cell: - For I=N...1, - Decrypt with Kb_I. If the payload is recognized (see - section 6..1), then stop and process the payload. - - For more information, see section 6 below. - -5.6. Handling relay_early cells - - A RELAY_EARLY cell is designed to limit the length any circuit can reach. - When an OR receives a RELAY_EARLY cell, and the next node in the circuit - is speaking v2 of the link protocol or later, the OR relays the cell as a - RELAY_EARLY cell. Otherwise, it relays it as a RELAY cell. - - If a node ever receives more than 8 RELAY_EARLY cells on a given - outbound circuit, it SHOULD close the circuit. (For historical reasons, - we don't limit the number of inbound RELAY_EARLY cells; they should - be harmless anyway because clients won't accept extend requests. See - bug 1038.) - - When speaking v2 of the link protocol or later, clients MUST only send - EXTEND cells inside RELAY_EARLY cells. Clients SHOULD send the first ~8 - RELAY cells that are not targeted at the first hop of any circuit as - RELAY_EARLY cells too, in order to partially conceal the circuit length. - - [In a future version of Tor, servers will reject any EXTEND cell not - received in a RELAY_EARLY cell. See proposal 110.] - -6. Application connections and stream management - -6.1. Relay cells - - Within a circuit, the OP and the exit node use the contents of - RELAY packets to tunnel end-to-end commands and TCP connections - ("Streams") across circuits. End-to-end commands can be initiated - by either edge; streams are initiated by the OP. - - 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] - - The relay commands are: - 1 -- RELAY_BEGIN [forward] - 2 -- RELAY_DATA [forward or backward] - 3 -- RELAY_END [forward or backward] - 4 -- RELAY_CONNECTED [backward] - 5 -- RELAY_SENDME [forward or backward] [sometimes control] - 6 -- RELAY_EXTEND [forward] [control] - 7 -- RELAY_EXTENDED [backward] [control] - 8 -- RELAY_TRUNCATE [forward] [control] - 9 -- RELAY_TRUNCATED [backward] [control] - 10 -- RELAY_DROP [forward or backward] [control] - 11 -- RELAY_RESOLVE [forward] - 12 -- RELAY_RESOLVED [backward] - 13 -- RELAY_BEGIN_DIR [forward] - - 32..40 -- Used for hidden services; see rend-spec.txt. - - Commands labelled as "forward" must only be sent by the originator - of the circuit. Commands labelled as "backward" must only be sent by - other nodes in the circuit back to the originator. Commands marked - as either can be sent either by the originator or other nodes. - - The 'recognized' field in any unencrypted relay payload is always set - to zero; the 'digest' field is computed as the first four bytes of - the running digest of all the bytes that have been destined for - this hop of the circuit or originated from this hop of the circuit, - seeded from Df or Db respectively (obtained in section 5.2 above), - and including this RELAY cell's entire payload (taken with the digest - field set to zero). - - When the 'recognized' field of a RELAY cell is zero, and the digest - is correct, the cell is considered "recognized" for the purposes of - decryption (see section 5.5 above). - - (The digest does not include any bytes from relay cells that do - not start or end at this hop of the circuit. That is, it does not - include forwarded data. Therefore if 'recognized' is zero but the - digest does not match, the running digest at that node should - not be updated, and the cell should be forwarded on.) - - All RELAY cells pertaining to the same tunneled stream have the - same stream ID. StreamIDs are chosen arbitrarily by the OP. RELAY - cells that affect the entire circuit rather than a particular - stream use a StreamID of zero -- they are marked in the table above - as "[control]" style cells. (Sendme cells are marked as "sometimes - control" because they can take include a StreamID or not depending - on their purpose -- see Section 7.) - - The 'Length' field of a relay cell contains the number of bytes in - the relay payload which contain real payload data. The remainder of - the payload is padded with NUL bytes. - - If the RELAY cell is recognized but the relay command is not - understood, the cell must be dropped and ignored. Its contents - still count with respect to the digests, though. - -6.2. Opening streams and transferring data - - To open a new anonymized TCP connection, the OP chooses an open - circuit to an exit that may be able to connect to the destination - address, selects an arbitrary StreamID not yet used on that circuit, - and constructs a RELAY_BEGIN cell with a payload encoding the address - and port of the destination host. The payload format is: - - ADDRESS | ':' | PORT | [00] - - where ADDRESS can be a DNS hostname, or an IPv4 address in - dotted-quad format, or an IPv6 address surrounded by square brackets; - and where PORT is a decimal integer between 1 and 65535, inclusive. - - [What is the [00] for? -NM] - [It's so the payload is easy to parse out with string funcs -RD] - - Upon receiving this cell, the exit node resolves the address as - necessary, and opens a new TCP connection to the target port. If the - address cannot be resolved, or a connection can't be established, the - exit node replies with a RELAY_END cell. (See 6.4 below.) - Otherwise, the exit node replies with a RELAY_CONNECTED cell, whose - payload is in one of the following formats: - The IPv4 address to which the connection was made [4 octets] - A number of seconds (TTL) for which the address may be cached [4 octets] - or - Four zero-valued octets [4 octets] - An address type (6) [1 octet] - The IPv6 address to which the connection was made [16 octets] - A number of seconds (TTL) for which the address may be cached [4 octets] - [XXXX No version of Tor currently generates the IPv6 format.] - - [Tor servers before 0.1.2.0 set the TTL field to a fixed value. Later - versions set the TTL to the last value seen from a DNS server, and expire - their own cached entries after a fixed interval. This prevents certain - attacks.] - - The OP waits for a RELAY_CONNECTED cell before sending any data. - Once a connection has been established, the OP and exit node - package stream data in RELAY_DATA cells, and upon receiving such - cells, echo their contents to the corresponding TCP stream. - RELAY_DATA cells sent to unrecognized streams are dropped. - - Relay RELAY_DROP cells are long-range dummies; upon receiving such - a cell, the OR or OP must drop it. - -6.2.1. Opening a directory stream - - If a Tor server is a directory server, it should respond to a - RELAY_BEGIN_DIR cell as if it had received a BEGIN cell requesting a - connection to its directory port. RELAY_BEGIN_DIR cells ignore exit - policy, since the stream is local to the Tor process. - - If the Tor server is not running a directory service, it should respond - with a REASON_NOTDIRECTORY RELAY_END cell. - - Clients MUST generate an all-zero payload for RELAY_BEGIN_DIR cells, - and servers MUST ignore the payload. - - [RELAY_BEGIN_DIR was not supported before Tor 0.1.2.2-alpha; clients - SHOULD NOT send it to routers running earlier versions of Tor.] - -6.3. Closing streams - - When an anonymized TCP connection is closed, or an edge node - encounters error on any stream, it sends a 'RELAY_END' cell along the - circuit (if possible) and closes the TCP connection immediately. If - an edge node receives a 'RELAY_END' cell for any stream, it closes - the TCP connection completely, and sends nothing more along the - circuit for that stream. - - The payload of a RELAY_END cell begins with a single 'reason' byte to - describe why the stream is closing, plus optional data (depending on - the reason.) The values are: - - 1 -- REASON_MISC (catch-all for unlisted reasons) - 2 -- REASON_RESOLVEFAILED (couldn't look up hostname) - 3 -- REASON_CONNECTREFUSED (remote host refused connection) [*] - 4 -- REASON_EXITPOLICY (OR refuses to connect to host or port) - 5 -- REASON_DESTROY (Circuit is being destroyed) - 6 -- REASON_DONE (Anonymized TCP connection was closed) - 7 -- REASON_TIMEOUT (Connection timed out, or OR timed out - while connecting) - 8 -- (unallocated) [**] - 9 -- REASON_HIBERNATING (OR is temporarily hibernating) - 10 -- REASON_INTERNAL (Internal error at the OR) - 11 -- REASON_RESOURCELIMIT (OR has no resources to fulfill request) - 12 -- REASON_CONNRESET (Connection was unexpectedly reset) - 13 -- REASON_TORPROTOCOL (Sent when closing connection because of - Tor protocol violations.) - 14 -- REASON_NOTDIRECTORY (Client sent RELAY_BEGIN_DIR to a - non-directory server.) - - (With REASON_EXITPOLICY, the 4-byte IPv4 address or 16-byte IPv6 address - forms the optional data, along with a 4-byte TTL; no other reason - currently has extra data.) - - OPs and ORs MUST accept reasons not on the above list, since future - versions of Tor may provide more fine-grained reasons. - - Tors SHOULD NOT send any reason except REASON_MISC for a stream that they - have originated. - - [*] Older versions of Tor also send this reason when connections are - reset. - [**] Due to a bug in versions of Tor through 0095, error reason 8 must - remain allocated until that version is obsolete. - - --- [The rest of this section describes unimplemented functionality.] - - Because TCP connections can be half-open, we follow an equivalent - to TCP's FIN/FIN-ACK/ACK protocol to close streams. - - An exit connection can have a TCP stream in one of three states: - 'OPEN', 'DONE_PACKAGING', and 'DONE_DELIVERING'. For the purposes - of modeling transitions, we treat 'CLOSED' as a fourth state, - although connections in this state are not, in fact, tracked by the - onion router. - - A stream begins in the 'OPEN' state. Upon receiving a 'FIN' from - the corresponding TCP connection, the edge node sends a 'RELAY_FIN' - cell along the circuit and changes its state to 'DONE_PACKAGING'. - Upon receiving a 'RELAY_FIN' cell, an edge node sends a 'FIN' to - the corresponding TCP connection (e.g., by calling - shutdown(SHUT_WR)) and changing its state to 'DONE_DELIVERING'. - - When a stream in already in 'DONE_DELIVERING' receives a 'FIN', it - also sends a 'RELAY_FIN' along the circuit, and changes its state - to 'CLOSED'. When a stream already in 'DONE_PACKAGING' receives a - 'RELAY_FIN' cell, it sends a 'FIN' and changes its state to - 'CLOSED'. - - If an edge node encounters an error on any stream, it sends a - 'RELAY_END' cell (if possible) and closes the stream immediately. - -6.4. Remote hostname lookup - - To find the address associated with a hostname, the OP sends a - RELAY_RESOLVE cell containing the hostname to be resolved with a nul - terminating byte. (For a reverse lookup, the OP sends a RELAY_RESOLVE - cell containing an in-addr.arpa address.) The OR replies with a - RELAY_RESOLVED cell containing a status byte, and any number of - answers. Each answer is of the form: - Type (1 octet) - Length (1 octet) - Value (variable-width) - TTL (4 octets) - "Length" is the length of the Value field. - "Type" is one of: - 0x00 -- Hostname - 0x04 -- IPv4 address - 0x06 -- IPv6 address - 0xF0 -- Error, transient - 0xF1 -- Error, nontransient - - If any answer has a type of 'Error', then no other answer may be given. - - The RELAY_RESOLVE cell must use a nonzero, distinct streamID; the - corresponding RELAY_RESOLVED cell must use the same streamID. No stream - is actually created by the OR when resolving the name. - -7. Flow control - -7.1. Link throttling - - Each client or relay should do appropriate bandwidth throttling to - keep its user happy. - - Communicants rely on TCP's default flow control to push back when they - stop reading. - - The mainline Tor implementation uses token buckets (one for reads, - one for writes) for the rate limiting. - - Since 0.2.0.x, Tor has let the user specify an additional pair of - token buckets for "relayed" traffic, so people can deploy a Tor relay - with strict rate limiting, but also use the same Tor as a client. To - avoid partitioning concerns we combine both classes of traffic over a - given OR connection, and keep track of the last time we read or wrote - a high-priority (non-relayed) cell. If it's been less than N seconds - (currently N=30), we give the whole connection high priority, else we - give the whole connection low priority. We also give low priority - to reads and writes for connections that are serving directory - information. See proposal 111 for details. - -7.2. Link padding - - Link padding can be created by sending PADDING cells along the - connection; relay cells of type "DROP" can be used for long-range - padding. - - Currently nodes are not required to do any sort of link padding or - dummy traffic. Because strong attacks exist even with link padding, - and because link padding greatly increases the bandwidth requirements - for running a node, we plan to leave out link padding until this - tradeoff is better understood. - -7.3. Circuit-level flow control - - To control a circuit's bandwidth usage, each OR keeps track of two - 'windows', consisting of how many RELAY_DATA cells it is allowed to - originate (package for transmission), and how many RELAY_DATA cells - it is willing to consume (receive for local streams). These limits - do not apply to cells that the OR receives from one host and relays - to another. - - Each 'window' value is initially set to 1000 data cells - in each direction (cells that are not data cells do not affect - the window). When an OR is willing to deliver more cells, it sends a - RELAY_SENDME cell towards the OP, with Stream ID zero. When an OR - receives a RELAY_SENDME cell with stream ID zero, it increments its - packaging window. - - Each of these cells increments the corresponding window by 100. - - The OP behaves identically, except that it must track a packaging - window and a delivery window for every OR in the circuit. - - An OR or OP sends cells to increment its delivery window when the - corresponding window value falls under some threshold (900). - - If a packaging window reaches 0, the OR or OP stops reading from - TCP connections for all streams on the corresponding circuit, and - sends no more RELAY_DATA cells until receiving a RELAY_SENDME cell. -[this stuff is badly worded; copy in the tor-design section -RD] - -7.4. Stream-level flow control - - Edge nodes use RELAY_SENDME cells to implement end-to-end flow - control for individual connections across circuits. Similarly to - circuit-level flow control, edge nodes begin with a window of cells - (500) per stream, and increment the window by a fixed value (50) - upon receiving a RELAY_SENDME cell. Edge nodes initiate RELAY_SENDME - cells when both a) the window is <= 450, and b) there are less than - ten cell payloads remaining to be flushed at that edge. - -A.1. Differences between spec and implementation - -- The current specification requires all ORs to have IPv4 addresses, but - allows servers to exit and resolve to IPv6 addresses, and to declare IPv6 - addresses in their exit policies. The current codebase has no IPv6 - support at all. - diff --git a/orchid/logging.properties b/orchid/logging.properties deleted file mode 100644 index 5a212d03..00000000 --- a/orchid/logging.properties +++ /dev/null @@ -1,8 +0,0 @@ -handlers=java.util.logging.ConsoleHandler -.level = INFO - -java.util.logging.ConsoleHandler.level = FINEST -java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter -java.util.logging.SimpleFormatter.format =[%1$tT] %4$s: %5$s%6$s%n - -# com.subgraph.orchid.circuits.level=FINE diff --git a/orchid/opt/xmlrpc/com/subgraph/orchid/xmlrpc/OrchidXmlRpcTransport.java b/orchid/opt/xmlrpc/com/subgraph/orchid/xmlrpc/OrchidXmlRpcTransport.java deleted file mode 100644 index bce6d6ab..00000000 --- a/orchid/opt/xmlrpc/com/subgraph/orchid/xmlrpc/OrchidXmlRpcTransport.java +++ /dev/null @@ -1,309 +0,0 @@ -package com.subgraph.orchid.xmlrpc; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.ConnectException; -import java.net.Socket; -import java.net.URL; -import java.net.UnknownHostException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.StringTokenizer; -import java.util.logging.Logger; - -import javax.net.SocketFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; - -import org.apache.xmlrpc.XmlRpcException; -import org.apache.xmlrpc.XmlRpcRequest; -import org.apache.xmlrpc.client.XmlRpcClient; -import org.apache.xmlrpc.client.XmlRpcClientException; -import org.apache.xmlrpc.client.XmlRpcHttpClientConfig; -import org.apache.xmlrpc.client.XmlRpcHttpTransport; -import org.apache.xmlrpc.client.XmlRpcHttpTransportException; -import org.apache.xmlrpc.client.XmlRpcLiteHttpTransport; -import org.apache.xmlrpc.common.XmlRpcStreamRequestConfig; -import org.apache.xmlrpc.util.HttpUtil; -import org.apache.xmlrpc.util.LimitedInputStream; -import org.xml.sax.SAXException; - -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.sockets.AndroidSSLSocketFactory; - -public class OrchidXmlRpcTransport extends XmlRpcHttpTransport { - - private final static Logger logger = Logger.getLogger(OrchidXmlRpcTransport.class.getName()); - - private final SocketFactory socketFactory; - private final SSLContext sslContext; - - private SSLSocketFactory sslSocketFactory; - - public OrchidXmlRpcTransport(XmlRpcClient pClient, SocketFactory socketFactory, SSLContext sslContext) { - super(pClient, userAgent); - this.socketFactory = socketFactory; - this.sslContext = sslContext; - } - - public synchronized SSLSocketFactory getSSLSocketFactory() { - if(sslSocketFactory == null) { - sslSocketFactory = createSSLSocketFactory(); - } - return sslSocketFactory; - } - - private SSLSocketFactory createSSLSocketFactory() { - if(Tor.isAndroidRuntime()) { - return createAndroidSSLSocketFactory(); - } - if(sslContext == null) { - return (SSLSocketFactory) SSLSocketFactory.getDefault(); - } else { - return sslContext.getSocketFactory(); - } - } - - private SSLSocketFactory createAndroidSSLSocketFactory() { - if(sslContext == null) { - try { - return new AndroidSSLSocketFactory(); - } catch (NoSuchAlgorithmException e) { - logger.severe("Failed to create default ssl context"); - System.exit(1); - return null; - } - } else { - return new AndroidSSLSocketFactory(sslContext); - } - } - - protected Socket newSocket(boolean pSSL, String pHostName, int pPort) throws UnknownHostException, IOException { - final Socket s = socketFactory.createSocket(pHostName, pPort); - if(pSSL) { - return getSSLSocketFactory().createSocket(s, pHostName, pPort, true); - } else { - return s; - } - } - - private static final String userAgent = USER_AGENT + " (Lite HTTP Transport)"; - private boolean ssl; - private String hostname; - private String host; - private int port; - private String uri; - private Socket socket; - private OutputStream output; - private InputStream input; - private final Map headers = new HashMap(); - private boolean responseGzipCompressed = false; - private XmlRpcHttpClientConfig config; - - - public Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException { - config = (XmlRpcHttpClientConfig) pRequest.getConfig(); - URL url = config.getServerURL(); - ssl = "https".equals(url.getProtocol()); - hostname = url.getHost(); - int p = url.getPort(); - port = p < 1 ? 80 : p; - String u = url.getFile(); - uri = (u == null || "".equals(u)) ? "/" : u; - host = port == 80 ? hostname : hostname + ":" + port; - headers.put("Host", host); - return super.sendRequest(pRequest); - } - - protected void setRequestHeader(String pHeader, String pValue) { - Object value = headers.get(pHeader); - if (value == null) { - headers.put(pHeader, pValue); - } else { - List list; - if (value instanceof String) { - list = new ArrayList(); - list.add((String)value); - headers.put(pHeader, list); - } else { - list = (List) value; - } - list.add(pValue); - } - } - - protected void close() throws XmlRpcClientException { - IOException e = null; - if (input != null) { - try { - input.close(); - } catch (IOException ex) { - e = ex; - } - } - if (output != null) { - try { - output.close(); - } catch (IOException ex) { - if (e != null) { - e = ex; - } - } - } - if (socket != null) { - try { - socket.close(); - } catch (IOException ex) { - if (e != null) { - e = ex; - } - } - } - if (e != null) { - throw new XmlRpcClientException("Failed to close connection: " + e.getMessage(), e); - } - } - - private OutputStream getOutputStream() throws XmlRpcException { - try { - final int retries = 3; - final int delayMillis = 100; - - for (int tries = 0; ; tries++) { - try { - socket = newSocket(ssl, hostname, port); - output = new BufferedOutputStream(socket.getOutputStream()){ - /** Closing the output stream would close the whole socket, which we don't want, - * because the don't want until the request is processed completely. - * A close will later occur within - * {@link XmlRpcLiteHttpTransport#close()}. - */ - public void close() throws IOException { - flush(); - if(!(socket instanceof SSLSocket)) { - socket.shutdownOutput(); - } - } - }; - break; - } catch (ConnectException e) { - if (tries >= retries) { - throw new XmlRpcException("Failed to connect to " - + hostname + ":" + port + ": " + e.getMessage(), e); - } else { - try { - Thread.sleep(delayMillis); - } catch (InterruptedException ignore) { - } - } - } - } - sendRequestHeaders(output); - return output; - } catch (IOException e) { - throw new XmlRpcException("Failed to open connection to " - + hostname + ":" + port + ": " + e.getMessage(), e); - } - } - - - - private byte[] toHTTPBytes(String pValue) throws UnsupportedEncodingException { - return pValue.getBytes("US-ASCII"); - } - - private void sendHeader(OutputStream pOut, String pKey, String pValue) throws IOException { - pOut.write(toHTTPBytes(pKey + ": " + pValue + "\r\n")); - } - - private void sendRequestHeaders(OutputStream pOut) throws IOException { - pOut.write(("POST " + uri + " HTTP/1.0\r\n").getBytes("US-ASCII")); - for (Iterator iter = headers.entrySet().iterator(); iter.hasNext(); ) { - Map.Entry entry = (Map.Entry) iter.next(); - String key = (String) entry.getKey(); - Object value = entry.getValue(); - if (value instanceof String) { - sendHeader(pOut, key, (String) value); - } else { - List list = (List) value; - for (int i = 0; i < list.size(); i++) { - sendHeader(pOut, key, (String) list.get(i)); - } - } - } - pOut.write(toHTTPBytes("\r\n")); - } - - protected boolean isResponseGzipCompressed(XmlRpcStreamRequestConfig pConfig) { - return responseGzipCompressed; - } - - protected InputStream getInputStream() throws XmlRpcException { - final byte[] buffer = new byte[2048]; - try { - // If reply timeout specified, set the socket timeout accordingly - if (config.getReplyTimeout() != 0) - socket.setSoTimeout(config.getReplyTimeout()); - input = new BufferedInputStream(socket.getInputStream()); - // start reading server response headers - String line = HttpUtil.readLine(input, buffer); - StringTokenizer tokens = new StringTokenizer(line); - tokens.nextToken(); // Skip HTTP version - String statusCode = tokens.nextToken(); - String statusMsg = tokens.nextToken("\n\r"); - final int code; - try { - code = Integer.parseInt(statusCode); - } catch (NumberFormatException e) { - throw new XmlRpcClientException("Server returned invalid status code: " - + statusCode + " " + statusMsg, null); - } - if (code < 200 || code > 299) { - throw new XmlRpcHttpTransportException(code, statusMsg); - } - int contentLength = -1; - for (;;) { - line = HttpUtil.readLine(input, buffer); - if (line == null || "".equals(line)) { - break; - } - line = line.toLowerCase(); - if (line.startsWith("content-length:")) { - contentLength = Integer.parseInt(line.substring("content-length:".length()).trim()); - } else if (line.startsWith("content-encoding:")) { - responseGzipCompressed = HttpUtil.isUsingGzipEncoding(line.substring("content-encoding:".length())); - } - } - InputStream result; - if (contentLength == -1) { - result = input; - } else { - result = new LimitedInputStream(input, contentLength); - } - return result; - } catch (IOException e) { - throw new XmlRpcClientException("Failed to read server response: " + e.getMessage(), e); - } - } - - protected boolean isUsingByteArrayOutput(XmlRpcHttpClientConfig pConfig) { - boolean result = super.isUsingByteArrayOutput(pConfig); - if (!result) { - throw new IllegalStateException("The Content-Length header is required with HTTP/1.0, and HTTP/1.1 is unsupported by the Lite HTTP Transport."); - } - return result; - } - - protected void writeRequest(ReqWriter pWriter) throws XmlRpcException, IOException, SAXException { - pWriter.write(getOutputStream()); - } -} diff --git a/orchid/opt/xmlrpc/com/subgraph/orchid/xmlrpc/OrchidXmlRpcTransportFactory.java b/orchid/opt/xmlrpc/com/subgraph/orchid/xmlrpc/OrchidXmlRpcTransportFactory.java deleted file mode 100644 index 3f7bcfc7..00000000 --- a/orchid/opt/xmlrpc/com/subgraph/orchid/xmlrpc/OrchidXmlRpcTransportFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.subgraph.orchid.xmlrpc; - -import javax.net.SocketFactory; -import javax.net.ssl.SSLContext; - -import org.apache.xmlrpc.client.XmlRpcClient; -import org.apache.xmlrpc.client.XmlRpcTransport; -import org.apache.xmlrpc.client.XmlRpcTransportFactory; -import com.subgraph.orchid.TorClient; -import com.subgraph.orchid.sockets.OrchidSocketFactory; - -public class OrchidXmlRpcTransportFactory implements XmlRpcTransportFactory { - private final XmlRpcClient client; - private final SSLContext sslContext; - private final SocketFactory socketFactory; - - public OrchidXmlRpcTransportFactory(XmlRpcClient client, TorClient torClient) { - this(client, torClient, null); - } - - public OrchidXmlRpcTransportFactory(XmlRpcClient client, TorClient torClient, SSLContext sslContext) { - this.client = client; - this.socketFactory = new OrchidSocketFactory(torClient); - this.sslContext = sslContext; - } - - public XmlRpcTransport getTransport() { - return new OrchidXmlRpcTransport(client, socketFactory, sslContext); - } -} diff --git a/orchid/pom.xml b/orchid/pom.xml deleted file mode 100644 index cad45ad4..00000000 --- a/orchid/pom.xml +++ /dev/null @@ -1,156 +0,0 @@ - - - 4.0.0 - - org.bitcoinj - bitcoinj-parent - 0.15-SNAPSHOT - - - orchid - 1.2.1 - - Orchid - Tor library - - jar - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - src - - - - maven-compiler-plugin - - 1.6 - 1.6 - - - - - - - - doclint-java8-disable - - [1.8,) - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - -Xdoclint:none - - - - - - - - release - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - verify - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-javadocs - - jar - - - - - - - org.apache.maven.plugins - maven-shade-plugin - - false - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - package - - shade - - - true - bundled - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - - - - - - - junit - junit - 4.11 - test - - - com.google.guava - guava - 18.0 - - - - diff --git a/orchid/src/com/subgraph/orchid/BridgeRouter.java b/orchid/src/com/subgraph/orchid/BridgeRouter.java deleted file mode 100644 index f7a2dfa0..00000000 --- a/orchid/src/com/subgraph/orchid/BridgeRouter.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.subgraph.orchid; - -import com.subgraph.orchid.data.HexDigest; - -public interface BridgeRouter extends Router { - void setIdentity(HexDigest identity); - void setDescriptor(RouterDescriptor descriptor); -} diff --git a/orchid/src/com/subgraph/orchid/Cell.java b/orchid/src/com/subgraph/orchid/Cell.java deleted file mode 100644 index b5122210..00000000 --- a/orchid/src/com/subgraph/orchid/Cell.java +++ /dev/null @@ -1,230 +0,0 @@ -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 buffer.length bytes from the cell into buffer. The data is copied starting - * from the current internal pointer location and afterwards the internal pointer is incremented by buffer.length - * 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 data at the current pointer location and increment - * the pointer by data.length bytes. - * - * @param data The array of bytes to store in the cell. - */ - void putByteArray(byte[] data); - - /** - * Store length bytes of the byte array data starting from - * offset into the array at the current pointer location and increment - * the pointer by length 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 - * VERSIONS, this array will be exactly CELL_LEN bytes long. - * - * @return The cell data as an array of bytes. - */ - byte[] getCellBytes(); - - void putString(String string); -} diff --git a/orchid/src/com/subgraph/orchid/Circuit.java b/orchid/src/com/subgraph/orchid/Circuit.java deleted file mode 100644 index 0a1341fe..00000000 --- a/orchid/src/com/subgraph/orchid/Circuit.java +++ /dev/null @@ -1,95 +0,0 @@ -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 true if the circuit is presently in the connected state or - * false otherwise. - * - * @return Returns true if the circuit is presently connected, or - * false otherwise. - */ - boolean isConnected(); - - boolean isPending(); - - boolean isClean(); - - boolean isMarkedForClose(); - - int getSecondsDirty(); - - /** - * Returns the entry router Connection 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 targetNode with command value relayCommand - * and a stream id value of streamId. The returned RelayCell - * 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 CIRCUIT_RELAY_RESPONSE_TIMEOUT milliseconds, null - * is returned. - * - * @return The next relay response cell received on this circuit or null if - * a timeout is reached before the next relay cell arrives. - */ - RelayCell receiveRelayCell(); - - /** - * Encrypt and deliver the relay cell cell. - * - * @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 getActiveStreams(); - - void markForClose(); - - void appendNode(CircuitNode node); -} diff --git a/orchid/src/com/subgraph/orchid/CircuitBuildHandler.java b/orchid/src/com/subgraph/orchid/CircuitBuildHandler.java deleted file mode 100644 index 40f6d3bb..00000000 --- a/orchid/src/com/subgraph/orchid/CircuitBuildHandler.java +++ /dev/null @@ -1,60 +0,0 @@ -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); -} diff --git a/orchid/src/com/subgraph/orchid/CircuitManager.java b/orchid/src/com/subgraph/orchid/CircuitManager.java deleted file mode 100644 index ee4fbc87..00000000 --- a/orchid/src/com/subgraph/orchid/CircuitManager.java +++ /dev/null @@ -1,51 +0,0 @@ -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 hostname and - * port. - * - * @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 address and - * port. - * - * @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 path) throws OpenFailedException; - InternalCircuit openInternalCircuitTo(List path) throws OpenFailedException; - DirectoryCircuit openDirectoryCircuitTo(List path) throws OpenFailedException; -} diff --git a/orchid/src/com/subgraph/orchid/CircuitNode.java b/orchid/src/com/subgraph/orchid/CircuitNode.java deleted file mode 100644 index aa09b88e..00000000 --- a/orchid/src/com/subgraph/orchid/CircuitNode.java +++ /dev/null @@ -1,90 +0,0 @@ -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 cell - * - * @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 TOR_DIGEST_SIZE 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 null if this is - * the first hop. - * - * @return The previous {@link CircuitNode} in the chain or - * null 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); -} diff --git a/orchid/src/com/subgraph/orchid/Connection.java b/orchid/src/com/subgraph/orchid/Connection.java deleted file mode 100644 index 54f0dfed..00000000 --- a/orchid/src/com/subgraph/orchid/Connection.java +++ /dev/null @@ -1,47 +0,0 @@ -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 true if the socket for this connection has been closed. Otherwise, false. - * - * @return true if this connection is closed or false 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); -} diff --git a/orchid/src/com/subgraph/orchid/ConnectionCache.java b/orchid/src/com/subgraph/orchid/ConnectionCache.java deleted file mode 100644 index 3c537607..00000000 --- a/orchid/src/com/subgraph/orchid/ConnectionCache.java +++ /dev/null @@ -1,23 +0,0 @@ -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(); - - boolean isClosed(); -} diff --git a/orchid/src/com/subgraph/orchid/ConnectionFailedException.java b/orchid/src/com/subgraph/orchid/ConnectionFailedException.java deleted file mode 100644 index b7f25c69..00000000 --- a/orchid/src/com/subgraph/orchid/ConnectionFailedException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.subgraph.orchid; - -public class ConnectionFailedException extends ConnectionIOException { - - private static final long serialVersionUID = -4484347156587613574L; - - public ConnectionFailedException(String message) { - super(message); - } - -} diff --git a/orchid/src/com/subgraph/orchid/ConnectionHandshakeException.java b/orchid/src/com/subgraph/orchid/ConnectionHandshakeException.java deleted file mode 100644 index 5e059319..00000000 --- a/orchid/src/com/subgraph/orchid/ConnectionHandshakeException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.subgraph.orchid; - -public class ConnectionHandshakeException extends ConnectionIOException { - - private static final long serialVersionUID = -2544633445932967966L; - - public ConnectionHandshakeException(String message) { - super(message); - } -} diff --git a/orchid/src/com/subgraph/orchid/ConnectionIOException.java b/orchid/src/com/subgraph/orchid/ConnectionIOException.java deleted file mode 100644 index a80d0fd4..00000000 --- a/orchid/src/com/subgraph/orchid/ConnectionIOException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.subgraph.orchid; - -public class ConnectionIOException extends Exception { - - private static final long serialVersionUID = -5537650738995969203L; - - public ConnectionIOException() { - super(); - } - - public ConnectionIOException(String message) { - super(message); - } -} diff --git a/orchid/src/com/subgraph/orchid/ConnectionTimeoutException.java b/orchid/src/com/subgraph/orchid/ConnectionTimeoutException.java deleted file mode 100644 index 7a539b65..00000000 --- a/orchid/src/com/subgraph/orchid/ConnectionTimeoutException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.subgraph.orchid; - -public class ConnectionTimeoutException extends ConnectionIOException { - - private static final long serialVersionUID = -6098661610150140151L; - - public ConnectionTimeoutException() { - super(); - } - - public ConnectionTimeoutException(String message) { - super(message); - } -} diff --git a/orchid/src/com/subgraph/orchid/ConsensusDocument.java b/orchid/src/com/subgraph/orchid/ConsensusDocument.java deleted file mode 100644 index ce31947f..00000000 --- a/orchid/src/com/subgraph/orchid/ConsensusDocument.java +++ /dev/null @@ -1,44 +0,0 @@ -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 getClientVersions(); - Set getServerVersions(); - boolean isLive(); - List getRouterStatusEntries(); - - SignatureStatus verifySignatures(); - Set getRequiredCertificates(); - - HexDigest getSigningHash(); - HexDigest getSigningHash256(); - - int getCircWindowParameter(); - int getWeightScaleParameter(); - - int getBandwidthWeight(String tag); - - boolean getUseNTorHandshake(); -} diff --git a/orchid/src/com/subgraph/orchid/Descriptor.java b/orchid/src/com/subgraph/orchid/Descriptor.java deleted file mode 100644 index 4fa1cb07..00000000 --- a/orchid/src/com/subgraph/orchid/Descriptor.java +++ /dev/null @@ -1,65 +0,0 @@ -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 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); -} diff --git a/orchid/src/com/subgraph/orchid/Directory.java b/orchid/src/com/subgraph/orchid/Directory.java deleted file mode 100644 index 5370e884..00000000 --- a/orchid/src/com/subgraph/orchid/Directory.java +++ /dev/null @@ -1,48 +0,0 @@ -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 getDirectoryAuthorities(); - DirectoryServer getRandomDirectoryAuthority(); - void addCertificate(KeyCertificate certificate); - Set getRequiredCertificates(); - void addRouterMicrodescriptors(List microdescriptors); - void addRouterDescriptors(List 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 getRouterListByNames(List names); - List getRoutersWithDownloadableDescriptors(); - List getAllRouters(); - - RouterMicrodescriptor getMicrodescriptorFromCache(HexDigest descriptorDigest); - RouterDescriptor getBasicDescriptorFromCache(HexDigest descriptorDigest); - - GuardEntry createGuardEntryFor(Router router); - List getGuardEntries(); - void removeGuardEntry(GuardEntry entry); - void addGuardEntry(GuardEntry entry); -} diff --git a/orchid/src/com/subgraph/orchid/DirectoryCircuit.java b/orchid/src/com/subgraph/orchid/DirectoryCircuit.java deleted file mode 100644 index e99f6863..00000000 --- a/orchid/src/com/subgraph/orchid/DirectoryCircuit.java +++ /dev/null @@ -1,16 +0,0 @@ -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; -} diff --git a/orchid/src/com/subgraph/orchid/DirectoryDownloader.java b/orchid/src/com/subgraph/orchid/DirectoryDownloader.java deleted file mode 100644 index 5133212f..00000000 --- a/orchid/src/com/subgraph/orchid/DirectoryDownloader.java +++ /dev/null @@ -1,27 +0,0 @@ -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 downloadKeyCertificates(Set required) throws DirectoryRequestFailedException; - List downloadKeyCertificates(Set required, DirectoryCircuit circuit) throws DirectoryRequestFailedException; - - List downloadRouterDescriptors(Set fingerprints) throws DirectoryRequestFailedException; - List downloadRouterDescriptors(Set fingerprints, DirectoryCircuit circuit) throws DirectoryRequestFailedException; - - List downloadRouterMicrodescriptors(Set fingerprints) throws DirectoryRequestFailedException; - List downloadRouterMicrodescriptors(Set fingerprints, DirectoryCircuit circuit) throws DirectoryRequestFailedException; -} diff --git a/orchid/src/com/subgraph/orchid/DirectoryServer.java b/orchid/src/com/subgraph/orchid/DirectoryServer.java deleted file mode 100644 index 7d443509..00000000 --- a/orchid/src/com/subgraph/orchid/DirectoryServer.java +++ /dev/null @@ -1,22 +0,0 @@ -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 getCertificates(); - void addCertificate(KeyCertificate certificate); -} diff --git a/orchid/src/com/subgraph/orchid/DirectoryStore.java b/orchid/src/com/subgraph/orchid/DirectoryStore.java deleted file mode 100644 index fa9e02b0..00000000 --- a/orchid/src/com/subgraph/orchid/DirectoryStore.java +++ /dev/null @@ -1,36 +0,0 @@ -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 documents); - void appendDocumentList(CacheFile cacheFile, List documents); - - void removeCacheFile(CacheFile cacheFile); - void removeAllCacheFiles(); -} diff --git a/orchid/src/com/subgraph/orchid/Document.java b/orchid/src/com/subgraph/orchid/Document.java deleted file mode 100644 index 92e2bfaa..00000000 --- a/orchid/src/com/subgraph/orchid/Document.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.subgraph.orchid; - -import java.nio.ByteBuffer; - -public interface Document { - ByteBuffer getRawDocumentBytes(); - String getRawDocumentData(); - boolean isValidDocument(); -} diff --git a/orchid/src/com/subgraph/orchid/ExitCircuit.java b/orchid/src/com/subgraph/orchid/ExitCircuit.java deleted file mode 100644 index 2178f13a..00000000 --- a/orchid/src/com/subgraph/orchid/ExitCircuit.java +++ /dev/null @@ -1,50 +0,0 @@ -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 ExitTarget. 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 ExitTarget as a failed connection so that {@link #canHandleExitTo(ExitTarget)} will - * no longer return true for this exit destination. - * - * @param target The ExitTarget to which a connection has failed through this circuit. - */ - public void recordFailedExitTarget(ExitTarget target); - -} diff --git a/orchid/src/com/subgraph/orchid/GuardEntry.java b/orchid/src/com/subgraph/orchid/GuardEntry.java deleted file mode 100644 index efed1405..00000000 --- a/orchid/src/com/subgraph/orchid/GuardEntry.java +++ /dev/null @@ -1,18 +0,0 @@ -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(); -} diff --git a/orchid/src/com/subgraph/orchid/HiddenServiceCircuit.java b/orchid/src/com/subgraph/orchid/HiddenServiceCircuit.java deleted file mode 100644 index 83b057ad..00000000 --- a/orchid/src/com/subgraph/orchid/HiddenServiceCircuit.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.subgraph.orchid; - -import java.util.concurrent.TimeoutException; - - -public interface HiddenServiceCircuit extends Circuit { - Stream openStream(int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException; -} diff --git a/orchid/src/com/subgraph/orchid/InternalCircuit.java b/orchid/src/com/subgraph/orchid/InternalCircuit.java deleted file mode 100644 index 39975f24..00000000 --- a/orchid/src/com/subgraph/orchid/InternalCircuit.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.subgraph.orchid; - -public interface InternalCircuit extends Circuit { - DirectoryCircuit cannibalizeToDirectory(Router target); - Circuit cannibalizeToIntroductionPoint(Router target); - HiddenServiceCircuit connectHiddenService(CircuitNode node); -} diff --git a/orchid/src/com/subgraph/orchid/KeyCertificate.java b/orchid/src/com/subgraph/orchid/KeyCertificate.java deleted file mode 100644 index f15b47bc..00000000 --- a/orchid/src/com/subgraph/orchid/KeyCertificate.java +++ /dev/null @@ -1,78 +0,0 @@ -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 null if no address was specified in the certificate. - * - * @return The network address of the directory authority this certificate - * belongs to, or null 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 true if the current time is past the key - * expiry time of this certificate. - * - * @return True if this certificate is currently expired. - */ - boolean isExpired(); -} diff --git a/orchid/src/com/subgraph/orchid/OpenFailedException.java b/orchid/src/com/subgraph/orchid/OpenFailedException.java deleted file mode 100644 index 4eb1fc47..00000000 --- a/orchid/src/com/subgraph/orchid/OpenFailedException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.subgraph.orchid; - -public class OpenFailedException extends Exception { - - private static final long serialVersionUID = 1989001056577214666L; - - public OpenFailedException() { - } - - public OpenFailedException(String message) { - super(message); - } -} diff --git a/orchid/src/com/subgraph/orchid/RelayCell.java b/orchid/src/com/subgraph/orchid/RelayCell.java deleted file mode 100644 index 632b9cce..00000000 --- a/orchid/src/com/subgraph/orchid/RelayCell.java +++ /dev/null @@ -1,65 +0,0 @@ -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); -} diff --git a/orchid/src/com/subgraph/orchid/Revision.java b/orchid/src/com/subgraph/orchid/Revision.java deleted file mode 100644 index 6dc6a5c9..00000000 --- a/orchid/src/com/subgraph/orchid/Revision.java +++ /dev/null @@ -1,35 +0,0 @@ -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(); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/Router.java b/orchid/src/com/subgraph/orchid/Router.java deleted file mode 100644 index fcccd926..00000000 --- a/orchid/src/com/subgraph/orchid/Router.java +++ /dev/null @@ -1,47 +0,0 @@ -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 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); -} diff --git a/orchid/src/com/subgraph/orchid/RouterDescriptor.java b/orchid/src/com/subgraph/orchid/RouterDescriptor.java deleted file mode 100644 index 89085e46..00000000 --- a/orchid/src/com/subgraph/orchid/RouterDescriptor.java +++ /dev/null @@ -1,164 +0,0 @@ -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 other descriptor. - * - * @param other Another router descriptor to compare. - * @return True if this descriptor was published later than other - */ - boolean isNewerThan(RouterDescriptor other); - - ExitPolicy getExitPolicy(); - - -} diff --git a/orchid/src/com/subgraph/orchid/RouterMicrodescriptor.java b/orchid/src/com/subgraph/orchid/RouterMicrodescriptor.java deleted file mode 100644 index 818585c6..00000000 --- a/orchid/src/com/subgraph/orchid/RouterMicrodescriptor.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.subgraph.orchid; - - -public interface RouterMicrodescriptor extends Descriptor { - -} diff --git a/orchid/src/com/subgraph/orchid/RouterStatus.java b/orchid/src/com/subgraph/orchid/RouterStatus.java deleted file mode 100644 index 87c1dd64..00000000 --- a/orchid/src/com/subgraph/orchid/RouterStatus.java +++ /dev/null @@ -1,24 +0,0 @@ -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(); -} diff --git a/orchid/src/com/subgraph/orchid/SocksPortListener.java b/orchid/src/com/subgraph/orchid/SocksPortListener.java deleted file mode 100644 index dd8a4d5a..00000000 --- a/orchid/src/com/subgraph/orchid/SocksPortListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.subgraph.orchid; - -public interface SocksPortListener { - void addListeningPort(int port); - void stop(); -} diff --git a/orchid/src/com/subgraph/orchid/Stream.java b/orchid/src/com/subgraph/orchid/Stream.java deleted file mode 100644 index 2a4bc07e..00000000 --- a/orchid/src/com/subgraph/orchid/Stream.java +++ /dev/null @@ -1,49 +0,0 @@ -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(); -} diff --git a/orchid/src/com/subgraph/orchid/StreamConnectFailedException.java b/orchid/src/com/subgraph/orchid/StreamConnectFailedException.java deleted file mode 100644 index 4c944a86..00000000 --- a/orchid/src/com/subgraph/orchid/StreamConnectFailedException.java +++ /dev/null @@ -1,35 +0,0 @@ -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; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/Threading.java b/orchid/src/com/subgraph/orchid/Threading.java deleted file mode 100644 index 39c10844..00000000 --- a/orchid/src/com/subgraph/orchid/Threading.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.subgraph.orchid; - -import com.google.common.util.concurrent.CycleDetectingLockFactory; -import com.google.common.util.concurrent.ThreadFactoryBuilder; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.locks.ReentrantLock; - -/** - * Created by android on 8/22/14. - */ -public class Threading { - static { - // Default policy goes here. If you want to change this, use one of the static methods before - // instantiating any orchid objects. The policy change will take effect only on new objects - // from that point onwards. - throwOnLockCycles(); - } - - private static CycleDetectingLockFactory.Policy policy; - public static CycleDetectingLockFactory factory; - - public static ReentrantLock lock(String name) { - return factory.newReentrantLock(name); - } - - public static void warnOnLockCycles() { - setPolicy(CycleDetectingLockFactory.Policies.WARN); - } - - public static void throwOnLockCycles() { - setPolicy(CycleDetectingLockFactory.Policies.THROW); - } - - public static void ignoreLockCycles() { - setPolicy(CycleDetectingLockFactory.Policies.DISABLED); - } - - public static void setPolicy(CycleDetectingLockFactory.Policy policy) { - Threading.policy = policy; - factory = CycleDetectingLockFactory.newInstance(policy); - } - - public static CycleDetectingLockFactory.Policy getPolicy() { - return policy; - } - - public static ExecutorService newPool(final String name) { - ThreadFactory factory = new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat(name + "-%d").build(); - return Executors.newCachedThreadPool(factory); - } - - public static ScheduledExecutorService newSingleThreadScheduledPool(final String name) { - ThreadFactory factory = new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat(name + "-%d").build(); - return Executors.newSingleThreadScheduledExecutor(factory); - } - - public static ScheduledExecutorService newScheduledPool(final String name) { - ThreadFactory factory = new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat(name + "-%d").build(); - return Executors.newScheduledThreadPool(1, factory); - } -} diff --git a/orchid/src/com/subgraph/orchid/Tor.java b/orchid/src/com/subgraph/orchid/Tor.java deleted file mode 100644 index 566ffe80..00000000 --- a/orchid/src/com/subgraph/orchid/Tor.java +++ /dev/null @@ -1,167 +0,0 @@ -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 Tor 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 TorConfig instance. - * - * @param logManager This is a required dependency. You must create a LogManager - * before calling this method to create a TorConfig - * @return A new TorConfig 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 Directory instance. - * - * @param logManager This is a required dependency. You must create a LogManager - * before creating a Directory. - * @param config This is a required dependency. You must create a TorConfig before - * calling this method to create a Directory - * @return A new Directory 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 CircuitManager instance. - * - * @return A new CircuitManager 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 SocksPortListener instance. - * - * @param logManager This is a required dependency. You must create a LogManager - * before calling this method to create a SocksPortListener. - * @param circuitManager This is a required dependency. You must create a CircuitManager - * before calling this method to create a SocksPortListener. - * @return A new SocksPortListener instance. - * @see SocksPortListener - */ - static public SocksPortListener createSocksPortListener(TorConfig config, CircuitManager circuitManager) { - return new SocksPortListenerImpl(config, circuitManager); - } - - /** - * Create and return a new DirectoryDownloader instance. - * - * @param logManager This is a required dependency. You must create a LogManager - * before calling this method to create a DirectoryDownloader. - - * @param directory This is a required dependency. You must create a Directory - * before calling this method to create a DirectoryDownloader - * - * @param circuitManager This is a required dependency. You must create a CircuitManager - * before calling this method to create a DirectoryDownloader. - * - * @return A new DirectoryDownloader instance. - * @see DirectoryDownloaderImpl - */ - static public DirectoryDownloaderImpl createDirectoryDownloader(TorConfig config, TorInitializationTracker initializationTracker) { - return new DirectoryDownloaderImpl(config, initializationTracker); - } -} diff --git a/orchid/src/com/subgraph/orchid/TorClient.java b/orchid/src/com/subgraph/orchid/TorClient.java deleted file mode 100644 index c87ae135..00000000 --- a/orchid/src/com/subgraph/orchid/TorClient.java +++ /dev/null @@ -1,217 +0,0 @@ -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 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"); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/TorConfig.java b/orchid/src/com/subgraph/orchid/TorConfig.java deleted file mode 100644 index 83971330..00000000 --- a/orchid/src/com/subgraph/orchid/TorConfig.java +++ /dev/null @@ -1,151 +0,0 @@ -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 getLongLivedPorts(); - void setLongLivedPorts(List ports); - - @ConfigVar(type=ConfigVarType.STRINGLIST) - List getExcludeNodes(); - void setExcludeNodes(List nodes); - - @ConfigVar(type=ConfigVarType.STRINGLIST) - List getExcludeExitNodes(); - - void setExcludeExitNodes(List nodes); - - @ConfigVar(type=ConfigVarType.STRINGLIST) - List getExitNodes(); - void setExitNodes(List nodes); - - @ConfigVar(type=ConfigVarType.STRINGLIST) - List getEntryNodes(); - void setEntryNodes(List 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 getFirewallPorts(); - void setFirewallPorts(List 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 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 ""; - } -} \ No newline at end of file diff --git a/orchid/src/com/subgraph/orchid/TorException.java b/orchid/src/com/subgraph/orchid/TorException.java deleted file mode 100644 index fd3406c0..00000000 --- a/orchid/src/com/subgraph/orchid/TorException.java +++ /dev/null @@ -1,22 +0,0 @@ -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); - } -} diff --git a/orchid/src/com/subgraph/orchid/TorInitializationListener.java b/orchid/src/com/subgraph/orchid/TorInitializationListener.java deleted file mode 100644 index e36d3e72..00000000 --- a/orchid/src/com/subgraph/orchid/TorInitializationListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.subgraph.orchid; - -public interface TorInitializationListener { - void initializationProgress(String message, int percent); - void initializationCompleted(); -} diff --git a/orchid/src/com/subgraph/orchid/TorParsingException.java b/orchid/src/com/subgraph/orchid/TorParsingException.java deleted file mode 100644 index d55e08ce..00000000 --- a/orchid/src/com/subgraph/orchid/TorParsingException.java +++ /dev/null @@ -1,14 +0,0 @@ -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; -} diff --git a/orchid/src/com/subgraph/orchid/VoteAuthorityEntry.java b/orchid/src/com/subgraph/orchid/VoteAuthorityEntry.java deleted file mode 100644 index 5693da6b..00000000 --- a/orchid/src/com/subgraph/orchid/VoteAuthorityEntry.java +++ /dev/null @@ -1,19 +0,0 @@ -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 getSignatures(); -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitBuildTask.java b/orchid/src/com/subgraph/orchid/circuits/CircuitBuildTask.java deleted file mode 100644 index 2d699a56..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitBuildTask.java +++ /dev/null @@ -1,127 +0,0 @@ -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); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitCreationRequest.java b/orchid/src/com/subgraph/orchid/circuits/CircuitCreationRequest.java deleted file mode 100644 index 59d0124b..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitCreationRequest.java +++ /dev/null @@ -1,91 +0,0 @@ -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 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 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); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitCreationTask.java b/orchid/src/com/subgraph/orchid/circuits/CircuitCreationTask.java deleted file mode 100644 index f4afc11c..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitCreationTask.java +++ /dev/null @@ -1,297 +0,0 @@ -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.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.Threading; -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 = Threading.newPool("CircuitCreationTask worker"); - 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 pendingExitStreams = circuitManager.getPendingExitStreams(); - if(pendingExitStreams.isEmpty()) - return; - - for(ExitCircuit c: circuitManager.getRandomlyOrderedListOfExitCircuits()) { - final Iterator 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 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() { - if (connectionCache.isClosed()) { - logger.warning("Not building circuits, because connection cache is closed"); - return; - } - - final List pendingExitStreams = circuitManager.getPendingExitStreams(); - final List predictedPorts = predictor.getPredictedPortTargets(); - final List exitTargets = new ArrayList(); - 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 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 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 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); - } - }; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitExtender.java b/orchid/src/com/subgraph/orchid/circuits/CircuitExtender.java deleted file mode 100644 index b6fd1e19..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitExtender.java +++ /dev/null @@ -1,155 +0,0 @@ -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; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitIO.java b/orchid/src/com/subgraph/orchid/circuits/CircuitIO.java deleted file mode 100644 index 11d765d4..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitIO.java +++ /dev/null @@ -1,351 +0,0 @@ -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.concurrent.locks.ReentrantLock; -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.Threading; -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 relayCellResponseQueue; - private final BlockingQueue controlCellResponseQueue; - private final Map streamMap; - private final ReentrantLock streamLock = Threading.lock("stream"); - private final ReentrantLock relaySendLock = Threading.lock("relaySend"); - - 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(); - this.controlCellResponseQueue = new LinkedBlockingQueue(); - this.streamMap = new HashMap(); - } - - 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()); - } - } - - streamLock.lock(); - try { - 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); - } - } finally { - streamLock.unlock(); - } - } - - RelayCell createRelayCell(int relayCommand, int streamId, CircuitNode targetNode) { - return new RelayCellImpl(targetNode, circuitId, streamId, relayCommand); - } - - void sendRelayCellTo(RelayCell cell, CircuitNode targetNode) { - relaySendLock.lock(); - try { - 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); - } finally { - relaySendLock.unlock(); - } - } - - - 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() { - boolean shouldClose; - streamLock.lock(); - try { - if(isMarkedForClose) { - return; - } - isMarkedForClose = true; - shouldClose = streamMap.isEmpty(); - } finally { - streamLock.unlock(); - } - if(shouldClose) - closeCircuit(); - } - - boolean isMarkedForClose() { - streamLock.lock(); - try { - return isMarkedForClose; - } finally { - streamLock.unlock(); - } - } - - 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() { - streamLock.lock(); - try { - if(isClosed) { - return; - } - circuit.setStateDestroyed(); - connection.removeCircuit(circuit); - final List tmpList = new ArrayList(streamMap.values()); - for(StreamImpl s: tmpList) { - s.close(); - } - isClosed = true; - } finally { - streamLock.unlock(); - } - } - - StreamImpl createNewStream(boolean autoclose) { - streamLock.lock(); - try { - final int streamId = circuit.getStatus().nextStreamId(); - final StreamImpl stream = new StreamImpl(circuit, circuit.getFinalCircuitNode(), streamId, autoclose); - streamMap.put(streamId, stream); - return stream; - } finally { - streamLock.unlock(); - } - } - - void removeStream(StreamImpl stream) { - boolean shouldClose; - streamLock.lock(); - try { - streamMap.remove(stream.getStreamId()); - shouldClose = streamMap.isEmpty() && isMarkedForClose; - } finally { - streamLock.unlock(); - } - if(shouldClose) - closeCircuit(); - } - - List getActiveStreams() { - streamLock.lock(); - try { - return new ArrayList(streamMap.values()); - } finally { - streamLock.unlock(); - } - } - - 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); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitImpl.java b/orchid/src/com/subgraph/orchid/circuits/CircuitImpl.java deleted file mode 100644 index 057afbe3..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitImpl.java +++ /dev/null @@ -1,289 +0,0 @@ -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 prechosenPath) { - return new ExitCircuitImpl(circuitManager, prechosenPath); - } - - static DirectoryCircuit createDirectoryCircuit(CircuitManagerImpl circuitManager) { - return new DirectoryCircuitImpl(circuitManager, null); - } - - static DirectoryCircuit createDirectoryCircuitTo(CircuitManagerImpl circuitManager, List prechosenPath) { - return new DirectoryCircuitImpl(circuitManager, prechosenPath); - } - - static InternalCircuit createInternalCircuitTo(CircuitManagerImpl circuitManager, List prechosenPath) { - return new InternalCircuitImpl(circuitManager, prechosenPath); - } - - private final CircuitManagerImpl circuitManager; - protected final List prechosenPath; - - private final List nodeList; - private final CircuitStatus status; - - private CircuitIO io; - - - - - - - protected CircuitImpl(CircuitManagerImpl circuitManager) { - this(circuitManager, null); - } - - protected CircuitImpl(CircuitManagerImpl circuitManager, List prechosenPath) { - nodeList = new ArrayList(); - this.circuitManager = circuitManager; - this.prechosenPath = prechosenPath; - status = new CircuitStatus(); - } - - List choosePath(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException { - if(prechosenPath != null) { - return new ArrayList(prechosenPath); - } else { - return choosePathForCircuit(pathChooser); - } - } - - protected abstract List 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 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() { - // We might not have bound this circuit yet - if (io != null) { - 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 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); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitManagerImpl.java b/orchid/src/com/subgraph/orchid/circuits/CircuitManagerImpl.java deleted file mode 100644 index 9b68cd0b..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitManagerImpl.java +++ /dev/null @@ -1,443 +0,0 @@ -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.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.locks.ReentrantLock; - -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.Threading; -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 activeCircuits; - private final Queue cleanInternalCircuits; - private int requestedInternalCircuitCount = 0; - private int pendingInternalCircuitCount = 0; - private final TorRandom random; - private final PendingExitStreams pendingExitStreams; - private final ScheduledExecutorService scheduledExecutor = Threading.newSingleThreadScheduledPool("CircuitManager worker"); - private final CircuitCreationTask circuitCreationTask; - private final TorInitializationTracker initializationTracker; - private final CircuitPathChooser pathChooser; - private final HiddenServiceManager hiddenServiceManager; - private final ReentrantLock lock = Threading.lock("circuitManager"); - - private boolean isBuilding = false; - - 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(); - this.cleanInternalCircuits = new LinkedList(); - this.random = new TorRandom(); - - this.initializationTracker = initializationTracker; - this.hiddenServiceManager = new HiddenServiceManager(config, directory, this); - - directoryDownloader.setCircuitManager(this); - } - - public void startBuildingCircuits() { - lock.lock(); - try { - isBuilding = true; - scheduledExecutor.scheduleAtFixedRate(circuitCreationTask, 0, 1000, TimeUnit.MILLISECONDS); - } finally { - lock.unlock(); - } - } - - public void stopBuildingCircuits(boolean killCircuits) { - lock.lock(); - try { - isBuilding = false; - scheduledExecutor.shutdownNow(); - } finally { - lock.unlock(); - } - - if (killCircuits) { - ArrayList circuits; - synchronized (activeCircuits) { - circuits = new ArrayList(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(); - } - - boolean doDestroy; - lock.lock(); - try { - doDestroy = !isBuilding; - } finally { - lock.unlock(); - } - - if (doDestroy) { - // we were asked to stop since this circuit was started - circuit.destroyCircuit(); - } - } - - void removeActiveCircuit(CircuitImpl circuit) { - synchronized (activeCircuits) { - activeCircuits.remove(circuit); - } - } - - int getActiveCircuitCount() { - synchronized (activeCircuits) { - return activeCircuits.size(); - } - } - - Set getPendingCircuits() { - return getCircuitsByFilter(new CircuitFilter() { - public boolean filter(Circuit circuit) { - return circuit.isPending(); - } - }); - } - - int getPendingCircuitCount() { - lock.lock(); - try { - return getPendingCircuits().size(); - } finally { - lock.unlock(); - } - } - - Set getCircuitsByFilter(CircuitFilter filter) { - final Set result = new HashSet(); - final Set circuits = new HashSet(); - - synchronized (activeCircuits) { - // the filter might lock additional objects, causing a deadlock, so don't - // call it inside the monitor - circuits.addAll(activeCircuits); - } - - for(CircuitImpl c: circuits) { - if(filter == null || filter.filter(c)) { - result.add(c); - } - } - return result; - } - - List getRandomlyOrderedListOfExitCircuits() { - final Set notDirectory = getCircuitsByFilter(new CircuitFilter() { - - public boolean filter(Circuit circuit) { - final boolean exitType = circuit instanceof ExitCircuit; - return exitType && !circuit.isMarkedForClose() && circuit.isConnected(); - } - }); - final ArrayList ac = new ArrayList(); - 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 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 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 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 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(); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitNodeCryptoState.java b/orchid/src/com/subgraph/orchid/circuits/CircuitNodeCryptoState.java deleted file mode 100644 index 37cb58e9..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitNodeCryptoState.java +++ /dev/null @@ -1,102 +0,0 @@ -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); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitNodeImpl.java b/orchid/src/com/subgraph/orchid/circuits/CircuitNodeImpl.java deleted file mode 100644 index c2e990f6..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitNodeImpl.java +++ /dev/null @@ -1,121 +0,0 @@ -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(); - } - - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitPredictor.java b/orchid/src/com/subgraph/orchid/circuits/CircuitPredictor.java deleted file mode 100644 index 21ed2ac6..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitPredictor.java +++ /dev/null @@ -1,99 +0,0 @@ -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 portsSeen; - - public CircuitPredictor() { - portsSeen = new HashMap(); - 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 e, long now) { - return (now - e.getValue()) > TIMEOUT_MS; - } - - private void removeExpiredPorts() { - final long now = System.currentTimeMillis(); - final Iterator> 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 getPredictedPorts() { - synchronized (portsSeen) { - removeExpiredPorts(); - final Set result = new HashSet(portsSeen.keySet()); - result.remove(INTERNAL_CIRCUIT_PORT_VALUE); - return result; - } - } - - List getPredictedPortTargets() { - final List targets = new ArrayList(); - 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(); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/CircuitStatus.java b/orchid/src/com/subgraph/orchid/circuits/CircuitStatus.java deleted file mode 100644 index a9a994f3..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/CircuitStatus.java +++ /dev/null @@ -1,115 +0,0 @@ -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; - } - } - -} diff --git a/orchid/src/com/subgraph/orchid/circuits/DirectoryCircuitImpl.java b/orchid/src/com/subgraph/orchid/circuits/DirectoryCircuitImpl.java deleted file mode 100644 index 646930fb..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/DirectoryCircuitImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -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 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 choosePathForCircuit(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException { - if(prechosenPath != null) { - return prechosenPath; - } - return pathChooser.chooseDirectoryPath(); - } - - @Override - protected String getCircuitTypeLabel() { - return "Directory"; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/ExitCircuitImpl.java b/orchid/src/com/subgraph/orchid/circuits/ExitCircuitImpl.java deleted file mode 100644 index e431ddf3..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/ExitCircuitImpl.java +++ /dev/null @@ -1,87 +0,0 @@ -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 failedExitRequests; - - ExitCircuitImpl(CircuitManagerImpl circuitManager, List prechosenPath) { - super(circuitManager, prechosenPath); - this.exitRouter = prechosenPath.get(prechosenPath.size() - 1); - this.failedExitRequests = new HashSet(); - } - - ExitCircuitImpl(CircuitManagerImpl circuitManager, Router exitRouter) { - super(circuitManager); - this.exitRouter = exitRouter; - this.failedExitRequests = new HashSet(); - } - - 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 choosePathForCircuit(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException { - return pathChooser.choosePathWithExit(exitRouter); - } - - @Override - protected String getCircuitTypeLabel() { - return "Exit"; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/InternalCircuitImpl.java b/orchid/src/com/subgraph/orchid/circuits/InternalCircuitImpl.java deleted file mode 100644 index 06bcbe04..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/InternalCircuitImpl.java +++ /dev/null @@ -1,118 +0,0 @@ -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 prechosenPath) { - super(circuitManager, prechosenPath); - this.type = InternalType.UNUSED; - this.ntorEnabled = circuitManager.isNtorEnabled(); - } - - protected InternalCircuitImpl(CircuitManagerImpl circuitManager) { - this(circuitManager, null); - } - - @Override - protected List 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)"; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/NTorCircuitExtender.java b/orchid/src/com/subgraph/orchid/circuits/NTorCircuitExtender.java deleted file mode 100644 index 45c705c1..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/NTorCircuitExtender.java +++ /dev/null @@ -1,114 +0,0 @@ -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); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/OpenExitStreamTask.java b/orchid/src/com/subgraph/orchid/circuits/OpenExitStreamTask.java deleted file mode 100644 index 3ea3d17e..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/OpenExitStreamTask.java +++ /dev/null @@ -1,50 +0,0 @@ -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()); - } - } - -} diff --git a/orchid/src/com/subgraph/orchid/circuits/PendingExitStreams.java b/orchid/src/com/subgraph/orchid/circuits/PendingExitStreams.java deleted file mode 100644 index f10fb264..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/PendingExitStreams.java +++ /dev/null @@ -1,77 +0,0 @@ -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 pendingRequests; - private final Object lock = new Object(); - private final TorConfig config; - - PendingExitStreams(TorConfig config) { - this.config = config; - pendingRequests = new HashSet(); - } - - 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 getUnreservedPendingRequests() { - final List result = new ArrayList(); - synchronized (lock) { - for(StreamExitRequest request: pendingRequests) { - if(!request.isReserved()) { - result.add(request); - } - } - } - return result; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/PredictedPortTarget.java b/orchid/src/com/subgraph/orchid/circuits/PredictedPortTarget.java deleted file mode 100644 index 62a1ac71..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/PredictedPortTarget.java +++ /dev/null @@ -1,29 +0,0 @@ -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; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/StreamExitRequest.java b/orchid/src/com/subgraph/orchid/circuits/StreamExitRequest.java deleted file mode 100644 index b2913c6a..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/StreamExitRequest.java +++ /dev/null @@ -1,170 +0,0 @@ -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; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/StreamImpl.java b/orchid/src/com/subgraph/orchid/circuits/StreamImpl.java deleted file mode 100644 index 461409b0..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/StreamImpl.java +++ /dev/null @@ -1,219 +0,0 @@ -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("]"); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/TapCircuitExtender.java b/orchid/src/com/subgraph/orchid/circuits/TapCircuitExtender.java deleted file mode 100644 index 078fe4de..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/TapCircuitExtender.java +++ /dev/null @@ -1,55 +0,0 @@ -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; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/TorInitializationTracker.java b/orchid/src/com/subgraph/orchid/circuits/TorInitializationTracker.java deleted file mode 100644 index c7874f52..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/TorInitializationTracker.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.subgraph.orchid.circuits; - -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.TorInitializationListener; - -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; - -public class TorInitializationTracker { - private final static Logger logger = Logger.getLogger(TorInitializationTracker.class.getName()); - private final static Map messageMap = new HashMap(); - - 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 network status consensus"); - messageMap.put(Tor.BOOTSTRAP_STATUS_LOADING_STATUS, "Loading network status 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 listeners = new ArrayList(); - - 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 getListeners() { - synchronized (listeners) { - return new ArrayList(listeners); - } - } - -} diff --git a/orchid/src/com/subgraph/orchid/circuits/TorInputStream.java b/orchid/src/com/subgraph/orchid/circuits/TorInputStream.java deleted file mode 100644 index a54d7739..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/TorInputStream.java +++ /dev/null @@ -1,228 +0,0 @@ -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 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(); - 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(); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/TorOutputStream.java b/orchid/src/com/subgraph/orchid/circuits/TorOutputStream.java deleted file mode 100644 index 83a54a4d..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/TorOutputStream.java +++ /dev/null @@ -1,85 +0,0 @@ -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(); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/cells/CellImpl.java b/orchid/src/com/subgraph/orchid/circuits/cells/CellImpl.java deleted file mode 100644 index 7c7496a8..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/cells/CellImpl.java +++ /dev/null @@ -1,215 +0,0 @@ -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; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/cells/RelayCellImpl.java b/orchid/src/com/subgraph/orchid/circuits/cells/RelayCellImpl.java deleted file mode 100644 index ccb09068..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/cells/RelayCellImpl.java +++ /dev/null @@ -1,180 +0,0 @@ -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; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/guards/BridgeRouterImpl.java b/orchid/src/com/subgraph/orchid/circuits/guards/BridgeRouterImpl.java deleted file mode 100644 index f487ef2b..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/guards/BridgeRouterImpl.java +++ /dev/null @@ -1,226 +0,0 @@ -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 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 + "]"; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/guards/Bridges.java b/orchid/src/com/subgraph/orchid/circuits/guards/Bridges.java deleted file mode 100644 index d11cbe99..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/guards/Bridges.java +++ /dev/null @@ -1,163 +0,0 @@ -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 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(); - this.random = new TorRandom(); - this.lock = new Object(); - this.outstandingDownloadTasks = new AtomicInteger(); - } - - BridgeRouter chooseRandomBridge(Set excluded) throws InterruptedException { - - synchronized(lock) { - if(!bridgesInitialized && !bridgesInitializing) { - initializeBridges(); - } - while(!bridgesInitialized && !hasCandidates(excluded)) { - lock.wait(); - } - final List 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 excluded) { - return !(getCandidates(excluded).isEmpty()); - } - - private List getCandidates(Set excluded) { - if(bridgeRouters.isEmpty()) { - return Collections.emptyList(); - } - final List candidates = new ArrayList(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 createDownloadTasks() { - final List tasks = new ArrayList(); - for(TorConfigBridgeLine line: config.getBridges()) { - tasks.add(new DescriptorDownloader(createBridgeFromLine(line))); - } - return tasks; - } - - private void startAllDownloadTasks() { - final List 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; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/guards/EntryGuards.java b/orchid/src/com/subgraph/orchid/circuits/guards/EntryGuards.java deleted file mode 100644 index bed49fd2..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/guards/EntryGuards.java +++ /dev/null @@ -1,305 +0,0 @@ -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.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.Threading; -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 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(); - this.bridges = new Bridges(config, directoryDownloader); - this.lock = new Object(); - this.executor = Threading.newPool("EntryGuards worker"); - } - - public boolean isUsingBridges() { - return config.getUseBridges(); - } - - public Router chooseRandomGuard(Set 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 usableGuards = getMinimumUsableGuards(excluded, MIN_USABLE_GUARDS); - final int n = Math.min(usableGuards.size(), NUM_ENTRY_GUARDS); - return usableGuards.get(random.nextInt(n)); - } - - private List getMinimumUsableGuards(Set excluded, int minSize) throws InterruptedException { - synchronized(lock) { - testStatusOfAllGuards(); - while(true) { - List 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 getUsableGuardRouters(Set excluded) { - List usableRouters = new ArrayList(); - for(GuardEntry entry: directory.getGuardEntries()) { - addRouterIfUsableAndNotExcluded(entry, excluded, usableRouters); - } - return usableRouters; - } - - private void addRouterIfUsableAndNotExcluded(GuardEntry entry, Set excluded, List routers) { - if(entry.testCurrentlyUsable() && entry.getDownSince() == null) { - final Router r = entry.getRouterForEntry(); - if(r != null && !excluded.contains(r)) { - routers.add(r); - } - } - } - - private Set getExcludedForChooseNew(Set excluded, List usable) { - final Set set = new HashSet(); - set.addAll(excluded); - set.addAll(usable); - addPendingInitialConnections(set); - return set; - } - - private void addPendingInitialConnections(Set 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 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 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; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/guards/GuardProbeTask.java b/orchid/src/com/subgraph/orchid/circuits/guards/GuardProbeTask.java deleted file mode 100644 index 553638e9..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/guards/GuardProbeTask.java +++ /dev/null @@ -1,42 +0,0 @@ -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); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HSAuthentication.java b/orchid/src/com/subgraph/orchid/circuits/hs/HSAuthentication.java deleted file mode 100644 index 0eba30ef..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HSAuthentication.java +++ /dev/null @@ -1,120 +0,0 @@ -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 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 readBasicEntries(ByteBuffer b) { - final int blockCount = b.get() & 0xFF; - final int entryCount = blockCount * 16; - final List entries = new ArrayList(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 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); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HSAuthenticationException.java b/orchid/src/com/subgraph/orchid/circuits/hs/HSAuthenticationException.java deleted file mode 100644 index 890fb8d1..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HSAuthenticationException.java +++ /dev/null @@ -1,14 +0,0 @@ -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); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptor.java b/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptor.java deleted file mode 100644 index 9d4e7cc0..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptor.java +++ /dev/null @@ -1,109 +0,0 @@ -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 introductionPoints; - - public HSDescriptor(HiddenService hiddenService) { - this.hiddenService = hiddenService; - introductionPoints = new ArrayList(); - } - - 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 getIntroductionPoints() { - return new ArrayList(introductionPoints); - } - - List getShuffledIntroductionPoints() { - return shuffle(getIntroductionPoints()); - } - - private List shuffle(List 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 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); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorCookie.java b/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorCookie.java deleted file mode 100644 index b0a38965..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorCookie.java +++ /dev/null @@ -1,33 +0,0 @@ -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; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorDirectory.java b/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorDirectory.java deleted file mode 100644 index bec763ad..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorDirectory.java +++ /dev/null @@ -1,28 +0,0 @@ -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; - } - -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorDownloader.java b/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorDownloader.java deleted file mode 100644 index 6a9d59ee..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorDownloader.java +++ /dev/null @@ -1,135 +0,0 @@ -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 directories; - - public HSDescriptorDownloader(HiddenService hiddenService, CircuitManagerImpl circuitManager, List 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 { - 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); - } - } -} \ No newline at end of file diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorKeyword.java b/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorKeyword.java deleted file mode 100644 index c883e86c..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorKeyword.java +++ /dev/null @@ -1,38 +0,0 @@ -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; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorParser.java b/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorParser.java deleted file mode 100644 index 08046ed7..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptorParser.java +++ /dev/null @@ -1,160 +0,0 @@ -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{ - private static final Logger logger = Logger.getLogger(HSDescriptor.class.getName()); - - private final DocumentFieldParser fieldParser; - private final HSDescriptor descriptor; - private final HSAuthentication authentication; - - private DocumentParsingResultHandler 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 resultHandler) { - this.resultHandler = resultHandler; - fieldParser.startSignedEntity(); - try { - fieldParser.processDocument(); - return true; - } catch(TorParsingException e) { - resultHandler.parsingError(e.getMessage()); - return false; - } - } - - - public DocumentParsingResult parse() { - final BasicDocumentParsingResult result = new BasicDocumentParsingResult(); - 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() { - - 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); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HSDirectories.java b/orchid/src/com/subgraph/orchid/circuits/hs/HSDirectories.java deleted file mode 100644 index 54292b3b..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HSDirectories.java +++ /dev/null @@ -1,116 +0,0 @@ -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 hsDirectories; - - HSDirectories(Directory directory) { - this.directory = directory; - this.hsDirectories = new ArrayList(); - this.random = new TorRandom(); - } - - List getDirectoriesForHiddenService(HiddenService hs) { - final List dirs = new ArrayList(2 * DIR_CLUSTER_SZ); - for(HexDigest id: hs.getAllCurrentDescriptorIds()) { - for(Router r: getDirectoriesForDescriptorId(id)) { - dirs.add(new HSDescriptorDirectory(id, r)); - } - } - return dirs; - } - - private List 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 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 dirs = new ArrayList(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() { - 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 dirs) { - for(int i = 0; i < dirs.size(); i++) { - swap(dirs, i, random.nextInt(dirs.size())); - } - } - - private void swap(List 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); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HiddenService.java b/orchid/src/com/subgraph/orchid/circuits/hs/HiddenService.java deleted file mode 100644 index 738590b7..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HiddenService.java +++ /dev/null @@ -1,139 +0,0 @@ -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 getAllCurrentDescriptorIds() { - final List ids = new ArrayList(); - 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; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/HiddenServiceManager.java b/orchid/src/com/subgraph/orchid/circuits/hs/HiddenServiceManager.java deleted file mode 100644 index 955901d4..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/HiddenServiceManager.java +++ /dev/null @@ -1,121 +0,0 @@ -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 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(); - 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 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; - } - } -} \ No newline at end of file diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPoint.java b/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPoint.java deleted file mode 100644 index df204819..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPoint.java +++ /dev/null @@ -1,58 +0,0 @@ -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; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPointKeyword.java b/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPointKeyword.java deleted file mode 100644 index 97ad5943..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPointKeyword.java +++ /dev/null @@ -1,37 +0,0 @@ -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; - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPointParser.java b/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPointParser.java deleted file mode 100644 index 59d3a792..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionPointParser.java +++ /dev/null @@ -1,118 +0,0 @@ -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{ - - private final DocumentFieldParser fieldParser; - - private DocumentParsingResultHandler resultHandler; - private IntroductionPoint currentIntroductionPoint; - - public IntroductionPointParser(DocumentFieldParser fieldParser) { - this.fieldParser = fieldParser; - this.fieldParser.setHandler(createParsingHandler()); - } - - public boolean parse(DocumentParsingResultHandler resultHandler) { - this.resultHandler = resultHandler; - try { - fieldParser.processDocument(); - return true; - } catch(TorParsingException e) { - resultHandler.parsingError(e.getMessage()); - return false; - } - } - - public DocumentParsingResult parse() { - final BasicDocumentParsingResult result = new BasicDocumentParsingResult(); - 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; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionProcessor.java b/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionProcessor.java deleted file mode 100644 index d182f155..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/IntroductionProcessor.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.subgraph.orchid.circuits.hs; - -import java.nio.ByteBuffer; -import java.util.logging.Logger; - -import com.subgraph.orchid.Cell; -import com.subgraph.orchid.Circuit; -import com.subgraph.orchid.RelayCell; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.crypto.HybridEncryption; -import com.subgraph.orchid.crypto.TorPublicKey; - -public class IntroductionProcessor { - private final static Logger logger = Logger.getLogger(IntroductionProcessor.class.getName()); - private final static int INTRODUCTION_PROTOCOL_VERSION = 3; - - private final HiddenService hiddenService; - private final Circuit introductionCircuit; - private final IntroductionPoint introductionPoint; - - protected IntroductionProcessor(HiddenService hiddenService, Circuit introductionCircuit, IntroductionPoint introductionPoint) { - this.hiddenService = hiddenService; - this.introductionCircuit = introductionCircuit; - this.introductionPoint = introductionPoint; - } - - TorPublicKey getServiceKey() { - return introductionPoint.getServiceKey(); - } - - boolean sendIntroduce(TorPublicKey permanentKey, byte[] publicKeyBytes, byte[] rendezvousCookie, Router rendezvousRouter) { - final RelayCell introduceCell = introductionCircuit.createRelayCell(RelayCell.RELAY_COMMAND_INTRODUCE1, 0, introductionCircuit.getFinalCircuitNode()); - - final byte[] payload = createIntroductionPayload(rendezvousRouter, publicKeyBytes, rendezvousCookie, permanentKey); - final TorPublicKey serviceKey = introductionPoint.getServiceKey(); - introduceCell.putByteArray(serviceKey.getFingerprint().getRawBytes()); - introduceCell.putByteArray(payload); - introductionCircuit.sendRelayCell(introduceCell); - - final RelayCell response = introductionCircuit.receiveRelayCell(); - if(response == null) { - logger.fine("Timeout waiting for response to INTRODUCE1 cell"); - return false; - } else if(response.getRelayCommand() != RelayCell.RELAY_COMMAND_INTRODUCE_ACK) { - logger.info("Unexpected relay cell type received waiting for response to INTRODUCE1 cell: "+ response.getRelayCommand()); - return false; - } else if(response.cellBytesRemaining() == 0) { - return true; - } else { - logger.info("INTRODUCE_ACK indicates that introduction was not forwarded: "+ response.getByte()); - return false; - } - } - - void markCircuitForClose() { - introductionCircuit.markForClose(); - } - - private byte[] createIntroductionPayload(Router rendezvousRouter, byte[] publicKeyBytes, byte[] rendezvousCookie, TorPublicKey encryptionKey) { - final ByteBuffer buffer = createIntroductionBuffer((int) (System.currentTimeMillis() / 1000), rendezvousRouter, rendezvousCookie, publicKeyBytes); - return encryptIntroductionBuffer(buffer, encryptionKey); - } - - private ByteBuffer createIntroductionBuffer(int timestamp, Router rr, byte[] cookie, byte[] dhPublic) { - final ByteBuffer buffer = ByteBuffer.allocate(Cell.CELL_LEN); - final byte[] rpAddress = rr.getAddress().getAddressDataBytes(); - final short rpPort = (short) rr.getOnionPort(); - final byte[] rpIdentity = rr.getIdentityHash().getRawBytes(); - final byte[] rpOnionKey = rr.getOnionKey().getRawBytes(); - - buffer.put((byte) INTRODUCTION_PROTOCOL_VERSION); // VER Version byte: set to 3. [1 octet] - addAuthentication(buffer); - //buffer.put((byte) 0); // AUTHT The auth type that is used [1 octet] - buffer.putInt(timestamp); // TS A timestamp [4 octets] - buffer.put(rpAddress); // IP Rendezvous point's address [4 octets] - buffer.putShort(rpPort); // PORT Rendezvous point's OR port [2 octets] - buffer.put(rpIdentity); // ID Rendezvous point identity ID [20 octets] - buffer.putShort((short) rpOnionKey.length); // KLEN Length of onion key [2 octets] - buffer.put(rpOnionKey); // KEY Rendezvous point onion key [KLEN octets] - buffer.put(cookie); // RC Rendezvous cookie [20 octets] - buffer.put(dhPublic); // g^x Diffie-Hellman data, part 1 [128 octets] - - return buffer; - } - - private void addAuthentication(ByteBuffer buffer) { - HSDescriptorCookie cookie = hiddenService.getAuthenticationCookie(); - if(cookie == null) { - buffer.put((byte) 0); - } else { - buffer.put(cookie.getAuthTypeByte()); - buffer.putShort((short) cookie.getValue().length); - buffer.put(cookie.getValue()); - } - } - - private byte[] encryptIntroductionBuffer(ByteBuffer buffer, TorPublicKey key) { - final int len = buffer.position(); - final byte[] payload = new byte[len]; - buffer.flip(); - buffer.get(payload); - final HybridEncryption enc = new HybridEncryption(); - return enc.encrypt(payload, key); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/RendezvousCircuitBuilder.java b/orchid/src/com/subgraph/orchid/circuits/hs/RendezvousCircuitBuilder.java deleted file mode 100644 index 089fb227..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/RendezvousCircuitBuilder.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.subgraph.orchid.circuits.hs; - -import java.util.concurrent.Callable; -import java.util.logging.Logger; - -import com.subgraph.orchid.Circuit; -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.HiddenServiceCircuit; -import com.subgraph.orchid.InternalCircuit; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.circuits.CircuitManagerImpl; -import com.subgraph.orchid.crypto.TorTapKeyAgreement; - -public class RendezvousCircuitBuilder implements Callable{ - private final Logger logger = Logger.getLogger(RendezvousCircuitBuilder.class.getName()); - - private final Directory directory; - - private final CircuitManagerImpl circuitManager; - private final HiddenService hiddenService; - private final HSDescriptor serviceDescriptor; - - public RendezvousCircuitBuilder(Directory directory, CircuitManagerImpl circuitManager, HiddenService hiddenService, HSDescriptor descriptor) { - this.directory = directory; - this.circuitManager = circuitManager; - this.hiddenService = hiddenService; - this.serviceDescriptor = descriptor; - } - - public HiddenServiceCircuit call() throws Exception { - - logger.fine("Opening rendezvous circuit for "+ logServiceName()); - - final InternalCircuit rendezvous = circuitManager.getCleanInternalCircuit(); - logger.fine("Establishing rendezvous for "+ logServiceName()); - RendezvousProcessor rp = new RendezvousProcessor(rendezvous); - if(!rp.establishRendezvous()) { - rendezvous.markForClose(); - return null; - } - logger.fine("Opening introduction circuit for "+ logServiceName()); - final IntroductionProcessor introductionProcessor = openIntroduction(); - if(introductionProcessor == null) { - logger.info("Failed to open connection to any introduction point"); - rendezvous.markForClose(); - return null; - } - logger.fine("Sending introduce cell for "+ logServiceName()); - final TorTapKeyAgreement kex = new TorTapKeyAgreement(); - final boolean icResult = introductionProcessor.sendIntroduce(introductionProcessor.getServiceKey(), kex.getPublicKeyBytes(), rp.getCookie(), rp.getRendezvousRouter()); - introductionProcessor.markCircuitForClose(); - if(!icResult) { - rendezvous.markForClose(); - return null; - } - logger.fine("Processing RV2 for "+ logServiceName()); - HiddenServiceCircuit hsc = rp.processRendezvous2(kex); - if(hsc == null) { - rendezvous.markForClose(); - } - - logger.fine("Rendezvous circuit opened for "+ logServiceName()); - - return hsc; - } - - private String logServiceName() { - return hiddenService.getOnionAddressForLogging(); - } - - private IntroductionProcessor openIntroduction() { - for(IntroductionPoint ip: serviceDescriptor.getShuffledIntroductionPoints()) { - final Circuit circuit = attemptOpenIntroductionCircuit(ip); - if(circuit != null) { - return new IntroductionProcessor(hiddenService, circuit, ip); - } - } - return null; - } - - private Circuit attemptOpenIntroductionCircuit(IntroductionPoint ip) { - final Router r = directory.getRouterByIdentity(ip.getIdentity()); - if(r == null) { - return null; - } - - try { - final InternalCircuit circuit = circuitManager.getCleanInternalCircuit(); - return circuit.cannibalizeToIntroductionPoint(r); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return null; - } catch (TorException e) { - logger.fine("cannibalizeTo() failed : "+ e.getMessage()); - return null; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/hs/RendezvousProcessor.java b/orchid/src/com/subgraph/orchid/circuits/hs/RendezvousProcessor.java deleted file mode 100644 index 9584fae6..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/hs/RendezvousProcessor.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.subgraph.orchid.circuits.hs; - -import java.math.BigInteger; -import java.util.logging.Logger; - -import com.subgraph.orchid.Cell; -import com.subgraph.orchid.HiddenServiceCircuit; -import com.subgraph.orchid.InternalCircuit; -import com.subgraph.orchid.RelayCell; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.circuits.CircuitNodeCryptoState; -import com.subgraph.orchid.circuits.CircuitNodeImpl; -import com.subgraph.orchid.crypto.TorMessageDigest; -import com.subgraph.orchid.crypto.TorRandom; -import com.subgraph.orchid.crypto.TorTapKeyAgreement; -import com.subgraph.orchid.data.HexDigest; - -public class RendezvousProcessor { - private final static Logger logger = Logger.getLogger(RendezvousProcessor.class.getName()); - - private final static int RENDEZVOUS_COOKIE_LEN = 20; - private final static TorRandom random = new TorRandom(); - - private final InternalCircuit circuit; - private final byte[] cookie; - - protected RendezvousProcessor(InternalCircuit circuit) { - this.circuit = circuit; - this.cookie = random.getBytes(RENDEZVOUS_COOKIE_LEN); - } - - boolean establishRendezvous() { - final RelayCell cell = circuit.createRelayCell(RelayCell.RELAY_COMMAND_ESTABLISH_RENDEZVOUS, 0, circuit.getFinalCircuitNode()); - cell.putByteArray(cookie); - circuit.sendRelayCell(cell); - final RelayCell response = circuit.receiveRelayCell(); - if(response == null) { - logger.info("Timeout waiting for Rendezvous establish response"); - return false; - } else if(response.getRelayCommand() != RelayCell.RELAY_COMMAND_RENDEZVOUS_ESTABLISHED) { - logger.info("Response received from Rendezvous establish was not expected acknowledgement, Relay Command: "+ response.getRelayCommand()); - return false; - } else { - return true; - } - } - - HiddenServiceCircuit processRendezvous2(TorTapKeyAgreement kex) { - final RelayCell cell = circuit.receiveRelayCell(); - if(cell == null) { - logger.info("Timeout waiting for RENDEZVOUS2"); - return null; - } else if (cell.getRelayCommand() != RelayCell.RELAY_COMMAND_RENDEZVOUS2) { - logger.info("Unexpected Relay cell type received while waiting for RENDEZVOUS2: "+ cell.getRelayCommand()); - return null; - } - final BigInteger peerPublic = readPeerPublic(cell); - final HexDigest handshakeDigest = readHandshakeDigest(cell); - if(peerPublic == null || handshakeDigest == null) { - return null; - } - final byte[] verifyHash = new byte[TorMessageDigest.TOR_DIGEST_SIZE]; - final byte[] keyMaterial = new byte[CircuitNodeCryptoState.KEY_MATERIAL_SIZE]; - if(!kex.deriveKeysFromDHPublicAndHash(peerPublic, handshakeDigest.getRawBytes(), keyMaterial, verifyHash)) { - logger.info("Error deriving session keys while extending to hidden service"); - return null; - } - return circuit.connectHiddenService(CircuitNodeImpl.createAnonymous(circuit.getFinalCircuitNode(), keyMaterial, verifyHash)); - } - - private BigInteger readPeerPublic(Cell cell) { - final byte[] dhPublic = new byte[TorTapKeyAgreement.DH_LEN]; - cell.getByteArray(dhPublic); - final BigInteger peerPublic = new BigInteger(1, dhPublic); - if(!TorTapKeyAgreement.isValidPublicValue(peerPublic)) { - logger.warning("Illegal DH public value received: "+ peerPublic); - return null; - } - return peerPublic; - } - - HexDigest readHandshakeDigest(Cell cell) { - final byte[] digestBytes = new byte[TorMessageDigest.TOR_DIGEST_SIZE]; - cell.getByteArray(digestBytes); - return HexDigest.createFromDigestBytes(digestBytes); - } - - - byte[] getCookie() { - return cookie; - } - - Router getRendezvousRouter() { - return circuit.getFinalCircuitNode().getRouter(); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/path/BandwidthWeightedRouters.java b/orchid/src/com/subgraph/orchid/circuits/path/BandwidthWeightedRouters.java deleted file mode 100644 index 1521d8a0..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/path/BandwidthWeightedRouters.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.subgraph.orchid.circuits.path; - -import java.util.ArrayList; -import java.util.List; - -import com.subgraph.orchid.Router; -import com.subgraph.orchid.crypto.TorRandom; - -public class BandwidthWeightedRouters { - private static class WeightedRouter { - private final Router router; - private boolean isUnknown; - private double weightedBandwidth; - private long scaledBandwidth; - - WeightedRouter(Router router, double bw) { - this.router = router; - this.weightedBandwidth = bw; - } - - void scaleBandwidth(double scaleFactor) { - scaledBandwidth = Math.round(weightedBandwidth * scaleFactor); - } - } - - private final static long MAX_SCALE = Long.MAX_VALUE / 4; - private final static double EPSILON = 0.1; - private final List weightedRouters = new ArrayList(); - private final TorRandom random = new TorRandom(); - - private double totalExitBw; - private double totalNonExitBw; - private double totalGuardBw; - - private boolean isScaled; - private int unknownCount; - - void addRouter(Router router, double weightedBandwidth) { - weightedRouters.add(new WeightedRouter(router, weightedBandwidth)); - adjustTotals(router, weightedBandwidth); - isScaled = false; - } - - - boolean isTotalBandwidthZero() { - return getTotalBandwidth() < EPSILON; - } - - double getTotalBandwidth() { - return totalExitBw + totalNonExitBw; - } - - double getTotalGuardBandwidth() { - return totalGuardBw; - } - - - double getTotalExitBandwidth() { - return totalExitBw; - } - - private void adjustTotals(Router router, double bw) { - if(router.isExit()) { - totalExitBw += bw; - } else { - totalNonExitBw += bw; - } - if(router.isPossibleGuard()) { - totalGuardBw += bw; - } - } - - void addRouterUnknown(Router router) { - final WeightedRouter wr = new WeightedRouter(router, 0); - wr.isUnknown = true; - weightedRouters.add(wr); - unknownCount += 1; - } - - int getRouterCount() { - return weightedRouters.size(); - } - - int getUnknownCount() { - return unknownCount; - } - - void fixUnknownValues() { - if(unknownCount == 0) { - return; - } - if(isTotalBandwidthZero()) { - fixUnknownValues(40000, 20000); - } else { - final int knownCount = weightedRouters.size() - unknownCount; - final long average = (long) (getTotalBandwidth() / knownCount); - fixUnknownValues(average, average); - } - } - - private void fixUnknownValues(long fastBw, long slowBw) { - for(WeightedRouter wr: weightedRouters) { - if(wr.isUnknown) { - long bw = wr.router.isFast() ? fastBw : slowBw; - wr.weightedBandwidth = bw; - wr.isUnknown = false; - adjustTotals(wr.router, bw); - } - } - unknownCount = 0; - isScaled = false; - } - - Router chooseRandomRouterByWeight() { - final long total = getScaledTotal(); - if(total == 0) { - if(weightedRouters.size() == 0) { - return null; - } - final int idx = random.nextInt(weightedRouters.size()); - return weightedRouters.get(idx).router; - } - return chooseFirstElementAboveRandom(random.nextLong(total)); - } - - void adjustWeights(double exitWeight, double guardWeight) { - for(WeightedRouter wr: weightedRouters) { - Router r = wr.router; - if(r.isExit() && r.isPossibleGuard()) { - wr.weightedBandwidth *= (exitWeight * guardWeight); - } else if(r.isPossibleGuard()) { - wr.weightedBandwidth *= guardWeight; - } else if(r.isExit()) { - wr.weightedBandwidth *= exitWeight; - } - } - scaleRouterWeights(); - } - - private Router chooseFirstElementAboveRandom(long randomValue) { - long sum = 0; - Router chosen = null; - for(WeightedRouter wr: weightedRouters) { - sum += wr.scaledBandwidth; - if(sum > randomValue) { - chosen = wr.router; - /* Don't return early to avoid leaking timing information about choice */ - randomValue = Long.MAX_VALUE; - } - } - if(chosen == null) { - return weightedRouters.get(weightedRouters.size() - 1).router; - } - return chosen; - } - - private double getWeightedTotal() { - double total = 0.0; - for(WeightedRouter wr: weightedRouters) { - total += wr.weightedBandwidth; - } - return total; - } - - private void scaleRouterWeights() { - final double scaleFactor = MAX_SCALE / getWeightedTotal(); - for(WeightedRouter wr: weightedRouters) { - wr.scaleBandwidth(scaleFactor); - } - isScaled = true; - } - - private long getScaledTotal() { - if(!isScaled) { - scaleRouterWeights(); - } - long total = 0; - for(WeightedRouter wr: weightedRouters) { - total += wr.scaledBandwidth; - } - return total; - } -} - \ No newline at end of file diff --git a/orchid/src/com/subgraph/orchid/circuits/path/CircuitNodeChooser.java b/orchid/src/com/subgraph/orchid/circuits/path/CircuitNodeChooser.java deleted file mode 100644 index 30047d94..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/path/CircuitNodeChooser.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.subgraph.orchid.circuits.path; - -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Logger; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.crypto.TorRandom; - -public class CircuitNodeChooser { - private final static Logger logger = Logger.getLogger(CircuitNodeChooser.class.getName()); - - public enum WeightRule { WEIGHT_FOR_DIR, WEIGHT_FOR_EXIT, WEIGHT_FOR_MID, WEIGHT_FOR_GUARD, NO_WEIGHTING}; - private final Directory directory; - private final TorRandom random = new TorRandom(); - - private final TorConfigNodeFilter configNodeFilter; - - - public CircuitNodeChooser(TorConfig config, Directory directory) { - this.directory = directory; - this.configNodeFilter = new TorConfigNodeFilter(config); - } - - /** - * - * @param candidates - * @return The chosen exit router or 'null' if no suitable router is available - */ - public Router chooseExitNode(List candidates) { - final List filteredCandidates = configNodeFilter.filterExitCandidates(candidates); - return chooseByBandwidth(filteredCandidates, WeightRule.WEIGHT_FOR_EXIT); - } - - public Router chooseDirectory() { - final RouterFilter filter = new RouterFilter() { - public boolean filter(Router router) { - return router.getDirectoryPort() != 0; - } - }; - final List candidates = getFilteredRouters(filter, false); - final Router choice = chooseByBandwidth(candidates, WeightRule.WEIGHT_FOR_DIR); - if(choice == null) { - return directory.getRandomDirectoryAuthority(); - } else { - return choice; - } - } - - /** - * - * @param rule - * @param routerFilter - * @return The chosen router or 'null' if no suitable router is available. - */ - public Router chooseRandomNode(WeightRule rule, RouterFilter routerFilter) { - final List candidates = getFilteredRouters(routerFilter, true); - final Router choice = chooseByBandwidth(candidates, rule); - if(choice == null) { - // try again with more permissive flags - return null; - } - return choice; - } - - private List getFilteredRouters(RouterFilter rf, boolean needDescriptor) { - final List routers = new ArrayList(); - for(Router r: getUsableRouters(needDescriptor)) { - if(rf.filter(r)) { - routers.add(r); - } - } - return routers; - } - - List getUsableRouters(boolean needDescriptor) { - final List routers = new ArrayList(); - for(Router r: directory.getAllRouters()) { - if(r.isRunning() && - r.isValid() && - !r.isHibernating() && - !(needDescriptor && r.getCurrentDescriptor() == null)) { - - routers.add(r); - } - } - - return routers; - } - - private Router chooseByBandwidth(List candidates, WeightRule rule) { - final Router choice = chooseNodeByBandwidthWeights(candidates, rule); - if(choice != null) { - return choice; - } else { - return chooseNodeByBandwidth(candidates, rule); - } - } - - private Router chooseNodeByBandwidthWeights(List candidates, WeightRule rule) { - final ConsensusDocument consensus = directory.getCurrentConsensusDocument(); - if(consensus == null) { - return null; - } - final BandwidthWeightedRouters bwr = computeWeightedBandwidths(candidates, consensus, rule); - return bwr.chooseRandomRouterByWeight(); - } - - - private BandwidthWeightedRouters computeWeightedBandwidths(List candidates, ConsensusDocument consensus, WeightRule rule) { - final CircuitNodeChooserWeightParameters wp = CircuitNodeChooserWeightParameters.create(consensus, rule); - if(!wp.isValid()) { - logger.warning("Got invalid bandwidth weights. Falling back to old selection method"); - return null; - } - final BandwidthWeightedRouters weightedRouters = new BandwidthWeightedRouters(); - for(Router r: candidates) { - double wbw = wp.calculateWeightedBandwidth(r); - weightedRouters.addRouter(r, wbw); - } - return weightedRouters; - } - - private Router chooseNodeByBandwidth(List routers, WeightRule rule) { - final BandwidthWeightedRouters bwr = new BandwidthWeightedRouters(); - for(Router r: routers) { - long bw = getRouterBandwidthBytes(r); - if(bw == -1) { - bwr.addRouterUnknown(r); - } else { - bwr.addRouter(r, bw); - } - } - bwr.fixUnknownValues(); - if(bwr.isTotalBandwidthZero()) { - if(routers.size() == 0) { - return null; - } - - final int idx = random.nextInt(routers.size()); - return routers.get(idx); - } - - computeFinalWeights(bwr, rule); - return bwr.chooseRandomRouterByWeight(); - } - - - private final static double EPSILON = 0.1; - - private void computeFinalWeights(BandwidthWeightedRouters bwr, WeightRule rule) { - final double exitWeight = calculateWeight(rule == WeightRule.WEIGHT_FOR_EXIT, - bwr.getTotalExitBandwidth(), bwr.getTotalBandwidth()); - final double guardWeight = calculateWeight(rule == WeightRule.WEIGHT_FOR_GUARD, - bwr.getTotalGuardBandwidth(), bwr.getTotalBandwidth()); - - bwr.adjustWeights(exitWeight, guardWeight); - } - - private double calculateWeight(boolean matchesRule, double totalByType, double total) { - if(matchesRule || totalByType < EPSILON) { - return 1.0; - } - final double result = 1.0 - (total / (3.0 * totalByType)); - if(result <= 0.0) { - return 0.0; - } else { - return result; - } - } - - private long getRouterBandwidthBytes(Router r) { - if(!r.hasBandwidth()) { - return -1; - } else { - return kbToBytes(r.getEstimatedBandwidth()); - } - } - - private long kbToBytes(long bw) { - return (bw > (Long.MAX_VALUE / 1000) ? Long.MAX_VALUE : bw * 1000); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/path/CircuitNodeChooserWeightParameters.java b/orchid/src/com/subgraph/orchid/circuits/path/CircuitNodeChooserWeightParameters.java deleted file mode 100644 index 49125ea6..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/path/CircuitNodeChooserWeightParameters.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.subgraph.orchid.circuits.path; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.Router; - -class CircuitNodeChooserWeightParameters { - private final static int VAR_WG = 0; - private final static int VAR_WM = 1; - private final static int VAR_WE = 2; - private final static int VAR_WD = 3; - private final static int VAR_WGB = 4; - private final static int VAR_WMB = 5; - private final static int VAR_WEB = 6; - private final static int VAR_WDB = 7; - private final static int VAR_COUNT = 8; - - private final static String ZERO = "zero"; - private final static String ONE = "one"; - - static CircuitNodeChooserWeightParameters create(ConsensusDocument consensus, CircuitNodeChooser.WeightRule rule) { - final double[] vars = new double[VAR_COUNT]; - final long scale = consensus.getWeightScaleParameter(); - final String[] tags = getTagsForWeightRule(rule); - if(!populateVars(consensus, scale, tags, vars)) { - return new CircuitNodeChooserWeightParameters(new double[VAR_COUNT], false); - } else { - return new CircuitNodeChooserWeightParameters(vars, true); - } - } - - static boolean populateVars(ConsensusDocument consensus, long scale, String[] tags, double[] vars) { - for(int i = 0; i < VAR_COUNT; i++) { - vars[i] = tagToVarValue(consensus, scale, tags[i]); - if(vars[i] < 0.0) { - return false; - } else { - vars[i] /= scale; - } - } - return true; - } - - static double tagToVarValue(ConsensusDocument consensus, long scale, String tag) { - if(tag.equals(ZERO)) { - return 0.0; - } else if (tag.equals(ONE)) { - return 1.0; - } else { - return consensus.getBandwidthWeight(tag); - } - } - - static String[] getTagsForWeightRule(CircuitNodeChooser.WeightRule rule) { - switch(rule) { - case WEIGHT_FOR_GUARD: - return new String[] { - "Wgg", "Wgm", ZERO, "Wgd", - "Wgb", "Wmb", "Web", "Wdb"}; - - case WEIGHT_FOR_MID: - return new String[] { - "Wmg", "Wmm", "Wme", "Wmd", - "Wgb", "Wmb", "Web", "Wdb"}; - - case WEIGHT_FOR_EXIT: - return new String[] { - "Wee", "Wem", "Wed", "Weg", - "Wgb", "Wmb", "Web", "Wdb"}; - - case WEIGHT_FOR_DIR: - return new String[] { - "Wbe", "Wbm", "Wbd", "Wbg", - ONE, ONE, ONE, ONE }; - - case NO_WEIGHTING: - return new String[] { - ONE, ONE, ONE, ONE, - ONE, ONE, ONE, ONE }; - default: - throw new IllegalArgumentException("Unhandled WeightRule type: "+ rule); - } - } - - private final double[] vars; - private final boolean isValid; - - private CircuitNodeChooserWeightParameters(double[] vars, boolean isValid) { - this.vars = vars; - this.isValid = isValid; - } - - boolean isValid() { - return isValid; - } - - double getWg() { - return vars[VAR_WG]; - } - - double getWm() { - return vars[VAR_WM]; - } - - double getWe() { - return vars[VAR_WE]; - } - - double getWd() { - return vars[VAR_WD]; - } - - double getWgb() { - return vars[VAR_WGB]; - } - double getWmb() { - return vars[VAR_WMB]; - } - double getWeb() { - return vars[VAR_WEB]; - } - double getWdb() { - return vars[VAR_WDB]; - } - - double calculateWeightedBandwidth(Router router) { - final long bw = kbToBytes(router.getEstimatedBandwidth()); - final double w = calculateWeight( - router.isExit() && !router.isBadExit(), - router.isPossibleGuard(), - router.getDirectoryPort() != 0); - return (w * bw) + 0.5; - } - - long kbToBytes(long kb) { - return (kb > (Long.MAX_VALUE / 1000) ? Long.MAX_VALUE : kb * 1000); - } - - private double calculateWeight(boolean isExit, boolean isGuard, boolean isDir) { - if(isGuard && isExit) { - return (isDir) ? getWdb() * getWd() : getWd(); - } else if (isGuard) { - return (isDir) ? getWgb() * getWg() : getWg(); - } else if (isExit) { - return (isDir) ? getWeb() * getWe() : getWe(); - } else { // middle - return (isDir) ? getWmb() * getWm() : getWm(); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/path/CircuitPathChooser.java b/orchid/src/com/subgraph/orchid/circuits/path/CircuitPathChooser.java deleted file mode 100644 index 5bae0b64..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/path/CircuitPathChooser.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.subgraph.orchid.circuits.path; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.circuits.guards.EntryGuards; -import com.subgraph.orchid.circuits.path.CircuitNodeChooser.WeightRule; -import com.subgraph.orchid.data.IPv4Address; -import com.subgraph.orchid.data.exitpolicy.ExitTarget; - -public class CircuitPathChooser { - - public static CircuitPathChooser create(TorConfig config, Directory directory) { - return new CircuitPathChooser(config, directory, new CircuitNodeChooser(config, directory)); - } - - private final Directory directory; - private final CircuitNodeChooser nodeChooser; - - private EntryGuards entryGuards; - private boolean useEntryGuards; - - CircuitPathChooser(TorConfig config, Directory directory, CircuitNodeChooser nodeChooser) { - this.directory = directory; - this.nodeChooser = nodeChooser; - this.entryGuards = null; - this.useEntryGuards = false; - } - - public void enableEntryGuards(EntryGuards entryGuards) { - this.entryGuards = entryGuards; - this.useEntryGuards = true; - } - - public List chooseDirectoryPath() throws InterruptedException { - if(useEntryGuards && entryGuards.isUsingBridges()) { - final Set empty = Collections.emptySet(); - final Router bridge = entryGuards.chooseRandomGuard(empty); - if(bridge == null) { - throw new IllegalStateException("Failed to choose bridge for directory request"); - } - return Arrays.asList(bridge); - } - final Router dir = nodeChooser.chooseDirectory(); - return Arrays.asList(dir); - } - - public List chooseInternalPath() throws InterruptedException, PathSelectionFailedException { - final Set excluded = Collections.emptySet(); - final Router finalRouter = chooseMiddleNode(excluded); - return choosePathWithFinal(finalRouter); - } - - public List choosePathWithExit(Router exitRouter) throws InterruptedException, PathSelectionFailedException { - return choosePathWithFinal(exitRouter); - } - - public List choosePathWithFinal(Router finalRouter) throws InterruptedException, PathSelectionFailedException { - final Set excluded = new HashSet(); - excludeChosenRouterAndRelated(finalRouter, excluded); - - final Router middleRouter = chooseMiddleNode(excluded); - if(middleRouter == null) { - throw new PathSelectionFailedException("Failed to select suitable middle node"); - } - excludeChosenRouterAndRelated(middleRouter, excluded); - - final Router entryRouter = chooseEntryNode(excluded); - if(entryRouter == null) { - throw new PathSelectionFailedException("Failed to select suitable entry node"); - } - return Arrays.asList(entryRouter, middleRouter, finalRouter); - } - - public Router chooseEntryNode(final Set excludedRouters) throws InterruptedException { - if(useEntryGuards) { - return entryGuards.chooseRandomGuard(excludedRouters); - } - - return nodeChooser.chooseRandomNode(WeightRule.WEIGHT_FOR_GUARD, new RouterFilter() { - public boolean filter(Router router) { - return router.isPossibleGuard() && !excludedRouters.contains(router); - } - }); - } - - Router chooseMiddleNode(final Set excludedRouters) { - return nodeChooser.chooseRandomNode(WeightRule.WEIGHT_FOR_MID, new RouterFilter() { - public boolean filter(Router router) { - return router.isFast() && !excludedRouters.contains(router); - } - }); - } - - public Router chooseExitNodeForTargets(List targets) { - final List routers = filterForExitTargets( - getUsableExitRouters(), targets); - return nodeChooser.chooseExitNode(routers); - } - - private List getUsableExitRouters() { - final List result = new ArrayList(); - for(Router r: nodeChooser.getUsableRouters(true)) { - if(r.isExit() && !r.isBadExit()) { - result.add(r); - } - } - return result; - } - - private void excludeChosenRouterAndRelated(Router router, Set excludedRouters) { - excludedRouters.add(router); - for(Router r: directory.getAllRouters()) { - if(areInSameSlash16(router, r)) { - excludedRouters.add(r); - } - } - - for(String s: router.getFamilyMembers()) { - Router r = directory.getRouterByName(s); - if(r != null) { - // Is mutual? - if(isFamilyMember(r.getFamilyMembers(), router)) { - excludedRouters.add(r); - } - } - } - } - - private boolean isFamilyMember(Collection familyMemberNames, Router r) { - for(String s: familyMemberNames) { - Router member = directory.getRouterByName(s); - if(member != null && member.equals(r)) { - return true; - } - } - return false; - } - - // Are routers r1 and r2 in the same /16 network - private boolean areInSameSlash16(Router r1, Router r2) { - final IPv4Address a1 = r1.getAddress(); - final IPv4Address a2 = r2.getAddress(); - final int mask = 0xFFFF0000; - return (a1.getAddressData() & mask) == (a2.getAddressData() & mask); - } - - private List filterForExitTargets(List routers, List exitTargets) { - int bestSupport = 0; - if(exitTargets.isEmpty()) { - return routers; - } - - final int[] nSupport = new int[routers.size()]; - - for(int i = 0; i < routers.size(); i++) { - final Router r = routers.get(i); - nSupport[i] = countTargetSupport(r, exitTargets); - if(nSupport[i] > bestSupport) { - bestSupport = nSupport[i]; - } - } - - if(bestSupport == 0) { - return routers; - } - - final List results = new ArrayList(); - for(int i = 0; i < routers.size(); i++) { - if(nSupport[i] == bestSupport) { - results.add(routers.get(i)); - } - } - return results; - } - - private int countTargetSupport(Router router, List targets) { - int count = 0; - for(ExitTarget t: targets) { - if(routerSupportsTarget(router, t)) { - count += 1; - } - } - return count; - } - - private boolean routerSupportsTarget(Router router, ExitTarget target) { - if(target.isAddressTarget()) { - return router.exitPolicyAccepts(target.getAddress(), target.getPort()); - } else { - return router.exitPolicyAccepts(target.getPort()); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/path/ConfigNodeFilter.java b/orchid/src/com/subgraph/orchid/circuits/path/ConfigNodeFilter.java deleted file mode 100644 index bb5d7524..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/path/ConfigNodeFilter.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.subgraph.orchid.circuits.path; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.subgraph.orchid.Router; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; - -/** - * Implements configuration options: - * - * ExcludeNodes,ExcludeExitNodes,ExitNodes,EntryNodes - * - */ -public class ConfigNodeFilter implements RouterFilter { - - private final static Pattern NETMASK_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)/(\\d+)$"); - private final static Pattern ADDRESS_BITS_PATTERN = Pattern.compile("^(\\d+\\.\\d+\\.\\d+\\.\\d+)/(\\d+)$"); - - private final static Pattern IDENTITY_PATTERN = Pattern.compile("^[A-Fa-f0-9]{40}$"); - private final static Pattern COUNTRYCODE_PATTERN = Pattern.compile("^\\{([A-Za-z]{2})\\}$"); - private final static Pattern ROUTERNAME_PATTERN = Pattern.compile("^\\w{1,19}$"); - - static class MaskFilter implements RouterFilter { - - private final int network; - private final int bits; - private final int mask; - - - static int createMask(final int maskBitCount) { - return maskBitCount == 0 ? 0 : (1 << 31) >> (maskBitCount - 1); - } - - MaskFilter(IPv4Address network, int bits) { - this.bits = bits; - this.mask = createMask(bits); - this.network = network.getAddressData() & mask; - } - - public boolean filter(Router router) { - final int routerAddress = router.getAddress().getAddressData(); - return (routerAddress & mask) == network; - } - - public String toString() { - IPv4Address a = new IPv4Address(network); - return a.toString() + "/" + bits; - - } - } - - static class IdentityFilter implements RouterFilter { - private final HexDigest identity; - IdentityFilter(HexDigest identity) { - this.identity = identity; - } - public boolean filter(Router router) { - return router.getIdentityHash().equals(identity); - } - } - - static class NameFilter implements RouterFilter { - private final String name; - NameFilter(String name) { - this.name = name; - } - public boolean filter(Router router) { - return name.equals(router.getNickname()); - } - } - - static class CountryCodeFilter implements RouterFilter { - private final String countryCode; - public CountryCodeFilter(String countryCode) { - this.countryCode = countryCode; - } - public boolean filter(Router router) { - return countryCode.equalsIgnoreCase(router.getCountryCode()); - } - } - - static boolean isAddressString(String s) { - Matcher matcher = NETMASK_PATTERN.matcher(s); - if(!matcher.matches()) { - return false; - } - try { - for(int i = 1; i < 5; i++) { - if(!isValidOctetString(matcher.group(i))) { - return false; - } - } - return isValidMaskValue(matcher.group(5)); - } catch (NumberFormatException e) { - return false; - } - } - - private static boolean isValidOctetString(String s) { - int n = Integer.parseInt(s); - return n >= 0 && n <= 255; - } - - private static boolean isValidMaskValue(String s) { - int n = Integer.parseInt(s); - return n > 0 && n <= 32; - } - - static boolean isIdentityString(String s) { - return IDENTITY_PATTERN.matcher(s).matches(); - } - - static boolean isCountryCodeString(String s) { - return COUNTRYCODE_PATTERN.matcher(s).matches(); - } - - static boolean isNameString(String s) { - return ROUTERNAME_PATTERN.matcher(s).matches(); - } - - static RouterFilter createFilterFor(String s) { - if(isAddressString(s)) { - return createAddressFilter(s); - } else if(isCountryCodeString(s)) { - return createCountryCodeFilter(s); - } else if(isIdentityString(s)) { - return createIdentityFilter(s); - } else if (isNameString(s)) { - return createNameFilter(s); - } else { - return null; - } - } - - private static RouterFilter createAddressFilter(String s) { - final Matcher matcher = ADDRESS_BITS_PATTERN.matcher(s); - if(!matcher.matches()) { - throw new IllegalArgumentException(); - } - final IPv4Address network = IPv4Address.createFromString(matcher.group(1)); - final int bits = Integer.parseInt(matcher.group(2)); - return new MaskFilter(network, bits); - } - - private static RouterFilter createIdentityFilter(String s) { - if(isIdentityString(s)) { - throw new IllegalArgumentException(); - } - final HexDigest identity = HexDigest.createFromString(s); - return new IdentityFilter(identity); - } - - private static RouterFilter createCountryCodeFilter(String s) { - final Matcher matcher = COUNTRYCODE_PATTERN.matcher(s); - if(!matcher.matches()) { - throw new IllegalArgumentException(); - } - return new CountryCodeFilter(matcher.group(1)); - } - - private static RouterFilter createNameFilter(String s) { - if(!isNameString(s)) { - throw new IllegalArgumentException(); - } - return new NameFilter(s); - } - - static ConfigNodeFilter createFromStrings(List stringList) { - final List filters = new ArrayList(); - for(String s: stringList) { - RouterFilter f = createFilterFor(s); - if(f != null) { - filters.add(f); - } - } - return new ConfigNodeFilter(filters); - } - - private final List filterList; - - private ConfigNodeFilter(List filterList) { - this.filterList = filterList; - } - - public boolean filter(Router router) { - for(RouterFilter f: filterList) { - if(f.filter(router)) { - return true; - } - } - return false; - } - - boolean isEmpty() { - return filterList.isEmpty(); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/path/PathSelectionFailedException.java b/orchid/src/com/subgraph/orchid/circuits/path/PathSelectionFailedException.java deleted file mode 100644 index a669ac2c..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/path/PathSelectionFailedException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.subgraph.orchid.circuits.path; - -public class PathSelectionFailedException extends Exception { - private static final long serialVersionUID = -8855252756021674268L; - - public PathSelectionFailedException() {} - - public PathSelectionFailedException(String message) { - super(message); - } -} diff --git a/orchid/src/com/subgraph/orchid/circuits/path/RouterFilter.java b/orchid/src/com/subgraph/orchid/circuits/path/RouterFilter.java deleted file mode 100644 index ae883017..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/path/RouterFilter.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.subgraph.orchid.circuits.path; - -import com.subgraph.orchid.Router; - -public interface RouterFilter { - boolean filter(Router router); -} diff --git a/orchid/src/com/subgraph/orchid/circuits/path/TorConfigNodeFilter.java b/orchid/src/com/subgraph/orchid/circuits/path/TorConfigNodeFilter.java deleted file mode 100644 index 800509bb..00000000 --- a/orchid/src/com/subgraph/orchid/circuits/path/TorConfigNodeFilter.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.subgraph.orchid.circuits.path; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import com.subgraph.orchid.Router; -import com.subgraph.orchid.TorConfig; - -public class TorConfigNodeFilter { - - /* - * Even though these are exactly the configuration file variable names, they are only - * used here as keys into a Map - */ - private final static String EXCLUDE_NODES_FILTER = "ExcludeNodes"; - private final static String EXCLUDE_EXIT_NODES_FILTER = "ExcludeExitNodes"; - private final static String ENTRY_NODES_FILTER = "EntryNodes"; - private final static String EXIT_NODES_FILTER = "ExitNodes"; - - private final Map filters; - - TorConfigNodeFilter(TorConfig config) { - this.filters = new HashMap(); - addFilter(filters, EXCLUDE_NODES_FILTER, config.getExcludeNodes()); - addFilter(filters, EXCLUDE_EXIT_NODES_FILTER, config.getExcludeExitNodes()); - addFilter(filters, ENTRY_NODES_FILTER, config.getEntryNodes()); - addFilter(filters, EXIT_NODES_FILTER, config.getExitNodes()); - } - - private static void addFilter(Map filters, String name, List filterStrings) { - if(filterStrings == null || filterStrings.isEmpty()) { - return; - } - filters.put(name, ConfigNodeFilter.createFromStrings(filterStrings)); - } - - List filterExitCandidates(List candidates) { - final List filtered = new ArrayList(); - for(Router r: candidates) { - if(isExitNodeIncluded(r)) { - filtered.add(r); - } - } - return filtered; - } - - boolean isExitNodeIncluded(Router exitRouter) { - return isIncludedByFilter(exitRouter, EXIT_NODES_FILTER) && - !(isExcludedByFilter(exitRouter, EXCLUDE_EXIT_NODES_FILTER) || - isExcludedByFilter(exitRouter, EXCLUDE_NODES_FILTER)); - } - - boolean isIncludedByFilter(Router r, String filterName) { - final ConfigNodeFilter f = filters.get(filterName); - if(f == null || f.isEmpty()) { - return true; - } - return f.filter(r); - } - - boolean isExcludedByFilter(Router r, String filterName) { - final ConfigNodeFilter f = filters.get(filterName); - if(f == null || f.isEmpty()) { - return false; - } - return f.filter(r); - } -} diff --git a/orchid/src/com/subgraph/orchid/config/TorConfigBridgeLine.java b/orchid/src/com/subgraph/orchid/config/TorConfigBridgeLine.java deleted file mode 100644 index ef25d31f..00000000 --- a/orchid/src/com/subgraph/orchid/config/TorConfigBridgeLine.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.subgraph.orchid.config; - -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; - -public class TorConfigBridgeLine { - - private final IPv4Address address; - private final int port; - private final HexDigest fingerprint; - - TorConfigBridgeLine(IPv4Address address, int port, HexDigest fingerprint) { - this.address = address; - this.port = port; - this.fingerprint = fingerprint; - } - - public IPv4Address getAddress() { - return address; - } - - public int getPort() { - return port; - } - - public HexDigest getFingerprint() { - return fingerprint; - } -} diff --git a/orchid/src/com/subgraph/orchid/config/TorConfigHSAuth.java b/orchid/src/com/subgraph/orchid/config/TorConfigHSAuth.java deleted file mode 100644 index 612c527c..00000000 --- a/orchid/src/com/subgraph/orchid/config/TorConfigHSAuth.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.subgraph.orchid.config; - -import java.util.HashMap; -import java.util.Map; - -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.circuits.hs.HSDescriptorCookie; -import com.subgraph.orchid.circuits.hs.HSDescriptorCookie.CookieType; -import com.subgraph.orchid.data.Base32; -import com.subgraph.orchid.encoders.Base64; - -public class TorConfigHSAuth { - - private final Map map = new HashMap(); - - void add(String key, String b64Value) { - final HSDescriptorCookie cookie = createFromBase64(b64Value); - final String k = validateKey(key); - map.put(k, cookie); - } - - private String validateKey(String key) { - final String k = (key.endsWith(".onion")) ? key.substring(0, (key.length() - 6)) : key; - try { - byte[] decoded = Base32.base32Decode(k); - if(decoded.length != 10) { - throw new IllegalArgumentException(); - } - return k; - } catch (TorException e) { - throw new IllegalArgumentException(e.getMessage()); - } - } - - HSDescriptorCookie get(String key) { - return map.get(validateKey(key)); - } - - private HSDescriptorCookie createFromBase64(String b64) { - if(b64.length() != 22) { - throw new IllegalArgumentException(); - } - final byte[] decoded = Base64.decode(b64 + "A="); - final byte lastByte = decoded[decoded.length - 1]; - final int flag = (lastByte & 0xFF) >> 4; - final byte[] cookie = new byte[decoded.length - 1]; - System.arraycopy(decoded, 0, cookie, 0, cookie.length); - switch(flag) { - case 0: - return new HSDescriptorCookie(CookieType.COOKIE_BASIC, cookie); - case 1: - return new HSDescriptorCookie(CookieType.COOKIE_STEALTH, cookie); - default: - throw new TorException("Illegal cookie descriptor with flag value: "+ flag); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/config/TorConfigInterval.java b/orchid/src/com/subgraph/orchid/config/TorConfigInterval.java deleted file mode 100644 index ecb3b89e..00000000 --- a/orchid/src/com/subgraph/orchid/config/TorConfigInterval.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.subgraph.orchid.config; - -import java.util.concurrent.TimeUnit; - -public class TorConfigInterval { - - public static TorConfigInterval createFrom(String s) { - final String[] ss = s.split(" "); - final long n = Long.parseLong(ss[0]); - if(ss.length == 1) { - return new TorConfigInterval(n, TimeUnit.SECONDS); - } else { - return createForValueAndUnit(n, ss[1]); - } - } - - private static TorConfigInterval createForValueAndUnit(long value, String unitName) { - if(stringMatchesUnit(unitName, "week")) { - return new TorConfigInterval(value * 7, TimeUnit.DAYS); - } else { - final TimeUnit unit = stringToUnit(unitName); - return new TorConfigInterval(value, unit); - } - } - - private static TimeUnit stringToUnit(String s) { - if(stringMatchesUnit(s, "day")) { - return TimeUnit.DAYS; - } else if(stringMatchesUnit(s, "hour")) { - return TimeUnit.HOURS; - } else if(stringMatchesUnit(s, "minute")) { - return TimeUnit.MINUTES; - } else if(stringMatchesUnit(s, "second")) { - return TimeUnit.SECONDS; - } else if(stringMatchesUnit(s, "millisecond")) { - return TimeUnit.MILLISECONDS; - } else { - throw new IllegalArgumentException(); - } - } - - private static boolean stringMatchesUnit(String s, String unitType) { - if(s == null) { - return false; - } else { - return s.equalsIgnoreCase(unitType) || s.equalsIgnoreCase(unitType + "s"); - } - } - - private final TimeUnit timeUnit; - private final long value; - - - public TorConfigInterval(long value, TimeUnit timeUnit) { - this.timeUnit = getTimeUnitFor(value, timeUnit); - this.value = getValueFor(value, timeUnit); - - } - - public long getMilliseconds() { - return TimeUnit.MILLISECONDS.convert(value, timeUnit); - } - - private static TimeUnit getTimeUnitFor(long value, TimeUnit timeUnit) { - if(timeUnit == TimeUnit.NANOSECONDS || timeUnit == TimeUnit.MICROSECONDS) { - return TimeUnit.MILLISECONDS; - } else { - return timeUnit; - } - } - - private static long getValueFor(long value, TimeUnit timeUnit) { - if(timeUnit == TimeUnit.NANOSECONDS || timeUnit == TimeUnit.MICROSECONDS) { - return TimeUnit.MILLISECONDS.convert(value, timeUnit); - } else { - return value; - } - } - - public String toString() { - if(timeUnit == TimeUnit.DAYS && (value % 7 == 0)) { - final long weeks = value / 7; - return (weeks == 1) ? "1 week" : (weeks + " weeks"); - } - final StringBuilder sb = new StringBuilder(); - sb.append(value); - sb.append(" "); - sb.append(unitToString(timeUnit)); - if(value != 1) { - sb.append("s"); - } - return sb.toString(); - } - - private static String unitToString(TimeUnit unit) { - switch(unit) { - case MILLISECONDS: - return "millisecond"; - case SECONDS: - return "second"; - case MINUTES: - return "minute"; - case HOURS: - return "hour"; - case DAYS: - return "days"; - default: - throw new IllegalArgumentException(); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/config/TorConfigParser.java b/orchid/src/com/subgraph/orchid/config/TorConfigParser.java deleted file mode 100644 index c44a066f..00000000 --- a/orchid/src/com/subgraph/orchid/config/TorConfigParser.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.subgraph.orchid.config; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.TorConfig.AutoBoolValue; -import com.subgraph.orchid.TorConfig.ConfigVarType; - -public class TorConfigParser { - - public Object parseValue(String value, ConfigVarType type) { - switch(type) { - case BOOLEAN: - return Boolean.parseBoolean(value); - case INTEGER: - return Integer.parseInt(value); - case INTERVAL: - return parseIntervalValue(value); - case PATH: - return parseFileValue(value); - case PORTLIST: - return parseIntegerList(value); - case STRING: - return value; - case STRINGLIST: - return parseCSV(value); - case AUTOBOOL: - return parseAutoBool(value); - case HS_AUTH: - default: - throw new IllegalArgumentException(); - } - } - - private File parseFileValue(String value) { - if(value.startsWith("~/")) { - final File home = new File(System.getProperty("user.home")); - return new File(home, value.substring(2)); - } - return new File(value); - } - private TorConfigInterval parseIntervalValue(String value) { - return TorConfigInterval.createFrom(value); - } - - private List parseIntegerList(String value) { - final List list = new ArrayList(); - for(String s: value.split(",")) { - list.add(Integer.parseInt(s)); - } - return list; - } - - private List parseCSV(String value) { - final List list = new ArrayList(); - for(String s: value.split(",")) { - list.add(s); - } - return list; - } - - private TorConfig.AutoBoolValue parseAutoBool(String value) { - if("auto".equalsIgnoreCase(value)) { - return AutoBoolValue.AUTO; - } else if("true".equalsIgnoreCase(value)) { - return AutoBoolValue.TRUE; - } else if("false".equalsIgnoreCase(value)) { - return AutoBoolValue.FALSE; - } else { - throw new IllegalArgumentException("Could not parse AutoBool value "+ value); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/config/TorConfigProxy.java b/orchid/src/com/subgraph/orchid/config/TorConfigProxy.java deleted file mode 100644 index 9e732293..00000000 --- a/orchid/src/com/subgraph/orchid/config/TorConfigProxy.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.subgraph.orchid.config; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.TorConfig.ConfigVar; -import com.subgraph.orchid.TorConfig.ConfigVarType; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; - -public class TorConfigProxy implements InvocationHandler { - - private final Map configValues; - private final List bridges; - private final TorConfigParser parser; - - public TorConfigProxy() { - this.configValues = new HashMap(); - this.bridges = new ArrayList(); - this.configValues.put("Bridges", bridges); - this.parser = new TorConfigParser(); - } - - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if(method.getName().startsWith("set")) { - invokeSetMethod(method, args); - return null; - } else if(method.getName().startsWith("get")) { - if(args == null) { - return invokeGetMethod(method); - } else { - return invokeGetMethodWithArgs(method, args); - } - } else if(method.getName().startsWith("add")) { - invokeAddMethod(method, args); - return null; - } else { - throw new IllegalArgumentException(); - } - } - - void invokeSetMethod(Method method, Object[] args) { - final String name = getVariableNameForMethod(method); - final ConfigVar annotation = getAnnotationForVariable(name); - if(annotation != null && annotation.type() == ConfigVarType.INTERVAL) { - setIntervalValue(name, args); - } else { - configValues.put(name, args[0]); - } - } - - private void setIntervalValue(String varName, Object[] args) { - if(!(args[0] instanceof Long && args[1] instanceof TimeUnit)) { - throw new IllegalArgumentException(); - } - final long time = (Long) args[0]; - final TimeUnit unit = (TimeUnit) args[1]; - final TorConfigInterval interval = new TorConfigInterval(time, unit); - configValues.put(varName, interval); - } - - - private Object invokeGetMethodWithArgs(Method method, Object[] args) { - final String varName = getVariableNameForMethod(method); - if(getVariableType(varName) == ConfigVarType.HS_AUTH) { - return invokeHSAuthGet(varName, args); - } else { - throw new IllegalArgumentException(); - } - } - - private Object invokeGetMethod(Method method) { - final String varName = getVariableNameForMethod(method); - final Object value = getVariableValue(varName); - - if(value instanceof TorConfigInterval) { - final TorConfigInterval interval = (TorConfigInterval) value; - return interval.getMilliseconds(); - } else { - return value; - } - } - - private Object invokeHSAuthGet(String varName, Object[] args) { - if(!(args[0] instanceof String)) { - throw new IllegalArgumentException(); - } - final TorConfigHSAuth hsAuth = getHSAuth(varName); - return hsAuth.get((String) args[0]); - } - - private void invokeAddMethod(Method method, Object[] args) { - final String name = getVariableNameForMethod(method); - final ConfigVarType type = getVariableType(name); - switch(type) { - case HS_AUTH: - invokeHSAuthAdd(name, args); - break; - - case BRIDGE_LINE: - invokeBridgeAdd(args); - break; - - default: - throw new UnsupportedOperationException("addX configuration methods only supported for HS_AUTH or BRIDGE_LINE type"); - } - } - - private void invokeBridgeAdd(Object[] args) { - if(args.length >= 2 && (args[0] instanceof IPv4Address) && (args[1] instanceof Integer)) { - if(args.length == 2) { - bridges.add(new TorConfigBridgeLine((IPv4Address)args[0], (Integer)args[1], null)); - return; - } else if(args.length == 3 && (args[2] instanceof HexDigest)) { - bridges.add(new TorConfigBridgeLine((IPv4Address) args[0], (Integer) args[1], (HexDigest) args[2])); - return; - } - } - throw new IllegalArgumentException(); - } - - private void invokeHSAuthAdd(String name, Object[] args) { - if(!(args.length == 2 && (args[0] instanceof String) && (args[1] instanceof String))) { - throw new IllegalArgumentException(); - } - final TorConfigHSAuth hsAuth = getHSAuth(name); - hsAuth.add((String)args[0], (String)args[1]); - } - - private TorConfigHSAuth getHSAuth(String keyName) { - if(!configValues.containsKey(keyName)) { - configValues.put(keyName, new TorConfigHSAuth()); - } - return (TorConfigHSAuth) configValues.get(keyName); - } - - private Object getVariableValue(String varName) { - if(configValues.containsKey(varName)) { - return configValues.get(varName); - } else { - return getDefaultVariableValue(varName); - } - } - - private Object getDefaultVariableValue(String varName) { - final String defaultValue = getDefaultValueString(varName); - final ConfigVarType type = getVariableType(varName); - if(defaultValue == null || type == null) { - return null; - } - return parser.parseValue(defaultValue, type); - } - - private String getDefaultValueString(String varName) { - final ConfigVar var = getAnnotationForVariable(varName); - if(var == null) { - return null; - } else { - return var.defaultValue(); - } - } - - private ConfigVarType getVariableType(String varName) { - if("Bridge".equals(varName)) { - return ConfigVarType.BRIDGE_LINE; - } - - final ConfigVar var = getAnnotationForVariable(varName); - if(var == null) { - return null; - } else { - return var.type(); - } - } - - private String getVariableNameForMethod(Method method) { - final String methodName = method.getName(); - if(methodName.startsWith("get") || methodName.startsWith("set") || methodName.startsWith("add")) { - return methodName.substring(3); - } - throw new IllegalArgumentException(); - } - - private ConfigVar getAnnotationForVariable(String varName) { - final String getName = "get"+ varName; - for(Method m: TorConfig.class.getDeclaredMethods()) { - if(getName.equals(m.getName())) { - return m.getAnnotation(TorConfig.ConfigVar.class); - } - } - return null; - } -} diff --git a/orchid/src/com/subgraph/orchid/connections/ConnectionCacheImpl.java b/orchid/src/com/subgraph/orchid/connections/ConnectionCacheImpl.java deleted file mode 100644 index 5f568351..00000000 --- a/orchid/src/com/subgraph/orchid/connections/ConnectionCacheImpl.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.subgraph.orchid.connections; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - -import javax.net.ssl.SSLSocket; - -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.TorConfig; -import com.subgraph.orchid.circuits.TorInitializationTracker; -import com.subgraph.orchid.dashboard.DashboardRenderable; -import com.subgraph.orchid.dashboard.DashboardRenderer; - -public class ConnectionCacheImpl implements ConnectionCache, DashboardRenderable { - private final static Logger logger = Logger.getLogger(ConnectionCacheImpl.class.getName()); - - private class ConnectionTask implements Callable { - - private final Router router; - private final boolean isDirectoryConnection; - - ConnectionTask(Router router, boolean isDirectoryConnection) { - this.router = router; - this.isDirectoryConnection = isDirectoryConnection; - } - - public ConnectionImpl call() throws Exception { - final SSLSocket socket = factory.createSocket(); - final ConnectionImpl conn = new ConnectionImpl(config, socket, router, initializationTracker, isDirectoryConnection); - conn.connect(); - return conn; - } - } - - private class CloseIdleConnectionCheckTask implements Runnable { - public void run() { - for(Future f: activeConnections.values()) { - if(f.isDone()) { - try { - final ConnectionImpl c = f.get(); - c.idleCloseCheck(); - } catch (Exception e) { } - } - } - } - } - - private final ConcurrentMap> activeConnections = new ConcurrentHashMap>(); - private final ConnectionSocketFactory factory = new ConnectionSocketFactory(); - private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); - - private final TorConfig config; - private final TorInitializationTracker initializationTracker; - private volatile boolean isClosed; - - - public ConnectionCacheImpl(TorConfig config, TorInitializationTracker tracker) { - this.config = config; - this.initializationTracker = tracker; - scheduledExecutor.scheduleAtFixedRate(new CloseIdleConnectionCheckTask(), 5000, 5000, TimeUnit.MILLISECONDS); - } - - public void close() { - if(isClosed) { - return; - } - isClosed = true; - for(Future f: activeConnections.values()) { - if(f.isDone()) { - try { - ConnectionImpl conn = f.get(); - conn.closeSocket(); - } catch (InterruptedException e) { - logger.warning("Unexpected interruption while closing connection"); - } catch (ExecutionException e) { - logger.warning("Exception closing connection: "+ e.getCause()); - } - } else { - // FIXME this doesn't close the socket, so the connection task lingers - // A proper fix would require maintaining pending connections in a separate - // collection. - f.cancel(true); - } - } - activeConnections.clear(); - scheduledExecutor.shutdownNow(); - } - - @Override - public boolean isClosed() { - return isClosed; - } - - public Connection getConnectionTo(Router router, boolean isDirectoryConnection) throws InterruptedException, ConnectionTimeoutException, ConnectionFailedException, ConnectionHandshakeException { - if(isClosed) { - throw new IllegalStateException("ConnectionCache has been closed"); - } - logger.fine("Get connection to "+ router.getAddress() + " "+ router.getOnionPort() + " " + router.getNickname()); - while(true) { - Future f = getFutureFor(router, isDirectoryConnection); - try { - Connection c = f.get(); - if(c.isClosed()) { - activeConnections.remove(router, f); - } else { - return c; - } - } catch (CancellationException e) { - activeConnections.remove(router, f); - } catch (ExecutionException e) { - activeConnections.remove(router, f); - final Throwable t = e.getCause(); - if(t instanceof ConnectionTimeoutException) { - throw (ConnectionTimeoutException) t; - } else if(t instanceof ConnectionFailedException) { - throw (ConnectionFailedException) t; - } else if(t instanceof ConnectionHandshakeException) { - throw (ConnectionHandshakeException) t; - } - throw new RuntimeException("Unexpected exception: "+ e, e); - } - } - } - - private Future getFutureFor(Router router, boolean isDirectoryConnection) { - Future f = activeConnections.get(router); - if(f != null) { - return f; - } - return createFutureForIfAbsent(router, isDirectoryConnection); - } - - private Future createFutureForIfAbsent(Router router, boolean isDirectoryConnection) { - final Callable task = new ConnectionTask(router, isDirectoryConnection); - final FutureTask futureTask = new FutureTask(task); - - final Future f = activeConnections.putIfAbsent(router, futureTask); - if(f != null) { - return f; - } - - futureTask.run(); - return futureTask; - } - - public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException { - if((flags & DASHBOARD_CONNECTIONS) == 0) { - return; - } - printDashboardBanner(writer, flags); - for(Connection c: getActiveConnections()) { - if(!c.isClosed()) { - renderer.renderComponent(writer, flags, c); - } - } - writer.println(); - } - - private void printDashboardBanner(PrintWriter writer, int flags) { - final boolean verbose = (flags & DASHBOARD_CONNECTIONS_VERBOSE) != 0; - if(verbose) { - writer.println("[Connection Cache (verbose)]"); - } else { - writer.println("[Connection Cache]"); - } - writer.println(); - } - - List getActiveConnections() { - final List cs = new ArrayList(); - for(Future future: activeConnections.values()) { - addConnectionFromFuture(future, cs); - } - return cs; - } - - private void addConnectionFromFuture(Future future, List connectionList) { - try { - if(future.isDone() && !future.isCancelled()) { - connectionList.add(future.get()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { } - } -} diff --git a/orchid/src/com/subgraph/orchid/connections/ConnectionHandshake.java b/orchid/src/com/subgraph/orchid/connections/ConnectionHandshake.java deleted file mode 100644 index 052bf8f2..00000000 --- a/orchid/src/com/subgraph/orchid/connections/ConnectionHandshake.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.subgraph.orchid.connections; - -import java.io.IOException; -import java.security.PublicKey; -import java.security.interfaces.RSAPublicKey; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.logging.Logger; - -import javax.net.ssl.SSLSocket; - -import com.subgraph.orchid.BridgeRouter; -import com.subgraph.orchid.Cell; -import com.subgraph.orchid.ConnectionHandshakeException; -import com.subgraph.orchid.ConnectionIOException; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.circuits.cells.CellImpl; -import com.subgraph.orchid.crypto.TorPublicKey; -import com.subgraph.orchid.data.IPv4Address; - -public abstract class ConnectionHandshake { - private final static Logger logger = Logger.getLogger(ConnectionHandshake.class.getName()); - - static ConnectionHandshake createHandshake(TorConfig config, ConnectionImpl connection, SSLSocket socket) throws ConnectionHandshakeException { - if(config.getHandshakeV3Enabled() && ConnectionHandshakeV3.sessionSupportsHandshake(socket.getSession())) { - return new ConnectionHandshakeV3(connection, socket); - } else if(config.getHandshakeV2Enabled()) { - return new ConnectionHandshakeV2(connection, socket); - } else { - throw new ConnectionHandshakeException("No valid handshake type available for this connection"); - } - - } - - protected final ConnectionImpl connection; - protected final SSLSocket socket; - - protected final List remoteVersions; - private int remoteTimestamp; - private IPv4Address myAddress; - private final List remoteAddresses; - - ConnectionHandshake(ConnectionImpl connection, SSLSocket socket) { - this.connection = connection; - this.socket = socket; - this.remoteVersions = new ArrayList(); - this.remoteAddresses = new ArrayList(); - } - - abstract void runHandshake() throws IOException, InterruptedException, ConnectionIOException; - - int getRemoteTimestamp() { - return remoteTimestamp; - } - - IPv4Address getMyAddress() { - return myAddress; - } - - protected Cell expectCell(Integer... expectedTypes) throws ConnectionHandshakeException { - try { - final Cell c = connection.readConnectionControlCell(); - for(int t: expectedTypes) { - if(c.getCommand() == t) { - return c; - } - } - final List expected = Arrays.asList(expectedTypes); - throw new ConnectionHandshakeException("Expecting Cell command "+ expected + " and got [ "+ c.getCommand() +" ] instead"); - } catch (ConnectionIOException e) { - throw new ConnectionHandshakeException("Connection exception while performing handshake "+ e); - } - } - - protected void sendVersions(int... versions) throws ConnectionIOException { - final Cell cell = CellImpl.createVarCell(0, Cell.VERSIONS, versions.length * 2); - for(int v: versions) { - cell.putShort(v); - } - connection.sendCell(cell); - } - - protected void receiveVersions() throws ConnectionHandshakeException { - final Cell c = expectCell(Cell.VERSIONS); - while(c.cellBytesRemaining() >= 2) { - remoteVersions.add(c.getShort()); - } - } - - protected void sendNetinfo() throws ConnectionIOException { - final Cell cell = CellImpl.createCell(0, Cell.NETINFO); - putTimestamp(cell); - putIPv4Address(cell, connection.getRouter().getAddress()); - putMyAddresses(cell); - connection.sendCell(cell); - } - - private void putTimestamp(Cell cell) { - final Date now = new Date(); - cell.putInt((int) (now.getTime() / 1000)); - } - - private void putIPv4Address(Cell cell, IPv4Address address) { - final byte[] data = address.getAddressDataBytes(); - cell.putByte(Cell.ADDRESS_TYPE_IPV4); - cell.putByte(data.length); - cell.putByteArray(data); - } - - private void putMyAddresses(Cell cell) { - cell.putByte(1); - putIPv4Address(cell, new IPv4Address(0)); - } - - protected void recvNetinfo() throws ConnectionHandshakeException { - processNetInfo(expectCell(Cell.NETINFO)); - } - - protected void processNetInfo(Cell netinfoCell) { - remoteTimestamp = netinfoCell.getInt(); - myAddress = readAddress(netinfoCell); - final int addressCount = netinfoCell.getByte(); - for(int i = 0; i < addressCount; i++) { - IPv4Address addr = readAddress(netinfoCell); - if(addr != null) { - remoteAddresses.add(addr); - } - } - } - - private IPv4Address readAddress(Cell cell) { - final int type = cell.getByte(); - final int len = cell.getByte(); - if(type == Cell.ADDRESS_TYPE_IPV4 && len == 4) { - return new IPv4Address(cell.getInt()); - } - final byte[] buffer = new byte[len]; - cell.getByteArray(buffer); - return null; - } - - protected void verifyIdentityKey(PublicKey publicKey) throws ConnectionHandshakeException { - if(!(publicKey instanceof RSAPublicKey)) { - throw new ConnectionHandshakeException("Identity certificate public key is not an RSA key as expected"); - } - final TorPublicKey identityKey = new TorPublicKey((RSAPublicKey)publicKey); - final Router router = connection.getRouter(); - if((router instanceof BridgeRouter) && (router.getIdentityHash() == null)) { - logger.info("Setting Bridge fingerprint from connection handshake for "+ router); - ((BridgeRouter) router).setIdentity(identityKey.getFingerprint()); - } else if(!identityKey.getFingerprint().equals(router.getIdentityHash())) { - throw new ConnectionHandshakeException("Router identity does not match certificate key"); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/connections/ConnectionHandshakeV2.java b/orchid/src/com/subgraph/orchid/connections/ConnectionHandshakeV2.java deleted file mode 100644 index 130a287b..00000000 --- a/orchid/src/com/subgraph/orchid/connections/ConnectionHandshakeV2.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.subgraph.orchid.connections; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.PublicKey; - -import javax.net.ssl.HandshakeCompletedEvent; -import javax.net.ssl.HandshakeCompletedListener; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; -import javax.security.cert.CertificateException; -import javax.security.cert.X509Certificate; - -import com.subgraph.orchid.ConnectionHandshakeException; -import com.subgraph.orchid.ConnectionIOException; - -/** - * This class performs a Version 2 handshake as described in section 2 of - * tor-spec.txt. The handshake is considered complete after VERSIONS and - * NETINFO cells have been exchanged between the two sides. - */ -public class ConnectionHandshakeV2 extends ConnectionHandshake { - - private static class HandshakeFinishedMonitor implements HandshakeCompletedListener { - final Object lock = new Object(); - boolean isFinished; - - public void handshakeCompleted(HandshakeCompletedEvent event) { - synchronized(lock) { - this.isFinished = true; - lock.notifyAll(); - } - } - - public void waitFinished() throws InterruptedException { - synchronized(lock) { - while(!isFinished) { - lock.wait(); - } - } - } - } - - ConnectionHandshakeV2(ConnectionImpl connection, SSLSocket socket) { - super(connection, socket); - } - - void runHandshake() throws IOException, InterruptedException, ConnectionIOException { - // Swap in V1-only ciphers for second handshake as a workaround for: - // - // https://trac.torproject.org/projects/tor/ticket/4591 - // - socket.setEnabledCipherSuites(ConnectionSocketFactory.V1_CIPHERS_ONLY); - - final HandshakeFinishedMonitor monitor = new HandshakeFinishedMonitor(); - socket.addHandshakeCompletedListener(monitor); - socket.startHandshake(); - monitor.waitFinished(); - socket.removeHandshakeCompletedListener(monitor); - - verifyIdentityKey(getIdentityKey()); - sendVersions(2); - receiveVersions(); - sendNetinfo(); - recvNetinfo(); - } - - private PublicKey getIdentityKey() throws ConnectionHandshakeException { - final X509Certificate identityCertificate = getIdentityCertificateFromSession(socket.getSession()); - return identityCertificate.getPublicKey(); - } - - private X509Certificate getIdentityCertificateFromSession(SSLSession session) throws ConnectionHandshakeException { - try { - X509Certificate[] chain = session.getPeerCertificateChain(); - if(chain.length != 2) { - throw new ConnectionHandshakeException("Expecting 2 certificate chain from router and received chain length "+ chain.length); - } - chain[0].verify(chain[1].getPublicKey()); - return chain[1]; - } catch (SSLPeerUnverifiedException e) { - throw new ConnectionHandshakeException("No certificates received from router"); - } catch (GeneralSecurityException e) { - throw new ConnectionHandshakeException("Incorrect signature on certificate chain"); - } catch (CertificateException e) { - throw new ConnectionHandshakeException("Malformed certificate received"); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/connections/ConnectionHandshakeV3.java b/orchid/src/com/subgraph/orchid/connections/ConnectionHandshakeV3.java deleted file mode 100644 index ca2ff75b..00000000 --- a/orchid/src/com/subgraph/orchid/connections/ConnectionHandshakeV3.java +++ /dev/null @@ -1,195 +0,0 @@ -package com.subgraph.orchid.connections; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.Principal; -import java.security.PublicKey; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.security.interfaces.RSAPublicKey; - -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; - -import com.subgraph.orchid.Cell; -import com.subgraph.orchid.ConnectionHandshakeException; -import com.subgraph.orchid.ConnectionIOException; - -public class ConnectionHandshakeV3 extends ConnectionHandshake { - - private X509Certificate linkCertificate; - private X509Certificate identityCertificate; - - ConnectionHandshakeV3(ConnectionImpl connection, SSLSocket socket) { - super(connection, socket); - } - - void runHandshake() throws IOException, InterruptedException, ConnectionIOException { - sendVersions(3); - receiveVersions(); - recvCerts(); - recvAuthChallengeAndNetinfo(); - verifyCertificates(); - sendNetinfo(); - } - - void recvCerts() throws ConnectionHandshakeException { - final Cell cell = expectCell(Cell.CERTS); - final int ncerts = cell.getByte(); - if(ncerts != 2) { - throw new ConnectionHandshakeException("Expecting 2 certificates and got "+ ncerts); - } - - linkCertificate = null; - identityCertificate = null; - - for(int i = 0; i < ncerts; i++) { - int type = cell.getByte(); - if(type == 1) { - linkCertificate = testAndReadCertificate(cell, linkCertificate, "Link (type = 1)"); - } else if(type == 2) { - identityCertificate = testAndReadCertificate(cell, identityCertificate, "Identity (type = 2)"); - } else { - throw new ConnectionHandshakeException("Unexpected certificate type = "+ type + " in CERTS cell"); - } - } - - } - - RSAPublicKey getConnectionPublicKey() { - try { - javax.security.cert.X509Certificate[] chain = socket.getSession().getPeerCertificateChain(); - return (RSAPublicKey) chain[0].getPublicKey(); - } catch (SSLPeerUnverifiedException e) { - return null; - } - } - - - private X509Certificate testAndReadCertificate(Cell cell, X509Certificate currentValue, String type) throws ConnectionHandshakeException { - if(currentValue == null) { - return readCertificateFromCell(cell); - } else { - throw new ConnectionHandshakeException("Duplicate "+ type + " certificates in CERTS cell"); - } - } - - private X509Certificate readCertificateFromCell(Cell cell) { - try { - final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - final int clen = cell.getShort(); - final byte[] certificateBuffer = new byte[clen]; - cell.getByteArray(certificateBuffer); - final ByteArrayInputStream bis = new ByteArrayInputStream(certificateBuffer); - return (X509Certificate) certificateFactory.generateCertificate(bis); - } catch (CertificateException e) { - return null; - } - - } - - void verifyCertificates() throws ConnectionHandshakeException { - PublicKey publicKey = identityCertificate.getPublicKey(); - verifyIdentityKey(publicKey); - RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey; - - if(rsaPublicKey.getModulus().bitLength() != 1024) { - throw new ConnectionHandshakeException("Invalid RSA modulus length in router identity key"); - } - - try { - identityCertificate.checkValidity(); - identityCertificate.verify(rsaPublicKey); - linkCertificate.checkValidity(); - linkCertificate.verify(rsaPublicKey); - } catch (GeneralSecurityException e) { - throw new ConnectionHandshakeException("Router presented invalid certificate chain in CERTS cell"); - } - - RSAPublicKey rsa2 = (RSAPublicKey) linkCertificate.getPublicKey(); - if(!getConnectionPublicKey().getModulus().equals(rsa2.getModulus())) { - throw new ConnectionHandshakeException("Link certificate in CERTS cell does not match connection certificate"); - } - } - - void recvAuthChallengeAndNetinfo() throws ConnectionHandshakeException { - final Cell cell = expectCell(Cell.AUTH_CHALLENGE, Cell.NETINFO); - if(cell.getCommand() == Cell.NETINFO) { - processNetInfo(cell); - return; - } - final Cell netinfo = expectCell(Cell.NETINFO); - processNetInfo(netinfo); - } - - public static boolean sessionSupportsHandshake(SSLSession session) { - javax.security.cert.X509Certificate cert = getConnectionCertificateFromSession(session); - if(cert == null) { - return false; - } - return isSelfSigned(cert) || testDName(cert.getSubjectDN()) || - testDName(cert.getIssuerDN()) || testModulusLength(cert); - } - - static private javax.security.cert.X509Certificate getConnectionCertificateFromSession(SSLSession session) { - try { - final javax.security.cert.X509Certificate[] chain = session.getPeerCertificateChain(); - return chain[0]; - } catch (SSLPeerUnverifiedException e) { - return null; - } - } - - static private boolean isSelfSigned(javax.security.cert.X509Certificate certificate) { - try { - certificate.verify(certificate.getPublicKey()); - return true; - } catch (Exception e) { - return false; - } - } - - /* - * * Some component other than "commonName" is set in the subject or - * issuer DN of the certificate. - * - * * The commonName of the subject or issuer of the certificate ends - * with a suffix other than ".net". - */ - static private boolean testDName(Principal dn) { - final String dname = dn.getName(); - if(dname.indexOf(",") >= 0) { - return true; - } - return !getCN(dname).endsWith(".net"); - } - - /* - * * The certificate's public key modulus is longer than 1024 bits. - */ - static private boolean testModulusLength(javax.security.cert.X509Certificate cert) { - if(!(cert.getPublicKey() instanceof RSAPublicKey)) { - return false; - } - final RSAPublicKey rsaPublicKey = (RSAPublicKey) cert.getPublicKey(); - final BigInteger modulus = rsaPublicKey.getModulus(); - return modulus.bitLength() > 1024; - } - - static private String getCN(String dname) { - final int idx = dname.indexOf("CN="); - if(idx == -1) { - return ""; - } - final int comma = dname.indexOf(',', idx); - if(comma == -1) { - return dname.substring(idx); - } else { - return dname.substring(idx, comma); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/connections/ConnectionImpl.java b/orchid/src/com/subgraph/orchid/connections/ConnectionImpl.java deleted file mode 100644 index ff3513ac..00000000 --- a/orchid/src/com/subgraph/orchid/connections/ConnectionImpl.java +++ /dev/null @@ -1,374 +0,0 @@ -package com.subgraph.orchid.connections; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.SocketTimeoutException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantLock; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.net.ssl.SSLSocket; - -import com.subgraph.orchid.Cell; -import com.subgraph.orchid.Circuit; -import com.subgraph.orchid.Connection; -import com.subgraph.orchid.ConnectionFailedException; -import com.subgraph.orchid.ConnectionHandshakeException; -import com.subgraph.orchid.ConnectionIOException; -import com.subgraph.orchid.ConnectionTimeoutException; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.Threading; -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.circuits.TorInitializationTracker; -import com.subgraph.orchid.circuits.cells.CellImpl; -import com.subgraph.orchid.crypto.TorRandom; -import com.subgraph.orchid.dashboard.DashboardRenderable; -import com.subgraph.orchid.dashboard.DashboardRenderer; - -/** - * This class represents a transport link between two onion routers or - * between an onion proxy and an entry router. - * - */ -public class ConnectionImpl implements Connection, DashboardRenderable { - private final static Logger logger = Logger.getLogger(ConnectionImpl.class.getName()); - private final static int CONNECTION_IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes - private final static int DEFAULT_CONNECT_TIMEOUT = 5000; - private final static Cell connectionClosedSentinel = CellImpl.createCell(0, 0); - - private final TorConfig config; - private final SSLSocket socket; - private InputStream input; - private OutputStream output; - private final Router router; - private final Map circuitMap; - private final BlockingQueue connectionControlCells; - private final TorInitializationTracker initializationTracker; - private final boolean isDirectoryConnection; - - private int currentId = 1; - private boolean isConnected; - private volatile boolean isClosed; - private final Thread readCellsThread; - private final ReentrantLock connectLock = Threading.lock("connect"); - private final ReentrantLock circuitsLock = Threading.lock("circuits"); - private final ReentrantLock outputLock = Threading.lock("output"); - private final AtomicLong lastActivity = new AtomicLong(); - - - public ConnectionImpl(TorConfig config, SSLSocket socket, Router router, TorInitializationTracker tracker, boolean isDirectoryConnection) { - this.config = config; - this.socket = socket; - this.router = router; - this.circuitMap = new HashMap(); - this.readCellsThread = new Thread(createReadCellsRunnable()); - this.readCellsThread.setDaemon(true); - this.connectionControlCells = new LinkedBlockingQueue(); - this.initializationTracker = tracker; - this.isDirectoryConnection = isDirectoryConnection; - initializeCurrentCircuitId(); - } - - private void initializeCurrentCircuitId() { - final TorRandom random = new TorRandom(); - currentId = random.nextInt(0xFFFF) + 1; - } - - public Router getRouter() { - return router; - } - - public boolean isClosed() { - return isClosed; - } - - public int bindCircuit(Circuit circuit) { - circuitsLock.lock(); - try { - while(circuitMap.containsKey(currentId)) - incrementNextId(); - final int id = currentId; - incrementNextId(); - circuitMap.put(id, circuit); - return id; - } finally { - circuitsLock.unlock(); - } - } - - private void incrementNextId() { - currentId++; - if(currentId > 0xFFFF) - currentId = 1; - } - - void connect() throws ConnectionFailedException, ConnectionTimeoutException, ConnectionHandshakeException { - connectLock.lock(); - try { - if(isConnected) { - return; - } - try { - doConnect(); - } catch (SocketTimeoutException e) { - throw new ConnectionTimeoutException(); - } catch (IOException e) { - throw new ConnectionFailedException(e.getClass().getName() + " : "+ e.getMessage()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new ConnectionHandshakeException("Handshake interrupted"); - } catch (ConnectionHandshakeException e) { - throw e; - } catch (ConnectionIOException e) { - throw new ConnectionFailedException(e.getMessage()); - } - isConnected = true; - } finally { - connectLock.unlock(); - } - } - - private void doConnect() throws IOException, InterruptedException, ConnectionIOException { - connectSocket(); - final ConnectionHandshake handshake = ConnectionHandshake.createHandshake(config, this, socket); - input = socket.getInputStream(); - output = socket.getOutputStream(); - readCellsThread.start(); - handshake.runHandshake(); - updateLastActivity(); - } - - private void connectSocket() throws IOException { - if(initializationTracker != null) { - if(isDirectoryConnection) { - initializationTracker.notifyEvent(Tor.BOOTSTRAP_STATUS_CONN_DIR); - } else { - initializationTracker.notifyEvent(Tor.BOOTSTRAP_STATUS_CONN_OR); - } - } - - socket.connect(routerToSocketAddress(router), DEFAULT_CONNECT_TIMEOUT); - - if(initializationTracker != null) { - if(isDirectoryConnection) { - initializationTracker.notifyEvent(Tor.BOOTSTRAP_STATUS_HANDSHAKE_DIR); - } else { - initializationTracker.notifyEvent(Tor.BOOTSTRAP_STATUS_HANDSHAKE_OR); - } - } - } - - private SocketAddress routerToSocketAddress(Router router) { - final InetAddress address = router.getAddress().toInetAddress(); - return new InetSocketAddress(address, router.getOnionPort()); - } - - public void sendCell(Cell cell) throws ConnectionIOException { - if(!socket.isConnected()) { - throw new ConnectionIOException("Cannot send cell because connection is not connected"); - } - updateLastActivity(); - outputLock.lock(); - try { - try { - output.write(cell.getCellBytes()); - } catch (IOException e) { - logger.fine("IOException writing cell to connection "+ e.getMessage()); - closeSocket(); - throw new ConnectionIOException(e.getClass().getName() + " : "+ e.getMessage()); - } - } finally { - outputLock.unlock(); - } - } - - private Cell recvCell() throws ConnectionIOException { - try { - return CellImpl.readFromInputStream(input); - } catch(EOFException e) { - closeSocket(); - throw new ConnectionIOException(); - } catch (IOException e) { - if(!isClosed) { - logger.fine("IOException reading cell from connection "+ this + " : "+ e.getMessage()); - closeSocket(); - } - throw new ConnectionIOException(e.getClass().getName() + " " + e.getMessage()); - } - } - - void closeSocket() { - try { - logger.fine("Closing connection to "+ this); - isClosed = true; - socket.close(); - isConnected = false; - } catch (IOException e) { - logger.warning("Error closing socket: "+ e.getMessage()); - } - } - - private Runnable createReadCellsRunnable() { - return new Runnable() { - public void run() { - try { - readCellsLoop(); - } catch(Exception e) { - logger.log(Level.WARNING, "Unhandled exception processing incoming cells on connection "+ e, e); - } - } - }; - } - - private void readCellsLoop() { - while(!Thread.interrupted()) { - try { - processCell( recvCell() ); - } catch(ConnectionIOException e) { - connectionControlCells.add(connectionClosedSentinel); - notifyCircuitsLinkClosed(); - return; - } catch(TorException e) { - logger.log(Level.WARNING, "Unhandled Tor exception reading and processing cells: "+ e.getMessage(), e); - } - } - } - - private void notifyCircuitsLinkClosed() { - - } - - Cell readConnectionControlCell() throws ConnectionIOException { - try { - return connectionControlCells.take(); - } catch (InterruptedException e) { - closeSocket(); - throw new ConnectionIOException(); - } - } - - private void processCell(Cell cell) { - updateLastActivity(); - final int command = cell.getCommand(); - - if(command == Cell.RELAY) { - processRelayCell(cell); - return; - } - - switch(command) { - case Cell.NETINFO: - case Cell.VERSIONS: - case Cell.CERTS: - case Cell.AUTH_CHALLENGE: - connectionControlCells.add(cell); - break; - - case Cell.CREATED: - case Cell.CREATED_FAST: - case Cell.DESTROY: - processControlCell(cell); - break; - default: - // Ignore everything else - break; - } - } - - private void processRelayCell(Cell cell) { - Circuit circuit; - circuitsLock.lock(); - try { - circuit = circuitMap.get(cell.getCircuitId()); - if(circuit == null) { - logger.warning("Could not deliver relay cell for circuit id = "+ cell.getCircuitId() +" on connection "+ this +". Circuit not found"); - return; - } - } finally { - circuitsLock.unlock(); - } - - circuit.deliverRelayCell(cell); - } - - private void processControlCell(Cell cell) { - Circuit circuit; - circuitsLock.lock(); - try { - circuit = circuitMap.get(cell.getCircuitId()); - } finally { - circuitsLock.unlock(); - } - - if(circuit != null) { - circuit.deliverControlCell(cell); - } - } - - void idleCloseCheck() { - circuitsLock.lock(); - try { - final boolean needClose = (!isClosed && circuitMap.isEmpty() && getIdleMilliseconds() > CONNECTION_IDLE_TIMEOUT); - if(needClose) { - logger.fine("Closing connection to "+ this +" on idle timeout"); - closeSocket(); - } - } finally { - circuitsLock.unlock(); - } - } - - private void updateLastActivity() { - lastActivity.set(System.currentTimeMillis()); - } - - private long getIdleMilliseconds() { - if(lastActivity.get() == 0) { - return 0; - } - return System.currentTimeMillis() - lastActivity.get(); - } - - public void removeCircuit(Circuit circuit) { - circuitsLock.lock(); - try { - circuitMap.remove(circuit.getCircuitId()); - } finally { - circuitsLock.unlock(); - } - } - - public String toString() { - return "!" + router.getNickname() + "!"; - } - - public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException { - final int circuitCount; - circuitsLock.lock(); - try { - circuitCount = circuitMap.size(); - } finally { - circuitsLock.unlock(); - } - if(circuitCount == 0 && (flags & DASHBOARD_CONNECTIONS_VERBOSE) == 0) { - return; - } - writer.print(" [Connection router="+ router.getNickname()); - writer.print(" circuits="+ circuitCount); - writer.print(" idle="+ (getIdleMilliseconds()/1000) + "s"); - writer.println("]"); - } -} diff --git a/orchid/src/com/subgraph/orchid/connections/ConnectionSocketFactory.java b/orchid/src/com/subgraph/orchid/connections/ConnectionSocketFactory.java deleted file mode 100644 index 9ffdf6c9..00000000 --- a/orchid/src/com/subgraph/orchid/connections/ConnectionSocketFactory.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.subgraph.orchid.connections; - -import java.io.IOException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import com.subgraph.orchid.TorException; - -public class ConnectionSocketFactory { - static final String[] V1_CIPHERS_ONLY = { - "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", - "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", - }; - - private static final String[] MANDATORY_CIPHERS = { - "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", - "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", - "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA"}; - - private static final TrustManager[] NULL_TRUST = { - new X509TrustManager() { - private final X509Certificate[] empty = {}; - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - } - - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - } - - public X509Certificate[] getAcceptedIssuers() { - return empty; - } - } - }; - - private static SSLContext createSSLContext() { - System.setProperty("sun.security.ssl.allowUnsafeRenegotiation", "true"); - try { - final SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, NULL_TRUST, null); - return sslContext; - } catch (NoSuchAlgorithmException e) { - throw new TorException(e); - } catch (KeyManagementException e) { - throw new TorException(e); - } - } - - private final SSLSocketFactory socketFactory; - - ConnectionSocketFactory() { - socketFactory = createSSLContext().getSocketFactory(); - } - - SSLSocket createSocket() { - try { - final SSLSocket socket = (SSLSocket) socketFactory.createSocket(); - socket.setEnabledCipherSuites(MANDATORY_CIPHERS); - socket.setUseClientMode(true); - return socket; - } catch (IOException e) { - throw new TorException(e); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/crypto/ASN1Parser.java b/orchid/src/com/subgraph/orchid/crypto/ASN1Parser.java deleted file mode 100644 index 0c60837e..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/ASN1Parser.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * A very minimal ASN.1 BER parser which only supports the ASN.1 object types needed - * for parsing encoded RSA public keys. - */ -public class ASN1Parser { - - private final static int ASN1_TAG_SEQUENCE = 16; - private final static int ASN1_TAG_INTEGER = 2; - private final static int ASN1_TAG_BITSTRING = 3; - - static interface ASN1Object {}; - - static class ASN1Sequence implements ASN1Object { - private final List items; - - ASN1Sequence(List items) { - this.items = items; - } - - List getItems() { - return items; - } - } - - static class ASN1Integer implements ASN1Object { - final BigInteger value; - ASN1Integer(BigInteger value) { - this.value = value; - } - BigInteger getValue() { - return value; - } - } - - - static class ASN1BitString implements ASN1Object { - final byte[] bytes; - - ASN1BitString(byte[] bytes) { - this.bytes = bytes; - } - - byte[] getBytes() { - return bytes; - } - } - - /* For object types we don't handle, just stuff the bytes into here */ - static class ASN1Blob extends ASN1BitString { - ASN1Blob(byte[] bytes) { - super(bytes); - } - } - - ASN1Object parseASN1(ByteBuffer data) { - final int typeOctet = data.get() & 0xFF; - final int tag = typeOctet & 0x1F; - final ByteBuffer objectBuffer = getObjectBuffer(data); - - switch(tag) { - case ASN1_TAG_SEQUENCE: - return parseASN1Sequence(objectBuffer); - case ASN1_TAG_INTEGER: - return parseASN1Integer(objectBuffer); - case ASN1_TAG_BITSTRING: - return parseASN1BitString(objectBuffer); - default: - return createBlob(objectBuffer); - } - - } - - /* - * Read 'length' from data buffer, create a new buffer as a slice() which - * contains 'length' bytes of data following length field and return this - * buffer. Increment position pointer of data buffer to skip over these bytes. - */ - ByteBuffer getObjectBuffer(ByteBuffer data) { - final int length = parseASN1Length(data); - if(length > data.remaining()) { - throw new IllegalArgumentException(); - } - final ByteBuffer objectBuffer = data.slice(); - objectBuffer.limit(length); - data.position(data.position() + length); - return objectBuffer; - } - - int parseASN1Length(ByteBuffer data) { - final int firstOctet = data.get() & 0xFF; - if(firstOctet < 0x80) { - return firstOctet; - } - return parseASN1LengthLong(firstOctet & 0x7F, data); - } - - int parseASN1LengthLong(int lengthOctets, ByteBuffer data) { - if(lengthOctets == 0 || lengthOctets > 3) { - // indefinite form or too long - throw new IllegalArgumentException(); - } - int length = 0; - for(int i = 0; i < lengthOctets; i++) { - length <<= 8; - length |= (data.get() & 0xFF); - } - return length; - } - - ASN1Sequence parseASN1Sequence(ByteBuffer data) { - final List obs = new ArrayList(); - while(data.hasRemaining()) { - obs.add(parseASN1(data)); - } - return new ASN1Sequence(obs); - } - - ASN1Integer parseASN1Integer(ByteBuffer data) { - return new ASN1Integer(new BigInteger(getRemainingBytes(data))); - } - - ASN1BitString parseASN1BitString(ByteBuffer data) { - final int unusedBits = data.get() & 0xFF; - if(unusedBits != 0) { - throw new IllegalArgumentException(); - } - return new ASN1BitString(getRemainingBytes(data)); - } - - ASN1Blob createBlob(ByteBuffer data) { - return new ASN1Blob(getRemainingBytes(data)); - } - - private byte[] getRemainingBytes(ByteBuffer data) { - final byte[] bs = new byte[data.remaining()]; - data.get(bs); - return bs; - } -} diff --git a/orchid/src/com/subgraph/orchid/crypto/Curve25519.java b/orchid/src/com/subgraph/orchid/crypto/Curve25519.java deleted file mode 100644 index 8cdb49c5..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/Curve25519.java +++ /dev/null @@ -1,469 +0,0 @@ -package com.subgraph.orchid.crypto; - -// -// Copyright (c) 2011, Neil Alexander T. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with -// or without modification, are permitted provided that the following -// conditions are met: -// -// - Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// - Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// - -/* - * https://github.com/neilalexander/jnacl/blob/master/crypto/curve25519.java - */ -public class Curve25519 -{ - final int CRYPTO_BYTES = 32; - final int CRYPTO_SCALARBYTES = 32; - - static byte[] basev = { 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - static int[] minusp = { 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; - - public static int crypto_scalarmult_base(byte[] q, byte[] n) - { - byte[] basevp = basev; - return crypto_scalarmult(q, n, basevp); - } - - static void add(int[] outv, int outvoffset, int[] a, int aoffset, int[] b, int boffset) - { - int u = 0; - - for (int j = 0; j < 31; ++j) - { - u += a[aoffset + j] + b[boffset + j]; - outv[outvoffset + j] = u & 255; - u >>>= 8; - } - - u += a[aoffset + 31] + b[boffset + 31]; - outv[outvoffset + 31] = u; - } - - static void sub(int[] outv, int outvoffset, int[] a, int aoffset, int[] b, int boffset) - { - int u = 218; - - for (int j = 0; j < 31; ++j) - { - u += a[aoffset + j] + 65280 - b[boffset + j]; - outv[outvoffset + j] = u & 255; - u >>>= 8; - } - - u += a[aoffset + 31] - b[boffset + 31]; - outv[outvoffset + 31] = u; - } - - static void squeeze(int[] a, int aoffset) - { - int u = 0; - - for (int j = 0; j < 31; ++j) - { - u += a[aoffset + j]; - a[aoffset + j] = u & 255; - u >>>= 8; - } - - u += a[aoffset + 31]; - a[aoffset + 31] = u & 127; - u = 19 * (u >>> 7); - - for (int j = 0; j < 31; ++j) - { - u += a[aoffset + j]; - a[aoffset + j] = u & 255; - u >>>= 8; - } - - u += a[aoffset + 31]; - a[aoffset + 31] = u; - } - - static void freeze(int[] a, int aoffset) - { - int[] aorig = new int[32]; - - for (int j = 0; j < 32; ++j) - aorig[j] = a[aoffset + j]; - - int[] minuspp = minusp; - - add(a, 0, a, 0, minuspp, 0); - - int negative = (int) (-((a[aoffset + 31] >>> 7) & 1)); - - for (int j = 0; j < 32; ++j) - a[aoffset + j] ^= negative & (aorig[j] ^ a[aoffset + j]); - } - - static void mult(int[] outv, int outvoffset, int[] a, int aoffset, int[] b, int boffset) - { - int j; - - for (int i = 0; i < 32; ++i) - { - int u = 0; - - for (j = 0; j <= i; ++j) - u += a[aoffset + j] * b[boffset + i - j]; - - for (j = i + 1; j < 32; ++j) - u += 38 * a[aoffset + j] * b[boffset + i + 32 - j]; - - outv[outvoffset + i] = u; - } - - squeeze(outv, outvoffset); - } - - static void mult121665(int[] outv, int[] a) - { - int j; - int u = 0; - - for (j = 0; j < 31; ++j) - { - u += 121665 * a[j]; - outv[j] = u & 255; - u >>>= 8; - } - - u += 121665 * a[31]; - outv[31] = u & 127; - u = 19 * (u >>> 7); - - for (j = 0; j < 31; ++j) - { - u += outv[j]; - outv[j] = u & 255; - u >>>= 8; - } - - u += outv[j]; - outv[j] = u; - } - - static void square(int[] outv, int outvoffset, int[] a, int aoffset) - { - int j; - - for (int i = 0; i < 32; ++i) - { - int u = 0; - - for (j = 0; j < i - j; ++j) - u += a[aoffset + j] * a[aoffset + i - j]; - - for (j = i + 1; j < i + 32 - j; ++j) - u += 38 * a[aoffset + j] * a[aoffset + i + 32 - j]; - - u *= 2; - - if ((i & 1) == 0) - { - u += a[aoffset + i / 2] * a[aoffset + i / 2]; - u += 38 * a[aoffset + i / 2 + 16] * a[aoffset + i / 2 + 16]; - } - - outv[outvoffset + i] = u; - } - - squeeze(outv, outvoffset); - } - - static void select(int[] p, int[] q, int[] r, int[] s, int b) - { - int bminus1 = b - 1; - - for (int j = 0; j < 64; ++j) - { - int t = bminus1 & (r[j] ^ s[j]); - p[j] = s[j] ^ t; - q[j] = r[j] ^ t; - } - } - - static void mainloop(int[] work, byte[] e) - { - int[] xzm1 = new int[64]; - int[] xzm = new int[64]; - int[] xzmb = new int[64]; - int[] xzm1b = new int[64]; - int[] xznb = new int[64]; - int[] xzn1b = new int[64]; - int[] a0 = new int[64]; - int[] a1 = new int[64]; - int[] b0 = new int[64]; - int[] b1 = new int[64]; - int[] c1 = new int[64]; - int[] r = new int[32]; - int[] s = new int[32]; - int[] t = new int[32]; - int[] u = new int[32]; - - for (int j = 0; j < 32; ++j) - xzm1[j] = work[j]; - - xzm1[32] = 1; - - for (int j = 33; j < 64; ++j) - xzm1[j] = 0; - - xzm[0] = 1; - - for (int j = 1; j < 64; ++j) - xzm[j] = 0; - - int[] xzmbp = xzmb, a0p = a0, xzm1bp = xzm1b; - int[] a1p = a1, b0p = b0, b1p = b1, c1p = c1; - int[] xznbp = xznb, up = u, xzn1bp = xzn1b; - int[] workp = work, sp = s, rp = r; - - for (int pos = 254; pos >= 0; --pos) - { - int b = ((int) ((e[pos / 8] & 0xFF) >>> (pos & 7))); - b &= 1; - select(xzmb, xzm1b, xzm, xzm1, b); - add(a0, 0, xzmb, 0, xzmbp, 32); - sub(a0p, 32, xzmb, 0, xzmbp, 32); - add(a1, 0, xzm1b, 0, xzm1bp, 32); - sub(a1p, 32, xzm1b, 0, xzm1bp, 32); - square(b0p, 0, a0p, 0); - square(b0p, 32, a0p, 32); - mult(b1p, 0, a1p, 0, a0p, 32); - mult(b1p, 32, a1p, 32, a0p, 0); - add(c1, 0, b1, 0, b1p, 32); - sub(c1p, 32, b1, 0, b1p, 32); - square(rp, 0, c1p, 32); - sub(sp, 0, b0, 0, b0p, 32); - mult121665(t, s); - add(u, 0, t, 0, b0p, 0); - mult(xznbp, 0, b0p, 0, b0p, 32); - mult(xznbp, 32, sp, 0, up, 0); - square(xzn1bp, 0, c1p, 0); - mult(xzn1bp, 32, rp, 0, workp, 0); - select(xzm, xzm1, xznb, xzn1b, b); - } - - for (int j = 0; j < 64; ++j) - work[j] = xzm[j]; - } - - static void recip(int[] outv, int outvoffset, int[] z, int zoffset) - { - int[] z2 = new int[32]; - int[] z9 = new int[32]; - int[] z11 = new int[32]; - int[] z2_5_0 = new int[32]; - int[] z2_10_0 = new int[32]; - int[] z2_20_0 = new int[32]; - int[] z2_50_0 = new int[32]; - int[] z2_100_0 = new int[32]; - int[] t0 = new int[32]; - int[] t1 = new int[32]; - - /* 2 */ - int[] z2p = z2; - square(z2p, 0, z, zoffset); - - /* 4 */ - square(t1, 0, z2, 0); - - /* 8 */ - square(t0, 0, t1, 0); - - /* 9 */ - int[] z9p = z9, t0p = t0; - mult(z9p, 0, t0p, 0, z, zoffset); - - /* 11 */ - mult(z11, 0, z9, 0, z2, 0); - - /* 22 */ - square(t0, 0, z11, 0); - - /* 2^5 - 2^0 = 31 */ - mult(z2_5_0, 0, t0, 0, z9, 0); - - /* 2^6 - 2^1 */ - square(t0, 0, z2_5_0, 0); - - /* 2^7 - 2^2 */ - square(t1, 0, t0, 0); - - /* 2^8 - 2^3 */ - square(t0, 0, t1, 0); - - /* 2^9 - 2^4 */ - square(t1, 0, t0, 0); - - /* 2^10 - 2^5 */ - square(t0, 0, t1, 0); - - /* 2^10 - 2^0 */ - mult(z2_10_0, 0, t0, 0, z2_5_0, 0); - - /* 2^11 - 2^1 */ - square(t0, 0, z2_10_0, 0); - - /* 2^12 - 2^2 */ - square(t1, 0, t0, 0); - - /* 2^20 - 2^10 */ - for (int i = 2; i < 10; i += 2) - { - square(t0, 0, t1, 0); - square(t1, 0, t0, 0); - } - - /* 2^20 - 2^0 */ - mult(z2_20_0, 0, t1, 0, z2_10_0, 0); - - /* 2^21 - 2^1 */ - square(t0, 0, z2_20_0, 0); - - /* 2^22 - 2^2 */ - square(t1, 0, t0, 0); - - /* 2^40 - 2^20 */ - for (int i = 2; i < 20; i += 2) - { - square(t0, 0, t1, 0); - square(t1, 0, t0, 0); - } - - /* 2^40 - 2^0 */ - mult(t0, 0, t1, 0, z2_20_0, 0); - - /* 2^41 - 2^1 */ - square(t1, 0, t0, 0); - - /* 2^42 - 2^2 */ - square(t0, 0, t1, 0); - - /* 2^50 - 2^10 */ - for (int i = 2; i < 10; i += 2) - { - square(t1, 0, t0, 0); - square(t0, 0, t1, 0); - } - - /* 2^50 - 2^0 */ - mult(z2_50_0, 0, t0, 0, z2_10_0, 0); - - /* 2^51 - 2^1 */ - square(t0, 0, z2_50_0, 0); - - /* 2^52 - 2^2 */ - square(t1, 0, t0, 0); - - /* 2^100 - 2^50 */ - for (int i = 2; i < 50; i += 2) - { - square(t0, 0, t1, 0); - square(t1, 0, t0, 0); - } - - /* 2^100 - 2^0 */ - mult(z2_100_0, 0, t1, 0, z2_50_0, 0); - - /* 2^101 - 2^1 */ - square(t1, 0, z2_100_0, 0); - - /* 2^102 - 2^2 */ - square(t0, 0, t1, 0); - - /* 2^200 - 2^100 */ - for (int i = 2; i < 100; i += 2) - { - square(t1, 0, t0, 0); - square(t0, 0, t1, 0); - } - - /* 2^200 - 2^0 */ - mult(t1, 0, t0, 0, z2_100_0, 0); - - /* 2^201 - 2^1 */ - square(t0, 0, t1, 0); - - /* 2^202 - 2^2 */ - square(t1, 0, t0, 0); - - /* 2^250 - 2^50 */ - for (int i = 2; i < 50; i += 2) - { - square(t0, 0, t1, 0); - square(t1, 0, t0, 0); - } - - /* 2^250 - 2^0 */ - mult(t0, 0, t1, 0, z2_50_0, 0); - - /* 2^251 - 2^1 */ - square(t1, 0, t0, 0); - - /* 2^252 - 2^2 */ - square(t0, 0, t1, 0); - - /* 2^253 - 2^3 */ - square(t1, 0, t0, 0); - - /* 2^254 - 2^4 */ - square(t0, 0, t1, 0); - - /* 2^255 - 2^5 */ - square(t1, 0, t0, 0); - - /* 2^255 - 21 */ - int[] t1p = t1, z11p = z11; - mult(outv, outvoffset, t1p, 0, z11p, 0); - } - - public static int crypto_scalarmult(byte[] q, byte[] n, byte[] p) - { - int[] work = new int[96]; - byte[] e = new byte[32]; - - for (int i = 0; i < 32; ++i) - e[i] = n[i]; - - e[0] &= 248; - e[31] &= 127; - e[31] |= 64; - - for (int i = 0; i < 32; ++i) - work[i] = p[i] & 0xFF; - - mainloop(work, e); - - recip(work, 32, work, 32); - mult(work, 64, work, 0, work, 32); - freeze(work, 64); - - for (int i = 0; i < 32; ++i) - q[i] = (byte) work[64 + i]; - - return 0; - } -} diff --git a/orchid/src/com/subgraph/orchid/crypto/HybridEncryption.java b/orchid/src/com/subgraph/orchid/crypto/HybridEncryption.java deleted file mode 100644 index 33a394c9..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/HybridEncryption.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; - -import com.subgraph.orchid.TorException; - -/** - * The HybridEncryption class implements the "hybrid encryption" scheme - * as described in section 0.3 of the main Tor specification (tor-spec.txt). - */ -public class HybridEncryption { - - private final static int PK_ENC_LEN = 128; - private final static int PK_PAD_LEN = 42; - private final static int PK_DATA_LEN = PK_ENC_LEN - PK_PAD_LEN; // 86 bytes - private final static int PK_DATA_LEN_WITH_KEY = PK_DATA_LEN - TorStreamCipher.KEY_LEN; // 70 bytes - /* - * The "hybrid encryption" of a byte sequence M with a public key PK is - * computed as follows: - * - * 1. If M is less than PK_ENC_LEN-PK_PAD_LEN (86), pad and encrypt M with PK. - * 2. Otherwise, generate a KEY_LEN byte random key K. - * Let M1 = the first PK_ENC_LEN-PK_PAD_LEN-KEY_LEN (70) bytes of M, - * and let M2 = the rest of M. - * Pad and encrypt K|M1 with PK. Encrypt M2 with our stream cipher, - * using the key K. Concatenate these encrypted values. - */ - final private Cipher cipher; - - /** - * Create a new HybridEncryption instance which can be used for performing - * "hybrid encryption" operations as described in the main Tor specification (tor-spec.txt). - */ - public HybridEncryption() { - try { - cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding"); - } catch (NoSuchAlgorithmException e) { - throw new TorException(e); - } catch (NoSuchPaddingException e) { - throw new TorException(e); - } - } - - /** - * Encrypt the entire contents of the byte array data with the given TorPublicKey - * according to the "hybrid encryption" scheme described in the main Tor specification (tor-spec.txt). - * - * @param data The bytes to be encrypted. - * @param publicKey The public key to use for encryption. - * @return A new array containing the encrypted data. - */ - public byte[] encrypt(byte[] data, TorPublicKey publicKey) { - if(data.length < PK_DATA_LEN) - return encryptSimple(data, publicKey); - - // RSA( K | M1 ) --> C1 - TorStreamCipher randomKeyCipher = TorStreamCipher.createWithRandomKey(); - final byte[] kAndM1 = new byte[PK_DATA_LEN]; - System.arraycopy(randomKeyCipher.getKeyBytes(), 0, kAndM1, 0, TorStreamCipher.KEY_LEN); - System.arraycopy(data, 0, kAndM1, TorStreamCipher.KEY_LEN, PK_DATA_LEN_WITH_KEY); - final byte[] c1 = encryptSimple(kAndM1, publicKey); - - // AES_CTR(M2) --> C2 - final byte[] c2 = new byte[data.length - PK_DATA_LEN_WITH_KEY]; - System.arraycopy(data, PK_DATA_LEN_WITH_KEY, c2, 0, c2.length); - randomKeyCipher.encrypt(c2); - //final byte[] c2 = randomKeyCipher.doFinal(data, PK_DATA_LEN_WITH_KEY, data.length - PK_DATA_LEN_WITH_KEY); - - // C1 | C2 - final byte[] output = new byte[c1.length + c2.length]; - System.arraycopy(c1, 0, output, 0, c1.length); - System.arraycopy(c2, 0, output, c1.length, c2.length); - return output; - } - - private byte[] encryptSimple(byte[] data, TorPublicKey publicKey) { - try { - cipher.init(Cipher.ENCRYPT_MODE, publicKey.getRSAPublicKey()); - return cipher.doFinal(data); - } catch (InvalidKeyException e) { - throw new TorException(e); - } catch (IllegalBlockSizeException e) { - throw new TorException(e); - } catch (BadPaddingException e) { - throw new TorException(e); - } - } - - /** - * Decrypt the contents of the byte array data with the given TorPrivateKey - * according to the "hybrid encryption" scheme described in the main Tor specification (tor-spec.txt). - * - * @param data Encrypted data to decrypt. - * @param privateKey The private key to use to decrypt the data. - * @return A new byte array containing the decrypted data. - */ - - public byte[] decrypt(byte[] data, TorPrivateKey privateKey) { - if(data.length < PK_ENC_LEN) - throw new TorException("Message is too short"); - - if(data.length == PK_ENC_LEN) - return decryptSimple(data, privateKey); - - // ( C1 | C2 ) --> C1, C2 - final byte[] c1 = new byte[PK_ENC_LEN]; - final byte[] c2 = new byte[data.length - PK_ENC_LEN]; - System.arraycopy(data, 0, c1, 0, PK_ENC_LEN); - System.arraycopy(data, PK_ENC_LEN, c2, 0, c2.length); - - // RSA( C1 ) --> ( K | M1 ) --> K, M1 - final byte[] kAndM1 = decryptSimple(c1, privateKey); - final byte[] streamKey = new byte[TorStreamCipher.KEY_LEN]; - final int m1Length = kAndM1.length - TorStreamCipher.KEY_LEN; - final byte[] m1 = new byte[m1Length]; - System.arraycopy(kAndM1, 0, streamKey, 0, TorStreamCipher.KEY_LEN); - System.arraycopy(kAndM1, TorStreamCipher.KEY_LEN, m1, 0, m1Length); - - // AES_CTR( C2 ) --> M2 - final TorStreamCipher streamCipher = TorStreamCipher.createFromKeyBytes(streamKey); - streamCipher.encrypt(c2); - final byte[] m2 = c2; - - final byte[] output = new byte[m1.length + m2.length]; - System.arraycopy(m1, 0, output, 0, m1.length); - System.arraycopy(m2, 0, output, m1.length, m2.length); - return output; - } - - private byte[] decryptSimple(byte[] data, TorPrivateKey privateKey) { - try { - cipher.init(Cipher.DECRYPT_MODE, privateKey.getRSAPrivateKey()); - return cipher.doFinal(data); - } catch (InvalidKeyException e) { - throw new TorException(e); - } catch (IllegalBlockSizeException e) { - throw new TorException(e); - } catch (BadPaddingException e) { - throw new TorException(e); - } - } - - -} diff --git a/orchid/src/com/subgraph/orchid/crypto/PRNGFixes.java b/orchid/src/com/subgraph/orchid/crypto/PRNGFixes.java deleted file mode 100644 index b49ffb5d..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/PRNGFixes.java +++ /dev/null @@ -1,360 +0,0 @@ -package com.subgraph.orchid.crypto; - -/* - * This software is provided 'as-is', without any express or implied - * warranty. In no event will Google be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, as long as the origin is not misrepresented. - */ - -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; -import java.security.SecureRandom; -import java.security.SecureRandomSpi; -import java.security.Security; -import java.util.logging.Logger; - -/** - * Fixes for the output of the default PRNG having low entropy. - * - * The fixes need to be applied via {@link #apply()} before any use of Java - * Cryptography Architecture primitives. A good place to invoke them is in the - * application's {@code onCreate}. - */ -public final class PRNGFixes { - - private final static Logger logger = Logger.getLogger(PRNGFixes.class.getName()); - - private static final int VERSION_CODE_JELLY_BEAN = 16; - private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; - private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = - getBuildFingerprintAndDeviceSerial(); - - /** Hidden constructor to prevent instantiation. */ - private PRNGFixes() {} - - /** - * Applies all fixes. - * - * @throws SecurityException if a fix is needed but could not be applied. - */ - public static void apply() { - applyOpenSSLFix(); - installLinuxPRNGSecureRandom(); - } - - /** - * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the - * fix is not needed. - * - * @throws SecurityException if the fix is needed but could not be applied. - */ - private static void applyOpenSSLFix() throws SecurityException { - int sdkVersion = getSdkVersion(); - if ((sdkVersion < VERSION_CODE_JELLY_BEAN) - || (sdkVersion > VERSION_CODE_JELLY_BEAN_MR2)) { - // No need to apply the fix - return; - } - - try { - // Mix in the device- and invocation-specific seed. - Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") - .getMethod("RAND_seed", byte[].class) - .invoke(null, generateSeed()); - - // Mix output of Linux PRNG into OpenSSL's PRNG - int bytesRead = (Integer) Class.forName( - "org.apache.harmony.xnet.provider.jsse.NativeCrypto") - .getMethod("RAND_load_file", String.class, long.class) - .invoke(null, "/dev/urandom", 1024); - if (bytesRead != 1024) { - throw new IOException( - "Unexpected number of bytes read from Linux PRNG: " - + bytesRead); - } - } catch (Exception e) { - throw new SecurityException("Failed to seed OpenSSL PRNG", e); - } - } - - /** - * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the - * default. Does nothing if the implementation is already the default or if - * there is not need to install the implementation. - * - * @throws SecurityException if the fix is needed but could not be applied. - */ - private static void installLinuxPRNGSecureRandom() - throws SecurityException { - if (getSdkVersion() > VERSION_CODE_JELLY_BEAN_MR2) { - // No need to apply the fix - return; - } - - // Install a Linux PRNG-based SecureRandom implementation as the - // default, if not yet installed. - Provider[] secureRandomProviders = - Security.getProviders("SecureRandom.SHA1PRNG"); - if ((secureRandomProviders == null) - || (secureRandomProviders.length < 1) - || (!LinuxPRNGSecureRandomProvider.class.equals( - secureRandomProviders[0].getClass()))) { - Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); - } - - // Assert that new SecureRandom() and - // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed - // by the Linux PRNG-based SecureRandom implementation. - SecureRandom rng1 = new SecureRandom(); - if (!LinuxPRNGSecureRandomProvider.class.equals( - rng1.getProvider().getClass())) { - throw new SecurityException( - "new SecureRandom() backed by wrong Provider: " - + rng1.getProvider().getClass()); - } - - SecureRandom rng2; - try { - rng2 = SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - throw new SecurityException("SHA1PRNG not available", e); - } - if (!LinuxPRNGSecureRandomProvider.class.equals( - rng2.getProvider().getClass())) { - throw new SecurityException( - "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" - + " Provider: " + rng2.getProvider().getClass()); - } - } - - /** - * {@code Provider} of {@code SecureRandom} engines which pass through - * all requests to the Linux PRNG. - */ - private static class LinuxPRNGSecureRandomProvider extends Provider { - - private static final long serialVersionUID = 1L; - - public LinuxPRNGSecureRandomProvider() { - super("LinuxPRNG", - 1.0, - "A Linux-specific random number provider that uses" - + " /dev/urandom"); - // Although /dev/urandom is not a SHA-1 PRNG, some apps - // explicitly request a SHA1PRNG SecureRandom and we thus need to - // prevent them from getting the default implementation whose output - // may have low entropy. - put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); - put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); - } - } - - /** - * {@link SecureRandomSpi} which passes all requests to the Linux PRNG - * ({@code /dev/urandom}). - */ - public static class LinuxPRNGSecureRandom extends SecureRandomSpi { - - /* - * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed - * are passed through to the Linux PRNG (/dev/urandom). Instances of - * this class seed themselves by mixing in the current time, PID, UID, - * build fingerprint, and hardware serial number (where available) into - * Linux PRNG. - * - * Concurrency: Read requests to the underlying Linux PRNG are - * serialized (on sLock) to ensure that multiple threads do not get - * duplicated PRNG output. - */ - - - private static final long serialVersionUID = 1L; - - private static final File URANDOM_FILE = new File("/dev/urandom"); - - private static final Object sLock = new Object(); - - /** - * Input stream for reading from Linux PRNG or {@code null} if not yet - * opened. - * - * @GuardedBy("sLock") - */ - private static DataInputStream sUrandomIn; - - /** - * Output stream for writing to Linux PRNG or {@code null} if not yet - * opened. - * - * @GuardedBy("sLock") - */ - private static OutputStream sUrandomOut; - - /** - * Whether this engine instance has been seeded. This is needed because - * each instance needs to seed itself if the client does not explicitly - * seed it. - */ - private boolean mSeeded; - - @Override - protected void engineSetSeed(byte[] bytes) { - try { - OutputStream out; - synchronized (sLock) { - out = getUrandomOutputStream(); - } - out.write(bytes); - out.flush(); - } catch (IOException e) { - // On a small fraction of devices /dev/urandom is not writable. - // Log and ignore. - logger.warning("Failed to mix seed into " + URANDOM_FILE); - } finally { - mSeeded = true; - } - } - - @Override - protected void engineNextBytes(byte[] bytes) { - if (!mSeeded) { - // Mix in the device- and invocation-specific seed. - engineSetSeed(generateSeed()); - } - - try { - DataInputStream in; - synchronized (sLock) { - in = getUrandomInputStream(); - } - synchronized (in) { - in.readFully(bytes); - } - } catch (IOException e) { - throw new SecurityException( - "Failed to read from " + URANDOM_FILE, e); - } - } - - @Override - protected byte[] engineGenerateSeed(int size) { - byte[] seed = new byte[size]; - engineNextBytes(seed); - return seed; - } - - private DataInputStream getUrandomInputStream() { - synchronized (sLock) { - if (sUrandomIn == null) { - // NOTE: Consider inserting a BufferedInputStream between - // DataInputStream and FileInputStream if you need higher - // PRNG output performance and can live with future PRNG - // output being pulled into this process prematurely. - try { - sUrandomIn = new DataInputStream( - new FileInputStream(URANDOM_FILE)); - } catch (IOException e) { - throw new SecurityException("Failed to open " - + URANDOM_FILE + " for reading", e); - } - } - return sUrandomIn; - } - } - - private OutputStream getUrandomOutputStream() throws IOException { - synchronized (sLock) { - if (sUrandomOut == null) { - sUrandomOut = new FileOutputStream(URANDOM_FILE); - } - return sUrandomOut; - } - } - } - - /** - * Generates a device- and invocation-specific seed to be mixed into the - * Linux PRNG. - */ - private static byte[] generateSeed() { - try { - ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); - DataOutputStream seedBufferOut = - new DataOutputStream(seedBuffer); - seedBufferOut.writeLong(System.currentTimeMillis()); - seedBufferOut.writeLong(System.nanoTime()); - //seedBufferOut.writeInt(Process.myPid()); - //seedBufferOut.writeInt(Process.myUid()); - seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); - seedBufferOut.close(); - return seedBuffer.toByteArray(); - } catch (IOException e) { - throw new SecurityException("Failed to generate seed", e); - } - } - - /** - * Gets the hardware serial number of this device. - * - * @return serial number or {@code null} if not available. - */ - private static String getDeviceSerialNumber() { - // We're using the Reflection API because Build.SERIAL is only available - // since API Level 9 (Gingerbread, Android 2.3). - try { - return (String) Class.forName("android.os.Build").getField("SERIAL").get(null); - //return (String) Build.class.getField("SERIAL").get(null); - } catch (Exception ignored) { - return null; - } - } - - private static int getSdkVersion() { - try { - return Class.forName("android.os.Build").getField("VERSION").getClass().getField("SDK_INT").getInt(null); - } catch (Exception e) { - logger.warning("Could not get Build.VERSION.SDK_INT value : "+ e); - return 0; - } - } - - private static String getBuildFingerprint() { - try { - return (String) Class.forName("android.os.Build").getField("FINGERPRINT").get(null); - } catch (Exception e) { - logger.warning("Could not get BUILD.FINGERPRINT value : "+ e); - return ""; - } - } - - private static byte[] getBuildFingerprintAndDeviceSerial() { - StringBuilder result = new StringBuilder(); - // String fingerprint = Build.FINGERPRINT; - String fingerprint = getBuildFingerprint(); - if (fingerprint != null) { - result.append(fingerprint); - } - String serial = getDeviceSerialNumber(); - if (serial != null) { - result.append(serial); - } - try { - return result.toString().getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("UTF-8 encoding not supported"); - } - } -} \ No newline at end of file diff --git a/orchid/src/com/subgraph/orchid/crypto/RSAKeyEncoder.java b/orchid/src/com/subgraph/orchid/crypto/RSAKeyEncoder.java deleted file mode 100644 index 28eb474a..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/RSAKeyEncoder.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.RSAPublicKeySpec; -import java.util.List; - -import com.subgraph.orchid.crypto.ASN1Parser.ASN1BitString; -import com.subgraph.orchid.crypto.ASN1Parser.ASN1Integer; -import com.subgraph.orchid.crypto.ASN1Parser.ASN1Object; -import com.subgraph.orchid.crypto.ASN1Parser.ASN1Sequence; -import com.subgraph.orchid.encoders.Base64; - -public class RSAKeyEncoder { - private final static String HEADER = "-----BEGIN RSA PUBLIC KEY-----"; - private final static String FOOTER = "-----END RSA PUBLIC KEY-----"; - - private final ASN1Parser asn1Parser = new ASN1Parser(); - - /** - * Parse a PKCS1 PEM encoded RSA public key into the modulus/exponent components - * and construct a new RSAPublicKey - * - * @param pem The PEM encoded string to parse. - * @return a new RSAPublicKey - * - * @throws GeneralSecurityException If an error occurs while parsing the pem argument or creating the RSA key. - */ - public RSAPublicKey parsePEMPublicKey(String pem) throws GeneralSecurityException { - try { - byte[] bs = decodeAsciiArmoredPEM(pem); - ByteBuffer data = ByteBuffer.wrap(bs); - final ASN1Object ob = asn1Parser.parseASN1(data); - final List seq = asn1ObjectToSequence(ob, 2); - final BigInteger modulus = asn1ObjectToBigInt(seq.get(0)); - final BigInteger exponent = asn1ObjectToBigInt(seq.get(1)); - return createKeyFromModulusAndExponent(modulus, exponent); - } catch (IllegalArgumentException e) { - throw new InvalidKeyException(); - } - } - - private RSAPublicKey createKeyFromModulusAndExponent(BigInteger modulus, BigInteger exponent) throws GeneralSecurityException { - RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); - KeyFactory fac = KeyFactory.getInstance("RSA"); - return (RSAPublicKey) fac.generatePublic(spec); - } - - /** - * Return the PKCS1 encoded representation of the specified RSAPublicKey. Since - * the primary encoding format for RSA public keys is X.509 SubjectPublicKeyInfo, - * this needs to be converted to PKCS1 by extracting the needed field. - * - * @param publicKey The RSA public key to encode. - * @return The PKCS1 encoded representation of the publicKey argument - */ - public byte[] getPKCS1Encoded(RSAPublicKey publicKey) { - return extractPKCS1KeyFromSubjectPublicKeyInfo(publicKey.getEncoded()); - } - - /* - * SubjectPublicKeyInfo encoding looks like this: - * - * SEQUENCE { - * SEQUENCE { - * OBJECT IDENTIFIER rsaEncryption (1 2 840 113549 1 1 1) - * NULL - * } - * BIT STRING (encapsulating) { <-- contains PKCS1 encoded key - * SEQUENCE { - * INTEGER (modulus) - * INTEGER (exponent) - * } - * } - * } - * - * See: http://www.jensign.com/JavaScience/dotnet/JKeyNet/index.html - */ - private byte[] extractPKCS1KeyFromSubjectPublicKeyInfo(byte[] input) { - final ASN1Object ob = asn1Parser.parseASN1(ByteBuffer.wrap(input)); - final List seq = asn1ObjectToSequence(ob, 2); - return asn1ObjectToBitString(seq.get(1)); - } - - private BigInteger asn1ObjectToBigInt(ASN1Object ob) { - if(!(ob instanceof ASN1Integer)) { - throw new IllegalArgumentException(); - } - final ASN1Integer n = (ASN1Integer) ob; - return n.getValue(); - } - - - private List asn1ObjectToSequence(ASN1Object ob, int expectedSize) { - if(ob instanceof ASN1Sequence) { - final ASN1Sequence seq = (ASN1Sequence) ob; - if(seq.getItems().size() != expectedSize) { - throw new IllegalArgumentException(); - } - return seq.getItems(); - } - throw new IllegalArgumentException(); - } - - private byte[] asn1ObjectToBitString(ASN1Object ob) { - if(!(ob instanceof ASN1BitString)) { - throw new IllegalArgumentException(); - } - final ASN1BitString bitstring = (ASN1BitString) ob; - return bitstring.getBytes(); - } - - private byte[] decodeAsciiArmoredPEM(String pem) { - final String trimmed = removeDelimiters(pem); - return Base64.decode(trimmed); - } - - private String removeDelimiters(String pem) { - final int headerIdx = pem.indexOf(HEADER); - final int footerIdx = pem.indexOf(FOOTER); - if(headerIdx == -1 || footerIdx == -1 || footerIdx <= headerIdx) { - throw new IllegalArgumentException("PEM object not formatted with expected header and footer"); - } - return pem.substring(headerIdx + HEADER.length(), footerIdx); - } - -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorCreateFastKeyAgreement.java b/orchid/src/com/subgraph/orchid/crypto/TorCreateFastKeyAgreement.java deleted file mode 100644 index 7563b2af..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorCreateFastKeyAgreement.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.nio.ByteBuffer; -import java.util.Arrays; - -public class TorCreateFastKeyAgreement implements TorKeyAgreement { - - private final byte[] xValue; - private byte[] yValue; - - public TorCreateFastKeyAgreement() { - final TorRandom random = new TorRandom(); - xValue = random.getBytes(TorMessageDigest.TOR_DIGEST_SIZE); - } - - public byte[] getPublicValue() { - return Arrays.copyOf(xValue, xValue.length); - } - - public void setOtherValue(byte[] yValue) { - if(yValue == null || yValue.length != TorMessageDigest.TOR_DIGEST_SIZE) { - throw new IllegalArgumentException(); - } - this.yValue = Arrays.copyOf(yValue, yValue.length); - } - - public byte[] getDerivedValue() { - if(yValue == null) { - throw new IllegalStateException("Must call setOtherValue() first"); - } - final byte[] result = new byte[2 * TorMessageDigest.TOR_DIGEST_SIZE]; - System.arraycopy(xValue, 0, result, 0, TorMessageDigest.TOR_DIGEST_SIZE); - System.arraycopy(yValue, 0, result, TorMessageDigest.TOR_DIGEST_SIZE, TorMessageDigest.TOR_DIGEST_SIZE); - return result; - } - - public byte[] createOnionSkin() { - return getPublicValue(); - } - - public boolean deriveKeysFromHandshakeResponse(byte[] handshakeResponse, - byte[] keyMaterialOut, byte[] verifyHashOut) { - final ByteBuffer bb = ByteBuffer.wrap(handshakeResponse); - final byte[] peerValue = new byte[TorMessageDigest.TOR_DIGEST_SIZE]; - final byte[] keyHash = new byte[TorMessageDigest.TOR_DIGEST_SIZE]; - bb.get(peerValue); - bb.get(keyHash); - setOtherValue(peerValue); - final byte[] seed = getDerivedValue(); - final TorKeyDerivation kdf = new TorKeyDerivation(seed); - kdf.deriveKeys(keyMaterialOut, verifyHashOut); - return Arrays.equals(verifyHashOut, keyHash); - } -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorKeyAgreement.java b/orchid/src/com/subgraph/orchid/crypto/TorKeyAgreement.java deleted file mode 100644 index c9467aa1..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorKeyAgreement.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.subgraph.orchid.crypto; - -public interface TorKeyAgreement { - byte[] createOnionSkin(); - boolean deriveKeysFromHandshakeResponse(byte[] handshakeResponse, byte[] keyMaterialOut, byte[] verifyHashOut); -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorKeyDerivation.java b/orchid/src/com/subgraph/orchid/crypto/TorKeyDerivation.java deleted file mode 100644 index e965984a..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorKeyDerivation.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.nio.ByteBuffer; - -public class TorKeyDerivation { - - private final byte[] kdfBuffer; - private int round; - - public TorKeyDerivation(byte[] seed) { - this.kdfBuffer = new byte[seed.length + 1]; - System.arraycopy(seed, 0, kdfBuffer, 0, seed.length); - } - public void deriveKeys(byte[] keyMaterialOut, byte[] verifyHashOut) { - final ByteBuffer keyData = deriveKeys(keyMaterialOut.length + verifyHashOut.length); - keyData.get(verifyHashOut); - keyData.get(keyMaterialOut); - } - - public ByteBuffer deriveKeys(int length) { - final ByteBuffer outputBuffer = ByteBuffer.allocate(length); - round = 0; - while(outputBuffer.hasRemaining()) { - byte[] bs = calculateRoundData(); - int n = Math.min(outputBuffer.remaining(), bs.length); - outputBuffer.put(bs, 0, n); - } - - outputBuffer.flip(); - return outputBuffer; - } - - private byte[] calculateRoundData() { - final TorMessageDigest md = new TorMessageDigest(); - kdfBuffer[kdfBuffer.length - 1] = (byte) round; - round += 1; - md.update(kdfBuffer); - return md.getDigestBytes(); - } -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorMessageDigest.java b/orchid/src/com/subgraph/orchid/crypto/TorMessageDigest.java deleted file mode 100644 index d6857877..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorMessageDigest.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.data.HexDigest; - -/** - * This class wraps the default cryptographic message digest algorithm - * used in Tor (SHA-1). - */ -public class TorMessageDigest { - - - public static final int TOR_DIGEST_SIZE = 20; - public static final int TOR_DIGEST256_SIZE = 32; - - private static final String TOR_DIGEST_ALGORITHM = "SHA-1"; - private static final String TOR_DIGEST256_ALGORITHM = "SHA-256"; - - private final MessageDigest digestInstance; - private final boolean isDigest256; - - public TorMessageDigest(boolean isDigest256) { - digestInstance = createDigestInstance(isDigest256); - this.isDigest256 = isDigest256; - } - - public TorMessageDigest() { - this(false); - } - - private MessageDigest createDigestInstance(boolean isDigest256) { - try { - final String algorithm = (isDigest256) ? TOR_DIGEST256_ALGORITHM : TOR_DIGEST_ALGORITHM; - return MessageDigest.getInstance(algorithm); - } catch (NoSuchAlgorithmException e) { - throw new TorException(e); - } - } - - /** - * Return true if this is a 256 bit digest instance. - * - * @return true if this is a 256 bit digest instance. - */ - public boolean isDigest256() { - return isDigest256; - } - - /** - * Return the digest value of all data processed up until this point. - * @return The digest value as an array of TOR_DIGEST_SIZE or TOR_DIGEST256_SIZE bytes. - */ - public byte[] getDigestBytes() { - try { - // Make a clone because #digest() will reset the MessageDigest instance - // and we want to be able to use this class for running digests on circuits - final MessageDigest clone = (MessageDigest) digestInstance.clone(); - return clone.digest(); - } catch (CloneNotSupportedException e) { - throw new TorException(e); - } - } - - /** - * Return what the digest for the current running hash would be IF we - * added data, but don't really add the data to the digest - * calculation. - */ - public byte[] peekDigest(byte[] data, int offset, int length) { - try { - final MessageDigest clone = (MessageDigest) digestInstance.clone(); - clone.update(data, offset, length); - return clone.digest(); - } catch (CloneNotSupportedException e) { - throw new TorException(e); - } - } - - /** - * Calculate the digest value of all data processed up until this point and convert - * the digest into a HexDigest object. - * @return A new HexDigest object representing the current digest value. - * @see HexDigest - */ - public HexDigest getHexDigest() { - return HexDigest.createFromDigestBytes(getDigestBytes()); - } - - /** - * Add the entire contents of the byte array input to the current digest calculation. - * - * @param input An array of input bytes to process. - */ - public void update(byte[] input) { - digestInstance.update(input); - } - - /** - * Add length bytes of the contents of the byte array input beginning at - * offset into the array to the current digest calculation. - * - * @param input An array of input bytes to process. - * @param offset The offset into the input array to begin processing. - * @param length A count of how many bytes of the input array to process. - */ - public void update(byte[] input, int offset, int length) { - digestInstance.update(input, offset, length); - } - - /** - * Convert the String input into an array of bytes using the ISO-8859-1 encoding - * and add these bytes to the current digest calculation. - * - * @param input A string to process. - */ - public void update(String input) { - try { - digestInstance.update(input.getBytes("ISO-8859-1")); - } catch (UnsupportedEncodingException e) { - throw new TorException(e); - } - } - -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorNTorKeyAgreement.java b/orchid/src/com/subgraph/orchid/crypto/TorNTorKeyAgreement.java deleted file mode 100644 index fab6414e..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorNTorKeyAgreement.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.misc.Utils; - -public class TorNTorKeyAgreement implements TorKeyAgreement { - public final static int CURVE25519_PUBKEY_LEN = 32; - final static int CURVE25519_OUTPUT_LEN = 32; - final static int DIGEST256_LEN = 32; - final static int DIGEST_LEN = 20; - final static int KEY_LEN = 16; - final static int NTOR_ONIONSKIN_LEN = 2 * CURVE25519_PUBKEY_LEN + DIGEST_LEN; - final static String PROTOID = "ntor-curve25519-sha256-1"; - final static String SERVER_STR = "Server"; - final static int SECRET_INPUT_LEN = CURVE25519_PUBKEY_LEN * 3 + CURVE25519_OUTPUT_LEN * 2 + DIGEST_LEN + PROTOID.length(); - final static int AUTH_INPUT_LEN = DIGEST256_LEN + DIGEST_LEN + (CURVE25519_PUBKEY_LEN * 3) + PROTOID.length() + SERVER_STR.length(); - final static Charset cs = Charset.forName("ISO-8859-1"); - - private final TorRandom random = new TorRandom(); - private final HexDigest peerIdentity; - private final byte[] peerNTorOnionKey; /* pubkey_B */ - private final byte[] secretKey_x; - private final byte[] publicKey_X; - - public TorNTorKeyAgreement(HexDigest peerIdentity, byte[] peerNTorOnionKey) { - this.peerIdentity = peerIdentity; - this.peerNTorOnionKey = peerNTorOnionKey; - this.secretKey_x = generateSecretKey(); - this.publicKey_X = getPublicKeyForPrivate(secretKey_x); - } - - - public byte[] createOnionSkin() { - final ByteBuffer buffer = makeBuffer(NTOR_ONIONSKIN_LEN); - buffer.put(peerIdentity.getRawBytes()); - buffer.put(peerNTorOnionKey); - buffer.put(publicKey_X); - return buffer.array(); - } - - private ByteBuffer makeBuffer(int sz) { - final byte[] array = new byte[sz]; - return ByteBuffer.wrap(array); - } - - byte[] generateSecretKey() { - final byte[]key = random.getBytes(32); - key[0] &= 248; - key[31] &= 127; - key[31] |= 64; - return key; - } - - byte[] getPublicKeyForPrivate(byte[] secretKey) { - final byte[] pub = new byte[32]; - Curve25519.crypto_scalarmult_base(pub, secretKey); - return pub; - } - - private boolean isBad; - - public boolean deriveKeysFromHandshakeResponse(byte[] handshakeResponse, byte[] keyMaterialOut, byte[] verifyHashOut) { - isBad = false; - - final ByteBuffer hr = ByteBuffer.wrap(handshakeResponse); - byte[] serverPub = new byte[CURVE25519_PUBKEY_LEN]; - byte[] authCandidate = new byte[DIGEST256_LEN]; - hr.get(serverPub); - hr.get(authCandidate); - - final byte[] secretInput = buildSecretInput(serverPub); - final byte[] verify = tweak("verify", secretInput); - final byte[] authInput = buildAuthInput(verify, serverPub); - final byte[] auth = tweak("mac", authInput); - isBad |= !Utils.constantTimeArrayEquals(auth, authCandidate); - final byte[] seed = tweak("key_extract", secretInput); - - final TorRFC5869KeyDerivation kdf = new TorRFC5869KeyDerivation(seed); - kdf.deriveKeys(keyMaterialOut, verifyHashOut); - - return !isBad; - } - - public byte[] getNtorCreateMagic() { - return "ntorNTORntorNTOR".getBytes(cs); - } - - private byte[] buildSecretInput(byte[] serverPublic_Y) { - final ByteBuffer bb = makeBuffer(SECRET_INPUT_LEN); - bb.put(scalarMult(serverPublic_Y)); - bb.put(scalarMult(peerNTorOnionKey)); - bb.put(peerIdentity.getRawBytes()); - bb.put(peerNTorOnionKey); - bb.put(publicKey_X); - bb.put(serverPublic_Y); - bb.put(PROTOID.getBytes()); - return bb.array(); - } - - private byte[] buildAuthInput(byte[] verify, byte[] serverPublic_Y) { - final ByteBuffer bb = makeBuffer(AUTH_INPUT_LEN); - bb.put(verify); - bb.put(peerIdentity.getRawBytes()); - bb.put(peerNTorOnionKey); - bb.put(serverPublic_Y); - bb.put(publicKey_X); - bb.put(PROTOID.getBytes(cs)); - bb.put(SERVER_STR.getBytes(cs)); - return bb.array(); - } - - private byte[] scalarMult(byte[] peerValue) { - final byte[] out = new byte[CURVE25519_OUTPUT_LEN]; - Curve25519.crypto_scalarmult(out, secretKey_x, peerValue); - isBad |= isAllZero(out); - return out; - } - - boolean isAllZero(byte[] bs) { - boolean result = true; - for(byte b: bs) { - result &= (b == 0); - } - return result; - } - - byte[] tweak(String suffix, byte[] input) { - return hmac256(input, getStringConstant(suffix)); - } - - byte[] hmac256(byte[] input, byte[] key) { - final SecretKeySpec keyspec = new SecretKeySpec(key, "HmacSHA256"); - try { - final Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(keyspec); - return mac.doFinal(input); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Failed to create HmacSHA256 instance: "+ e); - } catch (InvalidKeyException e) { - throw new IllegalStateException("Failed to create HmacSHA256 instance: "+ e); - } - } - - byte[] getStringConstant(String suffix) { - if(suffix == null || suffix.isEmpty()) { - return PROTOID.getBytes(cs); - } else { - return (PROTOID + ":" + suffix).getBytes(cs); - } - } - -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorPrivateKey.java b/orchid/src/com/subgraph/orchid/crypto/TorPrivateKey.java deleted file mode 100644 index f6c14771..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorPrivateKey.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; - -import com.subgraph.orchid.TorException; - -public class TorPrivateKey { - - static public TorPrivateKey generateNewKeypair() { - KeyPairGenerator generator = createGenerator(); - generator.initialize(1024, new SecureRandom()); - KeyPair pair = generator.generateKeyPair(); - return new TorPrivateKey((RSAPrivateKey)pair.getPrivate(), (RSAPublicKey)pair.getPublic()); - } - - static KeyPairGenerator createGenerator() { - try { - return KeyPairGenerator.getInstance("RSA"); - } catch (NoSuchAlgorithmException e) { - throw new TorException(e); - } - } - - private final TorPublicKey publicKey; - private final RSAPrivateKey privateKey; - - TorPrivateKey(RSAPrivateKey privateKey, RSAPublicKey publicKey) { - this.privateKey = privateKey; - this.publicKey = new TorPublicKey(publicKey); - } - - public TorPublicKey getPublicKey() { - return publicKey; - } - - public RSAPublicKey getRSAPublicKey() { - return publicKey.getRSAPublicKey(); - } - - public RSAPrivateKey getRSAPrivateKey() { - return privateKey; - } -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorPublicKey.java b/orchid/src/com/subgraph/orchid/crypto/TorPublicKey.java deleted file mode 100644 index d66bedb3..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorPublicKey.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.interfaces.RSAPublicKey; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; - -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.misc.Utils; - -/** - * This class wraps the RSA public keys used in the Tor protocol. - */ -public class TorPublicKey { - static public TorPublicKey createFromPEMBuffer(String buffer) { - return new TorPublicKey(buffer); - } - - private final String pemBuffer; - private RSAPublicKey key; - - private byte[] rawKeyBytes = null; - private HexDigest keyFingerprint = null; - - private TorPublicKey(String pemBuffer) { - this.pemBuffer = pemBuffer; - this.key = null; - } - - public TorPublicKey(RSAPublicKey key) { - this.pemBuffer = null; - this.key = key; - } - - private synchronized RSAPublicKey getKey() { - if(key != null) { - return key; - } else if(pemBuffer != null) { - final RSAKeyEncoder encoder = new RSAKeyEncoder(); - try { - key = encoder.parsePEMPublicKey(pemBuffer); - } catch (GeneralSecurityException e) { - throw new IllegalArgumentException("Failed to parse PEM encoded key: "+ e); - } - } - return key; - } - - public synchronized byte[] getRawBytes() { - if(rawKeyBytes == null) { - final RSAKeyEncoder encoder = new RSAKeyEncoder(); - rawKeyBytes = encoder.getPKCS1Encoded(getKey()); - } - return rawKeyBytes; - } - - public synchronized HexDigest getFingerprint() { - if(keyFingerprint == null) { - keyFingerprint = HexDigest.createDigestForData(getRawBytes()); - } - return keyFingerprint; - } - - public boolean verifySignature(TorSignature signature, HexDigest digest) { - return verifySignatureFromDigestBytes(signature, digest.getRawBytes()); - } - - public boolean verifySignature(TorSignature signature, TorMessageDigest digest) { - return verifySignatureFromDigestBytes(signature, digest.getDigestBytes()); - } - - public boolean verifySignatureFromDigestBytes(TorSignature signature, byte[] digestBytes) { - final Cipher cipher = createCipherInstance(); - try { - byte[] decrypted = cipher.doFinal(signature.getSignatureBytes()); - return Utils.constantTimeArrayEquals(decrypted, digestBytes); - } catch (IllegalBlockSizeException e) { - throw new TorException(e); - } catch (BadPaddingException e) { - throw new TorException(e); - } - } - - private Cipher createCipherInstance() { - try { - Cipher cipher = getCipherInstance(); - cipher.init(Cipher.DECRYPT_MODE, getKey()); - return cipher; - } catch (InvalidKeyException e) { - throw new TorException(e); - } - } - - private Cipher getCipherInstance() { - try { - try { - return Cipher.getInstance("RSA/ECB/PKCS1Padding", "SunJCE"); - } catch (NoSuchProviderException e) { - return Cipher.getInstance("RSA/ECB/PKCS1Padding"); - } - } catch (NoSuchAlgorithmException e) { - throw new TorException(e); - } catch (NoSuchPaddingException e) { - throw new TorException(e); - } - } - - public RSAPublicKey getRSAPublicKey() { - return getKey(); - } - - public String toString() { - return "Tor Public Key: " + getFingerprint(); - } - - public boolean equals(Object o) { - if(!(o instanceof TorPublicKey)) - return false; - final TorPublicKey other = (TorPublicKey) o; - return other.getFingerprint().equals(getFingerprint()); - } - - public int hashCode() { - return getFingerprint().hashCode(); - } -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorRFC5869KeyDerivation.java b/orchid/src/com/subgraph/orchid/crypto/TorRFC5869KeyDerivation.java deleted file mode 100644 index 93f7cd55..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorRFC5869KeyDerivation.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.nio.ByteBuffer; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import com.subgraph.orchid.Tor; - -public class TorRFC5869KeyDerivation { - private final static String PROTOID = "ntor-curve25519-sha256-1"; - private final static String M_EXPAND = PROTOID + ":key_expand"; - private final static byte[] M_EXPAND_BYTES = M_EXPAND.getBytes(Tor.getDefaultCharset()); - - private final byte[] seed; - - public TorRFC5869KeyDerivation(byte[] seed) { - this.seed = new byte[seed.length]; - System.arraycopy(seed, 0, this.seed, 0, seed.length); - } - - public void deriveKeys(byte[] keyMaterialOut, byte[] verifyHashOut) { - final ByteBuffer keyData = deriveKeys(keyMaterialOut.length + verifyHashOut.length); - keyData.get(keyMaterialOut); - keyData.get(verifyHashOut); - } - - public ByteBuffer deriveKeys(int length) { - int round = 1; - final ByteBuffer bb = makeBuffer(length); - byte[] macOutput = null; - while(bb.hasRemaining()) { - macOutput = expandRound(round, macOutput); - if(macOutput.length > bb.remaining()) { - bb.put(macOutput, 0, bb.remaining()); - } else { - bb.put(macOutput); - } - round += 1; - } - bb.flip(); - return bb; - } - - private byte[] expandRound(int round, byte[] priorMac) { - final ByteBuffer bb; - if(round == 1) { - bb = makeBuffer(M_EXPAND_BYTES.length + 1); - } else { - bb = makeBuffer(M_EXPAND_BYTES.length + TorMessageDigest.TOR_DIGEST256_SIZE + 1); - bb.put(priorMac); - } - bb.put(M_EXPAND_BYTES); - bb.put((byte) round); - - final Mac mac = createMacInstance(); - return mac.doFinal(bb.array()); - } - - private ByteBuffer makeBuffer(int len) { - final byte[] bs = new byte[len]; - return ByteBuffer.wrap(bs); - } - - private Mac createMacInstance() { - final SecretKeySpec keyspec = new SecretKeySpec(seed, "HmacSHA256"); - try { - final Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(keyspec); - return mac; - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Could not create HmacSHA256 instance: "+ e); - } catch (InvalidKeyException e) { - throw new IllegalStateException("Could not create HmacSHA256 instance: "+ e); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorRandom.java b/orchid/src/com/subgraph/orchid/crypto/TorRandom.java deleted file mode 100644 index bbe9d07d..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorRandom.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - -import com.subgraph.orchid.TorException; - -public class TorRandom { - - private final SecureRandom random; - - public TorRandom() { - random = createRandom(); - } - - private static SecureRandom createRandom() { - try { - return SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - throw new TorException(e); - } - } - - public byte[] getBytes(int n) { - final byte[] bs = new byte[n]; - random.nextBytes(bs); - return bs; - } - - public long nextLong(long n) { - long bits, val; - do { - bits = nextLong(); - val = bits % n; - } while(bits - val + (n - 1) < 0); - return val; - } - - public int nextInt(int n) { - return random.nextInt(n); - } - - public int nextInt() { - return random.nextInt() & Integer.MAX_VALUE; - } - - /** - * Return a uniformly distributed positive random value between 0 and Long.MAX_VALUE - * - * @return A positive random value between 0 and Long.MAX_VALUE. - */ - public long nextLong() { - return random.nextLong() & Long.MAX_VALUE; - } - -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorSignature.java b/orchid/src/com/subgraph/orchid/crypto/TorSignature.java deleted file mode 100644 index dcdbc49a..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorSignature.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; -import java.util.Arrays; - -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.encoders.Base64; -import com.subgraph.orchid.encoders.Hex; - -public class TorSignature { - private final static String SIGNATURE_BEGIN = "-----BEGIN SIGNATURE-----"; - private final static String ID_SIGNATURE_BEGIN = "-----BEGIN ID SIGNATURE-----"; - private final static String SIGNATURE_END = "-----END SIGNATURE-----"; - private final static String ID_SIGNATURE_END = "-----END ID SIGNATURE-----"; - - static public TorSignature createFromPEMBuffer(String buffer) { - BufferedReader reader = new BufferedReader(new StringReader(buffer)); - final String header = nextLine(reader); - if(!(SIGNATURE_BEGIN.equals(header) || ID_SIGNATURE_BEGIN.equals(header))) - throw new TorParsingException("Did not find expected signature BEGIN header"); - return new TorSignature(Base64.decode(parseBase64Data(reader)), DigestAlgorithm.DIGEST_SHA1); - } - static private String parseBase64Data(BufferedReader reader) { - final StringBuilder base64Data = new StringBuilder(); - while(true) { - final String line = nextLine(reader); - if(SIGNATURE_END.equals(line) || ID_SIGNATURE_END.equals(line)) - return base64Data.toString(); - base64Data.append(line); - } - } - static String nextLine(BufferedReader reader) { - try { - final String line = reader.readLine(); - if(line == null) - throw new TorParsingException("Did not find expected signature END header"); - return line; - } catch (IOException e) { - throw new TorException(e); - } - } - - public enum DigestAlgorithm { DIGEST_SHA1, DIGEST_SHA256 }; - - private final byte[] signatureBytes; - private final DigestAlgorithm digestAlgorithm; - - private TorSignature(byte[] signatureBytes, DigestAlgorithm digestAlgorithm) { - this.signatureBytes = signatureBytes; - this.digestAlgorithm = digestAlgorithm; - } - - - public byte[] getSignatureBytes() { - return Arrays.copyOf(signatureBytes, signatureBytes.length); - } - - public boolean verify(TorPublicKey publicKey, TorMessageDigest digest) { - return publicKey.verifySignature(this, digest); - } - - public DigestAlgorithm getDigestAlgorithm() { - return digestAlgorithm; - } - - public String toString() { - return "TorSignature: (" + signatureBytes.length + " bytes) " + new String(Hex.encode(signatureBytes)); - } - - - -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorStreamCipher.java b/orchid/src/com/subgraph/orchid/crypto/TorStreamCipher.java deleted file mode 100644 index 57093258..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorStreamCipher.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.security.GeneralSecurityException; - -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import com.subgraph.orchid.TorException; - -public class TorStreamCipher { - public static final int KEY_LEN = 16; - - public static TorStreamCipher createWithRandomKey() { - final SecretKey randomKey = generateRandomKey(); - return new TorStreamCipher(randomKey.getEncoded()); - } - - public static TorStreamCipher createFromKeyBytes(byte[] keyBytes) { - return new TorStreamCipher(keyBytes); - } - - public static TorStreamCipher createFromKeyBytesWithIV(byte[] keyBytes, byte[] iv) { - return new TorStreamCipher(keyBytes, iv); - } - - private static final int BLOCK_SIZE = 16; - private final Cipher cipher; - private final byte[] counter; - private final byte[] counterOut; - /* Next byte of keystream in counterOut */ - private int keystreamPointer = -1; - private final SecretKeySpec key; - - - private TorStreamCipher(byte[] keyBytes) { - this(keyBytes, null); - } - - private TorStreamCipher(byte[] keyBytes, byte[] iv) { - key = keyBytesToSecretKey(keyBytes); - cipher = createCipher(key); - counter = new byte[BLOCK_SIZE]; - counterOut = new byte[BLOCK_SIZE]; - - if(iv != null) { - applyIV(iv); - } - } - - private void applyIV(byte[] iv) { - if(iv.length != BLOCK_SIZE) { - throw new IllegalArgumentException(); - } - System.arraycopy(iv, 0, counter, 0, BLOCK_SIZE); - } - - public void encrypt(byte[] data) { - encrypt(data, 0, data.length); - } - - public synchronized void encrypt(byte[] data, int offset, int length) { - for(int i = 0; i < length; i++) - data[i + offset] ^= nextKeystreamByte(); - } - - public byte[] getKeyBytes() { - return key.getEncoded(); - } - - private static SecretKeySpec keyBytesToSecretKey(byte[] keyBytes) { - return new SecretKeySpec(keyBytes, "AES"); - } - - private static Cipher createCipher(SecretKeySpec keySpec) { - try { - final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, keySpec); - return cipher; - } catch (GeneralSecurityException e) { - throw new TorException(e); - } - } - - private static SecretKey generateRandomKey() { - try { - KeyGenerator generator = KeyGenerator.getInstance("AES"); - generator.init(128); - return generator.generateKey(); - } catch (GeneralSecurityException e) { - throw new TorException(e); - } - } - - private byte nextKeystreamByte() { - if(keystreamPointer == -1 || (keystreamPointer >= BLOCK_SIZE)) - updateCounter(); - return counterOut[keystreamPointer++]; - } - private void updateCounter() { - encryptCounter(); - incrementCounter(); - keystreamPointer = 0; - } - - private void encryptCounter() { - try { - cipher.doFinal(counter, 0, BLOCK_SIZE, counterOut, 0); - } catch (GeneralSecurityException e) { - throw new TorException(e); - } - } - - private void incrementCounter() { - int carry = 1; - for(int i = counter.length - 1; i >= 0; i--) { - int x = (counter[i] & 0xff) + carry; - if(x > 0xff) - carry = 1; - else - carry = 0; - counter[i] = (byte)x; - } - } - -} diff --git a/orchid/src/com/subgraph/orchid/crypto/TorTapKeyAgreement.java b/orchid/src/com/subgraph/orchid/crypto/TorTapKeyAgreement.java deleted file mode 100644 index eae5eaef..00000000 --- a/orchid/src/com/subgraph/orchid/crypto/TorTapKeyAgreement.java +++ /dev/null @@ -1,195 +0,0 @@ -package com.subgraph.orchid.crypto; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.PublicKey; -import java.util.Arrays; - -import javax.crypto.KeyAgreement; -import javax.crypto.interfaces.DHPublicKey; -import javax.crypto.spec.DHParameterSpec; -import javax.crypto.spec.DHPublicKeySpec; - -import com.subgraph.orchid.TorException; -/** - * The TorKeyAgreement class implements the diffie-hellman key agreement - * protocol using the parameters specified in the main Tor specification (tor-spec.txt). - * - * An instance of this class can only be used to perform a single key agreement operation. - * - * After instantiating the class, a user calls {@link #getPublicValue()} or {@link #getPublicKeyBytes()} - * to retrieve the public value to transmit to the peer in the key agreement operation. After receiving - * a public value from the peer, this value should be converted into a BigInteger and - * {@link #isValidPublicValue(BigInteger)} should be called to verify that the peer has sent a safe - * and legal public value. If {@link #isValidPublicValue(BigInteger)} returns true, the peer public - * value is valid and {@link #getSharedSecret(BigInteger)} can be called to complete the key agreement - * protocol and return the shared secret value. - * - */ -public class TorTapKeyAgreement implements TorKeyAgreement { - public final static int DH_LEN = 128; - public final static int DH_SEC_LEN = 40; - /* - * tor-spec 0.3 - * - * For Diffie-Hellman, we use a generator (g) of 2. For the modulus (p), we - * use the 1024-bit safe prime from rfc2409 section 6.2 whose hex - * representation is: - */ - private static final BigInteger P1024 = new BigInteger( - "00FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08" - + "8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B" - + "302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9" - + "A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6" - + "49286651ECE65381FFFFFFFFFFFFFFFF", 16); - private static final BigInteger G = new BigInteger("2"); - - /* - * tor-spec 0.3 - * - * As an optimization, implementations SHOULD choose DH private keys (x) of - * 320 bits. - */ - private static final int PRIVATE_KEY_SIZE = 320; - private static final DHParameterSpec DH_PARAMETER_SPEC = new DHParameterSpec(P1024, G, PRIVATE_KEY_SIZE); - - private final KeyAgreement dh; - private final KeyPair keyPair; - private final TorPublicKey onionKey; - - /** - * Create a new TorKeyAgreement instance which can be used to perform a single - * key agreement operation. A new set of ephemeral Diffie-Hellman parameters are generated - * when this class is instantiated. - */ - public TorTapKeyAgreement(TorPublicKey onionKey) { - this.keyPair = generateKeyPair(); - this.dh = createDH(); - this.onionKey = onionKey; - - } - - public TorTapKeyAgreement() { - this(null); - } - - /** - * Return the generated public value for this key agreement operation as a BigInteger. - * - * @return The diffie-hellman public value as a BigInteger. - */ - public BigInteger getPublicValue() { - DHPublicKey pubKey = (DHPublicKey) keyPair.getPublic(); - return pubKey.getY(); - } - - /** - * Return the generated public value for this key agreement operation as an array with the value - * encoded in big-endian byte order. - * - * @return A byte array containing the encoded public value for this key agreement operation. - */ - public byte[] getPublicKeyBytes() { - final byte[] output = new byte[128]; - final byte[] yBytes = getPublicValue().toByteArray(); - if(yBytes[0] == 0 && yBytes.length == (DH_LEN + 1)) { - System.arraycopy(yBytes, 1, output, 0, DH_LEN); - } else if (yBytes.length <= DH_LEN) { - final int offset = DH_LEN - yBytes.length; - System.arraycopy(yBytes, 0, output, offset, yBytes.length); - } else { - throw new IllegalStateException("Public value is longer than DH_LEN but not because of sign bit"); - } - return output; - } - - - - /** - * Return true if the specified value is a legal public - * value rather than a dangerous degenerate or confined subgroup value. - * - * tor-spec 5.2 - * Before computing g^xy, both client and server MUST verify that - * the received g^x or g^y value is not degenerate; that is, it must - * be strictly greater than 1 and strictly less than p-1 where p is - * the DH modulus. Implementations MUST NOT complete a handshake - * with degenerate keys. - */ - public static boolean isValidPublicValue(BigInteger publicValue) { - if(publicValue.signum() < 1 || publicValue.equals(BigInteger.ONE)) - return false; - if(publicValue.compareTo(P1024.subtract(BigInteger.ONE)) >= 0) - return false; - return true; - } - - /** - * Complete the key agreement protocol with the peer public value - * otherPublic and return the calculated shared secret. - * - * @param otherPublic The peer public value. - * @return The shared secret value produced by the protocol. - */ - public byte[] getSharedSecret(BigInteger otherPublic) { - try { - KeyFactory factory = KeyFactory.getInstance("DH"); - DHPublicKeySpec pub = new DHPublicKeySpec(otherPublic, P1024, G); - PublicKey key = factory.generatePublic(pub); - dh.doPhase(key, true); - return dh.generateSecret(); - } catch (GeneralSecurityException e) { - throw new TorException(e); - } - } - private final KeyAgreement createDH() { - try { - KeyAgreement dh = KeyAgreement.getInstance("DH"); - dh.init(keyPair.getPrivate()); - return dh; - } catch (GeneralSecurityException e) { - throw new TorException(e); - } - } - - private final KeyPair generateKeyPair() { - try { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH"); - keyGen.initialize(DH_PARAMETER_SPEC); - return keyGen.generateKeyPair(); - } catch (GeneralSecurityException e) { - throw new TorException(e); - } - } - - public byte[] createOnionSkin() { - final byte[] yBytes = getPublicKeyBytes(); - final HybridEncryption hybrid = new HybridEncryption(); - return hybrid.encrypt(yBytes, onionKey); - } - - public boolean deriveKeysFromHandshakeResponse(byte[] handshakeResponse, - byte[] keyMaterialOut, byte[] verifyHashOut) { - ByteBuffer bb = ByteBuffer.wrap(handshakeResponse); - byte[] dhPublic = new byte[DH_LEN]; - byte[] keyHash = new byte[TorMessageDigest.TOR_DIGEST_SIZE]; - bb.get(dhPublic); - bb.get(keyHash); - BigInteger peerPublic = new BigInteger(1, dhPublic); - return deriveKeysFromDHPublicAndHash(peerPublic, keyHash, keyMaterialOut, verifyHashOut); - } - - public boolean deriveKeysFromDHPublicAndHash(BigInteger peerPublic, byte[] keyHash, byte[] keyMaterialOut, byte[] verifyHashOut) { - if(!isValidPublicValue(peerPublic)) { - throw new TorException("Illegal DH public value"); - } - final byte[] sharedSecret = getSharedSecret(peerPublic); - final TorKeyDerivation kdf = new TorKeyDerivation(sharedSecret); - kdf.deriveKeys(keyMaterialOut, verifyHashOut); - return Arrays.equals(verifyHashOut, keyHash); - } -} diff --git a/orchid/src/com/subgraph/orchid/dashboard/Dashboard.java b/orchid/src/com/subgraph/orchid/dashboard/Dashboard.java deleted file mode 100644 index 10e2c790..00000000 --- a/orchid/src/com/subgraph/orchid/dashboard/Dashboard.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.subgraph.orchid.dashboard; - -import java.io.IOException; -import java.io.PrintWriter; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import com.subgraph.orchid.Threading; -import com.subgraph.orchid.data.IPv4Address; -import com.subgraph.orchid.misc.GuardedBy; - -/** - * A debugging utility which displays continuously updated information about the internal state - * of various components to clients which connect to a network port listening on localhost. - */ -public class Dashboard implements DashboardRenderable, DashboardRenderer { - private final static Logger logger = Logger.getLogger(Dashboard.class.getName()); - - private final static String DASHBOARD_PORT_PROPERTY = "com.subgraph.orchid.dashboard.port"; - - private final static int DEFAULT_LISTENING_PORT = 12345; - private final static int DEFAULT_FLAGS = DASHBOARD_CIRCUITS | DASHBOARD_STREAMS; - private final static IPv4Address LOCALHOST = IPv4Address.createFromString("127.0.0.1"); - - @GuardedBy("this") private int listeningPort; - @GuardedBy("this") private int flags = DEFAULT_FLAGS; - @GuardedBy("this") private ServerSocket listeningSocket; - @GuardedBy("this") private boolean isListening; - - private final List renderables; - private final Executor executor; - - public Dashboard() { - renderables = new CopyOnWriteArrayList(); - renderables.add(this); - executor = Threading.newPool("Dashboard worker"); - listeningPort = chooseListeningPort(); - } - - private static int chooseListeningPort() { - final String dbPort = System.getProperty(DASHBOARD_PORT_PROPERTY); - final int port = parsePortProperty(dbPort); - if(port > 0 && port <= 0xFFFF) { - return port; - } else if(dbPort != null) { - logger.warning(DASHBOARD_PORT_PROPERTY + " was not a valid port value: "+ dbPort); - } - return DEFAULT_LISTENING_PORT; - } - - private static int parsePortProperty(String dbPort) { - if(dbPort == null) { - return -1; - } - try { - return Integer.parseInt(dbPort); - } catch (NumberFormatException e) { - return -1; - } - } - - public void addRenderables(Object...objects) { - for(Object ob: objects) { - if(ob instanceof DashboardRenderable) { - renderables.add((DashboardRenderable) ob); - } - } - } - - public void addRenderable(DashboardRenderable renderable) { - renderables.add(renderable); - } - - public synchronized void enableFlag(int flag) { - flags |= flag; - } - - public synchronized void disableFlag(int flag) { - flags &= ~flag; - } - - - public synchronized boolean isEnabled(int f) { - return (flags & f) != 0; - } - - public synchronized void setListeningPort(int port) { - if(port != listeningPort) { - listeningPort = port; - if(isListening) { - stopListening(); - startListening(); - } - } - } - - public boolean isEnabledByProperty() { - return System.getProperty(DASHBOARD_PORT_PROPERTY) != null; - } - - public synchronized void startListening() { - if(isListening) { - return; - } - try { - listeningSocket = new ServerSocket(listeningPort, 50, LOCALHOST.toInetAddress()); - isListening = true; - logger.info("Dashboard listening on "+ LOCALHOST + ":"+ listeningPort); - executor.execute(createAcceptLoopRunnable(listeningSocket)); - } catch (IOException e) { - logger.warning("Failed to create listening Dashboard socket on port "+ listeningPort +": "+ e); - } - } - - public synchronized void stopListening() { - if(!isListening) { - return; - } - if(listeningSocket != null) { - closeQuietly(listeningSocket); - listeningSocket = null; - } - isListening = false; - } - - public synchronized boolean isListening() { - return isListening; - } - - private Runnable createAcceptLoopRunnable(final ServerSocket ss) { - return new Runnable() { - public void run() { - acceptConnections(ss); - } - }; - } - - private void acceptConnections(ServerSocket ss) { - while(true) { - try { - Socket s = ss.accept(); - executor.execute(new DashboardConnection(this, s)); - } catch (IOException e) { - if(!ss.isClosed()) { - logger.warning("IOException on dashboard server socket: "+ e); - } - stopListening(); - return; - } - } - } - - void renderAll(PrintWriter writer) throws IOException { - final int fs; - synchronized (this) { - fs = flags; - } - - for(DashboardRenderable dr: renderables) { - dr.dashboardRender(this, writer, fs); - } - } - - private void closeQuietly(ServerSocket s) { - try { - s.close(); - } catch (IOException e) { } - } - - public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) { - writer.println("[Dashboard]"); - writer.println(); - } - - public void renderComponent(PrintWriter writer, int flags, Object component) throws IOException { - if(!(component instanceof DashboardRenderable)) { - return; - } - final DashboardRenderable renderable = (DashboardRenderable) component; - renderable.dashboardRender(this, writer, flags); - } -} diff --git a/orchid/src/com/subgraph/orchid/dashboard/DashboardConnection.java b/orchid/src/com/subgraph/orchid/dashboard/DashboardConnection.java deleted file mode 100644 index 3d1baa15..00000000 --- a/orchid/src/com/subgraph/orchid/dashboard/DashboardConnection.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.subgraph.orchid.dashboard; - -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; -import java.io.Writer; -import java.net.Socket; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class DashboardConnection implements Runnable { - - private final static int REFRESH_INTERVAL = 1000; - - private final Dashboard dashboard; - private final Socket socket; - private final ScheduledExecutorService refreshExecutor; - - public DashboardConnection(Dashboard dashboard, Socket socket) { - this.dashboard = dashboard; - this.socket = socket; - this.refreshExecutor = new ScheduledThreadPoolExecutor(1); - } - - public void run() { - ScheduledFuture handle = null; - try { - final PrintWriter writer = new PrintWriter(socket.getOutputStream()); - handle = refreshExecutor.scheduleAtFixedRate(createRefreshRunnable(writer), 0, REFRESH_INTERVAL, TimeUnit.MILLISECONDS); - runInputLoop(socket.getInputStream()); - } catch (IOException e) { - closeQuietly(socket); - } finally { - if(handle != null) { - handle.cancel(true); - } - refreshExecutor.shutdown(); - } - } - - private void closeQuietly(Socket s) { - try { - s.close(); - } catch (IOException e) { } - } - - private void runInputLoop(InputStream input) throws IOException { - int c; - - while((c = input.read()) != -1) { - switch(c) { - case 'c': - toggleFlagWithVerbose(DashboardRenderable.DASHBOARD_CONNECTIONS, DashboardRenderable.DASHBOARD_CONNECTIONS_VERBOSE); - break; - case 'p': - toggleFlag(DashboardRenderable.DASHBOARD_PREDICTED_PORTS); - break; - default: - break; - } - } - } - - // Rotate between 3 states - // 0 (no flags), - // basicFlag, - // basicFlag|verboseFlag - private void toggleFlagWithVerbose(int basicFlag, int verboseFlag) { - if(dashboard.isEnabled(verboseFlag)) { - dashboard.disableFlag(basicFlag | verboseFlag); - } else if(dashboard.isEnabled(basicFlag)) { - dashboard.enableFlag(verboseFlag); - } else { - dashboard.enableFlag(basicFlag); - } - } - - private void toggleFlag(int flag) { - if(dashboard.isEnabled(flag)) { - dashboard.disableFlag(flag); - } else { - dashboard.enableFlag(flag); - } - } - - private void hideCursor(Writer writer) throws IOException { - emitCSI(writer); - writer.write("?25l"); - } - - private void emitCSI(Writer writer) throws IOException { - writer.append((char) 0x1B); - writer.append('['); - } - - private void clear(PrintWriter writer) throws IOException { - emitCSI(writer); - writer.write("2J"); - } - - private void moveTo(PrintWriter writer, int x, int y) throws IOException { - emitCSI(writer); - writer.printf("%d;%dH", x+1, y+1); - } - - private void refresh(PrintWriter writer) { - try { - if(socket.isClosed()) { - return; - } - hideCursor(writer); - clear(writer); - moveTo(writer, 0, 0); - dashboard.renderAll(writer); - writer.flush(); - } catch(IOException e) { - closeQuietly(socket); - } - } - - private Runnable createRefreshRunnable(final PrintWriter writer) { - return new Runnable() { - public void run() { - refresh(writer); - } - }; - } -} diff --git a/orchid/src/com/subgraph/orchid/dashboard/DashboardRenderable.java b/orchid/src/com/subgraph/orchid/dashboard/DashboardRenderable.java deleted file mode 100644 index 30a58b99..00000000 --- a/orchid/src/com/subgraph/orchid/dashboard/DashboardRenderable.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.subgraph.orchid.dashboard; - -import java.io.IOException; -import java.io.PrintWriter; - -public interface DashboardRenderable { - - static int DASHBOARD_CONNECTIONS = 1 << 0; - static int DASHBOARD_CONNECTIONS_VERBOSE = 1 << 1; - static int DASHBOARD_PREDICTED_PORTS = 1 << 2; - static int DASHBOARD_CIRCUITS = 1 << 3; - static int DASHBOARD_STREAMS = 1 << 4; - - void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException; -} diff --git a/orchid/src/com/subgraph/orchid/dashboard/DashboardRenderer.java b/orchid/src/com/subgraph/orchid/dashboard/DashboardRenderer.java deleted file mode 100644 index b14f602a..00000000 --- a/orchid/src/com/subgraph/orchid/dashboard/DashboardRenderer.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.subgraph.orchid.dashboard; - -import java.io.IOException; -import java.io.PrintWriter; - -public interface DashboardRenderer { - void renderComponent(PrintWriter writer, int flags, Object component) throws IOException; -} diff --git a/orchid/src/com/subgraph/orchid/data/BandwidthHistory.java b/orchid/src/com/subgraph/orchid/data/BandwidthHistory.java deleted file mode 100644 index 9f8e4c80..00000000 --- a/orchid/src/com/subgraph/orchid/data/BandwidthHistory.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.subgraph.orchid.data; - -import java.util.ArrayList; -import java.util.List; - -public class BandwidthHistory { - - private final Timestamp reportingTime; - private final int reportingInterval; - private final List samples = new ArrayList(); - - public BandwidthHistory(Timestamp reportingTime, int reportingInterval) { - this.reportingTime = reportingTime; - this.reportingInterval = reportingInterval; - } - - public int getReportingInterval() { - return reportingInterval; - } - - public Timestamp getReportingTime() { - return reportingTime; - } - - public void addSample(int value) { - samples.add(value); - } - -} diff --git a/orchid/src/com/subgraph/orchid/data/Base32.java b/orchid/src/com/subgraph/orchid/data/Base32.java deleted file mode 100644 index f0067eef..00000000 --- a/orchid/src/com/subgraph/orchid/data/Base32.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.subgraph.orchid.data; - -import com.subgraph.orchid.TorException; - -public class Base32 { - private final static String BASE32_CHARS = "abcdefghijklmnopqrstuvwxyz234567"; - - public static String base32Encode(byte[] source) { - return base32Encode(source, 0, source.length); - } - - public static String base32Encode(byte[] source, int offset, int length) { - final int nbits = length * 8; - if(nbits % 5 != 0) - throw new TorException("Base32 input length must be a multiple of 5 bits"); - - final int outlen = nbits / 5; - final StringBuffer outbuffer = new StringBuffer(); - int bit = 0; - for(int i = 0; i < outlen; i++) { - int v = (source[bit / 8] & 0xFF) << 8; - if(bit + 5 < nbits) v += (source[bit / 8 + 1] & 0xFF); - int u = (v >> (11 - (bit % 8))) & 0x1F; - outbuffer.append(BASE32_CHARS.charAt(u)); - bit += 5; - } - return outbuffer.toString(); - } - - public static byte[] base32Decode(String source) { - int[] v = stringToIntVector(source); - - int nbits = source.length() * 5; - if(nbits % 8 != 0) - throw new TorException("Base32 decoded array must be a muliple of 8 bits"); - - int outlen = nbits / 8; - byte[] outbytes = new byte[outlen]; - - int bit = 0; - for(int i = 0; i < outlen; i++) { - int bb = bit / 5; - outbytes[i] = (byte) decodeByte(bit, v[bb], v[bb + 1], v[bb + 2]); - bit += 8; - } - return outbytes; - } - - private static int decodeByte(int bitOffset, int b0, int b1, int b2) { - switch(bitOffset % 40) { - case 0: - return ls(b0, 3) + rs(b1, 2); - case 8: - return ls(b0, 6) + ls(b1, 1) + rs (b2, 4); - case 16: - return ls(b0, 4) + rs(b1, 1); - case 24: - return ls(b0, 7) + ls(b1, 2) + rs(b2, 3); - case 32: - return ls(b0, 5) + (b1 & 0xFF); - } - throw new TorException("Illegal bit offset"); - } - - private static int ls(int n, int shift) { - return ((n << shift) & 0xFF); - } - - private static int rs(int n, int shift) { - return ((n >> shift) & 0xFF); - } - - private static int[] stringToIntVector(String s) { - final int[] ints = new int[s.length() + 1]; - for(int i = 0; i < s.length(); i++) { - int b = s.charAt(i) & 0xFF; - if(b > 0x60 && b < 0x7B) - ints[i] = b - 0x61; - else if(b > 0x31 && b < 0x38) - ints[i] = b - 0x18; - else if(b > 0x40 && b < 0x5B) - ints[i] = b - 0x41; - else - throw new TorException("Illegal character in base32 encoded string: "+ s.charAt(i)); - } - return ints; - } -} diff --git a/orchid/src/com/subgraph/orchid/data/HexDigest.java b/orchid/src/com/subgraph/orchid/data/HexDigest.java deleted file mode 100644 index aedb632f..00000000 --- a/orchid/src/com/subgraph/orchid/data/HexDigest.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.subgraph.orchid.data; - -import java.util.Arrays; -import java.util.List; - -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.crypto.TorMessageDigest; -import com.subgraph.orchid.encoders.Base64; -import com.subgraph.orchid.encoders.Hex; - -/** - * This class represents both digests and fingerprints that appear in directory - * documents. The names fingerprint and digest are used interchangeably in - * the specification but generally a fingerprint is a message digest (ie: SHA1) - * over the DER ASN.1 encoding of a public key. A digest is usually - * a message digest over a set of fields in a directory document. - * - * Digests always appear as a 40 character hex string: - * - * 0EA20CAA3CE696E561BC08B15E00106700E8F682 - * - * Fingerprints may either appear as a single hex string as above or sometimes in - * a more easily human-parsed spaced format: - * - * 1E0F 5874 2268 E82F C600 D81D 9064 07C5 7CC2 C3A7 - * - */ -public class HexDigest { - public static HexDigest createFromStringList(List strings) { - StringBuilder builder = new StringBuilder(); - for(String chunk: strings) - builder.append(chunk); - return createFromString(builder.toString()); - } - - public static HexDigest createFromBase32String(String b32) { - return new HexDigest(Base32.base32Decode(b32)); - } - - public static HexDigest createFromString(String fingerprint) { - final String[] parts = fingerprint.split(" "); - if(parts.length > 1) - return createFromStringList(Arrays.asList(parts)); - final byte[] digestData = Hex.decode(fingerprint); - return new HexDigest(digestData); - } - - public static HexDigest createFromDigestBytes(byte[] data) { - return new HexDigest(data); - } - - public static HexDigest createDigestForData(byte[] data) { - final TorMessageDigest digest = new TorMessageDigest(); - digest.update(data); - return new HexDigest(digest.getDigestBytes()); - } - - private final byte[] digestBytes; - private final boolean isDigest256; - - private HexDigest(byte[] data) { - if(data.length != TorMessageDigest.TOR_DIGEST_SIZE && data.length != TorMessageDigest.TOR_DIGEST256_SIZE) { - throw new TorException("Digest data is not the correct length "+ data.length +" != (" + TorMessageDigest.TOR_DIGEST_SIZE + " or "+ TorMessageDigest.TOR_DIGEST256_SIZE +")"); - } - digestBytes = new byte[data.length]; - isDigest256 = digestBytes.length == TorMessageDigest.TOR_DIGEST256_SIZE; - System.arraycopy(data, 0, digestBytes, 0, data.length); - } - - public boolean isDigest256() { - return isDigest256; - } - - public byte[] getRawBytes() { - return Arrays.copyOf(digestBytes, digestBytes.length); - } - - public String toString() { - return new String(Hex.encode(digestBytes)); - } - - /** - * Return a spaced fingerprint representation of this HexDigest. - * - * ex: - * - * 1E0F 5874 2268 E82F C600 D81D 9064 07C5 7CC2 C3A7 - * - * @return A string representation of this HexDigest in the spaced fingerprint format. - */ - public String toSpacedString() { - final String original = toString(); - final StringBuilder builder = new StringBuilder(); - for(int i = 0; i < original.length(); i++) { - if(i > 0 && (i % 4) == 0) - builder.append(' '); - builder.append(original.charAt(i)); - } - return builder.toString(); - } - - public String toBase32() { - return Base32.base32Encode(digestBytes); - } - - public String toBase64(boolean stripTrailingEquals) { - final String b64 = new String(Base64.encode(digestBytes), Tor.getDefaultCharset()); - if(stripTrailingEquals) { - return stripTrailingEquals(b64); - } else { - return b64; - } - } - - private String stripTrailingEquals(String s) { - int idx = s.length(); - while(idx > 0 && s.charAt(idx - 1) == '=') { - idx -= 1; - } - return s.substring(0, idx); - } - - public boolean equals(Object o) { - if(!(o instanceof HexDigest)) - return false; - final HexDigest other = (HexDigest)o; - return Arrays.equals(other.digestBytes, this.digestBytes); - } - - public int hashCode() { - int hash = 0; - for(int i = 0; i < 4; i++) { - hash <<= 8; - hash |= (digestBytes[i] & 0xFF); - } - return hash; - } - -} diff --git a/orchid/src/com/subgraph/orchid/data/IPv4Address.java b/orchid/src/com/subgraph/orchid/data/IPv4Address.java deleted file mode 100644 index 77151457..00000000 --- a/orchid/src/com/subgraph/orchid/data/IPv4Address.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.subgraph.orchid.data; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.TorParsingException; - -public class IPv4Address { - - public static IPv4Address createFromString(String addressString) { - return new IPv4Address(parseStringToAddressData(addressString)); - } - - private static int parseStringToAddressData(String ipString) { - final String[] octets = ipString.split("\\."); - final int[] shifts = {24, 16, 8, 0}; - int addressData = 0; - int i = 0; - for(String o: octets) - addressData |= (octetStringToInt(o) << shifts[i++]); - - return addressData; - } - - private static int octetStringToInt(String octet) { - try { - int result = Integer.parseInt(octet); - if(result < 0 || result > 255) - throw new TorParsingException("Octet out of range: " + octet); - return result; - } catch(NumberFormatException e) { - throw new TorParsingException("Failed to parse octet: " + octet); - } - } - - public static boolean isValidIPv4AddressString(String addressString) { - try { - createFromString(addressString); - return true; - } catch (Exception e) { - return false; - } - } - - private final int addressData; - - public IPv4Address(int addressData) { - this.addressData = addressData; - - } - public int getAddressData() { - return addressData; - } - - public byte[] getAddressDataBytes() { - final byte[] result = new byte[4]; - result[0] = (byte)((addressData >> 24) & 0xFF); - result[1] = (byte)((addressData >> 16) & 0xFF); - result[2] = (byte)((addressData >> 8) & 0xFF); - result[3] = (byte)(addressData & 0xFF); - return result; - } - - public InetAddress toInetAddress() { - try { - return InetAddress.getByAddress(getAddressDataBytes()); - } catch (UnknownHostException e) { - throw new TorException(e); - } - } - - public static String stringFormat(int addressData) { - return ((addressData >> 24) & 0xFF) +"."+ - ((addressData >> 16) & 0xFF) +"."+ - ((addressData >> 8) & 0xFF) +"."+ - (addressData & 0xFF); - } - - public String toString() { - return stringFormat(addressData); - } - - public boolean equals(Object ob) { - if(this == ob) - return true; - if(!(ob instanceof IPv4Address)) - return false; - IPv4Address other = (IPv4Address)ob; - return (other.addressData == addressData); - } - - public int hashCode() { - int n = 0; - for(int i = 0; i < 4; i++) { - n <<= 4; - n ^= ((addressData >> (i * 8)) & 0xFF); - } - return n; - } - -} diff --git a/orchid/src/com/subgraph/orchid/data/RandomSet.java b/orchid/src/com/subgraph/orchid/data/RandomSet.java deleted file mode 100644 index 990d21c8..00000000 --- a/orchid/src/com/subgraph/orchid/data/RandomSet.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.subgraph.orchid.data; - -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.subgraph.orchid.TorException; - -public class RandomSet { - - private final Set set; - private final List list; - private final SecureRandom random; - - public RandomSet() { - set = new HashSet(); - list = new ArrayList(); - random = createRandom(); - } - - private static SecureRandom createRandom() { - try { - return SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - throw new TorException(e); - } - } - - public boolean add(E o) { - if(set.add(o)) { - list.add(o); - return true; - } else { - return false; - } - } - - public boolean contains(Object o) { - return set.contains(o); - } - - public boolean isEmpty() { - return set.isEmpty(); - } - - public void clear() { - set.clear(); - list.clear(); - } - - public boolean remove(Object o) { - if(set.remove(o)) { - list.remove(o); - return true; - } else { - return false; - } - } - - public int size() { - return set.size(); - } - - public E getRandomElement() { - int idx = random.nextInt(list.size()); - return list.get(idx); - } - -} diff --git a/orchid/src/com/subgraph/orchid/data/Timestamp.java b/orchid/src/com/subgraph/orchid/data/Timestamp.java deleted file mode 100644 index c8272d24..00000000 --- a/orchid/src/com/subgraph/orchid/data/Timestamp.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.subgraph.orchid.data; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; - -import com.subgraph.orchid.TorParsingException; - -public class Timestamp { - private final Date date; - - public static Timestamp createFromDateAndTimeString(String dateAndTime) { - final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - format.setLenient(false); - try { - Timestamp ts = new Timestamp(format.parse(dateAndTime)); - return ts; - } catch (ParseException e) { - throw new TorParsingException("Could not parse timestamp string: "+ dateAndTime); - } - } - - public Timestamp(Date date) { - this.date = date; - } - - public long getTime() { - return date.getTime(); - } - - public Date getDate() { - return new Date(date.getTime()); - } - - public boolean hasPassed() { - final Date now = new Date(); - return date.before(now); - } - - public boolean isBefore(Timestamp ts) { - return date.before(ts.getDate()); - } - - public String toString() { - return date.toString(); - } - -} diff --git a/orchid/src/com/subgraph/orchid/data/exitpolicy/ExitPolicy.java b/orchid/src/com/subgraph/orchid/data/exitpolicy/ExitPolicy.java deleted file mode 100644 index 2a0f0651..00000000 --- a/orchid/src/com/subgraph/orchid/data/exitpolicy/ExitPolicy.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.subgraph.orchid.data.exitpolicy; - -import java.util.ArrayList; -import java.util.List; - -import com.subgraph.orchid.data.IPv4Address; - -public class ExitPolicy { - private final List rules = new ArrayList(); - - public void addAcceptRule(String rule) { - rules.add(PolicyRule.createAcceptFromString(rule)); - } - - public void addRejectRule(String rule) { - rules.add(PolicyRule.createRejectFromString(rule)); - } - - public boolean acceptsTarget(ExitTarget target) { - if(target.isAddressTarget()) - return acceptsDestination(target.getAddress(), target.getPort()); - else - return acceptsPort(target.getPort()); - } - - public boolean acceptsDestination(IPv4Address address, int port) { - if(address == null) - return acceptsPort(port); - - for(PolicyRule r: rules) { - if(r.matchesDestination(address, port)) - return r.isAcceptRule(); - } - // Default accept (see dir-spec.txt section 2.1, 'accept'/'reject' keywords) - return true; - } - - public boolean acceptsPort(int port) { - for(PolicyRule r: rules) { - if(r.matchesPort(port)) - return r.isAcceptRule(); - } - return false; - } - - public String toString() { - final StringBuilder sb = new StringBuilder(); - for(PolicyRule r: rules) { - sb.append(r); - sb.append("\n"); - } - return sb.toString(); - } -} diff --git a/orchid/src/com/subgraph/orchid/data/exitpolicy/ExitPorts.java b/orchid/src/com/subgraph/orchid/data/exitpolicy/ExitPorts.java deleted file mode 100644 index 9c8926bd..00000000 --- a/orchid/src/com/subgraph/orchid/data/exitpolicy/ExitPorts.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.subgraph.orchid.data.exitpolicy; - -import java.util.ArrayList; -import java.util.List; - - -/** - * Used by router status entries in consensus documents - */ -public class ExitPorts { - public static ExitPorts createAcceptExitPorts(String ports) { - final ExitPorts exitPorts = new ExitPorts(true); - exitPorts.parsePortRanges(ports); - return exitPorts; - } - - public static ExitPorts createRejectExitPorts(String ports) { - final ExitPorts exitPorts = new ExitPorts(false); - exitPorts.parsePortRanges(ports); - return exitPorts; - } - - private final List ranges = new ArrayList(); - private final boolean areAcceptPorts; - - private ExitPorts(boolean acceptPorts) { - this.areAcceptPorts = acceptPorts; - } - - public boolean areAcceptPorts() { - return areAcceptPorts; - } - - public boolean acceptsPort(int port) { - if(areAcceptPorts) - return contains(port); - else - return !contains(port); - } - public boolean contains(int port) { - for(PortRange r: ranges) - if(r.rangeContains(port)) - return true; - return false; - } - - private void parsePortRanges(String portRanges) { - final String[] args = portRanges.split(","); - for(String arg: args) - ranges.add(PortRange.createFromString(arg)); - } - - -} diff --git a/orchid/src/com/subgraph/orchid/data/exitpolicy/ExitTarget.java b/orchid/src/com/subgraph/orchid/data/exitpolicy/ExitTarget.java deleted file mode 100644 index de9a7077..00000000 --- a/orchid/src/com/subgraph/orchid/data/exitpolicy/ExitTarget.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.subgraph.orchid.data.exitpolicy; - -import com.subgraph.orchid.data.IPv4Address; - -public interface ExitTarget { - boolean isAddressTarget(); - IPv4Address getAddress(); - String getHostname(); - int getPort(); -} diff --git a/orchid/src/com/subgraph/orchid/data/exitpolicy/Network.java b/orchid/src/com/subgraph/orchid/data/exitpolicy/Network.java deleted file mode 100644 index a13c669e..00000000 --- a/orchid/src/com/subgraph/orchid/data/exitpolicy/Network.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.subgraph.orchid.data.exitpolicy; - -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.data.IPv4Address; - -public class Network { - public static final Network ALL_ADDRESSES = new Network(IPv4Address.createFromString("0.0.0.0"), 0, "*"); - public static Network createFromString(String networkString) { - final String[] parts = networkString.split("/"); - final IPv4Address network = IPv4Address.createFromString(parts[0]); - if(parts.length == 1) - return new Network(network, 32, networkString); - - if(parts.length != 2) - throw new TorParsingException("Invalid network CIDR notation: " + networkString); - - try { - final int maskBits = Integer.parseInt(parts[1]); - return new Network(network, maskBits, networkString); - } catch(NumberFormatException e) { - throw new TorParsingException("Invalid netblock mask bit value: " + parts[1]); - } - } - - private final IPv4Address network; - private final int maskValue; - private final String originalString; - - Network(IPv4Address network, int bits, String originalString) { - this.network = network; - this.maskValue = createMask(bits); - this.originalString = originalString; - } - - private static int createMask(int maskBits) { - return maskBits == 0 ? 0 : (1 << 31) >> (maskBits - 1); - } - - public boolean contains(IPv4Address address) { - return (address.getAddressData() & maskValue) == (network.getAddressData() & maskValue); - } - - public String toString() { - return originalString; - } - -} diff --git a/orchid/src/com/subgraph/orchid/data/exitpolicy/PolicyRule.java b/orchid/src/com/subgraph/orchid/data/exitpolicy/PolicyRule.java deleted file mode 100644 index 94768acb..00000000 --- a/orchid/src/com/subgraph/orchid/data/exitpolicy/PolicyRule.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.subgraph.orchid.data.exitpolicy; - -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.data.IPv4Address; - -public class PolicyRule { - private final static String WILDCARD = "*"; - - public static PolicyRule createAcceptFromString(String rule) { - return createRule(rule, true); - } - - public static PolicyRule createRejectFromString(String rule) { - return createRule(rule, false); - } - - private static PolicyRule createRule(String rule, boolean isAccept) { - final String[] args = rule.split(":"); - if(args.length != 2) - throw new TorParsingException("Could not parse exit policy rule: "+ rule); - - return new PolicyRule(parseNetwork(args[0]), parsePortRange(args[1]), isAccept); - } - - private static Network parseNetwork(String network) { - if(network.equals(WILDCARD)) - return Network.ALL_ADDRESSES; - else - return Network.createFromString(network); - } - - private static PortRange parsePortRange(String portRange) { - if(portRange.equals(WILDCARD)) - return PortRange.ALL_PORTS; - else - return PortRange.createFromString(portRange); - } - - private final boolean isAcceptRule; - private final Network network; - private final PortRange portRange; - - private PolicyRule(Network network, PortRange portRange, boolean isAccept) { - this.network = network; - this.portRange = portRange; - this.isAcceptRule = isAccept; - } - - public boolean matchesPort(int port) { - if(!network.equals(Network.ALL_ADDRESSES)) - return false; - return portRange.rangeContains(port); - } - - public boolean matchesDestination(IPv4Address address, int port) { - if(!network.contains(address)) - return false; - return portRange.rangeContains(port); - } - - public boolean isAcceptRule() { - return isAcceptRule; - } - - public String toString() { - final String keyword = isAcceptRule ? "accept" : "reject"; - return keyword + " "+ network + ":"+ portRange; - } -} diff --git a/orchid/src/com/subgraph/orchid/data/exitpolicy/PortRange.java b/orchid/src/com/subgraph/orchid/data/exitpolicy/PortRange.java deleted file mode 100644 index 67347204..00000000 --- a/orchid/src/com/subgraph/orchid/data/exitpolicy/PortRange.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.subgraph.orchid.data.exitpolicy; - -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.TorParsingException; - -public class PortRange { - - public static PortRange createFromString(String ports) { - final String[] parts = ports.split("-"); - if(parts.length == 1) { - return new PortRange(stringToPort(parts[0])); - } else if(parts.length == 2) { - return new PortRange(stringToPort(parts[0]), stringToPort(parts[1])); - } else { - throw new TorParsingException("Could not parse port range from string: " + ports); - } - } - - private static int stringToPort(String port) { - try { - final int portValue = Integer.parseInt(port); - if(!isValidPort(portValue)) - throw new TorParsingException("Illegal port value: "+ port); - return portValue; - } catch(NumberFormatException e) { - throw new TorParsingException("Could not parse port value: "+ port); - } - } - private final static int MAX_PORT = 0xFFFF; - public final static PortRange ALL_PORTS = new PortRange(1,MAX_PORT); - private final int portStart; - private final int portEnd; - - PortRange(int portValue) { - this(portValue, portValue); - } - - PortRange(int start, int end) { - if(!isValidRange(start, end)) - throw new TorException("Invalid port range: "+ start +"-"+ end); - portStart = start; - portEnd = end; - } - - static private boolean isValidRange(int start, int end) { - if(!(isValidPort(start) && isValidPort(end))) - return false; - else if(start > end) - return false; - else - return true; - } - - static private boolean isValidPort(int port) { - return port >= 0 && port <= MAX_PORT; - } - - public boolean rangeContains(int port) { - return port >= portStart && port <= portEnd; - } - - public String toString() { - if(portStart == 1 && portEnd == MAX_PORT) { - return "*"; - } else if(portStart == portEnd) { - return Integer.toString(portStart); - } else { - return Integer.toString(portStart) + "-" + Integer.toString(portEnd); - } - } - -} diff --git a/orchid/src/com/subgraph/orchid/directory/DescriptorCache.java b/orchid/src/com/subgraph/orchid/directory/DescriptorCache.java deleted file mode 100644 index 7a72e3fd..00000000 --- a/orchid/src/com/subgraph/orchid/directory/DescriptorCache.java +++ /dev/null @@ -1,206 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - -import com.subgraph.orchid.Descriptor; -import com.subgraph.orchid.DirectoryStore; -import com.subgraph.orchid.DirectoryStore.CacheFile; -import com.subgraph.orchid.Threading; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.directory.parsing.DocumentParser; -import com.subgraph.orchid.directory.parsing.DocumentParsingResult; -import com.subgraph.orchid.misc.GuardedBy; - -public abstract class DescriptorCache { - private final static Logger logger = Logger.getLogger(DescriptorCache.class.getName()); - - private final DescriptorCacheData data; - - private final DirectoryStore store; - private final ScheduledExecutorService rebuildExecutor = - Threading.newScheduledPool("DescriptorCache rebuild worker"); - - private final CacheFile cacheFile; - private final CacheFile journalFile; - - @GuardedBy("this") - private int droppedBytes; - - @GuardedBy("this") - private int journalLength; - - @GuardedBy("this") - private int cacheLength; - - @GuardedBy("this") - private boolean initiallyLoaded; - - DescriptorCache(DirectoryStore store, CacheFile cacheFile, CacheFile journalFile) { - this.data = new DescriptorCacheData(); - this.store = store; - this.cacheFile = cacheFile; - this.journalFile = journalFile; - startRebuildTask(); - } - - public synchronized void initialLoad() { - if(initiallyLoaded) { - return; - } - reloadCache(); - } - - public void shutdown() { - rebuildExecutor.shutdownNow(); - } - - public T getDescriptor(HexDigest digest) { - return data.findByDigest(digest); - } - - public synchronized void addDescriptors(List descriptors) { - final List journalDescriptors = new ArrayList(); - int duplicateCount = 0; - for(T d: descriptors) { - if(data.addDescriptor(d)) { - if(d.getCacheLocation() == Descriptor.CacheLocation.NOT_CACHED) { - journalLength += d.getBodyLength(); - journalDescriptors.add(d); - } - } else { - duplicateCount += 1; - } - } - - if(!journalDescriptors.isEmpty()) { - store.appendDocumentList(journalFile, journalDescriptors); - } - if(duplicateCount > 0) { - logger.info("Duplicate descriptors added to journal, count = "+ duplicateCount); - } - } - - public void addDescriptor(T d) { - final List descriptors = new ArrayList(); - descriptors.add(d); - addDescriptors(descriptors); - } - - private synchronized void clearMemoryCache() { - data.clear(); - journalLength = 0; - cacheLength = 0; - droppedBytes = 0; - } - - private synchronized void reloadCache() { - clearMemoryCache(); - final ByteBuffer[] buffers = loadCacheBuffers(); - loadCacheFileBuffer(buffers[0]); - loadJournalFileBuffer(buffers[1]); - if(!initiallyLoaded) { - initiallyLoaded = true; - } - } - - private ByteBuffer[] loadCacheBuffers() { - synchronized (store) { - final ByteBuffer[] buffers = new ByteBuffer[2]; - buffers[0] = store.loadCacheFile(cacheFile); - buffers[1] = store.loadCacheFile(journalFile); - return buffers; - } - } - - private void loadCacheFileBuffer(ByteBuffer buffer) { - cacheLength = buffer.limit(); - if(cacheLength == 0) { - return; - } - final DocumentParser parser = createDocumentParser(buffer); - final DocumentParsingResult result = parser.parse(); - if(result.isOkay()) { - for(T d: result.getParsedDocuments()) { - d.setCacheLocation(Descriptor.CacheLocation.CACHED_CACHEFILE); - data.addDescriptor(d); - } - } - - } - - private void loadJournalFileBuffer(ByteBuffer buffer) { - journalLength = buffer.limit(); - if(journalLength == 0) { - return; - } - final DocumentParser parser = createDocumentParser(buffer); - final DocumentParsingResult result = parser.parse(); - if(result.isOkay()) { - int duplicateCount = 0; - logger.fine("Loaded "+ result.getParsedDocuments().size() + " descriptors from journal"); - for(T d: result.getParsedDocuments()) { - d.setCacheLocation(Descriptor.CacheLocation.CACHED_JOURNAL); - if(!data.addDescriptor(d)) { - duplicateCount += 1; - } - } - if(duplicateCount > 0) { - logger.info("Found "+ duplicateCount + " duplicate descriptors in journal file"); - } - } else if(result.isInvalid()) { - logger.warning("Invalid descriptor data parsing from journal file : "+ result.getMessage()); - } else if(result.isError()) { - logger.warning("Error parsing descriptors from journal file : "+ result.getMessage()); - } - } - - abstract protected DocumentParser createDocumentParser(ByteBuffer buffer); - - private ScheduledFuture startRebuildTask() { - return rebuildExecutor.scheduleAtFixedRate(new Runnable() { - public void run() { - maybeRebuildCache(); - } - }, 5, 30, TimeUnit.MINUTES); - } - - private synchronized void maybeRebuildCache() { - if(!initiallyLoaded) { - return; - } - - droppedBytes += data.cleanExpired(); - - if(!shouldRebuildCache()) { - return; - } - rebuildCache(); - } - - private boolean shouldRebuildCache() { - if(journalLength < 16384) { - return false; - } - if(droppedBytes > (journalLength + cacheLength) / 3) { - return true; - } - if(journalLength > (cacheLength / 2)) { - return true; - } - return false; - } - - private void rebuildCache() { - synchronized(store) { - store.writeDocumentList(cacheFile, data.getAllDescriptors()); - store.removeCacheFile(journalFile); - } - reloadCache(); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/DescriptorCacheData.java b/orchid/src/com/subgraph/orchid/directory/DescriptorCacheData.java deleted file mode 100644 index cfb65145..00000000 --- a/orchid/src/com/subgraph/orchid/directory/DescriptorCacheData.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.subgraph.orchid.Descriptor; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.misc.GuardedBy; - - - -public class DescriptorCacheData { - - /** 7 days */ - private final static long EXPIRY_PERIOD = 7 * 24 * 60 * 60 * 1000; - - @GuardedBy("this") - private final Map descriptorMap; - - @GuardedBy("this") - private final List allDescriptors; - - public DescriptorCacheData() { - this.descriptorMap = new HashMap(); - this.allDescriptors = new ArrayList(); - } - - synchronized T findByDigest(HexDigest digest) { - return descriptorMap.get(digest); - } - - synchronized List getAllDescriptors() { - return new ArrayList(allDescriptors); - } - - synchronized boolean addDescriptor(T d) { - if(descriptorMap.containsKey(d.getDescriptorDigest())) { - return false; - } - descriptorMap.put(d.getDescriptorDigest(), d); - allDescriptors.add(d); - return true; - } - - synchronized void clear() { - descriptorMap.clear(); - allDescriptors.clear(); - } - - synchronized int cleanExpired() { - final Set expired = getExpiredSet(); - - if(expired.isEmpty()) { - return 0; - } - - clear(); - int dropped = 0; - for(T d: allDescriptors) { - if(expired.contains(d)) { - dropped += d.getBodyLength(); - } else { - addDescriptor(d); - } - } - - return dropped; - } - - private Set getExpiredSet() { - final long now = System.currentTimeMillis(); - final Set expired = new HashSet(); - for(T d: allDescriptors) { - if(isExpired(d, now)) { - expired.add(d); - } - } - return expired; - } - - private boolean isExpired(T d, long now) { - return d.getLastListed() != 0 && d.getLastListed() < (now - EXPIRY_PERIOD); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/DirectoryAuthorityStatus.java b/orchid/src/com/subgraph/orchid/directory/DirectoryAuthorityStatus.java deleted file mode 100644 index cf9c08a3..00000000 --- a/orchid/src/com/subgraph/orchid/directory/DirectoryAuthorityStatus.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.util.HashSet; -import java.util.Set; - -import com.subgraph.orchid.RouterStatus; -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 class DirectoryAuthorityStatus implements RouterStatus { - - private String nickname; - private HexDigest identity; - private IPv4Address address; - private int routerPort; - private int directoryPort; - private Set flags = new HashSet(); - private HexDigest v3Ident; - - void setV1Authority() { } - void setHiddenServiceAuthority() { addFlag("HSDir"); } - void unsetHiddenServiceAuthority() { flags.remove("HSDir"); } - void setBridgeAuthority() { } - void unsetV2Authority() { flags.remove("V2Dir"); } - void setNickname(String name) { nickname = name; } - void setIdentity(HexDigest identity) { this.identity = identity; } - void setAddress(IPv4Address address) { this.address = address; } - void setRouterPort(int port) { this.routerPort = port; } - void setDirectoryPort(int port) { this.directoryPort = port; } - void addFlag(String flag) { this.flags.add(flag); } - void setV3Ident(HexDigest v3Ident) { this.v3Ident = v3Ident; } - - DirectoryAuthorityStatus() { - addFlag("Authority"); - addFlag("V2Dir"); - } - - public IPv4Address getAddress() { - return address; - } - - public HexDigest getDescriptorDigest() { - return null; - } - - public int getDirectoryPort() { - return directoryPort; - } - - public int getEstimatedBandwidth() { - return 0; - } - - public ExitPorts getExitPorts() { - return null; - } - - public HexDigest getIdentity() { - return identity; - } - - public boolean hasBandwidth() { - return false; - } - - public int getMeasuredBandwidth() { - return 0; - } - - public String getNickname() { - return nickname; - } - - public Timestamp getPublicationTime() { - return null; - } - - public int getRouterPort() { - return routerPort; - } - - public String getVersion() { - return null; - } - - public boolean hasFlag(String flag) { - return flags.contains(flag); - } - - public boolean isDirectory() { - return true; - } - - HexDigest getV3Ident() { - return v3Ident; - } - public HexDigest getMicrodescriptorDigest() { - return null; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/DirectoryImpl.java b/orchid/src/com/subgraph/orchid/directory/DirectoryImpl.java deleted file mode 100644 index 9bcccbe7..00000000 --- a/orchid/src/com/subgraph/orchid/directory/DirectoryImpl.java +++ /dev/null @@ -1,513 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.ConsensusDocument.ConsensusFlavor; -import com.subgraph.orchid.ConsensusDocument.RequiredCertificate; -import com.subgraph.orchid.Descriptor; -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.DirectoryServer; -import com.subgraph.orchid.DirectoryStore; -import com.subgraph.orchid.DirectoryStore.CacheFile; -import com.subgraph.orchid.GuardEntry; -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.RouterMicrodescriptor; -import com.subgraph.orchid.RouterStatus; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.TorConfig.AutoBoolValue; -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.crypto.TorRandom; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.RandomSet; -import com.subgraph.orchid.directory.parsing.DocumentParser; -import com.subgraph.orchid.directory.parsing.DocumentParserFactory; -import com.subgraph.orchid.directory.parsing.DocumentParsingResult; -import com.subgraph.orchid.events.Event; -import com.subgraph.orchid.events.EventHandler; -import com.subgraph.orchid.events.EventManager; - -public class DirectoryImpl implements Directory { - private final static Logger logger = Logger.getLogger(DirectoryImpl.class.getName()); - - private final Object loadLock = new Object(); - private boolean isLoaded = false; - - private final DirectoryStore store; - private final TorConfig config; - private final StateFile stateFile; - private final DescriptorCache microdescriptorCache; - private final DescriptorCache basicDescriptorCache; - - private final Map routersByIdentity; - private final Map routersByNickname; - private final RandomSet directoryCaches; - private final Set requiredCertificates; - private boolean haveMinimumRouterInfo; - private boolean needRecalculateMinimumRouterInfo; - private final EventManager consensusChangedManager; - private final TorRandom random; - private final static DocumentParserFactory parserFactory = new DocumentParserFactoryImpl(); - - private ConsensusDocument currentConsensus; - private ConsensusDocument consensusWaitingForCertificates; - - public DirectoryImpl(TorConfig config, DirectoryStore customDirectoryStore) { - store = (customDirectoryStore == null) ? (new DirectoryStoreImpl(config)) : (customDirectoryStore); - this.config = config; - stateFile = new StateFile(store, this); - microdescriptorCache = createMicrodescriptorCache(store); - basicDescriptorCache = createBasicDescriptorCache(store); - routersByIdentity = new HashMap(); - routersByNickname = new HashMap(); - directoryCaches = new RandomSet(); - requiredCertificates = new HashSet(); - consensusChangedManager = new EventManager(); - random = new TorRandom(); - } - - private static DescriptorCache createMicrodescriptorCache(DirectoryStore store) { - return new DescriptorCache(store, CacheFile.MICRODESCRIPTOR_CACHE, CacheFile.MICRODESCRIPTOR_JOURNAL) { - @Override - protected DocumentParser createDocumentParser(ByteBuffer buffer) { - return parserFactory.createRouterMicrodescriptorParser(buffer); - } - }; - } - - private static DescriptorCache createBasicDescriptorCache(DirectoryStore store) { - return new DescriptorCache(store, CacheFile.DESCRIPTOR_CACHE, CacheFile.DESCRIPTOR_JOURNAL) { - @Override - protected DocumentParser createDocumentParser(ByteBuffer buffer) { - return parserFactory.createRouterDescriptorParser(buffer, false); - } - }; - } - - public synchronized boolean haveMinimumRouterInfo() { - if(needRecalculateMinimumRouterInfo) { - checkMinimumRouterInfo(); - } - return haveMinimumRouterInfo; - } - - private synchronized void checkMinimumRouterInfo() { - if(currentConsensus == null || !currentConsensus.isLive()) { - needRecalculateMinimumRouterInfo = true; - haveMinimumRouterInfo = false; - return; - } - - int routerCount = 0; - int descriptorCount = 0; - for(Router r: routersByIdentity.values()) { - routerCount++; - if(!r.isDescriptorDownloadable()) - descriptorCount++; - } - needRecalculateMinimumRouterInfo = false; - haveMinimumRouterInfo = (descriptorCount * 4 > routerCount); - } - - public void loadFromStore() { - logger.info("Loading cached network information from disk"); - - synchronized(loadLock) { - if(isLoaded) { - return; - } - boolean useMicrodescriptors = config.getUseMicrodescriptors() != AutoBoolValue.FALSE; - last = System.currentTimeMillis(); - logger.info("Loading certificates"); - loadCertificates(store.loadCacheFile(CacheFile.CERTIFICATES)); - logElapsed(); - - logger.info("Loading consensus"); - loadConsensus(store.loadCacheFile(useMicrodescriptors ? CacheFile.CONSENSUS_MICRODESC : CacheFile.CONSENSUS)); - logElapsed(); - - if(!useMicrodescriptors) { - logger.info("Loading descriptors"); - basicDescriptorCache.initialLoad(); - } else { - logger.info("Loading microdescriptor cache"); - microdescriptorCache.initialLoad(); - } - needRecalculateMinimumRouterInfo = true; - logElapsed(); - - logger.info("loading state file"); - stateFile.parseBuffer(store.loadCacheFile(CacheFile.STATE)); - logElapsed(); - - isLoaded = true; - loadLock.notifyAll(); - } - } - - public void close() { - basicDescriptorCache.shutdown(); - microdescriptorCache.shutdown(); - } - - private long last = 0; - private void logElapsed() { - final long now = System.currentTimeMillis(); - final long elapsed = now - last; - last = now; - logger.fine("Loaded in "+ elapsed + " ms."); - } - - private void loadCertificates(ByteBuffer buffer) { - final DocumentParser parser = parserFactory.createKeyCertificateParser(buffer); - final DocumentParsingResult result = parser.parse(); - if(testResult(result, "certificates")) { - for(KeyCertificate cert: result.getParsedDocuments()) { - addCertificate(cert); - } - } - } - - private void loadConsensus(ByteBuffer buffer) { - final DocumentParser parser = parserFactory.createConsensusDocumentParser(buffer); - final DocumentParsingResult result = parser.parse(); - if(testResult(result, "consensus")) { - addConsensusDocument(result.getDocument(), true); - } - } - - private boolean testResult(DocumentParsingResult result, String type) { - if(result.isOkay()) { - return true; - } else if(result.isError()) { - logger.warning("Parsing error loading "+ type + " : "+ result.getMessage()); - } else if(result.isInvalid()) { - logger.warning("Problem loading "+ type + " : "+ result.getMessage()); - } else { - logger.warning("Unknown problem loading "+ type); - } - return false; - } - - public void waitUntilLoaded() { - synchronized (loadLock) { - while(!isLoaded) { - try { - loadLock.wait(); - } catch (InterruptedException e) { - logger.warning("Thread interrupted while waiting for directory to load from disk"); - } - } - } - } - - public Collection getDirectoryAuthorities() { - return TrustedAuthorities.getInstance().getAuthorityServers(); - } - - public DirectoryServer getRandomDirectoryAuthority() { - final List servers = TrustedAuthorities.getInstance().getAuthorityServers(); - final int idx = random.nextInt(servers.size()); - return servers.get(idx); - } - - public Set getRequiredCertificates() { - return new HashSet(requiredCertificates); - } - - public void addCertificate(KeyCertificate certificate) { - synchronized(TrustedAuthorities.getInstance()) { - final boolean wasRequired = removeRequiredCertificate(certificate); - final DirectoryServer as = TrustedAuthorities.getInstance().getAuthorityServerByIdentity(certificate.getAuthorityFingerprint()); - if(as == null) { - logger.warning("Certificate read for unknown directory authority with identity: "+ certificate.getAuthorityFingerprint()); - return; - } - as.addCertificate(certificate); - - if(consensusWaitingForCertificates != null && wasRequired) { - - switch(consensusWaitingForCertificates.verifySignatures()) { - case STATUS_FAILED: - consensusWaitingForCertificates = null; - return; - - case STATUS_VERIFIED: - addConsensusDocument(consensusWaitingForCertificates, false); - consensusWaitingForCertificates = null; - return; - - case STATUS_NEED_CERTS: - requiredCertificates.addAll(consensusWaitingForCertificates.getRequiredCertificates()); - return; - } - } - } - } - - private boolean removeRequiredCertificate(KeyCertificate certificate) { - final Iterator it = requiredCertificates.iterator(); - while(it.hasNext()) { - RequiredCertificate r = it.next(); - if(r.getSigningKey().equals(certificate.getAuthoritySigningKey().getFingerprint())) { - it.remove(); - return true; - } - } - return false; - } - - public void storeCertificates() { - synchronized(TrustedAuthorities.getInstance()) { - final List certs = new ArrayList(); - for(DirectoryServer ds: TrustedAuthorities.getInstance().getAuthorityServers()) { - certs.addAll(ds.getCertificates()); - } - store.writeDocumentList(CacheFile.CERTIFICATES, certs); - } - } - - public void addRouterDescriptors(List descriptors) { - basicDescriptorCache.addDescriptors(descriptors); - needRecalculateMinimumRouterInfo = true; - } - - public synchronized void addConsensusDocument(ConsensusDocument consensus, boolean fromCache) { - if(consensus.equals(currentConsensus)) - return; - - if(currentConsensus != null && consensus.getValidAfterTime().isBefore(currentConsensus.getValidAfterTime())) { - logger.warning("New consensus document is older than current consensus document"); - return; - } - - synchronized(TrustedAuthorities.getInstance()) { - switch(consensus.verifySignatures()) { - case STATUS_FAILED: - logger.warning("Unable to verify signatures on consensus document, discarding..."); - return; - - case STATUS_NEED_CERTS: - consensusWaitingForCertificates = consensus; - requiredCertificates.addAll(consensus.getRequiredCertificates()); - return; - - case STATUS_VERIFIED: - break; - } - requiredCertificates.addAll(consensus.getRequiredCertificates()); - - } - final Map oldRouterByIdentity = new HashMap(routersByIdentity); - - clearAll(); - - for(RouterStatus status: consensus.getRouterStatusEntries()) { - if(status.hasFlag("Running") && status.hasFlag("Valid")) { - final RouterImpl router = updateOrCreateRouter(status, oldRouterByIdentity); - addRouter(router); - classifyRouter(router); - } - final Descriptor d = getDescriptorForRouterStatus(status, consensus.getFlavor() == ConsensusFlavor.MICRODESC); - if(d != null) { - d.setLastListed(consensus.getValidAfterTime().getTime()); - } - } - - logger.fine("Loaded "+ routersByIdentity.size() +" routers from consensus document"); - currentConsensus = consensus; - - if(!fromCache) { - storeCurrentConsensus(); - } - consensusChangedManager.fireEvent(new Event() {}); - } - - private void storeCurrentConsensus() { - if(currentConsensus != null) { - if(currentConsensus.getFlavor() == ConsensusFlavor.MICRODESC) { - store.writeDocument(CacheFile.CONSENSUS_MICRODESC, currentConsensus); - } else { - store.writeDocument(CacheFile.CONSENSUS, currentConsensus); - } - } - } - - private Descriptor getDescriptorForRouterStatus(RouterStatus rs, boolean isMicrodescriptor) { - if(isMicrodescriptor) { - return microdescriptorCache.getDescriptor(rs.getMicrodescriptorDigest()); - } else { - return basicDescriptorCache.getDescriptor(rs.getDescriptorDigest()); - } - } - - private RouterImpl updateOrCreateRouter(RouterStatus status, Map knownRouters) { - final RouterImpl router = knownRouters.get(status.getIdentity()); - if(router == null) - return RouterImpl.createFromRouterStatus(this, status); - router.updateStatus(status); - return router; - } - - private void clearAll() { - routersByIdentity.clear(); - routersByNickname.clear(); - directoryCaches.clear(); - } - - private void classifyRouter(RouterImpl router) { - if(isValidDirectoryCache(router)) { - directoryCaches.add(router); - } else { - directoryCaches.remove(router); - } - } - - private boolean isValidDirectoryCache(RouterImpl router) { - if(router.getDirectoryPort() == 0) - return false; - if(router.hasFlag("BadDirectory")) - return false; - return router.hasFlag("V2Dir"); - } - - private void addRouter(RouterImpl router) { - routersByIdentity.put(router.getIdentityHash(), router); - addRouterByNickname(router); - } - - private void addRouterByNickname(RouterImpl router) { - final String name = router.getNickname(); - if(name == null || name.equals("Unnamed")) - return; - if(routersByNickname.containsKey(router.getNickname())) { - //logger.warn("Duplicate router nickname: "+ router.getNickname()); - return; - } - routersByNickname.put(name, router); - } - - public synchronized void addRouterMicrodescriptors(List microdescriptors) { - microdescriptorCache.addDescriptors(microdescriptors); - needRecalculateMinimumRouterInfo = true; - } - - synchronized public List getRoutersWithDownloadableDescriptors() { - waitUntilLoaded(); - final List routers = new ArrayList(); - for(RouterImpl router: routersByIdentity.values()) { - if(router.isDescriptorDownloadable()) - routers.add(router); - } - - for(int i = 0; i < routers.size(); i++) { - final Router a = routers.get(i); - final int swapIdx = random.nextInt(routers.size()); - final Router b = routers.get(swapIdx); - routers.set(i, b); - routers.set(swapIdx, a); - } - - return routers; - } - - public ConsensusDocument getCurrentConsensusDocument() { - return currentConsensus; - } - - public boolean hasPendingConsensus() { - synchronized (TrustedAuthorities.getInstance()) { - return consensusWaitingForCertificates != null; - } - } - - public void registerConsensusChangedHandler(EventHandler handler) { - consensusChangedManager.addListener(handler); - } - - public void unregisterConsensusChangedHandler(EventHandler handler) { - consensusChangedManager.removeListener(handler); - } - - public Router getRouterByName(String name) { - if(name.equals("Unnamed")) { - return null; - } - if(name.length() == 41 && name.charAt(0) == '$') { - try { - final HexDigest identity = HexDigest.createFromString(name.substring(1)); - return getRouterByIdentity(identity); - } catch (Exception e) { - return null; - } - } - waitUntilLoaded(); - return routersByNickname.get(name); - } - - public Router getRouterByIdentity(HexDigest identity) { - waitUntilLoaded(); - synchronized (routersByIdentity) { - return routersByIdentity.get(identity); - } - } - - public List getRouterListByNames(List names) { - waitUntilLoaded(); - final List routers = new ArrayList(); - for(String n: names) { - final Router r = getRouterByName(n); - if(r == null) - throw new TorException("Could not find router named: "+ n); - routers.add(r); - } - return routers; - } - - public List getAllRouters() { - waitUntilLoaded(); - synchronized(routersByIdentity) { - return new ArrayList(routersByIdentity.values()); - } - } - - public GuardEntry createGuardEntryFor(Router router) { - waitUntilLoaded(); - return stateFile.createGuardEntryFor(router); - } - - public List getGuardEntries() { - waitUntilLoaded(); - return stateFile.getGuardEntries(); - } - - public void removeGuardEntry(GuardEntry entry) { - waitUntilLoaded(); - stateFile.removeGuardEntry(entry); - } - - public void addGuardEntry(GuardEntry entry) { - waitUntilLoaded(); - stateFile.addGuardEntry(entry); - } - - public RouterMicrodescriptor getMicrodescriptorFromCache(HexDigest descriptorDigest) { - return microdescriptorCache.getDescriptor(descriptorDigest); - } - - - public RouterDescriptor getBasicDescriptorFromCache(HexDigest descriptorDigest) { - return basicDescriptorCache.getDescriptor(descriptorDigest); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/DirectoryServerImpl.java b/orchid/src/com/subgraph/orchid/directory/DirectoryServerImpl.java deleted file mode 100644 index d8f0bceb..00000000 --- a/orchid/src/com/subgraph/orchid/directory/DirectoryServerImpl.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import com.subgraph.orchid.DirectoryServer; -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.RouterStatus; -import com.subgraph.orchid.data.HexDigest; - -public class DirectoryServerImpl extends RouterImpl implements DirectoryServer { - - private List certificates = new ArrayList(); - - private boolean isHiddenServiceAuthority = false; - private boolean isBridgeAuthority = false; - private boolean isExtraInfoCache = false; - private int port; - private HexDigest v3Ident; - - DirectoryServerImpl(RouterStatus status) { - super(null, status); - } - - void setHiddenServiceAuthority() { isHiddenServiceAuthority = true; } - void unsetHiddenServiceAuthority() { isHiddenServiceAuthority = false; } - void setBridgeAuthority() { isBridgeAuthority = true; } - void setExtraInfoCache() { isExtraInfoCache = true; } - void setPort(int port) { this.port = port; } - void setV3Ident(HexDigest fingerprint) { this.v3Ident = fingerprint; } - - public boolean isTrustedAuthority() { - return true; - } - - /** - * Return true if this DirectoryServer entry has - * complete and valid information. - * @return - */ - public boolean isValid() { - return true; - } - - public boolean isV2Authority() { - return hasFlag("Authority") && hasFlag("V2Dir"); - } - - public boolean isV3Authority() { - return hasFlag("Authority") && v3Ident != null; - } - - public boolean isHiddenServiceAuthority() { - return isHiddenServiceAuthority; - } - - public boolean isBridgeAuthority() { - return isBridgeAuthority; - } - - public boolean isExtraInfoCache() { - return isExtraInfoCache; - } - - public HexDigest getV3Identity() { - return v3Ident; - } - - public KeyCertificate getCertificateByFingerprint(HexDigest fingerprint) { - for(KeyCertificate kc: getCertificates()) { - if(kc.getAuthoritySigningKey().getFingerprint().equals(fingerprint)) { - return kc; - } - } - return null; - } - - public List getCertificates() { - synchronized(certificates) { - purgeExpiredCertificates(); - purgeOldCertificates(); - return new ArrayList(certificates); - } - } - - private void purgeExpiredCertificates() { - Iterator it = certificates.iterator(); - while(it.hasNext()) { - KeyCertificate elem = it.next(); - if(elem.isExpired()) { - it.remove(); - } - } - } - - private void purgeOldCertificates() { - if(certificates.size() < 2) { - return; - } - final KeyCertificate newest = getNewestCertificate(); - final Iterator it = certificates.iterator(); - while(it.hasNext()) { - KeyCertificate elem = it.next(); - if(elem != newest && isMoreThan48HoursOlder(newest, elem)) { - it.remove(); - } - } - } - - private KeyCertificate getNewestCertificate() { - KeyCertificate newest = null; - for(KeyCertificate kc : certificates) { - if(newest == null || getPublishedMilliseconds(newest) > getPublishedMilliseconds(kc)) { - newest = kc; - } - } - return newest; - } - - private boolean isMoreThan48HoursOlder(KeyCertificate newer, KeyCertificate older) { - final long milliseconds = 48 * 60 * 60 * 1000; - return (getPublishedMilliseconds(newer) - getPublishedMilliseconds(older)) > milliseconds; - } - - private long getPublishedMilliseconds(KeyCertificate certificate) { - return certificate.getKeyPublishedTime().getDate().getTime(); - } - - public void addCertificate(KeyCertificate certificate) { - if(!certificate.getAuthorityFingerprint().equals(v3Ident)) { - throw new IllegalArgumentException("This certificate does not appear to belong to this directory authority"); - } - synchronized(certificates) { - certificates.add(certificate); - } - } - - public String toString() { - if(v3Ident != null) - return "(Directory: "+ getNickname() +" "+ getAddress() +":"+ port +" fingerprint="+ getIdentityHash() +" v3ident="+ - v3Ident +")"; - else - return "(Directory: "+ getNickname() +" "+ getAddress() +":"+ port +" fingerprint="+ getIdentityHash() +")"; - - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/DirectoryStoreFile.java b/orchid/src/com/subgraph/orchid/directory/DirectoryStoreFile.java deleted file mode 100644 index 0e702f46..00000000 --- a/orchid/src/com/subgraph/orchid/directory/DirectoryStoreFile.java +++ /dev/null @@ -1,226 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.io.Closeable; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.WritableByteChannel; -import java.util.List; -import java.util.logging.Logger; - -import com.subgraph.orchid.Document; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.crypto.TorRandom; - -public class DirectoryStoreFile { - private final static Logger logger = Logger.getLogger(DirectoryStoreFile.class.getName()); - private final static ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); - private final static TorRandom random = new TorRandom(); - - private final TorConfig config; - private final String cacheFilename; - - private RandomAccessFile openFile; - - private boolean openFileFailed; - private boolean directoryCreationFailed; - - DirectoryStoreFile(TorConfig config, String cacheFilename) { - this.config = config; - this.cacheFilename = cacheFilename; - } - - public void writeData(ByteBuffer data) { - final File tempFile = createTempFile(); - final FileOutputStream fos = openFileOutputStream(tempFile); - if(fos == null) { - return; - } - try { - writeAllToChannel(fos.getChannel(), data); - quietClose(fos); - installTempFile(tempFile); - } catch (IOException e) { - logger.warning("I/O error writing to temporary cache file "+ tempFile + " : "+ e); - return; - } finally { - quietClose(fos); - tempFile.delete(); - } - } - - public void writeDocuments(List documents) { - final File tempFile = createTempFile(); - final FileOutputStream fos = openFileOutputStream(tempFile); - if(fos == null) { - return; - } - try { - writeDocumentsToChannel(fos.getChannel(), documents); - quietClose(fos); - installTempFile(tempFile); - } catch (IOException e) { - logger.warning("I/O error writing to temporary cache file "+ tempFile + " : "+ e); - return; - } finally { - quietClose(fos); - tempFile.delete(); - } - } - - private FileOutputStream openFileOutputStream(File file) { - try { - createDirectoryIfMissing(); - return new FileOutputStream(file); - } catch (FileNotFoundException e) { - logger.warning("Failed to open file "+ file + " : "+ e); - return null; - } - } - - public void appendDocuments(List documents) { - if(!ensureOpened()) { - return; - } - try { - final FileChannel channel = openFile.getChannel(); - channel.position(channel.size()); - writeDocumentsToChannel(channel, documents); - channel.force(true); - } catch (IOException e) { - logger.warning("I/O error writing to cache file "+ cacheFilename); - return; - } - } - - public ByteBuffer loadContents() { - if(!(fileExists() && ensureOpened())) { - return EMPTY_BUFFER; - } - - try { - return readAllFromChannel(openFile.getChannel()); - } catch (IOException e) { - logger.warning("I/O error reading cache file "+ cacheFilename + " : "+ e); - return EMPTY_BUFFER; - } - } - - private ByteBuffer readAllFromChannel(FileChannel channel) throws IOException { - channel.position(0); - final ByteBuffer buffer = createBufferForChannel(channel); - while(buffer.hasRemaining()) { - if(channel.read(buffer) == -1) { - logger.warning("Unexpected EOF reading from cache file"); - return EMPTY_BUFFER; - } - } - buffer.rewind(); - return buffer; - } - - private ByteBuffer createBufferForChannel(FileChannel channel) throws IOException { - final int sz = (int) (channel.size() & 0xFFFFFFFF); - return ByteBuffer.allocateDirect(sz); - } - - void close() { - if(openFile != null) { - quietClose(openFile); - openFile = null; - } - } - - private boolean fileExists() { - final File file = getFile(); - return file.exists(); - } - - private boolean ensureOpened() { - if(openFileFailed) { - return false; - } - if(openFile != null) { - return true; - } - openFile = openFile(); - return openFile != null; - } - - private RandomAccessFile openFile() { - try { - final File f = new File(config.getDataDirectory(), cacheFilename); - createDirectoryIfMissing(); - return new RandomAccessFile(f, "rw"); - } catch (FileNotFoundException e) { - openFileFailed = true; - logger.warning("Failed to open cache file "+ cacheFilename); - return null; - } - } - - private void installTempFile(File tempFile) { - close(); - final File target = getFile(); - if(target.exists() && !target.delete()) { - logger.warning("Failed to delete file "+ target); - } - if(!tempFile.renameTo(target)) { - logger.warning("Failed to rename temp file "+ tempFile +" to "+ target); - } - tempFile.delete(); - ensureOpened(); - } - - private File createTempFile() { - final long n = random.nextLong(); - final File f = new File(config.getDataDirectory(), cacheFilename + Long.toString(n)); - f.deleteOnExit(); - return f; - } - - private void writeDocumentsToChannel(FileChannel channel, List documents) throws IOException { - for(Document d: documents) { - writeAllToChannel(channel, d.getRawDocumentBytes()); - } - } - - private void writeAllToChannel(WritableByteChannel channel, ByteBuffer data) throws IOException { - data.rewind(); - while(data.hasRemaining()) { - channel.write(data); - } - } - - private void quietClose(Closeable closeable) { - try { - closeable.close(); - } catch (IOException e) {} - } - - private File getFile() { - return new File(config.getDataDirectory(), cacheFilename); - } - - public void remove() { - close(); - getFile().delete(); - } - - private void createDirectoryIfMissing() { - if(directoryCreationFailed) { - return; - } - final File dd = config.getDataDirectory(); - if(!dd.exists()) { - if(!dd.mkdirs()) { - directoryCreationFailed = true; - logger.warning("Failed to create data directory "+ dd); - } - } - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/DirectoryStoreImpl.java b/orchid/src/com/subgraph/orchid/directory/DirectoryStoreImpl.java deleted file mode 100644 index f23b2935..00000000 --- a/orchid/src/com/subgraph/orchid/directory/DirectoryStoreImpl.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import com.subgraph.orchid.DirectoryStore; -import com.subgraph.orchid.Document; -import com.subgraph.orchid.TorConfig; - -public class DirectoryStoreImpl implements DirectoryStore { - - private final TorConfig config; - private Map fileMap; - - DirectoryStoreImpl(TorConfig config) { - this.config = config; - this.fileMap = new HashMap(); - } - - public synchronized ByteBuffer loadCacheFile(CacheFile cacheFile) { - return getStoreFile(cacheFile).loadContents(); - } - - public synchronized void writeData(CacheFile cacheFile, ByteBuffer data) { - getStoreFile(cacheFile).writeData(data); - } - - public synchronized void writeDocument(CacheFile cacheFile, Document document) { - writeDocumentList(cacheFile, Arrays.asList(document)); - } - - public synchronized void writeDocumentList(CacheFile cacheFile, List documents) { - getStoreFile(cacheFile).writeDocuments(documents); - } - - public synchronized void appendDocumentList(CacheFile cacheFile, List documents) { - getStoreFile(cacheFile).appendDocuments(documents); - } - - public synchronized void removeCacheFile(CacheFile cacheFile) { - getStoreFile(cacheFile).remove(); - } - - public synchronized void removeAllCacheFiles() { - for(CacheFile cf: CacheFile.values()) { - getStoreFile(cf).remove(); - } - } - - private DirectoryStoreFile getStoreFile(CacheFile cacheFile) { - if(!fileMap.containsKey(cacheFile)) { - fileMap.put(cacheFile, new DirectoryStoreFile(config, cacheFile.getFilename())); - } - return fileMap.get(cacheFile); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/DocumentFieldParserImpl.java b/orchid/src/com/subgraph/orchid/directory/DocumentFieldParserImpl.java deleted file mode 100644 index 24296e31..00000000 --- a/orchid/src/com/subgraph/orchid/directory/DocumentFieldParserImpl.java +++ /dev/null @@ -1,422 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.List; -import java.util.TimeZone; -import java.util.logging.Logger; - -import com.subgraph.orchid.TorException; -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.crypto.TorMessageDigest; -import com.subgraph.orchid.crypto.TorNTorKeyAgreement; -import com.subgraph.orchid.crypto.TorPublicKey; -import com.subgraph.orchid.crypto.TorSignature; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; -import com.subgraph.orchid.data.Timestamp; -import com.subgraph.orchid.directory.parsing.DocumentFieldParser; -import com.subgraph.orchid.directory.parsing.DocumentObject; -import com.subgraph.orchid.directory.parsing.DocumentParsingHandler; -import com.subgraph.orchid.directory.parsing.NameIntegerParameter; -import com.subgraph.orchid.encoders.Base64; - -public class DocumentFieldParserImpl implements DocumentFieldParser { - private final static Logger logger = Logger.getLogger(DocumentFieldParserImpl.class.getName()); - private final static String BEGIN_TAG = "-----BEGIN"; - private final static String END_TAG = "-----END"; - private final static String TAG_DELIMITER = "-----"; - private final static String DEFAULT_DELIMITER = " "; - private final ByteBuffer inputBuffer; - private final SimpleDateFormat dateFormat; - private String delimiter = DEFAULT_DELIMITER; - private String currentKeyword; - private List currentItems; - private int currentItemsPosition; - private boolean recognizeOpt; - /* If a line begins with this string do not include it in the current signature. */ - private String signatureIgnoreToken; - private boolean isProcessingSignedEntity = false; - private TorMessageDigest signatureDigest; - private TorMessageDigest signatureDigest256; - private StringBuilder rawDocumentBuffer; - - private DocumentParsingHandler callbackHandler; - - public DocumentFieldParserImpl(ByteBuffer buffer) { - buffer.rewind(); - this.inputBuffer = buffer; - rawDocumentBuffer = new StringBuilder(); - dateFormat = createDateFormat(); - } - - private static SimpleDateFormat createDateFormat() { - final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - format.setLenient(false); - return format; - } - - public String parseNickname() { - // XXX verify valid nickname - return getItem(); - } - public String parseString() { - return getItem(); - } - - public void setRecognizeOpt() { - recognizeOpt = true; - } - - public void setHandler(DocumentParsingHandler handler) { - callbackHandler = handler; - } - - public void setDelimiter(String delimiter) { - this.delimiter = delimiter; - } - - public int argumentsRemaining() { - return currentItems.size() - currentItemsPosition; - } - - private String getItem() { - if(currentItemsPosition >= currentItems.size()) - throw new TorParsingException("Overrun while reading arguments"); - return currentItems.get(currentItemsPosition++); - } - /* - * Return a string containing all remaining arguments concatenated together - */ - public String parseConcatenatedString() { - StringBuilder result = new StringBuilder(); - while(argumentsRemaining() > 0) { - if(result.length() > 0) - result.append(" "); - result.append(getItem()); - } - return result.toString(); - } - - public boolean parseBoolean() { - final int i = parseInteger(); - if(i == 1) - return true; - else if(i == 0) - return false; - else - throw new TorParsingException("Illegal boolean value: "+ i); - } - - public int parseInteger() { - return parseInteger(getItem()); - } - - public int parseInteger(String item) { - try { - return Integer.parseInt(item); - } catch(NumberFormatException e) { - throw new TorParsingException("Failed to parse expected integer value: " + item); - } - } - - public int[] parseIntegerList() { - final String item = getItem(); - final String[] ns = item.split(","); - final int[] result = new int[ns.length]; - for(int i = 0; i < result.length; i++) { - result[i] = parseInteger(ns[i]); - } - return result; - } - - public int parsePort() { - return parsePort(getItem()); - } - - public int parsePort(String item) { - final int port = parseInteger(item); - if(port < 0 || port > 65535) - throw new TorParsingException("Illegal port value: " + port); - return port; - } - - - public Timestamp parseTimestamp() { - String timeAndDate = getItem() + " " + getItem(); - try { - return new Timestamp(dateFormat.parse(timeAndDate)); - } catch (ParseException e) { - throw new TorParsingException("Could not parse timestamp value: "+ timeAndDate); - } - } - - public HexDigest parseHexDigest() { - return HexDigest.createFromString(parseString()); - } - - public HexDigest parseBase32Digest() { - return HexDigest.createFromBase32String(parseString()); - } - - public HexDigest parseFingerprint() { - return HexDigest.createFromString(parseConcatenatedString()); - } - - public void verifyExpectedArgumentCount(String keyword, int argumentCount) { - verifyExpectedArgumentCount(keyword, argumentCount, argumentCount); - } - - private void verifyExpectedArgumentCount(String keyword, int expectedMin, int expectedMax) { - final int argumentCount = argumentsRemaining(); - if(expectedMin != -1 && argumentCount < expectedMin) - throw new TorParsingException("Not enough arguments for keyword '"+ keyword +"' expected "+ expectedMin +" and got "+ argumentCount); - - if(expectedMax != -1 && argumentCount > expectedMax) - // Is this the correct thing to do, or should just be a warning? - throw new TorParsingException("Too many arguments for keyword '"+ keyword +"' expected "+ expectedMax +" and got "+ argumentCount); - } - - public byte[] parseBase64Data() { - final StringBuilder string = new StringBuilder(getItem()); - switch(string.length() % 4) { - case 2: - string.append("=="); - break; - case 3: - string.append("="); - break; - default: - break; - } - try { - return Base64.decode(string.toString().getBytes("ISO-8859-1")); - } catch (UnsupportedEncodingException e) { - throw new TorException(e); - } - - } - - public IPv4Address parseAddress() { - return IPv4Address.createFromString(getItem()); - } - - public TorPublicKey parsePublicKey() { - final DocumentObject documentObject = parseObject(); - return TorPublicKey.createFromPEMBuffer(documentObject.getContent()); - } - - - public byte[] parseNtorPublicKey() { - final byte[] key = parseBase64Data(); - if(key.length != TorNTorKeyAgreement.CURVE25519_PUBKEY_LEN) { - throw new TorParsingException("NTor public key was not expected length after base64 decoding. Length is "+ key.length); - } - return key; - } - - public TorSignature parseSignature() { - final DocumentObject documentObject = parseObject(); - TorSignature s = TorSignature.createFromPEMBuffer(documentObject.getContent()); - return s; - } - - public NameIntegerParameter parseParameter() { - final String item = getItem(); - final int eq = item.indexOf('='); - if(eq == -1) { - throw new TorParsingException("Parameter not in expected form name=value"); - } - final String name = item.substring(0, eq); - validateParameterName(name); - final int value = parseInteger(item.substring(eq + 1)); - return new NameIntegerParameter(name, value); - } - - private void validateParameterName(String name) { - if(name.isEmpty()) { - throw new TorParsingException("Parameter name cannot be empty"); - } - for(char c: name.toCharArray()) { - if(!(Character.isLetterOrDigit(c) || c == '_')) { - throw new TorParsingException("Parameter name can only contain letters. Rejecting: "+ name); - } - } - } - - public DocumentObject parseTypedObject(String type) { - final DocumentObject object = parseObject(); - if(!type.equals(object.getKeyword())) - throw new TorParsingException("Unexpected object type. Expecting: "+ type +", but got: "+ object.getKeyword()); - return object; - } - - public DocumentObject parseObject() { - final String line = readLine(); - final String keyword = parseObjectHeader(line); - final DocumentObject object = new DocumentObject(keyword, line); - parseObjectBody(object, keyword); - return object; - } - - private String parseObjectHeader(String headerLine) { - if(!(headerLine.startsWith(BEGIN_TAG) && headerLine.endsWith(TAG_DELIMITER))) - throw new TorParsingException("Did not find expected object start tag."); - return headerLine.substring(BEGIN_TAG.length() + 1, - headerLine.length() - TAG_DELIMITER.length()); - } - - private void parseObjectBody(DocumentObject object, String keyword) { - final String endTag = END_TAG +" "+ keyword +TAG_DELIMITER; - while(true) { - final String line = readLine(); - if(line == null) { - throw new TorParsingException("EOF reached before end of '"+ keyword +"' object."); - } - if(line.equals(endTag)) { - object.addFooterLine(line); - return; - } - parseObjectContent(object, line); - } - } - - private void parseObjectContent(DocumentObject object, String content) { - // XXX verify legal base64 data - object.addContent(content); - } - - public String getCurrentKeyword() { - return currentKeyword; - } - - public void processDocument() { - if(callbackHandler == null) - throw new TorException("DocumentFieldParser#processDocument() called with null callbackHandler"); - - while(true) { - final String line = readLine(); - if(line == null) { - callbackHandler.endOfDocument(); - return; - } - if(processLine(line)) - callbackHandler.parseKeywordLine(); - } - } - - public void startSignedEntity() { - isProcessingSignedEntity = true; - signatureDigest = new TorMessageDigest(); - signatureDigest256 = new TorMessageDigest(true); - } - - public void endSignedEntity() { - isProcessingSignedEntity = false; - } - - public void setSignatureIgnoreToken(String token) { - signatureIgnoreToken = token; - } - - public TorMessageDigest getSignatureMessageDigest() { - return signatureDigest; - } - - public TorMessageDigest getSignatureMessageDigest256() { - return signatureDigest256; - } - - private void updateRawDocument(String line) { - rawDocumentBuffer.append(line); - rawDocumentBuffer.append('\n'); - } - - public String getRawDocument() { - return rawDocumentBuffer.toString(); - } - - public void resetRawDocument() { - rawDocumentBuffer = new StringBuilder(); - } - - public void resetRawDocument(String initialContent) { - rawDocumentBuffer = new StringBuilder(); - rawDocumentBuffer.append(initialContent); - } - - public boolean verifySignedEntity(TorPublicKey publicKey, TorSignature signature) { - isProcessingSignedEntity = false; - return publicKey.verifySignature(signature, signatureDigest); - } - - private String readLine() { - final String line = nextLineFromInputBuffer(); - if(line != null) { - updateCurrentSignature(line); - updateRawDocument(line); - } - return line; - } - - private String nextLineFromInputBuffer() { - if(!inputBuffer.hasRemaining()) { - return null; - } - final StringBuilder sb = new StringBuilder(); - while(inputBuffer.hasRemaining()) { - char c = (char) (inputBuffer.get() & 0xFF); - if(c == '\n') { - return sb.toString(); - } else if(c != '\r') { - sb.append(c); - } - } - return sb.toString(); - } - - private void updateCurrentSignature(String line) { - if(!isProcessingSignedEntity) - return; - if(signatureIgnoreToken != null && line.startsWith(signatureIgnoreToken)) - return; - signatureDigest.update(line + "\n"); - signatureDigest256.update(line + "\n"); - } - - private boolean processLine(String line) { - final List lineItems = Arrays.asList(line.split(delimiter)); - if(lineItems.size() == 0 || lineItems.get(0).length() == 0) { - // XXX warn - return false; - } - - currentKeyword = lineItems.get(0); - currentItems = lineItems; - currentItemsPosition = 1; - - if(recognizeOpt && currentKeyword.equals("opt") && lineItems.size() > 1) { - currentKeyword = lineItems.get(1); - currentItemsPosition = 2; - } - - return true; - } - - public void logDebug(String message) { - logger.fine(message); - } - - public void logError(String message) { - logger.warning(message); - } - - public void logWarn(String message) { - logger.info(message); - } - -} diff --git a/orchid/src/com/subgraph/orchid/directory/DocumentParserFactoryImpl.java b/orchid/src/com/subgraph/orchid/directory/DocumentParserFactoryImpl.java deleted file mode 100644 index e837a85c..00000000 --- a/orchid/src/com/subgraph/orchid/directory/DocumentParserFactoryImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.nio.ByteBuffer; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.RouterMicrodescriptor; -import com.subgraph.orchid.directory.certificate.KeyCertificateParser; -import com.subgraph.orchid.directory.consensus.ConsensusDocumentParser; -import com.subgraph.orchid.directory.parsing.DocumentFieldParser; -import com.subgraph.orchid.directory.parsing.DocumentParser; -import com.subgraph.orchid.directory.parsing.DocumentParserFactory; -import com.subgraph.orchid.directory.router.RouterDescriptorParser; -import com.subgraph.orchid.directory.router.RouterMicrodescriptorParser; - -public class DocumentParserFactoryImpl implements DocumentParserFactory { - - public DocumentParser createKeyCertificateParser(ByteBuffer buffer) { - return new KeyCertificateParser(new DocumentFieldParserImpl(buffer)); - } - - public DocumentParser createRouterDescriptorParser(ByteBuffer buffer, boolean verifySignatures) { - return new RouterDescriptorParser(new DocumentFieldParserImpl(buffer), verifySignatures); - } - - public DocumentParser createRouterMicrodescriptorParser(ByteBuffer buffer) { - buffer.rewind(); - DocumentFieldParser dfp = new DocumentFieldParserImpl(buffer); - return new RouterMicrodescriptorParser(dfp); - } - - public DocumentParser createConsensusDocumentParser(ByteBuffer buffer) { - return new ConsensusDocumentParser(new DocumentFieldParserImpl(buffer)); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/GuardEntryImpl.java b/orchid/src/com/subgraph/orchid/directory/GuardEntryImpl.java deleted file mode 100644 index 6694cbde..00000000 --- a/orchid/src/com/subgraph/orchid/directory/GuardEntryImpl.java +++ /dev/null @@ -1,274 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.util.Date; - -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.GuardEntry; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.data.HexDigest; - -public class GuardEntryImpl implements GuardEntry { - private final static String NL = System.getProperty("line.separator"); - - private final Directory directory; - private final StateFile stateFile; - private final String nickname; - private final String identity; - private final Object lock = new Object(); - private String version; - private Date createdTime; - - private boolean isAdded; - private Date unlistedSince; - private Date downSince; - private Date lastConnect; - - GuardEntryImpl(Directory directory, StateFile stateFile, String nickname, String identity) { - this.directory = directory; - this.stateFile = stateFile; - this.nickname = nickname; - this.identity = identity; - } - - void setAddedFlag() { - isAdded = true; - } - - void setVersion(String version) { - this.version = version; - } - - void setCreatedTime(Date date) { - this.createdTime = date; - } - - void setUnlistedSince(Date date) { - synchronized(lock) { - unlistedSince = date; - } - } - - void setDownSince(Date downSince, Date lastTried) { - synchronized (lock) { - this.downSince = downSince; - this.lastConnect = lastTried; - } - } - - public boolean isAdded() { - return isAdded; - } - - public void markAsDown() { - synchronized(lock) { - final Date now = new Date(); - if(downSince == null) { - downSince = now; - } else { - lastConnect = now; - } - } - if(isAdded) { - stateFile.writeFile(); - } - } - - public void clearDownSince() { - synchronized (lock) { - downSince = null; - lastConnect = null; - } - if(isAdded) { - stateFile.writeFile(); - } - } - - public void clearUnlistedSince() { - synchronized (lock) { - unlistedSince = null; - } - if(isAdded) { - stateFile.writeFile(); - } - } - - public String getNickname() { - return nickname; - } - - public String getIdentity() { - return identity; - } - - public String getVersion() { - return version; - } - - public Date getCreatedTime() { - synchronized (lock) { - return dup(createdTime); - } - } - - public Date getDownSince() { - synchronized (lock) { - return dup(downSince); - } - } - - public Date getLastConnectAttempt() { - synchronized (lock) { - return dup(lastConnect); - } - } - - public Date getUnlistedSince() { - synchronized (lock) { - return dup(unlistedSince); - } - } - - private Date dup(Date date) { - if(date == null) { - return null; - } else { - return new Date(date.getTime()); - } - } - - public String writeToString() { - final StringBuilder sb = new StringBuilder(); - synchronized (lock) { - appendEntryGuardLine(sb); - appendEntryGuardAddedBy(sb); - if(downSince != null) { - appendEntryGuardDownSince(sb); - } - if(unlistedSince != null) { - appendEntryGuardUnlistedSince(sb); - } - } - return sb.toString(); - } - - private void appendEntryGuardLine(StringBuilder sb) { - sb.append(StateFile.KEYWORD_ENTRY_GUARD); - sb.append(" "); - sb.append(nickname); - sb.append(" "); - sb.append(identity); - sb.append(NL); - } - - - private void appendEntryGuardAddedBy(StringBuilder sb) { - sb.append(StateFile.KEYWORD_ENTRY_GUARD_ADDED_BY); - sb.append(" "); - sb.append(identity); - sb.append(" "); - sb.append(version); - sb.append(" "); - sb.append(formatDate(createdTime)); - sb.append(NL); - } - - private void appendEntryGuardDownSince(StringBuilder sb) { - if(downSince == null) { - return; - } - sb.append(StateFile.KEYWORD_ENTRY_GUARD_DOWN_SINCE); - sb.append(" "); - sb.append(formatDate(downSince)); - if(lastConnect != null) { - sb.append(" "); - sb.append(formatDate(lastConnect)); - } - sb.append(NL); - } - - private void appendEntryGuardUnlistedSince(StringBuilder sb) { - if(unlistedSince == null) { - return; - } - sb.append(StateFile.KEYWORD_ENTRY_GUARD_UNLISTED_SINCE); - sb.append(" "); - sb.append(formatDate(unlistedSince)); - sb.append(NL); - } - - private String formatDate(Date date) { - return stateFile.formatDate(date); - } - - public Router getRouterForEntry() { - final HexDigest id = HexDigest.createFromString(identity); - return directory.getRouterByIdentity(id); - } - - public boolean testCurrentlyUsable() { - final Router router = getRouterForEntry(); - boolean isUsable = router != null && router.isValid() && router.isPossibleGuard() && router.isRunning(); - if(isUsable) { - markUsable(); - return true; - } else { - markUnusable(); - return false; - } - } - - private void markUsable() { - synchronized (lock) { - if(unlistedSince != null) { - unlistedSince = null; - if(isAdded) { - stateFile.writeFile(); - } - } - } - } - - private synchronized void markUnusable() { - synchronized (lock) { - if(unlistedSince == null) { - unlistedSince = new Date(); - if(isAdded) { - stateFile.writeFile(); - } - } - } - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result - + ((identity == null) ? 0 : identity.hashCode()); - result = prime * result - + ((nickname == null) ? 0 : nickname.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - GuardEntryImpl other = (GuardEntryImpl) obj; - if (identity == null) { - if (other.identity != null) - return false; - } else if (!identity.equals(other.identity)) - return false; - if (nickname == null) { - if (other.nickname != null) - return false; - } else if (!nickname.equals(other.nickname)) - return false; - return true; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/RouterImpl.java b/orchid/src/com/subgraph/orchid/directory/RouterImpl.java deleted file mode 100644 index 38991c09..00000000 --- a/orchid/src/com/subgraph/orchid/directory/RouterImpl.java +++ /dev/null @@ -1,260 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.util.Collections; -import java.util.Set; - -import com.subgraph.orchid.Descriptor; -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.RouterStatus; -import com.subgraph.orchid.TorException; -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 RouterImpl implements Router { - static RouterImpl createFromRouterStatus(Directory directory, RouterStatus status) { - return new RouterImpl(directory, status); - } - - private final Directory directory; - private final HexDigest identityHash; - protected RouterStatus status; - private Descriptor descriptor; - - private volatile String cachedCountryCode; - - protected RouterImpl(Directory directory, RouterStatus status) { - this.directory = directory; - this.identityHash = status.getIdentity(); - this.status = status; - refreshDescriptor(); - } - - void updateStatus(RouterStatus status) { - if(!identityHash.equals(status.getIdentity())) - throw new TorException("Identity hash does not match status update"); - this.status = status; - this.cachedCountryCode = null; - this.descriptor = null; - refreshDescriptor(); - } - - public boolean isDescriptorDownloadable() { - refreshDescriptor(); - if(descriptor != null) { - return false; - } - - final long now = System.currentTimeMillis(); - final long diff = now - status.getPublicationTime().getDate().getTime(); - return diff > (1000 * 60 * 10); - } - - public String getVersion() { - return status.getVersion(); - } - - public HexDigest getDescriptorDigest() { - return status.getDescriptorDigest(); - } - - public IPv4Address getAddress() { - return status.getAddress(); - } - - public Descriptor getCurrentDescriptor() { - refreshDescriptor(); - return descriptor; - } - - private synchronized void refreshDescriptor() { - if(descriptor != null || directory == null) { - return; - } - if(status.getMicrodescriptorDigest() != null) { - descriptor = directory.getMicrodescriptorFromCache(status.getMicrodescriptorDigest()); - } else if(status.getDescriptorDigest() != null){ - descriptor = directory.getBasicDescriptorFromCache(status.getDescriptorDigest()); - } - } - - public HexDigest getMicrodescriptorDigest() { - return status.getMicrodescriptorDigest(); - } - - public boolean hasFlag(String flag) { - return status.hasFlag(flag); - } - - public boolean isHibernating() { - final RouterDescriptor rd = downcastDescriptor(); - if(rd == null) { - return false; - } else { - return rd.isHibernating(); - } - } - - public boolean isRunning() { - return hasFlag("Running"); - } - - public boolean isValid() { - return hasFlag("Valid"); - } - - public boolean isBadExit() { - return hasFlag("BadExit"); - } - - public boolean isPossibleGuard() { - return hasFlag("Guard"); - } - - public boolean isExit() { - return hasFlag("Exit"); - } - - public boolean isFast() { - return hasFlag("Fast"); - } - - public boolean isStable() { - return hasFlag("Stable"); - } - - public boolean isHSDirectory() { - return hasFlag("HSDir"); - } - - public int getDirectoryPort() { - return status.getDirectoryPort(); - } - - public HexDigest getIdentityHash() { - return identityHash; - } - - public TorPublicKey getIdentityKey() { - final RouterDescriptor rd = downcastDescriptor(); - if(rd != null) { - return rd.getIdentityKey(); - } else { - return null; - } - } - - public String getNickname() { - return status.getNickname(); - } - - public int getOnionPort() { - return status.getRouterPort(); - } - - public TorPublicKey getOnionKey() { - refreshDescriptor(); - if(descriptor != null) { - return descriptor.getOnionKey(); - } else { - return null; - } - } - - public byte[] getNTorOnionKey() { - refreshDescriptor(); - if(descriptor != null) { - return descriptor.getNTorOnionKey(); - } else { - return null; - } - } - - public boolean hasBandwidth() { - return status.hasBandwidth(); - } - - public int getEstimatedBandwidth() { - return status.getEstimatedBandwidth(); - } - - public int getMeasuredBandwidth() { - return status.getMeasuredBandwidth(); - } - - public Set getFamilyMembers() { - refreshDescriptor(); - if(descriptor != null) { - return descriptor.getFamilyMembers(); - } else { - return Collections.emptySet(); - } - } - - public int getAverageBandwidth() { - final RouterDescriptor rd = downcastDescriptor(); - if(rd == null) { - return 0; - } else { - return rd.getAverageBandwidth(); - } - } - - public int getBurstBandwidth() { - final RouterDescriptor rd = downcastDescriptor(); - if(rd == null) { - return 0; - } else { - return rd.getBurstBandwidth(); - } - } - - public int getObservedBandwidth() { - final RouterDescriptor rd = downcastDescriptor(); - if(rd == null) { - return 0; - } else { - return rd.getObservedBandwidth(); - } - } - - public boolean exitPolicyAccepts(IPv4Address address, int port) { - refreshDescriptor(); - if(descriptor == null) { - return false; - } else if(address == null) { - return descriptor.exitPolicyAccepts(port); - } else { - return descriptor.exitPolicyAccepts(address, port); - } - } - - public boolean exitPolicyAccepts(int port) { - return exitPolicyAccepts(null, port); - } - - public String toString() { - return "Router["+ getNickname() +" ("+getAddress() +":"+ getOnionPort() +")]"; - } - - public String getCountryCode() { - String cc = cachedCountryCode; - if(cc == null) { - cc = CountryCodeService.getInstance().getCountryCodeForAddress(getAddress()); - cachedCountryCode = cc; - } - return cc; - } - - private RouterDescriptor downcastDescriptor() { - refreshDescriptor(); - if(descriptor instanceof RouterDescriptor) { - return (RouterDescriptor) descriptor; - } else { - return null; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/StateFile.java b/orchid/src/com/subgraph/orchid/directory/StateFile.java deleted file mode 100644 index 8020547e..00000000 --- a/orchid/src/com/subgraph/orchid/directory/StateFile.java +++ /dev/null @@ -1,296 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.nio.ByteBuffer; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.logging.Logger; - -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.DirectoryStore; -import com.subgraph.orchid.GuardEntry; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.DirectoryStore.CacheFile; -import com.subgraph.orchid.crypto.TorRandom; - -public class StateFile { - private final static Logger logger = Logger.getLogger(StateFile.class.getName()); - - private final static int DATE_LENGTH = 19; - - final static String KEYWORD_ENTRY_GUARD = "EntryGuard"; - final static String KEYWORD_ENTRY_GUARD_ADDED_BY = "EntryGuardAddedBy"; - final static String KEYWORD_ENTRY_GUARD_DOWN_SINCE = "EntryGuardDownSince"; - final static String KEYWORD_ENTRY_GUARD_UNLISTED_SINCE = "EntryGuardUnlistedSince"; - - private final List guardEntries = new ArrayList(); - private final TorRandom random = new TorRandom(); - private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - private class Line { - final String line; - int offset; - - Line(String line) { - this.line = line; - offset = 0; - } - - private boolean hasChars() { - return offset < line.length(); - } - - private char getChar() { - return line.charAt(offset); - } - - private void incrementOffset(int n) { - offset += n; - if(offset > line.length()) { - offset = line.length(); - } - } - - private void skipWhitespace() { - while(hasChars() && Character.isWhitespace(getChar())) { - offset += 1; - } - } - - String nextToken() { - skipWhitespace(); - if(!hasChars()) { - return null; - } - - final StringBuilder token = new StringBuilder(); - while(hasChars() && !Character.isWhitespace(getChar())) { - token.append(getChar()); - offset += 1; - } - return token.toString(); - } - - Date parseDate() { - skipWhitespace(); - if(!hasChars()) { - return null; - } - try { - final Date date = dateFormat.parse(line.substring(offset)); - incrementOffset(DATE_LENGTH); - return date; - } catch (ParseException e) { - return null; - } - } - } - - String formatDate(Date date) { - return dateFormat.format(date); - } - - private final DirectoryStore directoryStore; - private final Directory directory; - - StateFile(DirectoryStore store, Directory directory) { - this.directoryStore = store; - this.directory = directory; - } - - public GuardEntry createGuardEntryFor(Router router) { - final GuardEntryImpl entry = new GuardEntryImpl(directory, this, router.getNickname(), router.getIdentityHash().toString()); - final String version = Tor.getImplementation() + "-" + Tor.getVersion(); - entry.setVersion(version); - - /* - * "Choose expiry time smudged over the last month." - * - * See add_an_entry_guard() in entrynodes.c - */ - final long createTime = (new Date()).getTime() - (random.nextInt(3600 * 24 * 30) * 1000L); - entry.setCreatedTime(new Date(createTime)); - return entry; - } - - public List getGuardEntries() { - synchronized (guardEntries) { - return new ArrayList(guardEntries); - } - } - - public void removeGuardEntry(GuardEntry entry) { - synchronized (guardEntries) { - guardEntries.remove(entry); - writeFile(); - } - } - - public void addGuardEntry(GuardEntry entry) { - addGuardEntry(entry, true); - } - - private void addGuardEntry(GuardEntry entry, boolean writeFile) { - synchronized(guardEntries) { - if(guardEntries.contains(entry)) { - return; - } - final GuardEntryImpl impl = (GuardEntryImpl) entry; - guardEntries.add(impl); - synchronized (impl) { - impl.setAddedFlag(); - if(writeFile) { - writeFile(); - } - } - } - } - - void writeFile() { - directoryStore.writeData(CacheFile.STATE, getFileContents()); - } - - ByteBuffer getFileContents() { - final StringBuilder sb = new StringBuilder(); - synchronized (guardEntries) { - for(GuardEntryImpl entry: guardEntries) { - sb.append(entry.writeToString()); - } - } - return ByteBuffer.wrap(sb.toString().getBytes(Tor.getDefaultCharset())); - } - - void parseBuffer(ByteBuffer buffer) { - synchronized (guardEntries) { - guardEntries.clear(); - loadGuardEntries(buffer); - } - } - - private void loadGuardEntries(ByteBuffer buffer) { - GuardEntryImpl currentEntry = null; - while(true) { - Line line = readLine(buffer); - if(line == null) { - addEntryIfValid(currentEntry); - return; - } - currentEntry = processLine(line, currentEntry); - } - } - - private GuardEntryImpl processLine(Line line, GuardEntryImpl current) { - final String keyword = line.nextToken(); - if(keyword == null) { - return current; - } else if(keyword.equals(KEYWORD_ENTRY_GUARD)) { - addEntryIfValid(current); - GuardEntryImpl newEntry = processEntryGuardLine(line); - if(newEntry == null) { - return current; - } else { - return newEntry; - } - } else if(keyword.equals(KEYWORD_ENTRY_GUARD_ADDED_BY)) { - processEntryGuardAddedBy(line, current); - return current; - } else if(keyword.equals(KEYWORD_ENTRY_GUARD_DOWN_SINCE)) { - processEntryGuardDownSince(line, current); - return current; - } else if(keyword.equals(KEYWORD_ENTRY_GUARD_UNLISTED_SINCE)) { - processEntryGuardUnlistedSince(line, current); - return current; - } else { - return current; - } - } - - private GuardEntryImpl processEntryGuardLine(Line line) { - final String name = line.nextToken(); - final String identity = line.nextToken(); - if(name == null || name.isEmpty() || identity == null || identity.isEmpty()) { - logger.warning("Failed to parse EntryGuard line: "+ line.line); - return null; - } - return new GuardEntryImpl(directory, this, name, identity); - } - - private void processEntryGuardAddedBy(Line line, GuardEntryImpl current) { - if(current == null) { - logger.warning("EntryGuardAddedBy line seen before EntryGuard in state file"); - return; - } - final String identity = line.nextToken(); - final String version = line.nextToken(); - final Date created = line.parseDate(); - if(identity == null || identity.isEmpty() || version == null || version.isEmpty() || created == null) { - logger.warning("Missing EntryGuardAddedBy field in state file"); - return; - } - current.setVersion(version); - current.setCreatedTime(created); - } - - private void processEntryGuardDownSince(Line line, GuardEntryImpl current) { - if(current == null) { - logger.warning("EntryGuardDownSince line seen before EntryGuard in state file"); - return; - } - - final Date downSince = line.parseDate(); - final Date lastTried = line.parseDate(); - if(downSince == null) { - logger.warning("Failed to parse date field in EntryGuardDownSince line in state file"); - return; - } - current.setDownSince(downSince, lastTried); - } - - private void processEntryGuardUnlistedSince(Line line, GuardEntryImpl current) { - if(current == null) { - logger.warning("EntryGuardUnlistedSince line seen before EntryGuard in state file"); - return; - } - final Date unlistedSince = line.parseDate(); - if(unlistedSince == null) { - logger.warning("Failed to parse date field in EntryGuardUnlistedSince line in state file"); - return; - } - current.setUnlistedSince(unlistedSince); - } - - private void addEntryIfValid(GuardEntryImpl entry) { - if(isValidEntry(entry)) { - addGuardEntry(entry, false); - } - } - - private boolean isValidEntry(GuardEntryImpl entry) { - return entry != null && - entry.getNickname() != null && - entry.getIdentity() != null && - entry.getVersion() != null && - entry.getCreatedTime() != null; - } - - private Line readLine(ByteBuffer buffer) { - if(!buffer.hasRemaining()) { - return null; - } - - final StringBuilder sb = new StringBuilder(); - while(buffer.hasRemaining()) { - char c = (char) (buffer.get() & 0xFF); - if(c == '\n') { - return new Line(sb.toString()); - } else if(c != '\r') { - sb.append(c); - } - } - return new Line(sb.toString()); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/TrustedAuthorities.java b/orchid/src/com/subgraph/orchid/directory/TrustedAuthorities.java deleted file mode 100644 index 95c13fba..00000000 --- a/orchid/src/com/subgraph/orchid/directory/TrustedAuthorities.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.subgraph.orchid.directory; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import com.subgraph.orchid.DirectoryServer; -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; -import com.subgraph.orchid.directory.parsing.DocumentFieldParser; -import com.subgraph.orchid.directory.parsing.DocumentParsingHandler; - -/* - * This class contains the hardcoded 'bootstrap' directory authority - * server information. - */ -public class TrustedAuthorities { - - private final static String[] dirServers = { - "authority moria1 orport=9101 no-v2 v3ident=D586D18309DED4CD6D57C18FDB97EFA96D330566 128.31.0.39:9131 9695 DFC3 5FFE B861 329B 9F1A B04C 4639 7020 CE31", - "authority tor26 v1 orport=443 v3ident=14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 86.59.21.38:80 847B 1F85 0344 D787 6491 A548 92F9 0493 4E4E B85D", - "authority dizum orport=443 v3ident=E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58 194.109.206.212:80 7EA6 EAD6 FD83 083C 538F 4403 8BBF A077 587D D755", - "authority Tonga orport=443 bridge no-v2 82.94.251.203:80 4A0C CD2D DC79 9508 3D73 F5D6 6710 0C8A 5831 F16D", - "authority longclaw orport=9090 no-v2 v3ident=23D15D965BC35114467363C165C4F724B64B4F66 202.85.227.202:80 74A9 1064 6BCE EFBC D2E8 74FC 1DC9 9743 0F96 8145", - "authority dannenberg orport=443 no-v2 v3ident=585769C78764D58426B8B52B6651A5A71137189A 193.23.244.244:80 7BE6 83E6 5D48 1413 21C5 ED92 F075 C553 64AC 7123", - "authority urras orport=80 no-v2 v3ident=80550987E1D626E3EBA5E5E75A458DE0626D088C 208.83.223.34:443 0AD3 FA88 4D18 F89E EA2D 89C0 1937 9E0E 7FD9 4417", - "authority maatuska orport=80 no-v2 v3ident=49015F787433103580E3B66A1707A00E60F2D15B 171.25.193.9:443 BD6A 8292 55CB 08E6 6FBE 7D37 4836 3586 E46B 3810", - "authority Faravahar orport=443 no-v2 v3ident=EFCBE720AB3A82B99F9E953CD5BF50F7EEFC7B97 154.35.32.5:80 CF6D 0AAF B385 BE71 B8E1 11FC 5CFF 4B47 9237 33BC", - "authority gabelmoo orport=443 no-v2 v3ident=ED03BB616EB2F60BEC80151114BB25CEF515B226 212.112.245.170:80 F204 4413 DAC2 E02E 3D6B CF47 35A1 9BCA 1DE9 7281", - }; - - private final List directoryServers = new ArrayList(); - private final int v3ServerCount; - - private final static TrustedAuthorities _instance = new TrustedAuthorities(); - - public static TrustedAuthorities getInstance() { - return _instance; - } - - private TrustedAuthorities() { - initialize(); - v3ServerCount = countV3Servers(); - } - - private int countV3Servers() { - int n = 0; - for(DirectoryServer ds: directoryServers) { - if(ds.getV3Identity() != null) { - n += 1; - } - } - return n; - } - - void initialize() { - final StringBuilder builder = new StringBuilder(); - for(String entry: dirServers) { - builder.append(entry); - builder.append('\n'); - } - final ByteBuffer buffer = ByteBuffer.wrap(builder.toString().getBytes(Tor.getDefaultCharset())); - final DocumentFieldParser parser = new DocumentFieldParserImpl(buffer); - - parser.setHandler(new DocumentParsingHandler() { - public void endOfDocument() {} - public void parseKeywordLine() { processKeywordLine(parser);} - }); - parser.processDocument(); - } - - private void processKeywordLine(DocumentFieldParser fieldParser) { - final DirectoryAuthorityStatus status = new DirectoryAuthorityStatus(); - status.setNickname(fieldParser.parseNickname()); - while(fieldParser.argumentsRemaining() > 0) - processArgument(fieldParser, status); - } - - private void processArgument(DocumentFieldParser fieldParser, DirectoryAuthorityStatus status) { - final String item = fieldParser.parseString(); - if(Character.isDigit(item.charAt(0))) { - parseAddressPort(fieldParser, item, status); - status.setIdentity(fieldParser.parseFingerprint()); - DirectoryServerImpl server = new DirectoryServerImpl(status); - if(status.getV3Ident() != null) { - server.setV3Ident(status.getV3Ident()); - } - fieldParser.logDebug("Adding trusted authority: " + server); - directoryServers.add(server); - return; - } else { - parseFlag(fieldParser, item, status); - } - } - - private void parseAddressPort(DocumentFieldParser parser, String item, DirectoryAuthorityStatus status) { - final String[] args = item.split(":"); - status.setAddress(IPv4Address.createFromString(args[0])); - status.setDirectoryPort(parser.parsePort(args[1])); - } - - private void parseFlag(DocumentFieldParser parser, String flag, DirectoryAuthorityStatus status) { - if(flag.equals("v1")) { - status.setV1Authority(); - status.setHiddenServiceAuthority(); - } else if(flag.equals("hs")) { - status.setHiddenServiceAuthority(); - } else if(flag.equals("no-hs")) { - status.unsetHiddenServiceAuthority(); - } else if(flag.equals("bridge")) { - status.setBridgeAuthority(); - } else if(flag.equals("no-v2")) { - status.unsetV2Authority(); - } else if(flag.startsWith("orport=")) { - status.setRouterPort( parser.parsePort(flag.substring(7))); - } else if(flag.startsWith("v3ident=")) { - status.setV3Ident(HexDigest.createFromString(flag.substring(8))); - } - } - - public int getV3AuthorityServerCount() { - return v3ServerCount; - } - - public List getAuthorityServers() { - return directoryServers; - } - - public DirectoryServer getAuthorityServerByIdentity(HexDigest identity) { - for(DirectoryServer ds: directoryServers) { - if(identity.equals(ds.getV3Identity())) { - return ds; - } - } - return null; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateImpl.java b/orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateImpl.java deleted file mode 100644 index cf3071bd..00000000 --- a/orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateImpl.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.subgraph.orchid.directory.certificate; - -import java.nio.ByteBuffer; - -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.crypto.TorPublicKey; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; -import com.subgraph.orchid.data.Timestamp; - -public class KeyCertificateImpl implements KeyCertificate { - - private IPv4Address directoryAddress; - private int directoryPort; - private HexDigest fingerprint; - private TorPublicKey identityKey; - private Timestamp keyPublished; - private Timestamp keyExpires; - private TorPublicKey signingKey; - private String rawDocumentData; - - private boolean hasValidSignature = false; - - void setDirectoryPort(int port) { this.directoryPort = port; } - void setDirectoryAddress(IPv4Address address) { this.directoryAddress = address; } - void setAuthorityFingerprint(HexDigest fingerprint) { this.fingerprint = fingerprint;} - void setAuthorityIdentityKey(TorPublicKey key) { this.identityKey = key; } - void setAuthoritySigningKey(TorPublicKey key) { this.signingKey = key; } - void setKeyPublishedTime(Timestamp time) { this.keyPublished = time; } - void setKeyExpiryTime(Timestamp time) { this.keyExpires = time; } - void setValidSignature() { hasValidSignature = true;} - void setRawDocumentData(String rawData) { rawDocumentData = rawData; } - - public boolean isValidDocument() { - return hasValidSignature && (fingerprint != null) && (identityKey != null) && - (keyPublished != null) && (keyExpires != null) && (signingKey != null); - } - - public IPv4Address getDirectoryAddress() { - return directoryAddress; - } - - public int getDirectoryPort() { - return directoryPort; - } - - public HexDigest getAuthorityFingerprint() { - return fingerprint; - } - - public TorPublicKey getAuthorityIdentityKey() { - return identityKey; - } - - public TorPublicKey getAuthoritySigningKey() { - return signingKey; - } - - public Timestamp getKeyPublishedTime() { - return keyPublished; - } - - public Timestamp getKeyExpiryTime() { - return keyExpires; - } - - public boolean isExpired() { - if(keyExpires != null) { - return keyExpires.hasPassed(); - } else { - return false; - } - } - - public String getRawDocumentData() { - return rawDocumentData; - } - - public ByteBuffer getRawDocumentBytes() { - if(getRawDocumentData() == null) { - return ByteBuffer.allocate(0); - } else { - return ByteBuffer.wrap(getRawDocumentData().getBytes(Tor.getDefaultCharset())); - } - } - - public String toString() { - return "(Certificate: address="+ directoryAddress +":"+ directoryPort - +" fingerprint="+ fingerprint +" published="+ keyPublished +" expires="+ keyExpires +")"+ - "\nident="+ identityKey +" sign="+ signingKey; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateKeyword.java b/orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateKeyword.java deleted file mode 100644 index 89737668..00000000 --- a/orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateKeyword.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.subgraph.orchid.directory.certificate; - -public enum KeyCertificateKeyword { - /* - * See dir-spec.txt - * Section 3.1 Key certificates - */ - DIR_KEY_CERTIFICATE_VERSION("dir-key-certificate-version", 1), - DIR_ADDRESS("dir-address", 1), - FINGERPRINT("fingerprint", 1), - DIR_IDENTITY_KEY("dir-identity-key", 0), - DIR_KEY_PUBLISHED("dir-key-published", 2), - DIR_KEY_EXPIRES("dir-key-expires", 2), - DIR_SIGNING_KEY("dir-signing-key", 0), - DIR_KEY_CROSSCERT("dir-key-crosscert", 0), - DIR_KEY_CERTIFICATION("dir-key-certification", 0), - UNKNOWN_KEYWORD("KEYWORD NOT FOUND", 0); - - private final String keyword; - private final int argumentCount; - - KeyCertificateKeyword(String keyword, int argumentCount) { - this.keyword = keyword; - this.argumentCount = argumentCount; - } - - String getKeyword() { - return keyword; - } - - int getArgumentCount() { - return argumentCount; - } - - static KeyCertificateKeyword findKeyword(String keyword) { - for(KeyCertificateKeyword k: values()) - if(k.getKeyword().equals(keyword)) - return k; - return UNKNOWN_KEYWORD; - } - -} diff --git a/orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateParser.java b/orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateParser.java deleted file mode 100644 index 7b69f381..00000000 --- a/orchid/src/com/subgraph/orchid/directory/certificate/KeyCertificateParser.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.subgraph.orchid.directory.certificate; - -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.crypto.TorPublicKey; -import com.subgraph.orchid.crypto.TorSignature; -import com.subgraph.orchid.data.IPv4Address; -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 KeyCertificateParser implements DocumentParser { - private final static int CURRENT_CERTIFICATE_VERSION = 3; - private final DocumentFieldParser fieldParser; - private KeyCertificateImpl currentCertificate; - private DocumentParsingResultHandler resultHandler; - - public KeyCertificateParser(DocumentFieldParser fieldParser) { - this.fieldParser = fieldParser; - this.fieldParser.setHandler(createParsingHandler()); - } - - private DocumentParsingHandler createParsingHandler() { - return new DocumentParsingHandler() { - public void parseKeywordLine() { - processKeywordLine(); - } - - public void endOfDocument() { - } - }; - } - - private void processKeywordLine() { - final KeyCertificateKeyword keyword = KeyCertificateKeyword.findKeyword(fieldParser.getCurrentKeyword()); - /* - * dirspec.txt (1.2) - * When interpreting a Document, software MUST ignore any KeywordLine that - * starts with a keyword it doesn't recognize; - */ - if(!keyword.equals(KeyCertificateKeyword.UNKNOWN_KEYWORD)) - processKeyword(keyword); - } - - private void startNewCertificate() { - fieldParser.resetRawDocument(); - fieldParser.startSignedEntity(); - currentCertificate = new KeyCertificateImpl(); - } - - public boolean parse(DocumentParsingResultHandler resultHandler) { - this.resultHandler = resultHandler; - startNewCertificate(); - try { - fieldParser.processDocument(); - return true; - } catch(TorParsingException e) { - resultHandler.parsingError(e.getMessage()); - return false; - } - } - - public DocumentParsingResult parse() { - final BasicDocumentParsingResult result = new BasicDocumentParsingResult(); - parse(result); - return result; - } - - private void processKeyword(KeyCertificateKeyword keyword) { - switch(keyword) { - case DIR_KEY_CERTIFICATE_VERSION: - processCertificateVersion(); - break; - case DIR_ADDRESS: - processDirectoryAddress(); - break; - case FINGERPRINT: - currentCertificate.setAuthorityFingerprint(fieldParser.parseHexDigest()); - break; - case DIR_IDENTITY_KEY: - currentCertificate.setAuthorityIdentityKey(fieldParser.parsePublicKey()); - break; - case DIR_SIGNING_KEY: - currentCertificate.setAuthoritySigningKey(fieldParser.parsePublicKey()); - break; - case DIR_KEY_PUBLISHED: - currentCertificate.setKeyPublishedTime(fieldParser.parseTimestamp()); - break; - case DIR_KEY_EXPIRES: - currentCertificate.setKeyExpiryTime(fieldParser.parseTimestamp()); - break; - case DIR_KEY_CROSSCERT: - verifyCrossSignature(fieldParser.parseSignature()); - break; - case DIR_KEY_CERTIFICATION: - processCertificateSignature(); - break; - case UNKNOWN_KEYWORD: - break; - } - } - - private void processCertificateVersion() { - final int version = fieldParser.parseInteger(); - if(version != CURRENT_CERTIFICATE_VERSION) - throw new TorParsingException("Unexpected certificate version: " + version); - } - - private void processDirectoryAddress() { - final String addrport = fieldParser.parseString(); - final String[] args = addrport.split(":"); - if(args.length != 2) - throw new TorParsingException("Address/Port string incorrectly formed: " + addrport); - currentCertificate.setDirectoryAddress(IPv4Address.createFromString(args[0])); - currentCertificate.setDirectoryPort(fieldParser.parsePort(args[1])); - } - - private void verifyCrossSignature(TorSignature crossSignature) { - TorPublicKey identityKey = currentCertificate.getAuthorityIdentityKey(); - TorPublicKey signingKey = currentCertificate.getAuthoritySigningKey(); - if(!signingKey.verifySignature(crossSignature, identityKey.getFingerprint())) - throw new TorParsingException("Cross signature on certificate failed."); - } - - private boolean verifyCurrentCertificate(TorSignature signature) { - if(!fieldParser.verifySignedEntity(currentCertificate.getAuthorityIdentityKey(), signature)) { - resultHandler.documentInvalid(currentCertificate, "Signature failed"); - fieldParser.logWarn("Signature failed for certificate with fingerprint: "+ currentCertificate.getAuthorityFingerprint()); - return false; - } - currentCertificate.setValidSignature(); - final boolean isValid = currentCertificate.isValidDocument(); - if(!isValid) { - resultHandler.documentInvalid(currentCertificate, "Certificate data is invalid"); - fieldParser.logWarn("Certificate data is invalid for certificate with fingerprint: "+ currentCertificate.getAuthorityFingerprint()); - } - return isValid; - } - - private void processCertificateSignature() { - fieldParser.endSignedEntity(); - if(verifyCurrentCertificate(fieldParser.parseSignature())) { - currentCertificate.setRawDocumentData(fieldParser.getRawDocument()); - resultHandler.documentParsed(currentCertificate); - } - startNewCertificate(); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/AuthoritySectionParser.java b/orchid/src/com/subgraph/orchid/directory/consensus/AuthoritySectionParser.java deleted file mode 100644 index e215bf52..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/AuthoritySectionParser.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import com.subgraph.orchid.directory.consensus.ConsensusDocumentParser.DocumentSection; -import com.subgraph.orchid.directory.parsing.DocumentFieldParser; - -public class AuthoritySectionParser extends ConsensusDocumentSectionParser { - - private VoteAuthorityEntryImpl currentEntry = null; - - AuthoritySectionParser(DocumentFieldParser parser , ConsensusDocumentImpl document) { - super(parser, document); - startEntry(); - } - - @Override - void parseLine(DocumentKeyword keyword) { - switch(keyword) { - case DIR_SOURCE: - parseDirSource(); - break; - case CONTACT: - currentEntry.setContact(fieldParser.parseConcatenatedString()); - break; - case VOTE_DIGEST: - currentEntry.setVoteDigest(fieldParser.parseHexDigest()); - addCurrentEntry(); - break; - default: - break; - } - } - - private void startEntry() { - currentEntry = new VoteAuthorityEntryImpl(); - } - - private void addCurrentEntry() { - document.addVoteAuthorityEntry(currentEntry); - startEntry(); - } - - private void parseDirSource() { - currentEntry.setNickname(fieldParser.parseNickname()); - currentEntry.setIdentity(fieldParser.parseHexDigest()); - currentEntry.setHostname(fieldParser.parseString()); - currentEntry.setAddress(fieldParser.parseAddress()); - currentEntry.setDirectoryPort(fieldParser.parsePort()); - currentEntry.setRouterPort(fieldParser.parsePort()); - } - - @Override - String getNextStateKeyword() { - return "r"; - } - - @Override - DocumentSection getSection() { - return DocumentSection.AUTHORITY; - } - - DocumentSection nextSection() { - return DocumentSection.ROUTER_STATUS; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentImpl.java b/orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentImpl.java deleted file mode 100644 index 7421f8bc..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentImpl.java +++ /dev/null @@ -1,344 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.DirectoryServer; -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.RouterStatus; -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.VoteAuthorityEntry; -import com.subgraph.orchid.crypto.TorPublicKey; -import com.subgraph.orchid.crypto.TorSignature.DigestAlgorithm; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.Timestamp; -import com.subgraph.orchid.directory.TrustedAuthorities; - -public class ConsensusDocumentImpl implements ConsensusDocument { - - enum SignatureVerifyStatus { STATUS_UNVERIFIED, STATUS_NEED_CERTS, STATUS_VERIFIED }; - - private final static Logger logger = Logger.getLogger(ConsensusDocumentImpl.class.getName()); - - private final static String BW_WEIGHT_SCALE_PARAM = "bwweightscale"; - private final static int BW_WEIGHT_SCALE_DEFAULT = 10000; - private final static int BW_WEIGHT_SCALE_MIN = 1; - private final static int BW_WEIGHT_SCALE_MAX = Integer.MAX_VALUE; - - private final static String CIRCWINDOW_PARAM = "circwindow"; - private final static int CIRCWINDOW_DEFAULT = 1000; - private final static int CIRCWINDOW_MIN = 100; - private final static int CIRCWINDOW_MAX = 1000; - - private final static String USE_NTOR_HANDSHAKE_PARAM = "UseNTorHandshake"; - - private Set requiredCertificates = new HashSet(); - - - private int consensusMethod; - private ConsensusFlavor flavor; - private Timestamp validAfter; - private Timestamp freshUntil; - private Timestamp validUntil; - private int distDelaySeconds; - private int voteDelaySeconds; - private Set clientVersions; - private Set serverVersions; - private Set knownFlags; - private HexDigest signingHash; - private HexDigest signingHash256; - private Map voteAuthorityEntries; - private List routerStatusEntries; - private Map bandwidthWeights; - private Map parameters; - private int signatureCount; - private boolean isFirstCallToVerifySignatures = true; - private String rawDocumentData; - - void setConsensusFlavor(ConsensusFlavor flavor) { this.flavor = flavor; } - void setConsensusMethod(int method) { consensusMethod = method; } - void setValidAfter(Timestamp ts) { validAfter = ts; } - void setFreshUntil(Timestamp ts) { freshUntil = ts; } - void setValidUntil(Timestamp ts) { validUntil = ts; } - void setDistDelaySeconds(int seconds) { distDelaySeconds = seconds; } - void setVoteDelaySeconds(int seconds) { voteDelaySeconds = seconds; } - void addClientVersion(String version) { clientVersions.add(version); } - void addServerVersion(String version) { serverVersions.add(version); } - void addParameter(String name, int value) { parameters.put(name, value); } - void addBandwidthWeight(String name, int value) { bandwidthWeights.put(name, value); } - - void addSignature(DirectorySignature signature) { - final VoteAuthorityEntry voteAuthority = voteAuthorityEntries.get(signature.getIdentityDigest()); - if(voteAuthority == null) { - logger.warning("Consensus contains signature for source not declared in authority section: "+ signature.getIdentityDigest()); - return; - } - final List signatures = voteAuthority.getSignatures(); - final DigestAlgorithm newSignatureAlgorithm = signature.getSignature().getDigestAlgorithm(); - for(DirectorySignature sig: signatures) { - DigestAlgorithm algo = sig.getSignature().getDigestAlgorithm(); - if(algo.equals(newSignatureAlgorithm)) { - logger.warning("Consensus contains two or more signatures for same source with same algorithm"); - return; - } - } - signatureCount += 1; - signatures.add(signature); - } - - void setSigningHash(HexDigest hash) { signingHash = hash; } - void setSigningHash256(HexDigest hash) { signingHash256 = hash; } - void setRawDocumentData(String rawData) { rawDocumentData = rawData; } - - ConsensusDocumentImpl() { - clientVersions = new HashSet(); - serverVersions = new HashSet(); - knownFlags = new HashSet(); - voteAuthorityEntries = new HashMap(); - routerStatusEntries = new ArrayList(); - bandwidthWeights = new HashMap(); - parameters = new HashMap(); - } - - void addKnownFlag(String flag) { - knownFlags.add(flag); - } - - void addVoteAuthorityEntry(VoteAuthorityEntry entry) { - voteAuthorityEntries.put(entry.getIdentity(), entry); - } - - void addRouterStatusEntry(RouterStatusImpl entry) { - routerStatusEntries.add(entry); - } - - public ConsensusFlavor getFlavor() { - return flavor; - } - - public Timestamp getValidAfterTime() { - return validAfter; - } - - public Timestamp getFreshUntilTime() { - return freshUntil; - } - - public Timestamp getValidUntilTime() { - return validUntil; - } - - public int getConsensusMethod() { - return consensusMethod; - } - - public int getVoteSeconds() { - return voteDelaySeconds; - } - - public int getDistSeconds() { - return distDelaySeconds; - } - - public Set getClientVersions() { - return clientVersions; - } - - public Set getServerVersions() { - return serverVersions; - } - - public boolean isLive() { - if(validUntil == null) { - return false; - } else { - return !validUntil.hasPassed(); - } - } - - public List getRouterStatusEntries() { - return Collections.unmodifiableList(routerStatusEntries); - } - - public String getRawDocumentData() { - return rawDocumentData; - } - - public ByteBuffer getRawDocumentBytes() { - if(getRawDocumentData() == null) { - return ByteBuffer.allocate(0); - } else { - return ByteBuffer.wrap(getRawDocumentData().getBytes(Tor.getDefaultCharset())); - } - } - - public boolean isValidDocument() { - return (validAfter != null) && (freshUntil != null) && (validUntil != null) && - (voteDelaySeconds > 0) && (distDelaySeconds > 0) && (signingHash != null) && - (signatureCount > 0); - } - - public HexDigest getSigningHash() { - return signingHash; - } - - public HexDigest getSigningHash256() { - return signingHash256; - } - - public synchronized SignatureStatus verifySignatures() { - boolean firstCall = isFirstCallToVerifySignatures; - isFirstCallToVerifySignatures = false; - requiredCertificates.clear(); - int verifiedCount = 0; - int certsNeededCount = 0; - final int v3Count = TrustedAuthorities.getInstance().getV3AuthorityServerCount(); - final int required = (v3Count / 2) + 1; - - for(VoteAuthorityEntry entry: voteAuthorityEntries.values()) { - switch(verifySingleAuthority(entry)) { - case STATUS_FAILED: - break; - case STATUS_NEED_CERTS: - certsNeededCount += 1; - break; - case STATUS_VERIFIED: - verifiedCount += 1; - break; - } - } - - if(verifiedCount >= required) { - return SignatureStatus.STATUS_VERIFIED; - } else if(verifiedCount + certsNeededCount >= required) { - if(firstCall) { - logger.info("Certificates need to be retrieved to verify consensus"); - } - return SignatureStatus.STATUS_NEED_CERTS; - } else { - return SignatureStatus.STATUS_FAILED; - } - } - - private SignatureStatus verifySingleAuthority(VoteAuthorityEntry authority) { - - boolean certsNeeded = false; - boolean validSignature = false; - - for(DirectorySignature s: authority.getSignatures()) { - DirectoryServer trusted = TrustedAuthorities.getInstance().getAuthorityServerByIdentity(s.getIdentityDigest()); - if(trusted == null) { - logger.warning("Consensus signed by unrecognized directory authority: "+ s.getIdentityDigest()); - return SignatureStatus.STATUS_FAILED; - } else { - switch(verifySignatureForTrustedAuthority(trusted, s)) { - case STATUS_NEED_CERTS: - certsNeeded = true; - break; - case STATUS_VERIFIED: - validSignature = true; - break; - default: - break; - } - } - } - - if(validSignature) { - return SignatureStatus.STATUS_VERIFIED; - } else if(certsNeeded) { - return SignatureStatus.STATUS_NEED_CERTS; - } else { - return SignatureStatus.STATUS_FAILED; - } - } - - private SignatureStatus verifySignatureForTrustedAuthority(DirectoryServer trustedAuthority, DirectorySignature signature) { - final KeyCertificate certificate = trustedAuthority.getCertificateByFingerprint(signature.getSigningKeyDigest()); - if(certificate == null) { - logger.fine("Missing certificate for signing key: "+ signature.getSigningKeyDigest()); - addRequiredCertificateForSignature(signature); - return SignatureStatus.STATUS_NEED_CERTS; - } - if(certificate.isExpired()) { - return SignatureStatus.STATUS_FAILED; - } - - final TorPublicKey signingKey = certificate.getAuthoritySigningKey(); - final HexDigest d = (signature.useSha256()) ? signingHash256 : signingHash; - if(!signingKey.verifySignature(signature.getSignature(), d)) { - logger.warning("Signature failed on consensus for signing key: "+ signature.getSigningKeyDigest()); - return SignatureStatus.STATUS_FAILED; - } - return SignatureStatus.STATUS_VERIFIED; - } - - public Set getRequiredCertificates() { - return requiredCertificates; - } - - private void addRequiredCertificateForSignature(DirectorySignature signature) { - requiredCertificates.add(new RequiredCertificateImpl(signature.getIdentityDigest(), signature.getSigningKeyDigest())); - } - - public boolean equals(Object o) { - if(!(o instanceof ConsensusDocumentImpl)) - return false; - final ConsensusDocumentImpl other = (ConsensusDocumentImpl) o; - return other.getSigningHash().equals(signingHash); - } - - public int hashCode() { - return (signingHash == null) ? 0 : signingHash.hashCode(); - } - - private int getParameterValue(String name, int defaultValue, int minValue, int maxValue) { - if(!parameters.containsKey(name)) { - return defaultValue; - } - final int value = parameters.get(name); - if(value < minValue) { - return minValue; - } else if(value > maxValue) { - return maxValue; - } else { - return value; - } - } - - private boolean getBooleanParameterValue(String name, boolean defaultValue) { - if(!parameters.containsKey(name)) { - return defaultValue; - } - final int value = parameters.get(name); - return value != 0; - } - - public int getCircWindowParameter() { - return getParameterValue(CIRCWINDOW_PARAM, CIRCWINDOW_DEFAULT, CIRCWINDOW_MIN, CIRCWINDOW_MAX); - } - - public int getWeightScaleParameter() { - return getParameterValue(BW_WEIGHT_SCALE_PARAM, BW_WEIGHT_SCALE_DEFAULT, BW_WEIGHT_SCALE_MIN, BW_WEIGHT_SCALE_MAX); - } - - public int getBandwidthWeight(String tag) { - if(bandwidthWeights.containsKey(tag)) { - return bandwidthWeights.get(tag); - } else { - return -1; - } - } - - public boolean getUseNTorHandshake() { - return getBooleanParameterValue(USE_NTOR_HANDSHAKE_PARAM, false); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentParser.java b/orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentParser.java deleted file mode 100644 index 71c2a4e2..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentParser.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.TorParsingException; -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 ConsensusDocumentParser implements DocumentParser { - public enum DocumentSection { NO_SECTION, PREAMBLE, AUTHORITY, ROUTER_STATUS, FOOTER }; - - // dir-spec.txt 3.2 - // Unlike other formats described above, a SP in these documents must be a - // single space character (hex 20). - private final static String ITEM_DELIMITER = " "; - - private final PreambleSectionParser preambleParser; - private final AuthoritySectionParser authorityParser; - private final RouterStatusSectionParser routerStatusParser; - private final FooterSectionParser footerParser; - private final DocumentFieldParser fieldParser; - private DocumentSection currentSection = DocumentSection.PREAMBLE; - private final ConsensusDocumentImpl document; - - private DocumentParsingResultHandler resultHandler; - - public ConsensusDocumentParser(DocumentFieldParser fieldParser) { - this.fieldParser = fieldParser; - initializeParser(); - - document = new ConsensusDocumentImpl(); - preambleParser = new PreambleSectionParser(fieldParser, document); - authorityParser = new AuthoritySectionParser(fieldParser, document); - routerStatusParser = new RouterStatusSectionParser(fieldParser, document); - footerParser = new FooterSectionParser(fieldParser, document); - } - - private void initializeParser() { - fieldParser.resetRawDocument(); - fieldParser.setHandler(createParsingHandler()); - fieldParser.setDelimiter(ITEM_DELIMITER); - fieldParser.setSignatureIgnoreToken("directory-signature"); - fieldParser.startSignedEntity(); - } - - public boolean parse(DocumentParsingResultHandler resultHandler) { - this.resultHandler = resultHandler; - try { - fieldParser.processDocument(); - return true; - } catch(TorParsingException e) { - resultHandler.parsingError(e.getMessage()); - return false; - } - } - - public DocumentParsingResult parse() { - final BasicDocumentParsingResult result = new BasicDocumentParsingResult(); - parse(result); - return result; - } - - private DocumentParsingHandler createParsingHandler() { - return new DocumentParsingHandler() { - - public void endOfDocument() { - document.setRawDocumentData(fieldParser.getRawDocument()); - resultHandler.documentParsed(document); - fieldParser.logDebug("Finished parsing status document."); - } - public void parseKeywordLine() { - processKeywordLine(); - } - - }; - } - private void processKeywordLine() { - DocumentSection newSection = null; - while(currentSection != DocumentSection.NO_SECTION) { - switch(currentSection) { - case PREAMBLE: - newSection = preambleParser.parseKeywordLine(); - break; - case AUTHORITY: - newSection = authorityParser.parseKeywordLine(); - break; - case ROUTER_STATUS: - newSection = routerStatusParser.parseKeywordLine(); - break; - case FOOTER: - newSection = footerParser.parseKeywordLine(); - break; - default: - break; - } - if(newSection == currentSection) - return; - - currentSection = newSection; - } - } - -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentSectionParser.java b/orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentSectionParser.java deleted file mode 100644 index 0190d320..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/ConsensusDocumentSectionParser.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import com.subgraph.orchid.directory.consensus.ConsensusDocumentParser.DocumentSection; -import com.subgraph.orchid.directory.parsing.DocumentFieldParser; - -public abstract class ConsensusDocumentSectionParser { - - protected final ConsensusDocumentImpl document; - protected final DocumentFieldParser fieldParser; - - - ConsensusDocumentSectionParser(DocumentFieldParser parser, ConsensusDocumentImpl document) { - this.fieldParser = parser; - this.document = document; - } - - DocumentSection parseKeywordLine() { - String keywordString = fieldParser.getCurrentKeyword(); - if(getNextStateKeyword() != null && getNextStateKeyword().equals(keywordString)) - return nextSection(); - - final DocumentKeyword keyword = DocumentKeyword.findKeyword(keywordString, getSection()); - /* - * dirspec.txt (1.2) - * When interpreting a Document, software MUST ignore any KeywordLine that - * starts with a keyword it doesn't recognize; - */ - if(!keyword.equals(DocumentKeyword.UNKNOWN_KEYWORD)) - parseLine(keyword); - - return getSection(); - } - - abstract void parseLine(DocumentKeyword keyword); - abstract String getNextStateKeyword(); - abstract DocumentSection getSection(); - abstract DocumentSection nextSection(); -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/DirectorySignature.java b/orchid/src/com/subgraph/orchid/directory/consensus/DirectorySignature.java deleted file mode 100644 index 305f828d..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/DirectorySignature.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import com.subgraph.orchid.crypto.TorSignature; -import com.subgraph.orchid.data.HexDigest; - -public class DirectorySignature { - - private final HexDigest identityDigest; - private final HexDigest signingKeyDigest; - private final TorSignature signature; - private final boolean useSha256; - - DirectorySignature(HexDigest identityDigest, HexDigest signingKeyDigest, TorSignature signature, boolean useSha256) { - this.identityDigest = identityDigest; - this.signingKeyDigest = signingKeyDigest; - this.signature = signature; - this.useSha256 = useSha256; - } - - public HexDigest getIdentityDigest() { - return identityDigest; - } - - public HexDigest getSigningKeyDigest() { - return signingKeyDigest; - } - - public TorSignature getSignature() { - return signature; - } - - public boolean useSha256() { - return useSha256; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/DocumentKeyword.java b/orchid/src/com/subgraph/orchid/directory/consensus/DocumentKeyword.java deleted file mode 100644 index 78913ac5..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/DocumentKeyword.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import com.subgraph.orchid.directory.consensus.ConsensusDocumentParser.DocumentSection; - -enum DocumentKeyword { - /* - * See dirspec.txt section 3.2 - */ - NETWORK_STATUS_VERSION("network-status-version", DocumentSection.PREAMBLE, 1), - VOTE_STATUS("vote-status", DocumentSection.PREAMBLE, 1), - CONSENSUS_METHODS("consensus-methods", DocumentSection.PREAMBLE, 1, true), - CONSENSUS_METHOD("consensus-method", DocumentSection.PREAMBLE, 1, false, true), - PUBLISHED("published", DocumentSection.PREAMBLE, 2, true), - VALID_AFTER("valid-after", DocumentSection.PREAMBLE,2), - FRESH_UNTIL("fresh-until", DocumentSection.PREAMBLE,2), - VALID_UNTIL("valid-until", DocumentSection.PREAMBLE,2), - VOTING_DELAY("voting-delay", DocumentSection.PREAMBLE,2), - CLIENT_VERSIONS("client-versions", DocumentSection.PREAMBLE,1), - SERVER_VERSIONS("server-versions", DocumentSection.PREAMBLE,1), - KNOWN_FLAGS("known-flags", DocumentSection.PREAMBLE), - PARAMS("params", DocumentSection.PREAMBLE), - - DIR_SOURCE("dir-source", DocumentSection.AUTHORITY, 6), - CONTACT("contact", DocumentSection.AUTHORITY), - VOTE_DIGEST("vote-digest", DocumentSection.AUTHORITY, 1, false, true), - - R("r", DocumentSection.ROUTER_STATUS, 8), - S("s", DocumentSection.ROUTER_STATUS), - V("v", DocumentSection.ROUTER_STATUS), - W("w", DocumentSection.ROUTER_STATUS, 1), - P("p", DocumentSection.ROUTER_STATUS, 2), - M("m", DocumentSection.ROUTER_STATUS, 1), - - DIRECTORY_FOOTER("directory-footer", DocumentSection.FOOTER), - BANDWIDTH_WEIGHTS("bandwidth-weights", DocumentSection.FOOTER, 19), - DIRECTORY_SIGNATURE("directory-signature", DocumentSection.FOOTER, 2), - - UNKNOWN_KEYWORD("KEYWORD NOT FOUND"); - - - public final static int VARIABLE_ARGUMENT_COUNT = -1; - - private final String keyword; - private final DocumentSection section; - private final int argumentCount; - private final boolean voteOnly; - private final boolean consensusOnly; - - - DocumentKeyword(String keyword) { - this(keyword, DocumentSection.NO_SECTION); - } - - DocumentKeyword(String keyword, DocumentSection section) { - this(keyword, section, VARIABLE_ARGUMENT_COUNT); - } - DocumentKeyword(String keyword, DocumentSection section, int argumentCount) { - this(keyword, section, argumentCount, false); - } - - DocumentKeyword(String keyword, DocumentSection section, int argumentCount, boolean voteOnly) { - this(keyword, section, argumentCount, voteOnly, false); - } - - - DocumentKeyword(String keyword, DocumentSection section, int argumentCount, boolean voteOnly, boolean consensusOnly) { - this.keyword = keyword; - this.section = section; - this.argumentCount = argumentCount; - this.voteOnly = voteOnly; - this.consensusOnly = consensusOnly; - } - - static DocumentKeyword findKeyword(String keyword, DocumentSection section) { - for(DocumentKeyword k : values()) { - if(k.getKeyword().equals(keyword) && k.getSection().equals(section)) - return k; - } - return UNKNOWN_KEYWORD; - } - - public String getKeyword() { - return keyword; - } - - public DocumentSection getSection() { - return section; - } - - public int getArgumentCount() { - return argumentCount; - } - - public boolean isConsensusOnly() { - return consensusOnly; - } - - public boolean isVoteOnly() { - return voteOnly; - } - - -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/FooterSectionParser.java b/orchid/src/com/subgraph/orchid/directory/consensus/FooterSectionParser.java deleted file mode 100644 index fcceb0f5..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/FooterSectionParser.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import com.subgraph.orchid.crypto.TorMessageDigest; -import com.subgraph.orchid.crypto.TorSignature; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.directory.consensus.ConsensusDocumentParser.DocumentSection; -import com.subgraph.orchid.directory.parsing.DocumentFieldParser; -import com.subgraph.orchid.directory.parsing.NameIntegerParameter; - -public class FooterSectionParser extends ConsensusDocumentSectionParser { - - private boolean seenFirstSignature = false; - - FooterSectionParser(DocumentFieldParser parser, ConsensusDocumentImpl document) { - super(parser, document); - } - - @Override - String getNextStateKeyword() { - return null; - } - - @Override - DocumentSection getSection() { - return DocumentSection.FOOTER; - } - - DocumentSection nextSection() { - return DocumentSection.NO_SECTION; - } - - @Override - void parseLine(DocumentKeyword keyword) { - switch(keyword) { - case BANDWIDTH_WEIGHTS: - processBandwidthWeights(); - break; - - case DIRECTORY_SIGNATURE: - processSignature(); - break; - - default: - break; - } - } - - private void doFirstSignature() { - seenFirstSignature = true; - fieldParser.endSignedEntity(); - final TorMessageDigest messageDigest = fieldParser.getSignatureMessageDigest(); - messageDigest.update("directory-signature "); - document.setSigningHash(messageDigest.getHexDigest()); - - TorMessageDigest messageDigest256 = fieldParser.getSignatureMessageDigest256(); - messageDigest256.update("directory-signature "); - document.setSigningHash256(messageDigest256.getHexDigest()); - } - - private void processSignature() { - if(!seenFirstSignature) { - doFirstSignature(); - } - final String s = fieldParser.parseString(); - final HexDigest identity; - boolean useSha256 = false; - if(s.length() < TorMessageDigest.TOR_DIGEST_SIZE) { - useSha256 = ("sha256".equals(s)); - identity = fieldParser.parseHexDigest(); - } else { - identity = HexDigest.createFromString(s); - } - HexDigest signingKey = fieldParser.parseHexDigest(); - TorSignature signature = fieldParser.parseSignature(); - document.addSignature(new DirectorySignature(identity, signingKey, signature, useSha256)); - } - - private void processBandwidthWeights() { - final int remaining = fieldParser.argumentsRemaining(); - for(int i = 0; i < remaining; i++) { - NameIntegerParameter p = fieldParser.parseParameter(); - document.addBandwidthWeight(p.getName(), p.getValue()); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/PreambleSectionParser.java b/orchid/src/com/subgraph/orchid/directory/consensus/PreambleSectionParser.java deleted file mode 100644 index f67d0f0a..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/PreambleSectionParser.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import java.util.Arrays; -import java.util.List; - -import com.subgraph.orchid.ConsensusDocument.ConsensusFlavor; -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.directory.consensus.ConsensusDocumentParser.DocumentSection; -import com.subgraph.orchid.directory.parsing.DocumentFieldParser; -import com.subgraph.orchid.directory.parsing.NameIntegerParameter; - -public class PreambleSectionParser extends ConsensusDocumentSectionParser { - private final static int CURRENT_DOCUMENT_VERSION = 3; - private boolean isFirstLine = true; - - PreambleSectionParser(DocumentFieldParser parser, ConsensusDocumentImpl document) { - super(parser, document); - } - - String getNextStateKeyword() { - return "dir-source"; - } - - DocumentSection getSection() { - return DocumentSection.PREAMBLE; - } - - DocumentSection nextSection() { - return DocumentSection.AUTHORITY; - } - - @Override - void parseLine(DocumentKeyword keyword) { - if(isFirstLine) { - parseFirstLine(keyword); - } else { - processKeyword(keyword); - } - } - - private void processKeyword(DocumentKeyword keyword) { - switch(keyword) { - case NETWORK_STATUS_VERSION: - throw new TorParsingException("Network status version may only appear on the first line of status document"); - case VOTE_STATUS: - final String voteStatus = fieldParser.parseString(); - if(!voteStatus.equals("consensus")) - throw new TorParsingException("Unexpected vote-status type: "+ voteStatus); - break; - case CONSENSUS_METHOD: - document.setConsensusMethod(fieldParser.parseInteger()); - break; - - case VALID_AFTER: - document.setValidAfter(fieldParser.parseTimestamp()); - break; - - case FRESH_UNTIL: - document.setFreshUntil(fieldParser.parseTimestamp()); - break; - - case VALID_UNTIL: - document.setValidUntil(fieldParser.parseTimestamp()); - break; - - case VOTING_DELAY: - document.setVoteDelaySeconds(fieldParser.parseInteger()); - document.setDistDelaySeconds(fieldParser.parseInteger()); - break; - - case CLIENT_VERSIONS: - for(String version: parseVersions(fieldParser.parseString())) - document.addClientVersion(version); - break; - case SERVER_VERSIONS: - for(String version: parseVersions(fieldParser.parseString())) - document.addServerVersion(version); - break; - case KNOWN_FLAGS: - while(fieldParser.argumentsRemaining() > 0) - document.addKnownFlag(fieldParser.parseString()); - break; - - case PARAMS: - parseParams(); - break; - - default: - break; - } - - } - - private void parseFirstLine(DocumentKeyword keyword) { - if(keyword != DocumentKeyword.NETWORK_STATUS_VERSION) - throw new TorParsingException("network-status-version not found at beginning of consensus document as expected."); - - final int documentVersion = fieldParser.parseInteger(); - - if(documentVersion != CURRENT_DOCUMENT_VERSION) - throw new TorParsingException("Unexpected consensus document version number: " + documentVersion); - - if(fieldParser.argumentsRemaining() > 0) { - parseConsensusFlavor(); - } - isFirstLine = false; - } - - private void parseConsensusFlavor() { - final String flavor = fieldParser.parseString(); - if("ns".equals(flavor)) { - document.setConsensusFlavor(ConsensusFlavor.NS); - } else if("microdesc".equals(flavor)) { - document.setConsensusFlavor(ConsensusFlavor.MICRODESC); - } else { - fieldParser.logWarn("Unknown consensus flavor: "+ flavor); - } - } - - private List parseVersions(String versions) { - return Arrays.asList(versions.split(",")); - } - - private void parseParams() { - final int remaining = fieldParser.argumentsRemaining(); - for(int i = 0; i < remaining; i++) { - NameIntegerParameter p = fieldParser.parseParameter(); - document.addParameter(p.getName(), p.getValue()); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/RequiredCertificateImpl.java b/orchid/src/com/subgraph/orchid/directory/consensus/RequiredCertificateImpl.java deleted file mode 100644 index f706ecae..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/RequiredCertificateImpl.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.data.HexDigest; - -public class RequiredCertificateImpl implements ConsensusDocument.RequiredCertificate { - - private final HexDigest identity; - private final HexDigest signingKey; - - private int downloadFailureCount; - - public RequiredCertificateImpl(HexDigest identity, HexDigest signingKey) { - this.identity = identity; - this.signingKey = signingKey; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result - + ((identity == null) ? 0 : identity.hashCode()); - result = prime * result - + ((signingKey == null) ? 0 : signingKey.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - RequiredCertificateImpl other = (RequiredCertificateImpl) obj; - if (identity == null) { - if (other.identity != null) - return false; - } else if (!identity.equals(other.identity)) - return false; - if (signingKey == null) { - if (other.signingKey != null) - return false; - } else if (!signingKey.equals(other.signingKey)) - return false; - return true; - } - - public void incrementDownloadFailureCount() { - downloadFailureCount += 1; - } - - public int getDownloadFailureCount() { - return downloadFailureCount; - } - - public HexDigest getAuthorityIdentity() { - return identity; - } - - public HexDigest getSigningKey() { - return signingKey; - } - - - -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/RouterStatusImpl.java b/orchid/src/com/subgraph/orchid/directory/consensus/RouterStatusImpl.java deleted file mode 100644 index 4fe9a896..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/RouterStatusImpl.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import java.util.HashSet; -import java.util.Set; - -import com.subgraph.orchid.RouterStatus; -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 class RouterStatusImpl implements RouterStatus { - - private String nickname; - private HexDigest identity; - private HexDigest digest; - private HexDigest microdescriptorDigest; - private Timestamp publicationTime; - private IPv4Address address; - private int routerPort; - private int directoryPort; - private Set flags = new HashSet(); - private String version; - private int bandwidthEstimate; - private int bandwidthMeasured; - private boolean hasBandwidth; - private ExitPorts exitPorts; - - void setNickname(String nickname) { this.nickname = nickname; } - void setIdentity(HexDigest identity) { this.identity = identity; } - void setDigest(HexDigest digest) { this.digest = digest; } - void setMicrodescriptorDigest(HexDigest digest) { this.microdescriptorDigest = digest; } - void setPublicationTime(Timestamp timestamp) { this.publicationTime = timestamp; } - void setAddress(IPv4Address address) { this.address = address; } - void setRouterPort(int port) { this.routerPort = port; } - void setDirectoryPort(int port) { this.directoryPort = port; } - void addFlag(String flag) { this.flags.add(flag); } - void setVersion(String version) { this.version = version; } - void setEstimatedBandwidth(int bandwidth) { this.bandwidthEstimate = bandwidth; hasBandwidth = true; } - void setMeasuredBandwidth(int bandwidth) { this.bandwidthMeasured = bandwidth; } - void setAcceptedPorts(String portList) { this.exitPorts = ExitPorts.createAcceptExitPorts(portList); } - void setRejectedPorts(String portList) { this.exitPorts = ExitPorts.createRejectExitPorts(portList); } - - public String toString() { - return "Router: ("+ nickname +" "+ identity +" "+ digest +" "+ address +" "+ routerPort +" " + directoryPort - +" "+ version +" "+ exitPorts +")"; - } - public String getNickname() { - return nickname; - } - - public HexDigest getIdentity() { - return identity; - } - - public HexDigest getDescriptorDigest() { - return digest; - } - - public HexDigest getMicrodescriptorDigest() { - return microdescriptorDigest; - } - - public Timestamp getPublicationTime() { - return publicationTime; - } - - public IPv4Address getAddress() { - return address; - } - - public int getRouterPort() { - return routerPort; - } - - public boolean isDirectory() { - return directoryPort != 0; - } - public int getDirectoryPort() { - return directoryPort; - } - - public boolean hasFlag(String flag) { - return flags.contains(flag); - } - - public String getVersion() { - return version; - } - - public boolean hasBandwidth() { - return hasBandwidth; - } - - public int getEstimatedBandwidth() { - return bandwidthEstimate; - } - - public int getMeasuredBandwidth() { - return bandwidthMeasured; - } - - public ExitPorts getExitPorts() { - return exitPorts; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/RouterStatusSectionParser.java b/orchid/src/com/subgraph/orchid/directory/consensus/RouterStatusSectionParser.java deleted file mode 100644 index 79f4078c..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/RouterStatusSectionParser.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import com.subgraph.orchid.ConsensusDocument.ConsensusFlavor; -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.crypto.TorMessageDigest; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.directory.consensus.ConsensusDocumentParser.DocumentSection; -import com.subgraph.orchid.directory.parsing.DocumentFieldParser; - -public class RouterStatusSectionParser extends ConsensusDocumentSectionParser { - - private RouterStatusImpl currentEntry = null; - - RouterStatusSectionParser(DocumentFieldParser parser, ConsensusDocumentImpl document) { - super(parser, document); - } - - @Override - void parseLine(DocumentKeyword keyword) { - if(!keyword.equals(DocumentKeyword.R)) - assertCurrentEntry(); - switch(keyword) { - case R: - parseFirstLine(); - break; - case S: - parseFlags(); - break; - case V: - parseVersion(); - break; - case W: - parseBandwidth(); - break; - case P: - parsePortList(); - break; - case M: - parseMicrodescriptorHash(); - break; - default: - break; - } - } - - private void assertCurrentEntry() { - if(currentEntry == null) - throw new TorParsingException("Router status entry must begin with an 'r' line"); - } - - private void addCurrentEntry() { - assertCurrentEntry(); - document.addRouterStatusEntry(currentEntry); - currentEntry = null; - } - - private void parseFirstLine() { - if(currentEntry != null) - throw new TorParsingException("Unterminated router status entry."); - currentEntry = new RouterStatusImpl(); - currentEntry.setNickname(fieldParser.parseNickname()); - currentEntry.setIdentity(parseBase64Digest()); - if(document.getFlavor() != ConsensusFlavor.MICRODESC) { - currentEntry.setDigest(parseBase64Digest()); - } - currentEntry.setPublicationTime(fieldParser.parseTimestamp()); - currentEntry.setAddress(fieldParser.parseAddress()); - currentEntry.setRouterPort(fieldParser.parsePort()); - currentEntry.setDirectoryPort(fieldParser.parsePort()); - } - - private HexDigest parseBase64Digest() { - return HexDigest.createFromDigestBytes(fieldParser.parseBase64Data()); - } - - private void parseFlags() { - while(fieldParser.argumentsRemaining() > 0) - currentEntry.addFlag(fieldParser.parseString()); - } - - private void parseVersion() { - currentEntry.setVersion(fieldParser.parseConcatenatedString()); - } - - private void parseBandwidth() { - while(fieldParser.argumentsRemaining() > 0) { - final String[] parts = fieldParser.parseString().split("="); - if(parts.length == 2) - parseBandwidthItem(parts[0], fieldParser.parseInteger(parts[1])); - } - if(document.getFlavor() == ConsensusFlavor.MICRODESC) { - addCurrentEntry(); - } - } - - private void parseBandwidthItem(String key, int value) { - if(key.equals("Bandwidth")) - currentEntry.setEstimatedBandwidth(value); - else if(key.equals("Measured")) - currentEntry.setMeasuredBandwidth(value); - } - - private void parsePortList() { - if(document.getFlavor() == ConsensusFlavor.MICRODESC) { - throw new TorParsingException("'p' line does not appear in consensus flavor 'microdesc'"); - } - final String arg = fieldParser.parseString(); - if(arg.equals("accept")) { - currentEntry.setAcceptedPorts(fieldParser.parseString()); - } else if(arg.equals("reject")) { - currentEntry.setRejectedPorts(fieldParser.parseString()); - } - addCurrentEntry(); - } - - private void parseMicrodescriptorHash() { - if(document.getFlavor() != ConsensusFlavor.MICRODESC) { - throw new TorParsingException("'m' line is invalid unless consensus flavor is microdesc"); - } - final byte[] hashBytes = fieldParser.parseBase64Data(); - if(hashBytes.length != TorMessageDigest.TOR_DIGEST256_SIZE) { - throw new TorParsingException("'m' line has incorrect digest size "+ hashBytes.length +" != "+ TorMessageDigest.TOR_DIGEST256_SIZE); - } - currentEntry.setMicrodescriptorDigest(HexDigest.createFromDigestBytes(hashBytes)); - } - - @Override - String getNextStateKeyword() { - return "directory-footer"; - } - - @Override - DocumentSection getSection() { - return DocumentSection.ROUTER_STATUS; - } - - DocumentSection nextSection() { - return DocumentSection.FOOTER; - } - -} diff --git a/orchid/src/com/subgraph/orchid/directory/consensus/VoteAuthorityEntryImpl.java b/orchid/src/com/subgraph/orchid/directory/consensus/VoteAuthorityEntryImpl.java deleted file mode 100644 index f9be7a49..00000000 --- a/orchid/src/com/subgraph/orchid/directory/consensus/VoteAuthorityEntryImpl.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.subgraph.orchid.directory.consensus; - -import java.util.ArrayList; -import java.util.List; - -import com.subgraph.orchid.VoteAuthorityEntry; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; - -public class VoteAuthorityEntryImpl implements VoteAuthorityEntry { - private String nickname; - private HexDigest identity; - private String hostname; - private IPv4Address address; - private int dirport = -1; - private int orport = -1; - - private String contact; - private HexDigest voteDigest; - - private final List signatures = new ArrayList(); - - void setNickname(String nickname) { this.nickname = nickname; } - void setIdentity(HexDigest identity) { this.identity = identity; } - void setHostname(String hostname) { this.hostname = hostname; } - void setAddress(IPv4Address address) { this.address = address; } - void setDirectoryPort(int port) { this.dirport = port; } - void setRouterPort(int port) { this.orport = port; } - void setContact(String contact) { this.contact = contact; } - void setVoteDigest(HexDigest digest) { this.voteDigest = digest; } - - public String getNickname() { - return nickname; - } - - public HexDigest getIdentity() { - return identity; - } - - public String getHostname() { - return hostname; - } - - public IPv4Address getAddress() { - return address; - } - - public int getDirectoryPort() { - return dirport; - } - - public int getRouterPort() { - return orport; - } - - public String getContact() { - return contact; - } - - public HexDigest getVoteDigest() { - return voteDigest; - } - - public List getSignatures() { - return signatures; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/BridgeDescriptorFetcher.java b/orchid/src/com/subgraph/orchid/directory/downloader/BridgeDescriptorFetcher.java deleted file mode 100644 index 4514bbd6..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/BridgeDescriptorFetcher.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.nio.ByteBuffer; - -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.directory.parsing.DocumentParser; - -public class BridgeDescriptorFetcher extends DocumentFetcher{ - - @Override - String getRequestPath() { - return "/tor/server/authority"; - } - - @Override - DocumentParser createParser(ByteBuffer response) { - return PARSER_FACTORY.createRouterDescriptorParser(response, true); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/CertificateFetcher.java b/orchid/src/com/subgraph/orchid/directory/downloader/CertificateFetcher.java deleted file mode 100644 index 799d8e9a..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/CertificateFetcher.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.nio.ByteBuffer; -import java.util.Set; - -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.ConsensusDocument.RequiredCertificate; -import com.subgraph.orchid.directory.parsing.DocumentParser; - -public class CertificateFetcher extends DocumentFetcher{ - - private final Set requiredCertificates; - - public CertificateFetcher(Set requiredCertificates) { - this.requiredCertificates = requiredCertificates; - } - - @Override - String getRequestPath() { - return "/tor/keys/fp-sk/"+ getRequiredCertificatesRequestString(); - } - - private String getRequiredCertificatesRequestString() { - final StringBuilder sb = new StringBuilder(); - for(RequiredCertificate rc: requiredCertificates) { - if(sb.length() > 0) { - sb.append("+"); - } - sb.append(rc.getAuthorityIdentity().toString()); - sb.append("-"); - sb.append(rc.getSigningKey().toString()); - } - return sb.toString(); - } - - @Override - DocumentParser createParser(ByteBuffer response) { - return PARSER_FACTORY.createKeyCertificateParser(response); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/ConsensusFetcher.java b/orchid/src/com/subgraph/orchid/directory/downloader/ConsensusFetcher.java deleted file mode 100644 index 9e92e09f..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/ConsensusFetcher.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.nio.ByteBuffer; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.directory.parsing.DocumentParser; - -public class ConsensusFetcher extends DocumentFetcher{ - - private final static String CONSENSUS_BASE_PATH = "/tor/status-vote/current/"; - - private final boolean useMicrodescriptors; - - - public ConsensusFetcher(boolean useMicrodescriptors) { - this.useMicrodescriptors = useMicrodescriptors; - } - - @Override - String getRequestPath() { - return CONSENSUS_BASE_PATH + ((useMicrodescriptors) ? - ("consensus-microdesc") : ("consensus")); - } - - @Override - DocumentParser createParser(ByteBuffer response) { - return PARSER_FACTORY.createConsensusDocumentParser(response); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/DescriptorProcessor.java b/orchid/src/com/subgraph/orchid/directory/downloader/DescriptorProcessor.java deleted file mode 100644 index 175d1b4b..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/DescriptorProcessor.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.TorConfig.AutoBoolValue; -import com.subgraph.orchid.data.HexDigest; - -public class DescriptorProcessor { - private final static int MAX_DL_PER_REQUEST = 96; - private final static int MAX_DL_TO_DELAY = 16; - private final static int MIN_DL_REQUESTS = 3; - private final static int MAX_CLIENT_INTERVAL_WITHOUT_REQUEST = 10 * 60 * 1000; - - private final TorConfig config; - private final Directory directory; - - private Date lastDescriptorDownload; - - - DescriptorProcessor(TorConfig config, Directory directory) { - this.config = config; - this.directory = directory; - } - - private boolean canDownloadDescriptors(int downloadableCount) { - if(downloadableCount >= MAX_DL_TO_DELAY) - return true; - if(downloadableCount == 0) - return false; - if(lastDescriptorDownload == null) - return true; - final Date now = new Date(); - final long diff = now.getTime() - lastDescriptorDownload.getTime(); - return diff > MAX_CLIENT_INTERVAL_WITHOUT_REQUEST; - } - - /* - * dir-spec.txt section 5.3 - */ - private List< List > partitionDescriptors(List descriptors) { - final int size = descriptors.size(); - final List< List > partitions = new ArrayList< List >(); - if(size <= 10) { - partitions.add(createPartitionList(descriptors, 0, size)); - return partitions; - } else if(size <= (MIN_DL_REQUESTS * MAX_DL_PER_REQUEST)) { - final int chunk = size / MIN_DL_REQUESTS; - int over = size % MIN_DL_REQUESTS; - int off = 0; - for(int i = 0; i < MIN_DL_REQUESTS; i++) { - int sz = chunk; - if(over != 0) { - sz++; - over--; - } - partitions.add(createPartitionList(descriptors, off, sz)); - off += sz; - } - return partitions; - - } else { - int off = 0; - while(off < descriptors.size()) { - partitions.add(createPartitionList(descriptors, off, MAX_DL_PER_REQUEST)); - off += MAX_DL_PER_REQUEST; - } - return partitions; - } - } - - private List createPartitionList(List descriptors, int offset, int size) { - final List newList = new ArrayList(); - for(int i = offset; i < (offset + size) && i < descriptors.size(); i++) { - final HexDigest digest = getDescriptorDigestForRouter(descriptors.get(i)); - newList.add(digest); - } - return newList; - } - - private HexDigest getDescriptorDigestForRouter(Router r) { - if(useMicrodescriptors()) { - return r.getMicrodescriptorDigest(); - } else { - return r.getDescriptorDigest(); - } - } - - private boolean useMicrodescriptors() { - return config.getUseMicrodescriptors() != AutoBoolValue.FALSE; - } - - List< List > getDescriptorDigestsToDownload() { - final ConsensusDocument consensus = directory.getCurrentConsensusDocument(); - if(consensus == null || !consensus.isLive()) { - return Collections.emptyList(); - } - final List downloadables = directory.getRoutersWithDownloadableDescriptors(); - if(!canDownloadDescriptors(downloadables.size())) { - return Collections.emptyList(); - } - - lastDescriptorDownload = new Date(); - return partitionDescriptors(downloadables); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDocumentRequestor.java b/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDocumentRequestor.java deleted file mode 100644 index 347c38c5..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDocumentRequestor.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.io.IOException; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeoutException; - -import com.subgraph.orchid.CircuitManager; -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.ConsensusDocument.RequiredCertificate; -import com.subgraph.orchid.DirectoryCircuit; -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.RouterMicrodescriptor; -import com.subgraph.orchid.Stream; -import com.subgraph.orchid.StreamConnectFailedException; -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.circuits.TorInitializationTracker; -import com.subgraph.orchid.data.HexDigest; - -/** - * Synchronously downloads directory documents. - */ -public class DirectoryDocumentRequestor { - private final static int OPEN_DIRECTORY_STREAM_TIMEOUT = 10 * 1000; - - private final DirectoryCircuit circuit; - private final TorInitializationTracker initializationTracker; - - - public DirectoryDocumentRequestor(DirectoryCircuit circuit) { - this(circuit, null); - } - - public DirectoryDocumentRequestor(DirectoryCircuit circuit, TorInitializationTracker initializationTracker) { - this.circuit = circuit; - this.initializationTracker = initializationTracker; - } - - public RouterDescriptor downloadBridgeDescriptor(Router bridge) throws DirectoryRequestFailedException { - return fetchSingleDocument(new BridgeDescriptorFetcher()); - } - - public ConsensusDocument downloadCurrentConsensus(boolean useMicrodescriptors) throws DirectoryRequestFailedException { - return fetchSingleDocument(new ConsensusFetcher(useMicrodescriptors), CircuitManager.DIRECTORY_PURPOSE_CONSENSUS); - } - - public List downloadKeyCertificates(Set required) throws DirectoryRequestFailedException { - return fetchDocuments(new CertificateFetcher(required), CircuitManager.DIRECTORY_PURPOSE_CERTIFICATES); - } - - public List downloadRouterDescriptors(Set fingerprints) throws DirectoryRequestFailedException { - return fetchDocuments(new RouterDescriptorFetcher(fingerprints), CircuitManager.DIRECTORY_PURPOSE_DESCRIPTORS); - } - - public List downloadRouterMicrodescriptors(Set fingerprints) throws DirectoryRequestFailedException { - return fetchDocuments(new MicrodescriptorFetcher(fingerprints), CircuitManager.DIRECTORY_PURPOSE_DESCRIPTORS); - } - - private T fetchSingleDocument(DocumentFetcher fetcher) throws DirectoryRequestFailedException { - return fetchSingleDocument(fetcher, 0); - } - - private T fetchSingleDocument(DocumentFetcher fetcher, int purpose) throws DirectoryRequestFailedException { - final List result = fetchDocuments(fetcher, purpose); - if(result.size() == 1) { - return result.get(0); - } - return null; - } - - private List fetchDocuments(DocumentFetcher fetcher, int purpose) throws DirectoryRequestFailedException { - try { - final HttpConnection http = createHttpConnection(purpose); - try { - return fetcher.requestDocuments(http); - } finally { - http.close(); - } - } catch (TimeoutException e) { - throw new DirectoryRequestFailedException("Directory request timed out"); - } catch (StreamConnectFailedException e) { - throw new DirectoryRequestFailedException("Failed to open directory stream", e); - } catch (IOException e) { - throw new DirectoryRequestFailedException("I/O exception processing directory request", e); - } catch (InterruptedException e) { - throw new DirectoryRequestFailedException("Directory request interrupted"); - } - } - - private HttpConnection createHttpConnection(int purpose) throws InterruptedException, TimeoutException, StreamConnectFailedException { - return new HttpConnection(openDirectoryStream(purpose)); - } - - private Stream openDirectoryStream(int purpose) throws InterruptedException, TimeoutException, StreamConnectFailedException { - final int requestEventCode = purposeToEventCode(purpose, false); - final int loadingEventCode = purposeToEventCode(purpose, true); - - notifyInitialization(requestEventCode); - - final Stream stream = circuit.openDirectoryStream(OPEN_DIRECTORY_STREAM_TIMEOUT, true); - notifyInitialization(loadingEventCode); - return stream; - } - - private int purposeToEventCode(int purpose, boolean getLoadingEvent) { - switch(purpose) { - case CircuitManager.DIRECTORY_PURPOSE_CONSENSUS: - return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_STATUS : Tor.BOOTSTRAP_STATUS_REQUESTING_STATUS; - case CircuitManager.DIRECTORY_PURPOSE_CERTIFICATES: - return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_KEYS : Tor.BOOTSTRAP_STATUS_REQUESTING_KEYS; - case CircuitManager.DIRECTORY_PURPOSE_DESCRIPTORS: - return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_DESCRIPTORS : Tor.BOOTSTRAP_STATUS_REQUESTING_DESCRIPTORS; - default: - return 0; - } - } - - private void notifyInitialization(int code) { - if(code > 0 && initializationTracker != null) { - initializationTracker.notifyEvent(code); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDownloadTask.java b/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDownloadTask.java deleted file mode 100644 index 4aa71735..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDownloadTask.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Logger; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.DirectoryDownloader; -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.Threading; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.TorConfig.AutoBoolValue; -import com.subgraph.orchid.crypto.TorRandom; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.Timestamp; - -public class DirectoryDownloadTask implements Runnable { - private final static Logger logger = Logger.getLogger(DirectoryDownloadTask.class.getName()); - - private final TorConfig config; - private final Directory directory; - - private final DirectoryDownloader downloader; - - private final TorRandom random; - private final DescriptorProcessor descriptorProcessor; - - private final ExecutorService executor = Threading.newPool("DirectoryDownloadTask worker"); - - private volatile boolean isDownloadingCertificates; - private volatile boolean isDownloadingConsensus; - private final AtomicInteger outstandingDescriptorTasks; - - private ConsensusDocument currentConsensus; - private Date consensusDownloadTime; - - private volatile boolean isStopped; - - DirectoryDownloadTask(TorConfig config, Directory directory, DirectoryDownloader downloader) { - this.config = config; - this.directory = directory; - this.downloader = downloader; - this.random = new TorRandom(); - this.outstandingDescriptorTasks = new AtomicInteger(); - this.descriptorProcessor = new DescriptorProcessor(config, directory); - } - - public synchronized void stop() { - if(isStopped) { - return; - } - executor.shutdownNow(); - isStopped = true; - } - - public void run() { - directory.loadFromStore(); - directory.waitUntilLoaded(); - setCurrentConsensus(directory.getCurrentConsensusDocument()); - while (!isStopped) { - checkCertificates(); - checkConsensus(); - checkDescriptors(); - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - } - } - - private void checkCertificates() { - if (isDownloadingCertificates - || directory.getRequiredCertificates().isEmpty()) { - return; - } - - isDownloadingCertificates = true; - executor.execute(new DownloadCertificatesTask()); - } - - void setCurrentConsensus(ConsensusDocument consensus) { - if (consensus != null) { - currentConsensus = consensus; - consensusDownloadTime = chooseDownloadTimeForConsensus(consensus); - } else { - currentConsensus = null; - consensusDownloadTime = null; - } - } - - /* - * dir-spec 5.1: Downloading network-status documents - * - * To avoid swarming the caches whenever a consensus expires, the clients - * download new consensuses at a randomly chosen time after the caches are - * expected to have a fresh consensus, but before their consensus will - * expire. (This time is chosen uniformly at random from the interval - * between the time 3/4 into the first interval after the consensus is no - * longer fresh, and 7/8 of the time remaining after that before the - * consensus is invalid.) - * - * [For example, if a cache has a consensus that became valid at 1:00, and - * is fresh until 2:00, and expires at 4:00, that cache will fetch a new - * consensus at a random time between 2:45 and 3:50, since 3/4 of the - * one-hour interval is 45 minutes, and 7/8 of the remaining 75 minutes is - * 65 minutes.] - */ - private Date chooseDownloadTimeForConsensus(ConsensusDocument consensus) { - final long va = getMilliseconds(consensus.getValidAfterTime()); - final long fu = getMilliseconds(consensus.getFreshUntilTime()); - final long vu = getMilliseconds(consensus.getValidUntilTime()); - final long i1 = fu - va; - final long start = fu + ((i1 * 3) / 4); - final long i2 = ((vu - start) * 7) / 8; - final long r = random.nextLong(i2); - final long download = start + r; - return new Date(download); - } - - private boolean needConsensusDownload() { - if(directory.hasPendingConsensus()) { - return false; - } - if (currentConsensus == null || !currentConsensus.isLive()) { - if(currentConsensus == null) { - logger.info("Downloading consensus because we have no consensus document"); - } else { - logger.info("Downloading consensus because the document we have is not live"); - } - return true; - } - return consensusDownloadTime.before(new Date()); - } - - private long getMilliseconds(Timestamp ts) { - return ts.getDate().getTime(); - } - - private void checkConsensus() { - if (isDownloadingConsensus || !needConsensusDownload()) { - return; - } - - isDownloadingConsensus = true; - executor.execute(new DownloadConsensusTask()); - } - - private void checkDescriptors() { - if (outstandingDescriptorTasks.get() > 0) { - return; - } - List> ds = descriptorProcessor - .getDescriptorDigestsToDownload(); - if (ds.isEmpty()) { - return; - } - for (List dlist : ds) { - outstandingDescriptorTasks.incrementAndGet(); - executor.execute(new DownloadRouterDescriptorsTask(dlist, useMicrodescriptors())); - } - } - - private boolean useMicrodescriptors() { - return config.getUseMicrodescriptors() != AutoBoolValue.FALSE; - } - - private class DownloadConsensusTask implements Runnable { - public void run() { - try { - final ConsensusDocument consensus = downloader.downloadCurrentConsensus(useMicrodescriptors()); - setCurrentConsensus(consensus); - directory.addConsensusDocument(consensus, false); - - } catch (DirectoryRequestFailedException e) { - logger.warning("Failed to download current consensus document: "+ e.getMessage()); - } finally { - isDownloadingConsensus = false; - } - } - } - - private class DownloadRouterDescriptorsTask implements Runnable { - private final Set fingerprints; - private final boolean useMicrodescriptors; - - public DownloadRouterDescriptorsTask(Collection fingerprints, boolean useMicrodescriptors) { - this.fingerprints = new HashSet(fingerprints); - this.useMicrodescriptors = useMicrodescriptors; - } - - public void run() { - try { - if(useMicrodescriptors) { - directory.addRouterMicrodescriptors(downloader.downloadRouterMicrodescriptors(fingerprints)); - } else { - directory.addRouterDescriptors(downloader.downloadRouterDescriptors(fingerprints)); - } - } catch (DirectoryRequestFailedException e) { - logger.warning("Failed to download router descriptors: "+ e.getMessage()); - } finally { - outstandingDescriptorTasks.decrementAndGet(); - } - } - } - - private class DownloadCertificatesTask implements Runnable { - public void run() { - try { - for(KeyCertificate c: downloader.downloadKeyCertificates(directory.getRequiredCertificates())) { - directory.addCertificate(c); - } - directory.storeCertificates(); - } catch (DirectoryRequestFailedException e) { - logger.warning("Failed to download key certificates: "+ e.getMessage()); - } finally { - isDownloadingCertificates = false; - } - } - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDownloaderImpl.java b/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDownloaderImpl.java deleted file mode 100644 index 389ba25e..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryDownloaderImpl.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Set; -import java.util.logging.Logger; - -import com.subgraph.orchid.CircuitManager; -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.ConsensusDocument.RequiredCertificate; -import com.subgraph.orchid.Descriptor; -import com.subgraph.orchid.Directory; -import com.subgraph.orchid.DirectoryCircuit; -import com.subgraph.orchid.DirectoryDownloader; -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.OpenFailedException; -import com.subgraph.orchid.Router; -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.RouterMicrodescriptor; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.circuits.TorInitializationTracker; -import com.subgraph.orchid.data.HexDigest; - -public class DirectoryDownloaderImpl implements DirectoryDownloader { - private final static Logger logger = Logger.getLogger(DirectoryDownloaderImpl.class.getName()); - - private final TorConfig config; - private final TorInitializationTracker initializationTracker; - private CircuitManager circuitManager; - private boolean isStarted; - private boolean isStopped; - private DirectoryDownloadTask downloadTask; - private Thread downloadTaskThread; - - - public DirectoryDownloaderImpl(TorConfig config, TorInitializationTracker initializationTracker) { - this.config = config; - this.initializationTracker = initializationTracker; - } - - public void setCircuitManager(CircuitManager circuitManager) { - this.circuitManager = circuitManager; - } - - public synchronized void start(Directory directory) { - if(isStarted) { - logger.warning("Directory downloader already running"); - return; - } - if(circuitManager == null) { - throw new IllegalStateException("Must set CircuitManager instance with setCircuitManager() before starting."); - } - - downloadTask = new DirectoryDownloadTask(config, directory, this); - downloadTaskThread = new Thread(downloadTask); - downloadTaskThread.start(); - isStarted = true; - } - - public synchronized void stop() { - if(!isStarted || isStopped) { - return; - } - downloadTask.stop(); - downloadTaskThread.interrupt(); - } - - public RouterDescriptor downloadBridgeDescriptor(Router bridge) throws DirectoryRequestFailedException { - final DirectoryDocumentRequestor requestor = new DirectoryDocumentRequestor(openBridgeCircuit(bridge)); - return requestor.downloadBridgeDescriptor(bridge); - } - - - public ConsensusDocument downloadCurrentConsensus(boolean useMicrodescriptors) throws DirectoryRequestFailedException { - return downloadCurrentConsensus(useMicrodescriptors, openCircuit()); - } - - public ConsensusDocument downloadCurrentConsensus(boolean useMicrodescriptors, DirectoryCircuit circuit) throws DirectoryRequestFailedException { - final DirectoryDocumentRequestor requestor = new DirectoryDocumentRequestor(circuit, initializationTracker); - return requestor.downloadCurrentConsensus(useMicrodescriptors); - } - - public List downloadKeyCertificates(Set required) throws DirectoryRequestFailedException { - return downloadKeyCertificates(required, openCircuit()); - } - - public List downloadKeyCertificates(Set required, DirectoryCircuit circuit) throws DirectoryRequestFailedException { - final DirectoryDocumentRequestor requestor = new DirectoryDocumentRequestor(circuit, initializationTracker); - return requestor.downloadKeyCertificates(required); - } - - public List downloadRouterDescriptors(Set fingerprints) throws DirectoryRequestFailedException { - return downloadRouterDescriptors(fingerprints, openCircuit()); - } - - public List downloadRouterDescriptors(Set fingerprints, DirectoryCircuit circuit) throws DirectoryRequestFailedException { - final DirectoryDocumentRequestor requestor = new DirectoryDocumentRequestor(circuit, initializationTracker); - final List ds = requestor.downloadRouterDescriptors(fingerprints); - return removeUnrequestedDescriptors(fingerprints, ds); - } - - public List downloadRouterMicrodescriptors(Set fingerprints) throws DirectoryRequestFailedException { - return downloadRouterMicrodescriptors(fingerprints, openCircuit()); - } - - public List downloadRouterMicrodescriptors(Set fingerprints, DirectoryCircuit circuit) throws DirectoryRequestFailedException { - final DirectoryDocumentRequestor requestor = new DirectoryDocumentRequestor(circuit, initializationTracker); - final List ds = requestor.downloadRouterMicrodescriptors(fingerprints); - return removeUnrequestedDescriptors(fingerprints, ds); - } - - private List removeUnrequestedDescriptors(Set requested, List received) { - final List result = new ArrayList(); - int unrequestedCount = 0; - for(T d: received) { - if(requested.contains(d.getDescriptorDigest())) { - result.add(d); - } else { - unrequestedCount += 1; - } - } - if(unrequestedCount > 0) { - logger.warning("Discarding "+ unrequestedCount + " received descriptor(s) with fingerprints that did not match requested descriptors"); - } - return result; - } - - private DirectoryCircuit openCircuit() throws DirectoryRequestFailedException { - try { - return circuitManager.openDirectoryCircuit(); - } catch (OpenFailedException e) { - throw new DirectoryRequestFailedException("Failed to open directory circuit", e); - } - } - - private DirectoryCircuit openBridgeCircuit(Router bridge) throws DirectoryRequestFailedException { - try { - return circuitManager.openDirectoryCircuitTo(Arrays.asList(bridge)); - } catch (OpenFailedException e) { - throw new DirectoryRequestFailedException("Failed to open directory circuit to bridge "+ bridge, e); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryRequestFailedException.java b/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryRequestFailedException.java deleted file mode 100644 index c41df470..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/DirectoryRequestFailedException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -public class DirectoryRequestFailedException extends Exception { - - private static final long serialVersionUID = 1L; - - public DirectoryRequestFailedException(String message) { - super(message); - } - - public DirectoryRequestFailedException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/DocumentFetcher.java b/orchid/src/com/subgraph/orchid/directory/downloader/DocumentFetcher.java deleted file mode 100644 index 5cf73baa..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/DocumentFetcher.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.List; - -import com.subgraph.orchid.directory.DocumentParserFactoryImpl; -import com.subgraph.orchid.directory.parsing.BasicDocumentParsingResult; -import com.subgraph.orchid.directory.parsing.DocumentParser; -import com.subgraph.orchid.directory.parsing.DocumentParserFactory; - -public abstract class DocumentFetcher { - protected final static DocumentParserFactory PARSER_FACTORY = new DocumentParserFactoryImpl(); - - - abstract String getRequestPath(); - abstract DocumentParser createParser(ByteBuffer response); - - public List requestDocuments(HttpConnection httpConnection) throws IOException, DirectoryRequestFailedException { - final ByteBuffer body = makeRequest(httpConnection); - if(body.hasRemaining()) { - return processResponse(body); - }else { - return Collections.emptyList(); - } - } - - private ByteBuffer makeRequest(HttpConnection httpConnection) throws IOException, DirectoryRequestFailedException { - - httpConnection.sendGetRequest(getRequestPath()); - httpConnection.readResponse(); - if(httpConnection.getStatusCode() == 200) { - return httpConnection.getMessageBody(); - } - - throw new DirectoryRequestFailedException("Request "+ getRequestPath() +" to directory "+ - httpConnection.getHost() +" returned error code: "+ - httpConnection.getStatusCode() + " "+ httpConnection.getStatusMessage()); - - } - - private List processResponse(ByteBuffer response) throws DirectoryRequestFailedException { - final DocumentParser parser = createParser(response); - final BasicDocumentParsingResult result = new BasicDocumentParsingResult(); - final boolean success = parser.parse(result); - if(success) { - return result.getParsedDocuments(); - } - throw new DirectoryRequestFailedException("Failed to parse response from directory: "+ result.getMessage()); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/HttpConnection.java b/orchid/src/com/subgraph/orchid/directory/downloader/HttpConnection.java deleted file mode 100644 index 44d778d3..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/HttpConnection.java +++ /dev/null @@ -1,247 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.zip.DataFormatException; -import java.util.zip.Inflater; - -import com.subgraph.orchid.Router; -import com.subgraph.orchid.Stream; - -public class HttpConnection { - private final static Charset CHARSET = Charset.forName("ISO-8859-1"); - - private final static String HTTP_RESPONSE_REGEX = "HTTP/1\\.(\\d) (\\d+) (.*)"; - private final static String CONTENT_LENGTH_HEADER = "Content-Length"; - private final static String CONTENT_ENCODING_HEADER = "Content-Encoding"; - private final static String COMPRESSION_SUFFIX = ".z"; - private final String hostname; - private final Stream stream; - private final InputStream input; - private final OutputStream output; - private final Map headers; - private final boolean useCompression; - private int responseCode; - private boolean bodyCompressed; - private String responseMessage; - private ByteBuffer messageBody; - - public HttpConnection(Stream stream) { - this(stream, true); - } - - public HttpConnection(Stream stream, boolean useCompression) { - this.hostname = getHostnameFromStream(stream); - this.stream = stream; - this.headers = new HashMap(); - this.input = stream.getInputStream(); - this.output = stream.getOutputStream(); - this.useCompression = useCompression; - } - - private static String getHostnameFromStream(Stream stream) { - final StringBuilder sb = new StringBuilder(); - final Router r = stream.getCircuit().getFinalCircuitNode().getRouter(); - if(r == null) { - return null; - } - sb.append(r.getAddress().toString()); - if(r.getOnionPort() != 80) { - sb.append(":"); - sb.append(r.getOnionPort()); - } - return sb.toString(); - } - - public void sendGetRequest(String request) throws IOException { - final StringBuilder sb = new StringBuilder(); - sb.append("GET "); - sb.append(request); - if(useCompression && !request.endsWith(COMPRESSION_SUFFIX)) { - sb.append(COMPRESSION_SUFFIX); - } - sb.append(" HTTP/1.0\r\n"); - if(hostname != null) { - sb.append("Host: "+ hostname +"\r\n"); - } - sb.append("\r\n"); - - final String requestLine = sb.toString(); - output.write(requestLine.getBytes(CHARSET)); - output.flush(); - } - - public String getHost() { - if(hostname == null) { - return hostname; - } else { - return "(none)"; - } - } - - public void readResponse() throws IOException, DirectoryRequestFailedException { - readStatusLine(); - readHeaders(); - readBody(); - } - - public int getStatusCode() { - return responseCode; - } - - public String getStatusMessage() { - return responseMessage; - } - - public ByteBuffer getMessageBody() { - return messageBody; - } - - public void close() { - if(stream == null) { - return; - } - stream.close(); - } - - private void readStatusLine() throws IOException, DirectoryRequestFailedException { - final String line = nextResponseLine(); - final Pattern p = Pattern.compile(HTTP_RESPONSE_REGEX); - final Matcher m = p.matcher(line); - if(!m.find() || m.groupCount() != 3) - throw new DirectoryRequestFailedException("Error parsing HTTP response line: "+ line); - - try { - int n1 = Integer.parseInt(m.group(1)); - int n2 = Integer.parseInt(m.group(2)); - if( (n1 != 0 && n1 != 1) || - (n2 < 100 || n2 >= 600)) - throw new DirectoryRequestFailedException("Failed to parse header: "+ line); - responseCode = n2; - responseMessage = m.group(3); - } catch(NumberFormatException e) { - throw new DirectoryRequestFailedException("Failed to parse header: "+ line); - } - } - - private void readHeaders() throws IOException, DirectoryRequestFailedException { - headers.clear(); - while(true) { - final String line = nextResponseLine(); - if(line.length() == 0) - return; - final String[] args = line.split(": ", 2); - if(args.length != 2) - throw new DirectoryRequestFailedException("Failed to parse HTTP header: "+ line); - headers.put(args[0], args[1]); - } - } - - private String nextResponseLine() throws IOException, DirectoryRequestFailedException { - final String line = readInputLine(); - if(line == null) { - throw new DirectoryRequestFailedException("Unexpected EOF reading HTTP response"); - } - return line; - } - - private void readBody() throws IOException, DirectoryRequestFailedException { - processContentEncodingHeader(); - - if(headers.containsKey(CONTENT_LENGTH_HEADER)) { - readBodyFromContentLength(); - } else { - readBodyUntilEOF(); - } - } - - private void processContentEncodingHeader() throws DirectoryRequestFailedException { - final String encoding = headers.get(CONTENT_ENCODING_HEADER); - if(encoding == null || encoding.equals("identity")) - bodyCompressed = false; - else if(encoding.equals("deflate") || encoding.equals("x-deflate")) - bodyCompressed = true; - else - throw new DirectoryRequestFailedException("Unrecognized content encoding: "+ encoding); - } - - private void readBodyFromContentLength() throws IOException { - int bodyLength = Integer.parseInt(headers.get(CONTENT_LENGTH_HEADER)); - byte[] bodyBuffer = new byte[bodyLength]; - readAll(bodyBuffer); - messageBody = bytesToBody(bodyBuffer); - } - - private void readBodyUntilEOF() throws IOException { - final byte[] bodyBuffer = readToEOF(); - messageBody = bytesToBody(bodyBuffer); - } - - private ByteBuffer bytesToBody(byte[] bs) throws IOException { - if(bodyCompressed) { - return ByteBuffer.wrap(decompressBuffer(bs)); - } else { - return ByteBuffer.wrap(bs); - } - } - - private byte[] decompressBuffer(byte[] buffer) throws IOException { - final ByteArrayOutputStream output = new ByteArrayOutputStream(); - final Inflater decompressor = new Inflater(); - final byte[] decompressBuffer = new byte[4096]; - decompressor.setInput(buffer); - int n; - try { - while((n = decompressor.inflate(decompressBuffer)) != 0) { - output.write(decompressBuffer, 0, n); - } - return output.toByteArray(); - } catch (DataFormatException e) { - throw new IOException("Error decompressing http body: "+ e); - } - } - - private byte[] readToEOF() throws IOException { - final ByteArrayOutputStream output = new ByteArrayOutputStream(); - final byte[] buffer = new byte[2048]; - int n; - while( (n = input.read(buffer, 0, buffer.length)) != -1) { - output.write(buffer, 0, n); - } - return output.toByteArray(); - } - - private void readAll(byte[] buffer) throws IOException { - int offset = 0; - int remaining = buffer.length; - while(remaining > 0) { - int n = input.read(buffer, offset, remaining); - if(n == -1) { - throw new IOException("Unexpected early EOF reading HTTP body"); - } - offset += n; - remaining -= n; - } - } - - private String readInputLine() throws IOException { - final StringBuilder sb = new StringBuilder(); - int c; - while((c = input.read()) != -1) { - if(c == '\n') { - return sb.toString(); - } else if(c != '\r') { - sb.append((char) c); - } - } - return (sb.length() == 0) ? (null) : (sb.toString()); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/MicrodescriptorFetcher.java b/orchid/src/com/subgraph/orchid/directory/downloader/MicrodescriptorFetcher.java deleted file mode 100644 index ab407cf9..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/MicrodescriptorFetcher.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import com.subgraph.orchid.RouterMicrodescriptor; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.directory.parsing.DocumentParser; - -public class MicrodescriptorFetcher extends DocumentFetcher{ - - private final List fingerprints; - - public MicrodescriptorFetcher(Collection fingerprints) { - this.fingerprints = new ArrayList(fingerprints); - } - - @Override - String getRequestPath() { - return "/tor/micro/d/"+ fingerprintsToRequestString(); - } - - private String fingerprintsToRequestString() { - final StringBuilder sb = new StringBuilder(); - for(HexDigest fp: fingerprints) { - appendFingerprint(sb, fp); - } - return sb.toString(); - } - - private void appendFingerprint(StringBuilder sb, HexDigest fp) { - if(sb.length() > 0) { - sb.append("-"); - } - sb.append(fp.toBase64(true)); - } - - @Override - DocumentParser createParser(ByteBuffer response) { - return PARSER_FACTORY.createRouterMicrodescriptorParser(response); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/downloader/RouterDescriptorFetcher.java b/orchid/src/com/subgraph/orchid/directory/downloader/RouterDescriptorFetcher.java deleted file mode 100644 index 4b03e88c..00000000 --- a/orchid/src/com/subgraph/orchid/directory/downloader/RouterDescriptorFetcher.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.subgraph.orchid.directory.downloader; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.directory.parsing.DocumentParser; - -public class RouterDescriptorFetcher extends DocumentFetcher{ - - private final List fingerprints; - - public RouterDescriptorFetcher(Collection fingerprints) { - this.fingerprints = new ArrayList(fingerprints); - } - - @Override - String getRequestPath() { - return "/tor/server/d/"+ fingerprintsToRequestString(); - } - - private String fingerprintsToRequestString() { - final StringBuilder sb = new StringBuilder(); - for(HexDigest fp: fingerprints) { - appendFingerprint(sb, fp); - } - return sb.toString(); - } - private void appendFingerprint(StringBuilder sb, HexDigest fp) { - if(sb.length() > 0) { - sb.append("+"); - } - sb.append(fp.toString()); - } - - @Override - DocumentParser createParser(ByteBuffer response) { - return PARSER_FACTORY.createRouterDescriptorParser(response, true); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/parsing/BasicDocumentParsingResult.java b/orchid/src/com/subgraph/orchid/directory/parsing/BasicDocumentParsingResult.java deleted file mode 100644 index e8a997dc..00000000 --- a/orchid/src/com/subgraph/orchid/directory/parsing/BasicDocumentParsingResult.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.subgraph.orchid.directory.parsing; - -import java.util.ArrayList; -import java.util.List; - -public class BasicDocumentParsingResult implements DocumentParsingResultHandler, DocumentParsingResult { - - private final List documents; - private T invalidDocument; - private boolean isOkay; - private boolean isInvalid; - private boolean isError; - private String message; - - public BasicDocumentParsingResult() { - documents = new ArrayList(); - isOkay = true; - isInvalid = false; - isError = false; - message = ""; - } - - public T getDocument() { - if(documents.size() != 1) { - throw new IllegalStateException(); - } - return documents.get(0); - } - - public List getParsedDocuments() { - return new ArrayList(documents); - } - - public boolean isOkay() { - return isOkay; - } - - public boolean isInvalid() { - return isInvalid; - } - - public T getInvalidDocument() { - return invalidDocument; - } - - public boolean isError() { - return isError; - } - - public String getMessage() { - return message; - } - - public void documentParsed(T document) { - documents.add(document); - } - - public void documentInvalid(T document, String message) { - isOkay = false; - isInvalid = true; - invalidDocument = document; - this.message = message; - } - - public void parsingError(String message) { - isOkay = false; - isError = true; - this.message = message; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentFieldParser.java b/orchid/src/com/subgraph/orchid/directory/parsing/DocumentFieldParser.java deleted file mode 100644 index a67fd94d..00000000 --- a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentFieldParser.java +++ /dev/null @@ -1,341 +0,0 @@ -package com.subgraph.orchid.directory.parsing; - -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.crypto.TorMessageDigest; -import com.subgraph.orchid.crypto.TorPublicKey; -import com.subgraph.orchid.crypto.TorSignature; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; -import com.subgraph.orchid.data.Timestamp; - -/** - * This helper class is used by document parsing classes to extract individual - * fields from a directory document. The DocumentFieldParser also manages the - * InputStream which is the source of the document to parse. Parsing classes - * are implemented by creating an instance of the DocumentParsingHandler interface. - * - */ -public interface DocumentFieldParser { - - /** - * Run the document parser. The {@link #setHandler(DocumentParsingHandler)} method must be - * called before calling this method to set a DocumentParsingHandler for processing - * this document. - * - * @throws TorParsingException If a parsing error occurs while processing the document. - */ - void processDocument(); - - /** - * Returns the number of unprocessed argument items on the current keyword line. - * - * @return The number of remaining arguments. - */ - int argumentsRemaining(); - - /** - * Extract the next argument item and return it as a String - * - * @return The next argument as a String - * @throws TorParsingException If no arguments are remaining on the current keyword line. - */ - String parseString(); - - /** - * Take all remaining arguments on the current keyword line and return them as a single space - * delimited String. If no arguments are remaining, then an empty String is returned. - * - * @return The remaining arguments on the current keyword line concatenated together. - */ - String parseConcatenatedString(); - - /** - * Extract the next argument and interpret it as an integer boolean value. The legal values - * are '1' for true or '0' for false. - * @return Return the next argument interpreted as a boolean value. - * @throws TorParsingException If no arguments are remaining or if the current argument cannot be - * parsed as a boolean integer value. - */ - boolean parseBoolean(); - - /** - * Extract the next argument item and return it as a String if it conforms to - * a legally formed router nickname (dir-spec.txt section 2.3). - * - * A router nickname must be between 1 and 19 alphanumeric characters ([A-Za-z0-9]) to - * be considered valid. - * - * @return The next argument as a String if it is a validly formatted nickname. - * @throws TorParsingException If no arguments are remaining or if the current argument is not - * a valid router nickname. - */ - String parseNickname(); - - /** - * Extract the next argument and interpret it as an integer. - * - * @return The next argument interpreted as an integer. - * @throws TorParsingException If no arguments are remaining or if the current argument cannot - * be parsed as an integer value. - */ - int parseInteger(); - - /** - * Parse the item argument as an integer. - * - * @param item A string to parse as an integer. - * @return The integer value of the item argument. - * @throws TorParsingException If the item argument cannot be parsed as an - * integer value. - */ - int parseInteger(String item); - - /** - * Extract the next argument and interpret it as a comma separated list of integers. - * - * @return An array of integers. - * @throws TorParsingException If no arguments are remaining or if the current argument cannot - * be parsed as a list of integers. - */ - int[] parseIntegerList(); - - /** - * Extract the next argument and interpret it as a network port value. A valid port - * value is an integer between 0 and 65535 inclusive. - * - * @return The next argument interpreted as an integer port value. - * @throws TorParsingException If no arguments are remaining or if the current argument cannot - * be parsed as a legal port value. - */ - int parsePort(); - - /** - * Parse the item arguement as a network port value. A valid port value - * is an integer between 0 and 65535 inclusive. - * - * @param item A string to parse as an integer port value. - * @return The port integer value of the item argument - * @throws TorParsingException If the item argument cannot be parsed as a - * legal port value. - */ - int parsePort(String item); - - /** - * Extract the next argument and interpret it as Base64 encoded binary data. - * - * @return The bytes decoded from the Base64 encoded argument. - * @throws TorParsingException If no arguments are remaining or if the current argument cannot - * be parsed as Base64 encoded data. - */ - byte[] parseBase64Data(); - - /** - * Extract the next two arguments and parse as a timestamp field. - * - * The format of a timestamp is: YYYY-MM-DD HH:MM:SS - * - * @return The parsed Timestamp value. - * @throws TorParsingException If there are not sufficient arguments remaining or if the current - * arguments could not be parsed as a timestamp field. - */ - Timestamp parseTimestamp(); - - /** - * Extract the next argument and interpret it as a hex encoded digest string. - * - * @return The parsed HexDigest value. - * @throws TorParsingException If no arguments are remaining or if the current argument cannot - * be parsed as a hex encoded digest string. - */ - HexDigest parseHexDigest(); - - - /** - * Extract the next argument and interpret it as a base 32 encoded digest string. - * - * @return The parsed HexDigest value. - * @throws TorParsingException If no arguments are remaining or if the current argument cannot - * be parsed as a base 32 encoded digest string. - */ - HexDigest parseBase32Digest(); - - /** - * Extract all remaining arguments and interpret the concatenated string as a - * hex encoded fingerprint string. - * - * @return The parsed HexDigest value extracted from the concatenated string. - * @throws TorParsingException If the concatenation of the remaining arguments could not be parsed - * as a hex encoded fingerprint string. - */ - HexDigest parseFingerprint(); - - /** - * Extract the next argument and interpret it as an IPv4 network address in dotted quad notation. - * - * @return The parsed IPv4Address value. - * @throws TorParsingException If no arguments are remaining or if the current argument cannot - * be parsed as an IPv4 network address. - */ - IPv4Address parseAddress(); - - /** - * Extract a document object following the current keyword line and interpret it as a PEM - * encoded public key. - * - * @return The extracted TorPublicKey value. - * @throws TorParsingException If no document object is found following the current keyword line, - * or if the document object cannot be parsed as a PEM encoded public key. - */ - TorPublicKey parsePublicKey(); - - byte[] parseNtorPublicKey(); - /** - * Extract a document object following the current keyword line and interpret it as a - * Base64 encoded PKCS1 signature object. - * - * @return The extracted TorSignature value. - * @throws TorParsingException If no document object is found following the current keyword line, - * or if the document object cannot be parsed as a signature. - */ - TorSignature parseSignature(); - - /** - * Extract a document object following the current keyword line and don't attempt to interpret - * it further. - * - * @return The extracted DocumentObject. - * @throws TorParsingException If no document object is found following the current keyword line. - */ - DocumentObject parseObject(); - - /** - * - * @return - */ - - NameIntegerParameter parseParameter(); - /** - * Return the keyword of the current keyword line. The keyword is the first token on the line - * unless the first token is 'opt' and 'opt' recognition is enabled. In this case, the keyword - * is the token immediately following the 'opt' token. - * - * @return The keyword token of the current keyword line. - */ - String getCurrentKeyword(); - - /** - * Return all lines from the current document as a single String. - * - * @return The raw data from the current document. - */ - String getRawDocument(); - - /** - * Empty the internal buffer which is capturing the raw data from - * the document which is being parsed. - */ - void resetRawDocument(); - - /** - * Empty the internal buffer which is capturing raw data from document being parsed and set buffer contents to initalContent. - * - * @param initialContent Initial raw document content. - */ - void resetRawDocument(String initialContent); - - /** - * Reset the document signing state. Any lines read after calling this method will be included - * in the current signature hash. - */ - void startSignedEntity(); - - /** - * Set the current keyword line as the last line included in the current signature hash. - */ - void endSignedEntity(); - - /** - * Tells the parser to not include lines that begin with token in the current - * signature calculation. - * - * @param token The parser will not include lines that begin with token in the - * current signature. - */ - void setSignatureIgnoreToken(String token); - - /** - * Return the internal message digest which is being used to calculate the - * signature over the current document. - * - * @return The TorMessageDigest instance or null if - * a signature is not being actively calculated. - */ - TorMessageDigest getSignatureMessageDigest(); - TorMessageDigest getSignatureMessageDigest256(); - - /** - * Verify that current signature hash matches the specified signature signed - * with the public key publicKey - * - * @param publicKey The public key used to verify the signature. - * @param signature The signature to verify against the current signature hash. - * @return trueIf the signature argument matches the hash currently - * calculated document hash. - */ - boolean verifySignedEntity(TorPublicKey publicKey, TorSignature signature); - - /** - * Test that the current keyword line has the correct number of arguments. - * - * @param keyword The name of the current keyword. (used for errors) - * @param argumentCount The expected number of arguments. - * @throws TorParsingException If the number of remaining arguments does not match - * argumentCount. - */ - void verifyExpectedArgumentCount(String keyword, int argumentCount); - - /** - * Set a flag so that 'opt' tokens will be recognized at the start of keyword lines. If - * this flag is set, a token string 'opt' at the start of a keyword line will be ignored - * and the token following the 'opt' string will be interpreted as the keyword. - */ - void setRecognizeOpt(); - - /** - * The default delimiter between keyword line tokens is any whitespace. This method may - * be called to specify a different delimiter policy. - * - * @param delimeter A regular expression which matches the desired delimiter. - */ - void setDelimiter(String delimeter); - - /** - * Set the callback handler which is used to process the document. This method must be called - * before calling {@link #processDocument()}. - * - * @param handler The callback handler. - */ - void setHandler(DocumentParsingHandler handler); - - /** - * Log the specified message at the debug logging level. - * - * @param message The message to log. - */ - void logDebug(String message); - - /** - * Log the specified message at the warn logging level. - * - * @param message The message to log. - */ - void logWarn(String message); - - /** - * Log the specified message at the error logging level. - * - * @param message The message to log. - */ - void logError(String message); - -} diff --git a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentObject.java b/orchid/src/com/subgraph/orchid/directory/parsing/DocumentObject.java deleted file mode 100644 index 84d3c7e2..00000000 --- a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentObject.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.subgraph.orchid.directory.parsing; - -public class DocumentObject { - - final private String keyword; - final private String headerLine; - private String footerLine; - private String bodyContent; - final private StringBuilder stringContent; - - public DocumentObject(String keyword, String headerLine) { - this.keyword = keyword; - this.headerLine = headerLine; - this.stringContent = new StringBuilder(); - } - - public String getKeyword() { - return keyword; - } - - public void addContent(String content) { - stringContent.append(content); - stringContent.append("\n"); - } - - public void addFooterLine(String footer) { - footerLine = footer; - bodyContent = stringContent.toString(); - } - - public String getContent() { - return getContent(true); - } - - public String getContent(boolean includeHeaders) { - if(includeHeaders) { - return headerLine + "\n" + bodyContent + footerLine + "\n"; - } else { - return bodyContent; - } - } - -} diff --git a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParser.java b/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParser.java deleted file mode 100644 index 6cedaf53..00000000 --- a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParser.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.subgraph.orchid.directory.parsing; - - -public interface DocumentParser { - boolean parse(DocumentParsingResultHandler resultHandler); - DocumentParsingResult parse(); -} diff --git a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParserFactory.java b/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParserFactory.java deleted file mode 100644 index a931bf06..00000000 --- a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParserFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.subgraph.orchid.directory.parsing; - -import java.nio.ByteBuffer; - -import com.subgraph.orchid.ConsensusDocument; -import com.subgraph.orchid.KeyCertificate; -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.RouterMicrodescriptor; - -public interface DocumentParserFactory { - DocumentParser createRouterDescriptorParser(ByteBuffer buffer, boolean verifySignatures); - - DocumentParser createRouterMicrodescriptorParser(ByteBuffer buffer); - - DocumentParser createKeyCertificateParser(ByteBuffer buffer); - - DocumentParser createConsensusDocumentParser(ByteBuffer buffer); -} diff --git a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingHandler.java b/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingHandler.java deleted file mode 100644 index c1c81ed1..00000000 --- a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingHandler.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.subgraph.orchid.directory.parsing; - -public interface DocumentParsingHandler { - void parseKeywordLine(); - void endOfDocument(); -} diff --git a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingResult.java b/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingResult.java deleted file mode 100644 index f7b53d0f..00000000 --- a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingResult.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.subgraph.orchid.directory.parsing; - -import java.util.List; - -public interface DocumentParsingResult { - T getDocument(); - List getParsedDocuments(); - boolean isOkay(); - boolean isInvalid(); - T getInvalidDocument(); - boolean isError(); - String getMessage(); -} diff --git a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingResultHandler.java b/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingResultHandler.java deleted file mode 100644 index b9034bc2..00000000 --- a/orchid/src/com/subgraph/orchid/directory/parsing/DocumentParsingResultHandler.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.subgraph.orchid.directory.parsing; - - -public interface DocumentParsingResultHandler { - void documentParsed(T document); - void documentInvalid(T document, String message); - void parsingError(String message); -} diff --git a/orchid/src/com/subgraph/orchid/directory/parsing/NameIntegerParameter.java b/orchid/src/com/subgraph/orchid/directory/parsing/NameIntegerParameter.java deleted file mode 100644 index 55756534..00000000 --- a/orchid/src/com/subgraph/orchid/directory/parsing/NameIntegerParameter.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.subgraph.orchid.directory.parsing; - -public class NameIntegerParameter { - - private final String name; - private final int value; - - public NameIntegerParameter(String name, int value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public int getValue() { - return value; - } - - public String toString() { - return name +"="+ value; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/router/MicrodescriptorCacheLocation.java b/orchid/src/com/subgraph/orchid/directory/router/MicrodescriptorCacheLocation.java deleted file mode 100644 index b1a6fd49..00000000 --- a/orchid/src/com/subgraph/orchid/directory/router/MicrodescriptorCacheLocation.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.subgraph.orchid.directory.router; - -public class MicrodescriptorCacheLocation { - - private final int offset; - private final int length; - - public MicrodescriptorCacheLocation(int offset, int length) { - this.offset = offset; - this.length = length; - } - - public int getOffset() { - return offset; - } - - public int getLength() { - return length; - } - - public String toString() { - return "MD Cache offset: "+ offset + " length: "+ length; - } - -} diff --git a/orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorImpl.java b/orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorImpl.java deleted file mode 100644 index 95869e97..00000000 --- a/orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorImpl.java +++ /dev/null @@ -1,317 +0,0 @@ -package com.subgraph.orchid.directory.router; - -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.crypto.TorPublicKey; -import com.subgraph.orchid.data.BandwidthHistory; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; -import com.subgraph.orchid.data.Timestamp; -import com.subgraph.orchid.data.exitpolicy.ExitPolicy; - -public class RouterDescriptorImpl implements RouterDescriptor { - private String nickname; - private IPv4Address address; - private int routerPort; - private int directoryPort; - - private int averageBandwidth = -1; - private int burstBandwidth = -1; - private int observedBandwidth = -1; - - private String platform; - - private Timestamp published; - - private HexDigest fingerprint; - - private boolean hibernating; - - private int uptime; - - private TorPublicKey onionKey; - private byte[] ntorOnionKey; - private TorPublicKey identityKey; - private ExitPolicy exitPolicy = new ExitPolicy(); - - private String contact; - private Set familyMembers = Collections.emptySet(); - private Set linkProtocols = Collections.emptySet(); - private Set circuitProtocols = Collections.emptySet(); - - private BandwidthHistory readHistory; - private BandwidthHistory writeHistory; - - private boolean eventDNS = false; - private boolean cachesExtraInfo = false; - private boolean hiddenServiceDir = false; - private HexDigest extraInfoDigest = null; - private boolean allowSingleHopExits = false; - private boolean hasValidSignature = false; - - private HexDigest descriptorDigest; - private String rawDocumentData; - - private long lastListed; - private CacheLocation cacheLocation = CacheLocation.NOT_CACHED; - - public void setNickname(String nickname) { this.nickname = nickname; } - public void setAddress(IPv4Address address) { this.address = address; } - public void setRouterPort(int port) { this.routerPort = port; } - void setDirectoryPort(int port) { this.directoryPort = port; } - void setPlatform(String platform) { this.platform = platform; } - void setPublished(Timestamp published) { this.published = published; } - void setFingerprint(HexDigest fingerprint) { this.fingerprint = fingerprint; } - void setHibernating(boolean flag) { this.hibernating = flag; } - void setUptime(int uptime) { this.uptime = uptime; } - public void setOnionKey(TorPublicKey key) { this.onionKey = key; } - void setNtorOnionKey(byte[] key) { this.ntorOnionKey = key; } - void setIdentityKey(TorPublicKey key) { this.identityKey = key; } - void setContact(String contact) { this.contact = contact; } - void setEventDNS() { eventDNS = true; } - void setHiddenServiceDir() { hiddenServiceDir = true; } - void setExtraInfoDigest(HexDigest digest) { this.extraInfoDigest = digest; } - void setCachesExtraInfo() { cachesExtraInfo = true; } - void setAllowSingleHopExits() { allowSingleHopExits = true; } - void setReadHistory(BandwidthHistory history) { this.readHistory= history; } - void setWriteHistory(BandwidthHistory history) { this.writeHistory = history; } - void setValidSignature() { hasValidSignature = true; } - void setDescriptorHash(HexDigest digest) { descriptorDigest = digest; } - void setRawDocumentData(String rawData) { rawDocumentData = rawData; } - - void addAcceptRule(String rule) { - exitPolicy.addAcceptRule(rule); - } - - void addRejectRule(String rule) { - exitPolicy.addRejectRule(rule); - } - - void setBandwidthValues(int average, int burst, int observed) { - this.averageBandwidth = average; - this.burstBandwidth = burst; - this.observedBandwidth = observed; - } - - void addFamilyMember(String familyMember) { - if(familyMembers.isEmpty()) { - familyMembers = new HashSet(); - } - familyMembers.add(familyMember); - } - - void addCircuitProtocolVersion(int version) { - if(circuitProtocols.isEmpty()) - circuitProtocols = new HashSet(); - circuitProtocols.add(version); - } - - void addLinkProtocolVersion(int version) { - if(linkProtocols.isEmpty()) - linkProtocols = new HashSet(); - linkProtocols.add(version); - } - - public boolean isValidDocument() { - // verify required fields exist, see dirspec.txt section 2.1 - return hasValidSignature && (nickname != null) && (address != null) && - (averageBandwidth != -1) && (routerPort != 0 || directoryPort != 0) && - (published != null) && (onionKey != null) && (identityKey != null) && - (descriptorDigest != null); - } - - public String getNickname() { - return nickname; - } - - public IPv4Address getAddress() { - return address; - } - - public int getRouterPort() { - return routerPort; - } - - public int getDirectoryPort() { - return directoryPort; - } - - public int getAverageBandwidth() { - return averageBandwidth; - } - - public int getBurstBandwidth() { - return burstBandwidth; - } - - public int getObservedBandwidth() { - return observedBandwidth; - } - - public String getPlatform() { - return platform; - } - - public HexDigest getFingerprint() { - return fingerprint; - } - - public int getUptime() { - return uptime; - } - - public TorPublicKey getOnionKey() { - return onionKey; - } - - public byte[] getNTorOnionKey() { - return ntorOnionKey; - } - - public TorPublicKey getIdentityKey() { - return identityKey; - } - - public String getContact() { - return contact; - } - - public boolean isHibernating() { - return hibernating; - } - - public boolean cachesExtraInfo() { - return cachesExtraInfo; - } - - public boolean allowsSingleHopExits() { - return allowSingleHopExits; - } - - public Timestamp getPublishedTime() { - return published; - } - - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("Router Descriptor: (name: "); - builder.append(nickname); - builder.append(" orport="); - builder.append(routerPort); - builder.append(" dirport="); - builder.append(directoryPort); - builder.append(" address="); - builder.append(address); - builder.append(" platform="); - builder.append(platform); - builder.append(" published="); - builder.append(published.getDate()); - builder.append(")"); - return builder.toString(); - } - - public void print() { - System.out.println("nickname: "+ nickname +" IP: "+ address +" port: "+ routerPort); - System.out.println("directory port: "+ directoryPort +" platform: "+ platform); - System.out.println("Bandwidth(avg/burst/observed): "+ averageBandwidth +"/"+ burstBandwidth +"/"+ observedBandwidth); - System.out.println("Publication time: "+ published +" Uptime: "+ uptime); - if(fingerprint != null) - System.out.println("Fingerprint: "+ fingerprint); - if(contact != null) - System.out.println("Contact: "+ contact); - } - public boolean exitPolicyAccepts(IPv4Address address, int port) { - return exitPolicy.acceptsDestination(address, port); - } - - public boolean exitPolicyAccepts(int port) { - return exitPolicy.acceptsPort(port); - } - - public HexDigest getExtraInfoDigest() { - return extraInfoDigest; - } - - public boolean isHiddenServiceDirectory() { - return hiddenServiceDir; - } - - public Set getFamilyMembers() { - return familyMembers; - } - - public boolean supportsEventDNS() { - return eventDNS; - } - - public BandwidthHistory getReadHistory() { - return readHistory; - } - - public BandwidthHistory getWriteHistory() { - return writeHistory; - } - - public boolean isNewerThan(RouterDescriptor other) { - return other.getPublishedTime().isBefore(published); - } - - public HexDigest getDescriptorDigest() { - return descriptorDigest; - } - - public String getRawDocumentData() { - return rawDocumentData; - } - - public ByteBuffer getRawDocumentBytes() { - if(getRawDocumentData() == null) { - return ByteBuffer.allocate(0); - } else { - return ByteBuffer.wrap(getRawDocumentData().getBytes(Tor.getDefaultCharset())); - } - } - - public boolean equals(Object o) { - if(!(o instanceof RouterDescriptorImpl)) - return false; - final RouterDescriptorImpl other = (RouterDescriptorImpl) o; - if(other.getDescriptorDigest() == null || descriptorDigest == null) - return false; - - return other.getDescriptorDigest().equals(descriptorDigest); - } - - public int hashCode() { - if(descriptorDigest == null) - return 0; - return descriptorDigest.hashCode(); - } - - public ExitPolicy getExitPolicy() { - return exitPolicy; - } - - public void setLastListed(long timestamp) { - this.lastListed = timestamp; - } - - public long getLastListed() { - return lastListed; - } - public void setCacheLocation(CacheLocation location) { - this.cacheLocation = location; - } - public CacheLocation getCacheLocation() { - return cacheLocation; - } - - public int getBodyLength() { - return rawDocumentData.length(); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorKeyword.java b/orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorKeyword.java deleted file mode 100644 index 50322dff..00000000 --- a/orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorKeyword.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.subgraph.orchid.directory.router; - -public enum RouterDescriptorKeyword { - /* - * See dir-spec.txt - * Section 2.1. Router descriptor format - */ - ROUTER("router", 5), - BANDWIDTH("bandwidth", 3), - PLATFORM("platform"), - PUBLISHED("published", 2), - FINGERPRINT("fingerprint", 10), - HIBERNATING("hibernating", 1), - UPTIME("uptime", 1), - ONION_KEY("onion-key", 0), - NTOR_ONION_KEY("ntor-onion-key", 1), - SIGNING_KEY("signing-key", 0), - ACCEPT("accept", 1), - REJECT("reject", 1), - ROUTER_SIGNATURE("router-signature", 0), - CONTACT("contact"), - FAMILY("family"), - READ_HISTORY("read-history"), - WRITE_HISTORY("write-history"), - EVENTDNS("eventdns", 1), - CACHES_EXTRA_INFO("caches-extra-info", 0), - EXTRA_INFO_DIGEST("extra-info-digest", 1), - HIDDEN_SERVICE_DIR("hidden-service-dir"), - PROTOCOLS("protocols"), - ALLOW_SINGLE_HOP_EXITS("allow-single-hop-exits", 0), - UNKNOWN_KEYWORD("KEYWORD NOT FOUND"); - - public final static int VARIABLE_ARGUMENT_COUNT = -1; - - private final String keyword; - private final int argumentCount; - - RouterDescriptorKeyword(String keyword) { - this(keyword, VARIABLE_ARGUMENT_COUNT); - } - - RouterDescriptorKeyword(String keyword, int argumentCount) { - this.keyword = keyword; - this.argumentCount = argumentCount; - } - - String getKeyword() { - return keyword; - } - - int getArgumentCount() { - return argumentCount; - } - - static RouterDescriptorKeyword findKeyword(String keyword) { - for(RouterDescriptorKeyword k: values()) - if(k.getKeyword().equals(keyword)) - return k; - - return UNKNOWN_KEYWORD; - } - -} diff --git a/orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorParser.java b/orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorParser.java deleted file mode 100644 index e23b68a5..00000000 --- a/orchid/src/com/subgraph/orchid/directory/router/RouterDescriptorParser.java +++ /dev/null @@ -1,223 +0,0 @@ -package com.subgraph.orchid.directory.router; - -import com.subgraph.orchid.RouterDescriptor; -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.crypto.TorSignature; -import com.subgraph.orchid.data.BandwidthHistory; -import com.subgraph.orchid.data.Timestamp; -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 RouterDescriptorParser implements DocumentParser { - private final DocumentFieldParser fieldParser; - private final boolean verifySignatures; - - private RouterDescriptorImpl currentDescriptor; - private DocumentParsingResultHandler resultHandler; - - public RouterDescriptorParser(DocumentFieldParser fieldParser, boolean verifySignatures) { - this.fieldParser = fieldParser; - this.fieldParser.setHandler(createParsingHandler()); - this.fieldParser.setRecognizeOpt(); - this.verifySignatures = verifySignatures; - } - - private DocumentParsingHandler createParsingHandler() { - return new DocumentParsingHandler() { - public void endOfDocument() { - } - public void parseKeywordLine() { - processKeywordLine(); - } - }; - } - - private void processKeywordLine() { - final RouterDescriptorKeyword keyword = RouterDescriptorKeyword.findKeyword(fieldParser.getCurrentKeyword()); - /* - * dirspec.txt (1.2) - * When interpreting a Document, software MUST ignore any KeywordLine that - * starts with a keyword it doesn't recognize; - */ - if(!keyword.equals(RouterDescriptorKeyword.UNKNOWN_KEYWORD)) - processKeyword(keyword); - } - - private void startNewDescriptor() { - fieldParser.resetRawDocument(); - fieldParser.startSignedEntity(); - currentDescriptor = new RouterDescriptorImpl(); - } - - public boolean parse(DocumentParsingResultHandler resultHandler) { - this.resultHandler = resultHandler; - startNewDescriptor(); - try { - fieldParser.processDocument(); - return true; - } catch(TorParsingException e) { - resultHandler.parsingError(e.getMessage()); - return false; - } - } - - public DocumentParsingResult parse() { - final BasicDocumentParsingResult result = new BasicDocumentParsingResult(); - parse(result); - return result; - } - - private void processKeyword(RouterDescriptorKeyword keyword) { - fieldParser.verifyExpectedArgumentCount(keyword.getKeyword(), keyword.getArgumentCount()); - - switch(keyword) { - case ROUTER: - processRouter(); - return; - case BANDWIDTH: - processBandwidth(); - break; - case PLATFORM: - currentDescriptor.setPlatform(fieldParser.parseConcatenatedString()); - break; - case PUBLISHED: - currentDescriptor.setPublished(fieldParser.parseTimestamp()); - break; - case FINGERPRINT: - currentDescriptor.setFingerprint(fieldParser.parseFingerprint()); - break; - case HIBERNATING: - currentDescriptor.setHibernating(fieldParser.parseBoolean()); - break; - case UPTIME: - currentDescriptor.setUptime(fieldParser.parseInteger()); - break; - case ONION_KEY: - currentDescriptor.setOnionKey(fieldParser.parsePublicKey()); - break; - case NTOR_ONION_KEY: - currentDescriptor.setNtorOnionKey(fieldParser.parseNtorPublicKey()); - break; - case SIGNING_KEY: - currentDescriptor.setIdentityKey(fieldParser.parsePublicKey()); - break; - case ROUTER_SIGNATURE: - processSignature(); - break; - case ACCEPT: - currentDescriptor.addAcceptRule(fieldParser.parseString()); - break; - case REJECT: - currentDescriptor.addRejectRule(fieldParser.parseString()); - break; - case CONTACT: - currentDescriptor.setContact(fieldParser.parseConcatenatedString()); - break; - case FAMILY: - while(fieldParser.argumentsRemaining() > 0) - currentDescriptor.addFamilyMember(fieldParser.parseString()); - break; - case EVENTDNS: - if(fieldParser.parseBoolean()) - currentDescriptor.setEventDNS(); - break; - case PROTOCOLS: - processProtocols(); - break; - case CACHES_EXTRA_INFO: - currentDescriptor.setCachesExtraInfo(); - break; - case HIDDEN_SERVICE_DIR: - currentDescriptor.setHiddenServiceDir(); - break; - case ALLOW_SINGLE_HOP_EXITS: - currentDescriptor.setAllowSingleHopExits(); - break; - case EXTRA_INFO_DIGEST: - currentDescriptor.setExtraInfoDigest(fieldParser.parseHexDigest()); - break; - case READ_HISTORY: - currentDescriptor.setReadHistory(parseHistory()); - break; - case WRITE_HISTORY: - currentDescriptor.setWriteHistory(parseHistory()); - break; - default: - break; - } - } - - private BandwidthHistory parseHistory() { - final Timestamp ts = fieldParser.parseTimestamp(); - final String nsec = fieldParser.parseString(); - fieldParser.parseString(); - final int interval = fieldParser.parseInteger(nsec.substring(1)); - final BandwidthHistory history = new BandwidthHistory(ts, interval); - if(fieldParser.argumentsRemaining() == 0) - return history; - final String[] samples = fieldParser.parseString().split(","); - for(String s: samples) - history.addSample(fieldParser.parseInteger(s)); - return history; - } - - private void processRouter() { - currentDescriptor.setNickname(fieldParser.parseNickname()); - currentDescriptor.setAddress(fieldParser.parseAddress()); - currentDescriptor.setRouterPort(fieldParser.parsePort()); - /* 2.1 SOCKSPort is deprecated and should always be 0 */ - fieldParser.parsePort(); - currentDescriptor.setDirectoryPort(fieldParser.parsePort()); - } - - private boolean verifyCurrentDescriptor(TorSignature signature) { - if(verifySignatures && !fieldParser.verifySignedEntity(currentDescriptor.getIdentityKey(), signature)) { - resultHandler.documentInvalid(currentDescriptor, "Signature failed."); - fieldParser.logWarn("Signature failed for router: " + currentDescriptor.getNickname()); - return false; - } - currentDescriptor.setValidSignature(); - if(!currentDescriptor.isValidDocument()) { - resultHandler.documentInvalid(currentDescriptor, "Router data invalid"); - fieldParser.logWarn("Router data invalid for router: " + currentDescriptor.getNickname()); - } - return currentDescriptor.isValidDocument(); - } - - private void processBandwidth() { - final int average = fieldParser.parseInteger(); - final int burst = fieldParser.parseInteger(); - final int observed = fieldParser.parseInteger(); - currentDescriptor.setBandwidthValues(average, burst, observed); - } - - private void processProtocols() { - String kw = fieldParser.parseString(); - if(!kw.equals("Link")) - throw new TorParsingException("Expected 'Link' token in protocol line got: " + kw); - while(true) { - kw = fieldParser.parseString(); - if(kw.equals("Circuit")) - break; - currentDescriptor.addLinkProtocolVersion(fieldParser.parseInteger(kw)); - } - while(fieldParser.argumentsRemaining() > 0) - currentDescriptor.addCircuitProtocolVersion(fieldParser.parseInteger()); - - } - - private void processSignature() { - fieldParser.endSignedEntity(); - currentDescriptor.setDescriptorHash(fieldParser.getSignatureMessageDigest().getHexDigest()); - final TorSignature signature = fieldParser.parseSignature(); - currentDescriptor.setRawDocumentData(fieldParser.getRawDocument()); - - if(verifyCurrentDescriptor(signature)) - resultHandler.documentParsed(currentDescriptor); - startNewDescriptor(); - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorImpl.java b/orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorImpl.java deleted file mode 100644 index af75e2f1..00000000 --- a/orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorImpl.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.subgraph.orchid.directory.router; - -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import com.subgraph.orchid.RouterMicrodescriptor; -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.crypto.TorPublicKey; -import com.subgraph.orchid.data.HexDigest; -import com.subgraph.orchid.data.IPv4Address; -import com.subgraph.orchid.data.exitpolicy.ExitPorts; - -public class RouterMicrodescriptorImpl implements RouterMicrodescriptor { - - private IPv4Address address; - private int routerPort; - private TorPublicKey onionKey; - private byte[] ntorOnionKey; - private Set familyMembers = Collections.emptySet(); - private ExitPorts acceptPorts; - private ExitPorts rejectPorts; - private String rawDocumentData; - private HexDigest descriptorDigest; - private long lastListed; - private CacheLocation cacheLocation = CacheLocation.NOT_CACHED; - - public void setAddress(IPv4Address address) { - this.address = address; - } - - public void setRouterPort(int port) { - this.routerPort = port; - } - - public void setOnionKey(TorPublicKey onionKey) { - this.onionKey = onionKey; - } - - public void setNtorOnionKey(byte[] ntorOnionKey) { - this.ntorOnionKey = ntorOnionKey; - } - - public void addFamilyMember(String familyMember) { - if(familyMembers.isEmpty()) { - familyMembers = new HashSet(); - } - familyMembers.add(familyMember); - } - - public void addAcceptPorts(String portlist) { - acceptPorts = ExitPorts.createAcceptExitPorts(portlist); - } - - public void addRejectPorts(String portlist) { - rejectPorts = ExitPorts.createRejectExitPorts(portlist); - } - - public void setRawDocumentData(String rawData) { - this.rawDocumentData = rawData; - } - - public void setDescriptorDigest(HexDigest descriptorDigest) { - this.descriptorDigest = descriptorDigest; - } - - public void setLastListed(long ts) { - this.lastListed = ts; - } - - public boolean isValidDocument() { - return (descriptorDigest != null) && (onionKey != null); - } - - public String getRawDocumentData() { - return rawDocumentData; - } - - public TorPublicKey getOnionKey() { - return onionKey; - } - - public byte[] getNTorOnionKey() { - return ntorOnionKey; - } - - public IPv4Address getAddress() { - return address; - } - - public int getRouterPort() { - return routerPort; - } - - public Set getFamilyMembers() { - return familyMembers; - } - - public boolean exitPolicyAccepts(IPv4Address address, int port) { - return exitPolicyAccepts(port); - } - - public boolean exitPolicyAccepts(int port) { - if(acceptPorts == null) { - return false; - } - if(rejectPorts != null && !rejectPorts.acceptsPort(port)) { - return false; - } - return acceptPorts.acceptsPort(port); - } - - public HexDigest getDescriptorDigest() { - return descriptorDigest; - } - - public boolean equals(Object o) { - if(!(o instanceof RouterMicrodescriptorImpl)) - return false; - final RouterMicrodescriptorImpl other = (RouterMicrodescriptorImpl) o; - if(other.getDescriptorDigest() == null || descriptorDigest == null) - return false; - - return other.getDescriptorDigest().equals(descriptorDigest); - } - - public int hashCode() { - if(descriptorDigest == null) - return 0; - return descriptorDigest.hashCode(); - } - - public long getLastListed() { - return lastListed; - } - - public void setCacheLocation(CacheLocation location) { - this.cacheLocation = location; - } - - public CacheLocation getCacheLocation() { - return cacheLocation; - } - - public int getBodyLength() { - return rawDocumentData.length(); - } - - public ByteBuffer getRawDocumentBytes() { - if(getRawDocumentData() == null) { - return ByteBuffer.allocate(0); - } else { - return ByteBuffer.wrap(getRawDocumentData().getBytes(Tor.getDefaultCharset())); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorKeyword.java b/orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorKeyword.java deleted file mode 100644 index 7f4cf6d5..00000000 --- a/orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorKeyword.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.subgraph.orchid.directory.router; - -public enum RouterMicrodescriptorKeyword { - ONION_KEY("onion-key", 0), - NTOR_ONION_KEY("ntor-onion-key", 1), - A("a", 1), - FAMILY("family"), - P("p", 2), - UNKNOWN_KEYWORD("KEYWORD NOT FOUNE"); - - public final static int VARIABLE_ARGUMENT_COUNT = -1; - - private final String keyword; - private final int argumentCount; - - RouterMicrodescriptorKeyword(String keyword) { - this(keyword, VARIABLE_ARGUMENT_COUNT); - } - - RouterMicrodescriptorKeyword(String keyword, int argumentCount) { - this.keyword = keyword; - this.argumentCount = argumentCount; - } - - String getKeyword() { - return keyword; - } - - int getArgumentCount() { - return argumentCount; - } - - static RouterMicrodescriptorKeyword findKeyword(String keyword) { - for(RouterMicrodescriptorKeyword k: values()) { - if(k.getKeyword().equals(keyword)) { - return k; - } - } - return UNKNOWN_KEYWORD; - } -} diff --git a/orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorParser.java b/orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorParser.java deleted file mode 100644 index 418ccf3d..00000000 --- a/orchid/src/com/subgraph/orchid/directory/router/RouterMicrodescriptorParser.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.subgraph.orchid.directory.router; - -import com.subgraph.orchid.RouterMicrodescriptor; -import com.subgraph.orchid.TorParsingException; -import com.subgraph.orchid.crypto.TorMessageDigest; -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 RouterMicrodescriptorParser implements DocumentParser{ - - - private final DocumentFieldParser fieldParser; - - private RouterMicrodescriptorImpl currentDescriptor; - private DocumentParsingResultHandler resultHandler; - - public RouterMicrodescriptorParser(DocumentFieldParser fieldParser) { - this.fieldParser = fieldParser; - this.fieldParser.setHandler(createParsingHandler()); - } - - private DocumentParsingHandler createParsingHandler() { - return new DocumentParsingHandler() { - public void parseKeywordLine() { - processKeywordLine(); - } - public void endOfDocument() { - if(currentDescriptor != null) { - finalizeDescriptor(currentDescriptor); - } - } - }; - } - - public boolean parse(DocumentParsingResultHandler resultHandler) { - this.resultHandler = resultHandler; - try { - fieldParser.processDocument(); - return true; - } catch(TorParsingException e) { - resultHandler.parsingError(e.getMessage()); - return false; - } - } - - public DocumentParsingResult parse() { - final BasicDocumentParsingResult result = new BasicDocumentParsingResult(); - parse(result); - return result; - } - - private void processKeywordLine() { - final RouterMicrodescriptorKeyword keyword = RouterMicrodescriptorKeyword.findKeyword(fieldParser.getCurrentKeyword()); - if(!keyword.equals(RouterMicrodescriptorKeyword.UNKNOWN_KEYWORD)) { - processKeyword(keyword); - } - if(currentDescriptor != null) { - currentDescriptor.setRawDocumentData(fieldParser.getRawDocument()); - } - - } - - - private void processKeyword(RouterMicrodescriptorKeyword keyword) { - fieldParser.verifyExpectedArgumentCount(keyword.getKeyword(), keyword.getArgumentCount()); - switch(keyword) { - case ONION_KEY: - processOnionKeyLine(); - break; - - case NTOR_ONION_KEY: - if(currentDescriptor != null) { - currentDescriptor.setNtorOnionKey(fieldParser.parseNtorPublicKey()); - } - break; - - case FAMILY: - while(fieldParser.argumentsRemaining() > 0 && currentDescriptor != null) { - currentDescriptor.addFamilyMember(fieldParser.parseString()); - } - break; - - case P: - processP(); - break; - - case A: - default: - break; - } - } - - private void processOnionKeyLine() { - if(currentDescriptor != null) { - finalizeDescriptor(currentDescriptor); - } - currentDescriptor = new RouterMicrodescriptorImpl(); - fieldParser.resetRawDocument(RouterMicrodescriptorKeyword.ONION_KEY.getKeyword() + "\n"); - currentDescriptor.setOnionKey(fieldParser.parsePublicKey()); - } - - private void finalizeDescriptor(RouterMicrodescriptorImpl descriptor) { - final TorMessageDigest digest = new TorMessageDigest(true); - digest.update(descriptor.getRawDocumentData()); - descriptor.setDescriptorDigest(digest.getHexDigest()); - if(!descriptor.isValidDocument()) { - resultHandler.documentInvalid(descriptor, "Microdescriptor data invalid"); - } else { - resultHandler.documentParsed(descriptor); - } - } - - private void processP() { - if(currentDescriptor == null) { - return; - } - final String ruleType = fieldParser.parseString(); - if("accept".equals(ruleType)) { - currentDescriptor.addAcceptPorts(fieldParser.parseString()); - } else if("reject".equals(ruleType)) { - currentDescriptor.addRejectPorts(fieldParser.parseString()); - } else { - fieldParser.logWarn("Unexpected P field in microdescriptor: "+ ruleType); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/encoders/Base64.java b/orchid/src/com/subgraph/orchid/encoders/Base64.java deleted file mode 100644 index e1aaa924..00000000 --- a/orchid/src/com/subgraph/orchid/encoders/Base64.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.subgraph.orchid.encoders; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -public class Base64 -{ - private static final Encoder encoder = new Base64Encoder(); - - /** - * encode the input data producing a base 64 encoded byte array. - * - * @return a byte array containing the base 64 encoded data. - */ - public static byte[] encode( - byte[] data) - { - int len = (data.length + 2) / 3 * 4; - ByteArrayOutputStream bOut = new ByteArrayOutputStream(len); - - try - { - encoder.encode(data, 0, data.length, bOut); - } - catch (Exception e) - { - throw new EncoderException("exception encoding base64 string: " + e.getMessage(), e); - } - - return bOut.toByteArray(); - } - - /** - * Encode the byte data to base 64 writing it to the given output stream. - * - * @return the number of bytes produced. - */ - public static int encode( - byte[] data, - OutputStream out) - throws IOException - { - return encoder.encode(data, 0, data.length, out); - } - - /** - * Encode the byte data to base 64 writing it to the given output stream. - * - * @return the number of bytes produced. - */ - public static int encode( - byte[] data, - int off, - int length, - OutputStream out) - throws IOException - { - return encoder.encode(data, off, length, out); - } - - /** - * decode the base 64 encoded input data. It is assumed the input data is valid. - * - * @return a byte array representing the decoded data. - */ - public static byte[] decode( - byte[] data) - { - int len = data.length / 4 * 3; - ByteArrayOutputStream bOut = new ByteArrayOutputStream(len); - - try - { - encoder.decode(data, 0, data.length, bOut); - } - catch (Exception e) - { - throw new DecoderException("unable to decode base64 data: " + e.getMessage(), e); - } - - return bOut.toByteArray(); - } - - /** - * decode the base 64 encoded String data - whitespace will be ignored. - * - * @return a byte array representing the decoded data. - */ - public static byte[] decode( - String data) - { - int len = data.length() / 4 * 3; - ByteArrayOutputStream bOut = new ByteArrayOutputStream(len); - - try - { - encoder.decode(data, bOut); - } - catch (Exception e) - { - throw new DecoderException("unable to decode base64 string: " + e.getMessage(), e); - } - - return bOut.toByteArray(); - } - - /** - * decode the base 64 encoded String data writing it to the given output stream, - * whitespace characters will be ignored. - * - * @return the number of bytes produced. - */ - public static int decode( - String data, - OutputStream out) - throws IOException - { - return encoder.decode(data, out); - } -} diff --git a/orchid/src/com/subgraph/orchid/encoders/Base64Encoder.java b/orchid/src/com/subgraph/orchid/encoders/Base64Encoder.java deleted file mode 100644 index d27eb6a5..00000000 --- a/orchid/src/com/subgraph/orchid/encoders/Base64Encoder.java +++ /dev/null @@ -1,328 +0,0 @@ -package com.subgraph.orchid.encoders; - -import java.io.IOException; -import java.io.OutputStream; - -public class Base64Encoder - implements Encoder -{ - protected final byte[] encodingTable = - { - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', - (byte)'w', (byte)'x', (byte)'y', (byte)'z', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', - (byte)'7', (byte)'8', (byte)'9', - (byte)'+', (byte)'/' - }; - - protected byte padding = (byte)'='; - - /* - * set up the decoding table. - */ - protected final byte[] decodingTable = new byte[128]; - - protected void initialiseDecodingTable() - { - for (int i = 0; i < decodingTable.length; i++) - { - decodingTable[i] = (byte)0xff; - } - - for (int i = 0; i < encodingTable.length; i++) - { - decodingTable[encodingTable[i]] = (byte)i; - } - } - - public Base64Encoder() - { - initialiseDecodingTable(); - } - - /** - * encode the input data producing a base 64 output stream. - * - * @return the number of bytes produced. - */ - public int encode( - byte[] data, - int off, - int length, - OutputStream out) - throws IOException - { - int modulus = length % 3; - int dataLength = (length - modulus); - int a1, a2, a3; - - for (int i = off; i < off + dataLength; i += 3) - { - a1 = data[i] & 0xff; - a2 = data[i + 1] & 0xff; - a3 = data[i + 2] & 0xff; - - out.write(encodingTable[(a1 >>> 2) & 0x3f]); - out.write(encodingTable[((a1 << 4) | (a2 >>> 4)) & 0x3f]); - out.write(encodingTable[((a2 << 2) | (a3 >>> 6)) & 0x3f]); - out.write(encodingTable[a3 & 0x3f]); - } - - /* - * process the tail end. - */ - int b1, b2, b3; - int d1, d2; - - switch (modulus) - { - case 0: /* nothing left to do */ - break; - case 1: - d1 = data[off + dataLength] & 0xff; - b1 = (d1 >>> 2) & 0x3f; - b2 = (d1 << 4) & 0x3f; - - out.write(encodingTable[b1]); - out.write(encodingTable[b2]); - out.write(padding); - out.write(padding); - break; - case 2: - d1 = data[off + dataLength] & 0xff; - d2 = data[off + dataLength + 1] & 0xff; - - b1 = (d1 >>> 2) & 0x3f; - b2 = ((d1 << 4) | (d2 >>> 4)) & 0x3f; - b3 = (d2 << 2) & 0x3f; - - out.write(encodingTable[b1]); - out.write(encodingTable[b2]); - out.write(encodingTable[b3]); - out.write(padding); - break; - } - - return (dataLength / 3) * 4 + ((modulus == 0) ? 0 : 4); - } - - private boolean ignore( - char c) - { - return (c == '\n' || c =='\r' || c == '\t' || c == ' '); - } - - /** - * decode the base 64 encoded byte data writing it to the given output stream, - * whitespace characters will be ignored. - * - * @return the number of bytes produced. - */ - public int decode( - byte[] data, - int off, - int length, - OutputStream out) - throws IOException - { - byte b1, b2, b3, b4; - int outLen = 0; - - int end = off + length; - - while (end > off) - { - if (!ignore((char)data[end - 1])) - { - break; - } - - end--; - } - - int i = off; - int finish = end - 4; - - i = nextI(data, i, finish); - - while (i < finish) - { - b1 = decodingTable[data[i++]]; - - i = nextI(data, i, finish); - - b2 = decodingTable[data[i++]]; - - i = nextI(data, i, finish); - - b3 = decodingTable[data[i++]]; - - i = nextI(data, i, finish); - - b4 = decodingTable[data[i++]]; - - if ((b1 | b2 | b3 | b4) < 0) - { - throw new IOException("invalid characters encountered in base64 data"); - } - - out.write((b1 << 2) | (b2 >> 4)); - out.write((b2 << 4) | (b3 >> 2)); - out.write((b3 << 6) | b4); - - outLen += 3; - - i = nextI(data, i, finish); - } - - outLen += decodeLastBlock(out, (char)data[end - 4], (char)data[end - 3], (char)data[end - 2], (char)data[end - 1]); - - return outLen; - } - - private int nextI(byte[] data, int i, int finish) - { - while ((i < finish) && ignore((char)data[i])) - { - i++; - } - return i; - } - - /** - * decode the base 64 encoded String data writing it to the given output stream, - * whitespace characters will be ignored. - * - * @return the number of bytes produced. - */ - public int decode( - String data, - OutputStream out) - throws IOException - { - byte b1, b2, b3, b4; - int length = 0; - - int end = data.length(); - - while (end > 0) - { - if (!ignore(data.charAt(end - 1))) - { - break; - } - - end--; - } - - int i = 0; - int finish = end - 4; - - i = nextI(data, i, finish); - - while (i < finish) - { - b1 = decodingTable[data.charAt(i++)]; - - i = nextI(data, i, finish); - - b2 = decodingTable[data.charAt(i++)]; - - i = nextI(data, i, finish); - - b3 = decodingTable[data.charAt(i++)]; - - i = nextI(data, i, finish); - - b4 = decodingTable[data.charAt(i++)]; - - if ((b1 | b2 | b3 | b4) < 0) - { - throw new IOException("invalid characters encountered in base64 data"); - } - - out.write((b1 << 2) | (b2 >> 4)); - out.write((b2 << 4) | (b3 >> 2)); - out.write((b3 << 6) | b4); - - length += 3; - - i = nextI(data, i, finish); - } - - length += decodeLastBlock(out, data.charAt(end - 4), data.charAt(end - 3), data.charAt(end - 2), data.charAt(end - 1)); - - return length; - } - - private int decodeLastBlock(OutputStream out, char c1, char c2, char c3, char c4) - throws IOException - { - byte b1, b2, b3, b4; - - if (c3 == padding) - { - b1 = decodingTable[c1]; - b2 = decodingTable[c2]; - - if ((b1 | b2) < 0) - { - throw new IOException("invalid characters encountered at end of base64 data"); - } - - out.write((b1 << 2) | (b2 >> 4)); - - return 1; - } - else if (c4 == padding) - { - b1 = decodingTable[c1]; - b2 = decodingTable[c2]; - b3 = decodingTable[c3]; - - if ((b1 | b2 | b3) < 0) - { - throw new IOException("invalid characters encountered at end of base64 data"); - } - - out.write((b1 << 2) | (b2 >> 4)); - out.write((b2 << 4) | (b3 >> 2)); - - return 2; - } - else - { - b1 = decodingTable[c1]; - b2 = decodingTable[c2]; - b3 = decodingTable[c3]; - b4 = decodingTable[c4]; - - if ((b1 | b2 | b3 | b4) < 0) - { - throw new IOException("invalid characters encountered at end of base64 data"); - } - - out.write((b1 << 2) | (b2 >> 4)); - out.write((b2 << 4) | (b3 >> 2)); - out.write((b3 << 6) | b4); - - return 3; - } - } - - private int nextI(String data, int i, int finish) - { - while ((i < finish) && ignore(data.charAt(i))) - { - i++; - } - return i; - } -} diff --git a/orchid/src/com/subgraph/orchid/encoders/DecoderException.java b/orchid/src/com/subgraph/orchid/encoders/DecoderException.java deleted file mode 100644 index 3d627937..00000000 --- a/orchid/src/com/subgraph/orchid/encoders/DecoderException.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.subgraph.orchid.encoders; - -public class DecoderException - extends IllegalStateException -{ - private static final long serialVersionUID = 4997418733670548381L; - private Throwable cause; - - DecoderException(String msg, Throwable cause) - { - super(msg); - - this.cause = cause; - } - - public Throwable getCause() - { - return cause; - } -} diff --git a/orchid/src/com/subgraph/orchid/encoders/Encoder.java b/orchid/src/com/subgraph/orchid/encoders/Encoder.java deleted file mode 100644 index f1b931cc..00000000 --- a/orchid/src/com/subgraph/orchid/encoders/Encoder.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.subgraph.orchid.encoders; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * Encode and decode byte arrays (typically from binary to 7-bit ASCII - * encodings). - */ -public interface Encoder -{ - int encode(byte[] data, int off, int length, OutputStream out) throws IOException; - - int decode(byte[] data, int off, int length, OutputStream out) throws IOException; - - int decode(String data, OutputStream out) throws IOException; -} diff --git a/orchid/src/com/subgraph/orchid/encoders/EncoderException.java b/orchid/src/com/subgraph/orchid/encoders/EncoderException.java deleted file mode 100644 index 5051540e..00000000 --- a/orchid/src/com/subgraph/orchid/encoders/EncoderException.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.subgraph.orchid.encoders; - -public class EncoderException - extends IllegalStateException -{ - private static final long serialVersionUID = 6589388628939318400L; - private Throwable cause; - - EncoderException(String msg, Throwable cause) - { - super(msg); - - this.cause = cause; - } - - public Throwable getCause() - { - return cause; - } -} diff --git a/orchid/src/com/subgraph/orchid/encoders/Hex.java b/orchid/src/com/subgraph/orchid/encoders/Hex.java deleted file mode 100644 index 225c22ad..00000000 --- a/orchid/src/com/subgraph/orchid/encoders/Hex.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.subgraph.orchid.encoders; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -public class Hex -{ - private static final Encoder encoder = new HexEncoder(); - - /** - * encode the input data producing a Hex encoded byte array. - * - * @return a byte array containing the Hex encoded data. - */ - public static byte[] encode( - byte[] data) - { - return encode(data, 0, data.length); - } - - /** - * encode the input data producing a Hex encoded byte array. - * - * @return a byte array containing the Hex encoded data. - */ - public static byte[] encode( - byte[] data, - int off, - int length) - { - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - - try - { - encoder.encode(data, off, length, bOut); - } - catch (Exception e) - { - throw new EncoderException("exception encoding Hex string: " + e.getMessage(), e); - } - - return bOut.toByteArray(); - } - - /** - * Hex encode the byte data writing it to the given output stream. - * - * @return the number of bytes produced. - */ - public static int encode( - byte[] data, - OutputStream out) - throws IOException - { - return encoder.encode(data, 0, data.length, out); - } - - /** - * Hex encode the byte data writing it to the given output stream. - * - * @return the number of bytes produced. - */ - public static int encode( - byte[] data, - int off, - int length, - OutputStream out) - throws IOException - { - return encoder.encode(data, off, length, out); - } - - /** - * decode the Hex encoded input data. It is assumed the input data is valid. - * - * @return a byte array representing the decoded data. - */ - public static byte[] decode( - byte[] data) - { - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - - try - { - encoder.decode(data, 0, data.length, bOut); - } - catch (Exception e) - { - throw new DecoderException("exception decoding Hex data: " + e.getMessage(), e); - } - - return bOut.toByteArray(); - } - - /** - * decode the Hex encoded String data - whitespace will be ignored. - * - * @return a byte array representing the decoded data. - */ - public static byte[] decode( - String data) - { - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - - try - { - encoder.decode(data, bOut); - } - catch (Exception e) - { - throw new DecoderException("exception decoding Hex string: " + e.getMessage(), e); - } - - return bOut.toByteArray(); - } - - /** - * decode the Hex encoded String data writing it to the given output stream, - * whitespace characters will be ignored. - * - * @return the number of bytes produced. - */ - public static int decode( - String data, - OutputStream out) - throws IOException - { - return encoder.decode(data, out); - } -} diff --git a/orchid/src/com/subgraph/orchid/encoders/HexEncoder.java b/orchid/src/com/subgraph/orchid/encoders/HexEncoder.java deleted file mode 100644 index c859b645..00000000 --- a/orchid/src/com/subgraph/orchid/encoders/HexEncoder.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.subgraph.orchid.encoders; - -import java.io.IOException; -import java.io.OutputStream; - -public class HexEncoder - implements Encoder -{ - protected final byte[] encodingTable = - { - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', - (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f' - }; - - /* - * set up the decoding table. - */ - protected final byte[] decodingTable = new byte[128]; - - protected void initialiseDecodingTable() - { - for (int i = 0; i < decodingTable.length; i++) - { - decodingTable[i] = (byte)0xff; - } - - for (int i = 0; i < encodingTable.length; i++) - { - decodingTable[encodingTable[i]] = (byte)i; - } - - decodingTable['A'] = decodingTable['a']; - decodingTable['B'] = decodingTable['b']; - decodingTable['C'] = decodingTable['c']; - decodingTable['D'] = decodingTable['d']; - decodingTable['E'] = decodingTable['e']; - decodingTable['F'] = decodingTable['f']; - } - - public HexEncoder() - { - initialiseDecodingTable(); - } - - /** - * encode the input data producing a Hex output stream. - * - * @return the number of bytes produced. - */ - public int encode( - byte[] data, - int off, - int length, - OutputStream out) - throws IOException - { - for (int i = off; i < (off + length); i++) - { - int v = data[i] & 0xff; - - out.write(encodingTable[(v >>> 4)]); - out.write(encodingTable[v & 0xf]); - } - - return length * 2; - } - - private static boolean ignore( - char c) - { - return c == '\n' || c =='\r' || c == '\t' || c == ' '; - } - - /** - * decode the Hex encoded byte data writing it to the given output stream, - * whitespace characters will be ignored. - * - * @return the number of bytes produced. - */ - public int decode( - byte[] data, - int off, - int length, - OutputStream out) - throws IOException - { - byte b1, b2; - int outLen = 0; - - int end = off + length; - - while (end > off) - { - if (!ignore((char)data[end - 1])) - { - break; - } - - end--; - } - - int i = off; - while (i < end) - { - while (i < end && ignore((char)data[i])) - { - i++; - } - - b1 = decodingTable[data[i++]]; - - while (i < end && ignore((char)data[i])) - { - i++; - } - - b2 = decodingTable[data[i++]]; - - if ((b1 | b2) < 0) - { - throw new IOException("invalid characters encountered in Hex data"); - } - - out.write((b1 << 4) | b2); - - outLen++; - } - - return outLen; - } - - /** - * decode the Hex encoded String data writing it to the given output stream, - * whitespace characters will be ignored. - * - * @return the number of bytes produced. - */ - public int decode( - String data, - OutputStream out) - throws IOException - { - byte b1, b2; - int length = 0; - - int end = data.length(); - - while (end > 0) - { - if (!ignore(data.charAt(end - 1))) - { - break; - } - - end--; - } - - int i = 0; - while (i < end) - { - while (i < end && ignore(data.charAt(i))) - { - i++; - } - - b1 = decodingTable[data.charAt(i++)]; - - while (i < end && ignore(data.charAt(i))) - { - i++; - } - - b2 = decodingTable[data.charAt(i++)]; - - if ((b1 | b2) < 0) - { - throw new IOException("invalid characters encountered in Hex string"); - } - - out.write((b1 << 4) | b2); - - length++; - } - - return length; - } -} diff --git a/orchid/src/com/subgraph/orchid/events/Event.java b/orchid/src/com/subgraph/orchid/events/Event.java deleted file mode 100644 index f9921f65..00000000 --- a/orchid/src/com/subgraph/orchid/events/Event.java +++ /dev/null @@ -1,3 +0,0 @@ -package com.subgraph.orchid.events; - -public interface Event {} diff --git a/orchid/src/com/subgraph/orchid/events/EventHandler.java b/orchid/src/com/subgraph/orchid/events/EventHandler.java deleted file mode 100644 index 9002e2b6..00000000 --- a/orchid/src/com/subgraph/orchid/events/EventHandler.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.subgraph.orchid.events; - -public interface EventHandler { - void handleEvent(Event event); -} diff --git a/orchid/src/com/subgraph/orchid/events/EventManager.java b/orchid/src/com/subgraph/orchid/events/EventManager.java deleted file mode 100644 index 235dc7d8..00000000 --- a/orchid/src/com/subgraph/orchid/events/EventManager.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.subgraph.orchid.events; - -import java.util.ArrayList; -import java.util.List; - -public class EventManager { - private final List handlers = new ArrayList(); - - public void addListener(final EventHandler listener) { - synchronized(this) { - handlers.add(listener); - } - } - - public void removeListener(final EventHandler listener) { - synchronized(this) { - handlers.remove(listener); - } - } - - public void fireEvent(final Event event) { - EventHandler[] handlersCopy; - - synchronized(this) { - handlersCopy = new EventHandler[handlers.size()]; - handlers.toArray(handlersCopy); - } - for(EventHandler handler : handlersCopy) { - handler.handleEvent(event); - } - - } - -} diff --git a/orchid/src/com/subgraph/orchid/geoip/CountryCodeService.java b/orchid/src/com/subgraph/orchid/geoip/CountryCodeService.java deleted file mode 100644 index b2c49f60..00000000 --- a/orchid/src/com/subgraph/orchid/geoip/CountryCodeService.java +++ /dev/null @@ -1,172 +0,0 @@ -package com.subgraph.orchid.geoip; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.logging.Logger; - -import com.subgraph.orchid.data.IPv4Address; - -public class CountryCodeService { - private final static Logger logger = Logger.getLogger(CountryCodeService.class.getName()); - private final static String DATABASE_FILENAME = "GeoIP.dat"; - private final static int COUNTRY_BEGIN = 16776960; - private final static int STANDARD_RECORD_LENGTH = 3; - private final static int MAX_RECORD_LENGTH = 4; - private final static CountryCodeService DEFAULT_INSTANCE = new CountryCodeService(); - - public static CountryCodeService getInstance() { - return DEFAULT_INSTANCE; - } - - private static final String[] COUNTRY_CODES = { "--", "AP", "EU", "AD", "AE", - "AF", "AG", "AI", "AL", "AM", "CW", "AO", "AQ", "AR", "AS", "AT", - "AU", "AW", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", - "BJ", "BM", "BN", "BO", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", - "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", - "CO", "CR", "CU", "CV", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", - "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", - "FK", "FM", "FO", "FR", "SX", "GA", "GB", "GD", "GE", "GF", "GH", - "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", - "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", - "IO", "IQ", "IR", "IS", "IT", "JM", "JO", "JP", "KE", "KG", "KH", - "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", - "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", - "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", - "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", - "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", - "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", - "PY", "QA", "RE", "RO", "RU", "RW", "SA", "SB", "SC", "SD", "SE", - "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "ST", - "SV", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TM", - "TN", "TO", "TL", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", - "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", - "WS", "YE", "YT", "RS", "ZA", "ZM", "ME", "ZW", "A1", "A2", "O1", - "AX", "GG", "IM", "JE", "BL", "MF", "BQ", "SS", "O1" }; - - private final byte[] database; - - public CountryCodeService() { - this.database = loadDatabase(); - } - - private static byte[] loadDatabase() { - final InputStream input = openDatabaseStream(); - if(input == null) { - logger.warning("Failed to open '"+ DATABASE_FILENAME + "' database file for country code lookups"); - return null; - } - try { - return loadEntireStream(input); - } catch (IOException e) { - logger.warning("IO error reading database file for country code lookups"); - return null; - } finally { - try { - input.close(); - } catch (IOException e) { } - } - } - - private static InputStream openDatabaseStream() { - final InputStream input = tryResourceOpen(); - if(input != null) { - return input; - } else { - return tryFilesystemOpen(); - } - } - - private static InputStream tryFilesystemOpen() { - final File dataDir = new File(System.getProperty("user.dir"), "data"); - final File dbFile = new File(dataDir, DATABASE_FILENAME); - if(!dbFile.canRead()) { - return null; - } - try { - return new FileInputStream(dbFile); - } catch (FileNotFoundException e) { - return null; - } - } - - private static InputStream tryResourceOpen() { - return CountryCodeService.class.getResourceAsStream("/data/"+ DATABASE_FILENAME); - } - - private static byte[] loadEntireStream(InputStream input) throws IOException { - final ByteArrayOutputStream output = new ByteArrayOutputStream(4096); - copy(input, output); - return output.toByteArray(); - } - - private static int copy(InputStream input, OutputStream output) throws IOException { - final byte[] buffer = new byte[4096]; - int count = 0; - int n = 0; - while((n = input.read(buffer)) != -1) { - output.write(buffer, 0, n); - count += n; - } - return count; - } - - public String getCountryCodeForAddress(IPv4Address address) { - return COUNTRY_CODES[seekCountry(address)]; - } - - private int seekCountry(IPv4Address address) { - if(database == null) { - return 0; - } - - final byte[] record = new byte[2 * MAX_RECORD_LENGTH]; - final int[] x = new int[2]; - final long ip = address.getAddressData() & 0xFFFFFFFFL; - - int offset = 0; - for(int depth = 31; depth >= 0; depth--) { - loadRecord(offset, record); - - x[0] = unpackRecordValue(record, 0); - x[1] = unpackRecordValue(record, 1); - - int xx = ((ip & (1 << depth)) > 0) ? (x[1]) : (x[0]); - - if(xx >= COUNTRY_BEGIN) { - final int idx = xx - COUNTRY_BEGIN; - if(idx < 0 || idx > COUNTRY_CODES.length) { - logger.warning("Invalid index calculated looking up country code record for ("+ address +") idx = "+ idx); - return 0; - } else { - return idx; - } - } else { - offset = xx; - } - - } - logger.warning("No record found looking up country code record for ("+ address + ")"); - return 0; - } - - private void loadRecord(int offset, byte[] recordBuffer) { - final int dbOffset = 2 * STANDARD_RECORD_LENGTH * offset; - System.arraycopy(database, dbOffset, recordBuffer, 0, recordBuffer.length); - } - - private int unpackRecordValue(byte[] record, int idx) { - final int valueOffset = idx * STANDARD_RECORD_LENGTH; - int value = 0; - for(int i = 0; i < STANDARD_RECORD_LENGTH; i++) { - int octet = record[valueOffset + i] & 0xFF; - value += (octet << (i * 8)); - } - return value; - } - -} diff --git a/orchid/src/com/subgraph/orchid/misc/GuardedBy.java b/orchid/src/com/subgraph/orchid/misc/GuardedBy.java deleted file mode 100644 index cd7f913b..00000000 --- a/orchid/src/com/subgraph/orchid/misc/GuardedBy.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.subgraph.orchid.misc; - - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * The field or method to which this annotation is applied can only be accessed - * when holding a particular lock, which may be a built-in (synchronization) lock, - * or may be an explicit java.util.concurrent.Lock. - * - * The argument determines which lock guards the annotated field or method: - *
    - *
  • - * this : The intrinsic lock of the object in whose class the field is defined. - *
  • - *
  • - * class-name.this : For inner classes, it may be necessary to disambiguate 'this'; - * the class-name.this designation allows you to specify which 'this' reference is intended - *
  • - *
  • - * itself : For reference fields only; the object to which the field refers. - *
  • - *
  • - * field-name : The lock object is referenced by the (instance or static) field - * specified by field-name. - *
  • - *
  • - * class-name.field-name : The lock object is reference by the static field specified - * by class-name.field-name. - *
  • - *
  • - * method-name() : The lock object is returned by calling the named nil-ary method. - *
  • - *
  • - * class-name.class : The Class object for the specified class should be used as the lock object. - *
  • - */ -@Target({ElementType.FIELD, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface GuardedBy { - String value(); -} diff --git a/orchid/src/com/subgraph/orchid/misc/Immutable.java b/orchid/src/com/subgraph/orchid/misc/Immutable.java deleted file mode 100644 index 18e8a6b3..00000000 --- a/orchid/src/com/subgraph/orchid/misc/Immutable.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.subgraph.orchid.misc; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/* - * Copyright (c) 2005 Brian Goetz and Tim Peierls - * Released under the Creative Commons Attribution License - * (http://creativecommons.org/licenses/by/2.5) - * Official home: http://www.jcip.net - * - * Any republication or derived work distributed in source code form - * must include this copyright and license notice. - */ - - -/** - * The class to which this annotation is applied is immutable. This means that - * its state cannot be seen to change by callers, which implies that - *
      - *
    • all public fields are final,
    • - *
    • all public final reference fields refer to other immutable objects, and
    • - *
    • constructors and methods do not publish references to any internal state - * which is potentially mutable by the implementation.
    • - *
    - * Immutable objects may still have internal mutable state for purposes of performance - * optimization; some state variables may be lazily computed, so long as they are computed - * from immutable state and that callers cannot tell the difference. - *

    - * Immutable objects are inherently thread-safe; they may be passed between threads or - * published without synchronization. - */ -@Documented -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface Immutable { - -} diff --git a/orchid/src/com/subgraph/orchid/misc/NotThreadSafe.java b/orchid/src/com/subgraph/orchid/misc/NotThreadSafe.java deleted file mode 100644 index 7ca2360f..00000000 --- a/orchid/src/com/subgraph/orchid/misc/NotThreadSafe.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.subgraph.orchid.misc; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/* - * Copyright (c) 2005 Brian Goetz and Tim Peierls - * Released under the Creative Commons Attribution License - * (http://creativecommons.org/licenses/by/2.5) - * Official home: http://www.jcip.net - * - * Any republication or derived work distributed in source code form - * must include this copyright and license notice. - */ - - -/** - * The class to which this annotation is applied is not thread-safe. - * This annotation primarily exists for clarifying the non-thread-safety of a class - * that might otherwise be assumed to be thread-safe, despite the fact that it is a bad - * idea to assume a class is thread-safe without good reason. - * @see ThreadSafe - */ -@Documented -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface NotThreadSafe { -} - - - diff --git a/orchid/src/com/subgraph/orchid/misc/ThreadSafe.java b/orchid/src/com/subgraph/orchid/misc/ThreadSafe.java deleted file mode 100644 index 6fe32781..00000000 --- a/orchid/src/com/subgraph/orchid/misc/ThreadSafe.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.subgraph.orchid.misc; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/* - * Copyright (c) 2005 Brian Goetz and Tim Peierls - * Released under the Creative Commons Attribution License - * (http://creativecommons.org/licenses/by/2.5) - * Official home: http://www.jcip.net - * - * Any republication or derived work distributed in source code form - * must include this copyright and license notice. - */ - - - -/** - * The class to which this annotation is applied is thread-safe. This means that - * no sequences of accesses (reads and writes to public fields, calls to public methods) - * may put the object into an invalid state, regardless of the interleaving of those actions - * by the runtime, and without requiring any additional synchronization or coordination on the - * part of the caller. - */ -@Documented -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface ThreadSafe { -} - diff --git a/orchid/src/com/subgraph/orchid/misc/Utils.java b/orchid/src/com/subgraph/orchid/misc/Utils.java deleted file mode 100644 index 4e12925d..00000000 --- a/orchid/src/com/subgraph/orchid/misc/Utils.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.subgraph.orchid.misc; - -public class Utils { - public static boolean constantTimeArrayEquals(byte[] a1, byte[] a2) { - if(a1.length != a2.length) { - return false; - } - int result = 0; - for(int i = 0; i < a1.length; i++) { - result += (a1[i] & 0xFF) ^ (a2[i] & 0xFF); - } - return result == 0; - } -} diff --git a/orchid/src/com/subgraph/orchid/sockets/AndroidSSLSocketFactory.java b/orchid/src/com/subgraph/orchid/sockets/AndroidSSLSocketFactory.java deleted file mode 100644 index 877a5f6c..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/AndroidSSLSocketFactory.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.subgraph.orchid.sockets; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.security.NoSuchAlgorithmException; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; - -import com.subgraph.orchid.sockets.sslengine.SSLEngineSSLSocket; - -public class AndroidSSLSocketFactory extends SSLSocketFactory { - - private final SSLContext sslContext; - - public AndroidSSLSocketFactory() throws NoSuchAlgorithmException { - this(SSLContext.getDefault()); - } - - public AndroidSSLSocketFactory(SSLContext sslContext) { - this.sslContext = sslContext; - } - - @Override - public String[] getDefaultCipherSuites() { - return sslContext.getDefaultSSLParameters().getCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return sslContext.getSupportedSSLParameters().getCipherSuites(); - - } - - @Override - public Socket createSocket(Socket s, String host, int port, - boolean autoClose) throws IOException { - - return new SSLEngineSSLSocket(s, sslContext); - } - - @Override - public Socket createSocket(String host, int port) throws IOException, - UnknownHostException { - throw new UnsupportedOperationException(); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, - int localPort) throws IOException, UnknownHostException { - throw new UnsupportedOperationException(); - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public Socket createSocket(InetAddress address, int port, - InetAddress localAddress, int localPort) throws IOException { - throw new UnsupportedOperationException(); - } -} diff --git a/orchid/src/com/subgraph/orchid/sockets/AndroidSocket.java b/orchid/src/com/subgraph/orchid/sockets/AndroidSocket.java deleted file mode 100644 index b911df93..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/AndroidSocket.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.subgraph.orchid.sockets; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; -import java.util.logging.Logger; - -public class AndroidSocket extends Socket { - private final static Logger logger = Logger.getLogger(AndroidSocket.class.getName()); - - private final Field isConnectedField; - private final OrchidSocketImpl impl; - private final Object lock = new Object(); - private boolean isSocketConnected; - - AndroidSocket(OrchidSocketImpl impl) throws SocketException { - super(impl); - this.impl = impl; - this.isConnectedField = getField("isConnected"); - } - - public void connect(SocketAddress endpoint) throws IOException { - connect(endpoint, 0); - } - - public void connect(SocketAddress endpoint, int timeout) throws IOException { - synchronized(lock) { - if(isSocketConnected) { - throw new SocketException("Already connected"); - } - try { - impl.connect(endpoint, timeout); - setIsConnected(); - } catch(IOException e) { - impl.close(); - throw e; - } - } - } - - protected void setIsConnected() { - isSocketConnected = true; - try { - if(isConnectedField != null) { - isConnectedField.setBoolean(this, true); - } - } catch (IllegalArgumentException e) { - logger.warning("Illegal argument trying to reflect value into isConnected field of Socket : "+ e.getMessage()); - } catch (IllegalAccessException e) { - logger.warning("Illegal access trying to reflect value into isConnected field of Socket : "+ e.getMessage()); - } - } - - private Field getField(String name) { - try { - final Field f = Socket.class.getDeclaredField(name); - f.setAccessible(true); - return f; - } catch (NoSuchFieldException e) { - logger.warning("Could not locate field '"+ name +"' in Socket class, disabling Android reflection"); - return null; - } catch (SecurityException e) { - logger.warning("Reflection access to field '"+ name +"' in Socket class not permitted."+ e.getMessage()); - return null; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/sockets/OrchidSocketFactory.java b/orchid/src/com/subgraph/orchid/sockets/OrchidSocketFactory.java deleted file mode 100644 index 28febaf5..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/OrchidSocketFactory.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.subgraph.orchid.sockets; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; -import java.net.UnknownHostException; - -import javax.net.SocketFactory; - -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.TorClient; - -public class OrchidSocketFactory extends SocketFactory { - private final TorClient torClient; - private final boolean exceptionOnLocalBind; - - public OrchidSocketFactory(TorClient torClient) { - this(torClient, true); - } - - public OrchidSocketFactory(TorClient torClient, boolean exceptionOnLocalBind) { - this.torClient = torClient; - this.exceptionOnLocalBind = exceptionOnLocalBind; - } - - @Override - public Socket createSocket() throws IOException { - return createSocketInstance(); - } - - @Override - public Socket createSocket(String host, int port) throws IOException, - UnknownHostException { - final Socket s = createSocketInstance(); - return connectOrchidSocket(s, host, port); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, - int localPort) throws IOException, UnknownHostException { - if(exceptionOnLocalBind) { - throw new UnsupportedOperationException("Cannot bind to local address"); - } - return createSocket(host, port); - } - - @Override - public Socket createSocket(InetAddress address, int port) throws IOException { - final Socket s = createSocketInstance(); - return connectOrchidSocket(s, address.getHostAddress(), port); - } - - @Override - public Socket createSocket(InetAddress address, int port, - InetAddress localAddress, int localPort) throws IOException { - if(exceptionOnLocalBind) { - throw new UnsupportedOperationException("Cannot bind to local address"); - } - return createSocket(address, port); - } - - private Socket connectOrchidSocket(Socket s, String host, int port) throws IOException { - final SocketAddress endpoint = InetSocketAddress.createUnresolved(host, port); - s.connect(endpoint); - return s; - } - - private Socket createSocketInstance() throws SocketException { - final OrchidSocketImpl impl = new OrchidSocketImpl(torClient); - if(Tor.isAndroidRuntime()) { - return new AndroidSocket(impl); - } else { - // call protected constructor - return new Socket(impl) {}; - } - } -} diff --git a/orchid/src/com/subgraph/orchid/sockets/OrchidSocketImpl.java b/orchid/src/com/subgraph/orchid/sockets/OrchidSocketImpl.java deleted file mode 100644 index 5b92a79a..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/OrchidSocketImpl.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.subgraph.orchid.sockets; - -import com.subgraph.orchid.OpenFailedException; -import com.subgraph.orchid.Stream; -import com.subgraph.orchid.Threading; -import com.subgraph.orchid.TorClient; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.*; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.locks.Lock; - -public class OrchidSocketImpl extends SocketImpl { - private final TorClient torClient; - - private Lock streamLock = Threading.lock("stream"); - private Stream stream; - - OrchidSocketImpl(TorClient torClient) { - this.torClient = torClient; - this.fd = new FileDescriptor(); - } - - public void setOption(int optID, Object value) throws SocketException { - // Ignored. - } - - public Object getOption(int optID) throws SocketException { - if(optID == SocketOptions.SO_LINGER) { - return 0; - } else if(optID == SocketOptions.TCP_NODELAY) { - return Boolean.TRUE; - } else if(optID == SocketOptions.SO_TIMEOUT) { - return 0; - } else { - return 0; - } - } - - @Override - protected void create(boolean stream) throws IOException { - - } - - @Override - protected void connect(String host, int port) throws IOException { - SocketAddress endpoint = - InetSocketAddress.createUnresolved(host, port); - connect(endpoint, 0); - } - - @Override - protected void connect(InetAddress address, int port) throws IOException { - SocketAddress endpoint = - InetSocketAddress.createUnresolved(address.getHostAddress(), port); - connect(endpoint, 0); - } - - @Override - protected void connect(SocketAddress address, int timeout) - throws IOException { - if(!(address instanceof InetSocketAddress)) { - throw new IllegalArgumentException("Unsupported address type"); - } - final InetSocketAddress inetAddress = (InetSocketAddress) address; - - doConnect(addressToName(inetAddress), inetAddress.getPort()); - } - - private String addressToName(InetSocketAddress address) { - if(address.getAddress() != null) { - return address.getAddress().getHostAddress(); - } else { - return address.getHostName(); - } - } - - private void doConnect(String host, int port) throws IOException { - Stream stream; - - // Try to avoid holding the stream lock here whilst calling into torclient to avoid accidental inversions. - - streamLock.lock(); - stream = this.stream; - streamLock.unlock(); - - if (stream != null) - throw new SocketException("Already connected"); - - try { - stream = torClient.openExitStreamTo(host, port); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new SocketException("connect() interrupted"); - } catch (TimeoutException e) { - throw new SocketTimeoutException(); - } catch (OpenFailedException e) { - throw new ConnectException(e.getMessage()); - } - - streamLock.lock(); - if (this.stream != null) { - // Raced with another concurrent call. - streamLock.unlock(); - stream.close(); - } else { - this.stream = stream; - streamLock.unlock(); - } - } - - @Override - protected void bind(InetAddress host, int port) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - protected void listen(int backlog) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - protected void accept(SocketImpl s) throws IOException { - throw new UnsupportedOperationException(); - } - - private Stream getStream() throws IOException { - streamLock.lock(); - try { - if (stream == null) - throw new IOException("Not connected"); - return stream; - } finally { - streamLock.unlock(); - } - } - - @Override - protected InputStream getInputStream() throws IOException { - return getStream().getInputStream(); - } - - @Override - protected OutputStream getOutputStream() throws IOException { - return getStream().getOutputStream(); - } - - @Override - protected int available() throws IOException { - return getStream().getInputStream().available(); - } - - @Override - protected void close() throws IOException { - Stream toClose; - streamLock.lock(); - toClose = this.stream; - this.stream = null; - streamLock.unlock(); - if (toClose != null) - toClose.close(); - } - - @Override - protected void sendUrgentData(int data) throws IOException { - throw new UnsupportedOperationException(); - } - - protected void shutdownInput() throws IOException { - //throw new IOException("Method not implemented!"); - } - - protected void shutdownOutput() throws IOException { - //throw new IOException("Method not implemented!"); - } -} diff --git a/orchid/src/com/subgraph/orchid/sockets/OrchidSocketImplFactory.java b/orchid/src/com/subgraph/orchid/sockets/OrchidSocketImplFactory.java deleted file mode 100644 index 7c96fcbb..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/OrchidSocketImplFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.subgraph.orchid.sockets; - -import java.net.SocketImpl; -import java.net.SocketImplFactory; - -import com.subgraph.orchid.TorClient; - -public class OrchidSocketImplFactory implements SocketImplFactory { - private final TorClient torClient; - - public OrchidSocketImplFactory(TorClient torClient) { - this.torClient = torClient; - } - - public SocketImpl createSocketImpl() { - return new OrchidSocketImpl(torClient); - } -} diff --git a/orchid/src/com/subgraph/orchid/sockets/sslengine/HandshakeCallbackHandler.java b/orchid/src/com/subgraph/orchid/sockets/sslengine/HandshakeCallbackHandler.java deleted file mode 100644 index b26c20c4..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/sslengine/HandshakeCallbackHandler.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.subgraph.orchid.sockets.sslengine; - -public interface HandshakeCallbackHandler { - void handshakeCompleted(); -} diff --git a/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineInputStream.java b/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineInputStream.java deleted file mode 100644 index 75ade871..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineInputStream.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.subgraph.orchid.sockets.sslengine; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -public class SSLEngineInputStream extends InputStream { - - private final SSLEngineManager manager; - private final ByteBuffer recvBuffer; - private boolean isEOF; - - SSLEngineInputStream(SSLEngineManager manager) { - this.manager = manager; - this.recvBuffer = manager.getRecvBuffer(); - } - - @Override - public int read() throws IOException { - if(!fillRecvBufferIfEmpty()) { - return -1; - } - final int b = recvBuffer.get() & 0xFF; - recvBuffer.compact(); - return b; - } - - @Override - public int read(byte b[], int off, int len) throws IOException { - if(!fillRecvBufferIfEmpty()) { - return -1; - } - final int copyLen = Math.min(recvBuffer.remaining(), len); - recvBuffer.get(b, off, copyLen); - recvBuffer.compact(); - return copyLen; - } - - @Override - public void close() throws IOException { - manager.close(); - } - - private boolean fillRecvBufferIfEmpty() throws IOException { - if(isEOF) { - return false; - } - if(recvBuffer.position() == 0) { - if(manager.read() < 0) { - isEOF = true; - return false; - } - } - recvBuffer.flip(); - return recvBuffer.hasRemaining(); - } -} diff --git a/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineManager.java b/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineManager.java deleted file mode 100644 index 5cc6928e..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineManager.java +++ /dev/null @@ -1,343 +0,0 @@ -package com.subgraph.orchid.sockets.sslengine; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.SocketException; -import java.nio.BufferOverflowException; -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLEngineResult.HandshakeStatus; -import javax.net.ssl.SSLEngineResult.Status; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; - -public class SSLEngineManager { - private final static Logger logger = Logger.getLogger(SSLEngineManager.class.getName()); - - private final SSLEngine engine; - private final InputStream input; - private final OutputStream output; - - private final ByteBuffer peerApplicationBuffer; - private final ByteBuffer peerNetworkBuffer; - private final ByteBuffer myApplicationBuffer; - private final ByteBuffer myNetworkBuffer; - - private final HandshakeCallbackHandler handshakeCallback; - - private boolean handshakeStarted = false; - - - SSLEngineManager(SSLEngine engine, HandshakeCallbackHandler handshakeCallback, InputStream input, OutputStream output) { - this.engine = engine; - this.handshakeCallback = handshakeCallback; - this.input = input; - this.output = output; - final SSLSession session = engine.getSession(); - this.peerApplicationBuffer = createApplicationBuffer(session); - this.peerNetworkBuffer = createPacketBuffer(session); - this.myApplicationBuffer = createApplicationBuffer(session); - this.myNetworkBuffer = createPacketBuffer(session); - } - - private static ByteBuffer createApplicationBuffer(SSLSession session) { - return createBuffer(session.getApplicationBufferSize()); - } - - private static ByteBuffer createPacketBuffer(SSLSession session) { - return createBuffer(session.getPacketBufferSize()); - } - - private static ByteBuffer createBuffer(int sz) { - final byte[] array = new byte[sz]; - return ByteBuffer.wrap(array); - } - - void startHandshake() throws IOException { - logger.fine("startHandshake()"); - handshakeStarted = true; - engine.beginHandshake(); - runHandshake(); - } - - ByteBuffer getSendBuffer() { - return myApplicationBuffer; - } - - ByteBuffer getRecvBuffer() { - return peerApplicationBuffer; - } - - - int write() throws IOException { - logger.fine("write()"); - if(!handshakeStarted) { - startHandshake(); - } - final int p = myApplicationBuffer.position(); - if(p == 0) { - return 0; - } - myNetworkBuffer.clear(); - myApplicationBuffer.flip(); - final SSLEngineResult result = engine.wrap(myApplicationBuffer, myNetworkBuffer); - myApplicationBuffer.compact(); - if(logger.isLoggable(Level.FINE)) { - logResult(result); - } - - switch(result.getStatus()) { - case BUFFER_OVERFLOW: - throw new BufferOverflowException(); - case BUFFER_UNDERFLOW: - throw new BufferUnderflowException(); - case CLOSED: - throw new SSLException("SSLEngine is closed"); - - case OK: - break; - default: - break; - } - - flush(); - if(runHandshake()) { - write(); - } - - return p - myApplicationBuffer.position(); - - } - - // either return -1 or peerApplicationBuffer has data to read - int read() throws IOException { - logger.fine("read()"); - if(!handshakeStarted) { - startHandshake(); - } - - if(engine.isInboundDone()) { - return -1; - } - - final int n = networkReadBuffer(peerNetworkBuffer); - if(n == -1) { - return -1; - } - final int p = peerApplicationBuffer.position(); - - peerNetworkBuffer.flip(); - final SSLEngineResult result = engine.unwrap(peerNetworkBuffer, peerApplicationBuffer); - peerNetworkBuffer.compact(); - if(logger.isLoggable(Level.FINE)) { - logResult(result); - } - - switch(result.getStatus()) { - case BUFFER_OVERFLOW: - throw new BufferOverflowException(); - - case BUFFER_UNDERFLOW: - return 0; // <-- illegal return according to invariant - - case CLOSED: - input.close(); - break; - case OK: - break; - default: - break; - } - - runHandshake(); - - if(n == -1) { // <-- can't happen - engine.closeInbound(); - } - if(engine.isInboundDone()) { - return -1; - } - return peerApplicationBuffer.position() - p; - } - - void close() throws IOException { - try { - flush(); - if(!engine.isOutboundDone()) { - engine.closeOutbound(); - runHandshake(); - } else if(!engine.isInboundDone()) { - engine.closeInbound(); - runHandshake(); - } - } finally { - output.close(); - } - } - - void flush() throws IOException { - myNetworkBuffer.flip(); - networkWriteBuffer(myNetworkBuffer); - myNetworkBuffer.compact(); - } - - - private boolean runHandshake() throws IOException { - boolean handshakeRan = false; - while(true) { - if(!processHandshake()) { - return handshakeRan; - } else { - handshakeRan = true; - } - } - } - - private boolean processHandshake() throws IOException { - final HandshakeStatus hs = engine.getHandshakeStatus(); - logger.fine("processHandshake() hs = "+ hs); - switch(hs) { - case NEED_TASK: - synchronousRunDelegatedTasks(); - return processHandshake(); - - case NEED_UNWRAP: - return handshakeUnwrap(); - - case NEED_WRAP: - return handshakeWrap(); - - default: - return false; - } - } - - private void synchronousRunDelegatedTasks() { - logger.fine("runDelegatedTasks()"); - while(true) { - Runnable r = engine.getDelegatedTask(); - if(r == null) { - return; - } - logger.fine("Running a task: "+ r); - r.run(); - } - } - - private boolean handshakeUnwrap() throws IOException { - logger.fine("handshakeUnwrap()"); - - if(!engine.isInboundDone() && peerNetworkBuffer.position() == 0) { - if(networkReadBuffer(peerNetworkBuffer) < 0) { - return false; - } - } - peerNetworkBuffer.flip(); - final SSLEngineResult result = engine.unwrap(peerNetworkBuffer, peerApplicationBuffer); - peerNetworkBuffer.compact(); - - if(logger.isLoggable(Level.FINE)) { - logResult(result); - } - - if(result.getHandshakeStatus() == HandshakeStatus.FINISHED) { - handshakeFinished(); - } - switch(result.getStatus()) { - - case CLOSED: - if(engine.isOutboundDone()) { - output.close(); - } - return false; - case OK: - return true; - case BUFFER_UNDERFLOW: - if(networkReadBuffer(peerNetworkBuffer) < 0) { - return false; - } - return true; - default: - return false; - } - } - - private boolean handshakeWrap() throws IOException { - logger.fine("handshakeWrap()"); - myApplicationBuffer.flip(); - final SSLEngineResult result = engine.wrap(myApplicationBuffer, myNetworkBuffer); - myApplicationBuffer.compact(); - if(logger.isLoggable(Level.FINE)) { - logResult(result); - } - - if(result.getHandshakeStatus() == HandshakeStatus.FINISHED) { - handshakeFinished(); - } - - if(result.getStatus() == Status.CLOSED) { - try { - flush(); - } catch (SocketException e) { - e.printStackTrace(); - } - } else { - flush(); - } - - switch(result.getStatus()) { - case CLOSED: - if(engine.isOutboundDone()) { - output.close(); - } - return false; - - case OK: - return true; - - default: - return false; - - } - } - - private void logResult(SSLEngineResult result) { - logger.fine("Result status="+result.getStatus() + " hss="+ result.getHandshakeStatus() + " consumed = "+ result.bytesConsumed() + " produced = "+ result.bytesProduced()); - } - - private void handshakeFinished() { - if(handshakeCallback != null) { - handshakeCallback.handshakeCompleted(); - } - } - - private void networkWriteBuffer(ByteBuffer buffer) throws IOException { - final byte[] bs = buffer.array(); - final int off = buffer.position(); - final int len = buffer.limit() - off; - logger.fine("networkWriteBuffer(b, "+ off + ", "+ len +")"); - output.write(bs, off, len); - output.flush(); - buffer.position(buffer.limit()); - } - - private int networkReadBuffer(ByteBuffer buffer) throws IOException { - final byte[] bs = buffer.array(); - final int off = buffer.position(); - final int len = buffer.limit() - off; - - final int n = input.read(bs, off, len); - if(n != -1) { - buffer.position(off + n); - } - logger.fine("networkReadBuffer(b, "+ off +", "+ len +") = "+ n); - return n; - } - -} diff --git a/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineOutputStream.java b/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineOutputStream.java deleted file mode 100644 index 62cc11aa..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineOutputStream.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.subgraph.orchid.sockets.sslengine; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -public class SSLEngineOutputStream extends OutputStream { - - private final SSLEngineManager manager; - private final ByteBuffer outputBuffer; - - public SSLEngineOutputStream(SSLEngineManager manager) { - this.manager = manager; - this.outputBuffer = manager.getSendBuffer(); - } - - @Override - public void write(int b) throws IOException { - outputBuffer.put((byte) b); - manager.write(); - } - - @Override - public void write(byte b[], int off, int len) throws IOException { - int written = 0; - - while(written < len) { - int n = doWrite(b, off + written, len - written); - - written += n; - } - } - - @Override - public void close() throws IOException { - manager.close(); - } - - private int doWrite(byte[] b, int off, int len) throws IOException { - int putLength = Math.min(len, outputBuffer.remaining()); - outputBuffer.put(b, off, putLength); - manager.write(); - return putLength; - } - -} diff --git a/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineSSLSocket.java b/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineSSLSocket.java deleted file mode 100644 index 0f87e05d..00000000 --- a/orchid/src/com/subgraph/orchid/sockets/sslengine/SSLEngineSSLSocket.java +++ /dev/null @@ -1,315 +0,0 @@ -package com.subgraph.orchid.sockets.sslengine; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import javax.net.ssl.HandshakeCompletedEvent; -import javax.net.ssl.HandshakeCompletedListener; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; - -public class SSLEngineSSLSocket extends SSLSocket implements HandshakeCallbackHandler { - - private final SSLEngine engine; - private final SSLEngineManager manager; - - private Socket socket; - private InputStream inputStream; - private OutputStream outputStream; - private final List listenerList; - public SSLEngineSSLSocket(Socket socket, SSLContext context) throws IOException { - this.engine = createSSLEngine(context); - this.socket = socket; - this.manager = new SSLEngineManager(engine, this, socket.getInputStream(), socket.getOutputStream()); - this.listenerList = new CopyOnWriteArrayList(); - } - - private static SSLEngine createSSLEngine(SSLContext context) { - final SSLEngine engine = context.createSSLEngine(); - engine.setUseClientMode(true); - return engine; - } - - @Override - public String[] getSupportedCipherSuites() { - return engine.getSupportedCipherSuites(); - } - - @Override - public String[] getEnabledCipherSuites() { - return engine.getEnabledCipherSuites(); - } - - @Override - public void setEnabledCipherSuites(String[] suites) { - engine.setEnabledCipherSuites(suites); - } - - @Override - public String[] getSupportedProtocols() { - return engine.getSupportedProtocols(); - } - - @Override - public String[] getEnabledProtocols() { - return engine.getEnabledProtocols(); - } - - @Override - public void setEnabledProtocols(String[] protocols) { - engine.setEnabledProtocols(protocols); - } - - @Override - public SSLSession getSession() { - return engine.getSession(); - } - - @Override - public void addHandshakeCompletedListener( - HandshakeCompletedListener listener) { - listenerList.add(listener); - } - - @Override - public void removeHandshakeCompletedListener( - HandshakeCompletedListener listener) { - listenerList.remove(listener); - } - - @Override - public void startHandshake() throws IOException { - manager.startHandshake(); - } - - @Override - public void setUseClientMode(boolean mode) { - engine.setUseClientMode(mode); - } - - @Override - public boolean getUseClientMode() { - return engine.getUseClientMode(); - } - - @Override - public void setNeedClientAuth(boolean need) { - engine.setNeedClientAuth(need); - } - - @Override - public boolean getNeedClientAuth() { - return engine.getNeedClientAuth(); - } - - @Override - public void setWantClientAuth(boolean want) { - engine.setWantClientAuth(want); - } - - @Override - public boolean getWantClientAuth() { - return engine.getWantClientAuth(); - } - - @Override - public void connect(SocketAddress endpoint) throws IOException { - throw new IOException("Socket is already connected"); - } - - @Override - public void connect(SocketAddress endpoint, int timeout) throws IOException { - throw new IOException("Socket is already connected"); - } - - @Override - public void bind(SocketAddress bindpoint) throws IOException { - throw new IOException("Socket is already connected"); - } - - @Override - public InetAddress getInetAddress() { - return socket.getInetAddress(); - } - - @Override - public InetAddress getLocalAddress() { - return socket.getLocalAddress(); - } - - @Override - public int getPort() { - return socket.getPort(); - } - - @Override - public int getLocalPort() { - return socket.getLocalPort(); - } - - @Override - public SocketAddress getRemoteSocketAddress() { - return socket.getRemoteSocketAddress(); - } - - @Override - public SocketAddress getLocalSocketAddress() { - return socket.getLocalSocketAddress(); - } - - @Override - public void setTcpNoDelay(boolean on) throws SocketException { - socket.setTcpNoDelay(on); - } - - @Override - public boolean getTcpNoDelay() throws SocketException { - return socket.getTcpNoDelay(); - } - - @Override - public void setSoLinger(boolean on, int linger) throws SocketException { - socket.setSoLinger(on, linger); - } - - @Override - public int getSoLinger() throws SocketException { - return socket.getSoLinger(); - } - - @Override - public void setOOBInline(boolean on) throws SocketException { - socket.setOOBInline(on); - } - - @Override - public boolean getOOBInline() throws SocketException { - return socket.getOOBInline(); - } - - @Override - public synchronized void setSoTimeout(int timeout) throws SocketException { - socket.setSoTimeout(timeout); - } - - @Override - public synchronized int getSoTimeout() throws SocketException { - return socket.getSoTimeout(); - } - - @Override - public synchronized void setSendBufferSize(int size) throws SocketException { - socket.setSendBufferSize(size); - } - - @Override - public synchronized int getSendBufferSize() throws SocketException { - return socket.getSendBufferSize(); - } - - @Override - public synchronized void setReceiveBufferSize(int size) - throws SocketException { - socket.setReceiveBufferSize(size); - } - - @Override - public synchronized int getReceiveBufferSize() throws SocketException { - return socket.getReceiveBufferSize(); - } - - @Override - public void setKeepAlive(boolean on) throws SocketException { - socket.setKeepAlive(on); - } - - @Override - public boolean getKeepAlive() throws SocketException { - return socket.getKeepAlive(); - } - - @Override - public void setTrafficClass(int tc) throws SocketException { - socket.setTrafficClass(tc); - } - - @Override - public int getTrafficClass() throws SocketException { - return socket.getTrafficClass(); - } - - @Override - public void setReuseAddress(boolean on) throws SocketException { - socket.setReuseAddress(on); - } - - @Override - public boolean getReuseAddress() throws SocketException { - return socket.getReuseAddress(); - } - - @Override - public void shutdownInput() throws IOException { - throw new UnsupportedOperationException("shutdownInput() not supported on SSL Sockets"); - } - - @Override - public void shutdownOutput() throws IOException { - throw new UnsupportedOperationException("shutdownOutput() not supported on SSL Sockets"); - } - - @Override - public boolean isInputShutdown() { - return socket.isInputShutdown(); - } - - @Override - public boolean isOutputShutdown() { - return socket.isOutputShutdown(); - } - - @Override - public void setEnableSessionCreation(boolean flag) { - engine.setEnableSessionCreation(flag); - } - - @Override - public boolean getEnableSessionCreation() { - return engine.getEnableSessionCreation(); - } - - @Override - public synchronized InputStream getInputStream() throws IOException { - if(inputStream == null) { - inputStream = new SSLEngineInputStream(manager); - } - return inputStream; - } - - @Override - public OutputStream getOutputStream() throws IOException { - if(outputStream == null) { - outputStream = new SSLEngineOutputStream(manager); - } - return outputStream; - } - - public void handshakeCompleted() { - if(listenerList.isEmpty()) { - return; - } - final HandshakeCompletedEvent event = new HandshakeCompletedEvent(this, engine.getSession()); - for(HandshakeCompletedListener listener: listenerList) { - listener.handshakeCompleted(event); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/socks/Socks4Request.java b/orchid/src/com/subgraph/orchid/socks/Socks4Request.java deleted file mode 100644 index ef79b6e0..00000000 --- a/orchid/src/com/subgraph/orchid/socks/Socks4Request.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.subgraph.orchid.socks; - -import java.net.Socket; - -import com.subgraph.orchid.TorConfig; - -public class Socks4Request extends SocksRequest { - final static int SOCKS_COMMAND_CONNECT = 1; - final static int SOCKS_COMMAND_RESOLV = 0xF0; - private final static int SOCKS_STATUS_SUCCESS = 0x5a; - private final static int SOCKS_STATUS_FAILURE = 0x5b; - private int command; - - Socks4Request(TorConfig config, Socket socket) { - super(config, socket); - } - - public boolean isConnectRequest() { - return command == SOCKS_COMMAND_CONNECT; - } - - public int getCommandCode() { - return command; - } - - public void sendConnectionRefused() throws SocksRequestException { - sendError(false); - } - - public void sendError(boolean isUnsupportedCommand) throws SocksRequestException { - sendResponse(SOCKS_STATUS_FAILURE); - } - - public void sendSuccess() throws SocksRequestException { - sendResponse(SOCKS_STATUS_SUCCESS); - } - - public void readRequest() throws SocksRequestException { - command = readByte(); - setPortData(readPortData()); - byte[] ipv4Data = readIPv4AddressData(); - readNullTerminatedString(); // Username - if(isVersion4aHostname(ipv4Data)) - setHostname(readNullTerminatedString()); - else - setIPv4AddressData(ipv4Data); - } - - private boolean isVersion4aHostname(byte[] data) { - /* - * For version 4A, if the client cannot resolve the destination host's - * domain name to find its IP address, it should set the first three bytes - * of DSTIP to NULL and the last byte to a non-zero value. (This corresponds - * to IP address 0.0.0.x, with x nonzero. - */ - if(data.length != 4) - return false; - for(int i = 0; i < 3; i++) - if(data[i] != 0) - return false; - return data[3] != 0; - } - - private void sendResponse(int code) throws SocksRequestException { - final byte[] responseBuffer = new byte[8]; - responseBuffer[0] = 0; - responseBuffer[1] = (byte) code; - socketWrite(responseBuffer); - } -} diff --git a/orchid/src/com/subgraph/orchid/socks/Socks5Request.java b/orchid/src/com/subgraph/orchid/socks/Socks5Request.java deleted file mode 100644 index f7f0a225..00000000 --- a/orchid/src/com/subgraph/orchid/socks/Socks5Request.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.subgraph.orchid.socks; - -import java.net.Socket; - -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.TorException; - -public class Socks5Request extends SocksRequest { - final static int SOCKS5_VERSION = 5; - final static int SOCKS5_AUTH_NONE = 0; - final static int SOCKS5_COMMAND_CONNECT = 1; - final static int SOCKS5_COMMAND_RESOLV = 0xF0; - final static int SOCKS5_COMMAND_RESOLV_PTR = 0xF1; - final static int SOCKS5_ADDRESS_IPV4 = 1; - final static int SOCKS5_ADDRESS_HOSTNAME = 3; - final static int SOCKS5_ADDRESS_IPV6 = 4; - final static int SOCKS5_STATUS_SUCCESS = 0; - final static int SOCKS5_STATUS_FAILURE = 1; - final static int SOCKS5_STATUS_CONNECTION_REFUSED = 5; - final static int SOCKS5_STATUS_COMMAND_NOT_SUPPORTED = 7; - - private int command; - private int addressType; - private byte[] addressBytes = new byte[0]; - private byte[] portBytes = new byte[0]; - - Socks5Request(TorConfig config, Socket socket) { - super(config, socket); - } - - public boolean isConnectRequest() { - return command == SOCKS5_COMMAND_CONNECT; - } - - public int getCommandCode() { - return command; - } - - private String addressBytesToHostname() { - if(addressType != SOCKS5_ADDRESS_HOSTNAME) - throw new TorException("SOCKS 4 request is not a hostname request"); - final StringBuilder sb = new StringBuilder(); - for(int i = 1; i < addressBytes.length; i++) { - char c = (char) (addressBytes[i] & 0xFF); - sb.append(c); - } - return sb.toString(); - } - - public void readRequest() throws SocksRequestException { - if(!processAuthentication()) { - throw new SocksRequestException("Failed to negotiate authentication"); - } - if(readByte() != SOCKS5_VERSION) - throw new SocksRequestException(); - - command = readByte(); - readByte(); // Reserved - addressType = readByte(); - addressBytes = readAddressBytes(); - portBytes = readPortData(); - if(addressType == SOCKS5_ADDRESS_IPV4) - setIPv4AddressData(addressBytes); - else if(addressType == SOCKS5_ADDRESS_HOSTNAME) - setHostname(addressBytesToHostname()); - else - throw new SocksRequestException(); - setPortData(portBytes); - } - - public void sendConnectionRefused() throws SocksRequestException { - sendResponse(SOCKS5_STATUS_CONNECTION_REFUSED); - } - - public void sendError(boolean isUnsupportedCommand) throws SocksRequestException { - if(isUnsupportedCommand) { - sendResponse(SOCKS5_STATUS_COMMAND_NOT_SUPPORTED); - } else { - sendResponse(SOCKS5_STATUS_FAILURE); - } - } - - public void sendSuccess() throws SocksRequestException { - sendResponse(SOCKS5_STATUS_SUCCESS); - } - - private void sendResponse(int status) throws SocksRequestException { - final int responseLength = 4 + addressBytes.length + portBytes.length; - final byte[] response = new byte[responseLength]; - response[0] = SOCKS5_VERSION; - response[1] = (byte) status; - response[2] = 0; - response[3] = (byte) addressType; - System.arraycopy(addressBytes, 0, response, 4, addressBytes.length); - System.arraycopy(portBytes, 0, response, 4 + addressBytes.length, portBytes.length); - socketWrite(response); - } - - private boolean processAuthentication() throws SocksRequestException { - final int nmethods = readByte(); - boolean foundAuthNone = false; - for(int i = 0; i < nmethods; i++) { - final int meth = readByte(); - if(meth == SOCKS5_AUTH_NONE) - foundAuthNone = true; - } - - if(foundAuthNone) { - sendAuthenticationResponse(SOCKS5_AUTH_NONE); - return true; - } else { - sendAuthenticationResponse(0xFF); - return false; - } - } - - - private void sendAuthenticationResponse(int method) throws SocksRequestException { - final byte[] response = new byte[2]; - response[0] = SOCKS5_VERSION; - response[1] = (byte) method; - socketWrite(response); - } - - private byte[] readAddressBytes() throws SocksRequestException { - switch(addressType) { - case SOCKS5_ADDRESS_IPV4: - return readIPv4AddressData(); - case SOCKS5_ADDRESS_IPV6: - return readIPv6AddressData(); - case SOCKS5_ADDRESS_HOSTNAME: - return readHostnameData(); - default: - throw new SocksRequestException(); - } - } - - private byte[] readHostnameData() throws SocksRequestException { - final int length = readByte(); - final byte[] addrData = new byte[length + 1]; - addrData[0] = (byte) length; - readAll(addrData, 1, length); - return addrData; - } -} diff --git a/orchid/src/com/subgraph/orchid/socks/SocksClientTask.java b/orchid/src/com/subgraph/orchid/socks/SocksClientTask.java deleted file mode 100644 index b7cf781d..00000000 --- a/orchid/src/com/subgraph/orchid/socks/SocksClientTask.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.subgraph.orchid.socks; - -import java.io.IOException; -import java.net.Socket; -import java.util.concurrent.TimeoutException; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.subgraph.orchid.CircuitManager; -import com.subgraph.orchid.OpenFailedException; -import com.subgraph.orchid.Stream; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.TorException; - -public class SocksClientTask implements Runnable { - private final static Logger logger = Logger.getLogger(SocksClientTask.class.getName()); - - private final TorConfig config; - private final Socket socket; - private final CircuitManager circuitManager; - - SocksClientTask(TorConfig config, Socket socket, CircuitManager circuitManager) { - this.config = config; - this.socket = socket; - this.circuitManager = circuitManager; - } - - public void run() { - final int version = readByte(); - dispatchRequest(version); - closeSocket(); - } - - private int readByte() { - try { - return socket.getInputStream().read(); - } catch (IOException e) { - logger.warning("IO error reading version byte: "+ e.getMessage()); - return -1; - } - } - - private void dispatchRequest(int versionByte) { - switch(versionByte) { - case 'H': - case 'G': - case 'P': - sendHttpPage(); - break; - case 4: - processRequest(new Socks4Request(config, socket)); - break; - case 5: - processRequest(new Socks5Request(config, socket)); - break; - default: - // fall through, do nothing - break; - } - } - - private void processRequest(SocksRequest request) { - try { - request.readRequest(); - if(!request.isConnectRequest()) { - logger.warning("Non connect command ("+ request.getCommandCode() + ")"); - request.sendError(true); - return; - } - - try { - final Stream stream = openConnectStream(request); - logger.fine("SOCKS CONNECT to "+ request.getTarget()+ " completed"); - request.sendSuccess(); - runOpenConnection(stream); - } catch (InterruptedException e) { - logger.info("SOCKS CONNECT to "+ request.getTarget() + " was thread interrupted"); - Thread.currentThread().interrupt(); - request.sendError(false); - } catch (TimeoutException e) { - logger.info("SOCKS CONNECT to "+ request.getTarget() + " timed out"); - request.sendError(false); - } catch (OpenFailedException e) { - logger.info("SOCKS CONNECT to "+ request.getTarget() + " failed: "+ e.getMessage()); - request.sendConnectionRefused(); - } - } catch (SocksRequestException e) { - logger.log(Level.WARNING, "Failure reading SOCKS request: "+ e.getMessage()); - try { - request.sendError(false); - socket.close(); - } catch (Exception ignore) { } - } - } - - - private void runOpenConnection(Stream stream) { - SocksStreamConnection.runConnection(socket, stream); - } - - private Stream openConnectStream(SocksRequest request) throws InterruptedException, TimeoutException, OpenFailedException { - if(request.hasHostname()) { - logger.fine("SOCKS CONNECT request to "+ request.getHostname() +":"+ request.getPort()); - return circuitManager.openExitStreamTo(request.getHostname(), request.getPort()); - } else { - logger.fine("SOCKS CONNECT request to "+ request.getAddress() +":"+ request.getPort()); - return circuitManager.openExitStreamTo(request.getAddress(), request.getPort()); - } - } - - private void sendHttpPage() { - throw new TorException("Returning HTTP page not implemented"); - } - - private void closeSocket() { - try { - socket.close(); - } catch (IOException e) { - logger.warning("Error closing SOCKS socket: "+ e.getMessage()); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/socks/SocksPortListenerImpl.java b/orchid/src/com/subgraph/orchid/socks/SocksPortListenerImpl.java deleted file mode 100644 index 76abfb3e..00000000 --- a/orchid/src/com/subgraph/orchid/socks/SocksPortListenerImpl.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.subgraph.orchid.socks; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Logger; - -import com.subgraph.orchid.CircuitManager; -import com.subgraph.orchid.SocksPortListener; -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.TorException; - -public class SocksPortListenerImpl implements SocksPortListener { - private final static Logger logger = Logger.getLogger(SocksPortListenerImpl.class.getName()); - private final Set listeningPorts = new HashSet(); - private final Map acceptThreads = new HashMap(); - private final TorConfig config; - private final CircuitManager circuitManager; - private final ExecutorService executor; - private boolean isStopped; - - public SocksPortListenerImpl(TorConfig config, CircuitManager circuitManager) { - this.config = config; - this.circuitManager = circuitManager; - executor = Executors.newCachedThreadPool(); - } - - public void addListeningPort(int port) { - if(port <= 0 || port > 65535) { - throw new TorException("Illegal listening port: "+ port); - } - - synchronized(listeningPorts) { - if(isStopped) { - throw new IllegalStateException("Cannot add listening port because Socks proxy has been stopped"); - } - if(listeningPorts.contains(port)) - return; - listeningPorts.add(port); - try { - startListening(port); - logger.fine("Listening for SOCKS connections on port "+ port); - } catch (IOException e) { - listeningPorts.remove(port); - throw new TorException("Failed to listen on port "+ port +" : "+ e.getMessage()); - } - } - - } - - public void stop() { - synchronized (listeningPorts) { - for(AcceptTask t: acceptThreads.values()) { - t.stop(); - } - executor.shutdownNow(); - isStopped = true; - } - } - - private void startListening(int port) throws IOException { - final AcceptTask task = new AcceptTask(port); - acceptThreads.put(port, task); - executor.execute(task); - } - - private Runnable newClientSocket(final Socket s) { - return new SocksClientTask(config, s, circuitManager); - } - - private class AcceptTask implements Runnable { - private final ServerSocket socket; - private final int port; - private volatile boolean stopped; - - AcceptTask(int port) throws IOException { - this.socket = new ServerSocket(port); - this.port = port; - } - - void stop() { - stopped = true; - try { - socket.close(); - } catch (IOException e) { } - } - - public void run() { - try { - runAcceptLoop(); - } catch (IOException e) { - if(!stopped) { - logger.warning("System error accepting SOCKS socket connections: "+ e.getMessage()); - } - } finally { - synchronized (listeningPorts) { - listeningPorts.remove(port); - acceptThreads.remove(port); - } - } - } - - private void runAcceptLoop() throws IOException { - while(!Thread.interrupted() && !stopped) { - final Socket s = socket.accept(); - executor.execute(newClientSocket(s)); - } - } - } -} diff --git a/orchid/src/com/subgraph/orchid/socks/SocksRequest.java b/orchid/src/com/subgraph/orchid/socks/SocksRequest.java deleted file mode 100644 index a28497d8..00000000 --- a/orchid/src/com/subgraph/orchid/socks/SocksRequest.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.subgraph.orchid.socks; - -import java.io.IOException; -import java.net.Socket; -import java.util.logging.Logger; - -import com.subgraph.orchid.TorConfig; -import com.subgraph.orchid.data.IPv4Address; - -public abstract class SocksRequest { - private final static Logger logger = Logger.getLogger(SocksRequest.class.getName()); - - private final TorConfig config; - private final Socket socket; - - private byte[] addressData; - private IPv4Address address; - private String hostname; - private int port; - - private long lastWarningTimestamp = 0; - - protected SocksRequest(TorConfig config, Socket socket) { - this.config = config; - this.socket = socket; - } - - abstract public void readRequest() throws SocksRequestException; - abstract public int getCommandCode(); - abstract public boolean isConnectRequest(); - abstract void sendError(boolean isUnsupportedCommand) throws SocksRequestException; - abstract void sendSuccess() throws SocksRequestException; - abstract void sendConnectionRefused() throws SocksRequestException; - - public int getPort() { - return port; - } - - public IPv4Address getAddress() { - return address; - } - - public boolean hasHostname() { - return hostname != null; - } - - public String getHostname() { - return hostname; - } - - public String getTarget() { - if(config.getSafeLogging()) { - return "[scrubbed]:"+ port; - } - if(hostname != null) { - return hostname + ":" + port; - } else { - return address + ":" + port; - } - } - - protected void setPortData(byte[] data) throws SocksRequestException { - if(data.length != 2) - throw new SocksRequestException(); - port = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); - } - - protected void setIPv4AddressData(byte[] data) throws SocksRequestException { - logUnsafeSOCKS(); - - if(data.length != 4) - throw new SocksRequestException(); - addressData = data; - - int addressValue = 0; - for(byte b: addressData) { - addressValue <<= 8; - addressValue |= (b & 0xFF); - } - address = new IPv4Address(addressValue); - } - - private boolean testRateLimit() { - final long now = System.currentTimeMillis(); - final long diff = now - lastWarningTimestamp; - lastWarningTimestamp = now; - return diff > 5000; - } - - private void logUnsafeSOCKS() throws SocksRequestException { - if((config.getWarnUnsafeSocks() || config.getSafeSocks()) && testRateLimit()) { - logger.warning("Your application is giving Orchid only "+ - "an IP address. Applications that do DNS "+ - "resolves themselves may leak information. "+ - "Consider using Socks4a (e.g. via privoxy or socat) "+ - "instead. For more information please see "+ - "https://wiki.torproject.org/TheOnionRouter/TorFAQ#SOCKSAndDNS"); - } - if(config.getSafeSocks()) { - throw new SocksRequestException("Rejecting unsafe SOCKS request"); - } - } - - protected void setHostname(String name) { - hostname = name; - } - - protected byte[] readPortData() throws SocksRequestException { - final byte[] data = new byte[2]; - readAll(data, 0, 2); - return data; - } - - protected byte[] readIPv4AddressData() throws SocksRequestException { - final byte[] data = new byte[4]; - readAll(data); - return data; - } - - protected byte[] readIPv6AddressData() throws SocksRequestException { - final byte[] data = new byte[16]; - readAll(data); - return data; - } - - protected String readNullTerminatedString() throws SocksRequestException { - try { - final StringBuilder sb = new StringBuilder(); - while(true) { - final int c = socket.getInputStream().read(); - if(c == -1) - throw new SocksRequestException(); - if(c == 0) - return sb.toString(); - char ch = (char) c; - sb.append(ch); - } - } catch (IOException e) { - throw new SocksRequestException(e); - } - } - - protected int readByte() throws SocksRequestException { - try { - final int n = socket.getInputStream().read(); - if(n == -1) - throw new SocksRequestException(); - return n; - } catch (IOException e) { - throw new SocksRequestException(e); - } - } - - protected void readAll(byte[] buffer) throws SocksRequestException { - readAll(buffer, 0, buffer.length); - } - - protected void readAll(byte[] buffer, int offset, int length) throws SocksRequestException { - try { - while(length > 0) { - int n = socket.getInputStream().read(buffer, offset, length); - if(n == -1) - throw new SocksRequestException(); - offset += n; - length -= n; - } - } catch (IOException e) { - throw new SocksRequestException(e); - } - } - - protected void socketWrite(byte[] buffer) throws SocksRequestException { - try { - socket.getOutputStream().write(buffer); - } catch(IOException e) { - throw new SocksRequestException(e); - } - } -} diff --git a/orchid/src/com/subgraph/orchid/socks/SocksRequestException.java b/orchid/src/com/subgraph/orchid/socks/SocksRequestException.java deleted file mode 100644 index 789f6b1c..00000000 --- a/orchid/src/com/subgraph/orchid/socks/SocksRequestException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.subgraph.orchid.socks; - -public class SocksRequestException extends Exception { - - private static final long serialVersionUID = 844055056337565049L; - - SocksRequestException() {} - SocksRequestException(String msg) { - super(msg); - } - - SocksRequestException(Throwable ex) { - super(ex); - } -} diff --git a/orchid/src/com/subgraph/orchid/socks/SocksStreamConnection.java b/orchid/src/com/subgraph/orchid/socks/SocksStreamConnection.java deleted file mode 100644 index 70c9ea1e..00000000 --- a/orchid/src/com/subgraph/orchid/socks/SocksStreamConnection.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.subgraph.orchid.socks; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.util.logging.Logger; - -import com.subgraph.orchid.Stream; - -public class SocksStreamConnection { - private final static Logger logger = Logger.getLogger(SocksStreamConnection.class.getName()); - - public static void runConnection(Socket socket, Stream stream) { - SocksStreamConnection ssc = new SocksStreamConnection(socket, stream); - ssc.run(); - } - private final static int TRANSFER_BUFFER_SIZE = 4096; - private final Stream stream; - private final InputStream torInputStream; - private final OutputStream torOutputStream; - private final Socket socket; - private final Thread incomingThread; - private final Thread outgoingThread; - private final Object lock = new Object(); - private volatile boolean outgoingClosed; - private volatile boolean incomingClosed; - - private SocksStreamConnection(Socket socket, Stream stream) { - this.socket = socket; - this.stream = stream; - torInputStream = stream.getInputStream(); - torOutputStream = stream.getOutputStream(); - - incomingThread = createIncomingThread(); - outgoingThread = createOutgoingThread(); - } - - private void run() { - incomingThread.start(); - outgoingThread.start(); - synchronized(lock) { - while(!(outgoingClosed && incomingClosed)) { - try { - lock.wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - } - - try { - socket.close(); - } catch (IOException e) { - logger.warning("IOException on SOCKS socket close(): "+ e.getMessage()); - } - closeStream(torInputStream); - closeStream(torOutputStream); - } - } - - private Thread createIncomingThread() { - return new Thread(new Runnable() { public void run() { - try { - incomingTransferLoop(); - } catch (IOException e) { - logger.fine("System error on incoming stream IO "+ stream +" : "+ e.getMessage()); - } finally { - synchronized(lock) { - incomingClosed = true; - lock.notifyAll(); - } - } - }}); - } - - private Thread createOutgoingThread() { - return new Thread(new Runnable() { public void run() { - try { - outgoingTransferLoop(); - } catch (IOException e) { - logger.fine("System error on outgoing stream IO "+ stream +" : "+ e.getMessage()); - } finally { - synchronized(lock) { - outgoingClosed = true; - lock.notifyAll(); - } - } - }}); - } - - private void incomingTransferLoop() throws IOException { - final byte[] incomingBuffer = new byte[TRANSFER_BUFFER_SIZE]; - while(true) { - final int n = torInputStream.read(incomingBuffer); - if(n == -1) { - logger.fine("EOF on TOR input stream "+ stream); - socket.shutdownOutput(); - return; - } else if(n > 0) { - logger.fine("Transferring "+ n +" bytes from "+ stream +" to SOCKS socket"); - if(!socket.isOutputShutdown()) { - socket.getOutputStream().write(incomingBuffer, 0, n); - socket.getOutputStream().flush(); - } else { - closeStream(torInputStream); - return; - } - } - } - } - - private void outgoingTransferLoop() throws IOException { - final byte[] outgoingBuffer = new byte[TRANSFER_BUFFER_SIZE]; - while(true) { - stream.waitForSendWindow(); - final int n = socket.getInputStream().read(outgoingBuffer); - if(n == -1) { - torOutputStream.close(); - logger.fine("EOF on SOCKS socket connected to "+ stream); - return; - } else if(n > 0) { - logger.fine("Transferring "+ n +" bytes from SOCKS socket to "+ stream); - torOutputStream.write(outgoingBuffer, 0, n); - torOutputStream.flush(); - } - } - } - - private void closeStream(Closeable c) { - try { - c.close(); - } catch (IOException e) { - logger.warning("Close failed on "+ c + " : "+ e.getMessage()); - } - } -} diff --git a/orchid/test/com/subgraph/orchid/TorConfigTest.java b/orchid/test/com/subgraph/orchid/TorConfigTest.java deleted file mode 100644 index e108fd9c..00000000 --- a/orchid/test/com/subgraph/orchid/TorConfigTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.subgraph.orchid; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.junit.Before; -import org.junit.Test; - -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; -import com.subgraph.orchid.encoders.Hex; - -public class TorConfigTest { - - private TorConfig config; - - @Before - public void setup() { - config = Tor.createConfig(); - } - - - @Test - public void testCircuitBuildTimeout() { - final long timeout = config.getCircuitBuildTimeout(); - assertEquals(TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS), timeout); - config.setCircuitBuildTimeout(2, TimeUnit.MINUTES); - assertTrue(config.getCircuitBuildTimeout() > timeout); - } - - @Test - public void testDataDirectory() { - final File dd = config.getDataDirectory(); - assertTrue(dd.getPath().charAt(0) != '~'); - final String testPath = "/foo/dir"; - config.setDataDirectory(new File(testPath)); - assertEquals(new File(testPath), config.getDataDirectory()); - } - - @Test - public void testMaxCircuitsPending() { - assertEquals(32, config.getMaxClientCircuitsPending()); - config.setMaxClientCircuitsPending(23); - assertEquals(23, config.getMaxClientCircuitsPending()); - } - - @Test - public void testEnforceDistinctSubnets() { - assertEquals(true, config.getEnforceDistinctSubnets()); - config.setEnforceDistinctSubnets(false); - assertEquals(false, config.getEnforceDistinctSubnets()); - } - - @Test - public void testCircuitStreamTimeout() { - assertEquals(0, config.getCircuitStreamTimeout()); - config.setCircuitStreamTimeout(30, TimeUnit.SECONDS); - assertEquals(30 * 1000, config.getCircuitStreamTimeout()); - } - - @Test - public void testHidServAuth() { - final String address = "3t43tfluce4qcxbo"; - final String onion = address + ".onion"; - - final String hex = "022b99d1d272285c80f7214bd6c07c27"; - final String descriptor = "AiuZ0dJyKFyA9yFL1sB8Jw"; - - assertNull(config.getHidServAuth(onion)); - - config.addHidServAuth(onion, descriptor); - - HSDescriptorCookie cookie = config.getHidServAuth(onion); - assertNotNull(cookie); - assertEquals(hex, new String(Hex.encode(cookie.getValue()))); - assertSame(cookie, config.getHidServAuth(address)); - } - - @Test - public void testAutoBool() { - assertEquals(TorConfig.AutoBoolValue.AUTO, config.getUseNTorHandshake()); - config.setUseNTorHandshake(TorConfig.AutoBoolValue.TRUE); - assertEquals(TorConfig.AutoBoolValue.TRUE, config.getUseNTorHandshake()); - config.setUseNTorHandshake(TorConfig.AutoBoolValue.AUTO); - assertEquals(TorConfig.AutoBoolValue.AUTO, config.getUseNTorHandshake()); - } - - @Test - public void testBridges() { - final IPv4Address a1 = IPv4Address.createFromString("1.2.3.4"); - final IPv4Address a2 = IPv4Address.createFromString("4.4.4.4"); - final HexDigest fp = HexDigest.createFromString("0EA20CAA3CE696E561BC08B15E00106700E8F682"); - config.addBridge(a1, 88); - config.addBridge(a2, 101, fp); - List bs = config.getBridges(); - assertEquals(2, bs.size()); - final TorConfigBridgeLine b1 = bs.get(0); - final TorConfigBridgeLine b2 = bs.get(1); - - assertEquals(a1, b1.getAddress()); - assertEquals(a2, b2.getAddress()); - assertEquals(88, b1.getPort()); - assertEquals(101, b2.getPort()); - assertNull(b1.getFingerprint()); - assertSame(b2.getFingerprint(), fp); - } -} diff --git a/orchid/test/com/subgraph/orchid/circuits/TorInputStreamTest.java b/orchid/test/com/subgraph/orchid/circuits/TorInputStreamTest.java deleted file mode 100644 index ff30e457..00000000 --- a/orchid/test/com/subgraph/orchid/circuits/TorInputStreamTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.subgraph.orchid.circuits; - -import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.expectLastCall; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; -import static org.junit.Assert.*; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Arrays; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import com.subgraph.orchid.RelayCell; -import com.subgraph.orchid.Stream; -import com.subgraph.orchid.circuits.TorInputStream; - -public class TorInputStreamTest { - - private TorInputStream inputStream; - private Stream mockStream; - - @Before - public void before() { - mockStream = createMock("mockStream", Stream.class); - mockStream.close(); - replay(mockStream); - inputStream = new TorInputStream(mockStream); - } - - @After - public void after() { - inputStream.close(); - verify(mockStream); - } - - private static RelayCell createDataCell(byte[] data) { - final RelayCell cell = createMock("dataCell", RelayCell.class); - expect(cell.cellBytesRemaining()).andReturn(data.length); - expectLastCall().times(2); - expect(cell.getRelayCommand()).andReturn(RelayCell.RELAY_DATA); - expect(cell.getPayloadBuffer()).andReturn(ByteBuffer.wrap(data)); - replay(cell); - return cell; - } - - private static RelayCell createEndCell() { - final RelayCell cell = createMock("endCell", RelayCell.class); - expect(cell.getRelayCommand()).andReturn(RelayCell.RELAY_END); - replay(cell); - return cell; - } - - private void sendData(int... data) { - byte[] bytes = new byte[data.length]; - for(int i = 0; i < data.length; i++) { - bytes[i] = (byte) data[i]; - } - inputStream.addInputCell(createDataCell(bytes)); - } - - private void sendEnd() { - inputStream.addEndCell(createEndCell()); - } - - @Test - public void testAvailable() throws IOException { - assertEquals(0, inputStream.available()); - sendData(1,2,3); - assertEquals(3, inputStream.available()); - assertEquals(1, inputStream.read()); - assertEquals(2, inputStream.available()); - sendData(4,5); - assertEquals(4, inputStream.available()); - } - - @Test(timeout=100) - public void testRead() throws IOException { - sendData(1,2,3); - sendData(4); - sendData(5); - assertEquals(1, inputStream.read()); - assertEquals(2, inputStream.read()); - sendEnd(); - assertEquals(3, inputStream.read()); - assertEquals(4, inputStream.read()); - assertEquals(5, inputStream.read()); - assertEquals(-1, inputStream.read()); - } - - - private void setupTestClose() throws IOException { - sendData(1,2,3,4,5,6); - sendEnd(); - - assertEquals(1, inputStream.read()); - assertEquals(2, inputStream.read()); - - inputStream.close(); - } - - @Test(expected=IOException.class, timeout=100) - public void testClose1() throws IOException { - setupTestClose(); - /* throws IOException("Input stream closed") */ - inputStream.read(); - } - - @Test(expected=IOException.class, timeout=100) - public void testClose2() throws IOException { - setupTestClose(); - /* throws IOException("Input stream closed") */ - inputStream.read(new byte[2]); - } - - @Test(timeout=100) - public void testReadBuffer() throws IOException { - final byte[] buffer = new byte[3]; - - sendData(1,2,3); - sendData(4,5,6); - - - /* read two bytes at offset 1 */ - assertEquals(2, inputStream.read(buffer, 1, 2)); - assertArrayEquals(new byte[] {0, 1, 2}, buffer); - - /* read entire buffer (3 bytes) */ - assertEquals(3, inputStream.read(buffer)); - assertArrayEquals(new byte[] {3, 4, 5 }, buffer); - - /* reset buffer to {0,0,0}, read entire buffer */ - Arrays.fill(buffer, (byte)0); - assertEquals(1, inputStream.read(buffer)); - assertArrayEquals(new byte[] { 6, 0, 0 }, buffer); - - sendEnd(); - /* read entire buffer at EOF */ - assertEquals(-1, inputStream.read(buffer)); - } - - private boolean doesNullBufferThrowException() throws IOException { - try { - inputStream.read(null); - return false; - } catch(NullPointerException e) { - return true; - } - } - - private boolean throwsOOBException(byte[] b, int off, int len) throws IOException { - try { - inputStream.read(b, off, len); - return false; - } catch (IndexOutOfBoundsException e) { - return true; - } - } - - private void testOOB(String message, int bufferLength, int off, int len) throws IOException { - final byte[] buffer = new byte[bufferLength]; - assertTrue(message, throwsOOBException(buffer, off, len)); - } - - @Test(timeout=100) - public void testBadReadArguments() throws IOException { - final byte[] buffer = new byte[16]; - sendData(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20); - sendEnd(); - - assertTrue("Null buffer must throw NPE", doesNullBufferThrowException()); - assertFalse("(offset + len) == b.length must not throw OOB", throwsOOBException(buffer, 8, 8)); - - testOOB("Negative offset must throw OOB", 16, -2, 4); - testOOB("Negative length must throw OOB", 16, 0, -10); - testOOB("off >= b.length must throw OOB", 16, 16, 10); - testOOB("(off + len) > b.length must throw OOB", 16, 8, 9); - testOOB("(off + len) < 0 must throw OOB", 16, Integer.MAX_VALUE, 10); - } -} diff --git a/orchid/test/com/subgraph/orchid/circuits/hs/HSDescriptorParserTest.java b/orchid/test/com/subgraph/orchid/circuits/hs/HSDescriptorParserTest.java deleted file mode 100644 index 1ec51d14..00000000 --- a/orchid/test/com/subgraph/orchid/circuits/hs/HSDescriptorParserTest.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.subgraph.orchid.circuits.hs; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.nio.ByteBuffer; -import java.util.List; - -import org.junit.Test; - -import com.subgraph.orchid.Tor; -import com.subgraph.orchid.directory.DocumentFieldParserImpl; -import com.subgraph.orchid.directory.parsing.DocumentFieldParser; -import com.subgraph.orchid.directory.parsing.DocumentParsingResult; - -public class HSDescriptorParserTest { - private final static String TEST_DESCRIPTOR = - "rendezvous-service-descriptor apue4vh2fduecfztrrwczoo7cprlki4s\n"+ - "version 2\n"+ - "permanent-key\n"+ - "-----BEGIN RSA PUBLIC KEY-----\n"+ - "MIGJAoGBAMNTmy7L/isS+XTkCf1B1aik0ApE9sxcNpLwNR2JOZyy5puEGPuVY1FW\n"+ - "nw+CnMmTWXchTTRfboFmIv4F3i8ZTLHdWJ7wqRGyc0aabvkDZBSRWVHby3oDf/uQ\n"+ - "abtrJxXzYjy/dP29v5bLkb7a2zaAeP1ojX8ZwpxgJ9BCI+2fvBArAgMBAAE=\n"+ - "-----END RSA PUBLIC KEY-----\n"+ - "secret-id-part xaib3au35yqklp5txmncxbi2uic6jqor\n"+ - "publication-time 2013-07-07 23:20:40\n"+ - "protocol-versions 2,3\n"+ - "introduction-points\n"+ - "-----BEGIN MESSAGE-----\n"+ - "aW50cm9kdWN0aW9uLXBvaW50IGpla2tubHY0dWh2cGNoajVpcnZtd3I0Ym5rb2Ry\n"+ - "N3ZkCmlwLWFkZHJlc3MgMTczLjI1NS4yNDkuMjIyCm9uaW9uLXBvcnQgNDQzCm9u\n"+ - "aW9uLWtleQotLS0tLUJFR0lOIFJTQSBQVUJMSUMgS0VZLS0tLS0KTUlHSkFvR0JB\n"+ - "TG9OeXdIeW1QRGo2c2NvdUsvbGJZR01MRllGRGxDOVJyN2Jjc0MxQW12MWp0MjBH\n"+ - "WlBOdGFHMgorbjdDdHhMK3JWM3g5eFRQSDZBWUlDQmxycnA3TngzRlJQMWorQ3JI\n"+ - "WWk3WkNrTWhDUmg3NXNadmhIV01GT3liClM1QUUyWlhCMTA4cUVucGJnSFdrWmFX\n"+ - "SXdZZXdGZUZxdU5JV3ZjYVgxTU1lc3BONTJ2c0JBZ01CQUFFPQotLS0tLUVORCBS\n"+ - "U0EgUFVCTElDIEtFWS0tLS0tCnNlcnZpY2Uta2V5Ci0tLS0tQkVHSU4gUlNBIFBV\n"+ - "QkxJQyBLRVktLS0tLQpNSUdKQW9HQkFMZDJqYVk0a3oydVBlS05MRnBVMW80MUFV\n"+ - "UmpiQW42bWdzWGtFNm15TTFhcDczS09FUGFQaUFwCmpib1pZSFdCV29QVVZFUFhu\n"+ - "ZE9XcU92ZmFEVGJsbndGU1F1NU54VWVPVkNELzdOYnd6Y0l0c2ZkQ1RBMzVzcHIK\n"+ - "ZGFUK3ZwWmFRWGgxbWt0NlFMN1dKeHZLaXI2bDFuMitwdXFwZ201ZUJhSXRCOEt4\n"+ - "cnVLaEFnTUJBQUU9Ci0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0KaW50cm9k\n"+ - "dWN0aW9uLXBvaW50IDcyb3R1Mnl4ZXJoNGZocHAybjNpMnBwNmo3Z2ZrNWZzCmlw\n"+ - "LWFkZHJlc3MgMTk4LjI3LjY0LjI5Cm9uaW9uLXBvcnQgNDQzCm9uaW9uLWtleQot\n"+ - "LS0tLUJFR0lOIFJTQSBQVUJMSUMgS0VZLS0tLS0KTUlHSkFvR0JBT0pLc2UzQmdv\n"+ - "TzhKdytFMURHUXhVbTV6UGQwcjFscHl3U25IamFKb2ZIbitDaUdSTHRnS2JNNQpN\n"+ - "R01UUnRhNVZKWTRUNjFpUFdmN05Ma0FiVnZuSllMcXVHZjdScnh4MCtnNm5jdTVG\n"+ - "blJRMTQwOVkwVXRpNDFmCmVMeGI2YWJlMkorQTRLN2ZGdkMzVjVBSnhtZDIrV2xt\n"+ - "dFhQbGV4aHlYN21SWGhBVzZ1UXpBZ01CQUFFPQotLS0tLUVORCBSU0EgUFVCTElD\n"+ - "IEtFWS0tLS0tCnNlcnZpY2Uta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVkt\n"+ - "LS0tLQpNSUdKQW9HQkFOa0I3eTVaN2FhVUs3R3ZTUWdKVHl6aU43anhlNXlvcEpU\n"+ - "LzBIRURCSGN3cVBqMkdZMytTZ2VJCjRpUWFCRG1SL1V0Y3FuU1JLaGNyMFBSRFBy\n"+ - "T2wxa3lSRmhLWTdqNWttSGRiMko3aEZ2eER1emRTNE43RWdCVVQKbHEvaFdZa2po\n"+ - "QzVDck9uVTNIY1h5Q3RlU1p0bk5qRlkxVHJnSUx4Z1NwcXA0SU5ZZ1NpcEFnTUJB\n"+ - "QUU9Ci0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0KaW50cm9kdWN0aW9uLXBv\n"+ - "aW50IHNzNTR1amRlcWJ6a3RzbnBibzJwZXV5eDJpem1wbmZhCmlwLWFkZHJlc3Mg\n"+ - "MzcuMTU3LjE5NS40OApvbmlvbi1wb3J0IDkwMDEKb25pb24ta2V5Ci0tLS0tQkVH\n"+ - "SU4gUlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFKaUZhRE05Nm1acWQ0QTRj\n"+ - "L3lkallpQjRXbGx5b0J5NGt2WXhYZUNnRHA4VGpNVmFzcUFRQUYzCjE5UnJUL01v\n"+ - "TzE4SHVnWjMrUkticFptK2xLeHZlRkpIdGpmTUZQL0NEbDVFOUZ3VFcrdEVUdXMy\n"+ - "RmVYcHJrVGMKbDc3YjIzSkpYd0FtQ3lYMFgyQWNUVVBoVkg4YXdGZU43T0xkRnk0\n"+ - "ZzF3UjZhVzA1SFpIRkFnTUJBQUU9Ci0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0t\n"+ - "LS0Kc2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1J\n"+ - "R0pBb0dCQUtSTVVIUTdWaWxRZ1l6c3ZJVEJuZko1QXVSOUNZQWc5eGFUQzNmVWZ1\n"+ - "T21udkZlNHZzZTFJeWsKUkNRSzZHOTVPbGxOd3B5akU4WXRCSlIxUlZLUFBqcHNI\n"+ - "YUxJTmszVmROM3Z5NWxlL0VVQXY2c0dEUnRZODNDLwo3MFhGN1h6YjFGUVBNcm5a\n"+ - "d1o2N3A4Wkc2KzNub1JSUFZLbFJzQU9wa2YwMWJOb3ZpR09iQWdNQkFBRT0KLS0t\n"+ - "LS1FTkQgUlNBIFBVQkxJQyBLRVktLS0tLQoK\n"+ - "-----END MESSAGE-----\n"+ - "signature\n"+ - "-----BEGIN SIGNATURE-----\n"+ - "p1yxzPiIWpS0m+MTQW9LdJmgiOgaUTbwTz9GyoInPi5lC/WvX8/AnccsLoOUWjKs\n"+ - "3q8xV/8Gtz3qyigsWSggFuXyc3mRGM28tpdCNNkFovKAQgiZ0KjLky9BaQPEFOpr\n"+ - "v4Yo65ZbYvujPyc9xpqbtPNRf7LBe6GaqHvzP4kWqr8=\n"+ - "-----END SIGNATURE-----\n"; - - private final static String[] TEST_IPS = { - "introduction-point mjxsa2bywdvbft6kltuqfwwyru4ggo7o\n" - + "ip-address 86.59.119.82\n" - + "onion-port 443\n" - + "onion-key\n" - + "-----BEGIN RSA PUBLIC KEY-----\n" - + "MIGJAoGBAKIXLeVl4ut60oNnXeZtxJk7DMKFmklF/zeD+TqB1oW/QALt5wMVmO8u\n" - + "RBK7BfSxXG6IWQ0O5vBVSM25qss7+Nv/brS61VcB7IZKDaEd4n3f6Tlu4G8vxjNm\n" - + "zX0S1iYLqMOY1vcvuBIN2T43khkO5uyKjgF7EkAXLaH6hJgMSW9bAgMBAAE=\n" - + "-----END RSA PUBLIC KEY-----\n" - + "service-key\n" - + "-----BEGIN RSA PUBLIC KEY-----\n" - + "MIGJAoGBAL3FbGOkQ8cjlB70Fy1gv4178MwdNZrBPXwySubW206S0WILGePcXrZX\n" - + "4yVCNb4V4i4l9XisSAzyYS2D3CSAtYkinnSlafV3tCvt+QCKeGgtALT42oLt5UOn\n" - + "v494xZHVYKCiAwBScjqi7f+/BeclDPqBnm9af8p+cIkeCNrLt0WRAgMBAAE=\n" - + "-----END RSA PUBLIC KEY-----\n" - + "introduction-point 3ju2px3yec7ylznlwr2fyflabz5nq5kq\n" - + "ip-address 209.236.66.136\n" - + "onion-port 443\n" - + "onion-key\n" - + "-----BEGIN RSA PUBLIC KEY-----\n" - + "MIGJAoGBAKQLOS9Z5oKUE3EkYgXf5M086S/iJ6YzPB8wPsPRNCNgnGDFYXCLHtw8\n" - + "9mfm3jEG7/I5ab3+9hShMfls3uk0kIuOvD7b2VxNpsf5+z7RhZIpkCdby7etR3VL\n" - + "RlQO41EIujAfoVFKMk0WmmtpMp7FzPZc8pg3jAfvkwN/wkCeONcBAgMBAAE=\n" - + "-----END RSA PUBLIC KEY-----\n" - + "service-key\n" - + "-----BEGIN RSA PUBLIC KEY-----\n" - + "MIGJAoGBALGabFwhhBa5P8br8SScwAK7qJIJlirf95pKASeY4phORZaZFo9qOy7B\n" - + "qcIHQNGt3XIbW3MGMvOgIBklus97Bti8KDSTapWvmL4G3uF/XUoP8aPxUO56F+Gv\n" - + "RqDQEuf/sk6MbMLPLipG7xWLnn5wYzwsCxutcv2RJdA4mCDcQJYlAgMBAAE=\n" - + "-----END RSA PUBLIC KEY-----\n", - - "introduction-point f6b7o3f7hh7eudpc4cjocmew6kmnacsy\n" - + "ip-address 37.157.192.150\n" - + "onion-port 443\n" - + "onion-key\n" - + "-----BEGIN RSA PUBLIC KEY-----\n" - + "MIGJAoGBALoaGfBx/MWM4yVrYO4jxKiVfyTVtvgXlk523ifA2beO6yfeDVKR+4u0\n" - + "S/ABa9/kdQFXw4s9Ahz6vI0imdMPyUgYTXp+mP7pa45xp2uLi8kPgZLYzsJZc1Lm\n" - + "pyS5CA4Fzq7jblR3R7rGJyRBm1h8Pa8p9xE3RI6oRJnjAoCW+3LBAgMBAAE=\n" - + "-----END RSA PUBLIC KEY-----\n" - + "service-key\n" - + "-----BEGIN RSA PUBLIC KEY-----\n" - + "MIGJAoGBAK4GlIJ95emUzWG3zfWGemJbR7UZU+Ufysrgn8VZh2oH01jvTXj14qwD\n" - + "8PxI5R8CDlgfzCMMsUwp4tDZHd1IQZSyxRtonprq+j1ACDYm1hvYzwB1kjwlbp5g\n" - + "OYl2PtveH5zu2pkvCjknZxW8TCKry5jL8RqY23zLwe+AZWU9BZJdAgMBAAE=\n" - + "-----END RSA PUBLIC KEY-----\n"}; - - - @Test - public void testDescriptorParser() { - final HSDescriptorParser parser = createDescriptorParserFromString(TEST_DESCRIPTOR); - DocumentParsingResult result = parser.parse(); - assertTrue(result.isOkay()); - HSDescriptor descriptor = result.getDocument(); - List ips = descriptor.getIntroductionPoints(); - assertEquals(3, ips.size()); - for(IntroductionPoint ip: ips) { - assertTrue(ip.isValidDocument()); - } - } - - @Test - public void testIntroductionPointParser() { - final IntroductionPointParser parser = createIntroductionPointParserFromString(TEST_IPS[0]); - DocumentParsingResult result = parser.parse(); - assertTrue(result.isOkay()); - final List ips = result.getParsedDocuments(); - assertEquals(2, ips.size()); - for(IntroductionPoint ip: result.getParsedDocuments()) { - assertTrue(ip.isValidDocument()); - } - - } - - - private HSDescriptorParser createDescriptorParserFromString(String s) { - return new HSDescriptorParser(null, createFieldParser(s)); - } - - private IntroductionPointParser createIntroductionPointParserFromString(String s) { - return new IntroductionPointParser(createFieldParser(s)); - } - - private DocumentFieldParser createFieldParser(String s) { - ByteBuffer buffer = ByteBuffer.wrap(s.getBytes(Tor.getDefaultCharset())); - return new DocumentFieldParserImpl(buffer); - } -} diff --git a/orchid/test/com/subgraph/orchid/circuits/path/ConfigNodeFilterTest.java b/orchid/test/com/subgraph/orchid/circuits/path/ConfigNodeFilterTest.java deleted file mode 100644 index 5ccc589a..00000000 --- a/orchid/test/com/subgraph/orchid/circuits/path/ConfigNodeFilterTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.subgraph.orchid.circuits.path; - -import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.util.Arrays; -import java.util.List; - -import org.junit.Test; - -import com.subgraph.orchid.Router; -import com.subgraph.orchid.circuits.path.ConfigNodeFilter; -import com.subgraph.orchid.circuits.path.RouterFilter; -import com.subgraph.orchid.data.IPv4Address; - -public class ConfigNodeFilterTest { - - @Test - public void testIsAddressString() { - final List validStrings = Arrays.asList( - "1.2.3.4/16", - "0.0.0.0/1", - "255.0.255.0/16"); - - final List invalidStrings = Arrays.asList( - "1.2.3.256/16", - "1.2.3.4/61", - "1.2.3.4/0", - "1.2.3.4/22x", - "1.2.3.4/", - "1.2.3.4"); - - for(String s: validStrings) { - assertTrue(s, ConfigNodeFilter.isAddressString(s)); - } - for(String s: invalidStrings) { - assertFalse(s, ConfigNodeFilter.isAddressString(s)); - } - - } - - @Test - public void testIsCountryCode() { - final List validStrings = Arrays.asList("{CC}", "{xx}"); - final List invalidStrings = Arrays.asList("US", "{xxx}"); - for(String s: validStrings) { assertTrue(s, ConfigNodeFilter.isCountryCodeString(s)); } - for(String s: invalidStrings) { assertFalse(s, ConfigNodeFilter.isCountryCodeString(s)); } - } - - private Router createRouterMockWithAddress(String ip) { - final IPv4Address address = IPv4Address.createFromString(ip); - final Router router = createMock("mockRouter", Router.class); - expect(router.getAddress()).andReturn(address); - replay(router); - return router; - } - - @Test - public void testMaskFilter() { - final Router r1 = createRouterMockWithAddress("1.2.3.4"); - final Router r2 = createRouterMockWithAddress("1.7.3.4"); - final RouterFilter f = ConfigNodeFilter.createFilterFor("1.2.3.0/16"); - assertTrue(f.filter(r1)); - assertFalse(f.filter(r2)); - verify(r1, r2); - } - - -} diff --git a/orchid/test/com/subgraph/orchid/crypto/ASN1ParserTest.java b/orchid/test/com/subgraph/orchid/crypto/ASN1ParserTest.java deleted file mode 100644 index 38a5bae8..00000000 --- a/orchid/test/com/subgraph/orchid/crypto/ASN1ParserTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.subgraph.orchid.crypto; - -import static org.junit.Assert.*; - -import java.math.BigInteger; -import java.nio.ByteBuffer; - -import org.junit.Before; -import org.junit.Test; - -import com.subgraph.orchid.crypto.ASN1Parser.ASN1BitString; -import com.subgraph.orchid.crypto.ASN1Parser.ASN1Integer; -import com.subgraph.orchid.encoders.Hex; - -public class ASN1ParserTest { - - private ASN1Parser parser; - - @Before - public void setup() { - parser = new ASN1Parser(); - } - - ByteBuffer createBuffer(String hexData) { - final byte[] bs = Hex.decode(hexData); - return ByteBuffer.wrap(bs); - } - - - - @Test - public void testParseASN1Length() { - assertEquals(20, parser.parseASN1Length(createBuffer("14000000"))); - assertEquals(23, parser.parseASN1Length(createBuffer("81170000"))); - assertEquals(256, parser.parseASN1Length(createBuffer("82010000"))); - assertEquals(65535, parser.parseASN1Length(createBuffer("82FFFF00"))); - } - - @Test(expected=IllegalArgumentException.class) - public void testParseASN1LengthException() { - parser.parseASN1Length(createBuffer("80ACDCACDC")); - } - - @Test(expected=IllegalArgumentException.class) - public void testParseASN1LengthException2() { - parser.parseASN1Length(createBuffer("88ABCDABCD")); - } - - @Test - public void testParseASN1Integer() { - ASN1Integer asn1Integer = parser.parseASN1Integer(createBuffer("01020304")); - assertEquals(new BigInteger("01020304", 16), asn1Integer.getValue()); - } - - @Test - public void testParseASN1BitString() { - ASN1BitString bitString = parser.parseASN1BitString(createBuffer("0001020304")); - assertArrayEquals(new byte[] {1, 2, 3, 4}, bitString.getBytes()); - } - - @Test(expected=IllegalArgumentException.class) - public void testParseASN1BitStringException() { - parser.parseASN1BitString(createBuffer("01020304")); - } -} diff --git a/orchid/test/com/subgraph/orchid/crypto/RSAKeyEncoderTest.java b/orchid/test/com/subgraph/orchid/crypto/RSAKeyEncoderTest.java deleted file mode 100644 index ad09ba01..00000000 --- a/orchid/test/com/subgraph/orchid/crypto/RSAKeyEncoderTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.subgraph.orchid.crypto; - -import static org.junit.Assert.assertEquals; - -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.interfaces.RSAPublicKey; - -import org.junit.Before; -import org.junit.Test; - -public class RSAKeyEncoderTest { - - private RSAKeyEncoder encoder; - - final static String PEM_ENCODED_PUBKEY = - - "-----BEGIN RSA PUBLIC KEY-----\n"+ - "MIGJAoGBAMuf0v+d3HUNk5jbYJuZA+q30NlqFStNBmB/BA4y6h9DTpJ2ULhdy6I8\n"+ - "5tLq76TSTbGl2wiWpDjW73OkAfpbUyb+2fIFz4Ildth18ZA4dqNvnYNCnckO1p+B\n"+ - "x6e+8YoafedZhXsv1Z9RMl6WK6WGXpmgCSTTlLnXlrsJLrG/mW9dAgMBAAE=\n"+ - "-----END RSA PUBLIC KEY-----\n"; - - final static String MODULUS_STRING = - - "142989855534119842624281223201112183062179043858844190077277374317180853428"+ - "067855510754484639210124041049484315690046733530717435491654607786952431473"+ - "291786675652833142146809594339105386135143284841697658385761023403765912288"+ - "684940376854709443039663769117423844056151668935507268155717373127166136614"+ - "724923229"; - - final static BigInteger MODULUS = new BigInteger(MODULUS_STRING); - final static BigInteger EXPONENT = BigInteger.valueOf(65537); - - @Before - public void setup() { - encoder = new RSAKeyEncoder(); - } - - @Test - public void testParsePEMPublicKey() throws GeneralSecurityException { - final RSAPublicKey publicKey = encoder.parsePEMPublicKey(PEM_ENCODED_PUBKEY); - assertEquals(MODULUS, publicKey.getModulus()); - assertEquals(EXPONENT, publicKey.getPublicExponent()); - } - - @Test(expected=InvalidKeyException.class) - public void testParsePEMPublicKeyException() throws GeneralSecurityException { - encoder.parsePEMPublicKey(PEM_ENCODED_PUBKEY.substring(1)); - } - -} diff --git a/orchid/test/com/subgraph/orchid/geoip/CountryCodeServiceTest.java b/orchid/test/com/subgraph/orchid/geoip/CountryCodeServiceTest.java deleted file mode 100644 index b90005fd..00000000 --- a/orchid/test/com/subgraph/orchid/geoip/CountryCodeServiceTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.subgraph.orchid.geoip; - -import static org.junit.Assert.assertEquals; - -import java.io.IOException; - -import org.junit.Before; -import org.junit.Test; - -import com.subgraph.orchid.data.IPv4Address; -import com.subgraph.orchid.geoip.CountryCodeService; - -public class CountryCodeServiceTest { - - private CountryCodeService ccs; - - @Before - public void before() { - ccs = CountryCodeService.getInstance(); - } - - @Test - public void test() throws IOException { - testAddress("FR", "217.70.184.1"); // www.gandi.net - testAddress("DE", "213.165.65.50"); // www.gmx.de - testAddress("AR", "200.42.136.212"); // www.clarin.com - testAddress("GB", "77.91.248.30"); // www.guardian.co.uk - testAddress("CA", "132.216.177.160"); // www.mcgill.ca - testAddress("US", "38.229.72.14"); // www.torproject.net - } - - private void testAddress(String expectedCC, String address) { - IPv4Address a = IPv4Address.createFromString(address); - String cc = ccs.getCountryCodeForAddress(a); - assertEquals("Country Code lookup for "+ address, expectedCC, cc); - } -} diff --git a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java index 3339caaf..fc74d458 100644 --- a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java +++ b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java @@ -41,7 +41,6 @@ import com.google.common.io.Resources; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.protobuf.ByteString; -import com.subgraph.orchid.TorClient; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; @@ -234,7 +233,6 @@ public class WalletTool { OptionSpec passwordFlag = parser.accepts("password").withRequiredArg(); OptionSpec paymentRequestLocation = parser.accepts("payment-request").withRequiredArg(); parser.accepts("no-pki"); - parser.accepts("tor"); parser.accepts("dump-privkeys"); OptionSpec refundFlag = parser.accepts("refund-to").withRequiredArg(); OptionSpec txHashFlag = parser.accepts("txhash").withRequiredArg(); @@ -1201,13 +1199,6 @@ public class WalletTool { } // This will ensure the wallet is saved when it changes. wallet.autosaveToFile(walletFile, 5, TimeUnit.SECONDS, null); - if (options.has("tor")) { - try { - peers = PeerGroup.newWithTor(params, chain, new TorClient()); - } catch (TimeoutException e) { - System.err.println("Tor startup timed out, falling back to clear net ..."); - } - } if (peers == null) { peers = new PeerGroup(params, chain); } @@ -1226,8 +1217,6 @@ public class WalletTool { System.exit(1); } } - } else if (!options.has("tor")) { - peers.addPeerDiscovery(new DnsDiscovery(params)); } } diff --git a/wallettemplate/src/main/java/wallettemplate/Main.java b/wallettemplate/src/main/java/wallettemplate/Main.java index 65e086b1..407ce927 100644 --- a/wallettemplate/src/main/java/wallettemplate/Main.java +++ b/wallettemplate/src/main/java/wallettemplate/Main.java @@ -144,10 +144,6 @@ public class Main extends Application { // or progress widget to keep the user engaged whilst we initialise, but we don't. if (params == RegTestParams.get()) { bitcoin.connectToLocalHost(); // You should run a regtest mode bitcoind locally. - } else if (params == TestNet3Params.get()) { - // As an example! - bitcoin.useTor(); - // bitcoin.setDiscovery(new HttpDiscovery(params, URI.create("http://localhost:8080/peers"), ECKey.fromPublicOnly(BaseEncoding.base16().decode("02cba68cfd0679d10b186288b75a59f9132b1b3e222f6332717cb8c4eb2040f940".toUpperCase())))); } bitcoin.setDownloadListener(controller.progressBarUpdater()) .setBlockingStartup(false) diff --git a/wallettemplate/src/main/java/wallettemplate/MainController.java b/wallettemplate/src/main/java/wallettemplate/MainController.java index b5e8b9e6..4fa1b1c8 100644 --- a/wallettemplate/src/main/java/wallettemplate/MainController.java +++ b/wallettemplate/src/main/java/wallettemplate/MainController.java @@ -19,13 +19,9 @@ package wallettemplate; import org.bitcoinj.core.listeners.DownloadProgressTracker; import org.bitcoinj.core.Coin; import org.bitcoinj.utils.MonetaryFormat; -import com.subgraph.orchid.TorClient; -import com.subgraph.orchid.TorInitializationListener; import javafx.animation.FadeTransition; import javafx.animation.ParallelTransition; import javafx.animation.TranslateTransition; -import javafx.application.Platform; -import javafx.beans.property.SimpleDoubleProperty; import javafx.event.ActionEvent; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -65,31 +61,7 @@ public class MainController { // Don't let the user click send money when the wallet is empty. sendMoneyOutBtn.disableProperty().bind(model.balanceProperty().isEqualTo(Coin.ZERO)); - TorClient torClient = Main.bitcoin.peerGroup().getTorClient(); - if (torClient != null) { - SimpleDoubleProperty torProgress = new SimpleDoubleProperty(-1); - String torMsg = "Initialising Tor"; - syncItem = Main.instance.notificationBar.pushItem(torMsg, torProgress); - torClient.addInitializationListener(new TorInitializationListener() { - @Override - public void initializationProgress(String message, int percent) { - Platform.runLater(() -> { - syncItem.label.set(torMsg + ": " + message); - torProgress.set(percent / 100.0); - }); - } - - @Override - public void initializationCompleted() { - Platform.runLater(() -> { - syncItem.cancel(); - showBitcoinSyncMessage(); - }); - } - }); - } else { - showBitcoinSyncMessage(); - } + showBitcoinSyncMessage(); model.syncProgressProperty().addListener(x -> { if (model.syncProgressProperty().get() >= 1.0) { readyToGoAnimation();