From 77bd69c5e55428ec4c2db1589a6af1da15c10d05 Mon Sep 17 00:00:00 2001 From: cvs_import Date: Thu, 8 Apr 2004 04:41:54 +0000 Subject: [PATCH] beginning of branch i2p.i2p.i2p --- apps/httptunnel/doc/COPYING | 278 +++++ apps/httptunnel/doc/readme.license.txt | 11 + apps/httptunnel/java/build.xml | 47 + .../src/net/i2p/httptunnel/HTTPListener.java | 67 ++ .../net/i2p/httptunnel/HTTPSocketHandler.java | 54 + .../src/net/i2p/httptunnel/HTTPTunnel.java | 110 ++ .../java/src/net/i2p/httptunnel/Request.java | 130 +++ .../i2p/httptunnel/SocketManagerProducer.java | 111 ++ .../i2p/httptunnel/filter/ChainFilter.java | 50 + .../src/net/i2p/httptunnel/filter/Filter.java | 22 + .../net/i2p/httptunnel/filter/NullFilter.java | 15 + .../i2p/httptunnel/handler/EepHandler.java | 93 ++ .../i2p/httptunnel/handler/ErrorHandler.java | 36 + .../i2p/httptunnel/handler/LocalHandler.java | 49 + .../i2p/httptunnel/handler/ProxyHandler.java | 47 + .../i2p/httptunnel/handler/RootHandler.java | 109 ++ apps/i2ptunnel/doc/COPYING | 278 +++++ apps/i2ptunnel/doc/readme.license.txt | 11 + apps/i2ptunnel/doc/readme.txt | 21 + apps/i2ptunnel/java/build.xml | 47 + .../src/net/i2p/i2ptunnel/BufferLogger.java | 61 ++ .../java/src/net/i2p/i2ptunnel/I2PTunnel.java | 998 ++++++++++++++++++ .../net/i2p/i2ptunnel/I2PTunnelClient.java | 61 ++ .../i2p/i2ptunnel/I2PTunnelClientBase.java | 292 +++++ .../src/net/i2p/i2ptunnel/I2PTunnelGUI.java | 48 + .../i2p/i2ptunnel/I2PTunnelHTTPClient.java | 334 ++++++ .../net/i2p/i2ptunnel/I2PTunnelRunner.java | 184 ++++ .../net/i2p/i2ptunnel/I2PTunnelServer.java | 138 +++ .../src/net/i2p/i2ptunnel/I2PTunnelTask.java | 75 ++ .../java/src/net/i2p/i2ptunnel/I2Ping.java | 228 ++++ .../java/src/net/i2p/i2ptunnel/Logging.java | 9 + .../src/net/i2p/i2ptunnel/TunnelManager.java | 433 ++++++++ .../i2ptunnel/TunnelManagerClientRunner.java | 193 ++++ .../i2p/i2ptunnel/socks/I2PSOCKSTunnel.java | 56 + .../net/i2p/i2ptunnel/socks/SOCKS5Server.java | 309 ++++++ .../i2p/i2ptunnel/socks/SOCKSException.java | 23 + .../net/i2p/i2ptunnel/socks/SOCKSServer.java | 100 ++ .../i2ptunnel/socks/SOCKSServerFactory.java | 53 + apps/ministreaming/doc/protocol.txt | 83 ++ apps/ministreaming/doc/readme.license.txt | 10 + apps/ministreaming/java/build.xml | 35 + .../i2p/client/streaming/ByteCollector.java | 159 +++ .../i2p/client/streaming/I2PServerSocket.java | 29 + .../client/streaming/I2PServerSocketImpl.java | 50 + .../net/i2p/client/streaming/I2PSocket.java | 39 + .../i2p/client/streaming/I2PSocketImpl.java | 335 ++++++ .../client/streaming/I2PSocketManager.java | 386 +++++++ .../streaming/I2PSocketManagerFactory.java | 86 ++ .../client/streaming/I2PSocketOptions.java | 21 + apps/phttprelay/doc/readme.license.txt | 10 + apps/phttprelay/java/build.xml | 43 + apps/phttprelay/java/lib/LICENSE.html | 159 +++ apps/phttprelay/java/lib/readme.txt | 6 + .../phttprelay/CheckSendStatusServlet.java | 113 ++ .../src/net/i2p/phttprelay/LockManager.java | 40 + .../net/i2p/phttprelay/PHTTPRelayServlet.java | 73 ++ .../src/net/i2p/phttprelay/PollServlet.java | 263 +++++ .../net/i2p/phttprelay/RegisterServlet.java | 154 +++ .../src/net/i2p/phttprelay/SendServlet.java | 318 ++++++ apps/phttprelay/java/web.xml | 71 ++ apps/tests/COPYING | 278 +++++ apps/tests/EchoServer.java | 44 + apps/tests/GuaranteedBug.java | 106 ++ apps/tests/README | 6 + .../echotester/BasicEchoTestAnalyzer.java | 94 ++ apps/tests/echotester/EchoTestAnalyzer.java | 19 + apps/tests/echotester/EchoTester.java | 170 +++ apps/tests/readme.license.txt | 10 + build.xml | 57 + hosts.txt | 82 ++ installer/doc/COPYING | 278 +++++ installer/doc/readme.license.txt | 10 + installer/java/build.xml | 77 ++ installer/java/src/.nbattrs | 7 + installer/java/src/CliInstall.java | 67 ++ installer/java/src/FetchSeeds.java | 86 ++ installer/java/src/GUIInstall.java | 338 ++++++ installer/java/src/Install.java | 612 +++++++++++ installer/java/src/install.config | 108 ++ installer/java/src/logger.config.template | 66 ++ installer/java/src/reseed.bat.template | 3 + installer/java/src/reseed.sh.template | 4 + installer/java/src/router.config.template | 152 +++ installer/java/src/startFoo.bat.template | 5 + installer/java/src/startFoo.sh.template | 6 + installer/java/src/startRouter.bat.template | 9 + installer/java/src/startRouter.sh.template | 8 + installer/java/src/stopRouter.sh.template | 3 + readme.txt | 88 ++ router/doc/readme.license.txt | 9 + router/java/build.xml | 35 + .../src/net/i2p/data/i2np/DataMessage.java | 84 ++ .../data/i2np/DatabaseFindNearestMessage.java | 99 ++ .../i2p/data/i2np/DatabaseLookupMessage.java | 165 +++ .../data/i2np/DatabaseSearchReplyMessage.java | 149 +++ .../i2p/data/i2np/DatabaseStoreMessage.java | 170 +++ .../i2p/data/i2np/DeliveryInstructions.java | 274 +++++ .../i2p/data/i2np/DeliveryStatusMessage.java | 91 ++ .../net/i2p/data/i2np/EndPointPrivateKey.java | 62 ++ .../net/i2p/data/i2np/EndPointPublicKey.java | 62 ++ .../src/net/i2p/data/i2np/GarlicClove.java | 171 +++ .../src/net/i2p/data/i2np/GarlicMessage.java | 84 ++ .../src/net/i2p/data/i2np/I2NPMessage.java | 51 + .../i2p/data/i2np/I2NPMessageException.java | 28 + .../net/i2p/data/i2np/I2NPMessageHandler.java | 92 ++ .../net/i2p/data/i2np/I2NPMessageImpl.java | 104 ++ .../net/i2p/data/i2np/I2NPMessageReader.java | 139 +++ .../net/i2p/data/i2np/SourceRouteBlock.java | 225 ++++ .../data/i2np/SourceRouteReplyMessage.java | 159 +++ .../i2np/TunnelConfigurationSessionKey.java | 61 ++ .../i2p/data/i2np/TunnelCreateMessage.java | 312 ++++++ .../data/i2np/TunnelCreateStatusMessage.java | 113 ++ .../src/net/i2p/data/i2np/TunnelMessage.java | 145 +++ .../net/i2p/data/i2np/TunnelSessionKey.java | 61 ++ .../data/i2np/TunnelSigningPrivateKey.java | 62 ++ .../i2p/data/i2np/TunnelSigningPublicKey.java | 61 ++ .../i2np/TunnelVerificationStructure.java | 90 ++ router/java/src/net/i2p/router/.nbattrs | 14 + .../net/i2p/router/ClientManagerFacade.java | 95 ++ .../src/net/i2p/router/ClientMessage.java | 94 ++ .../src/net/i2p/router/ClientMessagePool.java | 126 +++ .../net/i2p/router/ClientTunnelSettings.java | 175 +++ .../src/net/i2p/router/CommSystemFacade.java | 40 + .../i2p/router/GenerateStatusConsoleJob.java | 62 ++ .../src/net/i2p/router/HandlerJobBuilder.java | 35 + .../java/src/net/i2p/router/InNetMessage.java | 68 ++ .../src/net/i2p/router/InNetMessagePool.java | 181 ++++ router/java/src/net/i2p/router/Job.java | 41 + router/java/src/net/i2p/router/JobImpl.java | 52 + router/java/src/net/i2p/router/JobQueue.java | 748 +++++++++++++ .../src/net/i2p/router/JobQueueRunner.java | 109 ++ router/java/src/net/i2p/router/JobStats.java | 75 ++ router/java/src/net/i2p/router/JobTiming.java | 69 ++ .../java/src/net/i2p/router/KeyManager.java | 190 ++++ .../java/src/net/i2p/router/LeaseSetKeys.java | 94 ++ .../src/net/i2p/router/MessageHistory.java | 549 ++++++++++ .../net/i2p/router/MessageReceptionInfo.java | 33 + .../src/net/i2p/router/MessageSelector.java | 34 + .../src/net/i2p/router/MessageValidator.java | 126 +++ .../net/i2p/router/NetworkDatabaseFacade.java | 89 ++ .../src/net/i2p/router/OutNetMessage.java | 272 +++++ .../src/net/i2p/router/OutNetMessagePool.java | 194 ++++ .../src/net/i2p/router/PeerManagerFacade.java | 40 + .../net/i2p/router/PeerSelectionCriteria.java | 39 + .../src/net/i2p/router/ProfileManager.java | 133 +++ router/java/src/net/i2p/router/ReplyJob.java | 19 + router/java/src/net/i2p/router/Router.java | 425 ++++++++ .../src/net/i2p/router/RouterVersion.java | 26 + router/java/src/net/i2p/router/Service.java | 32 + .../router/SessionKeyPersistenceHelper.java | 91 ++ router/java/src/net/i2p/router/Shitlist.java | 99 ++ .../src/net/i2p/router/StatisticsManager.java | 147 +++ .../i2p/router/SubmitMessageHistoryJob.java | 118 +++ .../java/src/net/i2p/router/TunnelInfo.java | 348 ++++++ .../net/i2p/router/TunnelManagerFacade.java | 66 ++ .../i2p/router/TunnelSelectionCriteria.java | 47 + .../src/net/i2p/router/TunnelSettings.java | 134 +++ .../net/i2p/router/admin/AdminListener.java | 110 ++ .../net/i2p/router/admin/AdminManager.java | 51 + .../src/net/i2p/router/admin/AdminRunner.java | 102 ++ .../net/i2p/router/admin/StatsGenerator.java | 214 ++++ .../router/client/ClientConnectionRunner.java | 404 +++++++ .../router/client/ClientListenerRunner.java | 132 +++ .../net/i2p/router/client/ClientManager.java | 306 ++++++ .../client/ClientManagerFacadeImpl.java | 157 +++ .../client/ClientMessageEventListener.java | 228 ++++ .../i2p/router/client/CreateSessionJob.java | 62 ++ .../i2p/router/client/LeaseRequestState.java | 62 ++ .../i2p/router/client/MessageReceivedJob.java | 70 ++ .../net/i2p/router/client/ReportAbuseJob.java | 56 + .../i2p/router/client/RequestLeaseSetJob.java | 129 +++ .../message/BuildCreateTunnelMessageJob.java | 67 ++ .../router/message/BuildTestMessageJob.java | 202 ++++ .../src/net/i2p/router/message/CloveSet.java | 58 + .../net/i2p/router/message/GarlicConfig.java | 182 ++++ .../router/message/GarlicMessageBuilder.java | 206 ++++ .../router/message/GarlicMessageHandler.java | 32 + .../router/message/GarlicMessageParser.java | 92 ++ .../message/HandleGarlicMessageJob.java | 164 +++ .../HandleSourceRouteReplyMessageJob.java | 143 +++ .../message/HandleTunnelMessageJob.java | 527 +++++++++ .../i2p/router/message/MessageHandler.java | 179 ++++ .../message/OutboundClientMessageJob.java | 595 +++++++++++ .../OutboundClientMessageJobHelper.java | 179 ++++ .../router/message/PayloadGarlicConfig.java | 41 + .../net/i2p/router/message/SendGarlicJob.java | 122 +++ .../i2p/router/message/SendMessageAckJob.java | 59 ++ .../router/message/SendMessageDirectJob.java | 159 +++ .../router/message/SendReplyMessageJob.java | 63 ++ .../router/message/SendTunnelMessageJob.java | 426 ++++++++ .../SourceRouteReplyMessageHandler.java | 32 + .../router/message/TunnelMessageHandler.java | 32 + .../DatabaseLookupMessageHandler.java | 28 + .../DatabaseSearchReplyMessageHandler.java | 28 + .../DatabaseStoreMessageHandler.java | 28 + .../HandleDatabaseLookupMessageJob.java | 189 ++++ .../HandleDatabaseSearchReplyMessageJob.java | 71 ++ .../HandleDatabaseStoreMessageJob.java | 65 ++ .../networkdb/PublishLocalRouterInfoJob.java | 54 + .../networkdb/kademlia/DataPublisherJob.java | 88 ++ .../kademlia/DataRepublishingSelectorJob.java | 160 +++ .../router/networkdb/kademlia/DataStore.java | 22 + .../networkdb/kademlia/ExpireLeasesJob.java | 73 ++ .../networkdb/kademlia/ExpireRoutersJob.java | 106 ++ .../router/networkdb/kademlia/ExploreJob.java | 105 ++ .../kademlia/ExploreKeySelectorJob.java | 79 ++ .../router/networkdb/kademlia/KBucket.java | 79 ++ .../networkdb/kademlia/KBucketImpl.java | 286 +++++ .../router/networkdb/kademlia/KBucketSet.java | 149 +++ .../KademliaNetworkDatabaseFacade.java | 577 ++++++++++ .../networkdb/kademlia/PeerSelector.java | 111 ++ .../kademlia/PersistentDataStore.java | 323 ++++++ .../kademlia/RepublishLeaseSetJob.java | 80 ++ .../networkdb/kademlia/RouterGenerator.java | 136 +++ .../router/networkdb/kademlia/SearchJob.java | 469 ++++++++ .../kademlia/SearchMessageSelector.java | 86 ++ .../networkdb/kademlia/SearchState.java | 159 +++ .../kademlia/SearchUpdateReplyFoundJob.java | 68 ++ .../networkdb/kademlia/StartExplorersJob.java | 65 ++ .../router/networkdb/kademlia/StoreJob.java | 678 ++++++++++++ .../kademlia/TransientDataStore.java | 153 +++ .../i2p/router/peermanager/Calculator.java | 28 + .../net/i2p/router/peermanager/DBHistory.java | 212 ++++ .../peermanager/EvaluateProfilesJob.java | 50 + .../peermanager/IntegrationCalculator.java | 18 + .../peermanager/IsFailingCalculator.java | 50 + .../i2p/router/peermanager/PeerManager.java | 118 +++ .../peermanager/PeerManagerFacadeImpl.java | 43 + .../i2p/router/peermanager/PeerProfile.java | 317 ++++++ .../peermanager/PersistProfilesJob.java | 53 + .../peermanager/ProfileManagerImpl.java | 289 +++++ .../router/peermanager/ProfileOrganizer.java | 625 +++++++++++ .../peermanager/ProfilePersistenceHelper.java | 276 +++++ .../peermanager/ReliabilityCalculator.java | 71 ++ .../router/peermanager/SpeedCalculator.java | 60 ++ .../i2p/router/peermanager/TunnelHistory.java | 104 ++ .../i2p/router/startup/BootCommSystemJob.java | 51 + .../i2p/router/startup/BootNetworkDbJob.java | 30 + .../router/startup/BuildTrustedLinksJob.java | 33 + .../router/startup/CreateRouterInfoJob.java | 123 +++ .../i2p/router/startup/LoadRouterInfoJob.java | 115 ++ .../startup/ProcessInboundNetMessageJob.java | 47 + .../net/i2p/router/startup/ReadConfigJob.java | 69 ++ .../router/startup/RebuildRouterInfoJob.java | 196 ++++ .../startup/StartAcceptingClientsJob.java | 165 +++ .../net/i2p/router/startup/StartupJob.java | 40 + .../src/net/i2p/router/transport/.nbattrs | 7 + .../BandwidthLimitedInputStream.java | 45 + .../BandwidthLimitedOutputStream.java | 61 ++ .../router/transport/BandwidthLimiter.java | 78 ++ .../transport/CommSystemFacadeImpl.java | 112 ++ .../transport/FetchOutNetMessageJob.java | 49 + .../net/i2p/router/transport/GetBidsJob.java | 81 ++ .../transport/OutboundMessageRegistry.java | 315 ++++++ .../net/i2p/router/transport/Transport.java | 41 + .../i2p/router/transport/TransportBid.java | 76 ++ .../transport/TransportEventListener.java | 18 + .../i2p/router/transport/TransportImpl.java | 231 ++++ .../router/transport/TransportManager.java | 261 +++++ .../transport/TrivialBandwidthLimiter.java | 202 ++++ .../router/transport/phttp/PHTTPPoller.java | 236 +++++ .../router/transport/phttp/PHTTPSender.java | 283 +++++ .../transport/phttp/PHTTPTransport.java | 302 ++++++ .../tcp/RestrictiveTCPConnection.java | 322 ++++++ .../router/transport/tcp/SocketCreator.java | 114 ++ .../i2p/router/transport/tcp/TCPAddress.java | 107 ++ .../router/transport/tcp/TCPConnection.java | 533 ++++++++++ .../i2p/router/transport/tcp/TCPListener.java | 208 ++++ .../router/transport/tcp/TCPTransport.java | 846 +++++++++++++++ .../ClientLeaseSetManagerJob.java | 203 ++++ .../tunnelmanager/ClientTunnelPool.java | 191 ++++ .../ClientTunnelPoolExpirationJob.java | 106 ++ .../ClientTunnelPoolManagerJob.java | 201 ++++ .../HandleTunnelCreateMessageJob.java | 158 +++ .../PoolingTunnelManagerFacade.java | 201 ++++ .../tunnelmanager/PoolingTunnelSelector.java | 119 +++ .../RequestInboundTunnelJob.java | 29 + .../RequestOutboundTunnelJob.java | 23 + .../tunnelmanager/RequestTunnelJob.java | 863 +++++++++++++++ .../router/tunnelmanager/TestTunnelJob.java | 250 +++++ .../router/tunnelmanager/TunnelBuilder.java | 388 +++++++ .../TunnelCreateMessageHandler.java | 26 + .../router/tunnelmanager/TunnelGateway.java | 23 + .../i2p/router/tunnelmanager/TunnelPool.java | 643 +++++++++++ .../TunnelPoolExpirationJob.java | 132 +++ .../tunnelmanager/TunnelPoolManagerJob.java | 167 +++ .../TunnelPoolPersistenceHelper.java | 203 ++++ .../tunnelmanager/TunnelTestManager.java | 119 +++ router/java/test/net/i2p/data/i2np/.nbattrs | 7 + .../data/i2np/DatabaseStoreMessageTest.java | 38 + .../data/i2np/DeliveryInstructionsTest.java | 54 + .../i2p/data/i2np/I2NPMessageReaderTest.java | 78 ++ 292 files changed, 41035 insertions(+) create mode 100644 apps/httptunnel/doc/COPYING create mode 100644 apps/httptunnel/doc/readme.license.txt create mode 100644 apps/httptunnel/java/build.xml create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/HTTPListener.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/HTTPSocketHandler.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/HTTPTunnel.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/Request.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/SocketManagerProducer.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/filter/ChainFilter.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/filter/Filter.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/filter/NullFilter.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/handler/EepHandler.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/handler/ErrorHandler.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/handler/LocalHandler.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/handler/ProxyHandler.java create mode 100644 apps/httptunnel/java/src/net/i2p/httptunnel/handler/RootHandler.java create mode 100644 apps/i2ptunnel/doc/COPYING create mode 100644 apps/i2ptunnel/doc/readme.license.txt create mode 100644 apps/i2ptunnel/doc/readme.txt create mode 100644 apps/i2ptunnel/java/build.xml create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/BufferLogger.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelTask.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2Ping.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/Logging.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelManager.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelManagerClientRunner.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSException.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java create mode 100644 apps/ministreaming/doc/protocol.txt create mode 100644 apps/ministreaming/doc/readme.license.txt create mode 100644 apps/ministreaming/java/build.xml create mode 100644 apps/ministreaming/java/src/net/i2p/client/streaming/ByteCollector.java create mode 100644 apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java create mode 100644 apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocketImpl.java create mode 100644 apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocket.java create mode 100644 apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketImpl.java create mode 100644 apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManager.java create mode 100644 apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java create mode 100644 apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketOptions.java create mode 100644 apps/phttprelay/doc/readme.license.txt create mode 100644 apps/phttprelay/java/build.xml create mode 100644 apps/phttprelay/java/lib/LICENSE.html create mode 100644 apps/phttprelay/java/lib/readme.txt create mode 100644 apps/phttprelay/java/src/net/i2p/phttprelay/CheckSendStatusServlet.java create mode 100644 apps/phttprelay/java/src/net/i2p/phttprelay/LockManager.java create mode 100644 apps/phttprelay/java/src/net/i2p/phttprelay/PHTTPRelayServlet.java create mode 100644 apps/phttprelay/java/src/net/i2p/phttprelay/PollServlet.java create mode 100644 apps/phttprelay/java/src/net/i2p/phttprelay/RegisterServlet.java create mode 100644 apps/phttprelay/java/src/net/i2p/phttprelay/SendServlet.java create mode 100644 apps/phttprelay/java/web.xml create mode 100644 apps/tests/COPYING create mode 100644 apps/tests/EchoServer.java create mode 100644 apps/tests/GuaranteedBug.java create mode 100644 apps/tests/README create mode 100644 apps/tests/echotester/BasicEchoTestAnalyzer.java create mode 100644 apps/tests/echotester/EchoTestAnalyzer.java create mode 100644 apps/tests/echotester/EchoTester.java create mode 100644 apps/tests/readme.license.txt create mode 100644 build.xml create mode 100644 hosts.txt create mode 100644 installer/doc/COPYING create mode 100644 installer/doc/readme.license.txt create mode 100644 installer/java/build.xml create mode 100644 installer/java/src/.nbattrs create mode 100644 installer/java/src/CliInstall.java create mode 100644 installer/java/src/FetchSeeds.java create mode 100644 installer/java/src/GUIInstall.java create mode 100644 installer/java/src/Install.java create mode 100644 installer/java/src/install.config create mode 100644 installer/java/src/logger.config.template create mode 100644 installer/java/src/reseed.bat.template create mode 100644 installer/java/src/reseed.sh.template create mode 100644 installer/java/src/router.config.template create mode 100644 installer/java/src/startFoo.bat.template create mode 100644 installer/java/src/startFoo.sh.template create mode 100644 installer/java/src/startRouter.bat.template create mode 100644 installer/java/src/startRouter.sh.template create mode 100644 installer/java/src/stopRouter.sh.template create mode 100644 readme.txt create mode 100644 router/doc/readme.license.txt create mode 100644 router/java/build.xml create mode 100644 router/java/src/net/i2p/data/i2np/DataMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/DatabaseFindNearestMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/DatabaseSearchReplyMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/DeliveryInstructions.java create mode 100644 router/java/src/net/i2p/data/i2np/DeliveryStatusMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/EndPointPrivateKey.java create mode 100644 router/java/src/net/i2p/data/i2np/EndPointPublicKey.java create mode 100644 router/java/src/net/i2p/data/i2np/GarlicClove.java create mode 100644 router/java/src/net/i2p/data/i2np/GarlicMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/I2NPMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/I2NPMessageException.java create mode 100644 router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java create mode 100644 router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java create mode 100644 router/java/src/net/i2p/data/i2np/I2NPMessageReader.java create mode 100644 router/java/src/net/i2p/data/i2np/SourceRouteBlock.java create mode 100644 router/java/src/net/i2p/data/i2np/SourceRouteReplyMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/TunnelConfigurationSessionKey.java create mode 100644 router/java/src/net/i2p/data/i2np/TunnelCreateMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/TunnelCreateStatusMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/TunnelMessage.java create mode 100644 router/java/src/net/i2p/data/i2np/TunnelSessionKey.java create mode 100644 router/java/src/net/i2p/data/i2np/TunnelSigningPrivateKey.java create mode 100644 router/java/src/net/i2p/data/i2np/TunnelSigningPublicKey.java create mode 100644 router/java/src/net/i2p/data/i2np/TunnelVerificationStructure.java create mode 100644 router/java/src/net/i2p/router/.nbattrs create mode 100644 router/java/src/net/i2p/router/ClientManagerFacade.java create mode 100644 router/java/src/net/i2p/router/ClientMessage.java create mode 100644 router/java/src/net/i2p/router/ClientMessagePool.java create mode 100644 router/java/src/net/i2p/router/ClientTunnelSettings.java create mode 100644 router/java/src/net/i2p/router/CommSystemFacade.java create mode 100644 router/java/src/net/i2p/router/GenerateStatusConsoleJob.java create mode 100644 router/java/src/net/i2p/router/HandlerJobBuilder.java create mode 100644 router/java/src/net/i2p/router/InNetMessage.java create mode 100644 router/java/src/net/i2p/router/InNetMessagePool.java create mode 100644 router/java/src/net/i2p/router/Job.java create mode 100644 router/java/src/net/i2p/router/JobImpl.java create mode 100644 router/java/src/net/i2p/router/JobQueue.java create mode 100644 router/java/src/net/i2p/router/JobQueueRunner.java create mode 100644 router/java/src/net/i2p/router/JobStats.java create mode 100644 router/java/src/net/i2p/router/JobTiming.java create mode 100644 router/java/src/net/i2p/router/KeyManager.java create mode 100644 router/java/src/net/i2p/router/LeaseSetKeys.java create mode 100644 router/java/src/net/i2p/router/MessageHistory.java create mode 100644 router/java/src/net/i2p/router/MessageReceptionInfo.java create mode 100644 router/java/src/net/i2p/router/MessageSelector.java create mode 100644 router/java/src/net/i2p/router/MessageValidator.java create mode 100644 router/java/src/net/i2p/router/NetworkDatabaseFacade.java create mode 100644 router/java/src/net/i2p/router/OutNetMessage.java create mode 100644 router/java/src/net/i2p/router/OutNetMessagePool.java create mode 100644 router/java/src/net/i2p/router/PeerManagerFacade.java create mode 100644 router/java/src/net/i2p/router/PeerSelectionCriteria.java create mode 100644 router/java/src/net/i2p/router/ProfileManager.java create mode 100644 router/java/src/net/i2p/router/ReplyJob.java create mode 100644 router/java/src/net/i2p/router/Router.java create mode 100644 router/java/src/net/i2p/router/RouterVersion.java create mode 100644 router/java/src/net/i2p/router/Service.java create mode 100644 router/java/src/net/i2p/router/SessionKeyPersistenceHelper.java create mode 100644 router/java/src/net/i2p/router/Shitlist.java create mode 100644 router/java/src/net/i2p/router/StatisticsManager.java create mode 100644 router/java/src/net/i2p/router/SubmitMessageHistoryJob.java create mode 100644 router/java/src/net/i2p/router/TunnelInfo.java create mode 100644 router/java/src/net/i2p/router/TunnelManagerFacade.java create mode 100644 router/java/src/net/i2p/router/TunnelSelectionCriteria.java create mode 100644 router/java/src/net/i2p/router/TunnelSettings.java create mode 100644 router/java/src/net/i2p/router/admin/AdminListener.java create mode 100644 router/java/src/net/i2p/router/admin/AdminManager.java create mode 100644 router/java/src/net/i2p/router/admin/AdminRunner.java create mode 100644 router/java/src/net/i2p/router/admin/StatsGenerator.java create mode 100644 router/java/src/net/i2p/router/client/ClientConnectionRunner.java create mode 100644 router/java/src/net/i2p/router/client/ClientListenerRunner.java create mode 100644 router/java/src/net/i2p/router/client/ClientManager.java create mode 100644 router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java create mode 100644 router/java/src/net/i2p/router/client/ClientMessageEventListener.java create mode 100644 router/java/src/net/i2p/router/client/CreateSessionJob.java create mode 100644 router/java/src/net/i2p/router/client/LeaseRequestState.java create mode 100644 router/java/src/net/i2p/router/client/MessageReceivedJob.java create mode 100644 router/java/src/net/i2p/router/client/ReportAbuseJob.java create mode 100644 router/java/src/net/i2p/router/client/RequestLeaseSetJob.java create mode 100644 router/java/src/net/i2p/router/message/BuildCreateTunnelMessageJob.java create mode 100644 router/java/src/net/i2p/router/message/BuildTestMessageJob.java create mode 100644 router/java/src/net/i2p/router/message/CloveSet.java create mode 100644 router/java/src/net/i2p/router/message/GarlicConfig.java create mode 100644 router/java/src/net/i2p/router/message/GarlicMessageBuilder.java create mode 100644 router/java/src/net/i2p/router/message/GarlicMessageHandler.java create mode 100644 router/java/src/net/i2p/router/message/GarlicMessageParser.java create mode 100644 router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java create mode 100644 router/java/src/net/i2p/router/message/HandleSourceRouteReplyMessageJob.java create mode 100644 router/java/src/net/i2p/router/message/HandleTunnelMessageJob.java create mode 100644 router/java/src/net/i2p/router/message/MessageHandler.java create mode 100644 router/java/src/net/i2p/router/message/OutboundClientMessageJob.java create mode 100644 router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java create mode 100644 router/java/src/net/i2p/router/message/PayloadGarlicConfig.java create mode 100644 router/java/src/net/i2p/router/message/SendGarlicJob.java create mode 100644 router/java/src/net/i2p/router/message/SendMessageAckJob.java create mode 100644 router/java/src/net/i2p/router/message/SendMessageDirectJob.java create mode 100644 router/java/src/net/i2p/router/message/SendReplyMessageJob.java create mode 100644 router/java/src/net/i2p/router/message/SendTunnelMessageJob.java create mode 100644 router/java/src/net/i2p/router/message/SourceRouteReplyMessageHandler.java create mode 100644 router/java/src/net/i2p/router/message/TunnelMessageHandler.java create mode 100644 router/java/src/net/i2p/router/networkdb/DatabaseLookupMessageHandler.java create mode 100644 router/java/src/net/i2p/router/networkdb/DatabaseSearchReplyMessageHandler.java create mode 100644 router/java/src/net/i2p/router/networkdb/DatabaseStoreMessageHandler.java create mode 100644 router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/HandleDatabaseSearchReplyMessageJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/HandleDatabaseStoreMessageJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/DataPublisherJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/DataRepublishingSelectorJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/DataStore.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/ExpireLeasesJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/ExploreKeySelectorJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/KBucket.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/KBucketImpl.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/KBucketSet.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/RouterGenerator.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java create mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java create mode 100644 router/java/src/net/i2p/router/peermanager/Calculator.java create mode 100644 router/java/src/net/i2p/router/peermanager/DBHistory.java create mode 100644 router/java/src/net/i2p/router/peermanager/EvaluateProfilesJob.java create mode 100644 router/java/src/net/i2p/router/peermanager/IntegrationCalculator.java create mode 100644 router/java/src/net/i2p/router/peermanager/IsFailingCalculator.java create mode 100644 router/java/src/net/i2p/router/peermanager/PeerManager.java create mode 100644 router/java/src/net/i2p/router/peermanager/PeerManagerFacadeImpl.java create mode 100644 router/java/src/net/i2p/router/peermanager/PeerProfile.java create mode 100644 router/java/src/net/i2p/router/peermanager/PersistProfilesJob.java create mode 100644 router/java/src/net/i2p/router/peermanager/ProfileManagerImpl.java create mode 100644 router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java create mode 100644 router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java create mode 100644 router/java/src/net/i2p/router/peermanager/ReliabilityCalculator.java create mode 100644 router/java/src/net/i2p/router/peermanager/SpeedCalculator.java create mode 100644 router/java/src/net/i2p/router/peermanager/TunnelHistory.java create mode 100644 router/java/src/net/i2p/router/startup/BootCommSystemJob.java create mode 100644 router/java/src/net/i2p/router/startup/BootNetworkDbJob.java create mode 100644 router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java create mode 100644 router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java create mode 100644 router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java create mode 100644 router/java/src/net/i2p/router/startup/ProcessInboundNetMessageJob.java create mode 100644 router/java/src/net/i2p/router/startup/ReadConfigJob.java create mode 100644 router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java create mode 100644 router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java create mode 100644 router/java/src/net/i2p/router/startup/StartupJob.java create mode 100644 router/java/src/net/i2p/router/transport/.nbattrs create mode 100644 router/java/src/net/i2p/router/transport/BandwidthLimitedInputStream.java create mode 100644 router/java/src/net/i2p/router/transport/BandwidthLimitedOutputStream.java create mode 100644 router/java/src/net/i2p/router/transport/BandwidthLimiter.java create mode 100644 router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java create mode 100644 router/java/src/net/i2p/router/transport/FetchOutNetMessageJob.java create mode 100644 router/java/src/net/i2p/router/transport/GetBidsJob.java create mode 100644 router/java/src/net/i2p/router/transport/OutboundMessageRegistry.java create mode 100644 router/java/src/net/i2p/router/transport/Transport.java create mode 100644 router/java/src/net/i2p/router/transport/TransportBid.java create mode 100644 router/java/src/net/i2p/router/transport/TransportEventListener.java create mode 100644 router/java/src/net/i2p/router/transport/TransportImpl.java create mode 100644 router/java/src/net/i2p/router/transport/TransportManager.java create mode 100644 router/java/src/net/i2p/router/transport/TrivialBandwidthLimiter.java create mode 100644 router/java/src/net/i2p/router/transport/phttp/PHTTPPoller.java create mode 100644 router/java/src/net/i2p/router/transport/phttp/PHTTPSender.java create mode 100644 router/java/src/net/i2p/router/transport/phttp/PHTTPTransport.java create mode 100644 router/java/src/net/i2p/router/transport/tcp/RestrictiveTCPConnection.java create mode 100644 router/java/src/net/i2p/router/transport/tcp/SocketCreator.java create mode 100644 router/java/src/net/i2p/router/transport/tcp/TCPAddress.java create mode 100644 router/java/src/net/i2p/router/transport/tcp/TCPConnection.java create mode 100644 router/java/src/net/i2p/router/transport/tcp/TCPListener.java create mode 100644 router/java/src/net/i2p/router/transport/tcp/TCPTransport.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/ClientLeaseSetManagerJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPool.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPoolExpirationJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPoolManagerJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/HandleTunnelCreateMessageJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelManagerFacade.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelSelector.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/RequestInboundTunnelJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/RequestOutboundTunnelJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/RequestTunnelJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/TestTunnelJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/TunnelBuilder.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/TunnelCreateMessageHandler.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/TunnelGateway.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/TunnelPool.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/TunnelPoolExpirationJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/TunnelPoolManagerJob.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/TunnelPoolPersistenceHelper.java create mode 100644 router/java/src/net/i2p/router/tunnelmanager/TunnelTestManager.java create mode 100644 router/java/test/net/i2p/data/i2np/.nbattrs create mode 100644 router/java/test/net/i2p/data/i2np/DatabaseStoreMessageTest.java create mode 100644 router/java/test/net/i2p/data/i2np/DeliveryInstructionsTest.java create mode 100644 router/java/test/net/i2p/data/i2np/I2NPMessageReaderTest.java diff --git a/apps/httptunnel/doc/COPYING b/apps/httptunnel/doc/COPYING new file mode 100644 index 0000000000..5ec43ee156 --- /dev/null +++ b/apps/httptunnel/doc/COPYING @@ -0,0 +1,278 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. diff --git a/apps/httptunnel/doc/readme.license.txt b/apps/httptunnel/doc/readme.license.txt new file mode 100644 index 0000000000..7f7c30bc7e --- /dev/null +++ b/apps/httptunnel/doc/readme.license.txt @@ -0,0 +1,11 @@ +$Id$ + +the i2p/apps/httptunnel module is the root of the +HTTPTunnel application, and everything within it +is released according to the terms of the I2P +license policy. That means everything contained +within the i2p/apps/httptunnel module is released +under the GPL plus the java exception unless +otherwise marked. Alternate licenses that may be +used include BSD, Cryptix, and MIT, as well as +code granted into the public domain. diff --git a/apps/httptunnel/java/build.xml b/apps/httptunnel/java/build.xml new file mode 100644 index 0000000000..cda44d6524 --- /dev/null +++ b/apps/httptunnel/java/build.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/HTTPListener.java b/apps/httptunnel/java/src/net/i2p/httptunnel/HTTPListener.java new file mode 100644 index 0000000000..b1bedcbd31 --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/HTTPListener.java @@ -0,0 +1,67 @@ +package net.i2p.httptunnel; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; + +import net.i2p.util.Log; + +/** + * Listens on a port for HTTP connections. + */ +public class HTTPListener extends Thread { + + private static final Log _log = new Log(HTTPListener.class); + + private int port; + private String listenHost; + private SocketManagerProducer smp; + + public HTTPListener(SocketManagerProducer smp, int port, + String listenHost) { + this.smp = smp; + this.port = port; + start(); + } + + public void run() { + try { + InetAddress lh = listenHost == null + ? null + : InetAddress.getByName(listenHost); + ServerSocket ss = new ServerSocket(port, 0, lh); + while(true) { + Socket s = ss.accept(); + new HTTPSocketHandler(this, s); + } + } catch (IOException ex) { + _log.error("Error while accepting connections", ex); + } + } + + private boolean proxyUsed=false; + + public boolean firstProxyUse() { + // FIXME: check a config option here + if (true) return false; + if (proxyUsed) { + return false; + } else { + proxyUsed=true; + return true; + } + } + + public SocketManagerProducer getSMP() { + return smp; + } + + /** @deprecated */ + public void handleNotImplemented(OutputStream out) throws IOException { + out.write(("HTTP/1.1 200 Document following\n\n"+ + "

Feature not implemented

").getBytes("ISO-8859-1")); + out.flush(); + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/HTTPSocketHandler.java b/apps/httptunnel/java/src/net/i2p/httptunnel/HTTPSocketHandler.java new file mode 100644 index 0000000000..2ff714fa2c --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/HTTPSocketHandler.java @@ -0,0 +1,54 @@ +package net.i2p.httptunnel; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; + +import net.i2p.httptunnel.handler.RootHandler; +import net.i2p.util.Log; + +/** + * Handles a single HTTP socket connection. + */ +public class HTTPSocketHandler extends Thread { + + private static final Log _log = new Log(HTTPSocketHandler.class); + + private Socket s; + private HTTPListener httpl; + private RootHandler h; + + public HTTPSocketHandler(HTTPListener httpl, Socket s) { + this.httpl = httpl; + this.s=s; + h = RootHandler.getInstance(); + start(); + } + + public void run() { + InputStream in = null; + OutputStream out = null; + try { + in = new BufferedInputStream(s.getInputStream()); + out = new BufferedOutputStream(s.getOutputStream()); + Request req = new Request(in); + h.handle(req, httpl, out); + } catch (IOException ex) { + _log.error("Error while handling data", ex); + } finally { + try { + if (in != null) in.close(); + if (out != null) { + out.flush(); + out.close(); + } + s.close(); + } catch (IOException ex) { + _log.error("IOException in finalizer", ex); + } + } + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/HTTPTunnel.java b/apps/httptunnel/java/src/net/i2p/httptunnel/HTTPTunnel.java new file mode 100644 index 0000000000..1e012e75c7 --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/HTTPTunnel.java @@ -0,0 +1,110 @@ +/* + * HTTPTunnel + * (c) 2003 - 2004 mihi + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * In addition, as a special exception, mihi gives permission to link + * the code of this program with the proprietary Java implementation + * provided by Sun (or other vendors as well), and distribute linked + * combinations including the two. You must obey the GNU General + * Public License in all respects for all of the code used other than + * the proprietary Java implementation. If you modify this file, you + * may extend this exception to your version of the file, but you are + * not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ +package net.i2p.httptunnel; + +import net.i2p.client.streaming.I2PSocketManager; + +/** + * HTTPTunnel main class. + */ +public class HTTPTunnel { + + /** + * Create a HTTPTunnel instance. + * + * @param initialManagers a list of socket managers to use + * @param maxManagers how many managers to have in the cache + * @param mcDonaldsMode whether to throw away a manager after use + * @param listenPort which port to listen on + */ + public HTTPTunnel(I2PSocketManager[] initialManagers, int maxManagers, + boolean mcDonaldsMode, int listenPort) { + this(initialManagers, maxManagers, mcDonaldsMode, listenPort, + "127.0.0.1", 7654); + } + + /** + * Create a HTTPTunnel instance. + * + * @param initialManagers a list of socket managers to use + * @param maxManagers how many managers to have in the cache + * @param mcDonaldsMode whether to throw away a manager after use + * @param listenPort which port to listen on + * @param i2cpAddress the I2CP address + * @param i2cpPort the I2CP port + */ + public HTTPTunnel(I2PSocketManager[] initialManagers, int maxManagers, + boolean mcDonaldsMode, int listenPort, + String i2cpAddress, int i2cpPort) { + SocketManagerProducer smp = + new SocketManagerProducer(initialManagers, maxManagers, + mcDonaldsMode, i2cpAddress, i2cpPort); + new HTTPListener(smp, listenPort, "127.0.0.1"); + } + + public static void main(String[] args) { + String host = "127.0.0.1"; + int port = 7654, max = 1; + boolean mc = false; + if (args.length >1) { + if (args.length == 4) { + host = args[2]; + port = Integer.parseInt(args[3]); + } else if (args.length != 2) { + showInfo(); return; + } + max = Integer.parseInt(args[1]); + } else if (args.length != 1) { + showInfo(); return; + } + if (max == 0) { + max = 1; + } else if (max <0) { + max = -max; + mc = true; + } + new HTTPTunnel(null, max, mc, Integer.parseInt(args[0]), host, port); + } + + private static void showInfo() { + System.out.println + ("Usage: java HTTPTunnel [ "+ + "[ ]]\n"+ + " port to listen for browsers\n"+ + " max number of SocketMangers in pool, "+ + "use neg. number\n"+ + " to use each SocketManager only once "+ + "(default: 1)\n"+ + " host to connect to the router "+ + "(default: 127.0.0.1)\n"+ + " port to connect to the router "+ + "(default: 7654)"); + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/Request.java b/apps/httptunnel/java/src/net/i2p/httptunnel/Request.java new file mode 100644 index 0000000000..e46abf785c --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/Request.java @@ -0,0 +1,130 @@ +package net.i2p.httptunnel; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; + +import net.i2p.util.Log; + +/** + * A HTTP request (GET or POST). This will be passed to a hander for + * handling it. + */ +public class Request { + + private static final Log _log = new Log(Request.class); + + // all strings are forced to be ISO-8859-1 encoding + private String method; + private String url; + private String proto; + private String params; + private String postData; + + public Request(InputStream in) throws IOException { + BufferedReader br = new BufferedReader + (new InputStreamReader(in, "ISO-8859-1")); + String line = br.readLine(); + if (line == null) { // no data at all + method = null; + _log.error("Connection but no data"); + return; + } + int pos = line.indexOf(" "); + if (pos == -1) { + method = line; + url=""; + _log.error("Malformed HTTP request: "+line); + } else { + method = line.substring(0,pos); + url=line.substring(pos+1); + } + proto=""; + pos = url.indexOf(" "); + if (pos != -1) { + proto=url.substring(pos); // leading space intended + url = url.substring(0,pos); + } + StringBuffer sb = new StringBuffer(512); + while((line=br.readLine()) != null) { + if (line.length() == 0) break; + sb.append(line).append("\r\n"); + } + params = sb.toString(); // no leading empty line! + sb = new StringBuffer(); + // hack for POST requests, ripped from HttpClient + // this won't work for large POSTDATA + // FIXME: do this better, please. + if (!method.equals("GET")) { + while (br.ready()) { // empty the buffer (POST requests) + int i=br.read(); + if (i != -1) { + sb.append((char)i); + } + } + postData = sb.toString(); + } else { + postData=""; + } + } + + public byte[] toByteArray() throws IOException { + if (method == null) return null; + return toISO8859_1String().getBytes("ISO-8859-1"); + + } + + private String toISO8859_1String() throws IOException { + if (method == null) return null; + return method+" "+url+proto+"\r\n"+params+"\r\n"+postData; + } + + public String getURL() { + return url; + } + + public void setURL(String newURL) { + url=newURL; + } + + public String getParam(String name) { + try { + BufferedReader br= new BufferedReader(new StringReader(params)); + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith(name)) { + return line.substring(name.length()); + } + } + return null; + } catch (IOException ex) { + _log.error("Error getting parameter", ex); + return null; + } + } + + public void setParam(String name, String value) { + try { + StringBuffer sb = new StringBuffer(params.length()+value.length()); + BufferedReader br= new BufferedReader(new StringReader(params)); + String line; + boolean replaced = false; + while((line=br.readLine()) != null) { + if (line.startsWith(name)) { + replaced=true; + if (value == null) continue; // kill param + line = name+value; + } + sb.append(line).append("\r\n"); + } + if (!replaced && value != null) { + sb.append(name).append(value).append("\r\n"); + } + params=sb.toString(); + } catch (IOException ex) { + _log.error("Error getting parameter", ex); + } + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/SocketManagerProducer.java b/apps/httptunnel/java/src/net/i2p/httptunnel/SocketManagerProducer.java new file mode 100644 index 0000000000..4ad3a4642b --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/SocketManagerProducer.java @@ -0,0 +1,111 @@ +package net.i2p.httptunnel; + +import java.util.*; +import net.i2p.client.streaming.*; + +/** + * Produces SocketManagers in a thread and gives them to those who + * need them. + */ +public class SocketManagerProducer extends Thread { + + private ArrayList myManagers = new ArrayList(); + private HashMap usedManagers = new HashMap(); + private int port; + private String host; + private int maxManagers; + private boolean mcDonalds; + + public SocketManagerProducer(I2PSocketManager[] initialManagers, + int maxManagers, + boolean mcDonaldsMode, + String host, int port) { + if (maxManagers < 1) { + throw new IllegalArgumentException("maxManagers < 1"); + } + this.host=host; + this.port=port; + mcDonalds=mcDonaldsMode; + if (initialManagers != null) { + myManagers.addAll(Arrays.asList(initialManagers)); + } + this.maxManagers=maxManagers; + mcDonalds=mcDonaldsMode; + setDaemon(true); + start(); + } + + + /** + * Thread producing new SocketManagers. + */ + public void run() { + while (true) { + synchronized(this) { + // without mcDonalds mode, we most probably need no + // new managers. + while (!mcDonalds && myManagers.size() == maxManagers) { + myWait(); + } + } + // produce a new manager, regardless whether it is needed + // or not. Do not synchronized this part, since it can be + // quite time-consuming. + I2PSocketManager newManager = + I2PSocketManagerFactory.createManager(host, port, + new Properties()); + // when done, check if it is needed. + synchronized(this) { + while(myManagers.size() == maxManagers) { + myWait(); + } + myManagers.add(newManager); + notifyAll(); + } + } + } + + /** + * Get a manager for connecting to a given destination. Each + * destination will always get the same manager. + * + * @param dest the destination to connect to + * @return the SocketManager to use + */ + public synchronized I2PSocketManager getManager(String dest) { + I2PSocketManager result = (I2PSocketManager) usedManagers.get(dest); + if (result == null) { + result = getManager(); + usedManagers.put(dest,result); + } + return result; + } + + /** + * Get a "new" SocketManager. Depending on the anonymity settings, + * this can be a completely new one or one randomly selected from + * a pool. + * + * @return the SocketManager to use + */ + public synchronized I2PSocketManager getManager() { + while (myManagers.size() == 0) { + myWait(); // no manager here, so wait until one is produced + } + int which = (int)(Math.random()*myManagers.size()); + I2PSocketManager result = (I2PSocketManager) myManagers.get(which); + if (mcDonalds) { + myManagers.remove(which); + notifyAll(); + } + return result; + } + + public void myWait() { + try { + wait(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/filter/ChainFilter.java b/apps/httptunnel/java/src/net/i2p/httptunnel/filter/ChainFilter.java new file mode 100644 index 0000000000..14d240187f --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/filter/ChainFilter.java @@ -0,0 +1,50 @@ +package net.i2p.httptunnel.filter; + +import net.i2p.util.Log; + +import java.io.*; +import java.util.*; + +/** + * Chain multiple filters. Decorator pattern... + */ +public class ChainFilter implements Filter { + + private static final Log _log = new Log(ChainFilter.class); + + public Collection filters; + + public ChainFilter(Collection filters) { + this.filters=filters; + } + + public byte[] filter(byte[] toFilter) { + byte[] buf = toFilter; + for (Iterator it = filters.iterator(); it.hasNext();) { + Filter f = (Filter) it.next(); + buf = f.filter(buf); + } + return buf; + } + + public byte[] finish() { + // this is a bit complicated. Think about it... + try { + byte[] buf = EMPTY; + for (Iterator it = filters.iterator(); it.hasNext();) { + Filter f = (Filter) it.next(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if (buf.length != 0) { + baos.write(f.filter(buf)); + } + baos.write(f.finish()); + buf = baos.toByteArray(); + } + return buf; + } catch (IOException ex) { + _log.error("Error chaining filters", ex); + return EMPTY; + } + } + +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/filter/Filter.java b/apps/httptunnel/java/src/net/i2p/httptunnel/filter/Filter.java new file mode 100644 index 0000000000..200059a354 --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/filter/Filter.java @@ -0,0 +1,22 @@ +package net.i2p.httptunnel.filter; + +/** + * A generic filtering interface. + */ +public interface Filter { + + /** + * An empty byte array. + */ + public static final byte[] EMPTY = new byte[0]; + + /** + * Filter some data. Not all filtered data need to be returned. + */ + public byte[] filter(byte[] toFilter); + + /** + * Data stream has finished. Return all of the rest data. + */ + public byte[] finish(); +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/filter/NullFilter.java b/apps/httptunnel/java/src/net/i2p/httptunnel/filter/NullFilter.java new file mode 100644 index 0000000000..2ea1833c85 --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/filter/NullFilter.java @@ -0,0 +1,15 @@ +package net.i2p.httptunnel.filter; + +/** + * A filter letting everything pass as is. + */ +public class NullFilter implements Filter { + + public byte[] filter(byte[] toFilter) { + return toFilter; + } + + public byte[] finish() { + return EMPTY; + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/handler/EepHandler.java b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/EepHandler.java new file mode 100644 index 0000000000..31e9c69b2a --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/EepHandler.java @@ -0,0 +1,93 @@ +package net.i2p.httptunnel.handler; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import net.i2p.I2PException; +import net.i2p.client.naming.NamingService; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.client.streaming.I2PSocketOptions; +import net.i2p.data.Destination; +import net.i2p.httptunnel.HTTPListener; +import net.i2p.httptunnel.Request; +import net.i2p.httptunnel.SocketManagerProducer; +import net.i2p.httptunnel.filter.Filter; +import net.i2p.httptunnel.filter.NullFilter; +import net.i2p.util.Log; + +/** + * Handler for browsing Eepsites. + */ +public class EepHandler { + + private static final Log _log = new Log(EepHandler.class); + + protected ErrorHandler errorHandler; + + /* package private */ EepHandler(ErrorHandler eh) { + errorHandler=eh; + } + + public void handle(Request req, HTTPListener httpl, OutputStream out, + boolean fromProxy, String destination) + throws IOException { + SocketManagerProducer smp = httpl.getSMP(); + Destination dest = NamingService.getInstance().lookup(destination); + if (dest == null) { + errorHandler.handle(req, httpl, out, + "Could not lookup host: "+destination); + return; + } + I2PSocketManager sm = smp.getManager(destination); + Filter f = new NullFilter(); //FIXME: use other filter + req.setParam("Host: ", dest.toBase64()); + if (!handle(req, f, out, dest, sm)) { + errorHandler.handle(req, httpl, out, "Unable to reach peer"); + } + } + + public boolean handle(Request req, Filter f, OutputStream out, + Destination dest, I2PSocketManager sm) + throws IOException { + I2PSocket s = null; + boolean written = false; + try { + synchronized(sm) { + s = sm.connect(dest, new I2PSocketOptions()); + } + InputStream in = new BufferedInputStream(s.getInputStream()); + OutputStream sout = new BufferedOutputStream(s.getOutputStream()); + sout.write(req.toByteArray()); + sout.flush(); + byte[] buffer = new byte[16384], filtered; + int len; + while ((len=in.read(buffer)) != -1) { + if (len != buffer.length) { + byte[] b2 = new byte[len]; + System.arraycopy(buffer, 0, b2, 0, len); + filtered=f.filter(b2); + } else { + filtered=f.filter(buffer); + } + written=true; + out.write(filtered); + } + filtered=f.finish(); + written=true; + out.write(filtered); + out.flush(); + } catch (IOException ex) { + _log.error("Error while handling eepsite request"); + return written; + } catch (I2PException ex) { + _log.error("Error while handling eepsite request"); + return written; + } finally { + if (s != null) s.close(); + } + return true; + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/handler/ErrorHandler.java b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/ErrorHandler.java new file mode 100644 index 0000000000..fe63256cdd --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/ErrorHandler.java @@ -0,0 +1,36 @@ +package net.i2p.httptunnel.handler; +import java.io.IOException; +import java.io.OutputStream; + +import net.i2p.httptunnel.HTTPListener; +import net.i2p.httptunnel.Request; +import net.i2p.util.Log; + +/** + * Handler for general error messages. + */ +public class ErrorHandler { + + private static final Log _log = new Log(ErrorHandler.class); + + /* package private */ ErrorHandler() { + + } + + public void handle(Request req, HTTPListener httpl, + OutputStream out, String error) throws IOException { + // FIXME: Make nicer messages for more likely errors. + out.write(("HTTP/1.1 500 Internal Server Error\r\n"+ + "Content-Type: text/html; charset=iso-8859-1\r\n\r\n") + .getBytes("ISO-8859-1")); + out.write((""+error+"

"+ + error+"

An internal error occurred while "+ + "handling a request by HTTPTunnel:
"+error+ + "

Complete request:

---
\r\n")
+		  .getBytes("ISO-8859-1"));
+	out.write(req.toByteArray());
+	out.write(("

---") + .getBytes("ISO-8859-1")); + out.flush(); + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/handler/LocalHandler.java b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/LocalHandler.java new file mode 100644 index 0000000000..9632edacb3 --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/LocalHandler.java @@ -0,0 +1,49 @@ +package net.i2p.httptunnel.handler; +import java.io.IOException; +import java.io.OutputStream; + +import net.i2p.httptunnel.HTTPListener; +import net.i2p.httptunnel.Request; +import net.i2p.util.Log; + +/** + * Handler for requests that do not require any connection to anyone + * (except errors). + */ +public class LocalHandler { + + private static final Log _log = new Log(LocalHandler.class); + + /* package private */ LocalHandler() { + + } + + public void handle(Request req, HTTPListener httpl, OutputStream out, + boolean fromProxy) throws IOException { + //FIXME: separate multiple pages, not only a start page + //FIXME: provide some info on this page + out.write(("HTTP/1.1 200 Document following\r\n"+ + "Content-Type: text/html; charset=iso-8859-1\r\n\r\n"+ + "Welcome to I2P HTTPTunnel"+ + "

Welcome to I2P HTTPTunnel

You can "+ + "browse Eepsites by adding an eepsite name to the request."+ + "").getBytes("ISO-8859-1")); + out.flush(); + } + + public void handleProxyConfWarning(Request req, HTTPListener httpl, + OutputStream out) throws IOException { + //FIXME + throw new IOException("jrandom ate the deprecated method. mooo"); + //httpl.handleNotImplemented(out); + + } + + public void handleHTTPWarning(Request req, HTTPListener httpl, + OutputStream out, boolean fromProxy) + throws IOException { + // FIXME + throw new IOException("jrandom ate the deprecated method. mooo"); + //httpl.handleNotImplemented(out); + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/handler/ProxyHandler.java b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/ProxyHandler.java new file mode 100644 index 0000000000..5177dfcf62 --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/ProxyHandler.java @@ -0,0 +1,47 @@ +package net.i2p.httptunnel.handler; +import java.io.IOException; +import java.io.OutputStream; + +import net.i2p.client.naming.NamingService; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.data.Destination; +import net.i2p.httptunnel.HTTPListener; +import net.i2p.httptunnel.Request; +import net.i2p.httptunnel.SocketManagerProducer; +import net.i2p.httptunnel.filter.Filter; +import net.i2p.httptunnel.filter.NullFilter; +import net.i2p.util.Log; + +/** + * Handler for proxying "normal" HTTP requests. + */ +public class ProxyHandler extends EepHandler { + + private static final Log _log = new Log(ErrorHandler.class); + + /* package private */ ProxyHandler(ErrorHandler eh) { + super(eh); + } + + public void handle(Request req, HTTPListener httpl, OutputStream out, + boolean fromProxy) throws IOException { + SocketManagerProducer smp = httpl.getSMP(); + Destination dest = findProxy(); + if (dest == null) { + errorHandler.handle(req, httpl, out, + "Could not find proxy"); + return; + } + // one manager for all proxy requests + I2PSocketManager sm = smp.getManager("--proxy--"); + Filter f = new NullFilter(); //FIXME: use other filter + if (!handle(req, f, out, dest, sm)) { + errorHandler.handle(req, httpl, out, "Unable to reach peer"); + } + } + + private Destination findProxy() { + //FIXME! + return NamingService.getInstance().lookup("squid.i2p"); + } +} diff --git a/apps/httptunnel/java/src/net/i2p/httptunnel/handler/RootHandler.java b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/RootHandler.java new file mode 100644 index 0000000000..154a172db8 --- /dev/null +++ b/apps/httptunnel/java/src/net/i2p/httptunnel/handler/RootHandler.java @@ -0,0 +1,109 @@ +package net.i2p.httptunnel.handler; +import java.io.IOException; +import java.io.OutputStream; + +import net.i2p.httptunnel.HTTPListener; +import net.i2p.httptunnel.Request; +import net.i2p.util.Log; + +/** + * Main handler for all requests. Dispatches requests to other handlers. + */ +public class RootHandler { + + private static final Log _log = new Log(RootHandler.class); + + private RootHandler() { + errorHandler=new ErrorHandler(); + localHandler=new LocalHandler(); + proxyHandler=new ProxyHandler(errorHandler); + eepHandler=new EepHandler(errorHandler); + } + + private ErrorHandler errorHandler; + private ProxyHandler proxyHandler; + private LocalHandler localHandler; + private EepHandler eepHandler; + + private static RootHandler instance; + + public static synchronized RootHandler getInstance() { + if (instance == null) { + instance = new RootHandler(); + } + return instance; + } + + public void handle(Request req, HTTPListener httpl, + OutputStream out) throws IOException { + String url=req.getURL(); + System.out.println(url); + boolean byProxy = false; + int pos; + if (url.startsWith("http://")) { // access via proxy + byProxy=true; + if (httpl.firstProxyUse()) { + localHandler.handleProxyConfWarning(req,httpl,out); + return; + } + url = url.substring(7); + pos = url.indexOf("/"); + String host; + String rest; + if (pos == -1) { + errorHandler.handle(req, httpl, out, "No host end in URL"); + return; + } else { + host = url.substring(0,pos); + url = url.substring(pos); + if ("i2p".equals(host) || "i2p.i2p".equals(host)) { + // normal request; go on below... + } else if (host.endsWith(".i2p")) { + // "old" service request, send a redirect... + out.write(("HTTP/1.1 302 Moved\r\nLocation: "+ + "http://i2p.i2p/"+host+url+ + "\r\n\r\n").getBytes("ISO-8859-1")); + return; + } else { + // this is for proxying to the real web + proxyHandler.handle(req, httpl, out, true); + return; + } + } + } + if (url.equals("/")) { // main page + url="/_/local/index"; + } else if (!url.startsWith("/")) { + errorHandler.handle(req, httpl, out, + "No leading slash in URL: "+url); + return; + } + String dest; + url=url.substring(1); + pos = url.indexOf("/"); + if (pos == -1) { + dest=url; + url="/"; + } else { + dest = url.substring(0,pos); + url=url.substring(pos); + } + req.setURL(url); + if (dest.equals("_")) { // no eepsite + if (url.startsWith("/local/")) { // local request + req.setURL(url.substring(6)); + localHandler.handle(req, httpl, out, byProxy); + } else if (url.startsWith("/http/")) { // http warning + localHandler.handleHTTPWarning(req, httpl, out, byProxy); + } else if (url.startsWith("/proxy/")) { // http proxying + req.setURL("http://"+url.substring(7)); + proxyHandler.handle(req, httpl, out, byProxy); + } else { + errorHandler.handle(req, httpl, out, + "No local handler for this URL: "+url); + } + } else { + eepHandler.handle(req, httpl, out, byProxy, dest); + } + } +} diff --git a/apps/i2ptunnel/doc/COPYING b/apps/i2ptunnel/doc/COPYING new file mode 100644 index 0000000000..5ec43ee156 --- /dev/null +++ b/apps/i2ptunnel/doc/COPYING @@ -0,0 +1,278 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. diff --git a/apps/i2ptunnel/doc/readme.license.txt b/apps/i2ptunnel/doc/readme.license.txt new file mode 100644 index 0000000000..2adca9796c --- /dev/null +++ b/apps/i2ptunnel/doc/readme.license.txt @@ -0,0 +1,11 @@ +$Id$ + +the i2p/apps/i2ptunnel module is the root of the +I2PTunnel application, and everything within it +is released according to the terms of the I2P +license policy. That means everything contained +within the i2p/apps/i2ptunnel module is released +under the GPL plus the java exception unless +otherwise marked. Alternate licenses that may be +used include BSD, Cryptix, and MIT, as well as +code granted into the public domain. diff --git a/apps/i2ptunnel/doc/readme.txt b/apps/i2ptunnel/doc/readme.txt new file mode 100644 index 0000000000..19e4c8844d --- /dev/null +++ b/apps/i2ptunnel/doc/readme.txt @@ -0,0 +1,21 @@ +I2PTunnel allows to tunnel a usual TCP connection over I2P. + +A server needs to have a public key; a client connects to that. + +Starting a client: + +tunnel + localport: the port where you want to connect to + pubkey: the public key of the server + + +Starting a server: + +tunnel + remotehost: the host to connect to when a connection comes. + remoteport: the port to connect to when a connection comes. + privkey: your private (secret) key + +To stop a client/server, hit + +Have fun! \ No newline at end of file diff --git a/apps/i2ptunnel/java/build.xml b/apps/i2ptunnel/java/build.xml new file mode 100644 index 0000000000..8f26dc78a4 --- /dev/null +++ b/apps/i2ptunnel/java/build.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/BufferLogger.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/BufferLogger.java new file mode 100644 index 0000000000..e9642bf092 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/BufferLogger.java @@ -0,0 +1,61 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Read what i2ptunnel logs, and expose it in a buffer + * + */ +class BufferLogger implements Logging { + private final static Log _log = new Log(BufferLogger.class); + private ByteArrayOutputStream _baos; + private boolean _ignore; + + public BufferLogger() { + _baos = new ByteArrayOutputStream(512); + _ignore = false; + } + + private final static String EMPTY = ""; + public String getBuffer() { + if (_ignore) return EMPTY; + else return new String(_baos.toByteArray()); + } + + /** + * We don't care about anything else the logger receives. This is useful + * for loggers passed in to servers and clients, since they will continue + * to add info to the logger, but if we're instantiated by the tunnel manager, + * its likely we only care about the first few messages it sends us. + * + */ + public void ignoreFurtherActions() { + _ignore = true; + synchronized (_baos) { + _baos.reset(); + } + _baos = null; + } + + /** + * Pass in some random data + */ + public void log(String s) { + if (_ignore) return; + if (s != null) { + _log.debug("logging [" + s + "]"); + try { + _baos.write(s.getBytes()); + _baos.write('\n'); + } catch (IOException ioe) { + _log.error("Error logging [" + s + "]"); + } + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java new file mode 100644 index 0000000000..e53b3075f0 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java @@ -0,0 +1,998 @@ +/* + * I2PTunnel + * (c) 2003 - 2004 mihi + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * In addition, as a special exception, mihi gives permission to link + * the code of this program with the proprietary Java implementation + * provided by Sun (or other vendors as well), and distribute linked + * combinations including the two. You must obey the GNU General + * Public License in all respects for all of the code used other than + * the proprietary Java implementation. If you modify this file, you + * may extend this exception to your version of the file, but you are + * not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ +package net.i2p.i2ptunnel; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; + +import net.i2p.I2PException; +import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PClientFactory; +import net.i2p.client.naming.NamingService; +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.util.EventDispatcher; +import net.i2p.util.EventDispatcherImpl; +import net.i2p.util.Log; + + +public class I2PTunnel implements Logging, EventDispatcher { + private final static Log _log = new Log(I2PTunnel.class); + private final EventDispatcherImpl _event = new EventDispatcherImpl(); + + public static final int PACKET_DELAY=100; + + public static boolean ownDest = false; + + public static String port = + System.getProperty(I2PClient.PROP_TCP_PORT, "7654"); + public static String host = System.getProperty + (I2PClient.PROP_TCP_HOST,"127.0.0.1"); + public static String listenHost = host; + + private static final String nocli_args[] = {"-nocli", "-die"}; + + private List tasks=new ArrayList(); + private int next_task_id = 1; + + private Set listeners = new HashSet(); + + public static void main(String[] args) throws IOException { + new I2PTunnel(args); + } + + public I2PTunnel() { + this(nocli_args); + } + + public I2PTunnel(String[] args) { + this(args, null); + } + public I2PTunnel(String[] args, ConnectionEventListener lsnr) { + addConnectionEventListener(lsnr); + boolean gui=true; + boolean checkRunByE = true;; + boolean cli=true; + boolean dontDie = true; + for(int i=0;i"); + String cmd = r.readLine(); + if (cmd == null) break; + runCommand(cmd, this); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + while (dontDie) { + synchronized (this) { + try { wait(); } catch (InterruptedException ie) {} + } + } + } + + private void addtask (I2PTunnelTask tsk) + { + tsk.setTunnel(this); + if (tsk.isOpen()) { + tsk.setId(next_task_id); + next_task_id++; + synchronized (tasks) { + tasks.add(tsk); + } + } + } + + /** java 1.3 vs 1.4 :) + */ + private static String[] split(String src, String delim) { + StringTokenizer tok = new StringTokenizer(src, delim); + String vals[] = new String[tok.countTokens()]; + for (int i = 0; i < vals.length; i++) + vals[i] = tok.nextToken(); + return vals; + } + public void runCommand(String cmd, Logging l) { + if (cmd.indexOf(" ")== -1) cmd+=" "; + int iii=cmd.indexOf(" "); + String cmdname= cmd.substring(0,iii).toLowerCase(); + String allargs = cmd.substring(iii+1); + String[] args = split(allargs, " "); // .split(" "); // java 1.4 + + if ("help".equals(cmdname)) { + runHelp(l); + } else if ("server".equals(cmdname)) { + runServer(args, l); + } else if ("textserver".equals(cmdname)) { + runTextServer(args, l); + } else if ("client".equals(cmdname)) { + runClient(args, l); + } else if ("httpclient".equals(cmdname)) { + runHttpClient(args, l); + } else if ("sockstunnel".equals(cmdname)) { + runSOCKSTunnel(args, l); + } else if ("config".equals(cmdname)) { + runConfig(args, l); + } else if ("listen_on".equals(cmdname)) { + runListenOn(args, l); + } else if ("genkeys".equals(cmdname)) { + runGenKeys(args, l); + } else if ("gentextkeys".equals(cmdname)) { + runGenTextKeys(l); + } else if (cmdname.equals("quit")) { + runQuit(l); + } else if (cmdname.equals("list")) { + runList(l); + } else if (cmdname.equals("close")) { + runClose(args, l); + } else if (cmdname.equals("run")) { + runRun(args, l); + } else if (cmdname.equals("lookup")) { + runLookup(args, l); + } else if (cmdname.equals("ping")) { + runPing(allargs, l); + } else if (cmdname.equals("owndest")) { + runOwnDest(args, l); + } else { + l.log("Unknown command [" + cmdname + "]"); + } + } + + /** + * Display help information through the given logger. + * + * Does not fire any events to the logger + * + * @param l logger to receive events and output + */ + public void runHelp(Logging l) { + l.log("Command list:"); + l.log("config "); + l.log("listen_on "); + l.log("owndest yes|no"); + l.log("ping "); + l.log("server "); + l.log("textserver "); + l.log("genkeys []"); + l.log("gentextkeys"); + l.log("client |file:"); + l.log("httpclient "); + l.log("lookup "); + l.log("quit"); + l.log("close [forced] |all"); + l.log("list"); + l.log("run "); + } + + /** + * Run the server pointing at the host and port specified using the private i2p + * destination loaded from the specified file + * + * Sets the event "serverTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error) + * Also sets the event "openServerResult" = "ok" or "error" (displaying "Ready!" on the logger after + * 'ok'). So, success = serverTaskId != -1 and openServerResult = ok. + * + * @param args {hostname, portNumber, privKeyFilename} + * @param l logger to receive events and output + */ + public void runServer(String args[], Logging l) { + if (args.length==3) { + InetAddress serverHost = null; + int portNum = -1; + File privKeyFile = null; + try { + serverHost = InetAddress.getByName(args[0]); + } catch (UnknownHostException uhe) { + l.log("unknown host"); + _log.error("Error resolving " + args[0], uhe); + notifyEvent("serverTaskId", new Integer(-1)); + return; + } + + try { + portNum = Integer.parseInt(args[1]); + } catch (NumberFormatException nfe) { + l.log("invalid port"); + _log.error("Port specified is not valid: " + args[1], nfe); + notifyEvent("serverTaskId", new Integer(-1)); + return; + } + + privKeyFile = new File(args[2]); + if (!privKeyFile.canRead()) { + l.log("private key file does not exist"); + _log.error("Private key file does not exist or is not readable: " + args[2]); + notifyEvent("serverTaskId", new Integer(-1)); + return; + } + I2PTunnelTask task; + task = new I2PTunnelServer(serverHost, portNum, privKeyFile, + args[2], l, (EventDispatcher)this); + addtask(task); + notifyEvent("serverTaskId", new Integer(task.getId())); + return; + } else { + l.log("server "); + l.log(" creates a server that sends all incoming data\n"+ + " of its destination to host:port."); + notifyEvent("serverTaskId", new Integer(-1)); + } + } + + /** + * Run the server pointing at the host and port specified using the private i2p + * destination loaded from the given base64 stream + * + * Sets the event "serverTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error) + * Also sets the event "openServerResult" = "ok" or "error" (displaying "Ready!" on the logger after + * 'ok'). So, success = serverTaskId != -1 and openServerResult = ok. + * + * @param args {hostname, portNumber, privKeyBase64} + * @param l logger to receive events and output + */ + public void runTextServer(String args[], Logging l) { + if (args.length==3) { + InetAddress serverHost = null; + int portNum = -1; + try { + serverHost = InetAddress.getByName(args[0]); + } catch (UnknownHostException uhe) { + l.log("unknown host"); + _log.error("Error resolving " + args[0], uhe); + notifyEvent("serverTaskId", new Integer(-1)); + return; + } + + try { + portNum = Integer.parseInt(args[1]); + } catch (NumberFormatException nfe) { + l.log("invalid port"); + _log.error("Port specified is not valid: " + args[1], nfe); + notifyEvent("serverTaskId", new Integer(-1)); + return; + } + + I2PTunnelTask task; + task = new I2PTunnelServer(serverHost, portNum, args[2], l, + (EventDispatcher)this); + addtask(task); + notifyEvent("serverTaskId", new Integer(task.getId())); + } else { + l.log("textserver "); + l.log(" creates a server that sends all incoming data\n"+ + " of its destination to host:port."); + notifyEvent("textserverTaskId", new Integer(-1)); + } + } + + /** + * Run the client on the given port number pointing at the specified destination + * (either the base64 of the destination or file:fileNameContainingDestination) + * + * Sets the event "clientTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error) + * Also sets the event "openClientResult" = "error" or "ok" (before setting the value to "ok" it also + * adds "Ready! Port #" to the logger as well). In addition, it will also set "clientLocalPort" = + * Integer port number if the client is listening + * + * @param args {portNumber, destinationBase64 or "file:filename"} + * @param l logger to receive events and output + */ + public void runClient(String args[], Logging l) { + if (args.length==2) { + int port = -1; + try { + port = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + l.log("invalid port"); + _log.error("Port specified is not valid: " + args[0], nfe); + notifyEvent("clientTaskId", new Integer(-1)); + return; + } + I2PTunnelTask task; + task = new I2PTunnelClient(port, args[1], l, ownDest, + (EventDispatcher)this); + addtask(task); + notifyEvent("clientTaskId", new Integer(task.getId())); + } else { + l.log("client |file:"); + l.log(" creates a client that forwards port to the pubkey.\n"+ + " use 0 as port to get a free port assigned."); + notifyEvent("clientTaskId", new Integer(-1)); + } + } + + /** + * Run an HTTP client on the given port number + * + * Sets the event "httpclientTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error). + * Also sets "httpclientStatus" = "ok" or "error" after the client tunnel has started. + * + * @param args {portNumber and (optionally) proxy to be used for the WWW} + * @param l logger to receive events and output + */ + public void runHttpClient(String args[], Logging l) { + if (args.length >= 1 && args.length <= 2) { + int port = -1; + try { + port = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + l.log("invalid port"); + _log.error("Port specified is not valid: " + args[0], nfe); + notifyEvent("httpclientTaskId", new Integer(-1)); + return; + } + + String proxy = "squid.i2p"; + if (args.length == 2) { + proxy = args[1]; + } + I2PTunnelTask task; + task = new I2PTunnelHTTPClient(port, l, ownDest, proxy, + (EventDispatcher)this); + addtask(task); + notifyEvent("httpclientTaskId", new Integer(task.getId())); + } else { + l.log("httpclient []"); + l.log(" creates a client that distributes HTTP requests."); + l.log(" (optional) indicates a proxy server to be used"); + l.log(" when trying to access an address out of the .i2p domain"); + l.log(" (the default proxy is squid.i2p)."); + notifyEvent("httpclientTaskId", new Integer(-1)); + } + } + + /** + * Run an SOCKS tunnel on the given port number + * + * Sets the event "sockstunnelTaskId" = Integer(taskId) after the + * tunnel has been started (or -1 on error). Also sets + * "openSOCKSTunnelResult" = "ok" or "error" after the client tunnel has + * started. + * + * @param args {portNumber} + * @param l logger to receive events and output + */ + public void runSOCKSTunnel(String args[], Logging l) { + if (args.length >= 1 && args.length <= 2) { + int port = -1; + try { + port = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + l.log("invalid port"); + _log.error("Port specified is not valid: " + args[0], nfe); + notifyEvent("sockstunnelTaskId", new Integer(-1)); + return; + } + + I2PTunnelTask task; + task = new I2PSOCKSTunnel(port, l, ownDest, (EventDispatcher)this); + addtask(task); + notifyEvent("sockstunnelTaskId", new Integer(task.getId())); + } else { + l.log("sockstunnel "); + l.log(" creates a tunnel that distributes SOCKS requests."); + notifyEvent("sockstunnelTaskId", new Integer(-1)); + } + } + + /** + * Specify the i2cp host and port + * + * Sets the event "configResult" = "ok" or "error" after the configuration has been specified + * + * @param args {hostname, portNumber} + * @param l logger to receive events and output + */ + public void runConfig(String args[], Logging l) { + if (args.length==2) { + host=args[0]; + listenHost=host; + port=args[1]; + notifyEvent("configResult", "ok"); + } else { + l.log("config "); + l.log(" sets the connection to the i2p router."); + notifyEvent("configResult", "error"); + } + } + + /** + * Specify whether to use its own destination for each outgoing tunnel + * + * Sets the event "owndestResult" = "ok" or "error" after the configuration has been specified + * + * @param args {yes or no} + * @param l logger to receive events and output + */ + public void runOwnDest(String args[], Logging l) { + if (args.length==1 && + (args[0].equalsIgnoreCase("yes") + || args[0].equalsIgnoreCase("no"))) { + ownDest = args[0].equalsIgnoreCase("yes"); + notifyEvent("owndestResult", "ok"); + } else { + l.log("owndest yes|no"); + l.log(" Specifies whether to use its own destination \n"+ + " for each outgoing tunnel"); + notifyEvent("owndestResult", "error"); + } + } + + /** + * Specify the hostname / IP address of the interface that the tunnels should bind to + * + * Sets the event "listen_onResult" = "ok" or "error" after the interface has been specified + * + * @param args {hostname} + * @param l logger to receive events and output + */ + public void runListenOn(String args[], Logging l) { + if (args.length==1) { + listenHost=args[0]; + notifyEvent("listen_onResult", "ok"); + } else { + l.log("listen_on "); + l.log(" sets the interface to listen for the I2PClient."); + notifyEvent("listen_onResult", "ok"); + } + } + + /** + * Generate a new keypair + * + * Sets the event "genkeysResult" = "ok" or "error" after the generation is complete + * + * @param args {privateKeyFilename, publicKeyFilename} or {privateKeyFilename} + * @param l logger to receive events and output + */ + public void runGenKeys(String args[], Logging l) { + OutputStream pubdest=null; + if (args.length == 2) { + try { + pubdest=new FileOutputStream(args[1]); + } catch (IOException ioe) { + l.log("Error opening output stream"); + _log.error("Error generating keys to out", ioe); + notifyEvent("genkeysResult", "error"); + return; + } + } else if (args.length != 1) { + l.log("genkeys []"); + l.log(" creates a new keypair and prints the public key.\n"+ + " if pubkeyfile is given, saves the public key there."+ + "\n"+ + " if the privkeyfile already exists, just print/save"+ + "the pubkey."); + notifyEvent("genkeysResult", "error"); + } + try { + File privKeyFile = new File(args[0]); + if (privKeyFile.exists()) { + l.log("File already exists."); + showKey(new FileInputStream(privKeyFile), pubdest, l); + } else { + makeKey(new FileOutputStream(privKeyFile), pubdest, l); + } + notifyEvent("genkeysResult", "ok"); + } catch (IOException ioe) { + l.log("Error generating keys - " + ioe.getMessage()); + notifyEvent("genkeysResult", "error"); + _log.error("Error generating keys", ioe); + } + } + + /** + * Generate a new keypair + * + * Sets the event "privateKey" = base64 of the privateKey stream and + * sets the event "publicDestination" = base64 of the destination + * + * @param l logger to receive events and output + */ + public void runGenTextKeys(Logging l) { + ByteArrayOutputStream privkey = new ByteArrayOutputStream(512); + ByteArrayOutputStream pubkey = new ByteArrayOutputStream(512); + makeKey(privkey, pubkey, l); + l.log("Private key: "+Base64.encode(privkey.toByteArray())); + notifyEvent("privateKey", Base64.encode(privkey.toByteArray())); + notifyEvent("publicDestination", Base64.encode(pubkey.toByteArray())); + } + + /** + * Exit the JVM if there are no more tasks left running. If there are tunnels + * running, it returns. + * + * Sets the event "quitResult" = "error" if there are tasks running (but if there + * aren't, well, there's no point in setting the quitResult to "ok", now is there?) + * + * @param l logger to receive events and output + */ + public void runQuit(Logging l) { + purgetasks(l); + synchronized (tasks) { + if (tasks.isEmpty()) { + System.exit(0); + } + } + l.log("There are running tasks. Try 'list'."); + notifyEvent("quitResult", "error"); + } + + /** + * Retrieve a list of currently running tasks + * + * Sets the event "listDone" = "done" after dumping the tasks to + * the logger + * + * @param l logger to receive events and output + */ + public void runList(Logging l) { + purgetasks(l); + synchronized (tasks) { + for (int i=0;i 2) { + l.log("close [forced] |all"); + l.log(" stop running tasks. either only one or all.\n"+ + " use 'forced' to also stop tasks with active connections.\n"+ + " use the 'list' command to show the job numbers"); + notifyEvent("closeResult", "error"); + } else { + int argindex=0; // parse optional 'forced' keyword + boolean forced=false; + if (args[argindex].equalsIgnoreCase("forced")) { + forced=true; + argindex++; + } + if (args[argindex].equalsIgnoreCase("all")) { + List curTasks = null; + synchronized (tasks) { + curTasks = new LinkedList(tasks); + } + + boolean error = false; + for (int i=0;i"); + l.log(" loads commandfile and runs each line in it. \n"+ + " You can also give the filename on the commandline."); + notifyEvent("runResult", "error"); + } + } + + /** + * Perform a lookup of the name specified + * + * Sets the event "lookupResult" = base64 of the destination, or an error message + * + * @param args {name} + * @param l logger to receive events and output + */ + public void runLookup(String args[], Logging l) { + if (args.length != 1) { + l.log("lookup "); + l.log(" try to resolve the name into a destination key"); + notifyEvent("lookupResult", "invalidUsage"); + } else { + String target = args[0]; + try { + Destination dest = destFromName(args[0]); + if (dest == null) { + l.log("Unknown host"); + notifyEvent("lookupResult", "unkown host"); + } else { + l.log(dest.toBase64()); + notifyEvent("lookupResult", dest.toBase64()); + } + } catch (DataFormatException dfe) { + l.log("Unknown or invalid host"); + notifyEvent("lookupResult", "invalid host"); + } + } + } + + /** + * Start up a ping task with the specified args (currently supporting -ns, -h, -l) + * + * Sets the event "pingTaskId" = Integer of the taskId, or -1 + * + * @param allargs arguments to pass to the I2Ping task + * @param l logger to receive events and output + */ + public void runPing(String allargs, Logging l) { + if(allargs.length() != 0) { + I2PTunnelTask task; + // pings always use the main destination + task = new I2Ping(allargs, l, false, (EventDispatcher)this); + addtask(task); + notifyEvent("pingTaskId", new Integer(task.getId())); + } else { + l.log("ping "); + l.log("ping -h"); + l.log("ping -l "); + l.log(" Tests communication with peers.\n"+ + " opts can be -ns (nosync) or not."); + notifyEvent("pingTaskId", new Integer(-1)); + } + } + + /** + * Helper method to actually close the given task number (optionally forcing + * closure) + * + */ + private boolean closetask(int num, boolean forced, Logging l) { + boolean closed = false; + + _log.debug("closetask(): looking for task " + num); + synchronized (tasks) { + for (Iterator it=tasks.iterator(); it.hasNext();) { + I2PTunnelTask t = (I2PTunnelTask) it.next(); + int id = t.getId(); + _log.debug("closetask(): parsing task " + id + " (" + + t.toString() + ")"); + if (id == num) { + closed = closetask(t, forced, l); + break; + } else if (id > num) { + break; + } + } + } + return closed; + } + + /** + * Helper method to actually close the given task number + * (optionally forcing closure) + * + */ + private boolean closetask(I2PTunnelTask t, boolean forced, Logging l) { + l.log("Closing task " + t.getId() + (forced ? " forced..." : "...")); + if (t.close(forced)) { + l.log("Task " + t.getId() + " closed."); + return true; + } + return false; + } + + /** + * Helper task to remove closed / completed tasks. + * + */ + private void purgetasks(Logging l) { + synchronized (tasks) { + for (Iterator it=tasks.iterator(); it.hasNext();) { + I2PTunnelTask t = (I2PTunnelTask) it.next(); + if (!t.isOpen()) { + _log.debug("Purging inactive tunnel: [" + + t.getId() + "] " + + t.toString()); + it.remove(); + } + } + } + } + + /** + * Log the given message (using both the logging subsystem and standard output...) + * + */ + public void log(String s) { + System.out.println(s); + _log.info("Display: " + s); + } + + /** + * Create a new destination, storing the destination and its private keys where + * instructed + * + * @param writeTo location to store the private keys + * @param pubDest location to store the destination + * @param l logger to send messages to + */ + public static void makeKey(OutputStream writeTo, OutputStream pubDest, + Logging l) { + try { + l.log("Generating new keys..."); + ByteArrayOutputStream priv = new ByteArrayOutputStream(), + pub = new ByteArrayOutputStream(); + I2PClient client = I2PClientFactory.createClient(); + Destination d = client.createDestination(writeTo); + l.log("Secret key saved."); + l.log("Public key: "+d.toBase64()); + writeTo.flush(); + writeTo.close(); + writePubKey(d, pubDest, l); + } catch (I2PException ex) { + ex.printStackTrace(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + /** + * Read in the given destination, display it, and write it to the given location + * + * @param readFrom stream to read the destination from + * @param pubDest stream to write the destination to + * @param l logger to send messages to + */ + public static void showKey(InputStream readFrom, OutputStream pubDest, + Logging l) { + try { + I2PClient client = I2PClientFactory.createClient(); + Destination d = new Destination(); + d.readBytes(readFrom); + l.log("Public key: "+d.toBase64()); + readFrom.close(); + writePubKey(d, pubDest, l); + } catch (I2PException ex) { + ex.printStackTrace(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + /** + * Write out the destination to the stream + * + * @param d Destination to write + * @param o stream to write the destination to + * @param l logger to send messages to + */ + private static void writePubKey(Destination d, OutputStream o, Logging l) + throws I2PException, IOException { + if (o==null) return; + d.writeBytes(o); + l.log("Public key saved."); + } + + /** + * Generates a Destination from a name. Now only supports base64 + * names - may support naming servers later. "file:" is + * also supported, where filename is a file that either contains a + * binary Destination structure or the Base64 encoding of that + * structure. + */ + public static Destination destFromName(String name) + throws DataFormatException { + + if ( (name == null) || (name.trim().length() <= 0) ) + throw new DataFormatException("Empty destination provided"); + + if (name.startsWith("file:")) { + Destination result=new Destination(); + byte content[] = null; + FileInputStream in = null; + try { + in = new FileInputStream(name.substring("file:".length())); + byte buf[] = new byte[1024]; + int read = DataHelper.read(in, buf); + content = new byte[read]; + System.arraycopy(buf, 0, content, 0, read); + } catch (IOException ioe) { + System.out.println(ioe.getMessage()); + return null; + } finally { + if (in != null) try { in.close(); } catch (IOException io) {} + } + try { + result.fromByteArray(content); + return result; + } catch (Exception ex) { + if (_log.shouldLog(Log.INFO)) + _log.info("File is not a binary destination - trying base64"); + try { + byte decoded[] = Base64.decode(new String(content)); + result.fromByteArray(decoded); + return result; + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("File is not a base64 destination either - failing!"); + return null; + } + } + } else { + // ask naming service + NamingService inst = NamingService.getInstance(); + return inst.lookup(name); + } + } + + public void addConnectionEventListener(ConnectionEventListener lsnr) { + if (lsnr == null) return; + synchronized (listeners) { + listeners.add(lsnr); + } + } + public void removeConnectionEventListener(ConnectionEventListener lsnr) { + if (lsnr == null) return; + synchronized (listeners) { + listeners.remove(lsnr); + } + } + + /** + * Call this whenever we lose touch with the router involuntarily (aka the router + * is off / crashed / etc) + * + */ + void routerDisconnected() { + _log.error("Router disconnected - firing notification events"); + synchronized (listeners) { + for (Iterator iter = listeners.iterator(); iter.hasNext();) { + ConnectionEventListener lsnr = (ConnectionEventListener)iter.next(); + if (lsnr != null) + lsnr.routerDisconnected(); + } + } + } + + /** + * Callback routine to find out + */ + public interface ConnectionEventListener { + public void routerDisconnected(); + } + + /* Required by the EventDispatcher interface */ + public EventDispatcher getEventDispatcher() { return _event; } + public void attachEventDispatcher(EventDispatcher e) { _event.attachEventDispatcher(e.getEventDispatcher()); } + public void detachEventDispatcher(EventDispatcher e) { _event.detachEventDispatcher(e.getEventDispatcher()); } + public void notifyEvent(String e, Object a) { _event.notifyEvent(e,a); } + public Object getEventValue(String n) { return _event.getEventValue(n); } + public Set getEvents() { return _event.getEvents(); } + public void ignoreEvents() { _event.ignoreEvents(); } + public void unIgnoreEvents() { _event.unIgnoreEvents(); } + public Object waitEventValue(String n) { return _event.waitEventValue(n); } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java new file mode 100644 index 0000000000..3780922e20 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java @@ -0,0 +1,61 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.net.Socket; + +import net.i2p.I2PException; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.util.EventDispatcher; +import net.i2p.util.Log; + +public class I2PTunnelClient extends I2PTunnelClientBase { + + private static final Log _log = new Log(I2PTunnelClient.class); + + protected Destination dest; + + public I2PTunnelClient(int localPort, String destination, + Logging l, boolean ownDest, + EventDispatcher notifyThis) { + super(localPort, ownDest, l, notifyThis, "SynSender"); + + if (waitEventValue("openBaseClientResult").equals("error")) { + notifyEvent("openClientResult", "error"); + return; + } + + try { + dest=I2PTunnel.destFromName(destination); + if (dest == null) { + l.log("Could not resolve " + destination + "."); + return; + } + } catch (DataFormatException e) { + l.log("Bad format in destination \"" + destination + "\"."); + notifyEvent("openClientResult", "error"); + return; + } + + setName(getLocalPort() + " -> " + destination); + + startRunning(); + + notifyEvent("openClientResult", "ok"); + } + + protected void clientConnectionRun(Socket s) { + try { + I2PSocket i2ps = createI2PSocket(dest); + new I2PTunnelRunner(s, i2ps, sockLock, null); + } catch (I2PException ex) { + _log.info("Error connecting", ex); + l.log("Unable to reach peer"); + // s has been initialized before the try block... + closeSocket(s); + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java new file mode 100644 index 0000000000..21cf31ed02 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java @@ -0,0 +1,292 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +import net.i2p.I2PException; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.client.streaming.I2PSocketManagerFactory; +import net.i2p.client.streaming.I2PSocketOptions; +import net.i2p.data.Destination; +import net.i2p.util.EventDispatcher; +import net.i2p.util.Log; + +public abstract class I2PTunnelClientBase extends I2PTunnelTask + implements Runnable { + + private static final Log _log = new Log(I2PTunnelClientBase.class); + protected Logging l; + + private static final long DEFAULT_CONNECT_TIMEOUT = 60*1000; + + protected Object sockLock = new Object(); // Guards sockMgr and mySockets + private I2PSocketManager sockMgr; + private List mySockets = new ArrayList(); + + protected Destination dest = null; + private int localPort; + + private boolean listenerReady = false; + + private ServerSocket ss; + + private Object startLock = new Object(); + private boolean startRunning = false; + + private Object closeLock = new Object(); + + private byte[] pubkey; + + private String handlerName; + + //public I2PTunnelClientBase(int localPort, boolean ownDest, + // Logging l) { + // I2PTunnelClientBase(localPort, ownDest, l, (EventDispatcher)null); + //} + + public I2PTunnelClientBase(int localPort, boolean ownDest, + Logging l, EventDispatcher notifyThis, + String handlerName) { + super(localPort+" (uninitialized)", notifyThis); + this.localPort=localPort; + this.l = l; + this.handlerName=handlerName; + + synchronized(sockLock) { + if (ownDest) { + sockMgr=buildSocketManager(); + } else { + sockMgr=getSocketManager(); + } + } + if (sockMgr == null) throw new NullPointerException(); + l.log("I2P session created"); + + Thread t = new Thread(this); + t.setName("Client"); + listenerReady=false; + t.start(); + open=true; + synchronized (this) { + while (!listenerReady) { + try { + wait(); + } + catch (InterruptedException e) { + // ignore + } + } + } + + if (open && listenerReady) { + l.log("Ready! Port " + getLocalPort()); + notifyEvent("openBaseClientResult", "ok"); + } else { + l.log("Error!"); + notifyEvent("openBaseClientResult", "error"); + } + } + + private static I2PSocketManager socketManager; + + protected static synchronized I2PSocketManager getSocketManager() { + if (socketManager == null) { + socketManager = buildSocketManager(); + } + return socketManager; + } + + protected static I2PSocketManager buildSocketManager() { + Properties props = new Properties(); + props.putAll(System.getProperties()); + return I2PSocketManagerFactory.createManager + (I2PTunnel.host, Integer.parseInt(I2PTunnel.port), props); + } + + public final int getLocalPort() { + return localPort; + } + + protected final InetAddress getListenHost(Logging l) { + try { + return InetAddress.getByName(I2PTunnel.listenHost); + } catch (UnknownHostException uhe) { + l.log("Could not find listen host to bind to [" + + I2PTunnel.host + "]"); + _log.error("Error finding host to bind", uhe); + notifyEvent("openBaseClientResult", "error"); + return null; + } + } + + /** + * Actually start working on incoming connections. *Must* be + * called by derived classes after initialization. + * + */ + public final void startRunning() { + synchronized (startLock) { + startRunning = true; + startLock.notify(); + } + } + + /** + * create the default options (using the default timeout, etc) + * + */ + private I2PSocketOptions getDefaultOptions() { + I2PSocketOptions opts = new I2PSocketOptions(); + opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT); + return opts; + } + + /** + * Create a new I2PSocket towards to the specified destination, + * adding it to the list of connections actually managed by this + * tunnel. + * + * @param dest The destination to connect to + * @return a new I2PSocket + */ + public I2PSocket createI2PSocket(Destination dest) throws I2PException { + return createI2PSocket(dest, getDefaultOptions()); + } + + /** + * Create a new I2PSocket towards to the specified destination, + * adding it to the list of connections actually managed by this + * tunnel. + * + * @param dest The destination to connect to + * @param opt Option to be used to open when opening the socket + * @return a new I2PSocket + */ + public I2PSocket createI2PSocket(Destination dest, I2PSocketOptions opt) throws I2PException { + I2PSocket i2ps; + + synchronized (sockLock) { + i2ps = sockMgr.connect(dest, opt); + mySockets.add(i2ps); + } + + return i2ps; + } + + public final void run() { + try { + InetAddress addr = getListenHost(l); + if (addr == null) return; + ss = new ServerSocket(localPort, 0, addr); + + // If a free port was requested, find out what we got + if (localPort == 0) { + localPort = ss.getLocalPort(); + } + notifyEvent("clientLocalPort", new Integer(ss.getLocalPort())); + l.log("Listening for clients on port " + localPort + + " of " + I2PTunnel.listenHost); + + // Notify constructor that port is ready + synchronized(this) { + listenerReady = true; + notify(); + } + + // Wait until we are authorized to process data + synchronized (startLock) { + while (!startRunning) { + try { + startLock.wait(); + } catch (InterruptedException ie) {} + } + } + + while (true) { + Socket s = ss.accept(); + manageConnection(s); + } + } catch (IOException ex) { + _log.error("Error listening for connections", ex); + notifyEvent("openBaseClientResult", "error"); + } + } + + /** + * Manage the connection just opened on the specified socket + * + * @param s Socket to take care of + */ + protected void manageConnection(Socket s) { + new ClientConnectionRunner(s, handlerName); + } + + + public boolean close(boolean forced) { + if (!open) return true; + // FIXME: here we might have to wait quite a long time if + // there is a connection attempt atm. But without waiting we + // might risk to create an orphan socket. Would be better + // to return with an error in that situation quickly. + synchronized(sockLock) { + mySockets.retainAll(sockMgr.listSockets()); + if (!forced && mySockets.size() != 0) { + l.log("There are still active connections!"); + _log.debug("can't close: there are still active connections!"); + for (Iterator it = mySockets.iterator(); it.hasNext();) { + l.log("->"+it.next()); + } + return false; + } + l.log("Closing client "+toString()); + try { + if (ss != null) ss.close(); + } catch (IOException ex) { + ex.printStackTrace(); + return false; + } + l.log("Client closed."); + open=false; + return true; + } + } + + public static void closeSocket(Socket s) { + try { + s.close(); + } catch (IOException ex) { + _log.error("Could not close socket", ex); + } + } + + public class ClientConnectionRunner extends Thread { + private Socket s; + + public ClientConnectionRunner(Socket s, String name) { + this.s=s; + setName(name); + start(); + } + + public void run() { + clientConnectionRun(s); + } + } + + /** + * Manage a connection in a separate thread. This only works if + * you do not override manageConnection() + */ + protected abstract void clientConnectionRun(Socket s); +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java new file mode 100644 index 0000000000..f032558bde --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java @@ -0,0 +1,48 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.Frame; +import java.awt.TextArea; +import java.awt.TextField; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * AWT gui since kaffe doesn't support swing yet + */ +public class I2PTunnelGUI extends Frame implements ActionListener, Logging { + + TextField input; + TextArea log; + I2PTunnel t; + + public I2PTunnelGUI(I2PTunnel t) { + super("I2PTunnel control panel"); + this.t=t; + setLayout(new BorderLayout()); + add("South", input=new TextField()); + input.addActionListener(this); + Font font = new Font("Monospaced",Font.PLAIN,12); + add("Center",log=new TextArea("",20,80,TextArea.SCROLLBARS_VERTICAL_ONLY)); + log.setFont(font); + log.setEditable(false); + log("enter 'help' for help."); + pack(); + show(); + } + + public void log(String s) { + log.append(s+"\n"); + } + + public void actionPerformed(ActionEvent evt) { + log("I2PTunnel>"+input.getText()); + t.runCommand(input.getText(), this); + log("---"); + input.setText(""); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java new file mode 100644 index 0000000000..5914410cc5 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -0,0 +1,334 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Date; + +import net.i2p.I2PException; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.util.Clock; +import net.i2p.util.EventDispatcher; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +public class I2PTunnelHTTPClient extends I2PTunnelClientBase + implements Runnable { + private static final Log _log = + new Log(I2PTunnelHTTPClient.class); + + private String wwwProxy; + + private final static byte[] ERR_REQUEST_DENIED = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html; charset=iso-8859-1\r\nCache-control: no-cache\r\n\r\n

I2P ERROR: REQUEST DENIED

You attempted to connect to a non-I2P website or location.
".getBytes(); + private final static byte[] ERR_DESTINATION_UNKNOWN = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html; charset=iso-8859-1\r\nCache-control: no-cache\r\n\r\n

I2P ERROR: NOT FOUND

That Desitination was not found. Perhaps you pasted in the wrong BASE64 I2P Destination or the link you are following is bad. The host (or the WWW proxy, if you're using one) could also be temporarily offline. Could not find the following Destination:

".getBytes(); + private final static byte[] ERR_TIMEOUT = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html; charset=iso-8859-1\r\nCache-control: no-cache\r\n\r\n

I2P ERROR: TIMEOUT

That Desitination was reachable, but timed out getting a response. This may be a temporary error, so you should simply try to refresh, though if the problem persists, the remote destination may have issues. Could not get a response from the following Destination:

".getBytes(); + + //public I2PTunnelHTTPClient(int localPort, Logging l, + // boolean ownDest, + // String wwwProxy) { + // I2PTunnelHTTPClient(localPort, l, ownDest, wwwProxy, + // (EventDispatcher)null); + //} + + public I2PTunnelHTTPClient(int localPort, Logging l, + boolean ownDest, + String wwwProxy, EventDispatcher notifyThis) { + super(localPort, ownDest, l, notifyThis, "HTTPHandler"); + + if (waitEventValue("openBaseClientResult").equals("error")) { + notifyEvent("openHTTPClientResult", "error"); + return; + } + + this.wwwProxy = wwwProxy; + + setName(getLocalPort() + + " -> HTTPClient [WWW outproxy: " + this.wwwProxy + "]"); + + startRunning(); + + notifyEvent("openHTTPClientResult", "ok"); + } + + protected void clientConnectionRun(Socket s) { + OutputStream out = null; + String targetRequest = null; + boolean usingWWWProxy = false; + InactivityTimeoutThread timeoutThread = null; + try { + out = s.getOutputStream(); + BufferedReader br = new BufferedReader + (new InputStreamReader(s.getInputStream(), + "ISO-8859-1")); + String line, method=null, protocol=null, host=null, destination=null; + StringBuffer newRequest=new StringBuffer(); + while ((line=br.readLine()) != null) { + if (method==null) { // first line (GET /base64/realaddr) + int pos=line.indexOf(" "); + if (pos == -1) break; + method=line.substring(0, pos); + String request = line.substring(pos+1); + if (request.startsWith("/") && + System.getProperty("i2ptunnel.noproxy") != null) { + request="http://i2p"+request; + } + pos = request.indexOf("//"); + if (pos == -1) { + method=null; + break; + } + protocol=request.substring(0,pos+2); + request=request.substring(pos+2); + + targetRequest = request; + + pos = request.indexOf("/"); + if (pos == -1) { + method=null; + break; + } + host=request.substring(0,pos); + + // Quick hack for foo.bar.i2p + if (host.toLowerCase().endsWith( ".i2p")) { + destination=host; + host=getHostName(destination); + line=method+" "+request.substring(pos); + } else if (host.indexOf(".") != -1) { + // The request must be forwarded to a WWW proxy + destination = wwwProxy; + usingWWWProxy = true; + } else { + request=request.substring(pos+1); + pos = request.indexOf("/"); + destination=request.substring(0,pos); + line=method+" "+request.substring(pos); + } + + boolean isValid = usingWWWProxy || + isSupportedAddress(host, protocol); + if (!isValid) { + if (_log.shouldLog(Log.INFO)) + _log.info("notValid(" + host + ")"); + method=null; + destination=null; + break; + } else if (!usingWWWProxy) { + if (_log.shouldLog(Log.INFO)) + _log.info("host=getHostName(" + destination + ")"); + host=getHostName(destination); // hide original host + } + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("METHOD:"+method+":"); + _log.debug("PROTOC:"+protocol+":"); + _log.debug("HOST :"+host+":"); + _log.debug("DEST :"+destination+":"); + } + + } else if (line.startsWith("Host: ") && !usingWWWProxy) { + line="Host: "+host; + if (_log.shouldLog(Log.INFO)) + _log.info("Setting host = " + host); + } + newRequest.append(line).append("\r\n"); // HTTP spec + if (line.length()==0) break; + } + while (br.ready()) { // empty the buffer (POST requests) + int i=br.read(); + if (i != -1) { + newRequest.append((char)i); + } + } + if (method==null || destination==null) { + l.log("No HTTP method found in the request."); + if (out != null) { + out.write(ERR_REQUEST_DENIED); + out.write("

Generated on: ".getBytes()); + out.write(new Date().toString().getBytes()); + out.write("\n".getBytes()); + out.flush(); + } + s.close(); + return; + } + Destination dest=I2PTunnel.destFromName(destination); + if (dest == null) { + l.log("Could not resolve "+destination+"."); + writeErrorMessage(ERR_DESTINATION_UNKNOWN, out, targetRequest, + usingWWWProxy, destination); + s.close(); + return; + } + String remoteID; + I2PSocket i2ps = createI2PSocket(dest); + byte[] data=newRequest.toString().getBytes("ISO-8859-1"); + I2PTunnelRunner runner = new I2PTunnelRunner(s, i2ps, sockLock, data); + timeoutThread = new InactivityTimeoutThread(runner, out, targetRequest, usingWWWProxy, s); + timeoutThread.start(); + } catch (IOException ex) { + if (timeoutThread != null) timeoutThread.disable(); + _log.error("Error sending syn", ex); + handleHTTPClientException(ex, out, targetRequest, + usingWWWProxy, wwwProxy); + closeSocket(s); + } catch (I2PException ex) { + if (timeoutThread != null) timeoutThread.disable(); + _log.info("Error sending syn", ex); + l.log("Unable to reach peer"); + handleHTTPClientException(ex, out, targetRequest, + usingWWWProxy, wwwProxy); + closeSocket(s); + } + } + + private static final long INACTIVITY_TIMEOUT = 120*1000; + + private class InactivityTimeoutThread extends I2PThread { + + private Socket s; + private I2PTunnelRunner _runner; + private OutputStream _out; + private String _targetRequest; + private boolean _useWWWProxy; + private boolean _disabled; + private Object _disableLock = new Object(); + + public InactivityTimeoutThread(I2PTunnelRunner runner, OutputStream out, String targetRequest, boolean useWWWProxy, Socket s) { + this.s=s; + _runner = runner; + _out = out; + _targetRequest = targetRequest; + _useWWWProxy = useWWWProxy; + _disabled = false; + } + public void disable() { + _disabled = true; + synchronized (_disableLock) { _disableLock.notifyAll(); } + } + public void run() { + while (!_disabled) { + if (_runner.isFinished()) { + if (_log.shouldLog(Log.INFO)) + _log.info("HTTP client request completed prior to timeout"); + return; + } + if (_runner.getLastActivityOn() < Clock.getInstance().now() - INACTIVITY_TIMEOUT) { + if (_runner.getStartedOn() < Clock.getInstance().now() - INACTIVITY_TIMEOUT) { + if (_log.shouldLog(Log.WARN)) + _log.warn("HTTP client request timed out (lastActivity: " + new Date(_runner.getLastActivityOn()) + ", startedOn: " + new Date(_runner.getLastActivityOn()) + ")"); + timeout(); + return; + } else { + // runner hasn't been going to long enough + } + } else { + // there has been activity in the period + } + synchronized (_disableLock) { + try { + _disableLock.wait(INACTIVITY_TIMEOUT); + } catch (InterruptedException ie) {} + } + } + } + private void timeout() { + _log.info("Inactivity timeout reached"); + l.log("Inactivity timeout reached"); + if (_out != null) { + try { + if (_runner.getLastActivityOn() > 0) { + // some data has been sent, so don't 404 it + } else { + writeErrorMessage(ERR_TIMEOUT, _out, _targetRequest, + _useWWWProxy, wwwProxy); + } + } catch (IOException ioe) { + _log.warn("Error writing out the 'timeout' message", ioe); + } + } else { + _log.warn("Client disconnected before we could say we timed out"); + } + closeSocket(s); + } + } + + private final static String getHostName(String host) { + try { + Destination dest=I2PTunnel.destFromName(host); + if (dest == null) return "i2p"; + return dest.toBase64(); + } catch (DataFormatException dfe) { + return "i2p"; + } + } + + private static void writeErrorMessage(byte[] errMessage, OutputStream out, + String targetRequest, + boolean usingWWWProxy, + String wwwProxy) + throws IOException { + if (out != null) { + out.write(errMessage); + if (targetRequest != null) { + out.write(targetRequest.getBytes()); + if (usingWWWProxy) + out.write(("
WWW proxy: " + + wwwProxy).getBytes()); + } + out.write("

Generated on: ".getBytes()); + out.write(new Date().toString().getBytes()); + out.write("\n".getBytes()); + out.flush(); + } + } + + private static void handleHTTPClientException (Exception ex, OutputStream out, + String targetRequest, + boolean usingWWWProxy, + String wwwProxy) { + if (out != null) { + try { + writeErrorMessage(ERR_DESTINATION_UNKNOWN, out, targetRequest, + usingWWWProxy, wwwProxy); + } catch (IOException ioe) { + _log.warn("Error writing out the 'destination was unknown' "+ + "message", ioe); + } + } else { + _log.warn("Client disconnected before we could say that destination "+ + "was unknown", ex); + } + } + + private final static String SUPPORTED_HOSTS[] = { "i2p", "www.i2p.com", + "i2p." }; + + private boolean isSupportedAddress(String host, String protocol) { + if ( (host == null) || (protocol == null) ) return false; + boolean found = false; + String lcHost = host.toLowerCase(); + for (int i = 0; i < SUPPORTED_HOSTS.length; i++) { + if (SUPPORTED_HOSTS[i].equals(lcHost)) { + found = true; + break; + } + } + + if (!found) { + try { + Destination d = I2PTunnel.destFromName(host); + if (d == null) return false; + } catch (DataFormatException dfe) {} + } + + return protocol.equalsIgnoreCase("http://"); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java new file mode 100644 index 0000000000..4f1ed4f927 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java @@ -0,0 +1,184 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; +import java.util.HashMap; + +import net.i2p.client.I2PSession; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +public class I2PTunnelRunner extends Thread { + private final static Log _log = new Log(I2PTunnelRunner.class); + + /** + * max bytes streamed in a packet - smaller ones might be filled + * up to this size. Larger ones are not split (at least not on + * Sun's impl of BufferedOutputStream), but that is the streaming + * api's job... + */ + static int MAX_PACKET_SIZE = 1024*32; + + static final int NETWORK_BUFFER_SIZE = MAX_PACKET_SIZE; + + private Socket s; + private I2PSocket i2ps; + Object slock, finishLock = new Object(); + boolean finished=false; + HashMap ostreams, sockets; + I2PSession session; + byte[] initialData; + /** when the last data was sent/received (or -1 if never) */ + private long lastActivityOn; + /** when the runner started up */ + private long startedOn; + + public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, + byte[] initialData) { + this.s=s; + this.i2ps=i2ps; + this.slock=slock; + this.initialData = initialData; + lastActivityOn = -1; + startedOn = -1; + _log.info("I2PTunnelRunner started"); + setName("I2PTunnelRunner"); + start(); + } + + /** + * have we closed at least one (if not both) of the streams + * [aka we're done running the streams]? + * + */ + public boolean isFinished() { return finished; } + + /** + * When was the last data for this runner sent or received? (-1 if no data + * has been transferred yet) + * + */ + public long getLastActivityOn() { return lastActivityOn; } + private void updateActivity() { lastActivityOn = Clock.getInstance().now(); } + + /** + * When this runner started up transferring data + * + */ + public long getStartedOn() { return startedOn; } + + public void run() { + startedOn = Clock.getInstance().now(); + try { + InputStream in = s.getInputStream(); + OutputStream out = new BufferedOutputStream(s.getOutputStream(), + NETWORK_BUFFER_SIZE); + InputStream i2pin = i2ps.getInputStream(); + OutputStream i2pout = new BufferedOutputStream + (i2ps.getOutputStream(), MAX_PACKET_SIZE); + if (initialData != null) { + synchronized(slock) { + i2pout.write(initialData); + i2pout.flush(); + } + } + Thread t1 = new StreamForwarder(in, i2pout); + Thread t2 = new StreamForwarder(i2pin, out); + synchronized(finishLock) { + while (!finished) { + finishLock.wait(); + } + } + // now one connection is dead - kill the other as well. + s.close(); + s = null; + i2ps.close(); + i2ps = null; + t1.join(); + t2.join(); + } catch (InterruptedException ex) { + _log.error("Interrupted", ex); + } catch (IOException ex) { + ex.printStackTrace(); + _log.error("Error forwarding", ex); + } finally { + try { + if (s != null) s.close(); + if (i2ps != null) i2ps.close(); + } catch (IOException ex) { + ex.printStackTrace(); + _log.error("Could not close socket", ex); + } + } + } + + private class StreamForwarder extends Thread { + + InputStream in; + OutputStream out; + + private StreamForwarder(InputStream in, OutputStream out) { + this.in=in; + this.out=out; + setName("StreamForwarder"); + start(); + } + + public void run() { + byte[] buffer = new byte[NETWORK_BUFFER_SIZE]; + try { + int len; + while ((len=in.read(buffer)) != -1) { + out.write(buffer, 0, len); + + if (len > 0) + updateActivity(); + + if (in.available()==0) { + try { + Thread.sleep(I2PTunnel.PACKET_DELAY); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + if (in.available()==0) { + out.flush(); // make sure the data get though + } + } + } catch (SocketException ex) { + // this *will* occur when the other threads closes the socket + synchronized(finishLock) { + if (!finished) + _log.error("Error reading and writing", ex); + else + _log.warn("You may ignore this", ex); + } + } catch (IOException ex) { + if (!finished) + _log.error("Error forwarding", ex); + else + _log.warn("You may ignore this", ex); + } finally { + try { + out.close(); + in.close(); + } catch (IOException ex) { + _log.error("Error closing streams", ex); + } + synchronized(finishLock) { + finished=true; + finishLock.notifyAll(); + // the main thread will close sockets etc. now + } + } + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java new file mode 100644 index 0000000000..1711baae9b --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java @@ -0,0 +1,138 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.*; + +import net.i2p.I2PException; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PClientFactory; +import net.i2p.client.streaming.I2PServerSocket; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.client.streaming.I2PSocketManagerFactory; +import net.i2p.data.Base64; +import net.i2p.util.EventDispatcher; +import net.i2p.util.Log; + +public class I2PTunnelServer extends I2PTunnelTask + implements Runnable { + + private final static Log _log = new Log(I2PTunnelServer.class); + + private I2PSocketManager sockMgr; + private I2PServerSocket i2pss; + + private Object lock = new Object(), slock = new Object(); + + private InetAddress remoteHost; + private int remotePort; + + private Logging l; + + public I2PTunnelServer(InetAddress host, int port, + String privData, Logging l, + EventDispatcher notifyThis) { + super(host+":"+port+" <- "+privData, notifyThis); + ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(privData)); + init(host, port, bais, privData, l); + } + + public I2PTunnelServer(InetAddress host, int port, + File privkey, String privkeyname, + Logging l, EventDispatcher notifyThis) { + super(host+":"+port+" <- "+privkeyname, notifyThis); + try { + init(host, port, new FileInputStream(privkey), privkeyname, l); + } catch (IOException ioe) { + _log.error("Error starting server", ioe); + notifyEvent("openServerResult", "error"); + } + } + public I2PTunnelServer(InetAddress host, int port, + InputStream privData, String privkeyname, + Logging l, EventDispatcher notifyThis) { + super(host+":"+port+" <- "+privkeyname, notifyThis); + init(host, port, privData, privkeyname, l); + } + + private void init(InetAddress host, int port, InputStream privData, + String privkeyname, Logging l) { + this.l=l; + this.remoteHost=host; + this.remotePort=port; + I2PClient client = I2PClientFactory.createClient(); + Properties props = new Properties(); + props.putAll(System.getProperties()); + synchronized(slock) { + sockMgr = I2PSocketManagerFactory.createManager + (privData, I2PTunnel.host, + Integer.parseInt(I2PTunnel.port), props); + + } + l.log("Ready!"); + notifyEvent("openServerResult", "ok"); + open=true; + Thread t = new Thread(this); + t.setName("Server"); + t.start(); + } + + + public boolean close(boolean forced) { + if (!open) return true; + synchronized(lock) { + if (!forced && sockMgr.listSockets().size() != 0) { + l.log("There are still active connections!"); + for (Iterator it = sockMgr.listSockets().iterator(); + it.hasNext();) { + l.log("->"+it.next()); + } + return false; + } + l.log("Shutting down server "+toString()); + try { + if (i2pss != null) i2pss.close(); + sockMgr.getSession().destroySession(); + } catch (I2PException ex) { + _log.error("Error destroying the session", ex); + System.exit(1); + } + l.log("Server shut down."); + open=false; + return true; + } + } + + + public void run() { + try { + I2PServerSocket i2pss = sockMgr.getServerSocket(); + while (true) { + I2PSocket i2ps = i2pss.accept(); + //local is fast, so synchronously. Does not need that many + //threads. + try { + Socket s = new Socket(remoteHost, remotePort); + new I2PTunnelRunner(s, i2ps, slock, null); + } catch (SocketException ex) { + i2ps.close(); + } + } + } catch (I2PException ex) { + _log.error("Error while waiting for I2PConnections", ex); + } catch (IOException ex) { + _log.error("Error while waiting for I2PConnections", ex); + } + } +} + diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelTask.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelTask.java new file mode 100644 index 0000000000..a1082eca1d --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelTask.java @@ -0,0 +1,75 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.util.Set; + +import net.i2p.client.I2PSession; +import net.i2p.util.EventDispatcher; +import net.i2p.util.EventDispatcherImpl; + +/** + * Either a Server or a Client. + */ + +public abstract class I2PTunnelTask implements EventDispatcher { + + private final EventDispatcherImpl _event = new EventDispatcherImpl(); + + private int id; + private String name; + protected boolean open; + private I2PTunnel tunnel; + + //protected I2PTunnelTask(String name) { + // I2PTunnelTask(name, (EventDispatcher)null); + //} + + protected I2PTunnelTask(String name, EventDispatcher notifyThis) { + attachEventDispatcher(notifyThis); + this.name=name; + this.id = -1; + } + + /** for apps that use multiple I2PTunnel instances */ + public void setTunnel(I2PTunnel pTunnel) { tunnel = pTunnel; } + + public int getId() { + return this.id; + } + + public boolean isOpen() {return open;} + + public void setId(int id) { + this.id = id; + } + + protected void setName(String name) { + this.name=name; + } + + protected void routerDisconnected() { tunnel.routerDisconnected(); } + + public abstract boolean close(boolean forced); + + public void disconnected(I2PSession session) { routerDisconnected(); } + public void errorOccurred(I2PSession session, String message, + Throwable error) {} + public void reportAbuse(I2PSession session, int severity) {} + + public String toString() { + return name; + } + + /* Required by the EventDispatcher interface */ + public EventDispatcher getEventDispatcher() { return _event; } + public void attachEventDispatcher(EventDispatcher e) { _event.attachEventDispatcher(e.getEventDispatcher()); } + public void detachEventDispatcher(EventDispatcher e) { _event.detachEventDispatcher(e.getEventDispatcher()); } + public void notifyEvent(String e, Object a) { _event.notifyEvent(e,a); } + public Object getEventValue(String n) { return _event.getEventValue(n); } + public Set getEvents() { return _event.getEvents(); } + public void ignoreEvents() { _event.ignoreEvents(); } + public void unIgnoreEvents() { _event.unIgnoreEvents(); } + public Object waitEventValue(String n) { return _event.waitEventValue(n); } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2Ping.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2Ping.java new file mode 100644 index 0000000000..49b5be5b3e --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2Ping.java @@ -0,0 +1,228 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import net.i2p.I2PException; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.data.Destination; +import net.i2p.util.EventDispatcher; +import net.i2p.util.Log; + +public class I2Ping extends I2PTunnelTask implements Runnable { + private final static Log _log = new Log(I2Ping.class); + + private static final int PING_COUNT = 3; + private static final int CPING_COUNT = 5; + private static final int PING_TIMEOUT= 5000; + + private static final long PING_DISTANCE=1000; + + private int MAX_SIMUL_PINGS=10; // not really final... + + private boolean countPing=false; + + private I2PSocketManager sockMgr; + private Logging l; + private boolean finished=false; + private String command; + private long timeout = PING_TIMEOUT; + + private Object simulLock = new Object(); + private int simulPings = 0; + private long lastPingTime = 0; + + private Object lock = new Object(), slock = new Object(); + + //public I2Ping(String cmd, Logging l, + // boolean ownDest) { + // I2Ping(cmd, l, (EventDispatcher)null); + //} + + public I2Ping(String cmd, Logging l, + boolean ownDest, EventDispatcher notifyThis) { + super("I2Ping ["+cmd+"]", notifyThis); + this.l=l; + command=cmd; + synchronized(slock) { + if (ownDest) { + sockMgr = I2PTunnelClient.buildSocketManager(); + } else { + sockMgr = I2PTunnelClient.getSocketManager(); + } + } + Thread t = new Thread(this); + t.setName("Client"); + t.start(); + open=true; + } + + public void run() { + l.log("*** I2Ping results:"); + try { + runCommand(command); + } catch (InterruptedException ex) { + l.log("*** Interrupted"); + _log.error("Pinger interrupted",ex); + } catch (IOException ex) { + _log.error("Pinger exception",ex); + } + l.log("*** Finished."); + synchronized(lock) { + finished=true; + } + close(false); + } + + public void runCommand(String cmd) throws InterruptedException, + IOException { + if (cmd.startsWith("-t ")) { // timeout + cmd = cmd.substring(3); + int pos = cmd.indexOf(" "); + if (pos == -1) { + l.log("Syntax error"); + return; + } else { + timeout = Long.parseLong(cmd.substring(0, pos)); + cmd=cmd.substring(pos+1); + } + } + if (cmd.startsWith("-m ")) { // max simultaneous pings + cmd = cmd.substring(3); + int pos = cmd.indexOf(" "); + if (pos == -1) { + l.log("Syntax error"); + return; + } else { + MAX_SIMUL_PINGS = Integer.parseInt(cmd.substring(0, pos)); + cmd=cmd.substring(pos+1); + } + } + if (cmd.startsWith("-c ")) { // "count" ping + countPing=true; + cmd=cmd.substring(3); + } + if (cmd.equals("-h")) { // ping all hosts + cmd="-l hosts.txt"; + } + if (cmd.startsWith("-l ")) { // ping a list of hosts + BufferedReader br = new BufferedReader + (new FileReader(cmd.substring(3))); + String line; + List pingHandlers = new ArrayList(); + while ((line = br.readLine()) != null) { + if (line.startsWith("#")) continue; // comments + if (line.startsWith(";")) continue; + if (line.startsWith("!")) continue; + if (line.indexOf("=") != -1) { // maybe file is hosts.txt? + line=line.substring(0,line.indexOf("=")); + } + pingHandlers.add(new PingHandler(line)); + } + br.close(); + for (Iterator it= pingHandlers.iterator(); it.hasNext(); ) { + Thread t = (Thread) it.next(); + t.join(); + } + + } else { + Thread t = new PingHandler(cmd); + t.join(); + } + } + + public boolean close(boolean forced) { + if (!open) return true; + synchronized(lock) { + if (!forced && !finished) { + l.log("There are still pings running!"); + return false; + } + l.log("Closing pinger "+toString()); + l.log("Pinger closed."); + open=false; + return true; + } + } + + public boolean ping(Destination dest) throws I2PException { + try { + synchronized(simulLock) { + while (simulPings >= MAX_SIMUL_PINGS) { + simulLock.wait(); + } + simulPings++; + while (lastPingTime + PING_DISTANCE > + System.currentTimeMillis()) { + // no wait here, to delay all pingers + Thread.sleep(PING_DISTANCE/2); + } + lastPingTime=System.currentTimeMillis(); + } + boolean sent = sockMgr.ping(dest, PING_TIMEOUT); + synchronized(simulLock) { + simulPings--; + simulLock.notifyAll(); + } + return sent; + } catch (InterruptedException ex) { + _log.error("Interrupted", ex); + return false; + } + } + + + + public class PingHandler extends Thread { + private String destination; + + public PingHandler(String dest) { + this.destination=dest; + setName("PingHandler for " + dest); + start(); + } + + public void run() { + try { + Destination dest=I2PTunnel.destFromName(destination); + if (dest == null) { + synchronized(lock) { // Logger is not thread safe + l.log("Unresolvable: "+destination+""); + } + return; + } + int cnt = countPing ? CPING_COUNT : PING_COUNT; + StringBuffer pingResults = new StringBuffer + (2*cnt+ destination.length()+3); + for (int i=0;i "+destination); + } + pingResults.append(" ").append(destination); + synchronized(lock) { // Logger is not thread safe + l.log(pingResults.toString()); + } + } catch (I2PException ex) { + _log.error("Error pinging " + destination, ex); + } + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/Logging.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/Logging.java new file mode 100644 index 0000000000..308793973c --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/Logging.java @@ -0,0 +1,9 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + + +public interface Logging { + public void log(String s); +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelManager.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelManager.java new file mode 100644 index 0000000000..dda9a2219d --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelManager.java @@ -0,0 +1,433 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; + +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.util.Clock; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Quick and dirty socket listener to control an I2PTunnel. + * Basically run this class as TunnelManager [listenHost] [listenPort] and + * then send it commands on that port. Commands are one shot deals - + * Send a command + newline, get a response plus newline, then get disconnected. + *

+ * Implemented commands: + *

+ * -------------------------------------------------
+ * lookup <name>\n
+ * --
+ * <base64 of the destination>\n
+ *  or
+ * <error message, usually 'Unknown host'>\n
+ * 
+ *  Lookup the public key of a named destination (i.e. listed in hosts.txt)
+ * -------------------------------------------------
+ * genkey\n
+ * --
+ * <base64 of the destination>\t<base64 of private data>\n
+ * 
+ *  Generates a new public and private key pair
+ * -------------------------------------------------
+ * convertprivate <base64 of privkey>
+ * --
+ * <base64 of destination>\n
+ *  or
+ * <error message>\n
+ *
+ *  Returns the destination (pubkey) of a given private key. 
+ * -------------------------------------------------
+ * listen_on <ip>\n
+ * --
+ * ok\n
+ *  or
+ * error\n
+ * 
+ *  Sets the ip address clients will listen on. By default this is the
+ *  localhost (127.0.0.1)
+ * -------------------------------------------------
+ * openclient <listenPort> <peer>\n
+ * --
+ * ok [<jobId>]\n
+ *  or
+ * ok <listenPort> [<jobId>]\n
+ *  or
+ * error\n
+ * 
+ *  Open a tunnel on the given <listenport> to the destination specified
+ *  by <peer>. If <listenPort> is 0 a free port is picked and returned in
+ *  the reply message. Otherwise the short reply message is used.
+ *  Peer can be the base64 of the destination, a file with the public key
+ *  specified as 'file:<filename>' or the name of a destination listed in
+ *  hosts.txt. The <jobId> returned together with "ok" and <listenport> can
+ *  later be used as argument for the "close" command.
+ * -------------------------------------------------
+ * openhttpclient <listenPort> [<proxy>]\n
+ * --
+ * ok [<jobId>]\n
+ *  or
+ * ok <listenPort> [<jobId>]\n
+ *  or
+ * error\n
+ * 
+ *  Open an HTTP proxy through the I2P on the given
+ *  <listenport>. <proxy> (optional) specifies a
+ *  destination to be used as an outbound proxy, to access normal WWW
+ *  sites out of the .i2p domain. If <listenPort> is 0 a free
+ *  port is picked and returned in the reply message. Otherwise the
+ *  short reply message is used.  <proxy> can be the base64 of the
+ *  destination, a file with the public key specified as
+ *  'file:<filename>' or the name of a destination listed in
+ *  hosts.txt. The <jobId> returned together with "ok" and
+ *  <listenport> can later be used as argument for the "close"
+ *  command.
+ * -------------------------------------------------
+ * opensockstunnel <listenPort>\n
+ * --
+ * ok [<jobId>]\n
+ *  or
+ * ok <listenPort> [<jobId>]\n
+ *  or
+ * error\n
+ * 
+ *  Open an SOCKS tunnel through the I2P on the given
+ *  <listenport>. If <listenPort> is 0 a free port is
+ *  picked and returned in the reply message. Otherwise the short
+ *  reply message is used.  The <jobId> returned together with
+ *  "ok" and <listenport> can later be used as argument for the
+ *  "close" command.
+ * -------------------------------------------------
+ * openserver <serverHost> <serverPort> <serverKeys>\n
+ * --
+ * ok [<jobId>]\n
+ *  or
+ * error\n
+ * 
+ *  Starts receiving traffic for the destination specified by <serverKeys>
+ *  and forwards it to the <serverPort> of <serverHost>.
+ *  <serverKeys> is the base 64 encoded private key set of the local
+ *  destination. The <joId> returned together with "ok" can later be used
+ *  as argument for the "close" command.
+ * -------------------------------------------------
+ * close [forced] <jobId>\n
+ *  or
+ * close [forced] all\n
+ * --
+ * ok\n
+ *  or
+ * error\n
+ * 
+ *  Closes the job specified by <jobId> or all jobs. Use the list command
+ *  for a list of running jobs.
+ *  Normally a connection job is not closed when it still has an active
+ *  connection. Use the optional 'forced' keyword to close connections
+ *  regardless of their use.
+ * -------------------------------------------------
+ * list\n
+ * --
+ *  Example output:
+ * 
+ * [0] i2p.dnsalias.net/69.55.226.145:5555 <- C:\i2pKeys\squidPriv
+ * [1] 8767 -> HTTPClient
+ * [2] 7575 -> file:C:\i2pKeys\squidPub
+ * [3] 5252 -> sCcSANIO~f4AQtCNI1BvDp3ZBS~9Ag5O0k0Msm7XBWWz5eOnZWL3MQ-2rxlesucb9XnpASGhWzyYNBpWAfaIB3pux1J1xujQLOwscMIhm7T8BP76Ly5jx6BLZCYrrPj0BI0uV90XJyT~4UyQgUlC1jzFQdZ9HDgBPJDf1UI4-YjIwEHuJgdZynYlQ1oUFhgno~HhcDByXO~PDaO~1JDMDbBEfIh~v6MgmHp-Xchod1OfKFrxFrzHgcJbn7E8edTFjZA6JCi~DtFxFelQz1lSBd-QB1qJnA0g-pVL5qngNUojXJCXs4qWcQ7ICLpvIc-Fpfj-0F1gkVlGDSGkb1yLH3~8p4czYgR3W5D7OpwXzezz6clpV8kmbd~x2SotdWsXBPRhqpewO38coU4dJG3OEUbuYmdN~nJMfWbmlcM1lXzz2vBsys4sZzW6dV3hZnbvbfxNTqbdqOh-KXi1iAzXv7CVTun0ubw~CfeGpcAqutC5loRUq7Mq62ngOukyv8Z9AAAA
+ *
+ *  Lists descriptions of all running jobs. The exact format of the
+ *  description depends on the type of job.
+ * -------------------------------------------------
+ * 
+ */ +public class TunnelManager implements Runnable { + private final static Log _log = new Log(TunnelManager.class); + private I2PTunnel _tunnel; + private ServerSocket _socket; + private boolean _keepAccepting; + + public TunnelManager(int listenPort) { + this(null, listenPort); + } + public TunnelManager(String listenHost, int listenPort) { + _tunnel = new I2PTunnel(); + _keepAccepting = true; + try { + if (listenHost != null) { + _socket = new ServerSocket(listenPort, 0, InetAddress.getByName(listenHost)); + _log.info("Listening for tunnel management clients on " + listenHost + ":" + listenPort); + } else { + _socket = new ServerSocket(listenPort); + _log.info("Listening for tunnel management clients on localhost:" + listenPort); + } + } catch (Exception e) { + _log.error("Error starting up tunnel management listener on " + listenPort, e); + } + } + + public static void main(String args[]) { + int port = 7676; + String host = null; + if (args.length == 1) { + try { + port = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + _log.error("Usage: TunnelManager [host] [port]"); + return; + } + } else if (args.length == 2) { + host = args[0]; + try { + port = Integer.parseInt(args[1]); + } catch (NumberFormatException nfe) { + _log.error("Usage: TunnelManager [host] [port]"); + return; + } + } + + TunnelManager mgr = new TunnelManager(host, port); + Thread t = new Thread(mgr, "Listener"); + t.start(); + } + + public void run() { + if (_socket == null) { + _log.error("Unable to start listening, since the socket was not bound. Already running?"); + return; + } + _log.debug("Running"); + try { + while (_keepAccepting) { + Socket socket = _socket.accept(); + _log.debug("Client accepted"); + if (socket != null) { + Thread t = new I2PThread(new TunnelManagerClientRunner(this, socket)); + t.setName("TunnelManager Client"); + t.setPriority(I2PThread.MIN_PRIORITY); + t.start(); + } + } + } catch (IOException ioe) { + _log.error("Error accepting connections", ioe); + } catch (Exception e) { + _log.error("Other error?!", e); + } finally { + if (_socket != null) try { _socket.close(); } catch (IOException ioe) {} + } + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + } + + public void error(String msg, OutputStream out) throws IOException { + out.write(msg.getBytes()); + out.write('\n'); + } + + public void processQuit(OutputStream out) throws IOException { + out.write("Nice try".getBytes()); + out.write('\n'); + } + + public void processList(OutputStream out) throws IOException { + BufferLogger buf = new BufferLogger(); + long startCommand = Clock.getInstance().now(); + _tunnel.runCommand("list", buf); + Object obj = _tunnel.waitEventValue("listDone"); + long endCommand = Clock.getInstance().now(); + String str = buf.getBuffer(); + _log.debug("ListDone complete after " + (endCommand-startCommand) + "ms: [" + str + "]"); + out.write(str.getBytes()); + out.write('\n'); + buf.ignoreFurtherActions(); + } + + public void processListenOn(String ip, OutputStream out) throws IOException { + BufferLogger buf = new BufferLogger(); + _tunnel.runCommand("listen_on " + ip, buf); + String status = (String)_tunnel.waitEventValue("listen_onResult"); + out.write((status + "\n").getBytes()); + buf.ignoreFurtherActions(); + } + + /** + * "lookup " returns with the result in base64, else "Unknown host" [or something like that], + * then a newline. + * + */ + public void processLookup(String name, OutputStream out) throws IOException { + BufferLogger buf = new BufferLogger(); + _tunnel.runCommand("lookup " + name, buf); + String rv = (String)_tunnel.waitEventValue("lookupResult"); + out.write(rv.getBytes()); + out.write('\n'); + buf.ignoreFurtherActions(); + } + + public void processTestDestination(String destKey, OutputStream out) throws IOException { + try { + Destination d = new Destination(); + d.fromBase64(destKey); + out.write("valid\n".getBytes()); + } catch (DataFormatException dfe) { + out.write("invalid\n".getBytes()); + } + out.flush(); + } + + public void processConvertPrivate(String priv, OutputStream out) throws IOException { + try { + Destination dest = new Destination(); + dest.fromBase64(priv); + String str = dest.toBase64(); + out.write(str.getBytes()); + out.write('\n'); + } catch (DataFormatException dfe) { + _log.error("Error converting private data", dfe); + out.write("Error converting private key\n".getBytes()); + } + } + + public void processClose(String which, boolean forced, OutputStream out) throws IOException { + BufferLogger buf = new BufferLogger(); + _tunnel.runCommand((forced?"close forced ":"close ") + which, buf); + String str = (String)_tunnel.waitEventValue("closeResult"); + out.write((str + "\n").getBytes()); + buf.ignoreFurtherActions(); + } + + /** + * "genkey" returns with the base64 of the destination, followed by a tab, then the base64 of that + * destination's private keys, then a newline. + * + */ + public void processGenKey(OutputStream out) throws IOException { + BufferLogger buf = new BufferLogger(); + _tunnel.runCommand("gentextkeys", buf); + String priv = (String)_tunnel.waitEventValue("privateKey"); + String pub = (String)_tunnel.waitEventValue("publicDestination"); + out.write((pub + "\t" + priv).getBytes()); + out.write('\n'); + buf.ignoreFurtherActions(); + } + + public void processOpenClient(int listenPort, String peer, OutputStream out) throws IOException { + BufferLogger buf = new BufferLogger(); + _tunnel.runCommand("client " + listenPort + " " + peer, buf); + Integer taskId = (Integer)_tunnel.waitEventValue("clientTaskId"); + if (taskId.intValue() < 0) { + out.write("error\n".getBytes()); + buf.ignoreFurtherActions(); + return; + } + String rv = (String)_tunnel.waitEventValue("openClientResult"); + if (rv.equals("error")) { + out.write((rv + "\n").getBytes()); + buf.ignoreFurtherActions(); + return; + } + + if (listenPort != 0) { + out.write((rv + " [" + taskId.intValue() + "]\n").getBytes()); + buf.ignoreFurtherActions(); + return; + } + Integer port = (Integer)_tunnel.waitEventValue("clientLocalPort"); + out.write((rv + " " + port.intValue() + " [" + taskId.intValue() + + "]\n").getBytes()); + buf.ignoreFurtherActions(); + } + + public void processOpenHTTPClient(int listenPort, + String proxy, + OutputStream out) throws IOException { + BufferLogger buf = new BufferLogger(); + _tunnel.runCommand("httpclient " + listenPort + " " + proxy, buf); + Integer taskId = (Integer)_tunnel.waitEventValue("httpclientTaskId"); + if (taskId.intValue() < 0) { + out.write("error\n".getBytes()); + buf.ignoreFurtherActions(); + return; + } + String rv = (String)_tunnel.waitEventValue("openHTTPClientResult"); + if (rv.equals("error")) { + out.write((rv + "\n").getBytes()); + buf.ignoreFurtherActions(); + return; + } + + if (listenPort != 0) { + out.write((rv + " [" + taskId.intValue() + "]\n").getBytes()); + buf.ignoreFurtherActions(); + return; + } + Integer port = (Integer)_tunnel.waitEventValue("clientLocalPort"); + out.write((rv + " " + port.intValue() + " [" + taskId.intValue() + + "]\n").getBytes()); + buf.ignoreFurtherActions(); + } + + public void processOpenSOCKSTunnel(int listenPort, + OutputStream out) throws IOException { + BufferLogger buf = new BufferLogger(); + _tunnel.runCommand("sockstunnel " + listenPort, buf); + Integer taskId = (Integer)_tunnel.waitEventValue("sockstunnelTaskId"); + if (taskId.intValue() < 0) { + out.write("error\n".getBytes()); + buf.ignoreFurtherActions(); + return; + } + String rv = (String)_tunnel.waitEventValue("openSOCKSTunnelResult"); + if (rv.equals("error")) { + out.write((rv + "\n").getBytes()); + buf.ignoreFurtherActions(); + return; + } + + if (listenPort != 0) { + out.write((rv + " [" + taskId.intValue() + "]\n").getBytes()); + buf.ignoreFurtherActions(); + return; + } + Integer port = (Integer)_tunnel.waitEventValue("clientLocalPort"); + out.write((rv + " " + port.intValue() + " [" + taskId.intValue() + + "]\n").getBytes()); + buf.ignoreFurtherActions(); + } + + public void processOpenServer(String serverHost, int serverPort, String privateKeys, OutputStream out) throws IOException { + BufferLogger buf = new BufferLogger(); + _tunnel.runCommand("textserver " + serverHost + " " + serverPort + " " + privateKeys, buf); + Integer taskId = (Integer)_tunnel.waitEventValue("serverTaskId"); + if (taskId.intValue() < 0) { + out.write("error\n".getBytes()); + buf.ignoreFurtherActions(); + return; + } + + String rv = (String)_tunnel.waitEventValue("openServerResult"); + + if (rv.equals("error")) { + out.write((rv + "\n").getBytes()); + buf.ignoreFurtherActions(); + return; + } + + out.write((rv + " [" + taskId.intValue() + "]\n").getBytes()); + buf.ignoreFurtherActions(); + } + + /** + * Frisbee. + * + */ + public void unknownCommand(String command, OutputStream out) throws IOException { + out.write("Unknown command: ".getBytes()); + out.write(command.getBytes()); + out.write("\n".getBytes()); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelManagerClientRunner.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelManagerClientRunner.java new file mode 100644 index 0000000000..e264d87ef1 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelManagerClientRunner.java @@ -0,0 +1,193 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import net.i2p.util.Log; + +import java.util.StringTokenizer; + +import java.net.Socket; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.BufferedReader; +import java.io.OutputStream; + +/** + * Runner thread that reads commands from the socket and fires off commands to + * the TunnelManager + * + */ +class TunnelManagerClientRunner implements Runnable { + private final static Log _log = new Log(TunnelManagerClientRunner.class); + private TunnelManager _mgr; + private Socket _clientSocket; + + public TunnelManagerClientRunner(TunnelManager mgr, Socket socket) { + _clientSocket = socket; + _mgr = mgr; + } + + public void run() { + _log.debug("Client running"); + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(_clientSocket.getInputStream())); + OutputStream out = _clientSocket.getOutputStream(); + + String cmd = reader.readLine(); + if (cmd != null) + processCommand(cmd, out); + } catch (IOException ioe) { + _log.error("Error processing client commands", ioe); + } finally { + if (_clientSocket != null) try { _clientSocket.close(); } catch (IOException ioe) {} + } + _log.debug("Client closed"); + } + + /** + * Parse the command string and fire off the appropriate tunnelManager method, + * sending the results to the output stream + */ + private void processCommand(String command, OutputStream out) throws IOException { + _log.debug("Processing [" + command + "]"); + StringTokenizer tok = new StringTokenizer(command); + if (!tok.hasMoreTokens()) { + _mgr.unknownCommand(command, out); + } else { + String cmd = tok.nextToken(); + if ("quit".equalsIgnoreCase(cmd)) { + _mgr.processQuit(out); + } else if ("lookup".equalsIgnoreCase(cmd)) { + if (tok.hasMoreTokens()) + _mgr.processLookup(tok.nextToken(), out); + else + _mgr.error("Usage: lookup ", out); + } else if ("testdestination".equalsIgnoreCase(cmd)) { + if (tok.hasMoreTokens()) + _mgr.processTestDestination(tok.nextToken(), out); + else + _mgr.error("Usage: testdestination ", out); + } else if ("convertprivate".equalsIgnoreCase(cmd)) { + if (tok.hasMoreTokens()) + _mgr.processConvertPrivate(tok.nextToken(), out); + else + _mgr.error("Usage: convertprivate ", out); + } else if ("close".equalsIgnoreCase(cmd)) { + if (tok.hasMoreTokens()) { + String closeArg; + if ((closeArg = tok.nextToken()).equals("forced")) { + if (tok.hasMoreTokens()) { + _mgr.processClose(tok.nextToken(), true, out); + } else { + _mgr.error("Usage: close [forced] |all", out); + } + } else { + _mgr.processClose(closeArg, false, out); + } + } else { + _mgr.error("Usage: close [forced] |all", out); + } + } else if ("genkey".equalsIgnoreCase(cmd)) { + _mgr.processGenKey(out); + } else if ("list".equalsIgnoreCase(cmd)) { + _mgr.processList(out); + } else if ("listen_on".equalsIgnoreCase(cmd)) { + if (tok.hasMoreTokens()) { + _mgr.processListenOn(tok.nextToken(), out); + } else { + _mgr.error("Usage: listen_on ", out); + } + } else if ("openclient".equalsIgnoreCase(cmd)) { + int listenPort = 0; + String peer = null; + if (!tok.hasMoreTokens()) { + _mgr.error("Usage: openclient ", out); + return; + } + try { + String portStr = tok.nextToken(); + listenPort = Integer.parseInt(portStr); + } catch (NumberFormatException nfe) { + _mgr.error("Bad listen port", out); + return; + } + if (!tok.hasMoreTokens()) { + _mgr.error("Usage: openclient ", out); + return; + } + peer = tok.nextToken(); + _mgr.processOpenClient(listenPort, peer, out); + } else if ("openhttpclient".equalsIgnoreCase(cmd)) { + int listenPort = 0; + String proxy = "squid.i2p"; + if (!tok.hasMoreTokens()) { + _mgr.error("Usage: openhttpclient []",out); + return; + } + try { + String portStr = tok.nextToken(); + listenPort = Integer.parseInt(portStr); + } catch (NumberFormatException nfe) { + _mgr.error("Bad listen port", out); + return; + } + if (tok.hasMoreTokens()) { + proxy = tok.nextToken(); + } + if (tok.hasMoreTokens()) { + _mgr.error("Usage: openclient []",out); + return; + } + _mgr.processOpenHTTPClient(listenPort, proxy, out); + } else if ("opensockstunnel".equalsIgnoreCase(cmd)) { + int listenPort = 0; + if (!tok.hasMoreTokens()) { + _mgr.error("Usage: opensockstunnel ",out); + return; + } + try { + String portStr = tok.nextToken(); + listenPort = Integer.parseInt(portStr); + } catch (NumberFormatException nfe) { + _mgr.error("Bad listen port", out); + return; + } + if (tok.hasMoreTokens()) { + _mgr.error("Usage: opensockstunnel ",out); + return; + } + _mgr.processOpenSOCKSTunnel(listenPort, out); + } else if ("openserver".equalsIgnoreCase(cmd)) { + int listenPort = 0; + String serverHost = null; + String serverKeys = null; + if (!tok.hasMoreTokens()) { + _mgr.error("Usage: openserver ", out); + return; + } + serverHost = tok.nextToken(); + + if (!tok.hasMoreTokens()) { + _mgr.error("Usage: openserver ", out); + return; + } + try { + String portStr = tok.nextToken(); + listenPort = Integer.parseInt(portStr); + } catch (NumberFormatException nfe) { + _mgr.error("Bad listen port", out); + return; + } + if (!tok.hasMoreTokens()) { + _mgr.error("Usage: openserver ", out); + return; + } + serverKeys = tok.nextToken(); + _mgr.processOpenServer(serverHost, listenPort, serverKeys, out); + } else { + _mgr.unknownCommand(command, out); + } + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java new file mode 100644 index 0000000000..d7c8083703 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java @@ -0,0 +1,56 @@ +/* I2PSOCKSTunnel is released under the terms of the GNU GPL, + * with an additional exception. For further details, see the + * licensing terms in I2PTunnel.java. + * + * Copyright (c) 2004 by human + */ +package net.i2p.i2ptunnel.socks; + +import java.net.Socket; + +import net.i2p.client.streaming.I2PSocket; +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.I2PTunnelClientBase; +import net.i2p.i2ptunnel.I2PTunnelRunner; +import net.i2p.i2ptunnel.Logging; +import net.i2p.util.EventDispatcher; +import net.i2p.util.Log; + +public class I2PSOCKSTunnel extends I2PTunnelClientBase { + + private static final Log _log = new Log(I2PSOCKSTunnel.class); + + protected Destination outProxyDest = null; + + //public I2PSOCKSTunnel(int localPort, Logging l, boolean ownDest) { + // I2PSOCKSTunnel(localPort, l, ownDest, (EventDispatcher)null); + //} + + public I2PSOCKSTunnel(int localPort, Logging l, boolean ownDest, + EventDispatcher notifyThis) { + super(localPort, ownDest, l, notifyThis, "SOCKSHandler"); + + if (waitEventValue("openBaseClientResult").equals("error")) { + notifyEvent("openSOCKSTunnelResult", "error"); + return; + } + + setName(getLocalPort() + " -> SOCKSTunnel"); + + startRunning(); + + notifyEvent("openSOCKSTunnelResult", "ok"); + } + + protected void clientConnectionRun(Socket s) { + try { + SOCKSServer serv = SOCKSServerFactory.createSOCKSServer(s); + Socket clientSock = serv.getClientSocket(); + I2PSocket destSock = serv.getDestinationI2PSocket(); + new I2PTunnelRunner (clientSock, destSock, sockLock, null); + } catch (SOCKSException e) { + _log.error("Error from SOCKS connection: " + e.getMessage()); + closeSocket(s); + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java new file mode 100644 index 0000000000..713057e0e5 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java @@ -0,0 +1,309 @@ +/* I2PSOCKSTunnel is released under the terms of the GNU GPL, + * with an additional exception. For further details, see the + * licensing terms in I2PTunnel.java. + * + * Copyright (c) 2004 by human + */ +package net.i2p.i2ptunnel.socks; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; + +import net.i2p.util.HexDump; +import net.i2p.util.Log; + +/* + * Class that manages SOCKS5 connections, and forwards them to + * destination hosts or (eventually) some outproxy. + * + * @author human + */ +public class SOCKS5Server extends SOCKSServer { + private static final Log _log = new Log(SOCKS5Server.class); + + private static final int SOCKS_VERSION_5 = 0x05; + + private Socket clientSock = null; + + private boolean setupCompleted = false; + + /** + * Create a SOCKS5 server that communicates with the client using + * the specified socket. This method should not be invoked + * directly: new SOCKS5Server objects should be created by using + * SOCKSServerFactory.createSOCSKServer(). It is assumed that the + * SOCKS VER field has been stripped from the input stream of the + * client socket. + * + * @param clientSock client socket + */ + public SOCKS5Server(Socket clientSock) { + this.clientSock = clientSock; + } + + public Socket getClientSocket() throws SOCKSException { + setupServer(); + + return clientSock; + } + + protected void setupServer() throws SOCKSException { + if (setupCompleted) { + return; + } + + DataInputStream in; + DataOutputStream out; + try { + in = new DataInputStream(clientSock.getInputStream()); + out = new DataOutputStream(clientSock.getOutputStream()); + + init(in, out); + manageRequest(in, out); + } catch (IOException e) { + throw new SOCKSException("Connection error (" + + e.getMessage() + ")"); + } + + setupCompleted = true; + } + + /** + * SOCKS5 connection initialization. This method assumes that + * SOCKS "VER" field has been stripped from the input stream. + */ + private void init (DataInputStream in, + DataOutputStream out) throws IOException, SOCKSException { + int nMethods = in.readByte() & 0xff; + boolean methodOk = false; + int method = Method.NO_ACCEPTABLE_METHODS; + + for (int i = 0; i < nMethods; ++i) { + method = in.readByte() & 0xff; + if (method == Method.NO_AUTH_REQUIRED) { + // That's fine, we do support this method + break; + } + } + + boolean canContinue = false; + switch (method) { + case Method.NO_AUTH_REQUIRED: + _log.debug("no authentication required"); + sendInitReply(Method.NO_AUTH_REQUIRED, out); + return; + default: + _log.debug("no suitable authentication methods found (" + + Integer.toHexString(method)+ ")"); + sendInitReply(Method.NO_ACCEPTABLE_METHODS, out); + throw new SOCKSException("Unsupported authentication method"); + } + } + + /** + * SOCKS5 request management. This method assumes that all the + * stuff preceding or enveloping the actual request (e.g. protocol + * initialization, integrity/confidentiality encapsulations, etc) + * has been stripped out of the input/output streams. + */ + private void manageRequest(DataInputStream in, + DataOutputStream out) throws IOException, SOCKSException { + int socksVer = in.readByte() & 0xff; + if (socksVer != SOCKS_VERSION_5) { + _log.debug("error in SOCKS5 request (protocol != 5? wtf?)"); + throw new SOCKSException("Invalid protocol version in request"); + } + + int command = in.readByte() & 0xff; + switch (command) { + case Command.CONNECT: + break; + case Command.BIND: + _log.debug("BIND command is not supported!"); + sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, + AddressType.DOMAINNAME, null, + "0.0.0.0", 0, out); + throw new SOCKSException("BIND command not supported"); + case Command.UDP_ASSOCIATE: + _log.debug("UDP ASSOCIATE command is not supported!"); + sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, + AddressType.DOMAINNAME, null, + "0.0.0.0", 0, out); + throw new SOCKSException("UDP ASSOCIATE command not supported"); + default: + _log.debug("unknown command in request (" + + Integer.toHexString(command) + ")"); + throw new SOCKSException("Invalid command in request"); + } + + { + // Reserved byte, should be 0x00 + byte rsv = in.readByte(); + } + + int addressType = in.readByte() & 0xff; + switch (addressType) { + case AddressType.IPV4: + connHostName = new String(""); + for (int i = 0; i < 4; ++i) { + int octet = in.readByte() & 0xff; + connHostName += Integer.toString(octet); + if (i != 3) { + connHostName += "."; + } + } + _log.warn("IPV4 address type in request: " + connHostName + + ". Is your client secure?"); + break; + case AddressType.DOMAINNAME: + { + int addrLen = in.readByte() & 0xff; + if (addrLen == 0) { + _log.debug("0-sized address length? wtf?"); + throw new SOCKSException("Illegal DOMAINNAME length"); + } + byte addr[] = new byte[addrLen]; + in.readFully(addr); + connHostName = new String(addr); + } + _log.debug("DOMAINNAME address type in request: " + connHostName); + break; + case AddressType.IPV6: + _log.warn("IP V6 address type in request! Is your client secure?" + + " (IPv6 is not supported, anyway :-)"); + sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, + AddressType.DOMAINNAME, null, + "0.0.0.0", 0, out); + throw new SOCKSException("IPV6 addresses not supported"); + default: + _log.debug("unknown address type in request (" + + Integer.toHexString(command) + ")"); + throw new SOCKSException("Invalid addresses type in request"); + } + + connPort = in.readUnsignedShort(); + if (connPort == 0) { + _log.debug("trying to connect to TCP port 0? Dropping!"); + throw new SOCKSException("Invalid port number in request"); + } + } + + protected void confirmConnection() throws SOCKSException { + DataInputStream in; + DataOutputStream out; + try { + out = new DataOutputStream(clientSock.getOutputStream()); + + sendRequestReply(Reply.SUCCEEDED, + AddressType.IPV4, + InetAddress.getByName("127.0.0.1"), + null, 1, out); + } catch (IOException e) { + throw new SOCKSException("Connection error (" + + e.getMessage() + ")"); + } + } + + /** + * Send the specified reply during SOCKS5 initialization + */ + private void sendInitReply(int replyCode, + DataOutputStream out) throws IOException { + ByteArrayOutputStream reps = new ByteArrayOutputStream(); + + reps.write(SOCKS_VERSION_5); + reps.write(replyCode); + + byte[] reply = reps.toByteArray(); + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("Sending init reply:\n" + HexDump.dump(reply)); + } + + out.write(reply); + } + + /** + * Send the specified reply to a request of the client. Either + * one of inetAddr or domainName can be null, depending on + * addressType. + */ + private void sendRequestReply(int replyCode, + int addressType, + InetAddress inetAddr, + String domainName, + int bindPort, + DataOutputStream out) throws IOException { + ByteArrayOutputStream reps = new ByteArrayOutputStream(); + DataOutputStream dreps = new DataOutputStream(reps); + + dreps.write(SOCKS_VERSION_5); + dreps.write(replyCode); + + // Reserved byte, should be 0x00 + dreps.write(0x00); + + dreps.write(addressType); + + switch (addressType) { + case AddressType.IPV4: + dreps.write(inetAddr.getAddress()); + break; + case AddressType.DOMAINNAME: + dreps.writeByte(domainName.length()); + dreps.writeBytes(domainName); + break; + default: + _log.error("unknown address type passed to sendReply() (" + + Integer.toHexString(addressType) + ")! wtf?"); + return; + } + + dreps.writeShort(bindPort); + + byte[] reply = reps.toByteArray(); + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("Sending request reply:\n" + HexDump.dump(reply)); + } + + out.write(reply); + } + + + /* + * Some namespaces to enclose SOCKS protocol codes + */ + private class Method { + private static final int NO_AUTH_REQUIRED = 0x00; + private static final int NO_ACCEPTABLE_METHODS = 0xff; + } + + private class AddressType { + private static final int IPV4 = 0x01; + private static final int DOMAINNAME = 0x03; + private static final int IPV6 = 0x04; + } + + private class Command { + private static final int CONNECT = 0x01; + private static final int BIND = 0x02; + private static final int UDP_ASSOCIATE = 0x03; + } + + private class Reply { + private static final int SUCCEEDED = 0x00; + private static final int GENERAL_SOCKS_SERVER_FAILURE = 0x01; + private static final int CONNECTION_NOT_ALLOWED_BY_RULESET = 0x02; + private static final int NETWORK_UNREACHABLE = 0x03; + private static final int HOST_UNREACHABLE = 0x04; + private static final int CONNECTION_REFUSED = 0x05; + private static final int TTL_EXPIRED = 0x06; + private static final int COMMAND_NOT_SUPPORTED = 0x07; + private static final int ADDRESS_TYPE_NOT_SUPPORTED = 0x08; + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSException.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSException.java new file mode 100644 index 0000000000..489e057b4a --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSException.java @@ -0,0 +1,23 @@ +/* I2PSOCKSTunnel is released under the terms of the GNU GPL, + * with an additional exception. For further details, see the + * licensing terms in I2PTunnel.java. + * + * Copyright (c) 2004 by human + */ +package net.i2p.i2ptunnel.socks; + +/** + * Exception thrown by socket methods + * + * @author human + */ +public class SOCKSException extends Exception { + + public SOCKSException() { + super(); + } + + public SOCKSException(String s) { + super(s); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java new file mode 100644 index 0000000000..75894359b2 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java @@ -0,0 +1,100 @@ +/* I2PSOCKSTunnel is released under the terms of the GNU GPL, + * with an additional exception. For further details, see the + * licensing terms in I2PTunnel.java. + * + * Copyright (c) 2004 by human + */ +package net.i2p.i2ptunnel.socks; + +import java.net.Socket; + +import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.client.streaming.I2PSocketManagerFactory; +import net.i2p.client.streaming.I2PSocketOptions; +import net.i2p.data.DataFormatException; +import net.i2p.I2PException; +import net.i2p.i2ptunnel.I2PTunnel; + +import net.i2p.util.Log; + +/** + * Abstract base class used by all SOCKS servers. + * + * @author human + */ +public abstract class SOCKSServer { + private static final Log _log = new Log(SOCKSServer.class); + + /* Details about the connection requested by client */ + protected String connHostName = null; + protected int connPort = 0; + + I2PSocket destSocket = null; + + Object FIXME = new Object(); + + /** + * Perform server initialization (expecially regarding protected + * variables). + */ + protected abstract void setupServer() throws SOCKSException; + + /** + * Get a socket that can be used to send/receive 8-bit clean data + * to/from the client. + * + * @return a Socket connected with the client + */ + public abstract Socket getClientSocket() throws SOCKSException; + + + /** + * Confirm to the client that the connection has succeeded + */ + protected abstract void confirmConnection() throws SOCKSException; + + /** + * Get an I2PSocket that can be used to send/receive 8-bit clean data + * to/from the destination of the SOCKS connection. + * + * @return an I2PSocket connected with the destination + */ + public I2PSocket getDestinationI2PSocket() throws SOCKSException { + setupServer(); + + if (connHostName == null) { + _log.error("BUG: destination host name has not been initialized!"); + throw new SOCKSException("BUG! See the logs!"); + } + if (connPort == 0) { + _log.error("BUG: destination port has not been initialized!"); + throw new SOCKSException("BUG! See the logs!"); + } + + // FIXME: here we should read our config file, select an + // outproxy, and instantiate the proper socket class that + // handles the outproxy itself (SOCKS4a, SOCKS5, HTTP CONNECT...). + I2PSocket destSock; + + try { + if (connHostName.toLowerCase().endsWith(".i2p")) { + _log.debug("connecting to " + connHostName + "..."); + I2PSocketManager sm = I2PSocketManagerFactory.createManager(); + destSock = sm.connect(I2PTunnel.destFromName(connHostName), + new I2PSocketOptions()); + confirmConnection(); + _log.debug("connection confirmed - exchanging data..."); + } else { + _log.error("We don't support outproxies (yet)"); + throw new SOCKSException("Ouproxies not supported (yet)"); + } + } catch (DataFormatException e) { + throw new SOCKSException("Error in destination format"); + } catch (I2PException e) { + throw new SOCKSException("I2P error (" + e.getMessage() + ")"); + } + + return destSock; + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java new file mode 100644 index 0000000000..9f823e54b1 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java @@ -0,0 +1,53 @@ +/* I2PSOCKSTunnel is released under the terms of the GNU GPL, + * with an additional exception. For further details, see the + * licensing terms in I2PTunnel.java. + * + * Copyright (c) 2004 by human + */ +package net.i2p.i2ptunnel.socks; + +import java.io.DataInputStream; +import java.io.IOException; +import java.net.Socket; + +import net.i2p.util.Log; + +/** + * Factory class for creating SOCKS forwarders through I2P + */ +public class SOCKSServerFactory { + private final static Log _log = new Log(SOCKSServerFactory.class); + + /** + * Create a new SOCKS server, using the provided socket (that must + * be connected to a client) to select the proper SOCKS protocol + * version. This method wil strip the SOCKS VER field from the + * provided sockets's input stream. + * + * @param s a Socket used to choose the SOCKS server type + */ + public static SOCKSServer createSOCKSServer(Socket s) throws SOCKSException { + SOCKSServer serv; + + try { + DataInputStream in = new DataInputStream(s.getInputStream()); + int socksVer = in.readByte(); + + switch (socksVer) { + case 0x05: // SOCKS version 5 + serv = new SOCKS5Server(s); + break; + default: + _log.debug("SOCKS protocol version not supported (" + + Integer.toHexString(socksVer) + ")"); + return null; + } + } catch (IOException e) { + _log.debug("error reading SOCKS protocol version"); + throw new SOCKSException("Connection error (" + + e.getMessage() + ")"); + } + + return serv; + } +} diff --git a/apps/ministreaming/doc/protocol.txt b/apps/ministreaming/doc/protocol.txt new file mode 100644 index 0000000000..d7fe7d35c9 --- /dev/null +++ b/apps/ministreaming/doc/protocol.txt @@ -0,0 +1,83 @@ +ministreaming protocol: +******************* + +Each message looks like the following + +1 byte type +3 byte id +x byte payload. + +messages from client to server have type 0xA0 to 0xAF, +messages from server to client have type 0x50 to 0x5F. + +Each "connections" has 2 IDs, a client ID and a server ID. +These IDs may be any 3-byte values except 00 00 00, which is reserved. + +All connections are created as PROP_RELIABILITY_GUARANTEED. + +"actions" are the things a proper tunnel implementation SHOULD do +when it receives such a message. + + +Client->Server: +=============== + +0xA0 Send data + id: the server id + payload: the data to send + actions: send the data to the TCP connection + +0xA1 SYN + id: the client id + payload: the public key dynamically created by the client + actions: create a server ID and create a TCP connection. When successful, + send an ACK back, otherwise a close. + +0xA2 Close + id: the server id + payload: nothing + actions: close the connection + +Server->Client +============== + +0x50 Send data + id: the client id + payload: the data to send + actions: send the data to the TCP connection + +0x51 ACK + id: the client id + payload: the server id + actions: nothing + +0x52 Close + id: the client id + payload: nothing + actions: close the connection + + +Sample conversations: +===================== + +a) Service not available (e.g. the server on the TCP port is not running) + +C->S A1 12 34 56 key... (SYN, client ID = 12 34 56) +S->C 52 12 34 56 (Close) + +b) Service available, server sends data, client closes + +C->S A1 23 45 67 key... (SYN) +S->C 51 23 45 67 98 76 54 (ACK, server ID = 98 76 54) +S->C 50 23 45 67 data (Send data) +C->S A2 98 76 54 (Close) + +c) Service available, client sends data first, server closes after answer (HTTP) + +C->S A1 11 11 11 key... (SYN) +S->C 51 11 11 11 FF FF FF (ACK) +C->S A0 FF FF FF data (send data) +S->C 50 11 11 11 data (answer with data) +S->C 50 11 11 11 data (more data) +S->C 51 11 11 11 (Close) + diff --git a/apps/ministreaming/doc/readme.license.txt b/apps/ministreaming/doc/readme.license.txt new file mode 100644 index 0000000000..50a6a709a0 --- /dev/null +++ b/apps/ministreaming/doc/readme.license.txt @@ -0,0 +1,10 @@ +$Id$ + +the i2p/apps/ministreaming module is the root of the +ministreaming library, and everything within it +is released according to the terms of the I2P +license policy. That means everything contained +within the i2p/apps/ministreaming module is released +under a BSD license unless otherwise marked. +Alternate licenses that may be used include Cryptix, +MIT, as well as code granted into the public domain. diff --git a/apps/ministreaming/java/build.xml b/apps/ministreaming/java/build.xml new file mode 100644 index 0000000000..ff45583f93 --- /dev/null +++ b/apps/ministreaming/java/build.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/ByteCollector.java b/apps/ministreaming/java/src/net/i2p/client/streaming/ByteCollector.java new file mode 100644 index 0000000000..6d459ae327 --- /dev/null +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/ByteCollector.java @@ -0,0 +1,159 @@ +package net.i2p.client.streaming; + +/** Like a StringBuffer, but for bytes */ +public class ByteCollector { + byte[] contents; + int size; + + public ByteCollector() { + contents=new byte[80]; + size=0; + } + + public ByteCollector(byte[] b) { + this(); + append(b); + } + + public ByteCollector(byte b) { + this(); + append(b); + } + + public ByteCollector append (byte b) { + ensureCapacity(size+1); + contents[size++]=b; + return this; + } + + public ByteCollector append (byte[] b) { + ensureCapacity(size+b.length); + System.arraycopy(b,0,contents,size,b.length); + size+=b.length; + return this; + } + + public ByteCollector append(byte[] b, int len) { + return append(b,0,len); + } + + public ByteCollector append(byte[] b, int off, int len) { + ensureCapacity(size+len); + System.arraycopy(b,off,contents,size,len); + size+=len; + return this; + } + + public ByteCollector append(ByteCollector bc) { + // optimieren? + return append(bc.toByteArray()); + } + + public byte[] toByteArray() { + byte[] result=new byte[size]; + System.arraycopy(contents,0,result,0,size); + return result; + } + + public byte[] startToByteArray(int maxlen) { + if (size < maxlen) { + byte[] res = toByteArray(); + clear(); + return res; + } else { + byte[] result = new byte[maxlen]; + System.arraycopy(contents,0,result,0,maxlen); + System.arraycopy(contents,maxlen,contents,0,size-maxlen); + size-=maxlen; + return result; + } + } + + public int getCurrentSize() { + return size; + } + + public boolean ensureCapacity(int cap) { + if (contents.length1) + System.arraycopy(contents,1,contents,0,--size); + else + size=0; + return bb; + } +} diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java new file mode 100644 index 0000000000..8461d687b9 --- /dev/null +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java @@ -0,0 +1,29 @@ +package net.i2p.client.streaming; + +import net.i2p.I2PException; + +/** + * Defines how to listen for streaming peer connections + * + */ +public interface I2PServerSocket { + /** + * Closes the socket. + */ + public void close() throws I2PException; + + /** + * Waits for the next socket connecting. If a remote user tried to make a + * connection and the local application wasn't .accept()ing new connections, + * they should get refused (if .accept() doesnt occur in some small period) + * + * @throws I2PException if there is a problem with reading a new socket + * from the data available (aka the I2PSession closed, etc) + */ + public I2PSocket accept() throws I2PException; + + /** + * Access the manager which is coordinating the server socket + */ + public I2PSocketManager getManager(); +} diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocketImpl.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocketImpl.java new file mode 100644 index 0000000000..3cafcb9d68 --- /dev/null +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocketImpl.java @@ -0,0 +1,50 @@ +package net.i2p.client.streaming; + +import net.i2p.I2PException; +import net.i2p.util.Log; + +/** + * Initial stub implementation for the server socket + * + */ +class I2PServerSocketImpl implements I2PServerSocket { + private final static Log _log = new Log(I2PServerSocketImpl.class); + private I2PSocketManager mgr; + private I2PSocket cached=null; // buffer one socket here + + public I2PServerSocketImpl(I2PSocketManager mgr) { + this.mgr = mgr; + } + + public synchronized I2PSocket accept() throws I2PException { + while(cached == null) { + myWait(); + } + I2PSocket ret=cached; + cached=null; + notifyAll(); + _log.debug("TIMING: handed out accept result "+ret.hashCode()); + return ret; + } + + public synchronized boolean getNewSocket(I2PSocket s){ + while(cached != null) { + myWait(); + } + cached=s; + notifyAll(); + return true; + } + + public void close() throws I2PException { + //noop + } + + private void myWait() { + try{ + wait(); + } catch (InterruptedException ex) {} + } + + public I2PSocketManager getManager() { return mgr; } +} diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocket.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocket.java new file mode 100644 index 0000000000..6bee2a8e26 --- /dev/null +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocket.java @@ -0,0 +1,39 @@ +package net.i2p.client.streaming; + +import net.i2p.data.Destination; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +/** + * Minimalistic adapter between the socket api and I2PTunnel's way. + * Note that this interface is a "subinterface" of the interface + * defined in the "official" streaming api. + */ +public interface I2PSocket { + /** + * Return the Destination of this side of the socket. + */ + public Destination getThisDestination(); + + /** + * Return the destination of the peer. + */ + public Destination getPeerDestination(); + + /** + * Return an InputStream to read from the socket. + */ + public InputStream getInputStream() throws IOException; + + /** + * Return an OutputStream to write into the socket. + */ + public OutputStream getOutputStream() throws IOException; + + /** + * Closes the socket if not closed yet + */ + public void close() throws IOException; +} diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketImpl.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketImpl.java new file mode 100644 index 0000000000..73303e15ba --- /dev/null +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketImpl.java @@ -0,0 +1,335 @@ +package net.i2p.client.streaming; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; + +import net.i2p.I2PException; +import net.i2p.client.I2PSessionException; +import net.i2p.data.Destination; +import net.i2p.util.Log; + +/** + * Initial stub implementation for the socket + * + */ +class I2PSocketImpl implements I2PSocket { + private final static Log _log = new Log(I2PSocketImpl.class); + + public static final int MAX_PACKET_SIZE = 1024*32; + public static final int PACKET_DELAY=100; + + private I2PSocketManager manager; + private Destination local; + private Destination remote; + private String localID; + private String remoteID; + private Object remoteIDWaiter = new Object(); + private I2PInputStream in; + private I2POutputStream out; + private boolean outgoing; + private Object flagLock = new Object(); + private boolean closed = false, sendClose=true, closed2=false; + + public I2PSocketImpl(Destination peer, I2PSocketManager mgr, + boolean outgoing, String localID) { + this.outgoing=outgoing; + manager = mgr; + remote = peer; + local = mgr.getSession().getMyDestination(); + in = new I2PInputStream(); + I2PInputStream pin = new I2PInputStream(); + out = new I2POutputStream(pin); + new I2PSocketRunner(pin); + this.localID = localID; + } + + public String getLocalID() { + return localID; + } + + public void setRemoteID(String id) { + synchronized(remoteIDWaiter) { + remoteID=id; + remoteIDWaiter.notifyAll(); + } + } + + public String getRemoteID(boolean wait) throws InterruptedIOException { + return getRemoteID(wait, -1); + } + public String getRemoteID(boolean wait, long maxWait) throws InterruptedIOException { + long dieAfter = System.currentTimeMillis() + maxWait; + synchronized(remoteIDWaiter) { + while (wait && remoteID==null) { + try { + if (maxWait > 0) + remoteIDWaiter.wait(maxWait); + else + remoteIDWaiter.wait(); + } catch (InterruptedException ex) {} + + if ( (maxWait > 0) && (System.currentTimeMillis() > dieAfter) ) + throw new InterruptedIOException("Timed out waiting for remote ID"); + } + if (wait) { + _log.debug("TIMING: RemoteID set to " + I2PSocketManager.getReadableForm(remoteID) +" for "+this.hashCode()); + } + return remoteID; + } + } + + public String getRemoteID() throws InterruptedIOException { + return getRemoteID(false); + } + + public void queueData(byte[] data) { + in.queueData(data); + } + + /** + * Return the Destination of this side of the socket. + */ + public Destination getThisDestination() { return local; } + + /** + * Return the destination of the peer. + */ + public Destination getPeerDestination() { return remote; } + + /** + * Return an InputStream to read from the socket. + */ + public InputStream getInputStream() throws IOException { + if ( (in == null) ) + throw new IOException("Not connected"); + return in; + } + + /** + * Return an OutputStream to write into the socket. + */ + public OutputStream getOutputStream() throws IOException { + if ( (out == null) ) + throw new IOException("Not connected"); + return out; + } + + /** + * Closes the socket if not closed yet + */ + public void close() throws IOException { + synchronized(flagLock) { + _log.debug("Closing connection"); + closed=true; + } + out.close(); + in.notifyClosed(); + } + + public void internalClose() { + synchronized(flagLock) { + closed=true; + closed2=true; + sendClose=false; + } + out.close(); + in.notifyClosed(); + } + + + private byte getMask(int add) { + return (byte)((outgoing?(byte)0xA0:(byte)0x50)+(byte)add); + } + + //-------------------------------------------------- + public class I2PInputStream extends InputStream { + + private ByteCollector bc = new ByteCollector(); + + public int read() throws IOException { + byte[] b = new byte[1]; + int res = read(b); + if (res == 1) return b[0] & 0xff; + if (res == -1) return -1; + throw new RuntimeException("Incorrect read() result"); + } + + public synchronized int read(byte[] b, int off, int len) throws IOException { + _log.debug("Read called: "+this.hashCode()); + if (len==0) return 0; + byte[] read = bc.startToByteArray(len); + while (read.length==0) { + synchronized(flagLock) { + if (closed){ + _log.debug("Closed is set, so closing stream: "+this.hashCode()); + return -1; + } + } + try { + wait(); + } catch (InterruptedException ex) {} + read = bc.startToByteArray(len); + } + if (read.length>len) throw new RuntimeException("BUG"); + System.arraycopy(read,0,b,off,read.length); + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("Read from I2PInputStream " + this.hashCode() + + " returned "+read.length+" bytes"); + } + //if (_log.shouldLog(Log.DEBUG)) { + // _log.debug("Read from I2PInputStream " + this.hashCode() + // + " returned "+read.length+" bytes:\n" + // + HexDump.dump(read)); + //} + return read.length; + } + + public int available() { + return bc.getCurrentSize(); + } + + public void queueData(byte[] data) { + queueData(data,0,data.length); + } + + public synchronized void queueData(byte[] data, int off, int len) { + _log.debug("Insert "+len+" bytes into queue: "+this.hashCode()); + bc.append(data, off, len); + notifyAll(); + } + + public synchronized void notifyClosed() { + notifyAll(); + } + + } + + public class I2POutputStream extends OutputStream { + + public I2PInputStream sendTo; + + public I2POutputStream(I2PInputStream sendTo) { + this.sendTo=sendTo; + } + public void write(int b) throws IOException { + write(new byte[] {(byte)b}); + } + + public void write (byte[] b, int off, int len) throws IOException { + sendTo.queueData(b,off,len); + } + + public void close() { + sendTo.notifyClosed(); + } + } + + public class I2PSocketRunner extends Thread { + + public InputStream in; + + public I2PSocketRunner(InputStream in) { + _log.debug("Runner's input stream is: "+in.hashCode()); + this.in=in; + start(); + } + + public void run() { + byte[] buffer = new byte[MAX_PACKET_SIZE]; + ByteCollector bc = new ByteCollector(); + boolean sent = true; + try { + int len, bcsize; +// try { + while (true) { + len = in.read(buffer); + bcsize = bc.getCurrentSize(); + if (len != -1) { + bc.append(buffer,len); + } else if (bcsize == 0) { + break; + } + if ((bcsize < MAX_PACKET_SIZE) + && (in.available()==0)) { + _log.debug("Runner Point d: "+this.hashCode()); + + try { + Thread.sleep(PACKET_DELAY); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + if ((bcsize >= MAX_PACKET_SIZE) + || (in.available()==0) ) { + byte[] data = bc.startToByteArray(MAX_PACKET_SIZE); + if (data.length > 0) { + _log.debug("Message size is: "+data.length); + sent = sendBlock(data); + if (!sent) { + _log.error("Error sending message to peer. Killing socket runner"); + break; + } + } + } + } + if ((bc.getCurrentSize() > 0) && sent) { + _log.error("A SCARY MONSTER HAS EATEN SOME DATA! " + + "(input stream: " + in.hashCode() + "; " + + "queue size: " + bc.getCurrentSize() + ")"); + } + synchronized(flagLock) { + closed2=true; + } +// } catch (IOException ex) { +// if (_log.shouldLog(Log.INFO)) +// _log.info("Error reading and writing", ex); +// } + boolean sc; + synchronized(flagLock) { + sc=sendClose; + } // FIXME: Race here? + if (sc) { + _log.info("Sending close packet: "+outgoing); + byte[] packet = I2PSocketManager.makePacket + ((byte)(getMask(0x02)),remoteID, new byte[0]); + synchronized(manager.getSession()) { + sent = manager.getSession().sendMessage(remote, packet); + } + if (!sent) { + _log.error("Error sending close packet to peer"); + } + } + manager.removeSocket(I2PSocketImpl.this); + } catch (IOException ex) { + // WHOEVER removes this event on inconsistent + // state before fixing the inconsistent state (a + // reference on the socket in the socket manager + // etc.) will get hanged by me personally -- mihi + _log.error("Error running - **INCONSISTENT STATE!!!**", ex); + } catch (I2PException ex) { + _log.error("Error running - **INCONSISTENT STATE!!!**" , ex); + } + } + + private boolean sendBlock(byte data[]) throws I2PSessionException { + _log.debug("TIMING: Block to send for "+I2PSocketImpl.this.hashCode()); + if (remoteID==null) { + _log.error("NULL REMOTEID"); + return false; + } + byte[] packet = I2PSocketManager.makePacket(getMask(0x00), remoteID, + data); + boolean sent; + synchronized(flagLock) { + if (closed2) return false; + } + synchronized(manager.getSession()) { + sent = manager.getSession().sendMessage(remote, packet); + } + return sent; + } + } +} diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManager.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManager.java new file mode 100644 index 0000000000..d660b203d0 --- /dev/null +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManager.java @@ -0,0 +1,386 @@ +/* + * licensed under BSD license... + * (if you know the proper clause for that, add it ...) + */ +package net.i2p.client.streaming; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import net.i2p.I2PException; +import net.i2p.client.I2PSession; +import net.i2p.client.I2PSessionListener; +import net.i2p.data.Base64; +import net.i2p.data.Destination; +import net.i2p.util.Log; + +/** + * Centralize the coordination and multiplexing of the local client's streaming. + * There should be one I2PSocketManager for each I2PSession, and if an application + * is sending and receiving data through the streaming library using an + * I2PSocketManager, it should not attempt to call I2PSession's setSessionListener + * or receive any messages with its .receiveMessage + * + */ +public class I2PSocketManager implements I2PSessionListener { + private final static Log _log = new Log(I2PSocketManager.class); + private I2PSession _session; + private I2PServerSocketImpl _serverSocket; + private Object lock = new Object(); // for locking socket lists + private HashMap _outSockets; + private HashMap _inSockets; + private I2PSocketOptions _defaultOptions; + + public static final int PUBKEY_LENGTH=387; + + + public I2PSocketManager() { + _session=null; + _serverSocket = new I2PServerSocketImpl(this); + _inSockets = new HashMap(16); + _outSockets = new HashMap(16); + } + + public I2PSession getSession() { + return _session; + } + + public void setSession(I2PSession session) { + _session = session; + if (session != null) + session.setSessionListener(this); + } + + public void disconnected(I2PSession session) { + _log.error("Disconnected from the session"); + } + + public void errorOccurred(I2PSession session, String message, Throwable error) { + _log.error("Error occurred: [" + message + "]", error); + } + + public void messageAvailable(I2PSession session, int msgId, long size) { + try { + I2PSocketImpl s; + byte msg[] = session.receiveMessage(msgId); + if (msg.length == 1 && msg[0] == -1) { + _log.debug("Ping received"); + return; + } + if (msg.length <4) { + _log.error("==== packet too short ===="); + return; + } + int type = msg[0] & 0xff; + String id = new String(new byte[] {msg[1], msg[2], msg[3]}, + "ISO-8859-1"); + byte[] payload = new byte[msg.length-4]; + System.arraycopy(msg, 4, payload, 0, payload.length); + _log.debug("Message read: type = [" + Integer.toHexString(type) + + "] id = [" + getReadableForm(id)+ + "] payload length: " + payload.length + "]"); + synchronized(lock) { + switch(type) { + case 0x51: // ACK outgoing + s = (I2PSocketImpl) _outSockets.get(id); + if (s == null) { + _log.warn("No socket responsible for ACK packet"); + return; + } + if (payload.length==3 && s.getRemoteID(false)==null) { + String newID = new String(payload, + "ISO-8859-1"); + s.setRemoteID(newID); + return; + } else { + if (payload.length != 3) + _log.warn("Ack packet had " + payload.length + " bytes"); + else + _log.warn("Remote ID already exists? " + s.getRemoteID()); + return; + } + case 0x52: // disconnect outgoing + _log.debug("*Disconnect outgoing!"); + try { + s = (I2PSocketImpl) _outSockets.get(id); + if (payload.length==0 && s != null) { + s.internalClose(); + _outSockets.remove(id); + return; + } else { + if (payload.length > 0) + _log.warn("Disconnect packet had " + payload.length + " bytes"); + return; + } + } catch (Exception t) { + _log.error("Ignoring error on disconnect", t); + } + case 0x50: // packet send outgoing + _log.debug("*Packet send outgoing [" + payload.length + "]"); + s = (I2PSocketImpl) _outSockets.get(id); + if (s != null) { + s.queueData(payload); + return; + } else { + _log.error("Null socket with data available"); + throw new IllegalStateException("Null socket with data available"); + } + case 0xA1: // SYN incoming + _log.debug("*Syn!"); + if (payload.length==PUBKEY_LENGTH) { + String newLocalID = makeID(_inSockets); + Destination d = new Destination(); + d.readBytes(new ByteArrayInputStream(payload)); + + s = new I2PSocketImpl(d, this, false, + newLocalID); + s.setRemoteID(id); + if (_serverSocket.getNewSocket(s)) { + _inSockets.put(newLocalID, s); + byte[] packet = makePacket + ((byte)0x51, id, + newLocalID.getBytes("ISO-8859-1")); + boolean replySentOk = false; + synchronized(_session) { + replySentOk = _session.sendMessage(d, packet); + } + if (!replySentOk) { + _log.error("Error sending reply to " + + d.calculateHash().toBase64() + + " in response to a new con message", + new Exception("Failed creation")); + s.internalClose(); + } + } else { + byte[] packet = + (" "+id).getBytes("ISO-8859-1"); + packet[0]=0x52; + boolean nackSent = session.sendMessage(d, packet); + if (!nackSent) { + _log.error("Error sending NACK for session creation"); + } + s.internalClose(); + } + return; + } else { + _log.error("Syn packet that has a payload not equal to the pubkey length (" + payload.length + " != " + PUBKEY_LENGTH + ")"); + return; + } + case 0xA2: // disconnect incoming + _log.debug("*Disconnect incoming!"); + try { + s = (I2PSocketImpl) _inSockets.get(id); + if (payload.length==0 && s != null) { + s.internalClose(); + _inSockets.remove(id); + return; + } else { + if (payload.length > 0) + _log.warn("Disconnect packet had " + payload.length + " bytes"); + return; + } + } catch (Exception t) { + _log.error("Ignoring error on disconnect", t); + return; + } + case 0xA0: // packet send incoming + _log.debug("*Packet send incoming [" + payload.length + "]"); + s = (I2PSocketImpl) _inSockets.get(id); + if (s != null) { + s.queueData(payload); + return; + } else { + _log.error("Null socket with data available"); + throw new IllegalStateException("Null socket with data available"); + } + case 0xFF: // ignore + return; + } + _log.error("\n\n=============== Unknown packet! "+ + "============"+ + "\nType: "+(int)type+ + "\nID: " + getReadableForm(id)+ + "\nBase64'ed Data: "+Base64.encode(payload)+ + "\n\n\n"); + if (id != null) { + _inSockets.remove(id); + _outSockets.remove(id); + } + } + } catch (I2PException ise) { + _log.error("Error processing", ise); + } catch (IOException ioe) { + _log.error("Error processing", ioe); + } catch (IllegalStateException ise) { + _log.debug("Error processing", ise); + } + } + + public void reportAbuse(I2PSession session, int severity) { + _log.error("Abuse reported [" + severity + "]"); + } + + public void setDefaultOptions(I2PSocketOptions options) { _defaultOptions = options; } + + public I2PSocketOptions getDefaultOptions() { return _defaultOptions ; } + + public I2PServerSocket getServerSocket() { return _serverSocket; } + + /** + * Create a new connected socket (block until the socket is created) + * + * @throws I2PException if there is a problem connecting + */ + public I2PSocket connect(Destination peer, I2PSocketOptions options) throws I2PException { + + String localID, lcID; + I2PSocketImpl s; + synchronized(lock) { + localID=makeID(_outSockets); + lcID=getReadableForm(localID); + s = new I2PSocketImpl(peer, this, true, localID); + _outSockets.put(s.getLocalID(),s); + } + try { + ByteArrayOutputStream pubkey = new ByteArrayOutputStream(); + _session.getMyDestination().writeBytes(pubkey); + String remoteID; + byte[] packet = makePacket((byte)0xA1, localID, + pubkey.toByteArray()); + boolean sent = false; + synchronized(_session) { + sent = _session.sendMessage(peer, packet); + } + if (!sent) { + _log.info("Unable to send & receive ack for SYN packet"); + synchronized(lock) { + _outSockets.remove(s.getLocalID()); + } + throw new I2PException("Unable to reach peer"); + } + remoteID = s.getRemoteID(true, options.getConnectTimeout()); + if ("".equals(remoteID)) { + throw new I2PException("Unable to reach peer"); + } + _log.debug("TIMING: s given out for remoteID "+getReadableForm(remoteID)); + return s; + } catch (InterruptedIOException ioe) { + _log.error("Timeout waiting for ack from syn for id " + getReadableForm(lcID), ioe); + synchronized(lock) { + _outSockets.remove(s.getLocalID()); + } + throw new I2PException("Timeout waiting for ack"); + } catch (IOException ex) { + _log.error("Error sending syn on id " + getReadableForm(lcID), ex); + synchronized(lock) { + _outSockets.remove(s.getLocalID()); + } + throw new I2PException("IOException occurred"); + } catch (I2PException ex) { + _log.info("Error sending syn on id " + getReadableForm(lcID), ex); + synchronized(lock) { + _outSockets.remove(s.getLocalID()); + } + throw ex; + } + } + + public I2PSocket connect(Destination peer) throws I2PException { + return connect(peer, null); + } + + /** + * Retrieve a set of currently connected I2PSockets, either initiated locally or remotely. + * + */ + public Set listSockets() { + Set sockets = new HashSet(8); + synchronized (lock) { + sockets.addAll(_inSockets.values()); + sockets.addAll(_outSockets.values()); + } + return sockets; + } + + /** + * Ping the specified peer, returning true if they replied to the ping within + * the timeout specified, false otherwise. This call blocks. + * + */ + public boolean ping(Destination peer, long timeoutMs) { + try { + return _session.sendMessage(peer, new byte[] {(byte)0xFF}); + } catch (I2PException ex) { + _log.error("I2PException:",ex); + return false; + } + } + + public void removeSocket(I2PSocketImpl sock) { + synchronized(lock) { + _inSockets.remove(sock.getLocalID()); + _outSockets.remove(sock.getLocalID()); + } + } + + public static String getReadableForm(String id) { + try { + if (id.length() != 3) return "Bogus"; + return Base64.encode(id.getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException ex) { + ex.printStackTrace(); + return null; + } + } + + /** + * Create a new part the connection ID that is locally unique + * + * @param uniqueIn map of already known local IDs so we don't collide. WARNING - NOT THREADSAFE! + */ + public static String makeID(HashMap uniqueIn) { + String newID; + try { + do { + int id = (int)(Math.random()*16777215+1); + byte[] nid = new byte[3]; + nid[0]=(byte)(id / 65536); + nid[1] = (byte)((id/256) % 256); + nid[2]= (byte)(id %256); + newID = new String(nid, "ISO-8859-1"); + } while (uniqueIn.get(newID) != null); + return newID; + } catch (UnsupportedEncodingException ex) { + ex.printStackTrace(); + return null; + } + } + + /** + * Create a new packet of the given type for the specified connection containing + * the given payload + */ + public static byte[] makePacket(byte type, String id, byte[] payload) { + try { + byte[] packet = new byte[payload.length+4]; + packet[0]=type; + byte[] temp = id.getBytes("ISO-8859-1"); + if (temp.length != 3) + throw new RuntimeException("Incorrect ID length: "+ + temp.length); + System.arraycopy(temp,0,packet,1,3); + System.arraycopy(payload,0,packet,4,payload.length); + return packet; + } catch (UnsupportedEncodingException ex) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error building the packet", ex); + return new byte[0]; + } + } +} diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java new file mode 100644 index 0000000000..2aa1828a33 --- /dev/null +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java @@ -0,0 +1,86 @@ +package net.i2p.client.streaming; + +import net.i2p.client.I2PSession; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PClientFactory; +import net.i2p.client.I2PSessionException; +import net.i2p.I2PException; +import net.i2p.data.Destination; +import net.i2p.util.Log; + +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import java.util.Properties; + +/** + * Simplify the creation of I2PSession and transient I2P Destination objects if + * necessary to create a socket manager. This class is most likely how classes + * will begin their use of the socket library + * + */ +public class I2PSocketManagerFactory { + private final static Log _log = new Log(I2PSocketManagerFactory.class); + + /** + * Create a socket manager using a brand new destination connected to the + * I2CP router on the local machine on the default port (7654). + * + * @return the newly created socket manager, or null if there were errors + */ + public static I2PSocketManager createManager() { + return createManager("localhost", 7654, new Properties()); + } + + /** + * Create a socket manager using a brand new destination connected to the + * I2CP router on the given machine reachable through the given port. + * + * @return the newly created socket manager, or null if there were errors + */ + public static I2PSocketManager createManager(String i2cpHost, int i2cpPort, Properties opts) { + I2PClient client = I2PClientFactory.createClient(); + ByteArrayOutputStream keyStream = new ByteArrayOutputStream(512); + try { + Destination dest = client.createDestination(keyStream); + ByteArrayInputStream in = new ByteArrayInputStream(keyStream.toByteArray()); + return createManager(in, i2cpHost, i2cpPort, opts); + } catch (IOException ioe) { + _log.error("Error creating the destination for socket manager", ioe); + return null; + } catch (I2PException ie) { + _log.error("Error creating the destination for socket manager", ie); + return null; + } + } + + /** + * Create a socket manager using the destination loaded from the given private key + * stream and connected to the I2CP router on the specified machine on the given + * port + * + * @return the newly created socket manager, or null if there were errors + */ + public static I2PSocketManager createManager(InputStream myPrivateKeyStream, String i2cpHost, int i2cpPort, Properties opts) { + I2PClient client = I2PClientFactory.createClient(); + opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_GUARANTEED); + opts.setProperty(I2PClient.PROP_TCP_HOST, i2cpHost); + opts.setProperty(I2PClient.PROP_TCP_PORT, ""+i2cpPort); + try { + I2PSession session = client.createSession(myPrivateKeyStream, opts); + session.connect(); + return createManager(session); + } catch (I2PSessionException ise) { + _log.error("Error creating session for socket manager", ise); + return null; + } + } + + private static I2PSocketManager createManager(I2PSession session) { + I2PSocketManager mgr = new I2PSocketManager(); + mgr.setSession(session); + return mgr; + } +} diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketOptions.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketOptions.java new file mode 100644 index 0000000000..99feb7c09b --- /dev/null +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketOptions.java @@ -0,0 +1,21 @@ +package net.i2p.client.streaming; + +/** + * Define the configuration for streaming and verifying data on the socket. + * No options available... + * + */ +public class I2PSocketOptions { + private long _connectTimeout; + public I2PSocketOptions() { + _connectTimeout = -1; + } + + /** + * How long we will wait for the ACK from a SYN, in milliseconds. + * + * @return milliseconds to wait, or -1 if we will wait indefinitely + */ + public long getConnectTimeout() { return _connectTimeout; } + public void setConnectTimeout(long ms) { _connectTimeout = ms; } +} diff --git a/apps/phttprelay/doc/readme.license.txt b/apps/phttprelay/doc/readme.license.txt new file mode 100644 index 0000000000..18f90e09bb --- /dev/null +++ b/apps/phttprelay/doc/readme.license.txt @@ -0,0 +1,10 @@ +$Id$ + +the i2p/apps/phttprelay module is the root of the +PHTTPRelay application, and everything within it +is released according to the terms of the I2P +license policy. That means everything contained +within the i2p/apps/phttprelay module is released +into the public domain unless otherwise marked. +Alternate licenses that may be used include BSD, +Cryptix, and MIT. diff --git a/apps/phttprelay/java/build.xml b/apps/phttprelay/java/build.xml new file mode 100644 index 0000000000..580ddb3f1b --- /dev/null +++ b/apps/phttprelay/java/build.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/phttprelay/java/lib/LICENSE.html b/apps/phttprelay/java/lib/LICENSE.html new file mode 100644 index 0000000000..678cdec531 --- /dev/null +++ b/apps/phttprelay/java/lib/LICENSE.html @@ -0,0 +1,159 @@ + + + Jetty License + + + +
Jetty License
+
$Revision: 1.1 $
+ +Preamble:

+ +The intent of this document is to state the conditions under which the +Jetty Package may be copied, such that the Copyright Holder maintains some +semblance of control over the development of the package, while giving the +users of the package the right to use, distribute and make reasonable +modifications to the Package in accordance with the goals and ideals of +the Open Source concept as described at +http://www.opensource.org. +

+It is the intent of this license to allow commercial usage of the Jetty +package, so long as the source code is distributed or suitable visible +credit given or other arrangements made with the copyright holders. + +

Definitions:

+ +

    +
  • "Jetty" refers to the collection of Java classes that are + distributed as a HTTP server with servlet capabilities and + associated utilities.

    + +

  • "Package" refers to the collection of files distributed by the + Copyright Holder, and derivatives of that collection of files + created through textual modification.

    + +

  • "Standard Version" refers to such a Package if it has not been + modified, or has been modified in accordance with the wishes + of the Copyright Holder.

    + +

  • "Copyright Holder" is whoever is named in the copyright or + copyrights for the package.
    + Mort Bay Consulting Pty. Ltd. (Australia) is the "Copyright + Holder" for the Jetty package.

    + +

  • "You" is you, if you're thinking about copying or distributing + this Package.

    + +

  • "Reasonable copying fee" is whatever you can justify on the + basis of media cost, duplication charges, time of people involved, + and so on. (You will not be required to justify it to the + Copyright Holder, but only to the computing community at large + as a market that must bear the fee.)

    + +

  • "Freely Available" means that no fee is charged for the item + itself, though there may be fees involved in handling the item. + It also means that recipients of the item may redistribute it + under the same conditions they received it.

    +

+ +0. The Jetty Package is Copyright (c) Mort Bay Consulting Pty. Ltd. +(Australia) and others. Individual files in this package may contain +additional copyright notices. The javax.servlet packages are copyright +Sun Microsystems Inc.

+ +1. The Standard Version of the Jetty package is +available from http://jetty.mortbay.org.

+ +2. You may make and distribute verbatim copies of the source form +of the Standard Version of this Package without restriction, provided that +you include this license and all of the original copyright notices +and associated disclaimers.

+ +3. You may make and distribute verbatim copies of the compiled form of the +Standard Version of this Package without restriction, provided that you +include this license.

+ +4. You may apply bug fixes, portability fixes and other modifications +derived from the Public Domain or from the Copyright Holder. A Package +modified in such a way shall still be considered the Standard Version.

+ +5. You may otherwise modify your copy of this Package in any way, provided +that you insert a prominent notice in each changed file stating how and +when you changed that file, and provided that you do at least ONE of the +following:

+ +

+a) Place your modifications in the Public Domain or otherwise make them +Freely Available, such as by posting said modifications to Usenet or +an equivalent medium, or placing the modifications on a major archive +site such as ftp.uu.net, or by allowing the Copyright Holder to include +your modifications in the Standard Version of the Package.

+ +b) Use the modified Package only within your corporation or organization.

+ +c) Rename any non-standard classes so the names do not conflict +with standard classes, which must also be provided, and provide +a separate manual page for each non-standard class that clearly +documents how it differs from the Standard Version.

+ +d) Make other arrangements with the Copyright Holder.

+

+ +6. You may distribute modifications or subsets of this Package in source +code or compiled form, provided that you do at least ONE of the following:

+ +

+a) Distribute this license and all original copyright messages, together +with instructions (in the about dialog, manual page or equivalent) on where +to get the complete Standard Version.

+ +b) Accompany the distribution with the machine-readable source of +the Package with your modifications. The modified package must include +this license and all of the original copyright notices and associated +disclaimers, together with instructions on where to get the complete +Standard Version.

+ +c) Make other arrangements with the Copyright Holder.

+

+ +7. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this Package. +You may not charge a fee for this Package itself. However, +you may distribute this Package in aggregate with other (possibly +commercial) programs as part of a larger (possibly commercial) software +distribution provided that you meet the other distribution requirements +of this license.

+ +8. Input to or the output produced from the programs of this Package +do not automatically fall under the copyright of this Package, but +belong to whomever generated them, and may be sold commercially, and +may be aggregated with this Package.

+ +9. Any program subroutines supplied by you and linked into this Package +shall not be considered part of this Package.

+ +10. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written +permission.

+ +11. This license may change with each release of a Standard Version of +the Package. You may choose to use the license associated with version +you are using or the license of the latest Standard Version.

+ +12. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.

+ +13. If any superior law implies a warranty, the sole remedy under such shall +be , at the Copyright Holders option either a) return of any price paid or +b) use or reasonable endeavours to repair or replace the software.

+ +14. This license shall be read under the laws of Australia.

+ +

The End
+ +
This license was derived from the Artistic license published +on http://www.opensource.com
+
+ + diff --git a/apps/phttprelay/java/lib/readme.txt b/apps/phttprelay/java/lib/readme.txt new file mode 100644 index 0000000000..d16d9e6791 --- /dev/null +++ b/apps/phttprelay/java/lib/readme.txt @@ -0,0 +1,6 @@ +The file javax.servlet.jar is distributed under the terms of LICENSE.html, +which is the implementation of the java servlet classes as retrieved from +http://jetty.mortbay.org/jetty/ + +It is only included to assist in building the phttprelay.war file on hosts +that do not have a servlet container / implementation. diff --git a/apps/phttprelay/java/src/net/i2p/phttprelay/CheckSendStatusServlet.java b/apps/phttprelay/java/src/net/i2p/phttprelay/CheckSendStatusServlet.java new file mode 100644 index 0000000000..e303f733cd --- /dev/null +++ b/apps/phttprelay/java/src/net/i2p/phttprelay/CheckSendStatusServlet.java @@ -0,0 +1,113 @@ +package net.i2p.phttprelay; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.File; +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Check the status of previous message delivery, returning either pending or + * unknown, where pending means that particular message ID for that particular + * target is still on the server, and unknown means it has either not been created + * or it has been sent successfully. It does this by sending HTTP 204 (NO CONTENT) + * for pending, and HTTP 404 (NOT FOUND) for unknown.

+ * + * This servlet should be set up in web.xml as follows: + * + * + * CheckSendStatus + * net.i2p.phttprelay.CheckSendStatusServlet + * + * baseDir + * /usr/local/jetty/phttprelayDir + * + * + * + * + * CheckSendStatus + * /phttpCheckSendStatus + * + * + * baseDir is the directory under which registrants and their pending messages are stored + * + */ +public class CheckSendStatusServlet extends PHTTPRelayServlet { + /* URL parameters on the check */ + + /** H(routerIdent).toBase64() of the target to receive the message */ + public final static String PARAM_SEND_TARGET = "target"; + /** msgId parameter */ + public final static String PARAM_MSG_ID = "msgId"; + + public final static String PROP_STATUS = "status"; + public final static String STATUS_PENDING = "pending"; + public final static String STATUS_UNKNOWN = "unknown"; + + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String target = req.getParameter(PARAM_SEND_TARGET); + String msgIdStr = req.getParameter(PARAM_MSG_ID); + + log("Checking status of [" + target + "] message [" + msgIdStr + "]"); + if (!isKnownMessage(target, msgIdStr)) { + log("Not known - its not pending"); + notPending(req, resp); + return; + } else { + log("Known - its still pending"); + pending(req, resp); + return; + } + } + + private boolean isKnownMessage(String target, String msgId) throws IOException { + if ( (target == null) || (target.trim().length() <= 0) ) return false; + if ( (msgId == null) || (msgId.trim().length() <= 0) ) return false; + File identDir = getIdentDir(target); + if (identDir.exists()) { + File identFile = new File(identDir, "identity.dat"); + if (identFile.exists()) { + // known and valid (maybe we need to check the file format... naw, fuck it + File msgFile = new File(identDir, "msg" + msgId + ".dat"); + if (msgFile.exists()) + return true; + else + return false; + } else { + return false; + } + } else { + return false; + } + } + + private void pending(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setStatus(HttpServletResponse.SC_OK); + ServletOutputStream out = resp.getOutputStream(); + StringBuffer buf = new StringBuffer(); + buf.append(PROP_STATUS).append('=').append(STATUS_PENDING).append('\n'); + out.write(buf.toString().getBytes()); + out.flush(); + out.close(); + } + + private void notPending(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setStatus(HttpServletResponse.SC_OK); + ServletOutputStream out = resp.getOutputStream(); + StringBuffer buf = new StringBuffer(); + buf.append(PROP_STATUS).append('=').append(STATUS_UNKNOWN).append('\n'); + out.write(buf.toString().getBytes()); + out.flush(); + out.close(); + } +} diff --git a/apps/phttprelay/java/src/net/i2p/phttprelay/LockManager.java b/apps/phttprelay/java/src/net/i2p/phttprelay/LockManager.java new file mode 100644 index 0000000000..c84941191d --- /dev/null +++ b/apps/phttprelay/java/src/net/i2p/phttprelay/LockManager.java @@ -0,0 +1,40 @@ +package net.i2p.phttprelay; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.HashSet; +import java.util.Set; + +/** + * Lock identities for updating messages (so that they aren't read / deleted + * while being written) + * + */ +class LockManager { + private volatile static Set _locks = new HashSet(); // target + + public static void lockIdent(String target) { + while (true) { + synchronized (_locks) { + if (!_locks.contains(target)) { + _locks.add(target); + return; + } + try { _locks.wait(1000); } catch (InterruptedException ie) {} + } + } + } + + public static void unlockIdent(String target) { + synchronized (_locks) { + _locks.remove(target); + _locks.notifyAll(); + } + } +} diff --git a/apps/phttprelay/java/src/net/i2p/phttprelay/PHTTPRelayServlet.java b/apps/phttprelay/java/src/net/i2p/phttprelay/PHTTPRelayServlet.java new file mode 100644 index 0000000000..6c4365fc86 --- /dev/null +++ b/apps/phttprelay/java/src/net/i2p/phttprelay/PHTTPRelayServlet.java @@ -0,0 +1,73 @@ +package net.i2p.phttprelay; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.util.Log; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +import java.io.File; +import java.io.IOException; + +abstract class PHTTPRelayServlet extends HttpServlet { + private Log _log = new Log(getClass()); + protected String _baseDir; + + /* config params */ + /*public final static String PARAM_BASEDIR = "baseDir";*/ + public final static String ENV_BASEDIR = "phttpRelay.baseDir"; + + /** match the clock fudge factor on the router, rather than importing the entire router cvs module */ + public final static long CLOCK_FUDGE_FACTOR = 1*60*1000; + + protected String buildURL(HttpServletRequest req, String path) { + StringBuffer buf = new StringBuffer(); + buf.append(req.getScheme()).append("://"); + buf.append(req.getServerName()).append(":").append(req.getServerPort()); + buf.append(req.getContextPath()); + buf.append(path); + log("URL built: " + buf.toString()); + return buf.toString(); + } + + protected File getIdentDir(String target) throws IOException { + if ( (_baseDir == null) || (target == null) ) throw new IOException("dir not specified to deal with"); + File baseDir = new File(_baseDir); + if (!baseDir.exists()) { + boolean created = baseDir.mkdirs(); + log("Creating PHTTP Relay Base Directory: " + baseDir.getAbsolutePath() + " - ok? " + created); + } + File identDir = new File(baseDir, target); + log("Ident dir: " + identDir.getAbsolutePath()); + return identDir; + } + + public void init(ServletConfig config) throws ServletException { + super.init(config); + String dir = System.getProperty(ENV_BASEDIR); + if (dir == null) { + _log.warn("Base directory for the polling http relay system not in the environment [" + ENV_BASEDIR +"]"); + _log.warn("Setting the base directory to ./relayDir for " + getServletName()); + _baseDir = ".relayDir"; + } else { + _baseDir = dir; + log("Loaded up " + getServletName() + " with base directory " + _baseDir); + } + } + + public void log(String msg) { + _log.debug(msg); + } + public void log(String msg, Throwable t) { + _log.debug(msg, t); + } +} diff --git a/apps/phttprelay/java/src/net/i2p/phttprelay/PollServlet.java b/apps/phttprelay/java/src/net/i2p/phttprelay/PollServlet.java new file mode 100644 index 0000000000..7da0bcef6c --- /dev/null +++ b/apps/phttprelay/java/src/net/i2p/phttprelay/PollServlet.java @@ -0,0 +1,263 @@ +package net.i2p.phttprelay; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; + +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.i2p.crypto.DSAEngine; +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.RouterIdentity; +import net.i2p.data.Signature; +import net.i2p.util.Clock; + +/** + * Handle poll requests for new messages - checking the poll request for a valid signature, + * sending back all of the messages found, and after all messages are written out, delete + * them from the local store. If the signature fails, it sends back an HTTP 403 (UNAUTHORIZED). + * If the target is not registered, it sends back an HTTP 404 (NOT FOUND)

+ * + * This servlet should be set up in web.xml as follows: + * + * + * Poll + * net.i2p.phttprelay.PollServlet + * + * baseDir + * /usr/local/jetty/phttprelayDir + * + * + * + * + * Poll + * /phttpPoll + * + * + * baseDir is the directory under which registrants and their pending messages are stored + * + */ +public class PollServlet extends PHTTPRelayServlet { + /* URL parameters on the check */ + + /** H(routerIdent).toBase64() of the target to receive the message */ + public final static String PARAM_SEND_TARGET = "target"; + + /** HTTP error code if the target is not known*/ + public final static int CODE_UNKNOWN = HttpServletResponse.SC_NOT_FOUND; + /** HTTP error code if the signature failed */ + public final static int CODE_UNAUTHORIZED = HttpServletResponse.SC_UNAUTHORIZED; + /** HTTP error code if everything is ok */ + public final static int CODE_OK = HttpServletResponse.SC_OK; + + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + byte data[] = getData(req); + if (data == null) return; + ByteArrayInputStream bais = new ByteArrayInputStream(data); + String target = getTarget(bais); + if (target == null) { + log("Target not specified"); + resp.sendError(CODE_UNKNOWN); + return; + } + + if (!isKnown(target)) { + resp.sendError(CODE_UNKNOWN); + return; + } + + if (!isAuthorized(target, bais)) { + resp.sendError(CODE_UNAUTHORIZED); + return; + } else { + log("Authorized access for target " + target); + } + + sendMessages(resp, target); + } + + private byte[] getData(HttpServletRequest req) throws ServletException, IOException { + ServletInputStream in = req.getInputStream(); + int len = req.getContentLength(); + byte data[] = new byte[len]; + int cur = 0; + int read = DataHelper.read(in, data); + if (read != len) { + log("Size read is incorrect [" + read + " instead of expected " + len + "]"); + return null; + } else { + log("Read data length: " + data.length + " in base64: " + Base64.encode(data)); + return data; + } + } + + private String getTarget(InputStream in) throws IOException { + StringBuffer buf = new StringBuffer(64); + int numBytes = 0; + int c = 0; + while ( (c = in.read()) != -1) { + if (c == (int)'&') break; + buf.append((char)c); + numBytes++; + if (numBytes > 128) { + log("Target didn't find the & after 128 bytes [" + buf.toString() + "]"); + return null; + } + } + if (buf.toString().indexOf("target=") != 0) { + log("Did not start with target= [" + buf.toString() + "]"); + return null; + } + return buf.substring("target=".length()); + } + + private void sendMessages(HttpServletResponse resp, String target) throws IOException { + log("Before lock " + target); + LockManager.lockIdent(target); + log("Locked " + target); + try { + File identDir = getIdentDir(target); + expire(identDir); + File messageFiles[] = identDir.listFiles(); + resp.setStatus(CODE_OK); + log("Sending back " + (messageFiles.length -1) + " messages"); + ServletOutputStream out = resp.getOutputStream(); + DataHelper.writeDate(out, new Date(Clock.getInstance().now())); + DataHelper.writeLong(out, 2, messageFiles.length -1); + for (int i = 0; i < messageFiles.length; i++) { + if ("identity.dat".equals(messageFiles[i].getName())) { + // skip + } else { + log("Message file " + messageFiles[i].getName() + " is " + messageFiles[i].length() + " bytes"); + DataHelper.writeLong(out, 4, messageFiles[i].length()); + writeFile(out, messageFiles[i]); + boolean deleted = messageFiles[i].delete(); + if (!deleted) { + log("!!!Error removing message file " + messageFiles[i].getAbsolutePath() + " - please delete!"); + } + } + } + out.flush(); + out.close(); + } catch (DataFormatException dfe) { + log("Error sending message", dfe); + } finally { + LockManager.unlockIdent(target); + log("Unlocked " + target); + } + } + + private final static long EXPIRE_DELAY = 60*1000; // expire messages every minute + + private void expire(File identDir) throws IOException { + File files[] = identDir.listFiles(); + long now = System.currentTimeMillis(); + for (int i = 0 ; i < files.length; i++) { + if ("identity.dat".equals(files[i].getName())) { + continue; + } + if (files[i].lastModified() + EXPIRE_DELAY < now) { + log("Expiring " + files[i].getAbsolutePath()); + files[i].delete(); + } + } + } + + private void writeFile(ServletOutputStream out, File file) throws IOException { + FileInputStream fis = new FileInputStream(file); + try { + byte buf[] = new byte[4096]; + while (true) { + int read = DataHelper.read(fis, buf); + if (read > 0) + out.write(buf, 0, read); + else + break; + } + } finally { + fis.close(); + } + } + + + private boolean isKnown(String target) throws IOException { + File identDir = getIdentDir(target); + if (identDir.exists()) { + File identFile = new File(identDir, "identity.dat"); + if (identFile.exists()) { + // known and valid (maybe we need to check the file format... naw, fuck it + return true; + } else { + return false; + } + } else { + return false; + } + } + + private boolean isAuthorized(String target, InputStream in) throws IOException { + RouterIdentity ident = null; + try { + ident = getRouterIdentity(target); + } catch (DataFormatException dfe) { + log("Identity was not valid", dfe); + } + + if (ident == null) { + log("Identity not registered"); + return false; + } + + try { + long val = DataHelper.readLong(in, 4); + Signature sig = new Signature(); + sig.readBytes(in); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataHelper.writeLong(baos, 4, val); + if (DSAEngine.getInstance().verifySignature(sig, baos.toByteArray(), ident.getSigningPublicKey())) { + return true; + } else { + log("Signature does NOT match"); + return false; + } + } catch (DataFormatException dfe) { + log("Format error reading the nonce and signature", dfe); + return false; + } + } + + private RouterIdentity getRouterIdentity(String target) throws IOException, DataFormatException { + File identDir = getIdentDir(target); + if (identDir.exists()) { + File identFile = new File(identDir, "identity.dat"); + if (identFile.exists()) { + // known and valid (maybe we need to check the file format... naw, fuck it + RouterIdentity ident = new RouterIdentity(); + ident.readBytes(new FileInputStream(identFile)); + return ident; + } else { + return null; + } + } else { + return null; + } + } +} diff --git a/apps/phttprelay/java/src/net/i2p/phttprelay/RegisterServlet.java b/apps/phttprelay/java/src/net/i2p/phttprelay/RegisterServlet.java new file mode 100644 index 0000000000..3c52fa498d --- /dev/null +++ b/apps/phttprelay/java/src/net/i2p/phttprelay/RegisterServlet.java @@ -0,0 +1,154 @@ +package net.i2p.phttprelay; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Date; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.RouterIdentity; +import net.i2p.util.Clock; + +/** + * Accept registrations for PHTTP relaying, allowing the Polling HTTP (PHTTP) + * transport for I2P to bridge past firewalls, NATs, and proxy servers.

+ * + * This servlet should be set up in web.xml as follows: + * + * + * Register + * net.i2p.phttprelay.RegisterServlet + * + * baseDir + * /usr/local/jetty/phttprelayDir + * + * + * pollPath + * phttpPoll + * + * + * sendPath + * phttpSend + * + * + * + * + * Register + * /phttpRegister + * + * + * baseDir is the directory under which registrants and their pending messages are stored + * pollPath is the path under the current host that requests polling for messages should be sent + * sendPath is the path under the current host that requests submitting messages should be sent + * + * The pollPath and sendPath must not start with / as they are translated ala http://host:port/[path] + */ +public class RegisterServlet extends PHTTPRelayServlet { + private String _pollPath; + private String _sendPath; + + /* config params */ + public final static String PARAM_POLL_PATH = "pollPath"; + public final static String PARAM_SEND_PATH = "sendPath"; + + /* key=val keys sent back on registration */ + public final static String PROP_STATUS = "status"; + public final static String PROP_POLL_URL = "pollURL"; + public final static String PROP_SEND_URL = "sendURL"; + public final static String PROP_TIME_OFFSET = "timeOffset"; // ms (local-remote) + + /* values for the PROP_STATUS */ + public final static String STATUS_FAILED = "failed"; + public final static String STATUS_REGISTERED = "registered"; + + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ServletInputStream in = req.getInputStream(); + RouterIdentity ident = new RouterIdentity(); + try { + Date remoteTime = DataHelper.readDate(in); + long skew = getSkew(remoteTime); + ident.readBytes(in); + boolean ok = registerIdent(ident); + sendURLs(req, resp, skew, ok); + } catch (DataFormatException dfe) { + log("Invalid format for router identity posted", dfe); + } finally { + in.close(); + } + } + + private long getSkew(Date remoteDate) { + if (remoteDate == null) { + log("*ERROR: remote date was null"); + return Long.MAX_VALUE; + } else { + long diff = Clock.getInstance().now() - remoteDate.getTime(); + return diff; + } + } + + private boolean registerIdent(RouterIdentity ident) throws DataFormatException, IOException { + File identDir = getIdentDir(ident.getHash().toBase64()); + boolean created = identDir.mkdirs(); + File identFile = new File(identDir, "identity.dat"); + FileOutputStream fos = null; + try { + fos = new FileOutputStream(identFile); + ident.writeBytes(fos); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + log("Identity registered into " + identFile.getAbsolutePath()); + return true; + } + + private void sendURLs(HttpServletRequest req, HttpServletResponse resp, long skew, boolean ok) throws IOException { + ServletOutputStream out = resp.getOutputStream(); + + log("*Debug: clock skew of " + skew + "ms (local-remote)"); + + StringBuffer buf = new StringBuffer(); + if (ok) { + buf.append(PROP_POLL_URL).append("=").append(buildURL(req, _pollPath)).append("\n"); + buf.append(PROP_SEND_URL).append("=").append(buildURL(req, _sendPath)).append("\n"); + buf.append(PROP_TIME_OFFSET).append("=").append(skew).append("\n"); + buf.append(PROP_STATUS).append("=").append(STATUS_REGISTERED).append("\n"); + } else { + buf.append(PROP_TIME_OFFSET).append("=").append(skew).append("\n"); + buf.append(PROP_STATUS).append("=").append(STATUS_FAILED).append("\n"); + } + out.write(buf.toString().getBytes()); + out.close(); + } + + public void init(ServletConfig config) throws ServletException { + super.init(config); + + String pollPath = config.getInitParameter(PARAM_POLL_PATH); + if (pollPath == null) + throw new ServletException("Polling path for the registration servlet required [" + PARAM_POLL_PATH + "]"); + else + _pollPath = pollPath; + String sendPath = config.getInitParameter(PARAM_SEND_PATH); + if (sendPath == null) + throw new ServletException("Sending path for the registration servlet required [" + PARAM_SEND_PATH + "]"); + else + _sendPath = sendPath; + } +} diff --git a/apps/phttprelay/java/src/net/i2p/phttprelay/SendServlet.java b/apps/phttprelay/java/src/net/i2p/phttprelay/SendServlet.java new file mode 100644 index 0000000000..10d7be8720 --- /dev/null +++ b/apps/phttprelay/java/src/net/i2p/phttprelay/SendServlet.java @@ -0,0 +1,318 @@ +package net.i2p.phttprelay; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Accept messages for PHTTP relaying, allowing the Polling HTTP (PHTTP) + * transport for I2P to bridge past firewalls, NATs, and proxy servers. This + * delivers them into the queue, returning HTTP 201 (created) if the queue is + * known, as well as the URL at which requests can be made to check the delivery + * status of the message. If the queue is not known, HTTP 410 (resource gone) is + * sent back.

+ * + * This servlet should be set up in web.xml as follows: + * + * + * Send + * net.i2p.phttprelay.SendServlet + * + * baseDir + * /usr/local/jetty/phttprelayDir + * + * + * checkPath + * phttpCheckStatus + * + * + * maxMessagesPerIdent + * 100 + * + * + * + * + * Send + * /phttpSend + * + * + * baseDir is the directory under which registrants and their pending messages are stored + * checkPath is the path under the current host that requests for the status of delivery should be sent + * maxMessagesPerIdent is the maximum number of outstanding messages per peer being relayed + * + * The checkPath must not start with / as they are translated ala http://host:port/[path] + */ +public class SendServlet extends PHTTPRelayServlet { + private String _checkPath; + private int _maxMessagesPerIdent; + + /* config params */ + public final static String PARAM_CHECK_PATH = "checkPath"; + public final static String PARAM_MAX_MESSAGES_PER_IDENT = "maxMessagesPerIdent"; + + /* URL parameters on the send */ + + /** H(routerIdent).toBase64() of the target to receive the message */ + public final static String PARAM_SEND_TARGET = "target"; + /** # ms to wait for the message to be delivered before failing it */ + public final static String PARAM_SEND_TIMEOUTMS = "timeoutMs"; + /** # bytes to be sent in the message */ + public final static String PARAM_SEND_DATA_LENGTH = "dataLength"; + /** sending router's time in ms */ + public final static String PARAM_SEND_TIME = "localTime"; + + /** msgId parameter to access the check path servlet with (along side PARAM_SEND_TARGET) */ + public final static String PARAM_MSG_ID = "msgId"; + + + /* key=val keys sent back on registration */ + public final static String PROP_CHECK_URL = "statusCheckURL"; + public final static String PROP_STATUS = "status"; + public final static String STATUS_OK = "accepted"; + public final static String STATUS_UNKNOWN = "unknown"; + private final static String STATUS_CLOCKSKEW = "clockSkew_"; /** prefix for (local-remote) */ + + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ServletInputStream in = req.getInputStream(); + try { + int contentLen = req.getContentLength(); + String firstLine = getFirstLine(in, contentLen); + if (firstLine == null) { + return; + } + Map params = getParameters(firstLine); + String target = (String)params.get(PARAM_SEND_TARGET); + String timeoutStr = (String)params.get(PARAM_SEND_TIMEOUTMS); + String lenStr = (String)params.get(PARAM_SEND_DATA_LENGTH); + String remoteTimeStr = (String)params.get(PARAM_SEND_TIME); + long skew = 0; + try { + long remTime = Long.parseLong(remoteTimeStr); + skew = System.currentTimeMillis() - remTime; + } catch (Throwable t) { + skew = Long.MAX_VALUE; + log("*ERROR could not parse the remote time from [" + remoteTimeStr + "]"); + } + + log("Target [" + target + "] timeout [" + timeoutStr + "] length [" + lenStr + "] skew [" + skew + "]"); + + if ( (skew > CLOCK_FUDGE_FACTOR) || (skew < 0 - CLOCK_FUDGE_FACTOR) ) { + log("Attempt to send by a skewed router: skew = " + skew + "ms (local-remote)"); + failSkewed(req, resp, skew); + } + + if (!isValidTarget(target)) { + log("Attempt to send to an invalid target [" + target + "]"); + fail(req, resp, "Unknown or invalid target"); + return; + } + + long len = -1; + try { + len = Long.parseLong(lenStr); + } catch (Throwable t) { + log("Unable to parse length parameter [" + PARAM_SEND_DATA_LENGTH + "] (" + lenStr + ")"); + fail(req, resp, "Invalid length parameter"); + return; + } + + int msgId = saveFile(in, resp, target, len); + if (msgId >= 0) { + sendSuccess(req, resp, target, msgId); + } else { + fail(req, resp, "Unable to queue up the message for delivery"); + } + } finally { + try { in.close(); } catch (IOException ioe) {} + } + } + + + private String getFirstLine(ServletInputStream in, int len) throws ServletException, IOException { + StringBuffer buf = new StringBuffer(128); + int numBytes = 0; + int c = 0; + while ( (c = in.read()) != -1) { + if (c == (int)'\n') break; + buf.append((char)c); + numBytes++; + if (numBytes > 512) { + log("First line is > 512 bytes [" + buf.toString() + "]"); + return null; + } + } + log("First line: " + buf.toString()); + return buf.toString(); + } + + private static Map getParameters(String line) { + //StringTokenizer tok = new StringTokenizer(line, "&=", true); + Map params = new HashMap(); + while (line != null) { + String key = null; + String val = null; + int firstAmp = line.indexOf('&'); + int firstEq = line.indexOf('='); + if (firstAmp > 0) { + key = line.substring(0, firstEq); + val = line.substring(firstEq+1, firstAmp); + line = line.substring(firstAmp+1); + params.put(key, val); + } else { + line = null; + } + } + return params; + } + + private boolean isValidTarget(String target) throws IOException { + File identDir = getIdentDir(target); + if (identDir.exists()) { + File identFile = new File(identDir, "identity.dat"); + if (identFile.exists()) { + // known and valid (maybe we need to check the file format... naw, fuck it + String files[] = identDir.list(); + // we skip 1 because of identity.dat + if (files.length -1 > _maxMessagesPerIdent) { + log("Too many messages pending for " + target + ": " + (files.length-1)); + return false; + } else { + return true; + } + } else { + log("Ident directory exists, but identity does not... corrupt for " + target); + return false; + } + } else { + log("Unknown ident " + target); + return false; + } + } + + private int saveFile(InputStream in, HttpServletResponse resp, String target, long len) throws IOException { + File identDir = getIdentDir(target); + if (!identDir.exists()) return -1; + try { + LockManager.lockIdent(target); + int i = 0; + while (true) { + File curFile = new File(identDir, "msg" + i + ".dat"); + if (!curFile.exists()) { + boolean ok = writeFile(curFile, in, len); + if (ok) + return i; + else + return -1; + } + i++; + continue; + } + } finally { + LockManager.unlockIdent(target); + } + } + + private boolean writeFile(File file, InputStream in, long len) throws IOException { + long remaining = len; + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + byte buf[] = new byte[4096]; + while (remaining > 0) { + int read = in.read(buf); + if (read == -1) + break; + remaining -= read; + if (read > 0) + fos.write(buf, 0, read); + } + } finally { + if (fos != null) { + try { fos.close(); } catch (IOException ioe) {} + } + if (remaining != 0) { + log("Invalid remaining bytes [" + remaining + " out of " + len + "] - perhaps message was cancelled partway through delivery? deleting " + file.getAbsolutePath()); + boolean deleted = file.delete(); + if (!deleted) + log("!!!Error deleting temporary file " + file.getAbsolutePath()); + return false; + } + } + return true; + } + + private void sendSuccess(HttpServletRequest req, HttpServletResponse resp, String target, int msgId) throws IOException { + ServletOutputStream out = resp.getOutputStream(); + StringBuffer buf = new StringBuffer(); + buf.append(PROP_STATUS).append('=').append(STATUS_OK).append('\n'); + buf.append(PROP_CHECK_URL).append('=').append(buildURL(req, _checkPath)); + buf.append('?'); + buf.append(PARAM_SEND_TARGET).append('=').append(target).append("&"); + buf.append(PARAM_MSG_ID).append('=').append(msgId).append("\n"); + out.write(buf.toString().getBytes()); + out.flush(); + } + + private void fail(HttpServletRequest req, HttpServletResponse resp, String err) throws IOException { + ServletOutputStream out = resp.getOutputStream(); + StringBuffer buf = new StringBuffer(); + buf.append(PROP_STATUS).append('=').append(STATUS_UNKNOWN).append('\n'); + out.write(buf.toString().getBytes()); + out.flush(); + } + + private void failSkewed(HttpServletRequest req, HttpServletResponse resp, long skew) throws IOException { + ServletOutputStream out = resp.getOutputStream(); + StringBuffer buf = new StringBuffer(); + buf.append(PROP_STATUS).append('=').append(STATUS_CLOCKSKEW).append(skew).append('\n'); + out.write(buf.toString().getBytes()); + out.flush(); + } + + public void init(ServletConfig config) throws ServletException { + super.init(config); + + String checkPath = config.getInitParameter(PARAM_CHECK_PATH); + if (checkPath == null) + throw new ServletException("Check status path for the sending servlet required [" + PARAM_CHECK_PATH + "]"); + else + _checkPath = checkPath; + + String maxMessagesPerIdentStr = config.getInitParameter(PARAM_MAX_MESSAGES_PER_IDENT); + if (maxMessagesPerIdentStr == null) + throw new ServletException("Max messages per ident for the sending servlet required [" + PARAM_MAX_MESSAGES_PER_IDENT + "]"); + try { + _maxMessagesPerIdent = Integer.parseInt(maxMessagesPerIdentStr); + } catch (Throwable t) { + throw new ServletException("Valid max messages per ident for the sending servlet required [" + PARAM_MAX_MESSAGES_PER_IDENT + "]"); + } + } + + public static void main(String args[]) { + String line = "target=pp0ARjQiB~IKC-0FsMUsPEMrwR3gxVBZGRYfEr1IzHI=&timeoutMs=52068&dataLength=2691&"; + Map props = getParameters(line); + for (java.util.Iterator iter = props.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = (String)props.get(key); + System.out.println("[" + key + "] = [" + val + "]"); + } + } +} diff --git a/apps/phttprelay/java/web.xml b/apps/phttprelay/java/web.xml new file mode 100644 index 0000000000..8a82891fed --- /dev/null +++ b/apps/phttprelay/java/web.xml @@ -0,0 +1,71 @@ + + + + + + + I2P Polling HTTP Relay + + + Register + net.i2p.phttprelay.RegisterServlet + + pollPath + /phttpPoll + + + sendPath + /phttpSend + + 1 + + + + Send + net.i2p.phttprelay.SendServlet + + checkPath + /phttpCheckSendStatus + + + maxMessagesPerIdent + 100 + + 1 + + + + CheckSendStatus + net.i2p.phttprelay.CheckSendStatusServlet + 1 + + + + Poll + net.i2p.phttprelay.PollServlet + 1 + + + + Register + /phttpRegister + + + + Send + /phttpSend + + + + CheckSendStatus + /phttpCheckSendStatus + + + + Poll + /phttpPoll + + + diff --git a/apps/tests/COPYING b/apps/tests/COPYING new file mode 100644 index 0000000000..5ec43ee156 --- /dev/null +++ b/apps/tests/COPYING @@ -0,0 +1,278 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. diff --git a/apps/tests/EchoServer.java b/apps/tests/EchoServer.java new file mode 100644 index 0000000000..45a6d05444 --- /dev/null +++ b/apps/tests/EchoServer.java @@ -0,0 +1,44 @@ +/* + * A Minimal echo server. + * + * Copyright (c) 2004 Michael Schierl + * + * Licensed unter GNU General Public License. + */ + +import java.io.*; +import java.net.*; + +public class EchoServer extends Thread { + + public static void main(String[] args) throws IOException { + ServerSocket ss = new ServerSocket(Integer.parseInt(args[0])); + while (true) { + Socket s = ss.accept(); + new EchoServer(s); + } + } + + private Socket s; + + public EchoServer(Socket s) { + this.s=s; + start(); + } + + public void run() { + try { + InputStream in = s.getInputStream(); + OutputStream out = s.getOutputStream(); + byte[] b = new byte[4096]; + int len; + while ((len = in.read(b)) != -1) { + out.write(b, 0, len); + } + } catch (SocketException ex) { + // nothing + } catch (IOException ex) { + ex.printStackTrace(); + } + } +} diff --git a/apps/tests/GuaranteedBug.java b/apps/tests/GuaranteedBug.java new file mode 100644 index 0000000000..5acf937c0d --- /dev/null +++ b/apps/tests/GuaranteedBug.java @@ -0,0 +1,106 @@ +// compile & run this file against i2p.jar + +import java.io.*; +import java.util.*; +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.data.*; + +public class GuaranteedBug { + + + public void reproduce() { + try { + Destination d1 = null; + // first client (receiver) + if (true) { // smaller scope for variables ... + I2PClient client = I2PClientFactory.createClient(); + ByteArrayOutputStream keyStream = + new ByteArrayOutputStream(512); + d1 = client.createDestination(keyStream); + ByteArrayInputStream in = + new ByteArrayInputStream(keyStream.toByteArray()); + Properties opts = new Properties(); + opts.setProperty(I2PClient.PROP_RELIABILITY, + I2PClient.PROP_RELIABILITY_GUARANTEED); + opts.setProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1"); + opts.setProperty(I2PClient.PROP_TCP_PORT, "7654"); + I2PSession session = client.createSession(in, opts); + session.connect(); + session.setSessionListener(new PacketCounter()); + } + // second client (sender) + I2PClient client = I2PClientFactory.createClient(); + ByteArrayOutputStream keyStream = new ByteArrayOutputStream(512); + Destination d2 = client.createDestination(keyStream); + ByteArrayInputStream in = + new ByteArrayInputStream(keyStream.toByteArray()); + Properties opts = new Properties(); + opts.setProperty(I2PClient.PROP_RELIABILITY, + I2PClient.PROP_RELIABILITY_GUARANTEED); + opts.setProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1"); + opts.setProperty(I2PClient.PROP_TCP_PORT, "7654"); + I2PSession session = client.createSession(in, opts); + session.connect(); + session.setSessionListener(new DummyListener()); + for (int i=0;i<1000; i++) { + byte[] msg = (""+i).getBytes("ISO-8859-1"); + session.sendMessage(d1,msg); + System.out.println(">>"+i); + } + } catch (IOException ex) { + ex.printStackTrace(); + } catch (I2PException ex) { + ex.printStackTrace(); + } + } + + public static void main(String[] args) { + new GuaranteedBug().reproduce(); + } + + // ------------------------------------------------------- + public class DummyListener implements I2PSessionListener { + public void disconnected(I2PSession session) { + System.err.println("Disconnected: "+session); + } + + public void errorOccurred(I2PSession session, String message, + Throwable error) { + System.err.println("Error: "+session+"/"+message); + error.printStackTrace(); + } + + public void messageAvailable(I2PSession session, int msgId, + long size) { + System.err.println("Message here? "+session); + } + + public void reportAbuse(I2PSession session, int severity) { + System.err.println("Abuse: "+severity+"/"+session); + } + } + + public class PacketCounter extends DummyListener { + private int lastPacket = -1; + public void messageAvailable(I2PSession session, int msgId, + long size) { + try { + byte msg[] = session.receiveMessage(msgId); + String m = new String(msg, "ISO-8859-1"); + int no = Integer.parseInt(m); + if (no != ++lastPacket) { + System.out.println("ERROR: <<"+no); + } else { + System.out.println("<<"+no); + } + } catch (NumberFormatException ex) { + ex.printStackTrace(); + } catch (I2PException ex) { + ex.printStackTrace(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } +} diff --git a/apps/tests/README b/apps/tests/README new file mode 100644 index 0000000000..83f17a4a88 --- /dev/null +++ b/apps/tests/README @@ -0,0 +1,6 @@ +This directory is intended for tests which are useful for testing +I2P, but don't test any of the I2P components directly. Instead, +tests are run on "application" level (TCP, IRC, HTTP etc.). + +IOW: These tests may be useful for any other project that allows +tunneling of "normal" protocols, not only I2P. diff --git a/apps/tests/echotester/BasicEchoTestAnalyzer.java b/apps/tests/echotester/BasicEchoTestAnalyzer.java new file mode 100644 index 0000000000..5bab10e8ca --- /dev/null +++ b/apps/tests/echotester/BasicEchoTestAnalyzer.java @@ -0,0 +1,94 @@ +/** + * A basic implementation for the EchoTestAnalyzer. + */ +public class BasicEchoTestAnalyzer implements EchoTestAnalyzer { + + /** + * How many events must appear until a detailed report is + * printed. Default is every 20 events. + */ + private static int REPORT_DELAY = 20; + + private static int SUMMARY_SIZE = 100; + + public BasicEchoTestAnalyzer() { + this(20, 100); + } + + public BasicEchoTestAnalyzer(int reportDelay, int summarySize) { + REPORT_DELAY = reportDelay; + SUMMARY_SIZE = summarySize; + } + + private int events = 0, + packetLosses = 0, + packetLossesDisconnect=0, + disconnects = 0, + disconnectsRefused = 0, + delayCount=0, + lastDelayPtr = 0; + private long minDelay=Long.MAX_VALUE, maxDelay = 0, delaySum=0; + private long[] lastDelays = new long[SUMMARY_SIZE]; + + + public synchronized void packetLossOccurred(boolean beforeDisconnect) { + System.out.println("1: Packet lost"+ + (beforeDisconnect?" before disconnect":"")+ + "."); + packetLosses++; + if (beforeDisconnect) packetLossesDisconnect++; + countEvent(); + } + + public synchronized void successOccurred(long delay) { + System.out.println("0: Delay = "+delay); + if (delay > maxDelay) maxDelay=delay; + if (delay < minDelay) minDelay=delay; + delaySum+=delay; + delayCount++; + lastDelays[lastDelayPtr++]=delay; + lastDelayPtr%=SUMMARY_SIZE; + countEvent(); + } + + public synchronized void disconnected(boolean refused) { + System.out.println("2: Disconnected"+ + (refused?" (connection refused)":"")+ + "."); + disconnects++; + if (refused) disconnectsRefused++; + countEvent(); + } + + private void countEvent() { + events++; + if (events % REPORT_DELAY == 0) { + int packets = packetLosses+delayCount; + long delaySummary=0; + for (int i=0;i0 || true + ?"\n++ Average lost packets per disconnect: "+ + (packetLossesDisconnect/(float)disconnects) + :"")+ + "\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++"+ + "\n++ Minimal delay: "+minDelay+ + "\n++ Average delay: "+(delaySum/(float)delayCount)+ + "\n++ Maximal delay: "+maxDelay+ + (delayCount >=SUMMARY_SIZE + ?"\n++ Average delay over last " + SUMMARY_SIZE + ": "+(delaySummary/(float)SUMMARY_SIZE) + :"")+ + "\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + } + } +} diff --git a/apps/tests/echotester/EchoTestAnalyzer.java b/apps/tests/echotester/EchoTestAnalyzer.java new file mode 100644 index 0000000000..3067a1ba7f --- /dev/null +++ b/apps/tests/echotester/EchoTestAnalyzer.java @@ -0,0 +1,19 @@ + +/** + * A class that wants to analyze tests implements this interface. This + * allows to "mix" several test values (from different echo servers) + * as well as different algorithms for analyzing the data (for + * jrandom: Strategy Pattern *g*). + */ +public interface EchoTestAnalyzer { + + public void packetLossOccurred(boolean beforeDisconnect); + + public void successOccurred(long delay); + + public void disconnected(boolean refused); + +} + + + diff --git a/apps/tests/echotester/EchoTester.java b/apps/tests/echotester/EchoTester.java new file mode 100644 index 0000000000..4f09678c37 --- /dev/null +++ b/apps/tests/echotester/EchoTester.java @@ -0,0 +1,170 @@ +/* + * Test for an echo server. This test is intended to be used via an + * I2PTunnel, but should work as well on other networks that provide + * TCP tunneling and an echo server. + * + * Copyright (c) 2004 Michael Schierl + * + * Licensed unter GNU General Public License. + */ + +import java.io.*; +import java.net.*; + +/** + * The main engine for the EchoTester. + */ +public class EchoTester extends Thread { + + /** + * How long to wait between packets. Default is 6 seconds. + */ + private static long PACKET_DELAY= 6000; + + /** + * How many packets may be on the way before the connection is + * seen as "broken" and disconnected. + */ + private static final long MAX_PACKETS_QUEUED=50; // unused + + + private EchoTestAnalyzer eta; + private String host; + private int port; + + // the following vars are synchronized via the lock. + private Object lock = new Object(); + private long nextPacket=0; + private long nextUnreceived=0; + private boolean readerRunning=false; + + public static void main(String[] args) { + if (args.length == 3) + PACKET_DELAY = Long.parseLong(args[2]); + new EchoTester(args[0], Integer.parseInt(args[1]), + new BasicEchoTestAnalyzer()); + } + + public EchoTester(String host, int port, EchoTestAnalyzer eta) { + this.eta=eta; + this.host=host; + this.port=port; + start(); + } + + public void run() { + try { + while (true) { + Socket s; + try { + s = new Socket(host, port); + } catch (ConnectException ex) { + eta.disconnected(true); + Thread.sleep(PACKET_DELAY); + continue; + } + System.out.println("41: Connected to "+host+":"+port); + synchronized(lock) { + nextUnreceived=nextPacket; + } + Thread t = new ResponseReaderThread(s); + Writer w = new BufferedWriter(new OutputStreamWriter + (s.getOutputStream())); + while (true) { + long no; + synchronized(lock) { + no = nextPacket++; + } + try { + w.write(no+" "+System.currentTimeMillis()+"\n"); + w.flush(); + } catch (SocketException ex) { + break; + } + Thread.sleep(PACKET_DELAY); + } + s.close(); + t.join(); + synchronized(lock) { + if (readerRunning) { + System.out.println("*** WHY IS THIS THREAD STILL"+ + " RUNNING?"); + } + while (nextUnreceived < nextPacket) { + nextUnreceived++; + eta.packetLossOccurred(true); + } + if (nextUnreceived > nextPacket) { + System.out.println("*** WTF? "+nextUnreceived+" > "+ + nextPacket); + } + } + eta.disconnected(false); + } + } catch (InterruptedException ex) { + ex.printStackTrace(); + System.exit(1); // treat these errors as fatal + } catch (IOException ex) { + ex.printStackTrace(); + System.exit(1); // treat these errors as fatal + } + + } + + private class ResponseReaderThread extends Thread { + + private Socket s; + + public ResponseReaderThread(Socket s) { + this.s=s; + synchronized(lock) { + readerRunning=true; + } + start(); + } + + public void run() { + try { + BufferedReader br = new BufferedReader(new InputStreamReader + (s.getInputStream())); + String line; + int index; + while ((line=br.readLine()) != null) { + if ((index=line.indexOf(" ")) == -1) + continue; + long now, packetNumber, packetTime; + now = System.currentTimeMillis(); + try { + packetNumber = Long.parseLong + (line.substring(0,index)); + packetTime = Long.parseLong + (line.substring(index+1)); + } catch (NumberFormatException ex) { + System.out.println(ex.toString()); + continue; + } + synchronized (lock) { + while (packetNumber > nextUnreceived) { + nextUnreceived++; + eta.packetLossOccurred(false); + } + if (nextUnreceived > packetNumber) { + System.out.println("*** DOUBLE PACKET!"); + } else { + nextUnreceived++; + } + } + eta.successOccurred(now-packetTime); + } + } catch (SocketException ex) { + // ignore + } catch (IOException ex) { + ex.printStackTrace(); + System.exit(0); + } + synchronized(lock) { + readerRunning=false; + } + } + } +} diff --git a/apps/tests/readme.license.txt b/apps/tests/readme.license.txt new file mode 100644 index 0000000000..4c14112cb0 --- /dev/null +++ b/apps/tests/readme.license.txt @@ -0,0 +1,10 @@ +$Id$ + +the i2p/apps/tests module is the root of application +level tests, and everything within it is released +according to the terms of the I2P license policy. +That means everything contained within the +i2p/apps/tests module is released into the public +domain unless otherwise marked. Alternate licenses +that may be used include GPL, GPL + java exception, +BSD, Cryptix, and MIT. diff --git a/build.xml b/build.xml new file mode 100644 index 0000000000..cb42e888ab --- /dev/null +++ b/build.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosts.txt b/hosts.txt new file mode 100644 index 0000000000..89c9e509b9 --- /dev/null +++ b/hosts.txt @@ -0,0 +1,82 @@ +; TC's hosts.txt guaranteed freshness +; $Id: hosts.txt,v 1.28 2004/03/19 21:58:44 jrandom Exp $ +; changelog: +; (1.27) added jar.i2p +; (1.26) added tor-www-proxy.i2p (WWW through tor by human - won't be up 24/7!) +; (1.25) added ugha.i2p +; (1.24) added reefer.i2p +; (1.23) added nic.i2p +; (1.22) added www.janonymous.i2p +; (1.21) added bluebeam.i2p +; (1.20) added echo.baffled.i2p (which runs mihi's echo server - telnet and get replies) +; (1.19) added irc.baffled.i2p (which is hooked up with irc.duck.i2p over i2p) +; (1.18) added www.aum.i2p, nntp.baffled.i2p, www.baffled.i2p, and ardvark.i2p +; (1.17) added gernika's eepsite +; (1.16) added duck's scp and hosting server +; (1.15) added eco's beta i2psnark server +; (1.14) added duck's pgp keyserver +; (1.13) added human's eepsite, the mail servers, and luckypunk's eepsite +; (1.12) added fillament's chessd server (FICS based clients or telnet to it) +; (1.11) added mp3.tc.i2p and mesh.firerabbit.i2p +; (1.10) added aum's mp3 and ogg streams +; (1.9) added the FCP and HTTP keys for a private entropy node, as well as nm's eepsite +; (1.8) updated nightblade's eepsite (after confirming ident w/ trent on iip) +; (1.7) added aum's eepsite +; (1.6) added fillament's flog/plog and eco's eepsite +; (1.5) added duck's irc +; (1.4) added eco's JAP tunnel +; (1.3) added duck's eepsite +; (1.2) fixed duck's subdomain names +; (1.1) added nntp, jabber, squid, and i2pcvs (and imported into i2p's cvs for distribution) +; (1.0) added dyad.i2p and nightblade.i2p +; (0.5) added bozo.i2p +; (0.0) added tc.i2p +; +tc.i2p=3RPLOkQGlq8anNyNWhjbMyHxpAvUyUJKbiUejI80DnPR59T3blc7-XrBhQ2iPbf-BRAR~v1j34Kpba1eDyhPk2gevsE6ULO1irarJ3~C9WcQH2wAbNiVwfWqbh6onQ~YmkSpGNwGHD6ytwbvTyXeBJcS8e6gmfNN-sYLn1aQu8UqWB3D6BmTfLtyS3eqWVk66Nrzmwy8E1Hvq5z~1lukYb~cyiDO1oZHAOLyUQtd9eN16yJY~2SRG8LiscpPMl9nSJUr6fmXMUubW-M7QGFH82Om-735PJUk6WMy1Hi9Vgh4Pxhdl7gfqGRWioFABdhcypb7p1Ca77p73uabLDFK-SjIYmdj7TwSdbNa6PCmzEvCEW~IZeZmnZC5B6pK30AdmD9vc641wUGce9xTJVfNRupf5L7pSsVIISix6FkKQk-FTW2RsZKLbuMCYMaPzLEx5gzODEqtI6Jf2teMd5xCz51RPayDJl~lJ-W0IWYfosnjM~KxYaqc4agviBuF5ZWeAAAA +dyad.i2p=W~JFpqSH8uopylox2V5hMbpcHSsb-dJkSKvdJ1vj~KQcUFJWXFyfbetBAukcGH5S559aK9oslU0qbVoMDlJITVC4OXfXSnVbJBP1IhsK8SvjSYicjmIi2fA~k4HvSh9Wxu~bg8yo~jgfHA8tjYppK9QKc56BpkJb~hx0nNGy4Ny9eW~6A5AwAmHvwdt5NqcREYRMjRd63dMGm8BcEe-6FbOyMo3dnIFcETWAe8TCeoMxm~S1n~6Jlinw3ETxv-L6lQkhFFWnC5zyzQ~4JhVxxT3taTMYXg8td4CBGmrS078jcjW63rlSiQgZBlYfN3iEYmurhuIEV9NXRcmnMrBOQUAoXPpVuRIxJbaQNDL71FO2iv424n4YjKs84suAho34GGQKq7WoL5V5KQgihfcl0f~xne-qP3FtpoPFeyA9x-sA2JWDAsxoZlfvgkiP5eyOn23prT9TJK47HCVilHSV11uTVaC4Jc5YsjoBCZadWbgQnMCKlZ4jk-bLE1PSWLg7AAAA +nightblade.i2p=nyErwSseXbsojcWtNkDyUul0YULtqr6qyWSzIp639Ygpe8juCdgPMLURVXcmlCvo~QPoHg6zt53KpgpGvB1-Wv2SGvc2Mvs~o8USw3ius8fP1URphqcBbulK8Ci0bgknt0kD0AfxqfMz-p~xk1QEMxq2kZEoB3oyIIFnQlpb2ByS74Lx8iKzXTrwWk19I3Dvu4nIq8CBDDwu3lYoCD2kC-jT5pjgglverGPEGN4o55LYVTtfSg4gAJFZeaE4KjBR5P1z7cca6UDjGMWfR0iCa8P3qpkY2ODYpk~8w2xgBbgDq-8Hzik~uraHc598ccS8QpwB0f0Jw~2PZcTjOPdZ-239U6p3tESXa7FXzRBCujv4Bx6CVFRhCmBHpyFnCD-MugZ~vR6XFSS2XBsCT~duXKq94HH2n1iAWslG4Vu44ut1JVhDPFzp~Dk7wujB0tCo2HXH2icRQxOWe37foU4LZSJ4oMpFDACBzwSfcZdIPsVRxGttKQx4yzgffR1Q~Jl7AAAA +bozo.i2p=ubMPUwY0op6B7Jr8SAjY2bQXze8m1sT6xF2N0cv43dIHwLTO0gUqn7FCP9jXZDodE9DR3fu8fG8x1Yz1SpXFk4WtFmuDuhdN7uaHuLIQ71PATC2GRhDS7NXqn7GsVZgQxhHKenaE5BKjIKt2amZ2~8CM0qBKTqwievUO-Y6zG~-8l~RpnAxDZUMOjKKy5R3~jEN9DFZCaKvXSNcOVFjZRGaD6d8NvkAJjndHdE3bFSJUDNv0qhhp09-mm~Se9C~FzjrAbhgappdNRiwQepXTWqRbjjt6lUPT2eJISPDxYxoeZkBGZa9XmfO9QH3hoMo0g~RbwLeBqtgeRGhVgFiC4pN8lFt3z7j8L-12575SUeOnJPIm3hQWXdTjKX1hqf4LopYBG84N95IeydPJegsmkIkAMzEb0d~-UZfVSP9yFgs37j~Fds5yxBsu-NFc6qmZihpXEd7jrfX1-HuJVmXFmwaZgyumRSRDbj714wxr7RP4Hb-liA3JrU-FbqNQFoTMAAAA +duck.i2p=eFJdRYFmtjcpx9-Mw3JBdF6fwtb9cHRBR103Q8IGc91Jdfn0iYzK6Xx0oIzPvpbD4yOlPQm-C-7eTahrAkHa2FMCRTiiVq2a0nBp37W1uTvAToV-MKYPKTdFMxrXxvjS7qaSUXdJRcPaPexolfx-Gcjh~rN2tKCh0mz9beueiQ18~8qWGh6hUMb0yyA09ipL9vIkmHmooLwT9AZyzHXEzdLXZe1P~CG3L46QaXp9aTD7EkAwG6VBMjQrGiSJ-9FFhx4QcYAZWM-dfrtzbbVYfHxqQRBwzB27zLlaKVaqu4enC0N1cW1yy-cjnv0Wxokqe62B2uPzFYtloxQpBPfTLQZUfUzjskY-3Yg2AdSbEu37jYsnAJA95AlLz4t1W1vPTNiXzCaRqkkMX342SUkJK-HZE3wTjAgGPqJ9vMaC2YexPFViTxEO2Q0jDSjdPHG0D769qehkN5Bfb8MEI-yr3g9zLaY~w4r6T38ap33qfyISWlIJ5qCPcRVkeK8OqZ4vAAAA +irc.duck.i2p=Bxqr5E7-56oJeya1lsDLBN6L1gKme-FUS6Bh~TQS3HswomK9rpjrYNeqBTBoE8TCFl161~FI3soWqbnmFhIdhskausZsO0ez5-4IXMJW8NTilWqXQ3OJxA20M9grohx3RjkZgXU1ooTx7wviSHtXQYiiqnzGnIzmmEZo5-Xx6VjXakctebWwbi2PrsE6XLxrxXBzB34l4KlVsyX504BJiOT6KXNaVZxvG61GfGVfNHdeXljMDE5d25UdFC6RJdDnJ3Z7Yb7EjAww78aowbR0VCfJDH~cB868-VOKIxmor3Rs7giaLXmUyW~GRtFX10COJj5V6BrhKs61XOXxfbyQKGVXZ0mM2A8cdZE1ftr96SZgGy~V8uUHKvoa1HpjMNrPL5Tr6EGfJxOxAy6PHwotn6a8UMnCZgEdTbQ6U3BTywU~x3SCAQvfOT~dl3sZ-5ujYWNFRp7RhdY-WHn1Kj59MfU-VpczGYdV3bRkwT5lpIjST~vopLfkYUeB6gcVSr49AAAA +jabber.duck.i2p=AzR8Zrms1sLXflvaZZ5CI2TS1cRvlO99D5Jh641-KS0lMSQPUnSApTfy9R4aPxZ3SWwpZPfSSGN-e7~kw8ucGQVB9EOHnht7WCz5UKC423vX4fFZqpTyqemDBx0CYXfhqRf-~7mjslXigR2nbQwh8NMl2bgLPknGe55wMlZPfsacS2WKNQYbdOvrgG6Zb6DK8RzTDrsum5h2jtIorv3CuMrNKLdfziXBDICep2Zp9jKDPBHgSsRgl6dW8A-5~WbkIPLpQwqeTzw90r8uJ9EIln~GlFulblJprCfLzxJ2LAFpxNcTkqzOJPElFfhjVrJOYbm6IfllBlHMVNbJjYTOsiJLqODxMiVt3pQx~AGFrx5mVtMUcz9cu9hVPnz2ZVmIDB~a4UGkexf~FCHhaQ6Emnr4nnZBiFSP3f4nASOMHLOs4SE3UKiYUB3ntmJssVdD1w-ZGrovHynkvXMjrJ9GhIeldFm0M5cOAhra0OtJagZ-bgphR0v99ADGPl7X5DoGAAAA +home.duck.i2p=dQmTRCAgcKlbt3VKdW-iFI4zd3WnDaM8ULsfIZb8oEdDuTq5Sl~1JguWH2Wl8PMRuxkhHpfOZ6IUJZtxo-rr3tfaDsYPl8SZSIbvGBs5QooLl2NNTPA~gQMJOHZkuM4KWMWYnFRWoaqAUt9KAmdyl5vgF20xv0rmSgRmm8t6c3QSTfNWSeSDYkRgEumaUKP-kRJnSKNeIOez1N2HQSlaDu~cH27E-VqEuAu8HppI87dV~v6nVKXR9-KgHzCdfHXxy2pxV9trJ8vmTEZO53ab11-BdDMoZjOv5sjMUADNDnhIW1RJ3c-ZcF9OotYsUrf7E1fRo3BwGg27LKhFnyBDmzI0a5uQJ9HCDlzSQ7yfHNMqNCtMfHgZE6Bw6F3tJfr5ISP1j5ibfj4k9tODyfWVqTionQfgrP5szHdyGu14Y4AtDW01-d9BNmBTHA53jCl~Jq6Qm5~CKcGZGS-05iyhmg9x1FtR-rW5Vf6o8uitSjtE5uPt~UHXacaGxQMJSy9vAAAA +nntp.duck.i2p=5SIb-1I6SVwFlW~XlYTNJU1NCpbyHJ3co8KdrpDxBosnHtdsZzq2qzYzGDfvkKK3WRoKmRGYCE77uXAvOQjEyWoTkkGeY2xl3B1t4t8K4j8dFvYtDJkRGePxGaY3MW-9ANGUQsGOhh6qq4lcQ7rv-AWyDftfZ3pGaHc79VukSo7-6OSh4WDw~0l~DjdFjQsZOVNTbKIfDxzSjKnkTpNc73nrLhRE5nmnMj7bljTzNtAHiVf~hFMndPxF80JZt6erLqy62-~XbevTWpn2KCTjYzhYUkwYW95-SW27ph5rIVZneizLEanSPtdUkDGhMEjjmy39Qr5rD5i9H-ioIP3NppRkjPwrtI9VJpsJtzv75uANIXy48RCBVXXA18ng8o7FVevdyjVb~C90IXpJF~mT7DI94rTT5QnzlpJVCid65kOoNzXFC80lP-iiwXMMBEcys8RGA9hdOvagkFDJ64l0GwrVpFGO2AVMsMenR0WHEvJOjNv62sT6rlntO3ZywhNXAAAA +pgp.duck.i2p=kRm2KRa2EiWO~XQFYxSg6UM9lXYHZ93IB80j3ShFhJOOZ4AN05BrTGjMeT9nhn5n1LMEUhy9HJuN-Lhkd89Mbhufk7di6M40wqySns2g0T7XUCFUgQ8kl4~BoY8M9U0pHpM3RJcCw8WJEFGEk~fX8tgC4XQB43UIXfrdKTlNfKhxZODE4UdvlFfFdOYMgH53x8UgMZUprn~URTRNg1uwcaXr2luMwts6HnDt1bDd8elitsWViOJiw42yAFMqBnf-7mhiTCsoYg-nsOiq0Jirt58cBjAGUL5ujZo~vfXkLslDKjHOP32Y7HIi5ANEsbbRr~8QrVUojnVoJwFXs8BQBTevRpGLkCSLnKa31jNSt3msOnEP-n729Jabwj8o3pdRk9e-3~~y9gfj4bcmpSH9sOJoqmipXDiqOvv0tL9twERqse3tAwjE1NgXTvl5e2Zc~F0xJ-L2aG~6BX175ihjjEiYGWRYaoEisHMZdMtivsAK92dKl1JVEkuDF3W8KjL8AAAA +scp.duck.i2p=AE2Ff7x-tJMI901UKEEXkcwb9~5KZKs--VBXEoZnnpC~mlgnqGsZr8MBvRvs6xAzP7xXLeL~cHQ~gvPFT60obEBitwH~JcKMahhJDGb0p23Z8B81QajXijypDpVfMDfFbMiqQGctXhnBidNKWe7HQEcJGZer-SCu3m8CiftcZ0t0g5Y67B2AtqLhza5xfepq6FQ-Tl6fpgS-UDcGUAqMpLUYfrBFB11oM09TRVsfNp~NIAvMdrvXiX8dTDUHJM4FRdpV2OsJiyDdgf5-W6s6ssxohviT5MdijUSYQw5sWj8~9Xkv~~aa11Dvty0E7K9IhXAeJlwBe3VuDizsAJGnFIU7PwZXV7-9-28Zgldarg1P-rh2QDvCqF1vjdyZe99BBcTiCqvE3zx2N-9eT~FeOURrgE~2rFBKVBZfuAASSORqiXIeAANKklCW2pGQrM3DkX8ybi93Weg~eBjwGQugO1FRZ-ISq7npRWruMiC7f~fWLqkmRUy9HahPNp8E41s9AAAA +squid.i2p=8fiWZbRjOEzrj5n4jSqjN9UN54wTrsgEjqn7GRUQpLx1svf8lwckXPV5buP2VEYGo~83ftkIcDKyMLXkxSr8jqbb4yAEgPe2~w7OT~8LNvmVPz0xZhIO6fiw0WU4xD9x5PG1spYjWPnLFv7pynEvBpWFXaUlCacjWL2KkfViiGPXvveQqQIZs7VkxVD2HK-oT4yIjdqHpc7Y8nEV9xwds3-LX6to5p70jFe~kZJA2fjWHsSCm92TtPvoR3aTlL1VS3JUKpcH6BL5irsh-SKODEtDRCErPQI~j2SHzhcD6dMUsI7bm3AxivjFpSQHqyXLmLVdxECYsMET~nIHmuv8NYTHQQ0jM0XTQzwnQwEHjHRBd1~spR9uS2~LSnX4Pw~X1WTknJpPK1f4Cu1O44X4RYcLRCsxpEzytUBXA4BQizrbYgOJVGQa9-PNGxJeZsnNUZ3PxUi23Oh-c4jUaB0ZjyKTWJSpzj1GI6vc-gW-0ixGJ358TCSbKgqdBv~g~f7yAAAA +i2pcvs.i2p=okyFe1gfa8R5CRoUAiZA105EaZVuLV4V82bGvd~lRhVhDWGgGMzLycW6XTjtlSpAJOQ1sTox-91EjyFcX4Z0lQKzFGNJrcEvsGQ99qtBJefkOnHEUNlsfo096MZwzwuywB3x3wkEQ3R1~kKjazw6YBek0I4~UkFd1J8TlSMdcM5K0WQtjeJyMEN5NQ-YZupJk7gth1khdtK2HXphFYDQkkZaKPY9ZLMxDtHUgwEcDS5KXfnpLEssakAd0zRPS03xQ9hzlbRkjNAnc8FE6Vkp7zflnOFo4qglNAhJb1IBl53r~Gp91QeD4WY0mZgkVb5fbsF4b3F3SerutnrjhLqO7y~TUM0Ww1pYXD2E~aIqdSz46cZ7AEETc7KZl1BtAQmKQFeJiPHn4xqLlRsMv-hxB2pupthpZ5nLhaOr738AuCJVdrCAMZM9jw3TtUZnckq2tPmA6IofFNIHAdQCQTxee5EXvakTC0Mw6-MHVq-OLuGDP9anEK2IFeZolDIU5pUAAAAA +jap.eco.i2p=R5rLvDIz8JAHgR7tf6iC8z7-ugctL23ipkNr~3SNCTxzdggCndToz~kmVACG5mGaoNf--KZsIKM2pbgSci45cJ1xAipx8CAaC3bbP3paDAY01hnxBPXcMN-Q1bisJPQHjkCphLRZqIh7gb1oXxiTtXU7erqkdAPXg6kvscFiblv5JxRg4B3Y5HHJ91UEb~Bc2oIsESQX3pCK~zmhWlReYROnoenaa9DDP1b6Na3SOAhgECR3PjoneMco1qDqlndb3C5wJ7X9YDGcZaIE~N6c0erNzWX-mGkEGARVLjyhtsOHMC72CROehAB3tsjwneKFojUpGQ16xcHGX4hEKFLNPztAbvnT7fg01dfcQb01YzIuBxbbjsP8nnoLJbEpjOaRrdIfk0cXm3aE6AQt7LoKYA-bW2sxgBxEAKsjcj4rQ57SDAi97FjnvzH2Xt363eHt0Jf0CypyEO9ajQYChEeMj6ISyVGe2a7wNJutq7aAx0ilbYB3Tiq0ExrbEsmXVvsOAAAA +fillament.i2p=Pa50z7pU~ni5nWwUdaDZ5CJxG0fYjoarm9wlxnkgX~wHMX9RPgQAXz~r0Rr1Nadt2OA~dr9RMHswrMok0hutK3JZuFD707D7FjmWW2w979Ee9I3zxKyx9W5A2eE49PPT131NLa3uINXLXOYVA5frfDOmM75Dmvm533r8e2kloemJyj22HpvRiSXiQYgqYJGDMH3Hlnwk884eRkQu7P8DJL~hcuKpyY0FzLZtfxTNsdSavGjl7rKPMzJeP02-9TS5TkdHokZrstVM5Cn9ay1c8DQrMds7SPXJy13Ut34QRjb65JxRV0mrnY3teXewW6QFvFMXJCsf5C3i46t-9Fufy5D1H8cSd2Tx~Xl71MC5-1AJCcIS01Od23E9tFY3dU7IOSRhKC~FiAslyk3x-BnBSpKxbgl1w~LArBm5plNiCiUemJU88xYdn1UyukLer~yNrEHAWspckCRkXFwmUtPkaGNTvfwBSYns-skNHSd7MAUUoS-ewStBdmtnDgRkwSG9AAAA +eco.i2p=KhRG6BGxVPh-BUDDfIgy0570cppTdighytcaGVR0HzQo46tgRMBp9Shlpax5FQX4nLHn6qHQbdFFpFbAwe7CiDhURCVF9-CxYmPurGadxlMPHMjz9O3jHX0CQiv2iULsk4XPrYXF3PqBc4t1J6vVyBVO7uTUhDi0gF6sN1Ro-1GLcWcsoR8Kx-hb~Z4WqGD0QAROOBPHnSRSb236qVBkhFvSkfigfBq3jFgEsttadYJA9ZLSUj1XrFFRBjz~xkRra8kJQSZl5dbfg-eZMlL49h61U6Uta5n1~tL6sarmnl9CaTl2Qo27SKB1OmMLeZEteA5G0-~LiOjN0nxaKpwrCjKIOyvwbQy2QqE-GEb9m8SN8nC2bwYK9fH15pTMHY8GvPYGcUukbF6RhefwzkEJLZ~PaAECrZYuLsn9KE5C35uRnlWJiuJlJ25hG7da5tFMyDB95efzq5IMxPeI0pMigRfuVfRSaGDpNos6JxjfEIX8umk3jIJUPhz1d8gP4QgrAAAA +aum.i2p=09hSo56PTtkFLUEt1iUTO7zYTnO-B~ogsIsyyPWif6q1Iz4wz4JoBflAWZtedPmwGmH0nly4HYUS0gAADoUmBUnXwemmO6dxT-hPQkfEW-A7b3uEvYQWIN~kyFyg0Pa~FN6TaD9kGFttBN-GE4wxiHhXmWdzWNDVb0q5PVGnxMm6Jleik8xkd2Lgeexze8rIv8LCocAWx074USVkbCVQwoFi2P7EnjLq8odSz1cJAntbuCFeUZcjbslE3qmlcTFMCNCZXWKVzn7d5m4oszCQ83NidgekwxJ-S~iS6mBwIS0XKI--4iXiKXzzCFf0KtYfEWpvKCuqNJOcU8vQWAA2-i7~K28aLPzccDQn7acXWLKRTXF3tf0i6e-lSx-X6WTSWK-fuNitjAtKu~jqO10d~bCk7y~UPL-XwdH1XSTbk-Phhk7UoBTDiHY6zQHdD~iAzXER~9JXsJ4UoIrGFVabg7frzSt82CN7Ek5Li4AMz5gg3wq9H9HUa7xM5QfGIJpXAAAA +mp3.aum.i2p=vBOu1caCAajaL5WRMuwk4LwfXrlcn0WzA6iHUKV5ULhaBpJb9pR3SZpnQms2Ot2c5Fvu5I6Rp7WF6QRcyasAhUmC915ap~2~VG8KCDP0z3Quh1-eqGcmzErsIfXdh09j3CWuxN~fH84hd~KswqGudFkWtFTM9RcuQUGSC1efG7uF03uaDI-DKu7eb4VUV-hmpXb3Lqntgo5qSMBMmjyUND-f6RBoXnqM005mUZJpMoYfsBhnUEq37GG8u6P9T94nlMmtz9R3gNURpBJJKPlnEqCBN4mlE5rwspQ0ovxAlogVMhSCpQ4jr6cyWIbNx-nMzKGDj~hMQ0ndbVnWw3EDC3KsPnRnDv0yVz8Fc1YpoPwerHej7VnTupDKxc4T-j8XNA1dN8SfPmaKYEPfavlmy7HFAGcsbmeRZOq-PVvlDdrKNflug8Ysodd5XkDbh7y2k1pRDjwNBQ1EgDVAtL05-i9jerqekHkbFKJ6DlT76f06vj1R2v9qlQzAYjpcKbI3AAAA +ogg.aum.i2p=wR2ETKWn-mxsTurWwmSujvjpOiIjLg5TsldFUa4YFTgiRZdFIB-bXuK59shfnchlEgAZR0IR3~hH-O8bZ~j6wVBdZWq7bGTmyTxQ3MeYPdqK7wH7Jp147YUabFlqJkyI~DluwBDylJrIUyc2qw~ogJ67x-KyzIF7JLnoCC4E-T8Z0vmTAFWSa3XC-ncghrdZQCqEXaCMlG9PN~a7dcDq~qdWoNoyFcgLd0IQfE8JuJ1wSvmWUNEd9vkB2Zuu3EoSoDv4C53Fc0YhVACNug~VEEL-ZBGcCBcpVNud8dOMq-CbavkD5yKqHlvq~uzRi6BY5ajHI77uepJygkHcsm-8T0PXWXdc5ib4TtUI03tPkTar4Y2iVocY~oLk2jh7pQKZNioHJT4StWv9Pj8EWaVX4-emQB5kZOBwZItjo~EAGEoBT14NSM7CmKClgc6sg7fpvWF~-cNHkZsurBndni~~FKmUeWoO0FRQRF9Ao~C1DOt2V9oBbEW1~n6anjL5V~IyAAAA +fcp.entropy.i2p=jy0D13oJVmxSa1MltstV3FOfA5e2WAEEZJiYOJIZSUOcNnAkaR3ghE-AX4vuqyQPyOEUydpauD6cS8vfx4iZkb2U3ddlLcOU3YrFKdLrySpGtD~V126VO-9nOJFwDQOOaKAsiVGRKtMPLC64GkpU6TWSIhiVYWb7WmeAHXLLlR71DtgamAxEIlP3VhytxlS3vuvAoEH9ItsBwkv4N~7jec60WMQINl~c7uDDsuzKFY8wQlkHnLFQJCQ0VExfNYqK9nZ3x8TXNPmNKTMMQ1CUCowgwR783U7UAYqsxNrpkuWvTleadn7QcR9i2v4~L9zOeHd4nHBy8PAjO29g6nf6DIsYhg4c2HYnPYzktQ1NIElytmW83BhbXJXLgNBs1eI9gDaQmOiXi74FMgfg63IcXCYWeqCdwEzSouSphaXEHDcZZVTx7DE9R-1Bi4Dt~KvPOFsAoOqsjHCpHq1gS0u5HiL0hkSm1I1EMk4JBY0j4rM1nAt7e0ix~WiOz5jXlTVSAAAA +http.entropy.i2p=ON2Ud-B0-pJKbTR0Obpjp9wEG8grUpu55gEn5Mz3-dkVkPhHvHK6iLasr~P~Rf4kPPZvn-eK7z6rAVfsAytAJ9pcTH3lXERTjkd9FzVJJ0twbZSQ~XzX5d-24IPIMf00KegjnDkRJ82cRMKa-u4H-ayei~Y7xsSx64zC1eHv6qFxavtql3zRrS~du41~EHtpjqOtOo9Ea3lfFjhm2jUIJpYyVHqve3WbTfMBlguVALwGZIfenph7oQ1Hx~OnEtaviWuOEpupjm11LS9xqCNsccaEpJGvGt6ijxd9hrEuQZ5Ja~C0fNxf3xNtgRaUhakA8Xoo~jz8rCkV2vYQo58kj0E5xYrUQczomj8y-eDBZyq29BP8pfe2G1u3hpHA1z470LUeMPk8qVx8Cx8ZKmSK9XCvOl7WCnFS2~UUfzxbxSxPn9LfzxDZp05AVi9t~hJg-zkrL2n1wfEnScuUFapxarwK90rlAxNSnau-K61WfcXqyVMwDxl3leJOeVdHqhpbAAAA +www.mail.i2p=Y~V8YK2M-my6-Gw0lkrkJouxeqPuB03idp-4uT9pkIXCA5nki9m4YFfPObSPv0E7c2shBxwlUo-6beaRQ-7tCawJssDRc0C0PhRj12QUYYdtZP7JS8SQXy68gZIylY-wfyEXleIC4mYY5mSthhdUUfyo1lqzrdHc1NpjPBxRJcyMBFBGUeM7Of9E9M518jXpVl0bAmxSnr5dy7sgKAVNufzfqIBfEHnmL2ZYH78FoGnPybsV0F9~154emkmt89ZUbx0BuYvH3kT1zin8pSxKw1NqxvqYt7p8CElq1--U38rO9U5Y~kLB9f6F3RYJdkl28ANkvdgJUgqiHLVI5oPWATrJLAOokyGKhK4Xl4Bjp4SCuemxHwTOGyd-4Kl8cO41u3w1LksndX9stkV6U1X0gL9BeSIoa1997IgMLVbUiDMyCz7-cA0y2tc0EdQdlpc2y77nTdo7z23dMSJzWDXsrfmLhX7M24D70htLLc1dpwZ1BUEvM1uPqGfsBSrHdl-sAAAA +smtp.mail.i2p=gfSAYcvEsuU3oNGqeMpq1wZqH-whE1i~YCXEDwYzp8LrmukWsndvPER1~gF5QFrIp2RMXiietF3zEPtAJgevSG4ULxRU0s9MSAMXXlCACVhlf0m493J6kIYnkypOPC-Z8sulyF2kXM8BURLfSH57SS2uxLbx0hkc8j0kR9iys3ksxm5dgW-Rs7clAvLmmfASJkXZiU6DRhWbW84GbpAi9McE3ORhNLrWV5t1W4DXqzT0tzF2W0i8BEGns8XdOBQei9RAewzo5NRGPBmUl6ZKjEJ-2UtX189HPs7FcLknfsRxXhRcPQ1RombPezYCgcNhOgWY9owHq64mwGaDCnnpDSM01sdAuMlFfs1JJMoYNrILckjiHUNzV2XS8A0PIWdO4W0cT1EUs-V2a7Ocvg397HpR6Z4k-7fOrjs9yvpFsCPIEKYUD0mjr44N5pJIc61GGuNE~2ihQZGA3ju0OnUKTRZel3nK0rxl-qfFXsBsEB5vt-MTvKS73ZJdxUKWWzbWAAAA +pop.mail.i2p=aG6owmzirq7QZKYovpSVa4-WBLfI1uJ38cNmb6kkSkcS8A~JdoLWPj6eXieN8r3m7YJLxxyZhf3urmK9qJbiIPBp53M8bOSSkldpFz0NkQPWUWmYXiOrEsOBlugbJ8nNelDcOebqoieKBOTaF-WPJQil5C6RYdUy~PL50O6Qp-Hog1868zP26leYBBFiyzzWI3bOpOsgV~4bNXnqKQZeHXz1Ua2DkV-vDBpeamPzvNWQI6cNodf04PBKXK~TlduLZDK7v1yTt2LhPSBM5nE7ZRtS8KQIdh4o-nyYRmHjA~OQ70gowGpmsRqXHQxOpPro~C7w3gSe2N0zhqSHKstwoFJD-NmsBQ5opyOiKccATpWEQdAwmoICD6rw7TGG4XYXCtyTD2xxLffER0SEsJ-BJesKKhdm-qyEMAOQq00jatoEs9jBYoujFLFQMUgaDejRJdEHWkiGT~x4auosHGYavmrcm-0mdX0CWYgfjwVb~PORhBqHJ1G5IgRPjoZLuxiJAAAA +nm.i2p=UhUVbM972VwQgqS28SkGPtghCb~IpdpeMW7O9E7I3HtlB2I3XGbMeUAoya5RHsoG3TYxf~P6lA5IM5Z~mDZlcbZ~AG7255FE6Z3Jl9kfMArneou6AYaCfNBqNTRS-P7yX9s0Kss-vM63yBulxhS7CqmBKTZsXR27PNjJS0PMYsWBzciuy8UqUkE0YEhSLWSUYAXLP9FKs065CNjxsLumkkoF~MNUaBNEmbCrjpv5Ih9vrwz4XAJSE~S61qSMj6O-nvEPDVhTJJ5ymeoZnYMpIRt4r7FkCTH8vYSkXZkhqXUkLC11WPC33lw2wzh-irmIb5GQeab9o0-DuNQcvUnbK13Jxkq5XiilfK6kgKiPcEniqxMb-4paZAl8dj9Zp01LvhfjlS0c459Jv-gr7ZkjkX7hhTaEVvqwPyFgoVKnxQCitoZrK98WRKJwu7EQb-Kin2vzUsYfpGGI0aT68~gdr23oom2FsoZ~owVuur1h0bJr9mnCaMf6jaioQE7wezxgAAAA +mp3.tc.i2p=LRCWTiJouI6ohqtaVBNHWZe2ymjhtyj3z9KdeI2G2D9l0cYFsG0CRUVT5VPYOg~WykALRVBiL-2U24fbiQ28hhPdQgBMBDl9aQiZJM2hv~di0uVOdARhRSgCDgRQAWioAfpXeg6pyklzXU3TNLY4c2CRLe~9Y7wuLbK3lONsAApcxxKeHLrfGNkZZwJTKd7PcG78KAHU07E-TVNf4tQrOh8tSrHaMB1r7cQQv1Jl8mNUZWz4fGeNYEZ1wr04w74Em2~Z5K0VZ4mH5DhFGXc9ALYzZf6uOVzZKiUC0eOcdfGNdVUbIog2CYJGH69TgAX69d5vF~kEzvHSzX7RxUTt0y25Rlbi6rHSDF36xOfBrOUVnSPn5X~TdKLygz~zusYpRdGZwlsyOTKVTzJHKlU6Vp5Dofijj1bUwXDQ59XCpFUqEDA17nETiOO0H5jfieYBPS6Ke2cFTAhutSvaw~albCd1eV7RPqeGdw-vFfKoucDIEVUT40B5qalyFRKIxx3lAAAA +mesh.firerabbit.i2p=BLt3pciNQcVIU-IGnTfSsrputh~b6drZpc1vH8qeA745XoE~nMCGtw8S7HGYX4uEbwk876nQRPV4qRwGtWWkWBs8BX9qX8NABoXFk4G-6NifB4TxEizC~ZAnXZ2uFs~nrqodhyCR8bBHJL0tzBYK3E36zc~SA-DKqQ9XSHWp7ScW7Z3cdQnYKma~x4u5eZmcS23uie3OIfOCk6pJOabtaE-YWRa1eUizhucI-ysm789GumjTo858vHR1mTQfrsPTqNri3yz0aIe~w06ifciq~UlNjVfx89lLEso1vmg8rfTQ-hwxS-qz-u3K5x5vtqdGp673vCvmEnQpU6GEycmkqoLCho9pNQzGbka-OVHg8fZqlFMeBfj2iLz~zlv170jvX6HTlMCNfBYnFqavs2RQJj7--dJ0g7JHReGMKL~TciQjxljrV5AoN-0afRzTZqtDg13PL4tltJm5U1~f-GcxlsjKLZAlv26LlZXsvTDU5plldsernv3fDcBev9UaKYCwAAAA +chess.fillament.i2p=8xvXLwcBYu2MxqMAoG6DIahvkNAwBQs43kdZTg9SlouzC3JSQ25RHvspbrxWoGIVAlB~XCxVmBB6EolYEHoSZv2YCziOj4UVxEbVP7nHkh32-7Uc5T7xlcjk8-rsmZzdgz9NhxKVn2Uhp3xtcdVAiyG4mpXisvK-7NgHY-mXPNvj6goaH58roeDUZ5otJN7nCFmcRAUpaUk-nlQs8YfgSCCEsWKOWhsVnAaHwtqtDlpdTo1YKooACMRSa-DcV5W75Il80JEWBD79qpSAeONGAOMMPT~VEMwNNg001VG-UZbvyspZdxHaETw2yd7N9l3-mwI-sYcveTTnNXLWO8IjdgDLdUIP5qrIW6WS9XZIHRKqT2kpwEw7xsEgCA~qSNiZWeai8n6Zs0rLmdyeZeafVEEW9vr6CKcLZ5W7i1SMDqCKnzSbZdd2Hvpb9aWFf5foEjLt33n8w2KSaCUe4zmN~xuQMq2yiB-vQ9~5fgPmphlMxo3ca5MTCfbhshw5137YAAAA +kaji.i2p=i-nivH~36AgabgzvS06oKHR~MLoy6NA0oSv8JuNJDLZB8FXEDzIiyzWISmw9XJm8I7QqZ1yFH8~xe84STCHHvMMIQQrfBmOUODLWbKZor~~KhiHdNLtfVZC5BpnXkBCJkklj~fMYSpWa0C~pVRrZl75RoGtBjDVP9P8hioTv5B6RC86g2ypBH5r093rY0wnzxSL8-ZuV3F~H48VYbqro8cRlbMmjx2oEsSHkDpQyjCMVkIYKaCijkSArqZTG~zX6op6Ng9CJwdrkjKSsbzjV6MLnE4aNv-jm2WaGGD5pR24h7e3ImDOGAr17tXRtmNX5ZEQ1udQp8qIhd8UMUumrnm962r8KJWK~9WNzcVeqDrIxaaxC7vcQmXxoPeEW2efbH0yKhVZ7OFu~I9cAapSe~aNWp9UK4URSpuJvOedt0axp3ORaaM-a5U7noW3Ao-HB83qfFEPU-6uUu16HNiPqCFMJiA0qODTOwHiyyx4HKQvbhjujh4mmknSbsuapdgR1AAAA +human.i2p=ji02vZzrp51aAsi~NZ8hwMLbr1rzMtdPUSiWAU94H89kO-~9Oc8Vucpf2vc6NOvStXpeTOqcRz-WhF01W8gj-YLP3WFskbjCcUwz0yF8dHonBeC4A5l4CjupAaztBSMbhu4vyN9FJkqZUFN01eZbQ9UqgXgLWMp4DtbUwf78y8VrzdAfmUOrVn6Iu89B~HUfOAKnpIlQXyGsQk1fnLw3PzDo2PVi8Q3C1Ntn0ybovD1xDKPrrHliTK4or2YujTcEOhSBLK4tQGvouN-tWqcVoF9O814yNGtze~uot62ACGJj9nvEU3J7QPgOl~fgBJ5Hvom0Qu-yPAGJuAZa29LSHnvRhih~z~6lWZYHREBYXQ58IzKktk90xJWcTwlwRRhyO-Sz3A5JYR3jM97h4SsoYBVrjK9TWnvGKj~fc8wYRDzt1oFVfubLlT-17LUzNc59H-2Vhxx8yaey8J~dqdWO0YdowqekxxlZf2~IVSGuLvIZYsr7~f--mLAxCgQBCjOjAAAA +lp.i2p=i~VkqY6fes7yCR6Yn4Nowuo3h621nKC9yvMVOEGW6nC61kLRKS5xcr7JdsFohF02Z7neR7Nv3GLB1qTvqguU2SekCPpfzNYNSDCZZVPMy1IXegZupmMMtDXnY7dAwcy~d5hdjwtODidfiG37~C-AE2g8cogJSAG5-sgOTOcA288fu0n9qYn9lejK6T8vQYMJIgqW73K5ErLg8F9C8yPfCNwRlqOZ8xSWowpvlyzWy5OFMmhk00S4JA5TAKlw-ILQDT20qFOHNcCWc8biXjNDozMxcw~h3rq-TQtWyW0-J8ERf2tmjPSoRr7zxKTFXlP0ibQz8HqAhlt9xDKlpd2LZyI9cKvMNfmGBIrFZHckO~rHtCTNtA7dN-UheC~k2bX0hDlQ8A6QWKovTnt3yktOJjwxBQU3iL7UGXbS8M~8t9LmiEKdTZgMUnAcBLkrPOfTAKpkCLE-~yAYBDj~U8ZKJKEtD-gLDY9~slVuo2AgNLYF6bipXxSMsoCzevJvcWnqAAAA +bt1.eco.i2p=SUHjD4QvbuY5VVTGTm49U8B~DtvTS2bwO1lREMNfJtZU6rxa4tgdqaIpUjRRrbZYLxZcgxjUYx4Bq7gcjriETIRaMy7lQtMx9zFk~XLE5baTmZeXc1~xuQCCTelnYv0yUswWSCZx7ll2a-Y6d88jvTydfxcCTNAYclT~gHnA0kU2kRHD5vgEteidiXFBVer2Ps68tnARUqURDKxTRyRnWtrxwQocmP5MJG29e4dIptcecxX8bKhgqgONzPAmYzAR7F694wEOXJZQO67ro0YQEuQmDXICBI8IwkbAt8qM~BG~JF1H26iWblWWs8mGJonwl34-CpVjMSXWVk~SC~60-xr97CsWSGAeZqrD8pLJyDGFsJ0jNFV0L6Q97ryUViakVwsHaAMlxZ3Hh7KAc6TUcJEGvuygRJ2DKeKA1wyuUDFc08m40HrRVwydn0hwzs4Qfb8hXfGHQ9-yauK6Gk-HvYFUL9qcIrOfrZGgq4xLVdgVh2wB1mIOTkcBMMti4941AAAA +gernika.i2p=1D9ee5J04ZI7pvCwQ3hXQMpeMvbNw9cz3V2pioQ9LakwRQzfMEb9CVAeiFt-wE4HyTFWKeof5rz8F5vmIqFMaH~oMJSkCyNtwPfqiRAENeNeALHbY2plMPfqCfEFR4GkTqXnalAkJGqDjo55CokUfalEVTGMvNiv6i6dmNvwS8~0X56sXIaXgLKuGIK~UNOg9hT8A~uEGQWXwTKD3EDmECsJL40iYcT1UR9rffzuyxOvDhL12HbJ8bIlUGarzEscH-jolj5ShvZAbEyw-MnVzR9LiEqy7DaniKpPtC0oXRZuz7PpcQTqzN-zgQaLq8bHTx7NHIfTuA4P~hhz-STO4SjPot7h~Gbdglc193OmGlL5QwbfjXfdOIccBDh6~jtFaa28GxHrTMoi9GafjnllLfWpvynN1y5H7Jh7Uw3E7KDtBGVsDg9-btyvyQLP3kkqPfIAn7Oj6ePHr9u-TN9ZwDbWj~QBmXXutsE~lLu7aT7kv5Jx6PFmLEeWPib82UuGAAAA +www.aum.i2p=8x3TYbh9aq6EVo8rSuPWBVSrwD~yS1al6z0RZYvRaQFXL9hFUUJYJNL2n3oR3tBg7Zr00MjqntIuBMd20LUKasnklTp4hDlDCE0KfeftYh~bFGykMRf0yTYEWaMHYpIRBY-IJEvSVlgHe8E4AWLMv-b6VKCDZ0~0AdUrsHQ0Qb4NQ-igBPZfU6c~UU0tUVUl1Efsuz1CdAp5pw5RdPviFtPH4tMvUca2t54Rwa-6v6QCqFZ7S2awyhAa73Zb9YlcqT4hP1JHF0wR0rL-OEoJV0gG47Co4Zr903SNW6cgKDj3lW1tIpzcVMoH3BE22SMEVjYyEHgAORdoYwaj19CUg1slDGmvUCoq4dPsnCIrvV7N0LeoUkZekt2pvr~yCH47ENV3oQYpFVLcMLN82tzI0ZFz5IyBHWGr22vlDlT1C-QVhAYQKN92XubDXSEgrhWv5IHPB0h~EgZi-rDcsJG2zb6ZqjtKFHp4CnNvTUxE1cCJh0aR1MDzM~o0iSMiMqh0AAAA +madman2003.i2p=Uilou~5268x~UI5Cjg3XYKfBCeKVEMuD0g9Ea7~j2aNXbczbYXCdRQEetYMk0919Dyyj5Bfpiz5m4~xHMUoXs1aJedteFjvwwOHcXE1IuhORhILv8kKyW4zBYrK7LUnvn~xFsnl6~xaThgwxneXLGhKz5UOitIzDUJmxCAwfcARHgx9PIUuJ5LTninB3Luyev11TIqweOfe~X0dzVYSmemBgaw2T6dxCz7qz0mN5fJe18~qnTqJibKT~7teX~hQhRQolk38P3tLeBRq3wZGTRsJ6biGEydTbE-rqKrNvIMNYvUbhhRHEMdShAiUo4gPJwbyCRRScI~DbluYuy6iXXURnrCIEv2DKtfpHSS~JccPu25HKcpNBfCLzQye7L8hWgjAyaQoTi3idW2JJNEIBBBS4yfGk7wq9swlzBK2aanI-JzFcDrVDXd~fpN0JYN-phF3YJnKspJe2i83BW8J9b-y2JJVbTWBwU2mIEE~gXKggsCvqESfuKK5RhgFaS4dTAAAA +ardvark.i2p=lHNIrjt2OnxJG~p1ZVJ1UzdwoCNLSAGbXq81bSKFQsMNCXvVtYA8-uJPTk~F4uYyaiH7x9SGg5B5dK0RmLdacBNCpwPlCPPxEpHbWizUahUQMo8ifLsyOS02ev4Bxf~i50RHmjU1zPUDe6LNxlFmA60zehKAkSR~pp2SJdbs-3nyBhvG6SX5CUHY5r3g7PpcGvGDnPOuGGJtR0xIqi-~89TsKK74qS6JiKv4BzqbWUn9eZ3TrSaC1-LAjoIiowC6e2Ske9r2HEl3T4fJ3qmOTu6vZWLQCBkzprAVOFoTwou8MJU97n3l9~1UrgghU939jC~IrAye~VolxqCdbsSz~m4Ol9fWdGmAfs~l4ZNgA89Q0O9Z5ooCm5LBlR6SjtOLOO2tQJstSb0V7-XpOxafp-a4EmXMEzg6f9ttBZNDW86hxLUPUzT9O90~27ss6j9txgx7lmFrDL8EjoQIFis6MNZRhqK4O7elL55UltDQeSel2E11iogHa4oZs8S7BsReAAAA +nntp.baffled.i2p=FpTF-krVDnZvQrY3JPZ4A44P5ewaVMSppoNrfH60g~4O3AnU6Uz9V-izgw~Hrn9u10MexFZSYYDTznxCOc6MdSEME6X~hfWYP-g0zCt~OutUAyhyUiWV~g2PAD6u-SMAzxw8F9gesheQKU7drjplDXiTNMXqD9ILuAVaW4VABmci1k0mqSEOKPyZkRyJZJNbU6vx~ELytgQJqwK~LOqDiiG9~6-e3MpMJ0xHy2yUibd6xv3GRLMaJ9kWAruMM5otZdxJiOUOf7n7wWvSVDgmwUx6eEkS8bVlsPxD~HW7i~DNPa5Aa7QD0o46abx77YF2jaComxTzpFx6dwj-0AqTMRhVwtfPgiiXbzx6g28Eggyc93QFJ1wUX5Wgix3RjndsHE9bVOpxNjjDlRwpRP6ytu7zYDkyjJUXDiC-8SRIGDvcSQZi0Op7KsvrxgGt6hGeJyN4JhxE08vDHN~1S8DwqNzc5sQKkpmdJHywOBhzeeNucB3EcSvh6ZaJfJUywZ8FAAAA +www.baffled.i2p=XchfxmcxW-Ex4IJ9Udd8b3O4eKgjJJx0FKM6fUbqqqXUQmOOoXKhzKwlKYobN3JIOE5zYXuWoPiW13a6befjYhZ4x6lky5--GwZe9IgLsmBqwsqGRHRaCqGiDcsHLdIWUJFWmHz38U0GYW6ggcgODQgxs00Kc2u2zfhOfVbFb8ckjvPOFah5iyfh4UQn8uaOl7WOCQmg0U47vSd6glHRq50NdRryGJv722U9TZlnrara-5f7oQn2qm1ctwnnm3sUjRMsNCJbCwuvsVY04axL5VXggwrZ6nDaZOIQHvzoNVnQtbT82GQvcBEda4Jr~IX7gLYiDS7BGNbX1NsOWBTr8GwaozPzFnaA1HMHYH0n-m2Tf~8O~7oqWzlQSP4mqcPd4ePbaXdV1uNXAr4-kziO7J3ID4vhG-biS4fYAUZqEs2rwUgFU1jPocl~Aoi~8alcgWK98mN680ZrT6th~w1BC~rf1q8Zg3MkE5RwW5jRpkOpH5a7vU8Lp6x1xQlLx6YXAAAA +irc.baffled.i2p=AjR0lkZfdzmQqLofq5En-yeUYZ9XOugrcpqgA6giY1v880AnhB~xokcAdV76Sx~D3MLkXKjCYF-AMIrvfV8cRZ0XkY8FPFb-MnsnACGirQ4~WN6kEjQ18iEH-OStEeoATiEzCzhQlIaJkCLnRo~lAbsnmqMrV3tJIjdQOL~uxbZQB5rQl~50w01XWIzsjwo0ZqzPjsJ6715HMRp6hag0NYNgf8ZE7vFKkvRNubl60lI3LWduoA7BjwuqCvHP8QddYmmL1s-L0rjDB0JGxRJ7~YZlUA0BECTIxdv2Kv-QEwr2UmuSLFQiQ2jQ5U5z0tzwObaIcAX4aAaKY16mC~cQE5cltSS3ElnLrI0qhpIeTM6217uigbAO6iigYFK5sazru9YEt0jDk2gzxlTF6m5BAn70tnu5IAikzRe2NI8VnkRa7K473r~mS30CzwOHXuxlSEPQUK3AkkrQN1Nfw5dCBLTpIF6aMPqR5tracYPOEOlro76rzChnNu-QDZmOQGkxAAAA +echo.baffled.i2p=RbdhJKxX35YHvCFR8wNklf~4uJISsjbCy4ur4hsD~0nZ44UVbjbYXHG3ng-PG-rcU8WIhnS2bWvyEWToQNRW8tDreXzwL3uqWmD3ceDVWgaL96oi8dcaiuNu7ualetsP8PilEOWifgAY21whF3NpFtrGPqt4-jXsQvoWiUtdncezbJu1BbcCc-18QrSrJqUjp3MejFYIJwBRZEu6zLRcN1Canky1957WZeIZUKvEyaEY~nVWZXbjcLGZKqHWIhBOc28ldLBI0xGzcrlUv4VAz2AMH0E1hFCu1sb-GrHqOuyRK1pbDKVL9dg-l7pNl6MzVruBAuDyusp9nJKN4qCjk01dy5UgQrYIFfBKGg7TIiyBcQQJHVbZPLMLbJf52G0EaVsJMTAg9cwNsJihz8-Xrp-WIio9odX9VNT6VwADcn8NTVNuf8wA1j389Ns~e4HWz1oQK89eN29GV2SD5E3PZGJ-0iBlk-Zz6hnca455tf9VfFTxKUX-7~93tRjwWdoOAAAA +bluebeam.i2p=JE00TEW1khGvC~IB27LCrXcsZuNtzE6LE6rXL1Vy0D-hr-FkfZwh8AeQ6uocbsmGNagUY9w3C-y9XfZnCK3THev5QL3gh5SNk7iGMjUKTmR4HhKomYDXhIAOnkxeOJ7UrcuPc2H1n1SeKVXqmv7-ygRvVWY00QH7ucOAwjq3hRkuMn6r7bjxbi26Gd8xAQfjCBVvCyQWCAiBHKpMJZWSlOjWrUC~-t0uCQNWGxcskvz~todIdv3ES2XZhPVrYpYmR2mP2K9ihH1L~GpwQYM7G9-LfqfmeHA9L2i4c1ovqxPxfHwMFIevjtIBUPYZ8d7t~jkfBDjSNvA-pwIFvLmqkEOYfs8G1n6ER26PUEtp6JRfa6RbOFPcAK7WRb1lV1cVvGYr6Sm1~GkoIw5SJcFspKl~9UV-f9H5737W4mwXLvvBJWwFz9n~x0QEnEO16rnX4YVchNryTkSqcSingSQZdPQvYnHp9esCi~Mp4qvj4InRX~pfiHYGzJ1a3CPiNb2JAAAA +www.janonymous.i2p=ovdbTie3mEme3kkvCsXjfU2BzOB0oPv0UvQLoUDSUjmPHipugKXZFYdBu0ySbl0sbJEyEgUQxDqywDq53vGtJCjqb7wDa4~hicc9bncQGbR~jhppFi2LneXRW5qawilTNP17zpyISSleRPXPBzFO65n-Z2BYz8pBm9gx0gB81UanojtE0jvAap-aYySMR6-qka44YOnEgs61fuo-~VLTgcDae-4tS8fB1PPgoLH0eqR6SQV6j0hNB9oXY4SndNtgTgCuJFTA6rkZyKdf5r9BuWybiFMvLJa~pXqSAbLefTYQldDb7LH5KQBAS2DBM7~Ri1yc1JPf-8MqWRDfi1povUICgF9kYOF7qYNaxi2xPPpqUHMiB96nUu5Lrmrecjb13qBWLDpOhiuuWconuggLo0~nFafn~Wh1yj7vCbeFfAwYaN12MleG1pzjcb1kX2rhZ-I2mVItv1WRsIVrnGiXUxoN5ZJoymkWGB7P7KdA4iCZ~CHqNLKLGQxSfblK-3XKAAAA +nic.i2p=IV9bI7aaeN5cHSqgXe9df43lh5ZgWegT50j9FbZSc3xKFQYKKrsKYH8f7LCvzrkMGrRAjSw0yRQtRUvnTn-2lpLG4DMs9ElWHVzuIgQKL6hqRoMawcbQxth6KFLs-pjGAFfLqFRqOeY1kCGOryhuhQ7iJ7iYQicRVbfTjSYDE5Fx4oi5LbvIXL6UfcuN3SkF75EPUHZ910dKukWst1ahwaOYj4U40bGuebzDUd9Aha7tgvE~WJQ2lMLhEm6~wOg9zsDzaTL6FI7XgfREEU3XO~WOgpMJBRJT7pqAqYBK-yCI5DhfyETWZMVOuoKUhlKt78squ8B8XHlXFgpaiOE9Q1vxQWYNn~UaAjUNbeeyC1iKjj5wIlzT56G5kGAOlK-6hIPs31I02TlKABTDD-BzlyyjyBS32Lkx-5P~Z1r~okn4PFiWDJuY0idmrBNtd4Yrqc4JkXifD1evgJ5042cV231WNBbjo95jv4HICV8WaiT7LxYlkHSWqzt~SI5017MeAAAA +reefer.i2p=OgiTTgA1xghkPcwgGpnyIJCBG1cZk17UKMAJ7H0YiroAvrAU4mexKPkWoy3JFmsenJuHnydvHJLaVbfGfEYu9EIDPynmIfBdM~WzM3Fe4IYtg1BxNnPmkz98p7lERcXiZdKXgyKPXsMta3kyEWNLrfKDwcmC4CUphE2sKjM7J61uxriCNseXaahP-U6c0aON8AFWnW5nJzF60llQGdGibKsUXUJCC5K-kphU2TEnRKMSaoNOa-QDzFsWxc3PhTDC1sRcttfsYDmW3LrXBsSaVHxAUdPveDCfKHQDoMCWN8rTBLN2GcOrqEKt85CkU1fhACt~g5asUYEvPeqVW3XS~AysfJmWWdVFdSCtQ2LkGGt1CZ9SMDThs1kqiMinly6Srpcj~eUZbb4iyumcfclmiVotLGhAkm8JNwWXJc8yfTxas1GxlTlmTaATt1J1LZS~WRTykILPGRY8HEQNp1j-vFgpXpI7xubNzPGCHQWAkOTsWfU8NvyGnXk481O6uRS5AAAA +ugha.i2p=318iVBG9JmuN5R~ClMb6rkWqkKoCZ3yGvD4dyIJAZum4GXFMHKSLrRiBf~2tFJ2KW1Lg9tKeyDgTC6sMt0aEQTcmzZl48BsFPZlH~WiI0JnPadihezDowBSdBhMi0RXoa3~xbfOKgAsHJv1zjrfRJHYz9fgG9bNQv9~oLeKz6YRdi97yrvKZLuzaUakYWfwdk7t9ZVhaXxsW8USXLSeHfdeQb0NYmsNc6is7Gp15HvsVUMgZXuBGea-AzMY1SSA6KdJwwPlXFvvcTM7neZKJzFVXyFsaOMEDjoUFfEC3tq277H0cqv4rAVmYp7WN78oBC7JgjfvFtXCY1r1l2-Qh1AEMwevVwI7tWdPUp50eMKEiccaHNZ7q2Zt0Uk0vlxKYW84p8ZTvXqfYDWyN8DQH2NKcy87MV1ZapDrbJSrF7cb-LvZy~nHtx~UwdKLS2gziM25JTtGiC1litArYS5KaY8rQXtL9kkSx9J66gc-05S-nbFMN-f-rEO9Fl27RvdQPAAAA +tor-www-proxy.i2p=9bZhTZvATJzpBa6UPslEwJCiDcsNhguT1mwbayD-rY0TN4Igj1PqPeVranzoO4Ity87ABVu2XJXatMzp~xHHtNiH6hF2XwJlpzx1Rr7A7SCTZwfBB1qLglwEBqYNEV5WT1faIDaArLc6o-ukhrOkIa8aJdaEpkjDkHjPtoOWt3nIIYlDxIliABrjFxPDeQZcOJyw7ftckqWfv7RKjdC8YpZrjXuojmi-TuhdRPu58tCNoH3j9laG4fUuU1RPK0YEj1HNSRHJVHDpCATtgHHfPiQvYK2HbMb0UBUfCtccmYqu6Cft5xZGHEOKvrMLXeEryV9ye-aczeNfBG2KzaF9pgQ2AN~eKBW7~UaNOQakyIaHDiY7aZ5qOCNW1CjuyJsEkjgRvqHogh6k5d3CkP82VlbEpTl5XaJFuflNJ0pHqZq2Le2T3wMkyECLbR0cX~qifE6Uw79AJnu-XEYQHFvHb0tV3XY2STDulgZu3fqZsjOVw2lZMHPHsszqlhiDhZIZAAAA +jar.i2p=xPIYObh2AirO1xoWCj7Wwc5RsGmQ3qulIAOHux9pOm9tzErjAfxv~2EazsZjyXCZ0zi3ylUjxqfj3L1pWEQBM-VM7HshHwg-PTuGWcdcUSRRFpQ7Gcp9u~~I3HSLRdHDj6ZkBNBk0jXM03tSKQEE4V1eum0tJwlOhBCNVqtt~FhyOBvo9~ypv6zW0sb6I3NppTYGq1LL4py4KrHSjb80e3Adfyhl1E2TfSHv6Uwp8qB~a2ac2IGhB2s-FK4gKSolpV-cUsn3roZRyq9jKJ2ciT4Y-PVcIDl6D8kV~LcNUbqD7vHy1yQxv1ByCCyIi3IYDacl5n5udwHO64L0M2mtZ1zWHS7K0~IxZTyk~mpO98qslbRfQqbk0ohZ-JGFhuB~cbVWlH6tSLmMJqmD-rOWnuxRfHLVtnbQstObQ9~KVIaX9KeLusgna2ZOhFjcy424BHtaVnbnLVyh-DEq~LkJGNx9Feyi6Z-aSQvThuhyE-ALiSvSw05x2G0yM9Me6MOWAAAA + diff --git a/installer/doc/COPYING b/installer/doc/COPYING new file mode 100644 index 0000000000..5ec43ee156 --- /dev/null +++ b/installer/doc/COPYING @@ -0,0 +1,278 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. diff --git a/installer/doc/readme.license.txt b/installer/doc/readme.license.txt new file mode 100644 index 0000000000..84a9518be9 --- /dev/null +++ b/installer/doc/readme.license.txt @@ -0,0 +1,10 @@ +$Id$ + +the i2p/installer/ module is the root of the I2P +installer, and everything within it is released +according to the terms of the I2P license policy. +That means everything contained within the +i2p/installer module is released into the public +domain unless otherwise marked. Alternate licenses +that may be used include GPL, GPL + java exception, +BSD, Cryptix, and MIT. diff --git a/installer/java/build.xml b/installer/java/build.xml new file mode 100644 index 0000000000..4e7328104f --- /dev/null +++ b/installer/java/build.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/java/src/.nbattrs b/installer/java/src/.nbattrs new file mode 100644 index 0000000000..ef1a0f3c09 --- /dev/null +++ b/installer/java/src/.nbattrs @@ -0,0 +1,7 @@ + + + + + + + diff --git a/installer/java/src/CliInstall.java b/installer/java/src/CliInstall.java new file mode 100644 index 0000000000..9c66533784 --- /dev/null +++ b/installer/java/src/CliInstall.java @@ -0,0 +1,67 @@ +import java.io.*; + +public class CliInstall extends Install { + + private BufferedReader _in; + private PrintStream _out; + + public CliInstall() { + _out = System.out; + _in = new BufferedReader(new InputStreamReader(System.in)); + } + + public void showStatus(String s) { + _out.println(s); + } + + public void showOptError(String s) { + _out.println(s); + } + + public void handleOptInfo(String s) { + _out.println(s); + } + + public void startOptCategory(String s) { + _out.println("* "+s+"\n"); + } + + public void finishOptions() {} + + public void handleOption(int number, String question, + String def, String type) { + Object value; + while(true) { + String answer; + _out.print(question+(def == null?"": (" ["+def+"]"))+": "); + answer = readLine(); + if ("".equals(answer) && def != null) { + answer = def; + } + if (setOption(number,answer)) break; + } + } + + public boolean confirmOption(String question, boolean defaultYes) { + _out.print(question); + return readBool(defaultYes); + } + + private String readLine() { + try { + return _in.readLine().trim(); + } catch (IOException ex) { + ex.printStackTrace(); + System.exit(1); + return null; + } + } + + private boolean readBool(boolean defaultYes) { + String str = readLine().toLowerCase(); + if ("".equals(str)) return defaultYes; + return "yes".equals(str) || "y".equals(str) || "true".equals(str) + || "ok".equals(str) || "sure".equals(str) + || "whatever".equals(str); + } +} diff --git a/installer/java/src/FetchSeeds.java b/installer/java/src/FetchSeeds.java new file mode 100644 index 0000000000..88362985b0 --- /dev/null +++ b/installer/java/src/FetchSeeds.java @@ -0,0 +1,86 @@ +/* + * $Id $ + * Copyright (c) 2003 mihi + * Licensed under the GNU Public License (GPL) as published by the + * Free Software Foundation, using version 2 or later of the GPL. You + * should have recieved the GPL with this source code, otherwise see + * http://www.fsf.org/copyleft/ + */ + +import java.io.*; +import java.net.*; + +public class FetchSeeds { + + /** + * Fetch seednodes. + * + * @param destination the dir to store the seednodes to + * @param sourceURL the URL to fetch the seednode from - must end + * with a slash + * @return whether new seed nodes could be fetched + */ + public static boolean fetchSeeds(File destination, String sourceURL) { + InputStream in = null; + try { + URL source = new URL(sourceURL); + URLConnection con = source.openConnection(); + in = con.getInputStream(); + BufferedReader br = new BufferedReader + (new InputStreamReader(in)); + String line; + while ((line = br.readLine())!= null) { + int pos = line.indexOf(" "); + System.out.println(" or FetchSeeds "); + System.out.println("The default seedURL is http://i2p.dnsalias.net/i2pdb/"); + return; + } + } +} diff --git a/installer/java/src/GUIInstall.java b/installer/java/src/GUIInstall.java new file mode 100644 index 0000000000..b5fb872409 --- /dev/null +++ b/installer/java/src/GUIInstall.java @@ -0,0 +1,338 @@ +import java.awt.BorderLayout; +import java.awt.Button; +import java.awt.CardLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dialog; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.Label; +import java.awt.Panel; +import java.awt.TextArea; +import java.awt.TextField; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.StringTokenizer; + +public class GUIInstall extends Install { + + static final GridBagConstraints gbcLeft=new GridBagConstraints(); + static final GridBagConstraints gbcRight=new GridBagConstraints(); + static final GridBagConstraints gbcBottom=new GridBagConstraints(); + static { + gbcLeft.anchor=GridBagConstraints.EAST; + gbcRight.fill=GridBagConstraints.HORIZONTAL; + gbcRight.gridwidth=GridBagConstraints.REMAINDER; + gbcRight.weightx=1.0; + gbcBottom.weighty=1.0; + gbcBottom.gridwidth=GridBagConstraints.REMAINDER; + gbcBottom.fill=GridBagConstraints.BOTH; + gbcBottom.insets=new Insets(4,4,4,4); + } + + public static void main(String[] args) { + new GUIInstall().runInstall(); + } + + private InstallFrame frame; + boolean installing=false; + ArrayList categories = new ArrayList(); + InstallCategory currentCategory = null; + + public String handleSpliceParams(String s) { + // any better ideas? + return s; + } + + + public GUIInstall() { + frame = new InstallFrame(); + } + + public void showStatus(String s) { + if (!installing) + throw new RuntimeException("Not installing yet!"); + frame.showStatus(s); + } + + public void showOptError(String s) { + frame.showOptError(s); + } + + public void handleOptInfo(String s) { + currentCategory.addInfo(s); + } + + public void startOptCategory(String s) { + currentCategory = new InstallCategory(); + categories.add(currentCategory); + } + + public void finishOptions() { + frame.startInstall(categories); + System.out.println("Starting install..."); + } + + public void handleOption(int number, String question, + String def, String type) { + currentCategory.addOption(number, question, def, type); + } + + public boolean confirmOption(String question, boolean defaultYes) { + ConfirmFrame cf = new ConfirmFrame(frame, question, defaultYes); + return cf.getResult(); + } + + private class ConfirmFrame extends Dialog { + private boolean result; + private ConfirmFrame(Frame parent, String msg, + boolean defaultYes) { + super(parent,"Installer question",true); + setBackground(Color.lightGray); + setLayout(new BorderLayout()); + TextArea ta; + Panel p; + Button b1, b2; + add("Center", ta = new TextArea(msg, 3, 80)); + ta.setEditable(false); + add("South", p = new Panel(new FlowLayout())); + p.add(b1 = new Button("Yes")); + p.add(b2 = new Button("No")); + ActionListener al = new ActionListener() { + public void actionPerformed(ActionEvent evt) { + result = evt.getActionCommand().equals("Yes"); + ConfirmFrame.this.dispose(); + } + }; + b1.addActionListener(al); + b2.addActionListener(al); + pack(); + + // java 1.4 + //setLocationRelativeTo(parent); + show(); + (defaultYes?b1:b2).requestFocus(); + } + + private boolean getResult() { + return result; + } + } + + private class InstallCategory extends ArrayList { + public void addInfo(String s) { + add(new InfoOption(s)); + } + + public void addOption(int number, String question, + String def, String type) { + add(new RealOption(number, question, def, type)); + } + } + + private interface InstallOption { + public Component getComponent1(); + public Component getComponent2(); + public boolean setValue(); + public String getQuestion(); + } + + private class InfoOption extends Panel implements InstallOption { + + public InfoOption(String s) { + super(new GridLayout(0,1,0,0)); + for(StringTokenizer st = new StringTokenizer(s,"\n"); + st.hasMoreTokens();) { + add(new Label(st.nextToken())); + } + } + public Component getComponent1() { return null;} + public Component getComponent2() { return this;} + public boolean setValue() {return true;} + public String getQuestion() { return "";} + } + + private class RealOption implements InstallOption { + + private int number; + private String def, question; + private Label l; + private TextField t; + + public RealOption(int number, String question, + String def, String type) { + this.number = number; + l = new Label(question); + t = new TextField(def); + this.def=def; + this.question=question; + // type is not needed yet + } + + public void reset() {t.setText(def);} + + public String getQuestion() { return question; } + + public boolean setValue() { + return GUIInstall.this.setOption(number, t.getText()); + } + + public Component getComponent1() { return l;} + public Component getComponent2() { return t;} + + } + + private class InstallFrame extends Frame { + + private int current = -1; + private Panel cats; + private CardLayout cl; + private boolean windowOpen = true; + private TextArea log; + + public InstallFrame() { + super("I2P Installer"); + setBackground(Color.lightGray); + Panel p; + Button b; + setLayout(new BorderLayout()); + add("Center", cats = new Panel(cl = new CardLayout())); + cats.add("Start", p= new Panel(new BorderLayout())); + p.add("Center", new Label("Loading installer...")); + cats.add("Install", p= new Panel(new BorderLayout())); + p.add("Center", log=new TextArea("Installing...\n\n")); + log.setEditable(false); + add("South", p = new Panel(new FlowLayout())); + p.add(b = new Button("<< Back")); + b.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + if (current > 0) { + current --; + cl.show(cats,""+current); + } + } + }); + p.add(b = new Button("Next >>")); + b.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + if (current != -1) { + if (!saveCurrent()) return; + current ++; + if (current == categoryPanels.length) { + cl.show(cats,"Install"); + current = -1; + synchronized(InstallFrame.this) { + installing=true; + windowOpen=false; + InstallFrame.this.notify(); + } + } else { + cl.show(cats,""+current); + } + } + } + }); + p.add(b = new Button("Quit")); + b.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + System.exit(0); + } + }); + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent evt) { + System.exit(0); + } + }); + setSize(600,450); + + // java 1.4 + //setLocationRelativeTo(null); + show(); + } + + public void showStatus(String s) { + log.append(s+"\n"); + } + public void showOptError(String s) { + if (current == -1) throw new RuntimeException("No options here!"); + categoryPanels[current].showError(s); + } + + private CategoryPanel[] categoryPanels; + + public void startInstall(ArrayList categories) { + Panel p; + categoryPanels = new CategoryPanel[categories.size()]; + //build a panel for each category + Iterator it = categories.iterator(); + for (int i=0; it.hasNext(); i++) { + cats.add(""+i, categoryPanels[i] = + new CategoryPanel((InstallCategory)it.next())); + } + current = 0; + cl.show(cats,"0"); + // wait till config is complete + synchronized(this) { + while(windowOpen) { + try { + wait(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + } + } + + private boolean saveCurrent() { + return categoryPanels[current].saveOptions(); + } + } + + private class CategoryPanel extends Panel { + + private TextArea errorBox; + private InstallCategory ic; + + public CategoryPanel(InstallCategory ic) { + super(new GridBagLayout()); + this.ic=ic; + for (Iterator it = ic.iterator(); it.hasNext();) { + InstallOption io = (InstallOption) it.next(); + Component c1 = io.getComponent1(), + c2 = io.getComponent2(); + if (c1 != null) add(c1, gbcLeft); + add(c2, gbcRight); + } + add (errorBox = new TextArea(), gbcBottom); + errorBox.setEditable(false); + } + + private InstallOption currentOption; + public boolean saveOptions() { + errorBox.setText("Saving options...\n\n"); + for (Iterator it = ic.iterator(); it.hasNext();) { + InstallOption io = (InstallOption) it.next(); + currentOption=io; + if (!io.setValue()) return false; + currentOption= null; + } + return true; + } + + public void showError(String s) { + if (currentOption==null) { + throw new RuntimeException("No option to test"); + } + errorBox.append("While setting \""+currentOption.getQuestion()+ + "\":\n"+s+"\n\n"); + } + } +} diff --git a/installer/java/src/Install.java b/installer/java/src/Install.java new file mode 100644 index 0000000000..5d11130035 --- /dev/null +++ b/installer/java/src/Install.java @@ -0,0 +1,612 @@ +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Properties; +import java.util.Set; + +/** + * Please could someone write a real installer? Please. + * I should be shot for doing this. + * + * Note: this has dependencies upon how i2p/build.xml executes the "installer" + * task - namely that this class is in the jar "install.jar", and in that jar + * are the files COPYING, i2p.jar, i2ptunnel.jar, phttprelay.war, readme.txt, + * hosts.txt, and router.jar. + * + * Can you say "fragile code"? + * (oh well, enough caveats. commit) + */ +public abstract class Install { + private File _installDir; + private String _externalAddress; + private boolean _externalAddressIsReachable; + private int _inTCP; + private int _i2cpPort; + private String _phttpRegister; + private String _phttpSend; + private int _inBPS; + private int _outBPS; + private HashMap _answers; + private Properties _p; + + private boolean _isWindows; + + /** + * Show some installation status. + */ + public abstract void showStatus(String s); + + /** + * Show an error when setting an option. + */ + public abstract void showOptError(String s); + + /** + * Show some information about the following/preceding options + */ + public abstract void handleOptInfo(String s); + + /** + * Start a new Option category + */ + public abstract void startOptCategory(String s); + + + /** + * Handle an option. + */ + public abstract void handleOption(int number, String question, + String def, String type); + /** + * Tell the installer that all options have been given. + * Install will not continue until this method has ended. + * When this method has ended, all options must have been set. + */ + public abstract void finishOptions(); + + /** + * Confirm an option. This can occur both while verifying options + * and later in the install process. + */ + public abstract boolean confirmOption(String question, boolean defaultYes); + + public static void main(String args[]) { + Install install = new CliInstall(); + install.runInstall(); + } + + public Install() { + _inTCP = -2; + _i2cpPort = -2; + _phttpRegister = null; + _phttpSend = null; + _inBPS = -2; + _outBPS = -2; + _externalAddressIsReachable = false; + } + + public void runInstall() { + askQuestions(); + detectOS(); + configureAll(); + createConfigFile(); + createLoggerConfig(); + createStartScript(); + createReseedScript(); + createEepProxyScript(); + //createScripts("startSquid.sh", "startSquid.bat", 5555, "squid.i2p", "log-squid-#.txt", "Squid Proxy", "Squid proxying scripts written to startSquid"); + createScripts("startIrcProxy.sh", "startIrcProxy.bat", 6668, "irc.duck.i2p", "log-irc-#.txt", "IRC Proxy", "IRC proxying scripts written to startIrcProxy", "Starting IRC proxy (when you see Ready! you can connect your IRC client to localhost:6668)"); + //createScripts("startI2PCVSProxy.sh", "startI2PCVSProxy.bat", 2401, "i2pcvs.i2p", "log-i2pcvs-#.txt", "CVS Proxy", "Proxying scripts for I2P's CVS server written to startCVSProxy"); + // only pulling them temporarily, duck, until the network is + // reliable enough + //createScripts("startJabber.sh", "startJabber.bat", 5222, "jabber.duck.i2p", "log-jabber-#.txt", "Jabber Proxy", "Squid proxying scripts written to startSquid"); + + //createScripts("startNntpProxy.sh", "startNntpProxy.bat", 1119, "nntp.duck.i2p", "log-nntp-#.txt", "NNTP Proxy","NNTP proxying scripts written to startNntpProxy"); + createSeedNodes(); + copyLibraries(); + if (_isWindows) { + showStatus("To run the router, please run startRouter.bat in " + _installDir.getAbsolutePath()); + } else { + showStatus("To run the router, please run startRouter.sh in " + _installDir.getAbsolutePath()); + } + showStatus(""); + } + + private String numberTo4Digits(int number) { + String res = "0000"+number; // use four digit indices + return res.substring(res.length()-4); + } + + private void askQuestions() { + try { + InputStream in = + Install.class.getResourceAsStream("/install.config"); + _p = new Properties(); + _p.load(in); + in.close(); + } catch (IOException ex) { + ex.printStackTrace(); + System.exit(1); + } + int count = Integer.parseInt(_p.getProperty("qs.count")); + _answers = new HashMap(count+count); // load factor is 0.75, so + // there is some room left + for (int i=1;i<=count;i++) { + String ii = numberTo4Digits(i); + String question = _p.getProperty("qs."+ii+".question"), + param = _p.getProperty("qs."+ii+".param"), + type = _p.getProperty("qs."+ii+".type"), + def = _p.getProperty("qs."+ii+".default"); + if (question == null) continue; + if (type == null || "info".equals(type)) { + // just print some text + handleOptInfo(question); + } else if ("info_spliced".equals(type)) { + // splice in some params already queried + handleOptInfo(handleSpliceParams(question)); + } else if ("category".equals(type)) { + startOptCategory(question); + } else if ("skip".equals(type)) { + i = Integer.parseInt(question)-1; + } else { // a real question + if ("".equals(question)) { + if (!setOption(i, def)) + throw new RuntimeException("Fixed and invalid value"); + } else { + handleOption(i,question,def,type); + } + } + } + finishOptions(); + } + + public /* overridable */ String handleSpliceParams(String s) { + return spliceParams(s); + } + + public boolean setOption(int number, String answer) { + String ii = numberTo4Digits(number); + String param = _p.getProperty("qs."+ii+".param"), + type = _p.getProperty("qs."+ii+".type"); + Object value = getOptionValue(answer, type); + if (value == null) { + return false; + } else { + if (param == null || value == null) + throw new NullPointerException(); + _answers.put(param,value); + return true; + } + } + + private Object getOptionValue(String answer, String type) { + if ("string".equals(type)) { + // everything's okay till the very end + return answer; + } else if ("string>0".equals(type)) { + if (answer.length() > 0) { + return answer; + } else { + showOptError("Empty answers are not allowed."); + } + } else if ("directory".equals(type)) { + File f = new File(answer); + if (f.exists()) { + if (f.isDirectory()) { + showOptError("Using existing target directory " + f.getAbsolutePath()); + return f; + } else { + showOptError("Location " + f.getAbsolutePath()+ + " is not a directory. "+ + "Lets try again"); + } + } else { + boolean create = confirmOption + ("Target directory " + f.getAbsolutePath() + + " does not exist - create? ", false); + if (!create) { + showOptError("Lets try that again"); + } else { + boolean created = f.mkdirs(); + if (created) { + showOptError("Target directory " + + f.getAbsolutePath() + + " created"); + return f; + } else { + showOptError("Failed to create the "+ + "directory. Lets choose "+ + "another."); + } + } + } + } else if ("boolean".equals(type)) { + answer=answer.toLowerCase(); + if ("yes".equals(answer) || "y".equals(answer)) + answer="true"; + if ("no".equals(answer) || "n".equals(answer)) + answer="false"; + if ("true".equals(answer) || "false".equals(answer)) { + return new Boolean("true".equals(answer)); + } + showOptError("Incorrect boolean value, try `yes' òr `no'"); + } else if ("numeric".equals(type) || "port".equals(type)) { + try { + int num = Integer.parseInt(answer); + if ("numeric".equals(type) || + (num >0 && num < 65536)) { + return new Integer(num); + } + showOptError("Port number must be from 1 to 65535"); + } catch (NumberFormatException ex) { + showOptError("Incorrect value: "+ex.getMessage()); + } + } else if ("bandwidth".equals(type)) { + try { + answer = answer.toLowerCase(); + int factor = 1; + // first check to see if it ends with m, k, or g + if (answer.endsWith("g")) + factor = 1024*1024*1024; + if (answer.endsWith("m")) + factor = 1024*1024; + if (answer.endsWith("k")) + factor = 1024; + if (factor > 1) + answer = answer.substring(0, answer.length()-1); + int val = factor * Integer.parseInt(answer); + if (val == -1 || val >0 ) { + return new Integer(val); + } + showOptError("Value must be -1 or positive."); + } catch (NumberFormatException ex) { + showOptError("Invalid number [" + answer + "]. Valid numbers are of the form -1, 42, 68k, 7m, 9g"); + } + } else { + throw new RuntimeException ("cannot read installer option: " + type); + } + return null; + } + + private String spliceParams(String s) { + StringBuffer txt = new StringBuffer(s.length()+100); + int ind; + while((ind = s.indexOf("##")) != -1) { + txt.append(s.substring(0,ind)); + String temp = s.substring(ind+2); + ind = temp.indexOf("##"); + if (ind == -1) throw new RuntimeException + ("Incorrect info_spliced param"); + s=temp.substring(ind+2); + Object value = _answers.get(temp.substring(0,ind)); + if (value == null) { + System.err.println("ERROR: Could not insert parameter "+temp.substring(0,ind)); + System.exit(1); + } else { + txt.append(value.toString()); + } + } + txt.append(s); + return txt.toString(); + } + + private void detectOS() { + String os = System.getProperty("os.name"); + if (os.toLowerCase().indexOf("win") != -1) + _isWindows = true; + else + _isWindows = false; + // yes, this treats pre-os-x macs as unix, and perhaps some + // windows-esque OSes don't have "win" in their name, or some + // unix-esque OS does. fix when it occurs. + } + + private void configureAll() { + _installDir = (File) _answers.get("installDir"); + _externalAddress = _answers.get("externalAddress").toString(); + _externalAddressIsReachable = ((Boolean)_answers.get("externalAddressIsReachable")).booleanValue(); + + _inTCP=((Integer)_answers.get("inTCP")).intValue(); + _phttpRegister = _answers.get("phttpRegister").toString(); + _phttpSend = _answers.get("phttpSend").toString(); + _i2cpPort = ((Integer)_answers.get("i2cpPort")).intValue(); + _inBPS = ((Integer)_answers.get("inBPS")).intValue(); + _outBPS = ((Integer)_answers.get("outBPS")).intValue(); + } + + private void useTemplate(String templateName, File destFile) { + try { + BufferedWriter bw = new BufferedWriter(new FileWriter(destFile)); + BufferedReader br = new BufferedReader + (new InputStreamReader + (Install.class.getResourceAsStream(templateName))); + String line; + while ((line = br.readLine()) != null) { + if (!line.startsWith("####")) { + bw.write(spliceParams(line)); + bw.newLine(); + } + } + br.close(); + bw.close(); + } catch (IOException ioe) { + ioe.printStackTrace(); + System.exit(0); + } + } + + private void createLoggerConfig() { + boolean verbose = ((Boolean)_answers.get("verboseLogs")).booleanValue(); + createLogConfigOptions(verbose); + File logCfgFile = new File(_installDir, "logger.config"); + useTemplate("logger.config.template",logCfgFile); + } + + private void createSeedNodes() { + showStatus("To connect to I2P, you will need a reference to at least one other I2P router"); + showStatus("Rather than bundle some (soon to be out of date) references with the software, "); + showStatus("you can either run the included reseed script or get get your own references "); + showStatus("from some out of band location. "); + showStatus(""); + showStatus("The reseed script simply connects to http://i2p.net/i2pdb/ and downloads all"); + showStatus("of the routerInfo-*.dat files and save them into " + (new File(_installDir, "i2pdb")).getAbsolutePath()); + showStatus("That ../i2pdb/ directory is simply a mirror of one router's netDb directory, so those files"); + showStatus("can come from anyone else too"); + showStatus(""); + showStatus("You can run the reseed script or download references (from your friends, etc) as often"); + showStatus("as you like without restarting your router. If you find your netDb directory to have "); + showStatus("only one file in it (thats your router info), you will need more peers to get anything done."); + showStatus(""); + boolean reseed = confirmOption("Do you want to run the reseed script now? ", true); + if (reseed) { + reseed(); + } else { + showStatus("Ok ok, not reseeding - but please reseed before running the router"); + } + } + + private void reseed() { + try { + URL dir = new URL("http://i2p.net/i2pdb/"); + String content = new String(readURL(dir)); + Set urls = new HashSet(); + int cur = 0; + while (true) { + int start = content.indexOf("href=\"routerInfo-", cur); + if (start < 0) + break; + + int end = content.indexOf(".dat\">", start); + String name = content.substring(start+"href=\"routerInfo-".length(), end); + urls.add(name); + cur = end + 1; + } + + for (Iterator iter = urls.iterator(); iter.hasNext(); ) { + fetchSeed((String)iter.next()); + } + } catch (Throwable t) { + t.printStackTrace(); + showStatus("Error reseeding - " + t.getMessage()); + } + } + + private void fetchSeed(String peer) throws Exception { + URL url = new URL("http://i2p.net/i2pdb/routerInfo-" + peer + ".dat"); + showStatus("Fetching seed from " + url.toExternalForm()); + + byte data[] = readURL(url); + writeSeed(peer, data); + } + + private byte[] readURL(URL url) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + URLConnection con = url.openConnection(); + InputStream in = con.getInputStream(); + byte buf[] = new byte[1024]; + while (true) { + int read = in.read(buf); + if (read < 0) + break; + baos.write(buf, 0, read); + } + in.close(); + return baos.toByteArray(); + } + + private void writeSeed(String name, byte data[]) throws Exception { + File netDbDir = new File(_installDir, "netDb"); + if (!netDbDir.exists()) + netDbDir.mkdirs(); + FileOutputStream fos = new FileOutputStream(new File(netDbDir, "routerInfo-" + name + ".dat")); + fos.write(data); + fos.close(); + } + + private void copyLibraries() { + File libDir = new File(_installDir, "lib"); + if (!libDir.exists()) { + boolean libCreated = libDir.mkdirs(); + if (!libCreated) { + showStatus("Error creating library directory " + libDir.getAbsolutePath()); + return; + } + } + showStatus("Installing the libraries into " + libDir.getAbsolutePath()); + int cnt = Integer.parseInt(_p.getProperty("libs.count")); + try { + for (int i=1;i<=cnt;i++) { + String ii = numberTo4Digits(i), + file = _p.getProperty("libs."+ii+".name"); + boolean isLib = "true".equals(_p.getProperty("libs."+ii+".islib")); + InputStream is = Install.class.getResourceAsStream("/"+file); + if (is == null) throw new IOException("Resource /"+file+" not found"); + copyFile(is, file, isLib?libDir:_installDir); + } + } catch (IOException ioe) { + showStatus("Error extracting the libraries: " + ioe.getMessage()); + } + File dbDir = new File(_installDir, "netDb"); + dbDir.mkdirs(); + File logDir = new File(_installDir, "logs"); + logDir.mkdirs(); + } + + private void copyFile(InputStream in, String name, File destDir) { + File destFile = new File(destDir, name); + try { + byte buf[] = new byte[16*1024]; + FileOutputStream out = new FileOutputStream(destFile); + while (true) { + int read = in.read(buf); + if (read == -1) + break; + out.write(buf, 0, read); + } + in.close(); + out.close(); + showStatus("Installed file " + destFile.getName() + " in " + destFile.getParent()); + } catch (IOException ioe) { + showStatus("Error saving " + name + " to " + destFile.getAbsolutePath() + ": " + ioe.getMessage()); + } + } + + private void createLogConfigOptions(boolean verbose) { + _answers.put("_logger_level", verbose?"DEBUG":"INFO"); + _answers.put("_logger_level2", verbose?"WARN":"ERROR"); + StringBuffer buf = new StringBuffer(); + if (!verbose) { + // overrides for particularly chatty classes + _answers.put("_logger_notverbose", + "logger.record.net.i2p.router.transport.Triv=ERROR"+NL+ + "logger.record.net.i2p.router.transport.Band=ERROR"+NL+ + "logger.record.net.i2p.crypto=ERROR" +NL+ + "logger.record.net.i2p.crypto.DH=ERROR"); + } else { + _answers.put("_logger_notverbose",""); + } + } + + private void createScripts(String unixName, String windowsName, int listenPort, String targetDest, String logfilePattern, String windowTitle, String message, String scriptMessage) { + createScripts(unixName, windowsName, "client "+listenPort+" "+targetDest, logfilePattern, windowTitle, message, scriptMessage); + } + + private void createScripts(String unixName, String windowsName, String command, String logfilePattern, String windowTitle, String message, String scriptMessage) { + _answers.put("_scripts_port", ""+_i2cpPort); + _answers.put("_scripts_cmd", command); + _answers.put("_scripts_logname", logfilePattern); + _answers.put("_scripts_winttl", windowTitle); + _answers.put("_scripts_message", scriptMessage); + if (_isWindows) { + File windowsFile = new File(_installDir, windowsName); + useTemplate("startFoo.bat.template", windowsFile); + } else { + File unixFile = new File(_installDir, unixName); + useTemplate("startFoo.sh.template", unixFile); + chmodaplusx(unixFile); + } + showStatus(message); + } + + private void createEepProxyScript() { + StringBuffer buf = new StringBuffer(512); + buf.append("Eepsite proxying scripts written to startEepProxy").append(NL); + buf.append("IMPORTANT: While this installer packages the latest hosts.txt file available at the time ($Date: 2004/03/25 00:56:23 $), ").append(NL); + buf.append("a more recently updated one may be available. You can check for updates by fetching the file ").append(NL); + buf.append("http://i2p.net/i2p/hosts.txt and saving it in ").append(_installDir.getAbsolutePath()).append(NL); + buf.append("Or, of course, you can edit and update hosts.txt yourself to include mappings of your liking").append(NL); + + createScripts("startEepProxy.sh", "startEepProxy.bat", "httpclient 4444", "log-eepProxy-#.txt", "Eep Proxy", buf.toString(), "Starting EepProxy (when you see Ready, set your browsers HTTP proxy to localhost:4444)"); + } + + private void createReseedScript() { + if (_isWindows) { + File windowsFile = new File(_installDir, "reseed.bat"); + useTemplate("reseed.bat.template", windowsFile); + } else { + File unixFile = new File(_installDir, "reseed.sh"); + useTemplate("reseed.sh.template", unixFile); + chmodaplusx(unixFile); + } + } + + private void chmodaplusx(File f) { + try { + Runtime.getRuntime().exec("chmod a+x " + f.getAbsolutePath()); + } catch (IOException ioe) { + showStatus("Error setting "+f.getName()+" as executable"); + } + } + + private void createStartScript() { + _answers.put("_scripts_installdir", _installDir.getAbsolutePath()); + if (_isWindows) { + File windowsFile = new File(_installDir, "startRouter.bat"); + useTemplate("startRouter.bat.template", windowsFile); + } else { + File unixFile = new File(_installDir, "startRouter.sh"); + useTemplate("startRouter.sh.template", unixFile); + File unixStopFile = new File(_installDir, "stopRouter.sh"); + useTemplate("stopRouter.sh.template", unixStopFile); + chmodaplusx(unixFile); + chmodaplusx(unixStopFile); + } + } + + private void createConfigFile() { + File configFile = new File(_installDir, "router.config"); + setConfigFileOptions(); + useTemplate("router.config.template", configFile); + showStatus("Router configuration file written to " + configFile.getAbsolutePath()); + showStatus(""); + } + + private final static String NL = System.getProperty("line.separator"); + + private void setConfigFileOptions() { + // set fields needed for the config template + _answers.put("NOW", new Date().toString()); + if (_inTCP <= 0) { + _answers.put("_router_hn", "#i2np.tcp.hostname=[externally reachable hostname or IP address goes here]"); + _answers.put("_router_port", "#i2np.tcp.port=[TCP/IP port number]"); + _answers.put("_router_lavalid","#i2np.tcp.listenAddressIsValid=[true/false for whether your external address is locally reachable]"); + _answers.put("_router_tcpdisable","#i2np.tcp.disable=[true/false for whether you want absolutely no tcp connections to be established (forcing phttp, etc)])"); + } else { + _answers.put("_router_hn","i2np.tcp.hostname="+_externalAddress); + _answers.put("_router_port","i2np.tcp.port="+_inTCP); + _answers.put("_router_lavalid","i2np.tcp.listenAddressIsValid="+_externalAddressIsReachable); + _answers.put("_router_tcpdisable","i2np.tcp.disable=false"); + } + if ( (_phttpRegister == null) || (_phttpSend == null) ) { + _answers.put("_router_phttpreg","#i2np.phttp.registerURL=[full URL to a PHTTP registration server, e.g. http://someHost:8080/phttprelay/phttpRegister]"); + _answers.put("_router_phttpsend","#i2np.phttp.sendURL=[full URL to a PHTTP relay server, e.g. http://someHost:8080/phttprelay/phttpSend]"); + } else { + _answers.put("_router_phttpreg","i2np.phttp.registerURL="+_phttpRegister); + _answers.put("_router_phttpsend","i2np.phttp.sendURL="+_phttpSend); + } + _answers.put("_router_i2cp_port",""+_i2cpPort); + _answers.put("_router_inbps",""+(_inBPS*60)); + _answers.put("_router_outbps",""+(_outBPS*60)); + } +} diff --git a/installer/java/src/install.config b/installer/java/src/install.config new file mode 100644 index 0000000000..d35fc0cfb9 --- /dev/null +++ b/installer/java/src/install.config @@ -0,0 +1,108 @@ +# config file for the installer + +# intention for this file is that a gui installer can use the same +# settings as the cli installer + +qs.count=50 + +qs.0001.question=General settings + qs.0001.type=category +qs.0002.question=Installation directory + qs.0002.param=installDir + qs.0002.type=directory + qs.0002.default=. +qs.0003.question= + + +qs.0006.question=Network settings + qs.0006.type=category +qs.0007.question=Currently, to use I2P you must have a publicly reachable TCP/IP address \nthat you can receive new connections on. This means that if you're\nbehind a firewall or NAT, you will have to poke a hole in it for the \ninbound TCP port (which you'll specify in a moment), and if you have DHCP,\nyou must use a service like dyndns.org and specify that hostname as your\nexternal address. + qs.0007.type=info +qs.0008.question=External address + qs.0008.param=externalAddress + qs.0008.type=string>0 +qs.0009.question=Note to advanced users: the default settings bind any TCP listeners to \nall local IP addresses, but you can have it bind to a specific one if necessary\nPlease see the router.config for the options i2np.tcp.listenAddressIsValid + qs.0009.type=info +qs.0010.question= + qs.0010.param=externalAddressIsReachable + qs.0010.type=boolean + qs.0010.default=false +qs.0011.question=Inbound TCP port? (any unused port will do, just pick a number) + qs.0011.param=inTCP + qs.0011.type=port + qs.0011.default=8887 +qs.0012.question=If you have a NAT or firewall, please be sure port ##inTCP## is open and can receive inbound connections! + qs.0012.type=info_spliced +qs.0015.question= + qs.0015.param=phttpRegister + qs.0015.type=string>0 + qs.0015.default=http://i2p.net:8080/phttprelay/phttpRegister +qs.0016.question= + qs.0016.param=phttpSend + qs.0016.type=string>0 + qs.0016.default=http://i2p.net:8080/phttprelay/phttpSend +qs.0017.question= + qs.0017.param=i2cpPort + qs.0017.type=port + qs.0017.default=7654 +qs.0018.question= +qs.0019.question=40 + qs.0019.type=skip +## leave some numbers free, for future additions + +## bandwidth questions, skipped over +qs.0030.question=Bandwidth limits + qs.0030.type=category +qs.0031.question=These bandwidth limits are fairly hard and unforgiving.\nWe do our best to not let any data beyond these limits be transferred at all, ever.\nSo keep that in mind, and set the limits to the upper bounds of what you can handle.\n\nPlease take note that these bandwidth limits are currently very, very strict, and the network itself does not consume\nmuch bandwidth. Everything will stop transferring data without warning if the limits are met - you really should set\nthis to -1 for now, at least until the new classed bandwidth limiter is implemented (currently planned for the 0.3.2 release) + qs.0031.type=info +qs.0032.question= +qs.0033.question=Inbound bytes per second: (e.g. 16K, 16384, or -1 for unlimited) + qs.0033.param=inBPS + qs.0033.type=bandwidth + qs.0033.default=-1 +qs.0034.question=Outbound bytes per second: (e.g. 16K, 16384, or -1 for unlimited) + qs.0034.param=outBPS + qs.0034.type=bandwidth + qs.0034.default=-1 +qs.0035.question= +qs.0036.question=45 + qs.0036.type=skip + +qs.0040.question= + qs.0040.param=inBPS + qs.0040.default=-1 + qs.0040.type=numeric +qs.0041.question= + qs.0041.param=outBPS + qs.0041.default=-1 + qs.0041.type=numeric +qs.0042.question=45 + qs.0042.type=skip + +qs.0045.question= +#qs.0045.question=Do you want very verbose debug logs when running the router by default (y/n) + qs.0045.param=verboseLogs + qs.0045.default=no + qs.0045.type=boolean + +qs.0050.question=End of configuration. + + +libs.count=8 +libs.0001.name=i2p.jar + libs.0001.islib=true +libs.0002.name=i2ptunnel.jar + libs.0002.islib=true +libs.0003.name=router.jar + libs.0003.islib=true +libs.0004.name=fetchseeds.jar + libs.0004.islib=true +libs.0005.name=COPYING + libs.0005.islib=false +libs.0006.name=readme.txt + libs.0006.islib=false +libs.0007.name=hosts.txt + libs.0007.islib=false +libs.0008.name=mstreaming.jar + libs.0008.islib=true + diff --git a/installer/java/src/logger.config.template b/installer/java/src/logger.config.template new file mode 100644 index 0000000000..6e63b7d65d --- /dev/null +++ b/installer/java/src/logger.config.template @@ -0,0 +1,66 @@ +# I2P Log Configuration File +# Format: +# d = date +# c = class or log name +# t = thread name +# p = priority level of the log record +# m = message +logger.format=d p [t] c: m + +# Date format: +# uses the java.text.SimpleDateFormat +logger.dateFormat=HH:mm:ss.SSS + +# Log file name: +# This is the log file name before being rotated +# '#' is replaced with the current log number for that day +# If # is not specified, logs are not rotated +logger.logFileName=log-#.txt + +# Log file size: +# Maximum size of each log file: +# 32g <-- 32 gigabytes (32*1024*1024*1024 bytes) +# 10m <-- 10 megabytes (10*1024*1024 bytes) +# 66k <-- 66 kilobytes (66*1024 bytes) +# 42 <-- 42 bytes +logger.logFileSize=3m + +# Log rotation limit: +# Maximum number of logs to keep per day - lower log numbers +# are reused once the limit is reached. Ignored if there is no +# '#' in the logFileName. +logger.logRotationLimit=3 + +# Display on screen: +# In addition to the logging, send filtered log messages to the +# standard output +# true or false +logger.displayOnScreen=true + +# Default level: +# Define the minimum log level to be displayed unless +# specified otherwise. +# +# Log levels, from least severe to most, are: +# DEBUG : verbose debugging info +# INFO : component status messages +# WARN : bad situation but recoverable +# ERROR : component error +# CRIT : your hard drive is on fire +# +# Less severe levels always include more severe ones (e.g. +# if you're listening to debug messages, you'll get info messages +# too) +logger.defaultLevel=##_logger_level## + +# Minimum log level for a record to be displayed on the screen +# This check occurs after other filters occur, and only affects what is +# show on console, and only does anything if displayOnScreen=true +logger.minimumOnScreenLevel=DEBUG + +# Records: +# Override the defaultLevel for all classes under the given package +# or class +logger.record.net.i2p=##_logger_level2## +##_logger_notverbose## + diff --git a/installer/java/src/reseed.bat.template b/installer/java/src/reseed.bat.template new file mode 100644 index 0000000000..11b03252eb --- /dev/null +++ b/installer/java/src/reseed.bat.template @@ -0,0 +1,3 @@ +cd ##_scripts_installdir## +java -jar lib\fetchseeds.jar netDb +pause \ No newline at end of file diff --git a/installer/java/src/reseed.sh.template b/installer/java/src/reseed.sh.template new file mode 100644 index 0000000000..ee2aa8a726 --- /dev/null +++ b/installer/java/src/reseed.sh.template @@ -0,0 +1,4 @@ +#!/bin/sh +cd ##_scripts_installdir## +java -jar lib/fetchseeds.jar netDb +echo Router network database reseeded \ No newline at end of file diff --git a/installer/java/src/router.config.template b/installer/java/src/router.config.template new file mode 100644 index 0000000000..351393f890 --- /dev/null +++ b/installer/java/src/router.config.template @@ -0,0 +1,152 @@ +# I2P router configuration +# Created on ##NOW## + +# TCP configuration, for inbound TCP/IP connections +##_router_hn## +##_router_port## +##_router_lavalid## +# unless you really really know what you're doing, keep listenAddressIsValid=false +##_router_tcpdisable## + +# maximum number of TCP connections we will want to +# attempt to establish at once (each of which +# requires a 2048bit DH exchange) +i2np.tcp.concurrentEstablishers=5 + +# Polling HTTP configuration, which is used to keep your router's clock in sync +# [also for communication when no inbound connections are possible, once its fixed up again] +##_router_phttpreg## +##_router_phttpsend## + +# The following option specifies whether the router wants to keep the router's internal time in sync +# with the PHTTP relay's clock (which should be NTP synced). If however you are sure your local machine +# always has the correct time, you can set this to false (but your clock MUST be synced - see +# http://wiki.invisiblenet.net/iip-wiki?I2PTiming for more info. +i2np.phttp.trustRelayTime=true + +# I2CP client port, for client connections +i2cp.port=##_router_i2cp_port## + +# I2P router administrative web port (currently only responds to /routerConsole.html) +router.adminPort=7655 +# Bandwidth limits +# These limits are for all i2np connections - tcp or whatever +# They are hard enforced with no smoothing. +# XXX Until the 0.3.2 release, these should NOT BE USED. Their values will be ignored!!! +i2np.bandwidth.inboundBytesPerMinute=##_router_inbps## +i2np.bandwidth.outboundBytesPerMinute=##_router_outbps## + +# Publish peer rankings +# If true, include the current liveliness and reliability rankings in one's published RouterInfo data +# Setting this to true will help debug the network and is especially useful while we'return still testing +# However, traffic analysis may be easier with this data published (though there's no reason to think people +# can't just fake the info in this). +# Since we're still very much < 1.0, this will be true for the current release by default. As we get some +# network helth information and tune the ranking algorithms, this will become false by default. +# You, of course, can change this to either true or false whenever you'd like. This is only read +# on router startup though, so you need to restart the router if you change it. +router.publishPeerRankings=true + +# Keep message history +# This series of options can help out in debugging the network by keeping a +# seperate log of all messages sent over the network (but without any personally identifiable information) +# This is entirely optional, but would be greatly appreciated during the +# development phase of the network since it would allow the developers to detect +# errors much more easily +router.keepHistory=false + +# Submit message history +# This option works only if router.keepHistory is true and periodically sends +# in the router history logs to the developers (specifically, it submits the file +# via HTTP POST to http://i2p.net/cgi-bin/submitMessageHistory - you can see a sample of what +# one of those files looks like at http://i2p.net/~jrandom/sampleHist.txt) +# After submitting this file, it deletes the local copy (otherwise the file will grow +# without bound - tens of MB per day) +# Again, this is entirely optional, but would be greatly appreciated as it should help +# out the development process +router.submitHistory=false + +# If your router is really slow, you'll need to update the following job parameters + +# limit the maximum number of concurrent operations +router.maxJobRunners=1 + +# if a job waits more than this amount of time (in +# milliseconds) before running, spit out a warning +router.jobLagWarning=8000 + +# if a job waits more than this amount of time (in +# milliseconds) before running, kill the router +router.jobLagFatal=30000 + +# if a job takes more than this amount of time (in +# milliseconds) to run, spit out a warning +router.jobRunWarning=8000 + +# if a job takes more than this amount of time (in +# milliseconds) to run, kill the router +router.jobRunFatal=30000 + +# wait until the router has been up for this long +# (in milliseconds) before honoring any fatalities +# since during startup, jobs are run sequentially +# and CPU intensive tasks are needed +router.jobWarmupTime=600000 + +# Target clients +# How many concurrent clients the router should prepare for +# This, factored in with the tunnel settings, determines the size of the pools - +# too many, and your machine consumes excessive CPU and bandwidth, too few and your +# clients take too long to startup. +# e.g. If you are running an eepsite, an eepProxy, an irc proxy, and a squid proxy, set this to 4 +router.targetClients=2 + +# Number of inbound tunnels per client +# This determines how many inbound tunnels will be allocated per client at a time. +# This is a key factor in the reliability of a client receiving messages +# As above, too many and your machine gets hosed, too few and the pool is slow. +# 2 should be sufficient - prior to 0.2.5, we have all had only 1 +tunnels.numInbound=2 + +# Number of outbound tunnels per client +# This determines how many outbound tunnels must exist when a client is in operation. +# XXX Not currently enforced - ignore this setting +tunnels.numOutbound=2 + + +# Depth of inbound tunnels +# This determines the length of inbound tunnels created - how many remote routers to +# include (0 means no remote routers, 3 means a total of four routers, including +# the local one, etc). This is a key factor in the reliability and anonymity +# provided by I2P +# Users should simply leave this as 2 for now, at least until the tunnels are more reliable (post 0.3) +tunnels.depthInbound=2 + +# Depth of outbound tunnels +# This determines the length of outbound tunnels created - how many remote routers to +# include (0 means no remote routers, 3 means a total of four routers, including +# the local one, etc). This is a key factor in the reliability and anonymity +# provided by I2P +# Users should simply leave this as 2 for now, at least until the tunnels are more reliable (post 0.3) +tunnels.depthOutbound=2 + +# Tunnel duration +# This determines how long tunnels we create should last for (in milliseconds). Too +# long and they are more prone to failure, too short and people need to do more network +# database lookups. The default of 10 minutes (600000 ms) should be used +# You should not change this setting unless you really know what you're doing +tunnels.tunnelDuration=600000 + +# Max waiting jobs +# If your router is getting heavily overloaded (due to slow CPU or excess network +# activity), your router's performance will seriously degrade, increasing its +# load further and delaying any messages sent through your router. The max waiting +# jobs configuration parameter is a throttle, saying that if there are more than +# that many 'jobs' that want to run ASAP at any given time, additional jobs may +# be summarily dropped. That will reduce your load and cause others to reduce +# their dependence on you (further reducing your load). The default value of 20 +# should be sufficient, but may be increased if desired. Less than 20 is not +# recommended, as certain normal events can queue up 10 or so jobs at a time +# (all of which only take a few milliseconds). Leave this alone unless you know +# what you're doing +router.maxWaitingJobs=20 diff --git a/installer/java/src/startFoo.bat.template b/installer/java/src/startFoo.bat.template new file mode 100644 index 0000000000..54a2f4fc9f --- /dev/null +++ b/installer/java/src/startFoo.bat.template @@ -0,0 +1,5 @@ +@echo off +title ##_scripts_winttl## +cd ##_scripts_installdir## +echo ##_scripts_message## +java -DloggerFilenameOverride=logs\##_scripts_logname## -Djava.library.path=. -Dcrypto.dh.precalc.min=0 -cp lib\mstreaming.jar;lib\i2p.jar -jar lib\i2ptunnel.jar -nocli -e "config localhost ##_scripts_port##" -e "##_scripts_cmd##" diff --git a/installer/java/src/startFoo.sh.template b/installer/java/src/startFoo.sh.template new file mode 100644 index 0000000000..33d812a7a6 --- /dev/null +++ b/installer/java/src/startFoo.sh.template @@ -0,0 +1,6 @@ +#!/bin/sh +cd ##_scripts_installdir## +export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH +nohup nice java -DloggerFilenameOverride=logs/##_scripts_logname## -Djava.library.path=. -Dcrypto.dh.precalc.min=0 -cp lib/mstreaming.jar:lib/i2p.jar -jar lib/i2ptunnel.jar -nocli -e "config localhost ##_scripts_port##" -e "##_scripts_cmd##" > /dev/null & + +echo '##_scripts_message##' diff --git a/installer/java/src/startRouter.bat.template b/installer/java/src/startRouter.bat.template new file mode 100644 index 0000000000..155184490a --- /dev/null +++ b/installer/java/src/startRouter.bat.template @@ -0,0 +1,9 @@ +@echo off +title I2P Router +cd ##_scripts_installdir## + +REM the -XX args are workarounds for bugs in java 1.4.2's garbage collector +REM replace java with javaw if you don't want a window to pop up + +java -cp lib\i2p.jar;lib\router.jar -Djava.library.path=. -DloggerFilenameOverride=logs\log-router-#.txt -XX:NewSize=4M -XX:MaxNewSize=8M -XX:PermSize=8M -XX:MaxPermSize=32M net.i2p.router.Router +pause diff --git a/installer/java/src/startRouter.sh.template b/installer/java/src/startRouter.sh.template new file mode 100644 index 0000000000..04113678b7 --- /dev/null +++ b/installer/java/src/startRouter.sh.template @@ -0,0 +1,8 @@ +#!/bin/sh +cd ##_scripts_installdir## +export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH +# the -XX args are workarounds for bugs in java 1.4.2's garbage collector +nohup nice java -cp lib/i2p.jar:lib/router.jar -Djava.library.path=. -DloggerFilenameOverride=logs/log-router-#.txt -XX:NewSize=4M -XX:MaxNewSize=8M -XX:PermSize=8M -XX:MaxPermSize=32M net.i2p.router.Router --quiet > /dev/null & +# Save the pid just in case we ever want to stop the router +echo $! > router.pid +echo I2P Router started diff --git a/installer/java/src/stopRouter.sh.template b/installer/java/src/stopRouter.sh.template new file mode 100644 index 0000000000..0259fc2096 --- /dev/null +++ b/installer/java/src/stopRouter.sh.template @@ -0,0 +1,3 @@ +#!/bin/sh +cd ##_scripts_installdir## +kill `cat router.pid` \ No newline at end of file diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000000..387ca9061d --- /dev/null +++ b/readme.txt @@ -0,0 +1,88 @@ +$Id: readme.txt,v 1.5 2004/03/21 20:03:54 jrandom Exp $ +I2P Router 0.3 + +You have installed a development release of I2P - a variable latency secure +and anonymous communication network. I2P is NOT a filesharing network, or an +email network, or a publishing network - it is simply an anonymous communication +layer - kind of an anonymous IP. This installation includes an application +called "I2PTunnel", which allows normal TCP/IP applications to run over I2P, +offering functionality such as access to anonymous irc servers, anonymous +websites ("eepsites"), etc. + +Since this is a *development* release, I2P should not be depended upon for +anonymity or security. The network is very small, and there are certainly bugs +and suboptimal features. Participating in the network at this time should be +for exploration and to evaluate I2P's functionality and suitability for your +later needs. Once I2P 1.0 is out, wider adoption may be appropriate, but until +that time, we do want to keep the I2P community small, since you are all part of +the development team. + +=== How to get started + +Like a TCP/IP stack, installing the I2P "router" itself doesn't really do much. +You can fire it up with the script startRouter.sh (or startRouter.bat on +windows), and its management console can be seen via http://localhost:7655/ + +Once your router has started up, it may take a few minutes to get integrated +with the network (you'll see a few TCP connections listed on the management +console). At that point, you can start up any of the various proxies: +* startEepProxy: starts an HTTP proxy to access eepsites. Set your browser's + HTTP proxy to localhost:4444 and you can browse various anonymously hosted + sites, ala http://duck.i2p/. In addition, the default proxy is set up to + tunnel any HTTP requests that don't point at an eepsite (e.g. http://i2p.net/) + through I2P to an outbound squid proxy - with this, you can browse the web + anonymously. +* startIrcProxy: starts an anonymous tunnel to an anonymously hosted IRC server + (irc.duck.i2p) - use your favorite irc client and connect to localhost:6668 + and join us on #i2p + +=== Problems accessing eepsites or servers + +I2P is not a distributed data store (ala freenet / mnet / etc) - sites are only +reachable when the host serving that data is up (and their router is running). +If you persistently can't reach the irc server, the squid proxy, or some common +eepsites, check your management console (http://localhost:7655/) and make sure +you have TCP connections. If you don't have any, make sure your firewall / NAT +allows inbound access to your I2P port (which you specified during +installation). If thats fine, but you only see one routerInfo-*.dat file +underneath your netDb/ directory, run the reseed script to pull some new peer +references (it contacts http://i2p.net/i2pdb/ and downloads those files. +alternately, you can get routerInfo files from any of your friends, etc) + +If you still have problems, get in touch with the I2P team (contact info below) + +=== Resources / contact info + +I2P is currently revamping our website, so the two main resources are +http://i2p.net/ and http://wiki.invisiblenet.net/iip-wiki?I2P (our website's +work-in-progress location is http://drupal.i2p.net/ for those who want to see +whats coming down the pipe). The development and user community hangs out on +a few different linked irc chats - IIP's #i2p, freenode.net's #i2p, and the +in-I2P irc network's #i2p (both irc.baffled.i2p and irc.duck.i2p). All of those +channels are hooked together, so join whichever one meets your needs. + +There is also a relatively low traffic mailing list: +http://i2p.net/mailman/listinfo/i2p with archives at +http://i2p.net/pipermail/i2p/ + +The source can be retrieved from http://i2p.net/i2p/ as well as the latest +binary distributions. + +You can pull the latest code via cvs: +cvs -d :pserver:anoncvs@i2p.net:/cvsroot login +cvs -d :pserver:anoncvs@i2p.net:/cvsroot co i2p +The password is "anoncvs". + +=== Acknowledgements + +We are a small group of volunteers spread around several continents, working to +advance different aspects of the project and discussing the design of the +network. For a current list of team members, please see +http://drupal.i2p.net/team + +=== Licenses + +All code included here is released under an open source license. To review +the I2P license policy, please see http://drupal.i2p.net/node/view/66. +If there is any confusion, please see the source code or contact the +developers on the i2p list. diff --git a/router/doc/readme.license.txt b/router/doc/readme.license.txt new file mode 100644 index 0000000000..ac218597b8 --- /dev/null +++ b/router/doc/readme.license.txt @@ -0,0 +1,9 @@ +$Id$ + +the i2p/router/ module is the root of the I2P +router, and everything within it is released +according to the terms of the I2P license policy. +That means everything contained within the +i2p/router module is released into the public +domain unless otherwise marked. Alternate licenses +that may be used include BSD, Cryptix, and MIT. diff --git a/router/java/build.xml b/router/java/build.xml new file mode 100644 index 0000000000..70f97c67c0 --- /dev/null +++ b/router/java/build.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/router/java/src/net/i2p/data/i2np/DataMessage.java b/router/java/src/net/i2p/data/i2np/DataMessage.java new file mode 100644 index 0000000000..c7d19c8691 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/DataMessage.java @@ -0,0 +1,84 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines a message containing arbitrary bytes of data + * + * @author jrandom + */ +public class DataMessage extends I2NPMessageImpl { + private final static Log _log = new Log(DataMessage.class); + public final static int MESSAGE_TYPE = 20; + private byte _data[]; + + public DataMessage() { + _data = null; + } + + public byte[] getData() { return _data; } + public void setData(byte data[]) { _data = data; } + + public int getSize() { return _data.length; } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + int size = (int)DataHelper.readLong(in, 4); + _data = new byte[size]; + int read = read(in, _data); + if (read != size) + throw new DataFormatException("Not enough bytes to read (read = " + read + ", expected = " + size + ")"); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream((_data != null ? _data.length + 4 : 4)); + try { + DataHelper.writeLong(os, 4, (_data != null ? _data.length : 0)); + os.write(_data); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return DataHelper.hashCode(getData()); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof DataMessage) ) { + DataMessage msg = (DataMessage)object; + return DataHelper.eq(getData(),msg.getData()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DataMessage: "); + buf.append("\n\tData: ").append(DataHelper.toString(getData(), 64)); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/DatabaseFindNearestMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseFindNearestMessage.java new file mode 100644 index 0000000000..f3078734b7 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/DatabaseFindNearestMessage.java @@ -0,0 +1,99 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.util.Log; + +/** + * Defines the message a router sends to another router to help integrate into + * the network by searching for routers in a particular keyspace. + * + * @author jrandom + */ +public class DatabaseFindNearestMessage extends I2NPMessageImpl { + private final static Log _log = new Log(DatabaseFindNearestMessage.class); + public final static int MESSAGE_TYPE = 4; + private Hash _key; + private Hash _from; + + public DatabaseFindNearestMessage() { + setSearchKey(null); + setFromHash(null); + } + + /** + * Defines the key being searched for + */ + public Hash getSearchKey() { return _key; } + public void setSearchKey(Hash key) { _key = key; } + + /** + * Contains the SHA256 Hash of the RouterIdentity sending the message + */ + public Hash getFromHash() { return _from; } + public void setFromHash(Hash from) { _from = from; } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + _key = new Hash(); + _key.readBytes(in); + _from = new Hash(); + _from.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + if ( (_key == null) || (_from == null) ) throw new I2NPMessageException("Not enough data to write out"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(32); + try { + _key.writeBytes(os); + _from.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return DataHelper.hashCode(getSearchKey()) + + DataHelper.hashCode(getFromHash()); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof DatabaseFindNearestMessage) ) { + DatabaseFindNearestMessage msg = (DatabaseFindNearestMessage)object; + return DataHelper.eq(getSearchKey(),msg.getSearchKey()) && + DataHelper.eq(getFromHash(),msg.getFromHash()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DatabaseFindNearestMessage: "); + buf.append("\n\tSearch Key: ").append(getSearchKey()); + buf.append("\n\tFrom: ").append(getFromHash()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java new file mode 100644 index 0000000000..8ee73bbbae --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java @@ -0,0 +1,165 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; +import net.i2p.data.TunnelId; +import net.i2p.util.Log; + +/** + * Defines the message a router sends to another router to search for a + * key in the network database. + * + * @author jrandom + */ +public class DatabaseLookupMessage extends I2NPMessageImpl { + private final static Log _log = new Log(DatabaseLookupMessage.class); + public final static int MESSAGE_TYPE = 2; + private Hash _key; + private RouterInfo _from; + private TunnelId _replyTunnel; + private Set _dontIncludePeers; + + public DatabaseLookupMessage() { + setSearchKey(null); + setFrom(null); + setDontIncludePeers(null); + } + + /** + * Defines the key being searched for + */ + public Hash getSearchKey() { return _key; } + public void setSearchKey(Hash key) { _key = key; } + + /** + * Contains the current router info of the router who requested this lookup + * + */ + public RouterInfo getFrom() { return _from; } + public void setFrom(RouterInfo from) { _from = from; } + + /** + * Contains the tunnel ID a reply should be sent to + * + */ + public TunnelId getReplyTunnel() { return _replyTunnel; } + public void setReplyTunnel(TunnelId replyTunnel) { _replyTunnel = replyTunnel; } + + /** + * Set of peers that a lookup reply should NOT include + * + * @return Set of Hash objects, each of which is the H(routerIdentity) to skip + */ + public Set getDontIncludePeers() { return _dontIncludePeers; } + public void setDontIncludePeers(Set peers) { + if (peers != null) + _dontIncludePeers = new HashSet(peers); + else + _dontIncludePeers = null; + } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + _key = new Hash(); + _key.readBytes(in); + _from = new RouterInfo(); + _from.readBytes(in); + boolean tunnelSpecified = DataHelper.readBoolean(in).booleanValue(); + if (tunnelSpecified) { + _replyTunnel = new TunnelId(); + _replyTunnel.readBytes(in); + } + int numPeers = (int)DataHelper.readLong(in, 2); + if ( (numPeers < 0) || (numPeers >= (1<<16) ) ) + throw new DataFormatException("Invalid number of peers - " + numPeers); + Set peers = new HashSet(numPeers); + for (int i = 0; i < numPeers; i++) { + Hash peer = new Hash(); + peer.readBytes(in); + peers.add(peer); + } + _dontIncludePeers = peers; + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + if (_key == null) throw new I2NPMessageException("Key being searched for not specified"); + if (_from == null) throw new I2NPMessageException("From address not specified"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(32); + try { + _key.writeBytes(os); + _from.writeBytes(os); + if (_replyTunnel != null) { + DataHelper.writeBoolean(os, Boolean.TRUE); + _replyTunnel.writeBytes(os); + } else { + DataHelper.writeBoolean(os, Boolean.FALSE); + } + if ( (_dontIncludePeers == null) || (_dontIncludePeers.size() <= 0) ) { + DataHelper.writeLong(os, 2, 0); + } else { + DataHelper.writeLong(os, 2, _dontIncludePeers.size()); + for (Iterator iter = _dontIncludePeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + peer.writeBytes(os); + } + } + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return DataHelper.hashCode(getSearchKey()) + + DataHelper.hashCode(getFrom()) + + DataHelper.hashCode(getReplyTunnel()) + + DataHelper.hashCode(_dontIncludePeers); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof DatabaseLookupMessage) ) { + DatabaseLookupMessage msg = (DatabaseLookupMessage)object; + return DataHelper.eq(getSearchKey(),msg.getSearchKey()) && + DataHelper.eq(getFrom(),msg.getFrom()) && + DataHelper.eq(getReplyTunnel(),msg.getReplyTunnel()) && + DataHelper.eq(_dontIncludePeers,msg.getDontIncludePeers()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DatabaseLookupMessage: "); + buf.append("\n\tSearch Key: ").append(getSearchKey()); + buf.append("\n\tFrom: ").append(getFrom()); + buf.append("\n\tReply Tunnel: ").append(getReplyTunnel()); + buf.append("\n\tDont Include Peers: ").append(getDontIncludePeers()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/DatabaseSearchReplyMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseSearchReplyMessage.java new file mode 100644 index 0000000000..4200cb1fb6 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/DatabaseSearchReplyMessage.java @@ -0,0 +1,149 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; +import net.i2p.util.Log; + +/** + * Defines the message a router sends to another router in response to a + * search (DatabaseFindNearest or DatabaseLookup) when it doesn't have the value, + * specifying what routers it would search. + * + * @author jrandom + */ +public class DatabaseSearchReplyMessage extends I2NPMessageImpl { + private final static Log _log = new Log(DatabaseSearchReplyMessage.class); + public final static int MESSAGE_TYPE = 3; + private Hash _key; + private List _routerInfoStructures; + private Hash _from; + + public DatabaseSearchReplyMessage() { + setSearchKey(null); + _routerInfoStructures = new ArrayList(); + setFromHash(null); + } + + /** + * Defines the key being searched for + */ + public Hash getSearchKey() { return _key; } + public void setSearchKey(Hash key) { _key = key; } + + public int getNumReplies() { return _routerInfoStructures.size(); } + public RouterInfo getReply(int index) { return (RouterInfo)_routerInfoStructures.get(index); } + public void addReply(RouterInfo info) { _routerInfoStructures.add(info); } + public void addReplies(Collection replies) { _routerInfoStructures.addAll(replies); } + + public Hash getFromHash() { return _from; } + public void setFromHash(Hash from) { _from = from; } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + _key = new Hash(); + _key.readBytes(in); + + int compressedLength = (int)DataHelper.readLong(in, 2); + byte compressedData[] = new byte[compressedLength]; + int read = DataHelper.read(in, compressedData); + if (read != compressedLength) + throw new IOException("Not enough data to decompress"); + byte decompressedData[] = DataHelper.decompress(compressedData); + ByteArrayInputStream bais = new ByteArrayInputStream(decompressedData); + int num = (int)DataHelper.readLong(bais, 1); + _routerInfoStructures.clear(); + for (int i = 0; i < num; i++) { + RouterInfo info = new RouterInfo(); + info.readBytes(bais); + addReply(info); + } + + _from = new Hash(); + _from.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + if (_key == null) + throw new I2NPMessageException("Key in reply to not specified"); + if (_routerInfoStructures == null) + throw new I2NPMessageException("RouterInfo replies are null"); + if (_routerInfoStructures.size() <= 0) + throw new I2NPMessageException("No replies specified in SearchReply! Always include oneself!"); + if (_from == null) + throw new I2NPMessageException("No 'from' address specified!"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(32); + try { + _key.writeBytes(os); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataHelper.writeLong(baos, 1, _routerInfoStructures.size()); + for (int i = 0; i < getNumReplies(); i++) { + RouterInfo info = getReply(i); + info.writeBytes(baos); + } + + byte compressed[] = DataHelper.compress(baos.toByteArray()); + DataHelper.writeLong(os, 2, compressed.length); + os.write(compressed); + _from.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof DatabaseSearchReplyMessage) ) { + DatabaseSearchReplyMessage msg = (DatabaseSearchReplyMessage)object; + return DataHelper.eq(getSearchKey(),msg.getSearchKey()) && + DataHelper.eq(getFromHash(),msg.getFromHash()) && + DataHelper.eq(_routerInfoStructures,msg._routerInfoStructures); + } else { + return false; + } + } + + public int hashCode() { + return DataHelper.hashCode(getSearchKey()) + + DataHelper.hashCode(getFromHash()) + + DataHelper.hashCode(_routerInfoStructures); + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DatabaseSearchReplyMessage: "); + buf.append("\n\tSearch Key: ").append(getSearchKey()); + buf.append("\n\tReplies: # = ").append(getNumReplies()); + for (int i = 0; i < getNumReplies(); i++) { + buf.append("\n\t\tReply [").append(i).append("]: ").append(getReply(i)); + } + buf.append("\n\tFrom: ").append(getFromHash()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java new file mode 100644 index 0000000000..0221e2f4a0 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java @@ -0,0 +1,170 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.RouterInfo; +import net.i2p.util.Log; + +/** + * Defines the message a router sends to another router to test the network + * database reachability, as well as the reply message sent back. + * + * @author jrandom + */ +public class DatabaseStoreMessage extends I2NPMessageImpl { + private final static Log _log = new Log(DatabaseStoreMessage.class); + public final static int MESSAGE_TYPE = 1; + private Hash _key; + private int _type; + private LeaseSet _leaseSet; + private RouterInfo _info; + + public final static int KEY_TYPE_ROUTERINFO = 0; + public final static int KEY_TYPE_LEASESET = 1; + + public DatabaseStoreMessage() { + setValueType(-1); + setKey(null); + setLeaseSet(null); + setRouterInfo(null); + } + + /** + * Defines the key in the network database being stored + * + */ + public Hash getKey() { return _key; } + public void setKey(Hash key) { _key = key; } + + /** + * Defines the router info value in the network database being stored + * + */ + public RouterInfo getRouterInfo() { return _info; } + public void setRouterInfo(RouterInfo routerInfo) { + _info = routerInfo; + if (_info != null) + setValueType(KEY_TYPE_ROUTERINFO); + } + + /** + * Defines the lease set value in the network database being stored + * + */ + public LeaseSet getLeaseSet() { return _leaseSet; } + public void setLeaseSet(LeaseSet leaseSet) { + _leaseSet = leaseSet; + if (_leaseSet != null) + setValueType(KEY_TYPE_LEASESET); + } + + /** + * Defines type of key being stored in the network database - + * either KEY_TYPE_ROUTERINFO or KEY_TYPE_LEASESET + * + */ + public int getValueType() { return _type; } + public void setValueType(int type) { _type = type; } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + _key = new Hash(); + _key.readBytes(in); + _log.debug("Hash read: " + _key.toBase64()); + _type = (int)DataHelper.readLong(in, 1); + if (_type == KEY_TYPE_LEASESET) { + _leaseSet = new LeaseSet(); + _leaseSet.readBytes(in); + } else if (_type == KEY_TYPE_ROUTERINFO) { + _info = new RouterInfo(); + int compressedSize = (int)DataHelper.readLong(in, 2); + byte compressed[] = new byte[compressedSize]; + int read = DataHelper.read(in, compressed); + if (read != compressedSize) + throw new I2NPMessageException("Invalid compressed data size"); + ByteArrayInputStream bais = new ByteArrayInputStream(DataHelper.decompress(compressed)); + _info.readBytes(bais); + } else { + throw new I2NPMessageException("Invalid type of key read from the structure - " + _type); + } + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + if (_key == null) throw new I2NPMessageException("Invalid key"); + if ( (_type != KEY_TYPE_LEASESET) && (_type != KEY_TYPE_ROUTERINFO) ) throw new I2NPMessageException("Invalid key type"); + if ( (_type == KEY_TYPE_LEASESET) && (_leaseSet == null) ) throw new I2NPMessageException("Missing lease set"); + if ( (_type == KEY_TYPE_ROUTERINFO) && (_info == null) ) throw new I2NPMessageException("Missing router info"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(256); + try { + _key.writeBytes(os); + DataHelper.writeLong(os, 1, _type); + if (_type == KEY_TYPE_LEASESET) { + _leaseSet.writeBytes(os); + } else if (_type == KEY_TYPE_ROUTERINFO) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4*1024); + _info.writeBytes(baos); + byte uncompressed[] = baos.toByteArray(); + byte compressed[] = DataHelper.compress(uncompressed); + DataHelper.writeLong(os, 2, compressed.length); + os.write(compressed); + } + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return DataHelper.hashCode(getKey()) + + DataHelper.hashCode(getLeaseSet()) + + DataHelper.hashCode(getRouterInfo()) + + getValueType(); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof DatabaseStoreMessage) ) { + DatabaseStoreMessage msg = (DatabaseStoreMessage)object; + return DataHelper.eq(getKey(),msg.getKey()) && + DataHelper.eq(getLeaseSet(),msg.getLeaseSet()) && + DataHelper.eq(getRouterInfo(),msg.getRouterInfo()) && + DataHelper.eq(getValueType(),msg.getValueType()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DatabaseStoreMessage: "); + buf.append("\n\tExpiration: ").append(getMessageExpiration()); + buf.append("\n\tUnique ID: ").append(getUniqueId()); + buf.append("\n\tKey: ").append(getKey()); + buf.append("\n\tValue Type: ").append(getValueType()); + buf.append("\n\tRouter Info: ").append(getRouterInfo()); + buf.append("\n\tLease Set: ").append(getLeaseSet()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/DeliveryInstructions.java b/router/java/src/net/i2p/data/i2np/DeliveryInstructions.java new file mode 100644 index 0000000000..58c33e9884 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/DeliveryInstructions.java @@ -0,0 +1,274 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.data.TunnelId; + + +/** + * Contains the delivery instructions + * + * @author jrandom + */ +public class DeliveryInstructions extends DataStructureImpl { + private final static Log _log = new Log(DeliveryInstructions.class); + private boolean _encrypted; + private SessionKey _encryptionKey; + private int _deliveryMode; + public final static int DELIVERY_MODE_LOCAL = 0; + public final static int DELIVERY_MODE_DESTINATION = 1; + public final static int DELIVERY_MODE_ROUTER = 2; + public final static int DELIVERY_MODE_TUNNEL = 3; + private Hash _destinationHash; + private Hash _routerHash; + private TunnelId _tunnelId; + private boolean _delayRequested; + private long _delaySeconds; + + private final static int FLAG_MODE_LOCAL = 0; + private final static int FLAG_MODE_DESTINATION = 1; + private final static int FLAG_MODE_ROUTER = 2; + private final static int FLAG_MODE_TUNNEL = 3; + + private final static long FLAG_ENCRYPTED = 128; + private final static long FLAG_MODE = 96; + private final static long FLAG_DELAY = 16; + + public DeliveryInstructions() { + setEncrypted(false); + setEncryptionKey(null); + setDeliveryMode(-1); + setDestination(null); + setRouter(null); + setTunnelId(null); + setDelayRequested(false); + setDelaySeconds(0); + } + + public boolean getEncrypted() { return _encrypted; } + public void setEncrypted(boolean encrypted) { _encrypted = encrypted; } + public SessionKey getEncryptionKey() { return _encryptionKey; } + public void setEncryptionKey(SessionKey key) { _encryptionKey = key; } + public int getDeliveryMode() { return _deliveryMode; } + public void setDeliveryMode(int mode) { _deliveryMode = mode; } + public Hash getDestination() { return _destinationHash; } + public void setDestination(Hash dest) { _destinationHash = dest; } + public Hash getRouter() { return _routerHash; } + public void setRouter(Hash router) { _routerHash = router; } + public TunnelId getTunnelId() { return _tunnelId; } + public void setTunnelId(TunnelId id) { _tunnelId = id; } + public boolean getDelayRequested() { return _delayRequested; } + public void setDelayRequested(boolean req) { _delayRequested = req; } + public long getDelaySeconds() { return _delaySeconds; } + public void setDelaySeconds(long seconds) { _delaySeconds = seconds; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + long flags = DataHelper.readLong(in, 1); + _log.debug("Read flags: " + flags + " mode: " + flagMode(flags)); + + if (flagEncrypted(flags)) { + SessionKey k = new SessionKey(); + k.readBytes(in); + setEncryptionKey(k); + setEncrypted(true); + } else { + setEncrypted(false); + } + + setDeliveryMode(flagMode(flags)); + switch (flagMode(flags)) { + case FLAG_MODE_LOCAL: + break; + case FLAG_MODE_DESTINATION: + Hash destHash = new Hash(); + destHash.readBytes(in); + setDestination(destHash); + break; + case FLAG_MODE_ROUTER: + Hash routerHash = new Hash(); + routerHash.readBytes(in); + setRouter(routerHash); + break; + case FLAG_MODE_TUNNEL: + Hash tunnelRouterHash = new Hash(); + tunnelRouterHash.readBytes(in); + setRouter(tunnelRouterHash); + TunnelId id = new TunnelId(); + id.readBytes(in); + setTunnelId(id); + break; + } + + if (flagDelay(flags)) { + long delay = DataHelper.readLong(in, 4); + setDelayRequested(true); + setDelaySeconds(delay); + } else { + setDelayRequested(false); + } + } + + private boolean flagEncrypted(long flags) { + return (0 != (flags & FLAG_ENCRYPTED)); + } + + private int flagMode(long flags) { + long v = flags & FLAG_MODE; + v >>>= 5; + return (int)v; + } + + private boolean flagDelay(long flags) { + return (0 != (flags & FLAG_DELAY)); + } + + private long getFlags() { + long val = 0L; + if (getEncrypted()) + val = val | FLAG_ENCRYPTED; + long fmode = 0; + switch (getDeliveryMode()) { + case FLAG_MODE_LOCAL: + break; + case FLAG_MODE_DESTINATION: + fmode = FLAG_MODE_DESTINATION << 5; + break; + case FLAG_MODE_ROUTER: + fmode = FLAG_MODE_ROUTER << 5; + break; + case FLAG_MODE_TUNNEL: + fmode = FLAG_MODE_TUNNEL << 5; + break; + } + val = val | fmode; + if (getDelayRequested()) + val = val | FLAG_DELAY; + _log.debug("getFlags() = " + val); + return val; + } + + private byte[] getAdditionalInfo() throws DataFormatException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(64); + try { + if (getEncrypted()) { + if (_encryptionKey == null) throw new DataFormatException("Encryption key is not set"); + _encryptionKey.writeBytes(baos); + _log.debug("IsEncrypted"); + } else { + _log.debug("Is NOT Encrypted"); + } + switch (getDeliveryMode()) { + case FLAG_MODE_LOCAL: + _log.debug("mode = local"); + break; + case FLAG_MODE_DESTINATION: + if (_destinationHash == null) throw new DataFormatException("Destination hash is not set"); + _destinationHash.writeBytes(baos); + _log.debug("mode = destination, hash = " + _destinationHash); + break; + case FLAG_MODE_ROUTER: + if (_routerHash == null) throw new DataFormatException("Router hash is not set"); + _routerHash.writeBytes(baos); + _log.debug("mode = router, routerHash = " + _routerHash); + break; + case FLAG_MODE_TUNNEL: + if ( (_routerHash == null) || (_tunnelId == null) ) throw new DataFormatException("Router hash or tunnel ID is not set"); + _routerHash.writeBytes(baos); + _tunnelId.writeBytes(baos); + _log.debug("mode = tunnel, tunnelId = " + _tunnelId.getTunnelId() + ", routerHash = " + _routerHash); + break; + } + if (getDelayRequested()) { + _log.debug("delay requested: " + getDelaySeconds()); + DataHelper.writeLong(baos, 4, getDelaySeconds()); + } else { + _log.debug("delay NOT requested"); + } + } catch (IOException ioe) { + throw new DataFormatException("Unable to write out additional info", ioe); + } + return baos.toByteArray(); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if ( (_deliveryMode < 0) || (_deliveryMode > FLAG_MODE_TUNNEL) ) throw new DataFormatException("Invalid data: mode = " + _deliveryMode); + long flags = getFlags(); + _log.debug("Write flags: " + flags + " mode: " + getDeliveryMode() + " =?= " + flagMode(flags)); + byte additionalInfo[] = getAdditionalInfo(); + DataHelper.writeLong(out, 1, flags); + if (additionalInfo != null) { + out.write(additionalInfo); + out.flush(); + } + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof DeliveryInstructions)) + return false; + DeliveryInstructions instr = (DeliveryInstructions)obj; + return (getDelayRequested() == instr.getDelayRequested()) && + (getDelaySeconds() == instr.getDelaySeconds()) && + (getDeliveryMode() == instr.getDeliveryMode()) && + (getEncrypted() == instr.getEncrypted()) && + DataHelper.eq(getDestination(), instr.getDestination()) && + DataHelper.eq(getEncryptionKey(), instr.getEncryptionKey()) && + DataHelper.eq(getRouter(), instr.getRouter()) && + DataHelper.eq(getTunnelId(), instr.getTunnelId()); + } + + public int hashCode() { + return (int)getDelaySeconds() + + getDeliveryMode() + + DataHelper.hashCode(getDestination()) + + DataHelper.hashCode(getEncryptionKey()) + + DataHelper.hashCode(getRouter()) + + DataHelper.hashCode(getTunnelId()); + } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append("[DeliveryInstructions: "); + buf.append("\n\tDelivery mode: "); + switch (getDeliveryMode()) { + case DELIVERY_MODE_LOCAL: + buf.append("local"); + break; + case DELIVERY_MODE_DESTINATION: + buf.append("destination"); + break; + case DELIVERY_MODE_ROUTER: + buf.append("router"); + break; + case DELIVERY_MODE_TUNNEL: + buf.append("tunnel"); + break; + } + buf.append("\n\tDelay requested: ").append(getDelayRequested()); + buf.append("\n\tDelay seconds: ").append(getDelaySeconds()); + buf.append("\n\tDestination: ").append(getDestination()); + buf.append("\n\tEncrypted: ").append(getEncrypted()); + buf.append("\n\tEncryption key: ").append(getEncryptionKey()); + buf.append("\n\tRouter: ").append(getRouter()); + buf.append("\n\tTunnelId: ").append(getTunnelId()); + + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/DeliveryStatusMessage.java b/router/java/src/net/i2p/data/i2np/DeliveryStatusMessage.java new file mode 100644 index 0000000000..18c7d564ee --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/DeliveryStatusMessage.java @@ -0,0 +1,91 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message sent back in reply to a message when requested, containing + * the private ack id. + * + * @author jrandom + */ +public class DeliveryStatusMessage extends I2NPMessageImpl { + private final static Log _log = new Log(DeliveryStatusMessage.class); + public final static int MESSAGE_TYPE = 10; + private long _id; + private Date _arrival; + + public DeliveryStatusMessage() { + setMessageId(-1); + setArrival(null); + } + + public long getMessageId() { return _id; } + public void setMessageId(long id) { _id = id; } + + public Date getArrival() { return _arrival; } + public void setArrival(Date arrival) { _arrival = arrival; } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + _id = DataHelper.readLong(in, 4); + _arrival = DataHelper.readDate(in); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + if ( (_id < 0) || (_arrival == null) ) throw new I2NPMessageException("Not enough data to write out"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(32); + try { + DataHelper.writeLong(os, 4, _id); + DataHelper.writeDate(os, _arrival); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return (int)getMessageId() + + DataHelper.hashCode(getArrival()); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof DeliveryStatusMessage) ) { + DeliveryStatusMessage msg = (DeliveryStatusMessage)object; + return DataHelper.eq(getMessageId(),msg.getMessageId()) && + DataHelper.eq(getArrival(),msg.getArrival()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DeliveryStatusMessage: "); + buf.append("\n\tMessage ID: ").append(getMessageId()); + buf.append("\n\tArrival: ").append(getArrival()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/EndPointPrivateKey.java b/router/java/src/net/i2p/data/i2np/EndPointPrivateKey.java new file mode 100644 index 0000000000..417217518b --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/EndPointPrivateKey.java @@ -0,0 +1,62 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; +import net.i2p.data.PrivateKey; + +/** + * Contains the private key which matches the EndPointPublicKey which, in turn, + * is published on the LeaseSet and used to encrypt messages to the router to + * which a Destination is currently connected. + * + * @author jrandom + */ +public class EndPointPrivateKey extends DataStructureImpl { + private final static Log _log = new Log(EndPointPrivateKey.class); + private PrivateKey _key; + + public EndPointPrivateKey() { setKey(null); } + + public PrivateKey getKey() { return _key; } + public void setKey(PrivateKey key) { _key= key; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _key = new PrivateKey(); + _key.readBytes(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_key == null) throw new DataFormatException("Invalid key"); + _key.writeBytes(out); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof EndPointPublicKey)) + return false; + return DataHelper.eq(getKey(), ((EndPointPublicKey)obj).getKey()); + } + + public int hashCode() { + if (_key == null) return 0; + return getKey().hashCode(); + } + + public String toString() { + return "[EndPointPrivateKey: " + getKey() + "]"; + } +} diff --git a/router/java/src/net/i2p/data/i2np/EndPointPublicKey.java b/router/java/src/net/i2p/data/i2np/EndPointPublicKey.java new file mode 100644 index 0000000000..303e236c49 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/EndPointPublicKey.java @@ -0,0 +1,62 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; +import net.i2p.data.PublicKey; + +/** + * Contains the public key which matches the EndPointPrivateKey. This is + * published on the LeaseSet and used to encrypt messages to the router to + * which a Destination is currently connected. + * + * @author jrandom + */ +public class EndPointPublicKey extends DataStructureImpl { + private final static Log _log = new Log(EndPointPublicKey.class); + private PublicKey _key; + + public EndPointPublicKey() { setKey(null); } + + public PublicKey getKey() { return _key; } + public void setKey(PublicKey key) { _key= key; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _key = new PublicKey(); + _key.readBytes(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_key == null) throw new DataFormatException("Invalid key"); + _key.writeBytes(out); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof EndPointPublicKey)) + return false; + return DataHelper.eq(getKey(), ((EndPointPublicKey)obj).getKey()); + } + + public int hashCode() { + if (_key == null) return 0; + return getKey().hashCode(); + } + + public String toString() { + return "[EndPointPublicKey: " + getKey() + "]"; + } +} diff --git a/router/java/src/net/i2p/data/i2np/GarlicClove.java b/router/java/src/net/i2p/data/i2np/GarlicClove.java new file mode 100644 index 0000000000..4fbc0139c4 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/GarlicClove.java @@ -0,0 +1,171 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; +import net.i2p.data.Certificate; + +import java.util.Date; + +/** + * Contains one deliverable message encrypted to a router along with instructions + * and a certificate 'paying for' the delivery. + * + * @author jrandom + */ +public class GarlicClove extends DataStructureImpl { + private final static Log _log = new Log(GarlicClove.class); + private DeliveryInstructions _instructions; + private I2NPMessage _msg; + private long _cloveId; + private Date _expiration; + private Certificate _certificate; + private int _replyAction; + private SourceRouteBlock _sourceRouteBlock; + + /** No action requested with the source route block */ + public final static int ACTION_NONE = 0; + /** + * A DeliveryStatusMessage is requested with the source route block using + * the cloveId as the id received + * + */ + public final static int ACTION_STATUS = 1; + /** + * No DeliveryStatusMessage is requested, but the source route block is + * included for message specific replies + * + */ + public final static int ACTION_MESSAGE_SPECIFIC = 2; + + public GarlicClove() { + setInstructions(null); + setData(null); + setCloveId(-1); + setExpiration(null); + setCertificate(null); + setSourceRouteBlockAction(ACTION_NONE); + setSourceRouteBlock(null); + } + + public DeliveryInstructions getInstructions() { return _instructions; } + public void setInstructions(DeliveryInstructions instr) { _instructions = instr; } + public I2NPMessage getData() { return _msg; } + public void setData(I2NPMessage msg) { _msg = msg; } + public long getCloveId() { return _cloveId; } + public void setCloveId(long id) { _cloveId = id; } + public Date getExpiration() { return _expiration; } + public void setExpiration(Date exp) { _expiration = exp; } + public Certificate getCertificate() { return _certificate; } + public void setCertificate(Certificate cert) { _certificate = cert; } + public int getSourceRouteBlockAction() { return _replyAction; } + public void setSourceRouteBlockAction(int action) { _replyAction = action; } + public SourceRouteBlock getSourceRouteBlock() { return _sourceRouteBlock; } + public void setSourceRouteBlock(SourceRouteBlock block) { _sourceRouteBlock = block; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _instructions = new DeliveryInstructions(); + _instructions.readBytes(in); + _log.debug("Read instructions: " + _instructions); + try { + _msg = new I2NPMessageHandler().readMessage(in); + } catch (I2NPMessageException ime) { + throw new DataFormatException("Unable to read the message from a garlic clove", ime); + } + _cloveId = DataHelper.readLong(in, 4); + _expiration = DataHelper.readDate(in); + _log.debug("CloveID read: " + _cloveId + " expiration read: " + _expiration); + _certificate = new Certificate(); + _certificate.readBytes(in); + _log.debug("Read cert: " + _certificate); + int replyStyle = (int)DataHelper.readLong(in, 1); + setSourceRouteBlockAction(replyStyle); + if (replyStyle != ACTION_NONE) { + _sourceRouteBlock = new SourceRouteBlock(); + _sourceRouteBlock.readBytes(in); + } + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + StringBuffer error = new StringBuffer(); + if (_instructions == null) + error.append("No instructions "); + if (_msg == null) + error.append("No message "); + if (_cloveId < 0) + error.append("CloveID < 0 [").append(_cloveId).append("] "); + if (_expiration == null) + error.append("Expiration is null "); + if (_certificate == null) + error.append("Certificate is null "); + if (_replyAction < 0) + error.append("Reply action is < 0 [").append(_replyAction).append("] ");; + if (error.length() > 0) + throw new DataFormatException(error.toString()); + if ( (_replyAction != 0) && (_sourceRouteBlock == null) ) + throw new DataFormatException("Source route block must be specified for non-null action"); + _instructions.writeBytes(out); + + _log.debug("Wrote instructions: " + _instructions); + _msg.writeBytes(out); + DataHelper.writeLong(out, 4, _cloveId); + DataHelper.writeDate(out, _expiration); + _log.debug("CloveID written: " + _cloveId + " expiration written: " + _expiration); + _certificate.writeBytes(out); + _log.debug("Written cert: " + _certificate); + DataHelper.writeLong(out, 1, _replyAction); + if ( (_replyAction != 0) && (_sourceRouteBlock != null) ) + _sourceRouteBlock.writeBytes(out); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof GarlicClove)) + return false; + GarlicClove clove = (GarlicClove)obj; + return DataHelper.eq(getCertificate(), clove.getCertificate()) && + DataHelper.eq(getCloveId(), clove.getCloveId()) && + DataHelper.eq(getData(), clove.getData()) && + DataHelper.eq(getExpiration(), clove.getExpiration()) && + DataHelper.eq(getInstructions(), clove.getInstructions()) && + DataHelper.eq(getSourceRouteBlock(), clove.getSourceRouteBlock()) && + (getSourceRouteBlockAction() == clove.getSourceRouteBlockAction()); + } + + public int hashCode() { + return DataHelper.hashCode(getCertificate()) + + (int)getCloveId() + + DataHelper.hashCode(getData()) + + DataHelper.hashCode(getExpiration()) + + DataHelper.hashCode(getInstructions()) + + DataHelper.hashCode(getSourceRouteBlock()) + + getSourceRouteBlockAction(); + } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append("[GarlicClove: "); + buf.append("\n\tInstructions: ").append(getInstructions()); + buf.append("\n\tCertificate: ").append(getCertificate()); + buf.append("\n\tClove ID: ").append(getCloveId()); + buf.append("\n\tExpiration: ").append(getExpiration()); + buf.append("\n\tSource route style: ").append(getSourceRouteBlockAction()); + buf.append("\n\tSource route block: ").append(getSourceRouteBlock()); + buf.append("\n\tData: ").append(getData()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/GarlicMessage.java b/router/java/src/net/i2p/data/i2np/GarlicMessage.java new file mode 100644 index 0000000000..2971abc7bf --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/GarlicMessage.java @@ -0,0 +1,84 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the wrapped garlic message + * + * @author jrandom + */ +public class GarlicMessage extends I2NPMessageImpl { + private final static Log _log = new Log(GarlicMessage.class); + public final static int MESSAGE_TYPE = 11; + private byte[] _data; + + public GarlicMessage() { + setData(null); + } + + public byte[] getData() { return _data; } + public void setData(byte[] data) { _data = data; } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + long len = DataHelper.readLong(in, 4); + _data = new byte[(int)len]; + int read = read(in, _data); + if (read != len) + throw new I2NPMessageException("Incorrect size read"); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + if ( (_data == null) || (_data.length <= 0) ) throw new I2NPMessageException("Not enough data to write out"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(32); + try { + DataHelper.writeLong(os, 4, _data.length); + os.write(_data); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return DataHelper.hashCode(getData()); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof GarlicMessage) ) { + GarlicMessage msg = (GarlicMessage)object; + return DataHelper.eq(getData(),msg.getData()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[GarlicMessage: "); + buf.append("\n\tData length: ").append(getData().length).append(" bytes"); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessage.java b/router/java/src/net/i2p/data/i2np/I2NPMessage.java new file mode 100644 index 0000000000..850960c159 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/I2NPMessage.java @@ -0,0 +1,51 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; + +import net.i2p.data.DataStructure; + +/** + * Base interface for all I2NP messages + * + * @author jrandom + */ +public interface I2NPMessage extends DataStructure { + /** + * Read the body into the data structures, after the initial type byte, using + * the current class's format as defined by the I2NP specification + * + * @param in stream to read from + * @param type I2NP message type + * @throws I2NPMessageException if the stream doesn't contain a valid message + * that this class can read. + * @throws IOException if there is a problem reading from the stream + */ + public void readBytes(InputStream in, int type) throws I2NPMessageException, IOException; + + /** + * Return the unique identifier for this type of I2NP message, as defined in + * the I2NP spec + */ + public int getType(); + + /** + * Replay resistent message Id + */ + public long getUniqueId(); + + /** + * Date after which the message should be dropped (and the associated uniqueId forgotten) + * + */ + public Date getMessageExpiration(); +} diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageException.java b/router/java/src/net/i2p/data/i2np/I2NPMessageException.java new file mode 100644 index 0000000000..5f7708d5dd --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/I2NPMessageException.java @@ -0,0 +1,28 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.I2PException; +import net.i2p.util.Log; + +/** + * Represent an error serializing or deserializing an APIMessage + * + * @author jrandom + */ +public class I2NPMessageException extends I2PException { + private final static Log _log = new Log(I2NPMessageException.class); + + public I2NPMessageException(String message, Throwable parent) { + super(message, parent); + } + public I2NPMessageException(String message) { + super(message); + } +} diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java b/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java new file mode 100644 index 0000000000..79b7743c7d --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java @@ -0,0 +1,92 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.IOException; +import java.io.FileInputStream; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataFormatException; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Handle messages from router to router + * + */ +public class I2NPMessageHandler { + private final static Log _log = new Log(I2NPMessageHandler.class); + private long _lastReadBegin; + private long _lastReadEnd; + public I2NPMessageHandler() {} + + /** + * Read an I2NPMessage from the stream and return the fully populated object. + * + * @throws IOException if there is an IO problem reading from the stream + * @throws I2NPMessageException if there is a problem handling the particular + * message - if it is an unknown type or has improper formatting, etc. + */ + public I2NPMessage readMessage(InputStream in) throws IOException, I2NPMessageException { + try { + int type = (int)DataHelper.readLong(in, 1); + _lastReadBegin = Clock.getInstance().now(); + I2NPMessage msg = createMessage(in, type); + msg.readBytes(in, type); + _lastReadEnd = Clock.getInstance().now(); + return msg; + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error reading the message", dfe); + } + } + + public long getLastReadTime() { return _lastReadEnd - _lastReadBegin; } + + /** + * Yes, this is fairly ugly, but its the only place it ever happens. + * + */ + private static I2NPMessage createMessage(InputStream in, int type) throws IOException, I2NPMessageException { + switch (type) { + case DatabaseStoreMessage.MESSAGE_TYPE: + return new DatabaseStoreMessage(); + case DatabaseLookupMessage.MESSAGE_TYPE: + return new DatabaseLookupMessage(); + case DatabaseSearchReplyMessage.MESSAGE_TYPE: + return new DatabaseSearchReplyMessage(); + case DeliveryStatusMessage.MESSAGE_TYPE: + return new DeliveryStatusMessage(); + case GarlicMessage.MESSAGE_TYPE: + return new GarlicMessage(); + case TunnelMessage.MESSAGE_TYPE: + return new TunnelMessage(); + case DataMessage.MESSAGE_TYPE: + return new DataMessage(); + case SourceRouteReplyMessage.MESSAGE_TYPE: + return new SourceRouteReplyMessage(); + case TunnelCreateMessage.MESSAGE_TYPE: + return new TunnelCreateMessage(); + case TunnelCreateStatusMessage.MESSAGE_TYPE: + return new TunnelCreateStatusMessage(); + default: + throw new I2NPMessageException("The type "+ type + " is an unknown I2NP message"); + } + } + + public static void main(String args[]) { + try { + I2NPMessage msg = new I2NPMessageHandler().readMessage(new FileInputStream(args[0])); + System.out.println(msg); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java b/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java new file mode 100644 index 0000000000..c5ed08060c --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java @@ -0,0 +1,104 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +/** + * Defines the base message implementation. + * + * @author jrandom + */ +public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPMessage { + private final static Log _log = new Log(I2NPMessageImpl.class); + private Date _expiration; + private long _uniqueId; + + public final static long DEFAULT_EXPIRATION_MS = 1*60*1000; // 1 minute by default + + public I2NPMessageImpl() { + _expiration = new Date(Clock.getInstance().now() + DEFAULT_EXPIRATION_MS); + _uniqueId = RandomSource.getInstance().nextInt(Integer.MAX_VALUE); + } + + /** + * Write out the payload part of the message (not including the initial + * 1 byte type) + * + */ + protected abstract byte[] writeMessage() throws I2NPMessageException, IOException; + + /** + * Read the body into the data structures, after the initial type byte and + * the uniqueId / expiration, using the current class's format as defined by + * the I2NP specification + * + * @param in stream to read from + * @param type I2NP message type + * @throws I2NPMessageException if the stream doesn't contain a valid message + * that this class can read. + * @throws IOException if there is a problem reading from the stream + */ + protected abstract void readMessage(InputStream in, int type) throws I2NPMessageException, IOException; + + public void readBytes(InputStream in) throws DataFormatException, IOException { + try { + readBytes(in, -1); + } catch (I2NPMessageException ime) { + throw new DataFormatException("Bad bytes", ime); + } + } + public void readBytes(InputStream in, int type) throws I2NPMessageException, IOException { + try { + if (type < 0) + type = (int)DataHelper.readLong(in, 1); + _uniqueId = DataHelper.readLong(in, 4); + _expiration = DataHelper.readDate(in); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error reading the message header", dfe); + } + _log.debug("Reading bytes: type = " + type + " / uniqueId : " + _uniqueId + " / expiration : " + _expiration); + readMessage(in, type); + } + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + try { + DataHelper.writeLong(out, 1, getType()); + DataHelper.writeLong(out, 4, _uniqueId); + DataHelper.writeDate(out, _expiration); + _log.debug("Writing bytes: type = " + getType() + " / uniqueId : " + _uniqueId + " / expiration : " + _expiration); + byte[] data = writeMessage(); + out.write(data); + } catch (I2NPMessageException ime) { + throw new DataFormatException("Error writing out the I2NP message data", ime); + } + } + + /** + * Replay resistent message Id + */ + public long getUniqueId() { return _uniqueId; } + public void setUniqueId(long id) { _uniqueId = id; } + + /** + * Date after which the message should be dropped (and the associated uniqueId forgotten) + * + */ + public Date getMessageExpiration() { return _expiration; } + public void setMessageExpiration(Date exp) { _expiration = exp; } +} diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java b/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java new file mode 100644 index 0000000000..140e98a525 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java @@ -0,0 +1,139 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * The I2NPMessageReader reads an InputStream (using + * {@link I2NPMessageHandler I2NPMessageHandler}) and passes out events to a registered + * listener, where events are either messages being received, exceptions being + * thrown, or the connection being closed. Routers should use this rather + * than read from the stream themselves. + * + * @author jrandom + */ +public class I2NPMessageReader { + private final static Log _log = new Log(I2NPMessageReader.class); + private InputStream _stream; + private I2NPMessageEventListener _listener; + private I2NPMessageReaderRunner _reader; + private Thread _readerThread; + + public I2NPMessageReader(InputStream stream, I2NPMessageEventListener lsnr) { + this(stream, lsnr, "I2NP Reader"); + } + + public I2NPMessageReader(InputStream stream, I2NPMessageEventListener lsnr, String name) { + _stream = stream; + setListener(lsnr); + _reader = new I2NPMessageReaderRunner(); + _readerThread = new I2PThread(_reader); + _readerThread.setName(name); + _readerThread.setDaemon(true); + } + + public void setListener(I2NPMessageEventListener lsnr) { _listener = lsnr; } + public I2NPMessageEventListener getListener() { return _listener; } + + /** + * Instruct the reader to begin reading messages off the stream + * + */ + public void startReading() { _readerThread.start(); } + /** + * Have the already started reader pause its reading indefinitely + * + */ + public void pauseReading() { _reader.pauseRunner(); } + /** + * Resume reading after a pause + * + */ + public void resumeReading() { _reader.resumeRunner(); } + /** + * Cancel reading. + * + */ + public void stopReading() { _reader.cancelRunner(); } + + /** + * Defines the different events the reader produces while reading the stream + * + */ + public static interface I2NPMessageEventListener { + /** + * Notify the listener that a message has been received from the given + * reader + * + */ + public void messageReceived(I2NPMessageReader reader, I2NPMessage message, long msToRead); + /** + * Notify the listener that an exception was thrown while reading from the given + * reader + * + */ + public void readError(I2NPMessageReader reader, Exception error); + /** + * Notify the listener that the stream the given reader was running off + * closed + * + */ + public void disconnected(I2NPMessageReader reader); + } + + private class I2NPMessageReaderRunner implements Runnable { + private boolean _doRun; + private boolean _stayAlive; + private I2NPMessageHandler _handler; + public I2NPMessageReaderRunner() { + _doRun = true; + _stayAlive = true; + _handler = new I2NPMessageHandler(); + } + public void pauseRunner() { _doRun = false; } + public void resumeRunner() { _doRun = true; } + public void cancelRunner() { + _doRun = false; + _stayAlive = false; + } + public void run() { + while (_stayAlive) { + while (_doRun) { + // do read + try { + I2NPMessage msg = _handler.readMessage(_stream); + if (msg != null) { + long msToRead = _handler.getLastReadTime(); + _listener.messageReceived(I2NPMessageReader.this, msg, msToRead); + } + } catch (I2NPMessageException ime) { + //_log.warn("Error handling message", ime); + _listener.readError(I2NPMessageReader.this, ime); + _listener.disconnected(I2NPMessageReader.this); + cancelRunner(); + } catch (IOException ioe) { + _log.warn("IO Error handling message", ioe); + _listener.disconnected(I2NPMessageReader.this); + cancelRunner(); + } + } + if (!_doRun) { + // pause .5 secs when we're paused + try { Thread.sleep(500); } catch (InterruptedException ie) {} + } + } + // boom bye bye bad bwoy + } + } +} diff --git a/router/java/src/net/i2p/data/i2np/SourceRouteBlock.java b/router/java/src/net/i2p/data/i2np/SourceRouteBlock.java new file mode 100644 index 0000000000..58f1de6540 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/SourceRouteBlock.java @@ -0,0 +1,225 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; + +import net.i2p.crypto.ElGamalAESEngine; +import net.i2p.crypto.KeyGenerator; +import net.i2p.crypto.SessionKeyManager; +import net.i2p.data.Certificate; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.Hash; +import net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.util.Log; + + +/** + * Defines a single hop of a source routed message, as usable for building a + * SourceRouteReplyMessage + * + * @author jrandom + */ +public class SourceRouteBlock extends DataStructureImpl { + private final static Log _log = new Log(SourceRouteBlock.class); + private Hash _router; + private byte[] _data; + private SessionKey _key; + private byte[] _tag; + private DeliveryInstructions _decryptedInstructions; + private long _decryptedMessageId; + private Certificate _decryptedCertificate; + private long _decryptedExpiration; + + public SourceRouteBlock() { + setRouter(null); + setData(null); + setKey(null); + setTag((byte[])null); + _decryptedInstructions = null; + _decryptedMessageId = -1; + _decryptedCertificate = null; + _decryptedExpiration = -1; + } + + /** + * Get the router through which replies using this source route block must + * be sent (as the getData() is encrypted for their eyes only) + * + */ + public Hash getRouter() { return _router; } + public void setRouter(Hash router) { _router= router; } + /** + * Get the encrypted header. After decryption (via ElGamal+AES as defined + * in the data structures spec), this array contains: + * DeliveryInstructions + * 4 byte Integer for a message ID + * Certificate + * Date of expiration for replies + * + */ + public byte[] getData() { return _data; } + private void setData(byte data[]) { _data = data; } + /** + * Retrieve the session key which may be used in conjunction with the tag + * to encrypt a garlic message and send it as a reply to this message. + * The encryption would follow scenario 2 of the ElGamal+AES encryption method + * defined in the data structures spec. + * + */ + public SessionKey getKey() { return _key; } + public void setKey(SessionKey key) { _key = key; } + /** + * Get the tag made available for use in conjunction with the getKey() to + * ElGamal+AES encrypt a garlic message without knowing the public key to + * which the message is destined + * + */ + public byte[] getTag() { return _tag; } + public void setTag(SessionTag tag) { setTag(tag.getData()); } + public void setTag(byte tag[]) { + if ( (tag != null) && (tag.length != SessionTag.BYTE_LENGTH) ) + throw new IllegalArgumentException("Tag must be either null or 32 bytes"); + _tag = tag; + } + + /** + * After decryptData, this contains the delivery instructions for this block + */ + public DeliveryInstructions getDecryptedInstructions() { return _decryptedInstructions; } + /** + * After decryptData, this contains the message ID to be used with this block + */ + public long getDecryptedMessageId() { return _decryptedMessageId; } + /** + * After decryptData, this contains the Certificate 'paying' for the forwarding according to + * this block + */ + public Certificate getDecryptedCertificate() { return _decryptedCertificate; } + /** + * After decryptData, this contains the date after which this block should not be forwarded + */ + public long getDecryptedExpiration() { return _decryptedExpiration; } + + /** + * Set the raw data with the formatted and encrypted options specified + * + * @param instructions Where a message bearing this block should be sent + * @param messageId ID of the message for this block (not repeatable) + * @param expiration date after which this block expires + * @param replyThrough Encryption key of the router to whom this block is specified (not + * the router specified in the delivery instructions!) + * + * @throws DataFormatException if the data is invalid or could not be encrypted + */ + public void setData(DeliveryInstructions instructions, long messageId, Certificate cert, long expiration, PublicKey replyThrough) throws DataFormatException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(64); + + _decryptedInstructions = instructions; + _decryptedMessageId = messageId; + _decryptedCertificate = cert; + _decryptedExpiration = expiration; + + instructions.writeBytes(baos); + DataHelper.writeLong(baos, 4, messageId); + cert.writeBytes(baos); + DataHelper.writeDate(baos, new Date(expiration)); + + int paddedSize = 256; + SessionKey sessKey = null; + SessionTag tag = null; + if (instructions.getDelayRequested()) { + // always use a new key if we're delaying, since the reply block may not be used within the + // window of a session + sessKey = KeyGenerator.getInstance().generateSessionKey(); + tag = null; + _log.debug("Delay requested - creating a new session key"); + } else { + sessKey = SessionKeyManager.getInstance().getCurrentKey(replyThrough); + if (sessKey == null) { + sessKey = KeyGenerator.getInstance().generateSessionKey(); + tag = null; + _log.debug("No delay requested, but no session key is known"); + } else { + tag = SessionKeyManager.getInstance().consumeNextAvailableTag(replyThrough, sessKey); + } + } + byte encData[] = ElGamalAESEngine.encrypt(baos.toByteArray(), replyThrough, sessKey, null, tag, paddedSize); + setData(encData); + } catch (IOException ioe) { + throw new DataFormatException("Error writing out the source route block data", ioe); + } catch (DataFormatException dfe) { + throw new DataFormatException("Error writing out the source route block data", dfe); + } + } + + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _router = new Hash(); + _router.readBytes(in); + int size = (int)DataHelper.readLong(in, 2); + _data = new byte[size]; + int read = read(in, _data); + if (read != _data.length) + throw new DataFormatException("Incorrect # of bytes read for source route block: " + read); + _key = new SessionKey(); + _key.readBytes(in); + _tag = new byte[32]; + read = read(in, _tag); + if (read != _tag.length) + throw new DataFormatException("Incorrect # of bytes read for session tag: " + read); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if ( (_router == null) || (_data == null) || (_key == null) || (_tag == null) || (_tag.length != 32) ) + throw new DataFormatException("Insufficient data to write"); + _router.writeBytes(out); + DataHelper.writeLong(out, 2, _data.length); + out.write(_data); + _key.writeBytes(out); + out.write(_tag); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof SourceRouteBlock)) + return false; + SourceRouteBlock block = (SourceRouteBlock)obj; + return DataHelper.eq(getRouter(), block.getRouter()) && + DataHelper.eq(getData(), block.getData()) && + DataHelper.eq(getKey(), block.getKey()) && + DataHelper.eq(getTag(), block.getTag()); + } + + public int hashCode() { + return DataHelper.hashCode(getRouter()) + + DataHelper.hashCode(getData()) + + DataHelper.hashCode(getKey()) + + DataHelper.hashCode(getTag()); + } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append("[SourceRouteBlock: "); + buf.append("\n\tRouter: ").append(getRouter()); + buf.append("\n\tData: ").append(DataHelper.toString(getData(), getData().length)); + buf.append("\n\tTag: ").append(DataHelper.toString(getTag(), (getTag() != null ? getTag().length : 0))); + buf.append("\n\tKey: ").append(getKey()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/SourceRouteReplyMessage.java b/router/java/src/net/i2p/data/i2np/SourceRouteReplyMessage.java new file mode 100644 index 0000000000..017ee6bb28 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/SourceRouteReplyMessage.java @@ -0,0 +1,159 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.crypto.ElGamalAESEngine; +import net.i2p.data.Certificate; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.PrivateKey; +import net.i2p.util.Log; + +/** + * Defines a message directed by a source route block to deliver a message to an + * unknown location. + * + * @author jrandom + */ +public class SourceRouteReplyMessage extends I2NPMessageImpl { + private final static Log _log = new Log(SourceRouteReplyMessage.class); + public final static int MESSAGE_TYPE = 13; + private byte _encryptedHeader[]; + private I2NPMessage _message; + private DeliveryInstructions _decryptedInstructions; + private long _decryptedMessageId; + private Certificate _decryptedCertificate; + private long _decryptedExpiration; + + public SourceRouteReplyMessage() { + _encryptedHeader = null; + _message = null; + _decryptedInstructions = null; + _decryptedMessageId = -1; + _decryptedCertificate = null; + _decryptedExpiration = -1; + } + + /** + * Retrieve the message being sent as a reply + */ + public I2NPMessage getMessage() { return _message; } + public void setMessage(I2NPMessage message) { _message = message; } + + public void setEncryptedHeader(byte header[]) { _encryptedHeader = header; } + + /** + * After decryptHeader, this contains the delivery instructions for this block + */ + public DeliveryInstructions getDecryptedInstructions() { return _decryptedInstructions; } + /** + * After decryptHeader, this contains the message ID to be used with this block + */ + public long getDecryptedMessageId() { return _decryptedMessageId; } + /** + * After decryptHeader, this contains the Certificate 'paying' for the forwarding according to + * this block + */ + public Certificate getDecryptedCertificate() { return _decryptedCertificate; } + /** + * After decryptHeader, this contains the date after which this block should not be forwarded + */ + public long getDecryptedExpiration() { return _decryptedExpiration; } + + /** + * Decrypt the header and store it in the various getDecryptedXYZ() properties + * + * @throws DataFormatException if the decryption fails or if the data is somehow malformed + */ + public void decryptHeader(PrivateKey key) throws DataFormatException { + if ( (_encryptedHeader == null) || (_encryptedHeader.length <= 0) ) + throw new DataFormatException("No header to decrypt"); + + byte decr[] = ElGamalAESEngine.decrypt(_encryptedHeader, key); + + if (decr == null) + throw new DataFormatException("Decrypted data is null"); + + try { + ByteArrayInputStream bais = new ByteArrayInputStream(decr); + + _decryptedInstructions = new DeliveryInstructions(); + _decryptedInstructions.readBytes(bais); + _decryptedMessageId = DataHelper.readLong(bais, 4); + _decryptedCertificate = new Certificate(); + _decryptedCertificate.readBytes(bais); + _decryptedExpiration = DataHelper.readDate(bais).getTime(); + + } catch (IOException ioe) { + throw new DataFormatException("Error reading the source route reply header", ioe); + } catch (DataFormatException dfe) { + throw new DataFormatException("Error reading the source route reply header", dfe); + } + } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + int headerSize = (int)DataHelper.readLong(in, 2); + _encryptedHeader = new byte[headerSize]; + int read = read(in, _encryptedHeader); + if (read != headerSize) + throw new DataFormatException("Not enough bytes to read the header (read = " + read + ", required = " + headerSize + ")"); + _message = new I2NPMessageHandler().readMessage(in); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + if ( (_encryptedHeader == null) || (_message == null) ) + throw new I2NPMessageException("Not enough data to write out"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + try { + DataHelper.writeLong(os, 2, _encryptedHeader.length); + os.write(_encryptedHeader); + _message.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return DataHelper.hashCode(_encryptedHeader) + + DataHelper.hashCode(_message); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof SourceRouteReplyMessage) ) { + SourceRouteReplyMessage msg = (SourceRouteReplyMessage)object; + return DataHelper.eq(_message,msg._message) && + DataHelper.eq(_encryptedHeader,msg._encryptedHeader); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[SourceRouteReplyMessage: "); + buf.append("\n\tHeader: ").append(DataHelper.toString(_encryptedHeader, 64)); + buf.append("\n\tMessage: ").append(_message); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/TunnelConfigurationSessionKey.java b/router/java/src/net/i2p/data/i2np/TunnelConfigurationSessionKey.java new file mode 100644 index 0000000000..6d4d1a3820 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/TunnelConfigurationSessionKey.java @@ -0,0 +1,61 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; +import net.i2p.data.SessionKey; + +/** + * Contains the session key used by the owner/creator of the tunnel to modify + * its operational settings. + * + * @author jrandom + */ +public class TunnelConfigurationSessionKey extends DataStructureImpl { + private final static Log _log = new Log(TunnelConfigurationSessionKey.class); + private SessionKey _key; + + public TunnelConfigurationSessionKey() { setKey(null); } + + public SessionKey getKey() { return _key; } + public void setKey(SessionKey key) { _key= key; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _key = new SessionKey(); + _key.readBytes(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_key == null) throw new DataFormatException("Invalid key"); + _key.writeBytes(out); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof TunnelConfigurationSessionKey)) + return false; + return DataHelper.eq(getKey(), ((TunnelConfigurationSessionKey)obj).getKey()); + } + + public int hashCode() { + if (_key == null) return 0; + return getKey().hashCode(); + } + + public String toString() { + return "[TunnelConfigurationSessionKey: " + getKey() + "]"; + } +} diff --git a/router/java/src/net/i2p/data/i2np/TunnelCreateMessage.java b/router/java/src/net/i2p/data/i2np/TunnelCreateMessage.java new file mode 100644 index 0000000000..b6f851646e --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/TunnelCreateMessage.java @@ -0,0 +1,312 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.Certificate; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.TunnelId; +import net.i2p.util.Log; + +/** + * Defines the message sent to a router to request that it participate in a + * tunnel using the included configuration settings. + * + * @author jrandom + */ +public class TunnelCreateMessage extends I2NPMessageImpl { + private final static Log _log = new Log(TunnelCreateMessage.class); + public final static int MESSAGE_TYPE = 6; + private int _participantType; + private Hash _nextRouter; + private TunnelId _tunnelId; + private long _tunnelDuration; + private TunnelConfigurationSessionKey _configKey; + private long _maxPeakMessagesPerMin; + private long _maxAvgMessagesPerMin; + private long _maxPeakBytesPerMin; + private long _maxAvgBytesPerMin; + private boolean _includeDummyTraffic; + private boolean _reorderMessages; + private TunnelSigningPublicKey _verificationPubKey; + private TunnelSigningPrivateKey _verificationPrivKey; + private TunnelSessionKey _tunnelKey; + private Certificate _certificate; + private SourceRouteBlock _replyBlock; + + public static final int PARTICIPANT_TYPE_GATEWAY = 1; + public static final int PARTICIPANT_TYPE_ENDPOINT = 2; + public static final int PARTICIPANT_TYPE_OTHER = 3; + + private final static long FLAG_DUMMY = 1 << 7; + private final static long FLAG_REORDER = 1 << 6; + + public TunnelCreateMessage() { + setParticipantType(-1); + setNextRouter(null); + setTunnelId(null); + setTunnelDurationSeconds(-1); + setConfigurationKey(null); + setMaxPeakMessagesPerMin(-1); + setMaxAvgMessagesPerMin(-1); + setMaxPeakBytesPerMin(-1); + setMaxAvgBytesPerMin(-1); + setIncludeDummyTraffic(false); + setReorderMessages(false); + setVerificationPublicKey(null); + setVerificationPrivateKey(null); + setTunnelKey(null); + setCertificate(null); + setReplyBlock(null); + } + + public void setParticipantType(int type) { _participantType = type; } + public int getParticipantType() { return _participantType; } + public void setNextRouter(Hash routerIdentityHash) { _nextRouter = routerIdentityHash; } + public Hash getNextRouter() { return _nextRouter; } + public void setTunnelId(TunnelId id) { _tunnelId = id; } + public TunnelId getTunnelId() { return _tunnelId; } + public void setTunnelDurationSeconds(long durationSeconds) { _tunnelDuration = durationSeconds; } + public long getTunnelDurationSeconds() { return _tunnelDuration; } + public void setConfigurationKey(TunnelConfigurationSessionKey key) { _configKey = key; } + public TunnelConfigurationSessionKey getConfigurationKey() { return _configKey; } + public void setMaxPeakMessagesPerMin(long msgs) { _maxPeakMessagesPerMin = msgs; } + public long getMaxPeakMessagesPerMin() { return _maxPeakMessagesPerMin; } + public void setMaxAvgMessagesPerMin(long msgs) { _maxAvgMessagesPerMin = msgs; } + public long getMaxAvgMessagesPerMin() { return _maxAvgMessagesPerMin; } + public void setMaxPeakBytesPerMin(long bytes) { _maxPeakBytesPerMin = bytes; } + public long getMaxPeakBytesPerMin() { return _maxPeakBytesPerMin; } + public void setMaxAvgBytesPerMin(long bytes) { _maxAvgBytesPerMin = bytes; } + public long getMaxAvgBytesPerMin() { return _maxAvgBytesPerMin; } + public void setIncludeDummyTraffic(boolean include) { _includeDummyTraffic = include; } + public boolean getIncludeDummyTraffic() { return _includeDummyTraffic; } + public void setReorderMessages(boolean reorder) { _reorderMessages = reorder; } + public boolean getReorderMessages() { return _reorderMessages; } + public void setVerificationPublicKey(TunnelSigningPublicKey key) { _verificationPubKey = key; } + public TunnelSigningPublicKey getVerificationPublicKey() { return _verificationPubKey; } + public void setVerificationPrivateKey(TunnelSigningPrivateKey key) { _verificationPrivKey = key; } + public TunnelSigningPrivateKey getVerificationPrivateKey() { return _verificationPrivKey; } + public void setTunnelKey(TunnelSessionKey key) { _tunnelKey = key; } + public TunnelSessionKey getTunnelKey() { return _tunnelKey; } + public void setCertificate(Certificate cert) { _certificate = cert; } + public Certificate getCertificate() { return _certificate; } + public void setReplyBlock(SourceRouteBlock block) { _replyBlock = block; } + public SourceRouteBlock getReplyBlock() { return _replyBlock; } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + _participantType = (int)DataHelper.readLong(in, 1); + if (_participantType != PARTICIPANT_TYPE_ENDPOINT) { + _nextRouter = new Hash(); + _nextRouter.readBytes(in); + } + _tunnelId = new TunnelId(); + _tunnelId.readBytes(in); + _tunnelDuration = DataHelper.readLong(in, 4); + _configKey = new TunnelConfigurationSessionKey(); + _configKey.readBytes(in); + _maxPeakMessagesPerMin = DataHelper.readLong(in, 4); + _maxAvgMessagesPerMin = DataHelper.readLong(in, 4); + _maxPeakBytesPerMin = DataHelper.readLong(in, 4); + _maxAvgBytesPerMin = DataHelper.readLong(in, 4); + + int flags = (int)DataHelper.readLong(in, 1); + _includeDummyTraffic = flagsIncludeDummy(flags); + _reorderMessages = flagsReorder(flags); + + _verificationPubKey = new TunnelSigningPublicKey(); + _verificationPubKey.readBytes(in); + if (_participantType == PARTICIPANT_TYPE_GATEWAY) { + _verificationPrivKey = new TunnelSigningPrivateKey(); + _verificationPrivKey.readBytes(in); + } + if ( (_participantType == PARTICIPANT_TYPE_ENDPOINT) || (_participantType == PARTICIPANT_TYPE_GATEWAY) ) { + _tunnelKey = new TunnelSessionKey(); + _tunnelKey.readBytes(in); + } + _certificate = new Certificate(); + _certificate.readBytes(in); + _replyBlock = new SourceRouteBlock(); + _replyBlock.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(32); + try { + DataHelper.writeLong(os, 1, _participantType); + if (_participantType != PARTICIPANT_TYPE_ENDPOINT) { + _nextRouter.writeBytes(os); + } + _tunnelId.writeBytes(os); + DataHelper.writeLong(os, 4, _tunnelDuration); + _configKey.writeBytes(os); + + DataHelper.writeLong(os, 4, _maxPeakMessagesPerMin); + DataHelper.writeLong(os, 4, _maxAvgMessagesPerMin); + DataHelper.writeLong(os, 4, _maxPeakBytesPerMin); + DataHelper.writeLong(os, 4, _maxAvgBytesPerMin); + + long flags = getFlags(); + DataHelper.writeLong(os, 1, flags); + + _verificationPubKey.writeBytes(os); + if (_participantType == PARTICIPANT_TYPE_GATEWAY) { + _verificationPrivKey.writeBytes(os); + } + if ( (_participantType == PARTICIPANT_TYPE_ENDPOINT) || (_participantType == PARTICIPANT_TYPE_GATEWAY) ) { + _tunnelKey.writeBytes(os); + } + _certificate.writeBytes(os); + _replyBlock.writeBytes(os); + } catch (Throwable t) { + throw new I2NPMessageException("Error writing out the message data", t); + } + /* + try { + DataHelper.writeLong(os, 1, _participantType); + if (_participantType != PARTICIPANT_TYPE_ENDPOINT) { + if (_nextRouter == null) + throw new I2NPMessageException("Next router is not defined"); + _nextRouter.writeBytes(os); + } + if (_tunnelId == null) + throw new I2NPMessageException("Tunnel ID is not defined"); + _tunnelId.writeBytes(os); + if (_tunnelDuration < 0) + throw new I2NPMessageException("Tunnel duration is negative"); + DataHelper.writeLong(os, 4, _tunnelDuration); + if (_configKey == null) + throw new I2NPMessageException("Configuration key is not defined"); + _configKey.writeBytes(os); + if ( (_maxPeakMessagesPerMin < 0) || (_maxAvgMessagesPerMin < 0) || + (_maxAvgMessagesPerMin < 0) || (_maxAvgBytesPerMin < 0) ) + throw new I2NPMessageException("Negative limits defined"); + + long flags = getFlags(); + DataHelper.writeLong(os, 1, flags); + + if (_verificationPubKey == null) + throw new I2NPMessageException("Verification public key is not defined"); + _verificationPubKey.writeBytes(os); + if (_participantType == PARTICIPANT_TYPE_GATEWAY) { + if (_verificationPrivKey == null) + throw new I2NPMessageException("Verification private key is needed and not defined"); + _verificationPrivKey.writeBytes(os); + } + if ( (_participantType == PARTICIPANT_TYPE_ENDPOINT) || (_participantType == PARTICIPANT_TYPE_GATEWAY) ) { + if (_tunnelKey == null) + throw new I2NPMessageException("Tunnel key is needed and not defined"); + _tunnelKey.writeBytes(os); + } + if (_certificate == null) + throw new I2NPMessageException("Certificate is not defined"); + _certificate.writeBytes(os); + if (_replyBlock == null) + throw new I2NPMessageException("Reply block not defined"); + _replyBlock.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + */ + return os.toByteArray(); + } + + private boolean flagsIncludeDummy(long flags) { + return (0 != (flags & FLAG_DUMMY)); + } + private boolean flagsReorder(long flags) { + return (0 != (flags & FLAG_REORDER)); + } + + private long getFlags() { + long val = 0L; + if (getIncludeDummyTraffic()) + val = val | FLAG_DUMMY; + if (getReorderMessages()) + val = val | FLAG_REORDER; + return val; + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return (int)(DataHelper.hashCode(getCertificate()) + + DataHelper.hashCode(getConfigurationKey()) + + DataHelper.hashCode(getNextRouter()) + + DataHelper.hashCode(getReplyBlock()) + + DataHelper.hashCode(getTunnelId()) + + DataHelper.hashCode(getTunnelKey()) + + DataHelper.hashCode(getVerificationPrivateKey()) + + DataHelper.hashCode(getVerificationPublicKey()) + + (getIncludeDummyTraffic() ? 1 : 0) + + getMaxAvgBytesPerMin() + + getMaxAvgMessagesPerMin() + + getMaxPeakBytesPerMin() + + getMaxPeakMessagesPerMin() + + getParticipantType() + + (getReorderMessages() ? 1 : 0) + + getTunnelDurationSeconds()); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof TunnelCreateMessage) ) { + TunnelCreateMessage msg = (TunnelCreateMessage)object; + return DataHelper.eq(getCertificate(), msg.getCertificate()) && + DataHelper.eq(getConfigurationKey(), msg.getConfigurationKey()) && + DataHelper.eq(getNextRouter(), msg.getNextRouter()) && + DataHelper.eq(getReplyBlock(), msg.getReplyBlock()) && + DataHelper.eq(getTunnelId(), msg.getTunnelId()) && + DataHelper.eq(getTunnelKey(), msg.getTunnelKey()) && + DataHelper.eq(getVerificationPrivateKey(), msg.getVerificationPrivateKey()) && + DataHelper.eq(getVerificationPublicKey(), msg.getVerificationPublicKey()) && + (getIncludeDummyTraffic() == msg.getIncludeDummyTraffic()) && + (getMaxAvgBytesPerMin() == msg.getMaxAvgBytesPerMin()) && + (getMaxAvgMessagesPerMin() == msg.getMaxAvgMessagesPerMin()) && + (getMaxPeakBytesPerMin() == msg.getMaxPeakBytesPerMin()) && + (getMaxPeakMessagesPerMin() == msg.getMaxPeakMessagesPerMin()) && + (getParticipantType() == msg.getParticipantType()) && + (getReorderMessages() == msg.getReorderMessages()) && + (getTunnelDurationSeconds() == msg.getTunnelDurationSeconds()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[TunnelCreateMessage: "); + buf.append("\n\tParticipant Type: ").append(getParticipantType()); + buf.append("\n\tCertificate: ").append(getCertificate()); + buf.append("\n\tConfiguration Key: ").append(getConfigurationKey()); + buf.append("\n\tNext Router: ").append(getNextRouter()); + buf.append("\n\tReply Block: ").append(getReplyBlock()); + buf.append("\n\tTunnel ID: ").append(getTunnelId()); + buf.append("\n\tTunnel Key: ").append(getTunnelKey()); + buf.append("\n\tVerification Private Key: ").append(getVerificationPrivateKey()); + buf.append("\n\tVerification Public Key: ").append(getVerificationPublicKey()); + buf.append("\n\tInclude Dummy Traffic: ").append(getIncludeDummyTraffic()); + buf.append("\n\tMax Avg Bytes / Minute: ").append(getMaxAvgBytesPerMin()); + buf.append("\n\tMax Peak Bytes / Minute: ").append(getMaxPeakBytesPerMin()); + buf.append("\n\tMax Avg Messages / Minute: ").append(getMaxAvgMessagesPerMin()); + buf.append("\n\tMax Peak Messages / Minute: ").append(getMaxPeakMessagesPerMin()); + buf.append("\n\tReorder Messages: ").append(getReorderMessages()); + buf.append("\n\tTunnel Duration (seconds): ").append(getTunnelDurationSeconds()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/TunnelCreateStatusMessage.java b/router/java/src/net/i2p/data/i2np/TunnelCreateStatusMessage.java new file mode 100644 index 0000000000..f18fc17b9c --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/TunnelCreateStatusMessage.java @@ -0,0 +1,113 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.TunnelId; +import net.i2p.util.Log; + +/** + * Defines the message a router sends to another router in reply to a + * TunnelCreateMessage + * + * @author jrandom + */ +public class TunnelCreateStatusMessage extends I2NPMessageImpl { + private final static Log _log = new Log(TunnelCreateStatusMessage.class); + public final static int MESSAGE_TYPE = 7; + private TunnelId _tunnelId; + private int _status; + private Hash _from; + + public final static int STATUS_SUCCESS = 0; + public final static int STATUS_FAILED_DUPLICATE_ID = 1; + public final static int STATUS_FAILED_OVERLOADED = 2; + public final static int STATUS_FAILED_CERTIFICATE = 3; + public final static int STATUS_FAILED_DELETED = 100; + + public TunnelCreateStatusMessage() { + setTunnelId(null); + setStatus(-1); + setFromHash(null); + } + + public TunnelId getTunnelId() { return _tunnelId; } + public void setTunnelId(TunnelId id) { _tunnelId = id; } + + public int getStatus() { return _status; } + public void setStatus(int status) { _status = status; } + + /** + * Contains the SHA256 Hash of the RouterIdentity sending the message + */ + public Hash getFromHash() { return _from; } + public void setFromHash(Hash from) { _from = from; } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + _tunnelId = new TunnelId(); + _tunnelId.readBytes(in); + _status = (int)DataHelper.readLong(in, 1); + _from = new Hash(); + _from.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + if ( (_tunnelId == null) || (_from == null) ) throw new I2NPMessageException("Not enough data to write out"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(32); + try { + _tunnelId.writeBytes(os); + DataHelper.writeLong(os, 1, (_status < 0 ? 255 : _status)); + _from.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return DataHelper.hashCode(getTunnelId()) + + getStatus() + + DataHelper.hashCode(getFromHash()); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof TunnelCreateStatusMessage) ) { + TunnelCreateStatusMessage msg = (TunnelCreateStatusMessage)object; + return DataHelper.eq(getTunnelId(),msg.getTunnelId()) && + DataHelper.eq(getFromHash(),msg.getFromHash()) && + (getStatus() == msg.getStatus()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[TunnelCreateStatusMessage: "); + buf.append("\n\tTunnel ID: ").append(getTunnelId()); + buf.append("\n\tStatus: ").append(getStatus()); + buf.append("\n\tFrom: ").append(getFromHash()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/TunnelMessage.java b/router/java/src/net/i2p/data/i2np/TunnelMessage.java new file mode 100644 index 0000000000..26aeb38dcb --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/TunnelMessage.java @@ -0,0 +1,145 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.TunnelId; +import net.i2p.util.Log; + +/** + * Defines the message sent between routers for tunnel delivery + * + * @author jrandom + */ +public class TunnelMessage extends I2NPMessageImpl { + private final static Log _log = new Log(TunnelMessage.class); + public final static int MESSAGE_TYPE = 8; + private TunnelId _tunnelId; + private long _size; + private byte[] _data; + private TunnelVerificationStructure _verification; + private byte[] _encryptedInstructions; + + private final static int FLAG_INCLUDESTRUCTURE = 0; + private final static int FLAG_DONT_INCLUDESTRUCTURE = 1; + + public TunnelMessage() { + setTunnelId(null); + setData(null); + setVerificationStructure(null); + setEncryptedDeliveryInstructions(null); + } + + public TunnelId getTunnelId() { return _tunnelId; } + public void setTunnelId(TunnelId id) { _tunnelId = id; } + + public byte[] getData() { return _data; } + public void setData(byte data[]) { _data = data; } + + public TunnelVerificationStructure getVerificationStructure() { return _verification; } + public void setVerificationStructure(TunnelVerificationStructure verification) { _verification = verification; } + + public byte[] getEncryptedDeliveryInstructions() { return _encryptedInstructions; } + public void setEncryptedDeliveryInstructions(byte instructions[]) { _encryptedInstructions = instructions; } + + public void readMessage(InputStream in, int type) throws I2NPMessageException, IOException { + if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message"); + try { + _tunnelId = new TunnelId(); + _tunnelId.readBytes(in); + _log.debug("Read tunnel message for tunnel " + _tunnelId); + _size = DataHelper.readLong(in, 4); + _log.debug("Read tunnel message size: " + _size); + if (_size < 0) throw new I2NPMessageException("Invalid size in the structure: " + _size); + _data = new byte[(int)_size]; + int read = read(in, _data); + if (read != _size) + throw new I2NPMessageException("Incorrect number of bytes read (" + read + ", expected " + _size); + int includeVerification = (int)DataHelper.readLong(in, 1); + if (includeVerification == FLAG_INCLUDESTRUCTURE) { + _verification = new TunnelVerificationStructure(); + _verification.readBytes(in); + int len = (int)DataHelper.readLong(in, 2); + _encryptedInstructions = new byte[len]; + read = read(in, _encryptedInstructions); + if (read != len) + throw new I2NPMessageException("Incorrect number of bytes read for instructions (" + read + ", expected " + len + ")"); + } + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] writeMessage() throws I2NPMessageException, IOException { + if ( (_tunnelId == null) || (_data == null) || (_data.length <= 0) ) + throw new I2NPMessageException("Not enough data to write out"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(32); + try { + _tunnelId.writeBytes(os); + _log.debug("Writing tunnel message for tunnel " + _tunnelId); + DataHelper.writeLong(os, 4, _data.length); + _log.debug("Writing tunnel message length: " + _data.length); + os.write(_data); + _log.debug("Writing tunnel message data"); + if ( (_verification == null) || (_encryptedInstructions == null) ) { + DataHelper.writeLong(os, 1, FLAG_DONT_INCLUDESTRUCTURE); + _log.debug("Writing DontIncludeStructure flag"); + } else { + DataHelper.writeLong(os, 1, FLAG_INCLUDESTRUCTURE); + _log.debug("Writing IncludeStructure flag, then the verification structure, then the E(instr).length [" + _encryptedInstructions.length + "], then the E(instr)"); + _verification.writeBytes(os); + DataHelper.writeLong(os, 2, _encryptedInstructions.length); + os.write(_encryptedInstructions); + } + } catch (DataFormatException dfe) { + throw new I2NPMessageException("Error writing out the message data", dfe); + } + byte rv[] = os.toByteArray(); + _log.debug("Overall data being written: " + rv.length); + return rv; + } + + public int getType() { return MESSAGE_TYPE; } + + public int hashCode() { + return DataHelper.hashCode(getTunnelId()) + + DataHelper.hashCode(_data) + + DataHelper.hashCode(getVerificationStructure()) + + DataHelper.hashCode(getEncryptedDeliveryInstructions()); + } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof TunnelMessage) ) { + TunnelMessage msg = (TunnelMessage)object; + return DataHelper.eq(getTunnelId(),msg.getTunnelId()) && + DataHelper.eq(getVerificationStructure(),msg.getVerificationStructure()) && + DataHelper.eq(getData(),msg.getData()) && + DataHelper.eq(getEncryptedDeliveryInstructions(), msg.getEncryptedDeliveryInstructions()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[TunnelMessage: "); + buf.append("\n\tTunnel ID: ").append(getTunnelId()); + buf.append("\n\tVerification Structure: ").append(getVerificationStructure()); + buf.append("\n\tEncrypted Instructions: ").append(getEncryptedDeliveryInstructions()); + buf.append("\n\tData size: ").append(getData().length); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/data/i2np/TunnelSessionKey.java b/router/java/src/net/i2p/data/i2np/TunnelSessionKey.java new file mode 100644 index 0000000000..d5f4bd822e --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/TunnelSessionKey.java @@ -0,0 +1,61 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; +import net.i2p.data.SessionKey; + +/** + * Contains the session key used by the tunnel gateway to encrypt the DeliveryInstructions + * and used by the tunnel end point to decrypt those instructions. + * + * @author jrandom + */ +public class TunnelSessionKey extends DataStructureImpl { + private final static Log _log = new Log(TunnelSessionKey.class); + private SessionKey _key; + + public TunnelSessionKey() { setKey(null); } + + public SessionKey getKey() { return _key; } + public void setKey(SessionKey key) { _key= key; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _key = new SessionKey(); + _key.readBytes(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_key == null) throw new DataFormatException("Invalid key"); + _key.writeBytes(out); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof TunnelSessionKey)) + return false; + return DataHelper.eq(getKey(), ((TunnelSessionKey)obj).getKey()); + } + + public int hashCode() { + if (_key == null) return 0; + return getKey().hashCode(); + } + + public String toString() { + return "[TunnelSessionKey: " + getKey() + "]"; + } +} diff --git a/router/java/src/net/i2p/data/i2np/TunnelSigningPrivateKey.java b/router/java/src/net/i2p/data/i2np/TunnelSigningPrivateKey.java new file mode 100644 index 0000000000..15c41acead --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/TunnelSigningPrivateKey.java @@ -0,0 +1,62 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; +import net.i2p.data.SigningPrivateKey; + +/** + * Contains the private key which constructs a signature for the TunnelMessage + * which every participant in a tunnel uses to verify the + * TunnelVerificationStructure with. + * + * @author jrandom + */ +public class TunnelSigningPrivateKey extends DataStructureImpl { + private final static Log _log = new Log(EndPointPrivateKey.class); + private SigningPrivateKey _key; + + public TunnelSigningPrivateKey() { setKey(null); } + + public SigningPrivateKey getKey() { return _key; } + public void setKey(SigningPrivateKey key) { _key= key; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _key = new SigningPrivateKey(); + _key.readBytes(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_key == null) throw new DataFormatException("Invalid key"); + _key.writeBytes(out); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof TunnelSigningPrivateKey)) + return false; + return DataHelper.eq(getKey(), ((TunnelSigningPrivateKey)obj).getKey()); + } + + public int hashCode() { + if (_key == null) return 0; + return getKey().hashCode(); + } + + public String toString() { + return "[EndPointPrivateKey: " + getKey() + "]"; + } +} diff --git a/router/java/src/net/i2p/data/i2np/TunnelSigningPublicKey.java b/router/java/src/net/i2p/data/i2np/TunnelSigningPublicKey.java new file mode 100644 index 0000000000..4845c175b7 --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/TunnelSigningPublicKey.java @@ -0,0 +1,61 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; +import net.i2p.data.SigningPublicKey; + +/** + * Contains the public key which every participant in a tunnel uses to verify + * the TunnelVerificationStructure for TunnelMessages that pass by. + * + * @author jrandom + */ +public class TunnelSigningPublicKey extends DataStructureImpl { + private final static Log _log = new Log(TunnelSigningPublicKey.class); + private SigningPublicKey _key; + + public TunnelSigningPublicKey() { setKey(null); } + + public SigningPublicKey getKey() { return _key; } + public void setKey(SigningPublicKey key) { _key= key; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _key = new SigningPublicKey(); + _key.readBytes(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_key == null) throw new DataFormatException("Invalid key"); + _key.writeBytes(out); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof TunnelSigningPublicKey)) + return false; + return DataHelper.eq(getKey(), ((TunnelSigningPublicKey)obj).getKey()); + } + + public int hashCode() { + if (_key == null) return 0; + return getKey().hashCode(); + } + + public String toString() { + return "[TunnelSigningPublicKey: " + getKey() + "]"; + } +} diff --git a/router/java/src/net/i2p/data/i2np/TunnelVerificationStructure.java b/router/java/src/net/i2p/data/i2np/TunnelVerificationStructure.java new file mode 100644 index 0000000000..7bef3dbd2e --- /dev/null +++ b/router/java/src/net/i2p/data/i2np/TunnelVerificationStructure.java @@ -0,0 +1,90 @@ +package net.i2p.data.i2np; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; +import net.i2p.data.Hash; +import net.i2p.data.Signature; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.crypto.DSAEngine; + +/** + * + * @author jrandom + */ +public class TunnelVerificationStructure extends DataStructureImpl { + private final static Log _log = new Log(TunnelVerificationStructure.class); + private Hash _msgHash; + private Signature _authSignature; + + public TunnelVerificationStructure() { + setMessageHash(null); + setAuthorizationSignature(null); + } + + public Hash getMessageHash() { return _msgHash; } + public void setMessageHash(Hash hash) { _msgHash = hash; } + + public Signature getAuthorizationSignature() { return _authSignature; } + public void setAuthorizationSignature(Signature sig) { _authSignature = sig; } + + public void sign(SigningPrivateKey key) { + if (_msgHash != null) { + Signature sig = DSAEngine.getInstance().sign(_msgHash.getData(), key); + setAuthorizationSignature(sig); + } + } + public boolean verifySignature(SigningPublicKey key) { + if (_msgHash == null) return false; + return DSAEngine.getInstance().verifySignature(_authSignature, _msgHash.getData(), key); + } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _msgHash = new Hash(); + _msgHash.readBytes(in); + _authSignature = new Signature(); + _authSignature.readBytes(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_authSignature == null) { + _authSignature = new Signature(); + _authSignature.setData(Signature.FAKE_SIGNATURE); + } + if ( (_msgHash == null) || (_authSignature == null) ) throw new DataFormatException("Invalid data"); + _msgHash.writeBytes(out); + _authSignature.writeBytes(out); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof TunnelVerificationStructure)) + return false; + TunnelVerificationStructure str = (TunnelVerificationStructure)obj; + return DataHelper.eq(getMessageHash(), str.getMessageHash()) && + DataHelper.eq(getAuthorizationSignature(), str.getAuthorizationSignature()); + } + + public int hashCode() { + if ( (_msgHash == null) || (_authSignature == null) ) return 0; + return getMessageHash().hashCode() + getAuthorizationSignature().hashCode(); + } + + public String toString() { + return "[TunnelVerificationStructure: " + getMessageHash() + " " + getAuthorizationSignature() + "]"; + } +} diff --git a/router/java/src/net/i2p/router/.nbattrs b/router/java/src/net/i2p/router/.nbattrs new file mode 100644 index 0000000000..f6b2b5abc4 --- /dev/null +++ b/router/java/src/net/i2p/router/.nbattrs @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/router/java/src/net/i2p/router/ClientManagerFacade.java b/router/java/src/net/i2p/router/ClientManagerFacade.java new file mode 100644 index 0000000000..048278f37e --- /dev/null +++ b/router/java/src/net/i2p/router/ClientManagerFacade.java @@ -0,0 +1,95 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.client.ClientManagerFacadeImpl; + +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.SessionConfig; + +import net.i2p.data.Destination; +import net.i2p.data.LeaseSet; +import net.i2p.data.Hash; + +/** + * Manage all interactions with clients + * + * @author jrandom + */ +public abstract class ClientManagerFacade implements Service { + private static ClientManagerFacade _instance = new ClientManagerFacadeImpl(); + public static ClientManagerFacade getInstance() { return _instance; } + + /** + * Request that a particular client authorize the Leases contained in the + * LeaseSet, after which the onCreateJob is queued up. If that doesn't occur + * within the timeout specified, queue up the onFailedJob. This call does not + * block. + * + * @param dest Destination from which the LeaseSet's authorization should be requested + * @param set LeaseSet with requested leases - this object must be updated to contain the + * signed version (as well as any changed/added/removed Leases) + * @param timeout ms to wait before failing + * @param onCreateJob Job to run after the LeaseSet is authorized + * @param onFailedJob Job to run after the timeout passes without receiving authorization + */ + public abstract void requestLeaseSet(Destination dest, LeaseSet set, long timeout, Job onCreateJob, Job onFailedJob); + /** + * Instruct the client (or all clients) that they are under attack. This call + * does not block. + * + * @param dest Destination under attack, or null if all destinations are affected + * @param reason Why the router thinks that there is abusive behavior + * @param severity How severe the abuse is, with 0 being not severe and 255 is the max + */ + public abstract void reportAbuse(Destination dest, String reason, int severity); + /** + * Determine if the destination specified is managed locally. This call + * DOES block. + * + * @param dest Destination to be checked + */ + public abstract boolean isLocal(Destination dest); + /** + * Determine if the destination hash specified is managed locally. This call + * DOES block. + * + * @param destHash Hash of Destination to be checked + */ + public abstract boolean isLocal(Hash destHash); + public abstract void messageDeliveryStatusUpdate(Destination fromDest, MessageId id, boolean delivered); + + public abstract void messageReceived(ClientMessage msg); + + /** + * Return the client's current config, or null if not connected + * + */ + public abstract SessionConfig getClientSessionConfig(Destination dest); + public String renderStatusHTML() { return ""; } +} + +class DummyClientManagerFacade extends ClientManagerFacade { + public boolean isLocal(Hash destHash) { return true; } + public boolean isLocal(Destination dest) { return true; } + public void reportAbuse(Destination dest, String reason, int severity) { } + public void messageReceived(ClientMessage msg) {} + public void requestLeaseSet(Destination dest, LeaseSet set, long timeout, Job onCreateJob, Job onFailedJob) { + JobQueue.getInstance().addJob(onFailedJob); + } + public void startup() { + //JobQueue.getInstance().addJob(new PollOutboundClientMessagesJob()); + } + public void stopAcceptingClients() { } + public void shutdown() {} + + public void messageDeliveryStatusUpdate(Destination fromDest, MessageId id, boolean delivered) {} + + public SessionConfig getClientSessionConfig(Destination _dest) { return null; } +} diff --git a/router/java/src/net/i2p/router/ClientMessage.java b/router/java/src/net/i2p/router/ClientMessage.java new file mode 100644 index 0000000000..46855b50ca --- /dev/null +++ b/router/java/src/net/i2p/router/ClientMessage.java @@ -0,0 +1,94 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Payload; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.i2cp.SessionConfig; +import net.i2p.data.i2cp.MessageId; + +/** + * Wrap a message either destined for a local client or received from one. + * + * @author jrandom + */ +public class ClientMessage { + private Payload _payload; + private Destination _destination; + private Destination _fromDestination; + private MessageReceptionInfo _receptionInfo; + private SessionConfig _senderConfig; + private Hash _destinationHash; + private MessageId _messageId; + + public ClientMessage() { + setPayload(null); + setDestination(null); + setFromDestination(null); + setReceptionInfo(null); + setSenderConfig(null); + setDestinationHash(null); + setMessageId(null); + } + + /** + * Retrieve the payload of the message. All ClientMessage objects should have + * a payload + * + */ + public Payload getPayload() { return _payload; } + public void setPayload(Payload payload) { _payload = payload; } + + /** + * Retrieve the destination to which this message is directed. All ClientMessage + * objects should have a destination. + * + */ + public Destination getDestination() { return _destination; } + public void setDestination(Destination dest) { _destination = dest; } + + /** + * + * + */ + public Destination getFromDestination() { return _fromDestination; } + public void setFromDestination(Destination dest) { _fromDestination = dest; } + + /** + * Retrieve the destination to which this message is directed. All ClientMessage + * objects should have a destination. + * + */ + public Hash getDestinationHash() { return _destinationHash; } + public void setDestinationHash(Hash dest) { _destinationHash = dest; } + + /** + * + */ + public MessageId getMessageId() { return _messageId; } + public void setMessageId(MessageId id) { _messageId = id; } + + /** + * Retrieve the information regarding how the router received this message. Only + * messages received from the network will have this information, not locally + * originated ones. + * + */ + public MessageReceptionInfo getReceptionInfo() { return _receptionInfo; } + public void setReceptionInfo(MessageReceptionInfo info) { _receptionInfo = info; } + + /** + * Retrieve the session config of the client that sent the message. This will only be available + * if the client was local + * + */ + public SessionConfig getSenderConfig() { return _senderConfig; } + public void setSenderConfig(SessionConfig config) { _senderConfig = config; } +} diff --git a/router/java/src/net/i2p/router/ClientMessagePool.java b/router/java/src/net/i2p/router/ClientMessagePool.java new file mode 100644 index 0000000000..3fc437a026 --- /dev/null +++ b/router/java/src/net/i2p/router/ClientMessagePool.java @@ -0,0 +1,126 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +//import net.i2p.router.message.ProcessOutboundClientMessageJob; +import net.i2p.router.message.OutboundClientMessageJob; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +import net.i2p.util.Log; + +/** + * Manage all of the inbound and outbound client messages maintained by the router. + * The ClientManager subsystem fetches messages from this for locally deliverable + * messages and adds in remotely deliverable messages. Remotely deliverable messages + * are picked up by interested jobs and processed and transformed into an OutNetMessage + * to be eventually placed in the OutNetMessagePool. + * + */ +public class ClientMessagePool { + private final static Log _log = new Log(ClientMessagePool.class); + private static ClientMessagePool _instance = new ClientMessagePool(); + public static final ClientMessagePool getInstance() { return _instance; } + private List _inMessages; + private List _outMessages; + + private ClientMessagePool() { + _inMessages = new ArrayList(); + _outMessages = new ArrayList(); + } + + /** + * Add a new message to the pool. The message can either be locally or + * remotely destined. + * + */ + public void add(ClientMessage msg) { + if ( (ClientManagerFacade.getInstance().isLocal(msg.getDestination())) || + (ClientManagerFacade.getInstance().isLocal(msg.getDestinationHash())) ) { + _log.debug("Adding message for local delivery"); + ClientManagerFacade.getInstance().messageReceived(msg); + //synchronized (_inMessages) { + // _inMessages.add(msg); + //} + } else { + _log.debug("Adding message for remote delivery"); + //JobQueue.getInstance().addJob(new ProcessOutboundClientMessageJob(msg)); + JobQueue.getInstance().addJob(new OutboundClientMessageJob(msg)); + //synchronized (_outMessages) { + // _outMessages.add(msg); + //} + } + } + + /** + * Retrieve the next locally destined message, or null if none are available. + * + */ + public ClientMessage getNextLocal() { + synchronized (_inMessages) { + if (_inMessages.size() <= 0) return null; + return (ClientMessage)_inMessages.remove(0); + } + } + + /** + * Retrieve the next remotely destined message, or null if none are available. + * + */ + public ClientMessage getNextRemote() { + synchronized (_outMessages) { + if (_outMessages.size() <= 0) return null; + return (ClientMessage)_outMessages.remove(0); + } + } + + /** + * Determine how many locally bound messages are in the pool + * + */ + public int getLocalCount() { + synchronized (_inMessages) { + return _inMessages.size(); + } + } + + /** + * Determine how many remotely bound messages are in the pool. + * + */ + public int getRemoteCount() { + synchronized (_outMessages) { + return _outMessages.size(); + } + } + + public void dumpPoolInfo() { + StringBuffer buf = new StringBuffer(); + buf.append("\nDumping Client Message Pool. Local messages: ").append(getLocalCount()).append(" Remote messages: ").append(getRemoteCount()).append("\n"); + buf.append("Inbound messages\n"); + buf.append("----------------------------\n"); + synchronized (_inMessages) { + for (Iterator iter = _inMessages.iterator(); iter.hasNext();) { + ClientMessage msg = (ClientMessage)iter.next(); + buf.append(msg).append("\n\n"); + } + } + buf.append("Outbound messages\n"); + buf.append("----------------------------\n"); + synchronized (_outMessages) { + for (Iterator iter = _outMessages.iterator(); iter.hasNext();) { + ClientMessage msg = (ClientMessage)iter.next(); + buf.append(msg).append("\n\n"); + } + } + _log.debug(buf.toString()); + } +} diff --git a/router/java/src/net/i2p/router/ClientTunnelSettings.java b/router/java/src/net/i2p/router/ClientTunnelSettings.java new file mode 100644 index 0000000000..69198c1e0b --- /dev/null +++ b/router/java/src/net/i2p/router/ClientTunnelSettings.java @@ -0,0 +1,175 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Properties; +import java.util.Iterator; + +/** + * Wrap up the client settings specifying their tunnel criteria + * + */ +public class ClientTunnelSettings { + private int _numInbound; + private int _numOutbound; + private int _depthInbound; + private int _depthOutbound; + private long _msgsPerMinuteAvgInbound; + private long _bytesPerMinuteAvgInbound; + private long _msgsPerMinutePeakInbound; + private long _bytesPerMinutePeakInbound; + private boolean _includeDummyInbound; + private boolean _includeDummyOutbound; + private boolean _reorderInbound; + private boolean _reorderOutbound; + private long _inboundDuration; + private boolean _enforceStrictMinimumLength; + + public final static String PROP_NUM_INBOUND = "tunnels.numInbound"; + public final static String PROP_NUM_OUTBOUND = "tunnels.numOutbound"; + public final static String PROP_DEPTH_INBOUND = "tunnels.depthInbound"; + public final static String PROP_DEPTH_OUTBOUND = "tunnels.depthOutbound"; + public final static String PROP_MSGS_AVG = "tunnels.messagesPerMinuteAverage"; + public final static String PROP_MSGS_PEAK = "tunnels.messagesPerMinutePeak"; + public final static String PROP_BYTES_AVG = "tunnels.bytesPerMinuteAverage"; + public final static String PROP_BYTES_PEAK = "tunnels.bytesPerMinutePeak"; + public final static String PROP_DUMMY_INBOUND = "tunnels.includeDummyTrafficInbound"; + public final static String PROP_DUMMY_OUTBOUND = "tunnels.includeDummyTrafficOutbound"; + public final static String PROP_REORDER_INBOUND = "tunnels.reorderInboundMessages"; + public final static String PROP_REORDER_OUTBOUND = "tunnels.reoderOutboundMessages"; + public final static String PROP_DURATION = "tunnels.tunnelDuration"; + /** + * if tunnels.strictMinimumLength=true then never accept a tunnel shorter than the client's + * request, otherwise we'll try to meet that minimum, but if we don't have any that length, + * we'll accept the longest we do have. + * + */ + public final static String PROP_STRICT_MINIMUM_LENGTH = "tunnels.enforceStrictMinimumLength"; + + public final static int DEFAULT_NUM_INBOUND = 2; + public final static int DEFAULT_NUM_OUTBOUND = 1; + public final static int DEFAULT_DEPTH_INBOUND = 2; + public final static int DEFAULT_DEPTH_OUTBOUND = 2; + public final static long DEFAULT_MSGS_AVG = 0; + public final static long DEFAULT_MSGS_PEAK = 0; + public final static long DEFAULT_BYTES_AVG = 0; + public final static long DEFAULT_BYTES_PEAK = 0; + public final static boolean DEFAULT_DUMMY_INBOUND = false; + public final static boolean DEFAULT_DUMMY_OUTBOUND = false; + public final static boolean DEFAULT_REORDER_INBOUND = false; + public final static boolean DEFAULT_REORDER_OUTBOUND = false; + public final static long DEFAULT_DURATION = 10*60*1000; + public final static boolean DEFAULT_STRICT_MINIMUM_LENGTH = true; + + public ClientTunnelSettings() { + _numInbound = 0; + _numOutbound = 0; + _depthInbound = 0; + _depthOutbound = 0; + _msgsPerMinuteAvgInbound = 0; + _bytesPerMinuteAvgInbound = 0; + _msgsPerMinutePeakInbound = 0; + _bytesPerMinutePeakInbound = 0; + _includeDummyInbound = false; + _includeDummyOutbound = false; + _reorderInbound = false; + _reorderOutbound = false; + _inboundDuration = -1; + _enforceStrictMinimumLength = false; + } + + public int getNumInboundTunnels() { return _numInbound; } + public int getNumOutboundTunnels() { return _numOutbound; } + public int getDepthInbound() { return _depthInbound; } + public int getDepthOutbound() { return _depthOutbound; } + public long getMessagesPerMinuteInboundAverage() { return _msgsPerMinuteAvgInbound; } + public long getMessagesPerMinuteInboundPeak() { return _msgsPerMinutePeakInbound; } + public long getBytesPerMinuteInboundAverage() { return _bytesPerMinuteAvgInbound; } + public long getBytesPerMinuteInboundPeak() { return _bytesPerMinutePeakInbound; } + public boolean getIncludeDummyInbound() { return _includeDummyInbound; } + public boolean getIncludeDummyOutbound() { return _includeDummyOutbound; } + public boolean getReorderInbound() { return _reorderInbound; } + public boolean getReorderOutbound() { return _reorderOutbound; } + public long getInboundDuration() { return _inboundDuration; } + public boolean getEnforceStrictMinimumLength() { return _enforceStrictMinimumLength; } + + public void setNumInboundTunnels(int num) { _numInbound = num; } + public void setNumOutboundTunnels(int num) { _numOutbound = num; } + public void setEnforceStrictMinimumLength(boolean enforce) { _enforceStrictMinimumLength = enforce; } + + public void readFromProperties(Properties props) { + _numInbound = getInt(props.getProperty(PROP_NUM_INBOUND), DEFAULT_NUM_INBOUND); + _numOutbound = getInt(props.getProperty(PROP_NUM_OUTBOUND), DEFAULT_NUM_OUTBOUND); + _depthInbound = getInt(props.getProperty(PROP_DEPTH_INBOUND), DEFAULT_DEPTH_INBOUND); + _depthOutbound = getInt(props.getProperty(PROP_DEPTH_OUTBOUND), DEFAULT_DEPTH_OUTBOUND); + _msgsPerMinuteAvgInbound = getLong(props.getProperty(PROP_MSGS_AVG), DEFAULT_MSGS_AVG); + _bytesPerMinuteAvgInbound = getLong(props.getProperty(PROP_MSGS_PEAK), DEFAULT_BYTES_AVG); + _msgsPerMinutePeakInbound = getLong(props.getProperty(PROP_BYTES_AVG), DEFAULT_MSGS_PEAK); + _bytesPerMinutePeakInbound = getLong(props.getProperty(PROP_BYTES_PEAK), DEFAULT_BYTES_PEAK); + _includeDummyInbound = getBoolean(props.getProperty(PROP_DUMMY_INBOUND), DEFAULT_DUMMY_INBOUND); + _includeDummyOutbound = getBoolean(props.getProperty(PROP_DUMMY_OUTBOUND), DEFAULT_DUMMY_OUTBOUND); + _reorderInbound = getBoolean(props.getProperty(PROP_REORDER_INBOUND), DEFAULT_REORDER_INBOUND); + _reorderOutbound = getBoolean(props.getProperty(PROP_REORDER_OUTBOUND), DEFAULT_REORDER_OUTBOUND); + _inboundDuration = getLong(props.getProperty(PROP_DURATION), DEFAULT_DURATION); + _enforceStrictMinimumLength = getBoolean(props.getProperty(PROP_STRICT_MINIMUM_LENGTH), DEFAULT_STRICT_MINIMUM_LENGTH); + } + + public void writeToProperties(Properties props) { + if (props == null) return; + props.setProperty(PROP_NUM_INBOUND, ""+_numInbound); + props.setProperty(PROP_NUM_OUTBOUND, ""+_numOutbound); + props.setProperty(PROP_DEPTH_INBOUND, ""+_depthInbound); + props.setProperty(PROP_DEPTH_OUTBOUND, ""+_depthOutbound); + props.setProperty(PROP_MSGS_AVG, ""+_msgsPerMinuteAvgInbound); + props.setProperty(PROP_MSGS_PEAK, ""+_msgsPerMinutePeakInbound); + props.setProperty(PROP_BYTES_AVG, ""+_bytesPerMinuteAvgInbound); + props.setProperty(PROP_BYTES_PEAK, ""+_bytesPerMinutePeakInbound); + props.setProperty(PROP_DUMMY_INBOUND, (_includeDummyInbound ? Boolean.TRUE.toString() : Boolean.FALSE.toString())); + props.setProperty(PROP_DUMMY_OUTBOUND, (_includeDummyOutbound ? Boolean.TRUE.toString() : Boolean.FALSE.toString())); + props.setProperty(PROP_REORDER_INBOUND, (_reorderInbound ? Boolean.TRUE.toString() : Boolean.FALSE.toString())); + props.setProperty(PROP_REORDER_OUTBOUND, (_reorderOutbound ? Boolean.TRUE.toString() : Boolean.FALSE.toString())); + props.setProperty(PROP_DURATION, ""+_inboundDuration); + props.setProperty(PROP_STRICT_MINIMUM_LENGTH, (_enforceStrictMinimumLength ? Boolean.TRUE.toString() : Boolean.FALSE.toString())); + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + Properties p = new Properties(); + writeToProperties(p); + buf.append("Client tunnel settings:\n"); + buf.append("====================================\n"); + for (Iterator iter = p.keySet().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + String val = p.getProperty(name); + buf.append(name).append(" = [").append(val).append("]\n"); + } + buf.append("====================================\n"); + return buf.toString(); + } + + //// + //// + + private static final boolean getBoolean(String str, boolean defaultValue) { + if (str == null) return defaultValue; + String s = str.toUpperCase(); + boolean v = "TRUE".equals(s) || "YES".equals(s); + return v; + } + private static final int getInt(String str, int defaultValue) { return (int)getLong(str, defaultValue); } + private static final long getLong(String str, long defaultValue) { + if (str == null) return defaultValue; + try { + long val = Long.parseLong(str); + return val; + } catch (NumberFormatException nfe) { + return defaultValue; + } + } +} diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java new file mode 100644 index 0000000000..bed9616731 --- /dev/null +++ b/router/java/src/net/i2p/router/CommSystemFacade.java @@ -0,0 +1,40 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.transport.CommSystemFacadeImpl; + +import java.util.HashSet; +import java.util.Set; + +/** + * Manages the communication subsystem between peers, including connections, + * listeners, transports, connection keys, etc. + * + */ +public abstract class CommSystemFacade implements Service { + private static CommSystemFacade _instance = new CommSystemFacadeImpl(); + public static CommSystemFacade getInstance() { return _instance; } + + // getAddresses + // rotateAddress(address) + + public abstract void processMessage(OutNetMessage msg); + + public String renderStatusHTML() { return ""; } + + /** Create the set of RouterAddress structures based on the router's config */ + public Set createAddresses() { return new HashSet(); } +} + +class DummyCommSystemFacade extends CommSystemFacade { + public void shutdown() {} + public void startup() {} + public void processMessage(OutNetMessage msg) { } +} diff --git a/router/java/src/net/i2p/router/GenerateStatusConsoleJob.java b/router/java/src/net/i2p/router/GenerateStatusConsoleJob.java new file mode 100644 index 0000000000..f0f3fee0c1 --- /dev/null +++ b/router/java/src/net/i2p/router/GenerateStatusConsoleJob.java @@ -0,0 +1,62 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.util.Log; + +import java.io.FileOutputStream; +import java.io.IOException; + +public class GenerateStatusConsoleJob extends JobImpl { + private final static Log _log = new Log(GenerateStatusConsoleJob.class); + + private final static long REGENERATE_DELAY_MS = 60*1000; // once per minute update the console + public final static String CONFIG_CONSOLE_LOCATION = "routerConsoleFile"; + public final static String DEFAULT_CONSOLE_LOCATION = "routerConsole.html"; + + public final static String PARAM_GENERATE_CONFIG_CONSOLE = "router.generateConsole"; + public final static boolean DEFAULT_GENERATE_CONFIG_CONSOLE = true; + + private boolean shouldGenerateConsole() { + String str = Router.getInstance().getConfigSetting(PARAM_GENERATE_CONFIG_CONSOLE); + if ( (str == null) || (str.trim().length() <= 0) ) + return DEFAULT_GENERATE_CONFIG_CONSOLE; + if (Boolean.TRUE.toString().equalsIgnoreCase(str)) + return true; + else + return false; + } + + public String getName() { return "Generate Status Console"; } + public void runJob() { + if (shouldGenerateConsole()) { + String consoleHTML = Router.getInstance().renderStatusHTML(); + writeConsole(consoleHTML); + } + requeue(REGENERATE_DELAY_MS); + } + + private void writeConsole(String html) { + String loc = Router.getInstance().getConfigSetting(CONFIG_CONSOLE_LOCATION); + if (loc == null) + loc = DEFAULT_CONSOLE_LOCATION; + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(loc); + fos.write(html.getBytes()); + fos.flush(); + } catch (IOException ioe) { + _log.error("Error writing out the console", ioe); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + } + +} diff --git a/router/java/src/net/i2p/router/HandlerJobBuilder.java b/router/java/src/net/i2p/router/HandlerJobBuilder.java new file mode 100644 index 0000000000..d7c7086e3d --- /dev/null +++ b/router/java/src/net/i2p/router/HandlerJobBuilder.java @@ -0,0 +1,35 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.SourceRouteBlock; +import net.i2p.data.RouterIdentity; +import net.i2p.data.Hash; + +/** + * Defines a class that builds jobs to handle a particular message - these + * builders are registered with the InNetMessagePool for various I2NP message + * types, allowing immediate queueing of a handler job rather than waiting for + * a polling job to come pick it up. + * + */ +public interface HandlerJobBuilder { + /** + * Create a new job to handle the received message. + * + * @param receivedMessage I2NP message received + * @param from router that sent the message (if available) + * @param fromHash hash of the routerIdentity of the router that sent the message (if available) + * @param replyBlock block with which a reply could be sent (if available) + * @return a job or null if no particular job is appropriate (in which case, + * the message should go into the inbound message pool) + */ + public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock); +} diff --git a/router/java/src/net/i2p/router/InNetMessage.java b/router/java/src/net/i2p/router/InNetMessage.java new file mode 100644 index 0000000000..16dfec99cd --- /dev/null +++ b/router/java/src/net/i2p/router/InNetMessage.java @@ -0,0 +1,68 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.SourceRouteBlock; +import net.i2p.data.Hash; +import net.i2p.data.RouterIdentity; + +/** + * Wrap an I2NP message received from the network prior to handling and processing. + * + */ +public class InNetMessage { + private I2NPMessage _message; + private RouterIdentity _fromRouter; + private Hash _fromRouterHash; + private SourceRouteBlock _replyBlock; + + public InNetMessage() { + setMessage(null); + setFromRouter(null); + setFromRouterHash(null); + setReplyBlock(null); + } + + /** + * Retrieve the message + * + */ + public I2NPMessage getMessage() { return _message; } + public void setMessage(I2NPMessage msg) { _message = msg; } + + /** + * Hash of the router identity from which this message was received, if availale + * + */ + public Hash getFromRouterHash() { return _fromRouterHash; } + public void setFromRouterHash(Hash routerIdentHash) { _fromRouterHash = routerIdentHash; } + + /** + * Router identity from which this message was received, if availale + * + */ + public RouterIdentity getFromRouter() { return _fromRouter; } + public void setFromRouter(RouterIdentity router) { _fromRouter = router; } + + /** + * Retrieve any source route block supplied with this message for replies + * + * @return source route block, or null if it was not supplied /or/ if it was already + * used in an ack + */ + public SourceRouteBlock getReplyBlock() { return _replyBlock; } + public void setReplyBlock(SourceRouteBlock block) { _replyBlock = block; } + + public String toString() { + StringBuffer buf = new StringBuffer(512); + buf.append("InNetMessage: from [").append(getFromRouter()).append("] aka [").append(getFromRouterHash()).append("] message: ").append(getMessage()); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/InNetMessagePool.java b/router/java/src/net/i2p/router/InNetMessagePool.java new file mode 100644 index 0000000000..cae83369d8 --- /dev/null +++ b/router/java/src/net/i2p/router/InNetMessagePool.java @@ -0,0 +1,181 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import net.i2p.router.transport.OutboundMessageRegistry; +import net.i2p.util.Log; +import net.i2p.stat.StatManager; + +/** + * Manage a pool of inbound InNetMessages. This pool is filled by the + * Network communication system when it receives messages, and various jobs + * periodically retrieve them for processing. + * + */ +public class InNetMessagePool { + private final static Log _log = new Log(InNetMessagePool.class); + private static InNetMessagePool _instance = new InNetMessagePool(); + public final static InNetMessagePool getInstance() { return _instance; } + private List _messages; + private Map _handlerJobBuilders; + + private InNetMessagePool() { + _messages = new ArrayList(); + _handlerJobBuilders = new HashMap(); + StatManager.getInstance().createFrequencyStat("inNetPool.dropped", "How frequently we drop a message", "InNetPool", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createFrequencyStat("inNetPool.duplicate", "How frequently we receive a duplicate message", "InNetPool", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + public HandlerJobBuilder registerHandlerJobBuilder(int i2npMessageType, HandlerJobBuilder builder) { + return (HandlerJobBuilder)_handlerJobBuilders.put(new Integer(i2npMessageType), builder); + } + + public HandlerJobBuilder unregisterHandlerJobBuilder(int i2npMessageType) { + return (HandlerJobBuilder)_handlerJobBuilders.remove(new Integer(i2npMessageType)); + } + + /** + * Add a new message to the pool, returning the number of messages in the + * pool so that the comm system can throttle inbound messages. If there is + * a HandlerJobBuilder for the inbound message type, the message is loaded + * into a job created by that builder and queued up for processing instead + * (though if the builder doesn't create a job, it is added to the pool) + * + */ + public int add(InNetMessage msg) { + Date exp = msg.getMessage().getMessageExpiration(); + boolean valid = MessageValidator.getInstance().validateMessage(msg.getMessage().getUniqueId(), exp.getTime()); + if (!valid) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Duplicate message received [" + msg.getMessage().getUniqueId() + " expiring on " + exp + "]: " + msg.getMessage().getClass().getName()); + StatManager.getInstance().updateFrequency("inNetPool.dropped"); + StatManager.getInstance().updateFrequency("inNetPool.duplicate"); + MessageHistory.getInstance().droppedOtherMessage(msg.getMessage()); + MessageHistory.getInstance().messageProcessingError(msg.getMessage().getUniqueId(), msg.getMessage().getClass().getName(), "Duplicate/expired"); + return -1; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Message received [" + msg.getMessage().getUniqueId() + " expiring on " + exp + "] is NOT a duplicate or exipired"); + } + + int size = -1; + int type = msg.getMessage().getType(); + HandlerJobBuilder builder = (HandlerJobBuilder)_handlerJobBuilders.get(new Integer(type)); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Add message to the inNetMessage pool - builder: " + builder + " message class: " + msg.getMessage().getClass().getName()); + + if (builder != null) { + Job job = builder.createJob(msg.getMessage(), msg.getFromRouter(), msg.getFromRouterHash(), msg.getReplyBlock()); + if (job != null) { + JobQueue.getInstance().addJob(job); + synchronized (_messages) { + size = _messages.size(); + } + } + } + + List origMessages = OutboundMessageRegistry.getInstance().getOriginalMessages(msg.getMessage()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Original messages for inbound message: " + origMessages.size()); + if (origMessages.size() > 1) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Orig: " + origMessages + " \nthe above are replies for: " + msg, new Exception("Multiple matches")); + } + + for (int i = 0; i < origMessages.size(); i++) { + OutNetMessage omsg = (OutNetMessage)origMessages.get(i); + ReplyJob job = omsg.getOnReplyJob(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Original message [" + i + "] " + omsg.getReplySelector() + " : " + omsg + ": reply job: " + job); + + if (job != null) { + job.setMessage(msg.getMessage()); + JobQueue.getInstance().addJob(job); + } + } + + + if (origMessages.size() <= 0) { + // not handled as a reply + if (size == -1) { + // was not handled via HandlerJobBuilder + MessageHistory.getInstance().droppedOtherMessage(msg.getMessage()); + if (_log.shouldLog(Log.ERROR)) + _log.error("Message " + msg.getMessage() + " was not handled by a HandlerJobBuilder - DROPPING: " + msg, new Exception("DROPPED MESSAGE")); + StatManager.getInstance().updateFrequency("inNetPool.dropped"); + //_log.error("Pending registry: \n" + OutboundMessageRegistry.getInstance().renderStatusHTML()); + } else { + String mtype = msg.getMessage().getClass().getName(); + MessageHistory.getInstance().receiveMessage(mtype, msg.getMessage().getUniqueId(), msg.getMessage().getMessageExpiration(), msg.getFromRouterHash(), true); + return size; + } + } + + String mtype = msg.getMessage().getClass().getName(); + MessageHistory.getInstance().receiveMessage(mtype, msg.getMessage().getUniqueId(), msg.getMessage().getMessageExpiration(), msg.getFromRouterHash(), true); + return size; + } + + /** + * Remove up to maxNumMessages InNetMessages from the pool and return them. + * + */ + public List getNext(int maxNumMessages) { + ArrayList msgs = new ArrayList(maxNumMessages); + synchronized (_messages) { + for (int i = 0; (i < maxNumMessages) && (_messages.size() > 0); i++) + msgs.add(_messages.remove(0)); + } + return msgs; + } + + /** + * Retrieve the next message + * + */ + public InNetMessage getNext() { + synchronized (_messages) { + if (_messages.size() <= 0) return null; + return (InNetMessage)_messages.remove(0); + } + } + + /** + * Retrieve the size of the pool + * + */ + public int getCount() { + synchronized (_messages) { + return _messages.size(); + } + } + + public void dumpPoolInfo() { + if (!_log.shouldLog(Log.DEBUG)) return; + + StringBuffer buf = new StringBuffer(); + buf.append("\nDumping Inbound Network Message Pool. Total # message: ").append(getCount()).append("\n"); + synchronized (_messages) { + for (Iterator iter = _messages.iterator(); iter.hasNext();) { + InNetMessage msg = (InNetMessage)iter.next(); + buf.append("Message ").append(msg.getMessage()).append("\n\n"); + } + } + _log.debug(buf.toString()); + } + +} diff --git a/router/java/src/net/i2p/router/Job.java b/router/java/src/net/i2p/router/Job.java new file mode 100644 index 0000000000..2a9f12a061 --- /dev/null +++ b/router/java/src/net/i2p/router/Job.java @@ -0,0 +1,41 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + + +/** + * Defines an executable task + * + */ +public interface Job { + /** + * Descriptive name of the task + */ + public String getName(); + /** unique id */ + public int getJobId(); + /** + * Timing criteria for the task + */ + public JobTiming getTiming(); + /** + * Actually perform the task. This call blocks until the Job is complete. + */ + public void runJob(); + + public Exception getAddedBy(); + + /** + * the router is extremely overloaded, so this job has been dropped. if for + * some reason the job *must* do some cleanup / requeueing of other tasks, it + * should do so here. + * + */ + public void dropped(); +} diff --git a/router/java/src/net/i2p/router/JobImpl.java b/router/java/src/net/i2p/router/JobImpl.java new file mode 100644 index 0000000000..b4b3bf29ac --- /dev/null +++ b/router/java/src/net/i2p/router/JobImpl.java @@ -0,0 +1,52 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.util.Clock; +/** + * Base implementation of a Job + */ +public abstract class JobImpl implements Job { + private JobTiming _timing; + private static int _idSrc = 0; + private int _id; + private Exception _addedBy; + private long _madeReadyOn; + + public JobImpl() { + _timing = new JobTiming(); + _id = ++_idSrc; + _addedBy = null; + _madeReadyOn = 0; + } + + public int getJobId() { return _id; } + public JobTiming getTiming() { return _timing; } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append(super.toString()); + buf.append(": Job ").append(_id).append(": ").append(getName()); + return buf.toString(); + } + + void addedToQueue() { + _addedBy = new Exception(); + } + + public Exception getAddedBy() { return _addedBy; } + public long getMadeReadyOn() { return _madeReadyOn; } + public void madeReady() { _madeReadyOn = Clock.getInstance().now(); } + public void dropped() {} + + protected void requeue(long delayMs) { + getTiming().setStartAfter(Clock.getInstance().now() + delayMs); + JobQueue.getInstance().addJob(this); + } +} diff --git a/router/java/src/net/i2p/router/JobQueue.java b/router/java/src/net/i2p/router/JobQueue.java new file mode 100644 index 0000000000..23e5efdba9 --- /dev/null +++ b/router/java/src/net/i2p/router/JobQueue.java @@ -0,0 +1,748 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.TreeMap; + +import net.i2p.router.message.HandleSourceRouteReplyMessageJob; +import net.i2p.router.networkdb.HandleDatabaseLookupMessageJob; +import net.i2p.router.tunnelmanager.HandleTunnelCreateMessageJob; +import net.i2p.router.tunnelmanager.RequestTunnelJob; +import net.i2p.stat.StatManager; +import net.i2p.util.Clock; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Manage the pending jobs according to whatever algorithm is appropriate, giving + * preference to earlier scheduled jobs. + * + */ +public class JobQueue { + private final static Log _log = new Log(JobQueue.class); + private static JobQueue _instance = new JobQueue(); + public static JobQueue getInstance() { return _instance; } + + /** Integer (runnerId) to JobQueueRunner for created runners */ + private static HashMap _queueRunners; + /** a counter to identify a job runner */ + private volatile static int _runnerId = 0; + /** list of jobs that are ready to run ASAP */ + private LinkedList _readyJobs; + /** list of jobs that are scheduled for running in the future */ + private LinkedList _timedJobs; + /** when true, don't run any new jobs or update any limits, etc */ + private boolean _paused; + /** job name to JobStat for that job */ + private static TreeMap _jobStats; + /** how many job queue runners can go concurrently */ + private int _maxRunners; + private QueuePumper _pumper; + /** will we allow the # job runners to grow beyond 1? */ + private boolean _allowParallelOperation; + /** have we been killed or are we alive? */ + private boolean _alive; + + /** default max # job queue runners operating */ + private final static int DEFAULT_MAX_RUNNERS = 1; + /** router.config parameter to override the max runners */ + private final static String PROP_MAX_RUNNERS = "router.maxJobRunners"; + + /** how frequently should we check and update the max runners */ + private final static long MAX_LIMIT_UPDATE_DELAY = 60*1000; + + /** if a job is this lagged, spit out a warning, but keep going */ + private long _lagWarning = DEFAULT_LAG_WARNING; + private final static long DEFAULT_LAG_WARNING = 5*1000; + private final static String PROP_LAG_WARNING = "router.jobLagWarning"; + + /** if a job is this lagged, the router is hosed, so shut it down */ + private long _lagFatal = DEFAULT_LAG_FATAL; + private final static long DEFAULT_LAG_FATAL = 30*1000; + private final static String PROP_LAG_FATAL = "router.jobLagFatal"; + + /** if a job takes this long to run, spit out a warning, but keep going */ + private long _runWarning = DEFAULT_RUN_WARNING; + private final static long DEFAULT_RUN_WARNING = 5*1000; + private final static String PROP_RUN_WARNING = "router.jobRunWarning"; + + /** if a job takes this long to run, the router is hosed, so shut it down */ + private long _runFatal = DEFAULT_RUN_FATAL; + private final static long DEFAULT_RUN_FATAL = 30*1000; + private final static String PROP_RUN_FATAL = "router.jobRunFatal"; + + /** don't enforce fatal limits until the router has been up for this long */ + private long _warmupTime = DEFAULT_WARMUP_TIME; + private final static long DEFAULT_WARMUP_TIME = 10*60*1000; + private final static String PROP_WARMUM_TIME = "router.jobWarmupTime"; + + /** max ready and waiting jobs before we start dropping 'em */ + private int _maxWaitingJobs = DEFAULT_MAX_WAITING_JOBS; + private final static int DEFAULT_MAX_WAITING_JOBS = 20; + private final static String PROP_MAX_WAITING_JOBS = "router.maxWaitingJobs"; + + static { + StatManager.getInstance().createRateStat("jobQueue.readyJobs", "How many ready and waiting jobs there are?", "JobQueue", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("jobQueue.droppedJobs", "How many jobs do we drop due to insane overload?", "JobQueue", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + /** + * queue runners wait on this whenever they're not doing anything, and + * this gets notified *once* whenever there are ready jobs + */ + private Object _runnerLock = new Object(); + + private JobQueue() { + _alive = true; + _readyJobs = new LinkedList(); + _timedJobs = new LinkedList(); + _queueRunners = new HashMap(); + _paused = false; + _jobStats = new TreeMap(); + _allowParallelOperation = false; + _pumper = new QueuePumper(); + I2PThread pumperThread = new I2PThread(_pumper); + pumperThread.setDaemon(true); + pumperThread.setName("QueuePumper"); + pumperThread.setPriority(I2PThread.MIN_PRIORITY); + pumperThread.start(); + } + + /** + * Enqueue the specified job + * + */ + public void addJob(Job job) { + if (job == null) return; + + if (job instanceof JobImpl) + ((JobImpl)job).addedToQueue(); + + boolean isReady = false; + long numReady = 0; + boolean alreadyExists = false; + synchronized (_readyJobs) { + if (_readyJobs.contains(job)) + alreadyExists = true; + numReady = _readyJobs.size(); + } + if (!alreadyExists) { + synchronized (_timedJobs) { + if (_timedJobs.contains(job)) + alreadyExists = true; + } + } + + StatManager.getInstance().addRateData("jobQueue.readyJobs", numReady, 0); + if (shouldDrop(job, numReady)) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Dropping job due to overload! # ready jobs: " + numReady + ": job = " + job); + job.dropped(); + StatManager.getInstance().addRateData("jobQueue.droppedJobs", 1, 1); + awaken(1); + return; + } + + if (!alreadyExists) { + if (job.getTiming().getStartAfter() <= Clock.getInstance().now()) { + // don't skew us - its 'start after' its been queued, or later + job.getTiming().setStartAfter(Clock.getInstance().now()); + if (job instanceof JobImpl) + ((JobImpl)job).madeReady(); + synchronized (_readyJobs) { + _readyJobs.add(job); + isReady = true; + } + } else { + synchronized (_timedJobs) { + _timedJobs.add(job); + } + } + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Not adding already enqueued job " + job.getName()); + } + + if (isReady) { + // wake up at most one runner + awaken(1); + } + + return; + } + + /** + * are we so overloaded that we should drop the given job? + * This is driven both by the numReady and waiting jobs, the type of job + * in question, and what the router's router.maxWaitingJobs config parameter + * is set to. + * + */ + private boolean shouldDrop(Job job, long numReady) { + if (_maxWaitingJobs <= 0) return false; // dont ever drop jobs + if (!_allowParallelOperation) return false; // dont drop during startup [duh] + Class cls = job.getClass(); + if (numReady > _maxWaitingJobs) { + + // heavy cpu load, plus we're allowed to be unreliable with these two + // [but garlics can contain our payloads, so lets not drop them] + //if (cls == HandleGarlicMessageJob.class) + // return true; + if (cls == HandleSourceRouteReplyMessageJob.class) + return true; + + // lets not try to drop too many tunnel messages... + //if (cls == HandleTunnelMessageJob.class) + // return true; + + // we don't really *need* to answer DB lookup messages + if (cls == HandleDatabaseLookupMessageJob.class) + return true; + + // tunnels are a bitch, but its dropped() builds a pair of fake ones just in case + if (cls == RequestTunnelJob.class) + return true; + + // if we're already this loaded, dont take more tunnels + if (cls == HandleTunnelCreateMessageJob.class) + return true; + } + return false; + } + + public void allowParallelOperation() { _allowParallelOperation = true; } + void shutdown() { _alive = false; } + boolean isAlive() { return _alive; } + + /** + * Blocking call to retrieve the next ready job + * + */ + Job getNext() { + while (_alive) { + while (_paused) { + try { Thread.sleep(30); } catch (InterruptedException ie) {} + } + Job rv = null; + int ready = 0; + synchronized (_readyJobs) { + ready = _readyJobs.size(); + if (ready > 0) + rv = (Job)_readyJobs.remove(0); + } + if (rv != null) { + // we found one, but there may be more, so wake up enough + // other runners + awaken(ready-1); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Using a ready job after waking up " + (ready-1) + " others"); + return rv; + } + try { + synchronized (_runnerLock) { + _runnerLock.wait(1000); + } + } catch (InterruptedException ie) {} + } + return null; + } + + /** + * Move newly ready timed jobs to the ready queue. Returns the + * number of ready jobs after the check is completed + * + */ + private int checkJobTimings() { + boolean newJobsReady = false; + long now = Clock.getInstance().now(); + LinkedList toAdd = new LinkedList(); + synchronized (_timedJobs) { + for (int i = 0; i < _timedJobs.size(); i++) { + Job j = (Job)_timedJobs.get(i); + // find jobs due to start before now + if (j.getTiming().getStartAfter() <= now) { + if (j instanceof JobImpl) + ((JobImpl)j).madeReady(); + + toAdd.add(j); + _timedJobs.remove(i); + i--; // so the index stays consistent + } + } + } + + int ready = 0; + synchronized (_readyJobs) { + _readyJobs.addAll(toAdd); + ready = _readyJobs.size(); + } + + return ready; + } + + /** + * Start up the queue with the specified number of concurrent processors. + * If this method has already been called, it will adjust the number of + * runners to meet the new number. This does not kill jobs running on + * excess threads, it merely instructs the threads to die after finishing + * the current job. + * + */ + public void runQueue(int numThreads) { + synchronized (_queueRunners) { + // we're still starting up [serially] and we've got at least one runner, + // so dont do anything + if ( (_queueRunners.size() > 0) && (!_allowParallelOperation) ) return; + + // we've already enabled parallel operation, so grow to however many are + // specified + if (_queueRunners.size() < numThreads) { + if (_log.shouldLog(Log.INFO)) + _log.info("Increasing the number of queue runners from " + _queueRunners.size() + " to " + numThreads); + for (int i = _queueRunners.size(); i < numThreads; i++) { + JobQueueRunner runner = new JobQueueRunner(i); + _queueRunners.put(new Integer(i), runner); + Thread t = new I2PThread(runner); + t.setName("JobQueue"+(_runnerId++)); + t.setDaemon(false); + t.start(); + } + } else if (_queueRunners.size() == numThreads) { + // noop + } else { // numThreads < # runners, so shrink + //for (int i = _queueRunners.size(); i > numThreads; i++) { + // QueueRunner runner = (QueueRunner)_queueRunners.get(new Integer(i)); + // runner.stopRunning(); + //} + } + } + } + + //public void pauseQueue() { _paused = true; } + //public void unpauseQueue() { _paused = false; } + void removeRunner(int id) { _queueRunners.remove(new Integer(id)); } + + + /** + * Notify a sufficient number of waiting runners, and if necessary, increase + * the number of runners (up to maxRunners) + * + */ + private void awaken(int numMadeReady) { + // notify a sufficient number of waiting runners + for (int i = 0; i < numMadeReady; i++) { + synchronized (_runnerLock) { + _runnerLock.notify(); + } + } + + int numRunners = 0; + synchronized (_queueRunners) { + numRunners = _queueRunners.size(); + } + + if (numRunners > 1) { + if (numMadeReady > numRunners) { + if (numMadeReady < _maxRunners) { + _log.info("Too much job contention (" + numMadeReady + " ready and waiting, " + numRunners + " runners exist), adding " + numMadeReady + " new runners (with max " + _maxRunners + ")"); + runQueue(numMadeReady); + } else { + _log.info("Too much job contention (" + numMadeReady + " ready and waiting, " + numRunners + " runners exist), increasing to our max of " + _maxRunners + " runners"); + runQueue(_maxRunners); + } + } + } + } + + /** + * Responsible for moving jobs from the timed queue to the ready queue, + * adjusting the number of queue runners, as well as periodically updating the + * max number of runners. + * + */ + private final class QueuePumper implements Runnable, Clock.ClockUpdateListener { + private long _lastLimitUpdated; + public QueuePumper() { + _lastLimitUpdated = 0; + Clock.getInstance().addUpdateListener(this); + } + public void run() { + try { + while (_alive) { + while (_paused) { + try { Thread.sleep(1000); } catch (InterruptedException ie) {} + } + + // periodically update our max runners limit + long now = Clock.getInstance().now(); + if (now > _lastLimitUpdated + MAX_LIMIT_UPDATE_DELAY) { + if (_log.shouldLog(Log.INFO)) + _log.info("Updating the limits"); + updateMaxLimit(); + updateTimingLimits(); + _lastLimitUpdated = now; + } + + // turn timed jobs into ready jobs + int numMadeReady = checkJobTimings(); + + awaken(numMadeReady); + + try { Thread.sleep(500); } catch (InterruptedException ie) {} + } + } catch (Throwable t) { + Clock.getInstance().removeUpdateListener(this); + if (_log.shouldLog(Log.ERROR)) + _log.error("wtf, pumper killed", t); + } + } + + public void offsetChanged(long delta) { + if (_lastLimitUpdated > 0) + _lastLimitUpdated += delta; + } + + } + + /** + * calculate and update the job timings + * if it was lagged too much or took too long to run, spit out + * a warning (and if its really excessive, kill the router) + */ + void updateStats(Job job, long doStart, long origStartAfter, long duration) { + String key = job.getName(); + long lag = doStart - origStartAfter; // how long were we ready and waiting? + MessageHistory hist = MessageHistory.getInstance(); + long uptime = Router.getInstance().getUptime(); + + synchronized (_jobStats) { + if (!_jobStats.containsKey(key)) + _jobStats.put(key, new JobStats(key)); + JobStats stats = (JobStats)_jobStats.get(key); + + stats.jobRan(duration, lag); + } + + String dieMsg = null; + boolean dumpRunners = false; + + if (lag > _lagWarning) { + dieMsg = "Lag too long for job " + job.getName() + " [" + lag + "ms and a run time of " + duration + "ms]"; + dumpRunners = true; + } else if (duration > _runWarning) { + dieMsg = "Job run too long for job " + job.getName() + " [" + lag + "ms lag and run time of " + duration + "ms]"; + dumpRunners = true; + } + + if (dieMsg != null) { + if (_log.shouldLog(Log.WARN)) + _log.warn(dieMsg); + if (hist != null) + hist.messageProcessingError(-1, JobQueue.class.getName(), dieMsg); + } + + if (dumpRunners) + dumpRunners(true); + + if ( (lag > _lagFatal) && (uptime > _warmupTime) ) { + // this is fscking bad - the network at this size shouldn't have this much real contention + // so we're going to DIE DIE DIE + if (_log.shouldLog(Log.WARN)) + _log.log(Log.WARN, "The router is either incredibly overloaded or (more likely) there's an error.", new Exception("ttttooooo mmmuuuccccchhhh llllaaagggg")); + //try { Thread.sleep(5000); } catch (InterruptedException ie) {} + //Router.getInstance().shutdown(); + return; + } + if ( (uptime > _warmupTime) && (duration > _runFatal) ) { + // slow CPUs can get hosed with ElGamal, but 10s is too much. + if (_log.shouldLog(Log.WARN)) + _log.log(Log.WARN, "The router is incredibly overloaded - either you have a 386, or (more likely) there's an error. ", new Exception("ttttooooo sssllloooowww")); + //try { Thread.sleep(5000); } catch (InterruptedException ie) {} + //Router.getInstance().shutdown(); + return; + } + } + + //// + // update config params + //// + + /** + * Update the max number of job queue runners + * + */ + private void updateMaxLimit() { + String str = Router.getInstance().getConfigSetting(PROP_MAX_RUNNERS); + if (str != null) { + try { + _maxRunners = Integer.parseInt(str); + return; + } catch (NumberFormatException nfe) { + _log.error("Invalid maximum job runners [" + str + "]"); + } + } + if (_log.shouldLog(Log.INFO)) + _log.info("Defaulting the maximum job runners to " + DEFAULT_MAX_RUNNERS); + _maxRunners = DEFAULT_MAX_RUNNERS; + } + + /** + * Update the job lag and run threshold for warnings and fatalities, as well + * as the warmup time before which fatalities will be ignored + * + */ + private void updateTimingLimits() { + String str = Router.getInstance().getConfigSetting(PROP_LAG_WARNING); + if (str != null) { + try { + _lagWarning = Integer.parseInt(str); + } catch (NumberFormatException nfe) { + _log.error("Invalid job lag warning [" + str + "]"); + _lagWarning = DEFAULT_LAG_WARNING; + } + } else { + _lagWarning = DEFAULT_LAG_WARNING; + } + if (_log.shouldLog(Log.INFO)) + _log.info("Setting the warning job lag time to " + _lagWarning + "ms"); + + str = Router.getInstance().getConfigSetting(PROP_LAG_FATAL); + if (str != null) { + try { + _lagFatal = Integer.parseInt(str); + } catch (NumberFormatException nfe) { + _log.error("Invalid job lag fatal [" + str + "]"); + _lagFatal = DEFAULT_LAG_FATAL; + } + } else { + _lagFatal = DEFAULT_LAG_FATAL; + } + if (_log.shouldLog(Log.INFO)) + _log.info("Setting the fatal job lag time to " + _lagFatal + "ms"); + + str = Router.getInstance().getConfigSetting(PROP_RUN_WARNING); + if (str != null) { + try { + _runWarning = Integer.parseInt(str); + } catch (NumberFormatException nfe) { + _log.error("Invalid job run warning [" + str + "]"); + _runWarning = DEFAULT_RUN_WARNING; + } + } else { + _runWarning = DEFAULT_RUN_WARNING; + } + if (_log.shouldLog(Log.INFO)) + _log.info("Setting the warning job run time to " + _runWarning + "ms"); + + str = Router.getInstance().getConfigSetting(PROP_RUN_FATAL); + if (str != null) { + try { + _runFatal = Integer.parseInt(str); + } catch (NumberFormatException nfe) { + _log.error("Invalid job run fatal [" + str + "]"); + _runFatal = DEFAULT_RUN_FATAL; + } + } else { + _runFatal = DEFAULT_RUN_FATAL; + } + if (_log.shouldLog(Log.INFO)) + _log.info("Setting the fatal job run time to " + _runFatal + "ms"); + + str = Router.getInstance().getConfigSetting(PROP_WARMUM_TIME); + if (str != null) { + try { + _warmupTime = Integer.parseInt(str); + } catch (NumberFormatException nfe) { + _log.error("Invalid warmup time [" + str + "]"); + _warmupTime = DEFAULT_WARMUP_TIME; + } + } else { + _warmupTime = DEFAULT_WARMUP_TIME; + } + + str = Router.getInstance().getConfigSetting(PROP_MAX_WAITING_JOBS); + if (str != null) { + try { + _maxWaitingJobs = Integer.parseInt(str); + } catch (NumberFormatException nfe) { + _log.error("Invalid max waiting jobs [" + str + "]"); + _maxWaitingJobs = DEFAULT_MAX_WAITING_JOBS; + } + } else { + _maxWaitingJobs = DEFAULT_MAX_WAITING_JOBS; + } + if (_log.shouldLog(Log.INFO)) + _log.info("Setting the max waiting jobs to " + _maxWaitingJobs); + } + + //// + // the remainder are utility methods for dumping status info + //// + + public String renderStatusHTML() { + LinkedList readyJobs = new LinkedList(); + LinkedList timedJobs = new LinkedList(); + LinkedList activeJobs = new LinkedList(); + synchronized (_readyJobs) { readyJobs.addAll(_readyJobs); } + synchronized (_timedJobs) { timedJobs.addAll(_timedJobs); } + synchronized (_queueRunners) { + for (Iterator iter = _queueRunners.values().iterator(); iter.hasNext();) { + JobQueueRunner runner = (JobQueueRunner)iter.next(); + Job job = runner.getCurrentJob(); + if (job != null) + activeJobs.add(job.getName()); + } + } + StringBuffer buf = new StringBuffer(); + buf.append("

JobQueue

"); + buf.append("# runners: "); + synchronized (_queueRunners) { + buf.append(_queueRunners.size()); + } + buf.append("
\n"); + buf.append("# active jobs: ").append(activeJobs.size()).append("
    \n"); + for (int i = 0; i < activeJobs.size(); i++) { + buf.append("
  1. ").append(activeJobs.get(i)).append("
  2. \n"); + } + buf.append("
\n"); + buf.append("# ready/waiting jobs: ").append(readyJobs.size()).append(" (lots of these mean there's likely a big problem)
    \n"); + for (int i = 0; i < readyJobs.size(); i++) { + buf.append("
  1. ").append(readyJobs.get(i)).append("
  2. \n"); + } + buf.append("
\n"); + + buf.append("# timed jobs: ").append(timedJobs.size()).append("
    \n"); + TreeMap ordered = new TreeMap(); + for (int i = 0; i < timedJobs.size(); i++) { + Job j = (Job)timedJobs.get(i); + ordered.put(new Long(j.getTiming().getStartAfter()), j); + } + for (Iterator iter = ordered.values().iterator(); iter.hasNext(); ) { + Job j = (Job)iter.next(); + buf.append("
  1. ").append(j.getName()).append(" @ ").append(new Date(j.getTiming().getStartAfter())).append("
  2. \n"); + } + buf.append("
\n"); + buf.append(getJobStats()); + return buf.toString(); + } + + /** render the HTML for the job stats */ + private String getJobStats() { + StringBuffer buf = new StringBuffer(1024); + buf.append("\n"); + buf.append(""); + buf.append(""); + buf.append("\n"); + long totRuns = 0; + long totExecTime = 0; + long avgExecTime = 0; + long maxExecTime = -1; + long minExecTime = -1; + long totPendingTime = 0; + long avgPendingTime = 0; + long maxPendingTime = -1; + long minPendingTime = -1; + + TreeMap tstats = null; + synchronized (_jobStats) { + tstats = (TreeMap)_jobStats.clone(); + } + + for (Iterator iter = tstats.values().iterator(); iter.hasNext(); ) { + JobStats stats = (JobStats)iter.next(); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append("\n"); + totRuns += stats.getRuns(); + totExecTime += stats.getTotalTime(); + if (stats.getMaxTime() > maxExecTime) + maxExecTime = stats.getMaxTime(); + if ( (minExecTime < 0) || (minExecTime > stats.getMinTime()) ) + minExecTime = stats.getMinTime(); + totPendingTime += stats.getTotalPendingTime(); + if (stats.getMaxPendingTime() > maxPendingTime) + maxPendingTime = stats.getMaxPendingTime(); + if ( (minPendingTime < 0) || (minPendingTime > stats.getMinPendingTime()) ) + minPendingTime = stats.getMinPendingTime(); + } + + if (totRuns != 0) { + if (totExecTime != 0) + avgExecTime = totExecTime / totRuns; + if (totPendingTime != 0) + avgPendingTime = totPendingTime / totRuns; + } + + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append("\n"); + + buf.append("
JobRunsTimeAvgMaxMinPendingAvgMaxMin
").append(stats.getName()).append("").append(stats.getRuns()).append("").append(stats.getTotalTime()).append("").append(stats.getAvgTime()).append("").append(stats.getMaxTime()).append("").append(stats.getMinTime()).append("").append(stats.getTotalPendingTime()).append("").append(stats.getAvgPendingTime()).append("").append(stats.getMaxPendingTime()).append("").append(stats.getMinPendingTime()).append("

").append("SUMMARY").append("").append(totRuns).append("").append(totExecTime).append("").append(avgExecTime).append("").append(maxExecTime).append("").append(minExecTime).append("").append(totPendingTime).append("").append(avgPendingTime).append("").append(maxPendingTime).append("").append(minPendingTime).append("
\n"); + return buf.toString(); + } + + /** + * Log what each queue runner is doing at the moment + * + */ + void dumpRunners() { dumpRunners(false); } + /** if asError, dump the job runners in an error message, else as a debug message */ + void dumpRunners(boolean asError) { + if (!asError && (!_log.shouldLog(Log.DEBUG)) ) return; + if (asError && (!_log.shouldLog(Log.WARN)) ) return; + StringBuffer buf = new StringBuffer(1024); + buf.append("Queue runners:\n"); + synchronized (_queueRunners) { + for (Iterator iter = _queueRunners.values().iterator(); iter.hasNext(); ) { + JobQueueRunner runner = (JobQueueRunner)iter.next(); + Job job = runner.getCurrentJob(); + int id = runner.getRunnerId(); + buf.append("* Runner ").append(id).append(": \t"); + if (job == null) + buf.append("no job\n"); + else + buf.append(job.getName()).append('\n'); + } + } + synchronized (_timedJobs) { + buf.append("** Timed jobs: \t").append(_timedJobs.size()).append('\n'); + } + synchronized (_readyJobs) { + buf.append("** Ready jobs: \t").append(_readyJobs.size()).append('\n'); + } + + if (asError) + _log.warn(buf.toString()); + else + _log.debug(buf.toString()); + } +} diff --git a/router/java/src/net/i2p/router/JobQueueRunner.java b/router/java/src/net/i2p/router/JobQueueRunner.java new file mode 100644 index 0000000000..0bc4157992 --- /dev/null +++ b/router/java/src/net/i2p/router/JobQueueRunner.java @@ -0,0 +1,109 @@ +package net.i2p.router; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import net.i2p.stat.StatManager; + +/** a do run run run a do run run */ +class JobQueueRunner implements Runnable { + private final static Log _log = new Log(JobQueueRunner.class); + private boolean _keepRunning; + private int _id; + private long _numJobs; + private Job _currentJob; + + static { + StatManager.getInstance().createRateStat("jobQueue.jobRun", "How long jobs take", "JobQueue", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("jobQueue.jobLag", "How long jobs have to wait before running", "JobQueue", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("jobQueue.jobWait", "How long does a job sat on the job queue?", "JobQueue", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("jobQueue.jobRunnerInactive", "How long are runners inactive?", "JobQueue", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + public JobQueueRunner(int id) { + _id = id; + _keepRunning = true; + _numJobs = 0; + _currentJob = null; + } + public Job getCurrentJob() { return _currentJob; } + public int getRunnerId() { return _id; } + public void stopRunning() { _keepRunning = false; } + public void run() { + long lastActive = Clock.getInstance().now();; + while ( (_keepRunning) && (JobQueue.getInstance().isAlive()) ) { + try { + Job job = JobQueue.getInstance().getNext(); + if (job == null) continue; + long now = Clock.getInstance().now(); + + long enqueuedTime = 0; + if (job instanceof JobImpl) { + long when = ((JobImpl)job).getMadeReadyOn(); + if (when <= 0) { + _log.error("Job was not made ready?! " + job, new Exception("Not made ready?!")); + } else { + enqueuedTime = now - when; + } + } + + long betweenJobs = now - lastActive; + StatManager.getInstance().addRateData("jobQueue.jobRunnerInactive", betweenJobs, betweenJobs); + _currentJob = job; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Runner " + _id + " running job " + job.getJobId() + ": " + job.getName()); + long origStartAfter = job.getTiming().getStartAfter(); + long doStart = Clock.getInstance().now(); + job.getTiming().start(); + runCurrentJob(); + job.getTiming().end(); + long duration = job.getTiming().getActualEnd() - job.getTiming().getActualStart(); + + long beforeUpdate = Clock.getInstance().now(); + JobQueue.getInstance().updateStats(job, doStart, origStartAfter, duration); + long diff = Clock.getInstance().now() - beforeUpdate; + + StatManager.getInstance().addRateData("jobQueue.jobRun", duration, duration); + StatManager.getInstance().addRateData("jobQueue.jobLag", doStart - origStartAfter, 0); + StatManager.getInstance().addRateData("jobQueue.jobWait", enqueuedTime, enqueuedTime); + + if (diff > 100) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Updating statistics for the job took too long [" + diff + "ms]"); + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Job duration " + duration + "ms for " + job.getName() + " with lag of " + (doStart-origStartAfter) + "ms"); + lastActive = Clock.getInstance().now(); + _currentJob = null; + } catch (Throwable t) { + if (_log.shouldLog(Log.CRIT)) + _log.log(Log.CRIT, "WTF, error running?", t); + } + } + if (_log.shouldLog(Log.CRIT)) + _log.log(Log.CRIT, "Queue runner " + _id + " exiting"); + JobQueue.getInstance().removeRunner(_id); + } + + private void runCurrentJob() { + try { + _currentJob.runJob(); + } catch (OutOfMemoryError oom) { + try { + if (_log.shouldLog(Log.CRIT)) + _log.log(Log.CRIT, "Router ran out of memory, shutting down", oom); + Router.getInstance().shutdown(); + } catch (Throwable t) { + System.err.println("***Router ran out of memory, shutting down hard"); + } + try { Thread.sleep(1000); } catch (InterruptedException ie) {} + System.exit(-1); + } catch (Throwable t) { + if (_log.shouldLog(Log.CRIT)) + _log.log(Log.CRIT, "Error processing job [" + _currentJob.getName() + "] on thread " + _id + ": " + t.getMessage(), t); + if (_log.shouldLog(Log.ERROR)) + _log.error("The above job was enqueued by: ", _currentJob.getAddedBy()); + JobQueue.getInstance().dumpRunners(true); + } + } +} diff --git a/router/java/src/net/i2p/router/JobStats.java b/router/java/src/net/i2p/router/JobStats.java new file mode 100644 index 0000000000..c71479a042 --- /dev/null +++ b/router/java/src/net/i2p/router/JobStats.java @@ -0,0 +1,75 @@ +package net.i2p.router; + +import net.i2p.data.DataHelper; + +/** glorified struct to contain basic job stats */ +class JobStats { + private String _job; + private long _numRuns; + private long _totalTime; + private long _maxTime; + private long _minTime; + private long _totalPendingTime; + private long _maxPendingTime; + private long _minPendingTime; + + public JobStats(String name) { + _job = name; + _numRuns = 0; + _totalTime = 0; + _maxTime = -1; + _minTime = -1; + _totalPendingTime = 0; + _maxPendingTime = -1; + _minPendingTime = -1; + } + + public void jobRan(long runTime, long lag) { + _numRuns++; + _totalTime += runTime; + if ( (_maxTime < 0) || (runTime > _maxTime) ) + _maxTime = runTime; + if ( (_minTime < 0) || (runTime < _minTime) ) + _minTime = runTime; + _totalPendingTime += lag; + if ( (_maxPendingTime < 0) || (lag > _maxPendingTime) ) + _maxPendingTime = lag; + if ( (_minPendingTime < 0) || (lag < _minPendingTime) ) + _minPendingTime = lag; + } + + public String getName() { return _job; } + public long getRuns() { return _numRuns; } + public long getTotalTime() { return _totalTime; } + public long getMaxTime() { return _maxTime; } + public long getMinTime() { return _minTime; } + public long getAvgTime() { if (_numRuns > 0) return _totalTime / _numRuns; else return 0; } + public long getTotalPendingTime() { return _totalPendingTime; } + public long getMaxPendingTime() { return _maxPendingTime; } + public long getMinPendingTime() { return _minPendingTime; } + public long getAvgPendingTime() { if (_numRuns > 0) return _totalPendingTime / _numRuns; else return 0; } + + public int hashCode() { return _job.hashCode(); } + public boolean equals(Object obj) { + if ( (obj != null) && (obj instanceof JobStats) ) { + JobStats stats = (JobStats)obj; + return DataHelper.eq(getName(), stats.getName()) && + getRuns() == stats.getRuns() && + getTotalTime() == stats.getTotalTime() && + getMaxTime() == stats.getMaxTime() && + getMinTime() == stats.getMinTime(); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("Over ").append(getRuns()).append(" runs, job ").append(getName()).append(" took "); + buf.append(getTotalTime()).append("ms (").append(getAvgTime()).append("ms/").append(getMaxTime()).append("ms/"); + buf.append(getMinTime()).append("ms avg/max/min) after a total lag of "); + buf.append(getTotalPendingTime()).append("ms (").append(getAvgPendingTime()).append("ms/"); + buf.append(getMaxPendingTime()).append("ms/").append(getMinPendingTime()).append("ms avg/max/min)"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/JobTiming.java b/router/java/src/net/i2p/router/JobTiming.java new file mode 100644 index 0000000000..ab11e16f7c --- /dev/null +++ b/router/java/src/net/i2p/router/JobTiming.java @@ -0,0 +1,69 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.util.Clock; +/** + * Define the timing requirements and statistics for a particular job + * + */ +public class JobTiming implements Clock.ClockUpdateListener { + private long _start; + private long _actualStart; + private long _actualEnd; + + public JobTiming() { + _start = Clock.getInstance().now(); + _actualStart = 0; + _actualEnd = 0; + Clock.getInstance().addUpdateListener(this); + } + + /** + * # of milliseconds after the epoch to start the job + * + */ + public long getStartAfter() { return _start; } + public void setStartAfter(long startTime) { _start = startTime; } + + /** + * # of milliseconds after the epoch the job actually started + * + */ + public long getActualStart() { return _actualStart; } + public void setActualStart(long actualStartTime) { _actualStart = actualStartTime; } + /** + * Notify the timing that the job began + * + */ + public void start() { _actualStart = Clock.getInstance().now(); } + /** + * # of milliseconds after the epoch the job actually ended + * + */ + public long getActualEnd() { return _actualEnd; } + public void setActualEnd(long actualEndTime) { _actualEnd = actualEndTime; } + /** + * Notify the timing that the job finished + * + */ + public void end() { + _actualEnd = Clock.getInstance().now(); + Clock.getInstance().removeUpdateListener(this); + } + + public void offsetChanged(long delta) { + if (_start != 0) + _start += delta; + if (_actualStart != 0) + _actualStart += delta; + if (_actualEnd != 0) + _actualEnd += delta; + } +} diff --git a/router/java/src/net/i2p/router/KeyManager.java b/router/java/src/net/i2p/router/KeyManager.java new file mode 100644 index 0000000000..cf25aa21c7 --- /dev/null +++ b/router/java/src/net/i2p/router/KeyManager.java @@ -0,0 +1,190 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataStructure; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * Maintain all of the key pairs for the router. + * + */ +public class KeyManager { + private final static Log _log = new Log(KeyManager.class); + private static KeyManager _instance = new KeyManager(); + public static KeyManager getInstance() { return _instance; } + private PrivateKey _privateKey; + private PublicKey _publicKey; + private SigningPrivateKey _signingPrivateKey; + private SigningPublicKey _signingPublicKey; + private Map _leaseSetKeys; // Destination --> LeaseSetKeys + + public final static String PROP_KEYDIR = "router.keyBackupDir"; + public final static String DEFAULT_KEYDIR = "keyBackup"; + private final static String KEYFILE_PRIVATE_ENC = "privateEncryption.key"; + private final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key"; + private final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key"; + private final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key"; + private final static long DELAY = 30*1000; + + private KeyManager() { + setPrivateKey(null); + setPublicKey(null); + setSigningPrivateKey(null); + setSigningPublicKey(null); + _leaseSetKeys = new HashMap(); + JobQueue.getInstance().addJob(new SynchronizeKeysJob()); + } + + /** Configure the router's private key */ + public void setPrivateKey(PrivateKey key) { _privateKey = key; } + public PrivateKey getPrivateKey() { return _privateKey; } + /** Configure the router's public key */ + public void setPublicKey(PublicKey key) { _publicKey = key; } + public PublicKey getPublicKey() { return _publicKey; } + /** Configure the router's signing private key */ + public void setSigningPrivateKey(SigningPrivateKey key) { _signingPrivateKey = key; } + public SigningPrivateKey getSigningPrivateKey() { return _signingPrivateKey; } + /** Configure the router's signing public key */ + public void setSigningPublicKey(SigningPublicKey key) { _signingPublicKey = key; } + public SigningPublicKey getSigningPublicKey() { return _signingPublicKey; } + + public void registerKeys(Destination dest, SigningPrivateKey leaseRevocationPrivateKey, PrivateKey endpointDecryptionKey) { + _log.info("Registering keys for destination " + dest.calculateHash().toBase64()); + LeaseSetKeys keys = new LeaseSetKeys(dest, leaseRevocationPrivateKey, endpointDecryptionKey); + synchronized (_leaseSetKeys) { + _leaseSetKeys.put(dest, keys); + } + } + + public LeaseSetKeys unregisterKeys(Destination dest) { + _log.info("Unregistering keys for destination " + dest.calculateHash().toBase64()); + synchronized (_leaseSetKeys) { + return (LeaseSetKeys)_leaseSetKeys.remove(dest); + } + } + + public LeaseSetKeys getKeys(Destination dest) { + synchronized (_leaseSetKeys) { + return (LeaseSetKeys)_leaseSetKeys.get(dest); + } + } + + public Set getAllKeys() { + HashSet keys = new HashSet(); + synchronized (_leaseSetKeys) { + keys.addAll(_leaseSetKeys.values()); + } + return keys; + } + + private class SynchronizeKeysJob extends JobImpl { + public void runJob() { + String keyDir = Router.getInstance().getConfigSetting(PROP_KEYDIR); + if (keyDir == null) + keyDir = DEFAULT_KEYDIR; + File dir = new File(keyDir); + if (!dir.exists()) + dir.mkdirs(); + if (dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite()) + syncKeys(dir); + + getTiming().setStartAfter(Clock.getInstance().now()+DELAY); + JobQueue.getInstance().addJob(this); + } + + private void syncKeys(File keyDir) { + syncPrivateKey(keyDir); + syncPublicKey(keyDir); + syncSigningKey(keyDir); + syncVerificationKey(keyDir); + } + + private void syncPrivateKey(File keyDir) { + File keyFile = new File(keyDir, KeyManager.KEYFILE_PRIVATE_ENC); + boolean exists = (_privateKey != null); + if (!exists) + _privateKey = new PrivateKey(); + _privateKey = (PrivateKey)syncKey(keyFile, _privateKey, exists); + } + private void syncPublicKey(File keyDir) { + File keyFile = new File(keyDir, KeyManager.KEYFILE_PUBLIC_ENC); + boolean exists = (_publicKey != null); + if (!exists) + _publicKey = new PublicKey(); + _publicKey = (PublicKey)syncKey(keyFile, _publicKey, exists); + } + + private void syncSigningKey(File keyDir) { + File keyFile = new File(keyDir, KeyManager.KEYFILE_PRIVATE_SIGNING); + boolean exists = (_signingPrivateKey != null); + if (!exists) + _signingPrivateKey = new SigningPrivateKey(); + _signingPrivateKey = (SigningPrivateKey)syncKey(keyFile, _signingPrivateKey, exists); + } + private void syncVerificationKey(File keyDir) { + File keyFile = new File(keyDir, KeyManager.KEYFILE_PUBLIC_SIGNING); + boolean exists = (_signingPublicKey != null); + if (!exists) + _signingPublicKey = new SigningPublicKey(); + _signingPublicKey = (SigningPublicKey)syncKey(keyFile, _signingPublicKey, exists); + } + + private DataStructure syncKey(File keyFile, DataStructure structure, boolean exists) { + FileOutputStream out = null; + FileInputStream in = null; + try { + if (exists) { + out = new FileOutputStream(keyFile); + structure.writeBytes(out); + return structure; + } else { + if (keyFile.exists()) { + in = new FileInputStream(keyFile); + structure.readBytes(in); + return structure; + } else { + // we don't have it, and its not on disk. oh well. + return null; + } + } + } catch (IOException ioe) { + _log.error("Error syncing the structure to " + keyFile.getAbsolutePath(), ioe); + } catch (DataFormatException dfe) { + _log.error("Error syncing the structure with " + keyFile.getAbsolutePath(), dfe); + } finally { + if (out != null) try { out.close(); } catch (IOException ioe) {} + if (in != null) try { in.close(); } catch (IOException ioe) {} + } + + if (exists) + return structure; + else + return null; + } + + public String getName() { return "Synchronize Keys to Disk"; } + } +} diff --git a/router/java/src/net/i2p/router/LeaseSetKeys.java b/router/java/src/net/i2p/router/LeaseSetKeys.java new file mode 100644 index 0000000000..c88b0808af --- /dev/null +++ b/router/java/src/net/i2p/router/LeaseSetKeys.java @@ -0,0 +1,94 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKey; +import net.i2p.data.SigningPrivateKey; + +/** + * Wrap up the keys given to the router when a destination connects to it + * + */ +public class LeaseSetKeys extends DataStructureImpl { + private Destination _dest; + private SigningPrivateKey _revocationKey; + private PrivateKey _decryptionKey; + + public LeaseSetKeys() { + this(null, null, null); + } + public LeaseSetKeys(Destination dest, SigningPrivateKey revocationKey, PrivateKey decryptionKey) { + _dest = dest; + _revocationKey = revocationKey; + _decryptionKey = decryptionKey; + } + + /** + * Destination in question + */ + public Destination getDestination() { return _dest; } + /** + * Key with which a LeaseSet can be revoked (by republishing it with no Leases) + * + */ + public SigningPrivateKey getRevocationKey() { return _revocationKey; } + /** + * Decryption key which can open up garlic messages encrypted to the + * LeaseSet's public key. This is used because the general public does not + * know on what router the destination is connected and as such can't encrypt + * to that router's normal public key. + * + */ + public PrivateKey getDecryptionKey() { return _decryptionKey; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _dest = new Destination(); + _dest.readBytes(in); + _decryptionKey = new PrivateKey(); + _decryptionKey.readBytes(in); + _revocationKey = new SigningPrivateKey(); + _revocationKey.readBytes(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_dest == null) throw new DataFormatException("Null destination"); + if (_decryptionKey == null) throw new DataFormatException("Null decryption key"); + if (_revocationKey == null) throw new DataFormatException("Null revocation key"); + _dest.writeBytes(out); + _decryptionKey.writeBytes(out); + _revocationKey.writeBytes(out); + } + + public int hashCode() { + int rv = 0; + rv += DataHelper.hashCode(_dest); + rv += DataHelper.hashCode(_revocationKey); + rv += DataHelper.hashCode(_decryptionKey); + return rv; + } + + public boolean equals(Object obj) { + if ( (obj != null) && (obj instanceof LeaseSetKeys) ) { + LeaseSetKeys keys = (LeaseSetKeys)obj; + return DataHelper.eq(getDestination(), keys.getDestination()) && + DataHelper.eq(getDecryptionKey(), keys.getDecryptionKey()) && + DataHelper.eq(getRevocationKey(), keys.getRevocationKey()); + } else { + return false; + } + } +} diff --git a/router/java/src/net/i2p/router/MessageHistory.java b/router/java/src/net/i2p/router/MessageHistory.java new file mode 100644 index 0000000000..6aabd113b6 --- /dev/null +++ b/router/java/src/net/i2p/router/MessageHistory.java @@ -0,0 +1,549 @@ +package net.i2p.router; + +import java.util.List; +import java.util.LinkedList; +import java.util.Iterator; + +import java.util.Date; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +import net.i2p.data.TunnelId; +import net.i2p.data.Hash; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.io.IOException; +import java.io.FileOutputStream; + +/** + * Simply act as a pen register of messages sent in and out of the router. + * This will be pulled out later on, but is useful now for debugging. + * (with clock synchronization, this will generate a log that can be used to + * analyze the entire network, if everyone provides their logs honestly) + * + */ +public class MessageHistory { + private final static Log _log = new Log(MessageHistory.class); + private static MessageHistory _instance; + private List _unwrittenEntries; // list of raw entries (strings) yet to be written + private String _historyFile; // where to write + private String _localIdent; // placed in each entry to uniquely identify the local router + private boolean _doLog; // true == we want to log + private boolean _doPause; // true == briefly stop writing data to the log (used while submitting it) + + private final static byte[] NL = System.getProperty("line.separator").getBytes(); + private final static int FLUSH_SIZE = 1000; // write out at least once every 1000 entries + + /** config property determining whether we want to debug with the message history */ + public final static String PROP_KEEP_MESSAGE_HISTORY = "router.keepHistory"; + public final static boolean DEFAULT_KEEP_MESSAGE_HISTORY = false; + /** config property determining where we want to log the message history, if we're keeping one */ + public final static String PROP_MESSAGE_HISTORY_FILENAME = "router.historyFilename"; + public final static String DEFAULT_MESSAGE_HISTORY_FILENAME = "messageHistory.txt"; + + public final static MessageHistory getInstance() { + if (_instance == null) + initialize(); + return _instance; + } + private final static void setInstance(MessageHistory hist) { + if (_instance != null) { + synchronized (_instance._unwrittenEntries) { + for (Iterator iter = _instance._unwrittenEntries.iterator(); iter.hasNext(); ) { + hist.addEntry((String)iter.next()); + } + _instance._unwrittenEntries.clear(); + } + } + _instance = hist; + } + + void setDoLog(boolean log) { _doLog = log; } + boolean getDoLog() { return _doLog; } + + void setPauseFlushes(boolean doPause) { _doPause = doPause; } + String getFilename() { return _historyFile; } + + private void updateSettings() { + String keepHistory = Router.getInstance().getConfigSetting(PROP_KEEP_MESSAGE_HISTORY); + if (keepHistory != null) { + _doLog = Boolean.TRUE.toString().equalsIgnoreCase(keepHistory); + } else { + _doLog = DEFAULT_KEEP_MESSAGE_HISTORY; + } + + String filename = null; + if (_doLog) { + filename = Router.getInstance().getConfigSetting(PROP_MESSAGE_HISTORY_FILENAME); + if ( (filename == null) || (filename.trim().length() <= 0) ) + filename = DEFAULT_MESSAGE_HISTORY_FILENAME; + } + } + + /** + * Initialize the message history according to the router's configuration. + * Call this whenever the router identity changes. + * + */ + public static void initialize() { + initialize(false); + } + public static void initialize(boolean forceReinitialize) { + if ( (!forceReinitialize) && (_instance != null) ) return; + + if (Router.getInstance().getRouterInfo() == null) { + ReinitializeJob j = ReinitializeJob.getInstance(); + j.getTiming().setStartAfter(Clock.getInstance().now()+5000); + JobQueue.getInstance().addJob(j); + } else { + String filename = null; + filename = Router.getInstance().getConfigSetting(PROP_MESSAGE_HISTORY_FILENAME); + if ( (filename == null) || (filename.trim().length() <= 0) ) + filename = DEFAULT_MESSAGE_HISTORY_FILENAME; + MessageHistory hist = new MessageHistory(Router.getInstance().getRouterInfo().getIdentity().getHash(), filename); + setInstance(hist); + hist.updateSettings(); + getInstance().addEntry(getInstance().getPrefix() + "** Router initialized (started up or changed identities)"); + JobQueue.getInstance().addJob(new WriteJob()); + SubmitMessageHistoryJob histJob = new SubmitMessageHistoryJob(); + histJob.getTiming().setStartAfter(Clock.getInstance().now() + 2*60*1000); + JobQueue.getInstance().addJob(histJob); + } + } + + private static final class ReinitializeJob extends JobImpl { + private final static ReinitializeJob _jobInstance = new ReinitializeJob(); + public final static ReinitializeJob getInstance() { return _jobInstance; } + private ReinitializeJob() { + super(); + } + public void runJob() { + MessageHistory.initialize(); + } + public String getName() { return "Reinitialize message history"; } + } + + /** + * Create a component to monitor the message history of the router. + * + * @param localIdent Hash of local identity + * @param filename file to log trace info to + */ + private MessageHistory(Hash localIdent, String filename) { + _doLog = DEFAULT_KEEP_MESSAGE_HISTORY; + _historyFile = filename; + _localIdent = getName(localIdent); + _unwrittenEntries = new LinkedList(); + } + + /** + * We are requesting that the peerRequested create the tunnel specified with the + * given nextPeer, and we are sending that request to them through outTunnel with + * a request that the reply is sent back to us through replyTunnel on the given + * replyThrough router. + * + * @param createTunnel tunnel being created + * @param outTunnel tunnel we are sending this request out + * @param peerRequested peer asked to participate in the tunnel + * @param nextPeer who peerRequested should forward messages to (or null if it is the endpoint) + * @param sourceRoutePeer to whom peerRequested should forward its TunnelCreateStatusMessage through + * @param replyTunnel the tunnel sourceRoutePeer should forward the source routed message to + * @param replyThrough the gateway of the tunnel that the sourceRoutePeer will be sending to + */ + public void requestTunnelCreate(TunnelId createTunnel, TunnelId outTunnel, Hash peerRequested, Hash nextPeer, Hash sourceRoutePeer, TunnelId replyTunnel, Hash replyThrough) { + if (!_doLog) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("request [").append(getName(peerRequested)).append("] to create tunnel ["); + buf.append(createTunnel.getTunnelId()).append("] "); + if (nextPeer != null) + buf.append("(next [").append(getName(nextPeer)).append("]) "); + if (outTunnel != null) + buf.append("via [").append(outTunnel.getTunnelId()).append("] "); + if (sourceRoutePeer != null) + buf.append("with replies routed through [").append(getName(sourceRoutePeer)).append("] "); + if ( (replyTunnel != null) && (replyThrough != null) ) + buf.append("who forwards it through [").append(replyTunnel.getTunnelId()).append("] on [").append(getName(replyThrough)).append("]"); + addEntry(buf.toString()); + } + + /** + * The local router has received a request to join the createTunnel with the next hop being nextPeer, + * and we should send our decision to join it through sourceRoutePeer + * + * @param createTunnel tunnel being joined + * @param nextPeer next hop in the tunnel (or null if this is the endpoint) + * @param expire when this tunnel expires + * @param ok whether we will join the tunnel + * @param sourceRoutePeer peer through whom we should send our garlic routed ok through + */ + public void receiveTunnelCreate(TunnelId createTunnel, Hash nextPeer, Date expire, boolean ok, Hash sourceRoutePeer) { + if (!_doLog) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("receive tunnel create [").append(createTunnel.getTunnelId()).append("] "); + if (nextPeer != null) + buf.append("(next [").append(getName(nextPeer)).append("]) "); + buf.append("ok? ").append(ok).append(" expiring on [").append(getTime(expire)).append("]"); + addEntry(buf.toString()); + } + + /** + * The local router has joined the given tunnel operating in the given state. + * + * @param state {"free inbound", "allocated inbound", "inactive inbound", "outbound", "participant", "pending"} + * @param tunnel tunnel joined + */ + public void tunnelJoined(String state, TunnelInfo tunnel) { + if (!_doLog) return; + if (tunnel == null) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("joining tunnel [").append(tunnel.getTunnelId().getTunnelId()).append("] as [").append(state).append("] "); + buf.append(" (next: "); + TunnelInfo cur = tunnel; + while (cur.getNextHopInfo() != null) { + buf.append('[').append(getName(cur.getNextHopInfo().getThisHop())); + buf.append("], "); + cur = cur.getNextHopInfo(); + } + if (cur.getNextHop() != null) + buf.append('[').append(getName(cur.getNextHop())).append(']'); + buf.append(") expiring on [").append(getTime(new Date(tunnel.getSettings().getExpiration()))).append("]"); + addEntry(buf.toString()); + } + + /** + * The local router has detected a failure in the given tunnel + * + * @param tunnel tunnel failed + */ + public void tunnelFailed(TunnelId tunnel) { + if (!_doLog) return; + if (tunnel == null) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("failing tunnel [").append(tunnel.getTunnelId()).append("]"); + addEntry(buf.toString()); + } + + /** + * Note that we have reason to believe that the given tunnel is valid, since we could do something + * through it in the given amount of time + * + * @param tunnel tunnel in question + * @param timeToTest milliseconds to verify the tunnel + */ + public void tunnelValid(TunnelInfo tunnel, long timeToTest) { + if (!_doLog) return; + if (tunnel == null) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("tunnel ").append(tunnel.getTunnelId().getTunnelId()).append(" tested ok after ").append(timeToTest).append("ms (containing "); + TunnelInfo cur = tunnel; + while (cur != null) { + buf.append('[').append(getName(cur.getThisHop())).append("], "); + if (cur.getNextHopInfo() != null) { + cur = cur.getNextHopInfo(); + } else { + if (cur.getNextHop() != null) + buf.append('[').append(getName(cur.getNextHop())).append(']'); + cur = null; + } + } + buf.append(')'); + addEntry(buf.toString()); + } + + /** + * The peer did not accept the tunnel join for the given reason + * + */ + public void tunnelRejected(Hash peer, TunnelId tunnel, Hash replyThrough, String reason) { + if (!_doLog) return; + if ( (tunnel == null) || (peer == null) ) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("tunnel [").append(tunnel.getTunnelId()).append("] was rejected by ["); + buf.append(getName(peer)).append("] for [").append(reason).append("]"); + if (replyThrough != null) + buf.append(" with their reply intended to come through [").append(getName(replyThrough)).append("]"); + addEntry(buf.toString()); + } + + /** + * The peer did not accept the tunnel join for the given reason (this may be because + * of a timeout or an explicit refusal). + * + */ + public void tunnelRequestTimedOut(Hash peer, TunnelId tunnel, Hash replyThrough) { + if (!_doLog) return; + if ( (tunnel == null) || (peer == null) ) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("tunnel [").append(tunnel.getTunnelId()).append("] timed out on ["); + buf.append(getName(peer)).append("]"); + if (replyThrough != null) + buf.append(" with their reply intended to come through [").append(getName(replyThrough)).append("]"); + addEntry(buf.toString()); + } + + /** + * We don't know about the given tunnel, so we are dropping a message sent to us by the + * given router + * + * @param id tunnel ID we received a message for + * @param from peer that sent us this message (if known) + */ + public void droppedTunnelMessage(TunnelId id, Hash from) { + if (!_doLog) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("dropped message for unknown tunnel [").append(id.getTunnelId()).append("] from [").append(getName(from)).append("]"); + addEntry(buf.toString()); + } + + /** + * We received another message we weren't waiting for and don't know how to handle + */ + public void droppedOtherMessage(I2NPMessage message) { + if (!_doLog) return; + if (message == null) return; + StringBuffer buf = new StringBuffer(512); + buf.append(getPrefix()); + buf.append("dropped [").append(message.getClass().getName()).append("] ").append(message.getUniqueId()); + buf.append(" [").append(message.toString()).append("]"); + addEntry(buf.toString()); + } + + /** + * The message wanted a reply but no reply came in the time expected + * + * @param sentMessage message sent that didn't receive a reply + */ + public void replyTimedOut(OutNetMessage sentMessage) { + if (!_doLog) return; + if (sentMessage == null) return; + StringBuffer buf = new StringBuffer(512); + buf.append(getPrefix()); + buf.append("timed out waiting for a reply to [").append(sentMessage.getMessage().getClass().getName()); + buf.append("] [").append(sentMessage.getMessage().getUniqueId()).append("] expiring on ["); + if (sentMessage != null) + buf.append(getTime(new Date(sentMessage.getReplySelector().getExpiration()))); + buf.append("] ").append(sentMessage.getReplySelector().toString()); + addEntry(buf.toString()); + } + + /** + * There was an error processing the given message that was received + * + * @param messageId message received + * @param messageType type of message received + * @param error error message related to the processing of the message + */ + public void messageProcessingError(long messageId, String messageType, String error) { + if (!_doLog) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("Error processing [").append(messageType).append("] [").append(messageId).append("] failed with [").append(error).append("]"); + addEntry(buf.toString()); + } + + /** + * We just sent a message to the peer + * + * @param messageType class name for the message object (e.g. DatabaseFindNearestMessage, TunnelMessage, etc) + * @param messageId the unique message id of the message being sent (not including any tunnel or garlic wrapped + * message ids) + * @param expiration the expiration for the message sent + * @param peer router that the message was sent to + * @param sentOk whether the message was sent successfully + */ + public void sendMessage(String messageType, long messageId, Date expiration, Hash peer, boolean sentOk) { + if (!_doLog) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("send [").append(messageType).append("] message [").append(messageId).append("] "); + buf.append("to [").append(getName(peer)).append("] "); + buf.append("expiring on [").append(getTime(expiration)).append("] "); + if (sentOk) + buf.append("successfully"); + else + buf.append("failed"); + addEntry(buf.toString()); + } + + /** + * We just received a message from the peer + * + * @param messageType class name for the message object (e.g. DatabaseFindNearestMessage, TunnelMessage, etc) + * @param messageId the unique message id of the message received (not including any tunnel or garlic wrapped + * message ids) + * @param expiration the expiration for the message received + * @param from router that the message was sent from (or null if we don't know) + * @param isValid whether the message is valid (non duplicates, etc) + * + */ + public void receiveMessage(String messageType, long messageId, Date expiration, Hash from, boolean isValid) { + if (!_doLog) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("receive [").append(messageType).append("] with id [").append(messageId).append("] "); + if (from != null) + buf.append("from [").append(getName(from)).append("] "); + buf.append("expiring on [").append(getTime(expiration)).append("] valid? ").append(isValid); + addEntry(buf.toString()); + if (messageType.equals("net.i2p.data.i2np.TunnelMessage")) { + //_log.warn("ReceiveMessage tunnel message ["+messageId+"]", new Exception("Receive tunnel")); + } + } + public void receiveMessage(String messageType, long messageId, Date expiration, boolean isValid) { + receiveMessage(messageType, messageId, expiration, null, isValid); + } + + /** + * Note that we're wrapping the given message within another message (via tunnel/garlic) + * + * @param bodyMessageType class name for the message contained (e.g. DatabaseFindNearestMessage, DataMessage, etc) + * @param bodyMessageId the unique message id of the message + * @param containerMessageType class name for the message containing the body message (e.g. TunnelMessage, GarlicMessage, etc) + * @param containerMessageId the unique message id of the message + */ + public void wrap(String bodyMessageType, long bodyMessageId, String containerMessageType, long containerMessageId) { + if (!_doLog) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("Wrap message [").append(bodyMessageType).append("] id [").append(bodyMessageId).append("] "); + buf.append("in [").append(containerMessageType).append("] id [").append(containerMessageId).append("]"); + addEntry(buf.toString()); + } + + /** + * Receive a payload message to distribute to a client + * + */ + public void receivePayloadMessage(long messageId) { + if (!_doLog) return; + StringBuffer buf = new StringBuffer(64); + buf.append(getPrefix()); + buf.append("Receive payload message [").append(messageId).append("]"); + addEntry(buf.toString()); + } + + /** + * Note that the sending of a payload message completed (successfully or as a failure) + * + * @param messageId message that the payload message was sent in + * @param successfullySent whether the message was delivered to the peer successfully + * @param timeToSend how long it took to send the message + */ + public void sendPayloadMessage(long messageId, boolean successfullySent, long timeToSend) { + if (!_doLog) return; + StringBuffer buf = new StringBuffer(128); + buf.append(getPrefix()); + buf.append("Send payload message in [").append(messageId).append("] in [").append(timeToSend).append("] successfully? ").append(successfullySent); + addEntry(buf.toString()); + } + + /** + * Prettify the hash by doing a base64 and returning the first 6 characters + * + */ + private final static String getName(Hash router) { + if (router == null) return "unknown"; + String str = router.toBase64(); + if ( (str == null) || (str.length() < 6) ) return "invalid"; + return str.substring(0, 6); + } + + private final String getPrefix() { + StringBuffer buf = new StringBuffer(48); + buf.append(getTime(new Date(Clock.getInstance().now()))); + buf.append(' ').append(_localIdent).append(": "); + return buf.toString(); + } + + private final static SimpleDateFormat _fmt = new SimpleDateFormat("yy/MM/dd.HH:mm:ss.SSS"); + static { + _fmt.setTimeZone(TimeZone.getTimeZone("GMT")); + } + private final static String getTime(Date when) { + synchronized (_fmt) { + return _fmt.format(when); + } + } + + /** + * Responsible for adding the entry, flushing if necessary. + * This is the only thing that adds to _unwrittenEntries. + * + */ + private void addEntry(String entry) { + if (entry == null) return; + int sz = 0; + synchronized (_unwrittenEntries) { + _unwrittenEntries.add(entry); + sz = _unwrittenEntries.size(); + } + if (sz > FLUSH_SIZE) + flushEntries(); + } + + /** + * Write out any unwritten entries, and clear the pending list + */ + private void flushEntries() { + if (_doPause) return; + List entries = null; + synchronized (_unwrittenEntries) { + entries = new LinkedList(_unwrittenEntries); + _unwrittenEntries.clear(); + } + writeEntries(entries); + } + + /** + * Actually write the specified entries + * + */ + private void writeEntries(List entries) { + if (!_doLog) return; + FileOutputStream fos = null; + try { + fos = new FileOutputStream(_historyFile, true); + for (Iterator iter = entries.iterator(); iter.hasNext(); ) { + String entry = (String)iter.next(); + fos.write(entry.getBytes()); + fos.write(NL); + } + } catch (IOException ioe) { + _log.error("Error writing trace entries", ioe); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + } + + /** write out the message history once per minute, if not sooner */ + private final static long WRITE_DELAY = 60*1000; + private static class WriteJob extends JobImpl { + public String getName() { return "Write History Entries"; } + public void runJob() { + MessageHistory.getInstance().flushEntries(); + MessageHistory.getInstance().updateSettings(); + requeue(WRITE_DELAY); + } + } + + public static void main(String args[]) { + MessageHistory hist = new MessageHistory(new Hash(new byte[32]), "messageHistory.txt"); + MessageHistory.getInstance().setDoLog(false); + hist.addEntry("you smell before"); + hist.getInstance().setDoLog(true); + hist.addEntry("you smell after"); + hist.getInstance().setDoLog(false); + hist.addEntry("you smell finished"); + hist.flushEntries(); + } +} diff --git a/router/java/src/net/i2p/router/MessageReceptionInfo.java b/router/java/src/net/i2p/router/MessageReceptionInfo.java new file mode 100644 index 0000000000..9b5fb509ed --- /dev/null +++ b/router/java/src/net/i2p/router/MessageReceptionInfo.java @@ -0,0 +1,33 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Hash; +import net.i2p.data.TunnelId; + +/** + * Wrap up the details of how a ClientMessage was received from the network + * + */ +public class MessageReceptionInfo { + private Hash _fromPeer; + private TunnelId _fromTunnel; + + public MessageReceptionInfo() { + setFromPeer(null); + setFromTunnel(null); + } + + /** Hash of the RouterIdentity of the peer that sent the message */ + public Hash getFromPeer() { return _fromPeer; } + public void setFromPeer(Hash routerIdentityHash) { _fromPeer = routerIdentityHash; } + /** TunnelId the message came in on, if applicable */ + public TunnelId getFromTunnel() { return _fromTunnel; } + public void setFromTunnel(TunnelId fromTunnel) { _fromTunnel = fromTunnel; } +} diff --git a/router/java/src/net/i2p/router/MessageSelector.java b/router/java/src/net/i2p/router/MessageSelector.java new file mode 100644 index 0000000000..ef35485c6f --- /dev/null +++ b/router/java/src/net/i2p/router/MessageSelector.java @@ -0,0 +1,34 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.i2np.I2NPMessage; + +/** + * Define a mechanism to select what messages are associated with a particular + * OutNetMessage. This is used for finding replies to messages. + * + */ +public interface MessageSelector { + /** + * Returns true if the received message matches the selector + */ + public boolean isMatch(I2NPMessage message); + /** + * Returns true if the selector should still keep searching for further matches + * + */ + public boolean continueMatching(); + /** + * Returns the # of milliseconds since the epoch after which this selector should + * stop searching for matches + * + */ + public long getExpiration(); +} diff --git a/router/java/src/net/i2p/router/MessageValidator.java b/router/java/src/net/i2p/router/MessageValidator.java new file mode 100644 index 0000000000..918fdf5aaf --- /dev/null +++ b/router/java/src/net/i2p/router/MessageValidator.java @@ -0,0 +1,126 @@ +package net.i2p.router; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.TreeMap; +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; + +/** + * Singleton to manage the logic (and historical data) to determine whether a message + * is valid or not (meaning it isn't expired and hasn't already been received). We'll + * need a revamp once we start dealing with long message expirations (since it might + * involve keeping a significant number of entries in memory), but that probably won't + * be necessary until I2P 3.0. + * + */ +public class MessageValidator { + private final static Log _log = new Log(MessageValidator.class); + private final static MessageValidator _instance = new MessageValidator(); + public final static MessageValidator getInstance() { return _instance; } + + /** + * Expiration date (as a Long) to message id (as a Long). + * The expiration date (key) must be unique, so on collision, increment the value. + * This keeps messageIds around longer than they need to be, but hopefully not by much ;) + * + */ + private TreeMap _receivedIdExpirations = new TreeMap(); + /** Message id (as a Long) */ + private Set _receivedIds = new HashSet(1024); + /** synchronize on this before adjusting the received id data */ + private Object _receivedIdLock = new Object(); + + /** + * Determine if this message should be accepted as valid (not expired, not a duplicate) + * + * @return true if the message should be accepted as valid, false otherwise + */ + public boolean validateMessage(long messageId, long expiration) { + long now = Clock.getInstance().now(); + if (now - Router.CLOCK_FUDGE_FACTOR >= expiration) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Rejecting message " + messageId + " because it expired " + (now-expiration) + "ms ago"); + return false; + } + + boolean isDuplicate = noteReception(messageId, expiration); + if (isDuplicate) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Rejecting message " + messageId + " because it is a duplicate", new Exception("Duplicate origin")); + return false; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Accepting message " + messageId + " because it is NOT a duplicate", new Exception("Original origin")); + return true; + } + } + + /** + * Note that we've received the message (which has the expiration given). + * This functionality will need to be reworked for I2P 3.0 when we take into + * consideration messages with significant user specified delays (since we dont + * want to keep an infinite number of messages in RAM, etc) + * + * @return true if we HAVE already seen this message, false if not + */ + private boolean noteReception(long messageId, long messageExpiration) { + Long id = new Long(messageId); + synchronized (_receivedIdLock) { + locked_cleanReceivedIds(Clock.getInstance().now() - Router.CLOCK_FUDGE_FACTOR); + if (_receivedIds.contains(id)) { + return true; + } else { + long date = messageExpiration; + while (_receivedIdExpirations.containsKey(new Long(date))) + date++; + _receivedIdExpirations.put(new Long(date), id); + _receivedIds.add(id); + return false; + } + } + } + + /** + * Clean the ids that we no longer need to keep track of to prevent replay + * attacks. + * + */ + private void cleanReceivedIds() { + long now = Clock.getInstance().now() - Router.CLOCK_FUDGE_FACTOR ; + synchronized (_receivedIdLock) { + locked_cleanReceivedIds(now); + } + } + + /** + * Clean the ids that we no longer need to keep track of to prevent replay + * attacks - only call this from within a block synchronized on the received ID lock. + * + */ + private void locked_cleanReceivedIds(long now) { + Set toRemoveIds = new HashSet(4); + Set toRemoveDates = new HashSet(4); + for (Iterator iter = _receivedIdExpirations.keySet().iterator(); iter.hasNext(); ) { + Long date = (Long)iter.next(); + if (date.longValue() <= now) { + // no need to keep track of things in the past + toRemoveDates.add(date); + toRemoveIds.add(_receivedIdExpirations.get(date)); + } else { + // the expiration is in the future, we still need to keep track of + // it to prevent replays + break; + } + } + for (Iterator iter = toRemoveDates.iterator(); iter.hasNext(); ) + _receivedIdExpirations.remove(iter.next()); + for (Iterator iter = toRemoveIds.iterator(); iter.hasNext(); ) + _receivedIds.remove(iter.next()); + if (_log.shouldLog(Log.INFO)) + _log.info("Cleaned out " + toRemoveDates.size() + " expired messageIds, leaving " + _receivedIds.size() + " remaining"); + } + +} diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java new file mode 100644 index 0000000000..7e78f2ae24 --- /dev/null +++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java @@ -0,0 +1,89 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.RouterInfo; +import net.i2p.router.networkdb.kademlia.KademliaNetworkDatabaseFacade; + +/** + * Defines the mechanism for interacting with I2P's network database + * + */ +public abstract class NetworkDatabaseFacade implements Service { + private static NetworkDatabaseFacade _instance = new KademliaNetworkDatabaseFacade(); // NetworkDatabaseFacadeImpl(); + public static NetworkDatabaseFacade getInstance() { return _instance; } + + /** + * Return the RouterInfo structures for the routers closest to the given key. + * At most maxNumRouters will be returned + * + * @param key The key + * @param maxNumRouters The maximum number of routers to return + * @param peersToIgnore Hash of routers not to include + */ + public abstract Set findNearestRouters(Hash key, int maxNumRouters, Set peersToIgnore); + + public abstract void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs); + public abstract LeaseSet lookupLeaseSetLocally(Hash key); + public abstract void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs); + public abstract RouterInfo lookupRouterInfoLocally(Hash key); + /** return the leaseSet if another leaseSet already existed at that key */ + public abstract LeaseSet store(Hash key, LeaseSet leaseSet); + /** return the routerInfo if another router already existed at that key */ + public abstract RouterInfo store(Hash key, RouterInfo routerInfo); + public abstract void publish(RouterInfo localRouterInfo); + public abstract void publish(LeaseSet localLeaseSet); + public abstract void unpublish(LeaseSet localLeaseSet); + public abstract void fail(Hash dbEntry); + public String renderStatusHTML() { return ""; } +} + + +class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade { + private Map _routers; + + public DummyNetworkDatabaseFacade() { + _routers = new HashMap(); + } + + public void shutdown() {} + public void startup() { + RouterInfo info = Router.getInstance().getRouterInfo(); + _routers.put(info.getIdentity().getHash(), info); + } + + public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {} + public LeaseSet lookupLeaseSetLocally(Hash key) { return null; } + public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) { + RouterInfo info = lookupRouterInfoLocally(key); + if (info == null) + JobQueue.getInstance().addJob(onFailedLookupJob); + else + JobQueue.getInstance().addJob(onFindJob); + } + public RouterInfo lookupRouterInfoLocally(Hash key) { return (RouterInfo)_routers.get(key); } + public void publish(LeaseSet localLeaseSet) {} + public void publish(RouterInfo localRouterInfo) {} + public LeaseSet store(Hash key, LeaseSet leaseSet) { return leaseSet; } + public RouterInfo store(Hash key, RouterInfo routerInfo) { + _routers.put(key, routerInfo); + return routerInfo; + } + public void unpublish(LeaseSet localLeaseSet) {} + public void fail(Hash dbEntry) {} + + public Set findNearestRouters(Hash key, int maxNumRouters, Set peersToIgnore) { return new HashSet(_routers.values()); } +} diff --git a/router/java/src/net/i2p/router/OutNetMessage.java b/router/java/src/net/i2p/router/OutNetMessage.java new file mode 100644 index 0000000000..4e982f6f71 --- /dev/null +++ b/router/java/src/net/i2p/router/OutNetMessage.java @@ -0,0 +1,272 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.RouterInfo; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * Wrap up an outbound I2NP message, along with the information associated with its + * delivery and jobs to be fired off if particular events occur. + * + */ +public class OutNetMessage { + private final static Log _log = new Log(OutNetMessage.class); + private RouterInfo _target; + private I2NPMessage _message; + private long _messageSize; + private int _priority; + private long _expiration; + private Job _onSend; + private Job _onFailedSend; + private ReplyJob _onReply; + private Job _onFailedReply; + private MessageSelector _replySelector; + private Set _failedTransports; + private long _sendBegin; + private Exception _createdBy; + private long _created; + /** for debugging, contains a mapping of even name to Long (e.g. "begin sending", "handleOutbound", etc) */ + private HashMap _timestamps; + /** + * contains a list of timestamp event names in the order they were fired + * (some JVMs have less than 10ms resolution, so the Long above doesn't guarantee order) + */ + private List _timestampOrder; + + public OutNetMessage() { + setTarget(null); + _message = null; + _messageSize = 0; + setPriority(-1); + setExpiration(-1); + setOnSendJob(null); + setOnFailedSendJob(null); + setOnReplyJob(null); + setOnFailedReplyJob(null); + setReplySelector(null); + _timestamps = new HashMap(8); + _timestampOrder = new LinkedList(); + _failedTransports = new HashSet(); + _sendBegin = 0; + _createdBy = new Exception("Created by"); + _created = Clock.getInstance().now(); + timestamp("Created"); + } + + public void timestamp(String eventName) { + synchronized (_timestamps) { + _timestamps.put(eventName, new Long(Clock.getInstance().now())); + _timestampOrder.add(eventName); + } + } + public Map getTimestamps() { + synchronized (_timestamps) { + return (Map)_timestamps.clone(); + } + } + public Long getTimestamp(String eventName) { + synchronized (_timestamps) { + return (Long)_timestamps.get(eventName); + } + } + + public Exception getCreatedBy() { return _createdBy; } + + /** + * Specifies the router to which the message should be delivered. + * + */ + public RouterInfo getTarget() { return _target; } + public void setTarget(RouterInfo target) { _target = target; } + /** + * Specifies the message to be sent + * + */ + public I2NPMessage getMessage() { return _message; } + public void setMessage(I2NPMessage msg) { + _message = msg; + } + + public long getMessageSize() { + if (_messageSize <= 0) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); // large enough to hold most messages + _message.writeBytes(baos); + long sz = baos.size(); + baos.reset(); + _messageSize = sz; + } catch (DataFormatException dfe) { + _log.error("Error serializing the I2NPMessage for the OutNetMessage", dfe); + } catch (IOException ioe) { + _log.error("Error serializing the I2NPMessage for the OutNetMessage", ioe); + } + } + return _messageSize; + } + public byte[] getMessageData() { + if (_message == null) { + return null; + } else { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); // large enough to hold most messages + _message.writeBytes(baos); + byte data[] = baos.toByteArray(); + baos.reset(); + return data; + } catch (DataFormatException dfe) { + _log.error("Error serializing the I2NPMessage for the OutNetMessage", dfe); + } catch (IOException ioe) { + _log.error("Error serializing the I2NPMessage for the OutNetMessage", ioe); + } + return null; + } + } + + /** + * Specify the priority of the message, where higher numbers are higher + * priority. Higher priority messages should be delivered before lower + * priority ones, though some algorithm may be used to avoid starvation. + * + */ + public int getPriority() { return _priority; } + public void setPriority(int priority) { _priority = priority; } + /** + * Specify the # ms since the epoch after which if the message has not been + * sent the OnFailedSend job should be fired and the message should be + * removed from the pool. If the message has already been sent, this + * expiration is ignored and the expiration from the ReplySelector is used. + * + */ + public long getExpiration() { return _expiration; } + public void setExpiration(long expiration) { _expiration = expiration; } + /** + * After the message is successfully passed to the router specified, the + * given job is enqueued. + * + */ + public Job getOnSendJob() { return _onSend; } + public void setOnSendJob(Job job) { _onSend = job; } + /** + * If the router could not be reached or the expiration passed, this job + * is enqueued. + * + */ + public Job getOnFailedSendJob() { return _onFailedSend; } + public void setOnFailedSendJob(Job job) { _onFailedSend = job; } + /** + * If the MessageSelector detects a reply, this job is enqueued + * + */ + public ReplyJob getOnReplyJob() { return _onReply; } + public void setOnReplyJob(ReplyJob job) { _onReply = job; } + /** + * If the Message selector is specified but it doesn't find a reply before + * its expiration passes, this job is enqueued. + */ + public Job getOnFailedReplyJob() { return _onFailedReply; } + public void setOnFailedReplyJob(Job job) { _onFailedReply = job; } + /** + * Defines a MessageSelector to find a reply to this message. + * + */ + public MessageSelector getReplySelector() { return _replySelector; } + public void setReplySelector(MessageSelector selector) { _replySelector = selector; } + + public void transportFailed(String transportStyle) { _failedTransports.add(transportStyle); } + public Set getFailedTransports() { return new HashSet(_failedTransports); } + + /** when did the sending process begin */ + public long getSendBegin() { return _sendBegin; } + public void beginSend() { _sendBegin = Clock.getInstance().now(); } + + public long getCreated() { return _created; } + public long getLifetime() { return Clock.getInstance().now() - _created; } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append("[OutNetMessage contains "); + if (_message == null) { + buf.append("*no message*"); + } else { + buf.append("a ").append(_messageSize).append(" byte "); + buf.append(_message.getClass().getName()); + } + buf.append(" expiring on ").append(new Date(_expiration)); + buf.append(" failed delivery on transports ").append(_failedTransports); + if (_target == null) + buf.append(" targetting no one in particular..."); + else + buf.append(" targetting ").append(_target.getIdentity().getHash().toBase64()); + if (_onReply != null) + buf.append(" with onReply job: ").append(_onReply); + if (_onSend != null) + buf.append(" with onSend job: ").append(_onSend); + if (_onFailedReply != null) + buf.append(" with onFailedReply job: ").append(_onFailedReply); + if (_onFailedSend != null) + buf.append(" with onFailedSend job: ").append(_onFailedSend); + buf.append(" {timestamps: \n"); + synchronized (_timestamps) { + long lastWhen = -1; + for (int i = 0; i < _timestampOrder.size(); i++) { + String name = (String)_timestampOrder.get(i); + Long when = (Long)_timestamps.get(name); + buf.append("\t["); + long diff = when.longValue() - lastWhen; + if ( (lastWhen > 0) && (diff > 500) ) + buf.append("**"); + if (lastWhen > 0) + buf.append(diff); + else + buf.append(0); + buf.append("ms: \t").append(name).append('=').append(formatDate(when.longValue())).append("]\n"); + lastWhen = when.longValue(); + } + } + buf.append("}"); + buf.append("]"); + return buf.toString(); + } + + private final static SimpleDateFormat _fmt = new SimpleDateFormat("HH:mm:ss.SSS"); + private final static String formatDate(long when) { + Date d = new Date(when); + synchronized (_fmt) { + return _fmt.format(d); + } + } + + public int hashCode() { + int rv = 0; + rv += DataHelper.hashCode(_message); + rv += DataHelper.hashCode(_target); + // the others are pretty much inconsequential + return rv; + } + + public boolean equals(Object obj) { + return obj == this; // two OutNetMessages are different even if they contain the same message + } +} diff --git a/router/java/src/net/i2p/router/OutNetMessagePool.java b/router/java/src/net/i2p/router/OutNetMessagePool.java new file mode 100644 index 0000000000..aeb49884d2 --- /dev/null +++ b/router/java/src/net/i2p/router/OutNetMessagePool.java @@ -0,0 +1,194 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.List; +import java.util.ArrayList; +import java.util.TreeMap; +import java.util.Iterator; +import java.util.Comparator; + +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.router.transport.OutboundMessageRegistry; + +/** + * Maintain a pool of OutNetMessages destined for other routers, organized by + * priority, expiring messages as necessary. This pool is populated by anything + * that wants to send a message, and the communication subsystem periodically + * retrieves messages for delivery. + * + */ +public class OutNetMessagePool { + private final static Log _log = new Log(OutNetMessagePool.class); + private static OutNetMessagePool _instance = new OutNetMessagePool(); + public static OutNetMessagePool getInstance() { return _instance; } + private TreeMap _messageLists; // priority --> List of OutNetMessage objects, where HIGHEST priority first + + private OutNetMessagePool() { + _messageLists = new TreeMap(new ReverseIntegerComparator()); + } + + /** + * Remove the highest priority message, or null if none are available. + * + */ + public OutNetMessage getNext() { + synchronized (_messageLists) { + if (_messageLists.size() <= 0) return null; + for (Iterator iter = _messageLists.keySet().iterator(); iter.hasNext(); ) { + Integer priority = (Integer)iter.next(); + List messages = (List)_messageLists.get(priority); + if (messages.size() > 0) { + _log.debug("Found a message of priority " + priority); + return (OutNetMessage)messages.remove(0); + } + } + // no messages of any priority + return null; + } + } + + /** + * Add a new message to the pool + * + */ + public void add(OutNetMessage msg) { + boolean valid = validate(msg); + if (!valid) return; + if (true) { // skip the pool + MessageSelector selector = msg.getReplySelector(); + if (selector != null) { + OutboundMessageRegistry.getInstance().registerPending(msg); + } + CommSystemFacade.getInstance().processMessage(msg); + return; + } + + synchronized (_messageLists) { + Integer pri = new Integer(msg.getPriority()); + if ( (_messageLists.size() <= 0) || (!_messageLists.containsKey(pri)) ) + _messageLists.put(new Integer(msg.getPriority()), new ArrayList(32)); + List messages = (List)_messageLists.get(pri); + messages.add(msg); + } + } + + private boolean validate(OutNetMessage msg) { + if (msg == null) return false; + if (msg.getMessage() == null) { + _log.error("Null message in the OutNetMessage: " + msg, new Exception("Someone fucked up")); + return false; + } + if (msg.getTarget() == null) { + _log.error("No target in the OutNetMessage: " + msg, new Exception("Definitely a fuckup")); + return false; + } + if (msg.getPriority() < 0) { + _log.warn("Priority less than 0? sounds like nonsense to me... " + msg, new Exception("Negative priority")); + return false; + } + if (msg.getExpiration() <= Clock.getInstance().now()) { + _log.error("Already expired! wtf: " + msg, new Exception("Expired message")); + return false; + } + return true; + } + + /** + * Clear any messages that have expired, enqueuing any appropriate jobs + * + */ + public void clearExpired() { + long now = Clock.getInstance().now(); + List jobsToEnqueue = new ArrayList(); + synchronized (_messageLists) { + for (Iterator iter = _messageLists.values().iterator(); iter.hasNext();) { + List toRemove = new ArrayList(); + List messages = (List)iter.next(); + for (Iterator msgIter = messages.iterator(); msgIter.hasNext(); ) { + OutNetMessage msg = (OutNetMessage)msgIter.next(); + if (msg.getExpiration() <= now) { + _log.warn("Outbound network message expired: " + msg); + toRemove.add(msg); + jobsToEnqueue.add(msg.getOnFailedSendJob()); + } + } + messages.removeAll(toRemove); + } + } + for (int i = 0; i < jobsToEnqueue.size(); i++) { + Job j = (Job)jobsToEnqueue.get(i); + JobQueue.getInstance().addJob(j); + } + } + + /** + * Retrieve the number of messages, regardless of priority. + * + */ + public int getCount() { + int size = 0; + synchronized (_messageLists) { + for (Iterator iter = _messageLists.values().iterator(); iter.hasNext(); ) { + List lst = (List)iter.next(); + size += lst.size(); + } + } + return size; + } + + /** + * Retrieve the number of messages at the given priority. This can be used for + * subsystems that maintain a pool of messages to be sent whenever there is spare time, + * where all of these 'spare' messages are of the same priority. + * + */ + public int getCount(int priority) { + synchronized (_messageLists) { + Integer pri = new Integer(priority); + List messages = (List)_messageLists.get(pri); + if (messages == null) + return 0; + else + return messages.size(); + } + } + + public void dumpPoolInfo() { + StringBuffer buf = new StringBuffer(); + buf.append("\nDumping Outbound Network Message Pool. Total # message: ").append(getCount()).append("\n"); + synchronized (_messageLists) { + for (Iterator iter = _messageLists.keySet().iterator(); iter.hasNext();) { + Integer pri = (Integer)iter.next(); + List messages = (List)_messageLists.get(pri); + if (messages.size() > 0) { + buf.append("Messages of priority ").append(pri).append(": ").append(messages.size()).append("\n"); + buf.append("---------------------------\n"); + for (Iterator msgIter = messages.iterator(); msgIter.hasNext(); ) { + OutNetMessage msg = (OutNetMessage)msgIter.next(); + buf.append("Message ").append(msg.getMessage()).append("\n\n"); + } + buf.append("---------------------------\n"); + } + } + } + _log.debug(buf.toString()); + } + + private static class ReverseIntegerComparator implements Comparator { + public int compare(Object lhs, Object rhs) { + if ( (lhs == null) || (rhs == null) ) return 0; // invalid, but never used + if ( !(lhs instanceof Integer) || !(rhs instanceof Integer)) return 0; + Integer lv = (Integer)lhs; + Integer rv = (Integer)rhs; + return - (lv.compareTo(rv)); + } + } +} diff --git a/router/java/src/net/i2p/router/PeerManagerFacade.java b/router/java/src/net/i2p/router/PeerManagerFacade.java new file mode 100644 index 0000000000..474babb15d --- /dev/null +++ b/router/java/src/net/i2p/router/PeerManagerFacade.java @@ -0,0 +1,40 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.List; + +import net.i2p.router.peermanager.PeerManagerFacadeImpl; + +/** + * Manage peer references and keep them up to date so that when asked for peers, + * it can provide appropriate peers according to the criteria provided. This + * includes periodically queueing up outbound messages to the peers to test them. + * + */ +public abstract class PeerManagerFacade implements Service { + private static PeerManagerFacade _instance = new PeerManagerFacadeImpl(); + public static PeerManagerFacade getInstance() { return _instance; } + + /** + * Select peers from the manager's existing routing tables according to + * the specified criteria. This call DOES block. + * + * @return List of Hash objects of the RouterIdentity for matching peers + */ + public abstract List selectPeers(PeerSelectionCriteria criteria); + public String renderStatusHTML() { return ""; } +} + +class DummyPeerManagerFacade extends PeerManagerFacade { + public void shutdown() {} + public void startup() {} + + public List selectPeers(PeerSelectionCriteria criteria) { return null; } +} diff --git a/router/java/src/net/i2p/router/PeerSelectionCriteria.java b/router/java/src/net/i2p/router/PeerSelectionCriteria.java new file mode 100644 index 0000000000..8732b7166a --- /dev/null +++ b/router/java/src/net/i2p/router/PeerSelectionCriteria.java @@ -0,0 +1,39 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +/** + * Defines the criteria for selecting a set of peers for use when searching the + * PeerManager + * + */ +public class PeerSelectionCriteria { + /** The peers will be used in a tunnel */ + public final static int PURPOSE_TUNNEL = 1; + /** The peers will be used for garlic routed messages */ + public final static int PURPOSE_GARLIC = 2; + /** The peers will be used for a source routed reply block message */ + public final static int PURPOSE_SOURCE_ROUTE = 3; + /** The peers will be used for a test message */ + public final static int PURPOSE_TEST = 4; + + private int _minReq; + private int _maxReq; + private int _purpose; + + /** Minimum number of peers required */ + public int getMinimumRequired() { return _minReq; } + public void setMinimumRequired(int min) { _minReq = min; } + /** Maximum number of peers required */ + public int getMaximumRequired() { return _maxReq; } + public void setMaximumRequired(int max) { _maxReq = max; } + /** Purpose for which the peers will be used */ + public int getPurpose() { return _purpose; } + public void setPurpose(int purpose) { _purpose = purpose; } +} diff --git a/router/java/src/net/i2p/router/ProfileManager.java b/router/java/src/net/i2p/router/ProfileManager.java new file mode 100644 index 0000000000..57251e6674 --- /dev/null +++ b/router/java/src/net/i2p/router/ProfileManager.java @@ -0,0 +1,133 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Properties; + +import net.i2p.data.Hash; +import net.i2p.router.peermanager.ProfileManagerImpl; + +public abstract class ProfileManager { + private final static ProfileManager _instance = new ProfileManagerImpl(); + public static ProfileManager getInstance() { return _instance; } + + /** is this peer failing or already dropped? */ + public abstract boolean isFailing(Hash peer); + + /** + * Note that it took msToSend to send a message of size bytesSent to the peer over the transport. + * This should only be called if the transport considered the send successful. + * + */ + public abstract void messageSent(Hash peer, String transport, long msToSend, long bytesSent); + + /** + * Note that the router failed to send a message to the peer over the transport specified + * + */ + public abstract void messageFailed(Hash peer, String transport); + + /** + * Note that the router failed to send a message to the peer over any transport + * + */ + public abstract void messageFailed(Hash peer); + + /** + * Note that there was some sort of communication error talking with the peer + * + */ + public abstract void commErrorOccurred(Hash peer); + + /** + * Note that the router agreed to participate in a tunnel + * + */ + public abstract void tunnelJoined(Hash peer, long responseTimeMs); + + /** + * Note that a router explicitly rejected joining a tunnel + * + */ + public abstract void tunnelRejected(Hash peer, long responseTimeMs); + + /** + * Note that the peer participated in a tunnel that failed. Its failure may not have + * been the peer's fault however. + * + */ + public abstract void tunnelFailed(Hash peer); + + /** + * Note that the peer was able to return the valid data for a db lookup + * + */ + public abstract void dbLookupSuccessful(Hash peer, long responseTimeMs); + + /** + * Note that the peer was unable to reply to a db lookup - either with data or with + * a lookupReply redirecting the user elsewhere + * + */ + public abstract void dbLookupFailed(Hash peer); + + /** + * Note that the peer replied to a db lookup with a redirect to other routers, where + * the list of redirected users included newPeers routers that the local router didn't + * know about, oldPeers routers that the local router already knew about, the given invalid + * routers that were invalid in some way, and the duplicate number of routers that we explicitly + * asked them not to send us, but they did anyway + * + */ + public abstract void dbLookupReply(Hash peer, int newPeers, int oldPeers, int invalid, int duplicate, long responseTimeMs); + + /** + * Note that the local router received a db lookup from the given peer + * + */ + public abstract void dbLookupReceived(Hash peer); + + /** + * Note that the local router received an unprompted db store from the given peer + * + */ + public abstract void dbStoreReceived(Hash peer, boolean wasNewKey); + + /** + * Note that we've confirmed a successful send of db data to the peer (though we haven't + * necessarily requested it again from them, so they /might/ be lying) + * + */ + public abstract void dbStoreSent(Hash peer, long responseTimeMs); + + /** + * Note that we were unable to confirm a successful send of db data to + * the peer, at least not within our timeout period + * + */ + public abstract void dbStoreFailed(Hash peer); + + /** + * Note that the local router received a reference to the given peer, either + * through an explicit dbStore or in a dbLookupReply + */ + public abstract void heardAbout(Hash peer); + + /** + * Note that the router received a message from the given peer on the specified + * transport. Messages received without any "from" information aren't recorded + * through this metric. If msToReceive is negative, there was no timing information + * available + * + */ + public abstract void messageReceived(Hash peer, String style, long msToReceive, int bytesRead); + + /** provide a simple summary of a number of peers, suitable for publication in the netDb */ + public abstract Properties summarizePeers(int numPeers); +} diff --git a/router/java/src/net/i2p/router/ReplyJob.java b/router/java/src/net/i2p/router/ReplyJob.java new file mode 100644 index 0000000000..4b33eb198b --- /dev/null +++ b/router/java/src/net/i2p/router/ReplyJob.java @@ -0,0 +1,19 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.i2np.I2NPMessage; + +/** + * Defines an executable task that can be fired off in reply to a message + * + */ +public interface ReplyJob extends Job { + public void setMessage(I2NPMessage message); +} diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java new file mode 100644 index 0000000000..735db46f7d --- /dev/null +++ b/router/java/src/net/i2p/router/Router.java @@ -0,0 +1,425 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.BufferedReader; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.DecimalFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.TimeZone; + +import net.i2p.CoreVersion; +import net.i2p.crypto.DHSessionKeyBuilder; +import net.i2p.data.DataFormatException; +import net.i2p.data.RouterInfo; +import net.i2p.data.RoutingKeyGenerator; +import net.i2p.data.DataHelper; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.data.i2np.SourceRouteReplyMessage; +import net.i2p.data.i2np.TunnelMessage; +import net.i2p.router.message.GarlicMessageHandler; +import net.i2p.router.message.SourceRouteReplyMessageHandler; +import net.i2p.router.message.TunnelMessageHandler; +import net.i2p.router.startup.StartupJob; +import net.i2p.router.transport.BandwidthLimiter; +import net.i2p.router.transport.OutboundMessageRegistry; +import net.i2p.util.Clock; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; +import net.i2p.util.LogConsoleBuffer; +import net.i2p.util.LogManager; +import net.i2p.util.RandomSource; +import net.i2p.stat.StatManager; +import net.i2p.stat.Rate; +import net.i2p.stat.RateStat; +import net.i2p.router.admin.StatsGenerator; + +/** + * Main driver for the router. + * + */ +public class Router { + private final static Log _log = new Log(Router.class); + private final static Router _instance = new Router(); + public static Router getInstance() { return _instance; } + private Properties _config; + private String _configFilename; + private RouterInfo _routerInfo; + private long _started; + private boolean _higherVersionSeen; + + public final static String PROP_CONFIG_FILE = "router.configLocation"; + + /** let clocks be off by 1 minute */ + public final static long CLOCK_FUDGE_FACTOR = 1*60*1000; + + public final static String PROP_INFO_FILENAME = "router.info.location"; + public final static String PROP_INFO_FILENAME_DEFAULT = "router.info"; + public final static String PROP_KEYS_FILENAME = "router.keys.location"; + public final static String PROP_KEYS_FILENAME_DEFAULT = "router.keys"; + + private Router() { + _config = new Properties(); + _configFilename = System.getProperty(PROP_CONFIG_FILE, "router.config"); + _routerInfo = null; + _higherVersionSeen = false; + // grumble about sun's java caching DNS entries *forever* + System.setProperty("sun.net.inetaddr.ttl", "0"); + System.setProperty("networkaddress.cache.ttl", "0"); + // (no need for keepalive) + System.setProperty("http.keepAlive", "false"); + } + + public String getConfigFilename() { return _configFilename; } + public void setConfigFilename(String filename) { _configFilename = filename; } + + public String getConfigSetting(String name) { return _config.getProperty(name); } + public void setConfigSetting(String name, String value) { _config.setProperty(name, value); } + public Set getConfigSettings() { return new HashSet(_config.keySet()); } + public Properties getConfigMap() { return _config; } + + public RouterInfo getRouterInfo() { return _routerInfo; } + public void setRouterInfo(RouterInfo info) { + _routerInfo = info; + if (info != null) + JobQueue.getInstance().addJob(new PersistRouterInfoJob()); + } + + /** + * True if the router has tried to communicate with another router who is running a higher + * incompatible protocol version. + * + */ + public boolean getHigherVersionSeen() { return _higherVersionSeen; } + public void setHigherVersionSeen(boolean seen) { _higherVersionSeen = seen; } + + public long getWhenStarted() { return _started; } + /** wall clock uptime */ + public long getUptime() { return Clock.getInstance().now() - Clock.getInstance().getOffset() - _started; } + + private void runRouter() { + _started = Clock.getInstance().now(); + Runtime.getRuntime().addShutdownHook(new ShutdownHook()); + I2PThread.setOOMEventListener(new I2PThread.OOMEventListener() { + public void outOfMemory(OutOfMemoryError oom) { + _log.log(Log.CRIT, "Thread ran out of memory", oom); + shutdown(); + } + }); + setupHandlers(); + startupQueue(); + JobQueue.getInstance().addJob(new CoallesceStatsJob()); + JobQueue.getInstance().addJob(new UpdateRoutingKeyModifierJob()); + warmupCrypto(); + SessionKeyPersistenceHelper.getInstance().startup(); + JobQueue.getInstance().addJob(new StartupJob()); + } + + /** + * coallesce the stats framework every minute + * + */ + private final static class CoallesceStatsJob extends JobImpl { + public String getName() { return "Coallesce stats"; } + public void runJob() { + StatManager.getInstance().coallesceStats(); + requeue(60*1000); + } + } + + /** + * Update the routing Key modifier every day at midnight (plus on startup). + * This is done here because we want to make sure the key is updated before anyone + * uses it. + */ + private final static class UpdateRoutingKeyModifierJob extends JobImpl { + private Calendar _cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); + public String getName() { return "Update Routing Key Modifier"; } + public void runJob() { + RoutingKeyGenerator.getInstance().generateDateBasedModData(); + requeue(getTimeTillMidnight()); + } + private long getTimeTillMidnight() { + long now = Clock.getInstance().now(); + _cal.setTime(new Date(now)); + _cal.add(Calendar.DATE, 1); + _cal.set(Calendar.HOUR_OF_DAY, 0); + _cal.set(Calendar.MINUTE, 0); + _cal.set(Calendar.SECOND, 0); + _cal.set(Calendar.MILLISECOND, 0); + long then = _cal.getTime().getTime(); + _log.debug("Time till midnight: " + (then-now) + "ms"); + if (then - now <= 60*1000) { + // everyone wave at kaffe. + // "Hi Kaffe" + return 60*1000; + } else { + return then - now; + } + } + } + + private void warmupCrypto() { + RandomSource.getInstance().nextBoolean(); + new DHSessionKeyBuilder(); // load the class so it starts the precalc process + } + + private void startupQueue() { + JobQueue.getInstance().runQueue(1); + } + + private void setupHandlers() { + InNetMessagePool.getInstance().registerHandlerJobBuilder(GarlicMessage.MESSAGE_TYPE, new GarlicMessageHandler()); + InNetMessagePool.getInstance().registerHandlerJobBuilder(TunnelMessage.MESSAGE_TYPE, new TunnelMessageHandler()); + InNetMessagePool.getInstance().registerHandlerJobBuilder(SourceRouteReplyMessage.MESSAGE_TYPE, new SourceRouteReplyMessageHandler()); + } + + public String renderStatusHTML() { + StringBuffer buf = new StringBuffer(); + buf.append("I2P Router Console\n"); + buf.append("

Router console

\n"); + buf.append("
console | stats
\n"); + + buf.append("
"); + buf.append(""); + buf.append("
"); + + buf.append("
\n"); + + if ( (_routerInfo != null) && (_routerInfo.getIdentity() != null) ) + buf.append("Router: ").append(_routerInfo.getIdentity().getHash().toBase64()).append("
\n"); + buf.append("As of: ").append(new Date(Clock.getInstance().now())).append(" (uptime: ").append(DataHelper.formatDuration(getUptime())).append(")
\n"); + buf.append("Started on: ").append(new Date(getWhenStarted())).append("
\n"); + buf.append("Clock offset: ").append(Clock.getInstance().getOffset()).append("ms (OS time: ").append(new Date(Clock.getInstance().now() - Clock.getInstance().getOffset())).append(")
\n"); + long tot = Runtime.getRuntime().totalMemory()/1024; + long free = Runtime.getRuntime().freeMemory()/1024; + buf.append("Memory: In use: ").append((tot-free)).append("KB Free: ").append(free).append("KB
\n"); + buf.append("Version: Router: ").append(RouterVersion.VERSION).append(" / SDK: ").append(CoreVersion.VERSION).append("
\n"); + if (_higherVersionSeen) + buf.append("HIGHER VERSION SEEN - please check to see if there is a new release out
\n"); + + buf.append("

Bandwidth

\n"); + long sent = BandwidthLimiter.getInstance().getTotalSendBytes(); + long received = BandwidthLimiter.getInstance().getTotalReceiveBytes(); + buf.append("
    "); + + buf.append("
  • ").append(sent).append(" bytes sent, "); + buf.append(received).append(" bytes received
  • "); + + DecimalFormat fmt = new DecimalFormat("##0.00"); + + // we use the unadjusted time, since thats what getWhenStarted is based off + long lifetime = Clock.getInstance().now()-Clock.getInstance().getOffset() - getWhenStarted(); + lifetime /= 1000; + if ( (sent > 0) && (received > 0) ) { + double sendKBps = sent / (lifetime*1024.0); + double receivedKBps = received / (lifetime*1024.0); + buf.append("
  • Lifetime rate: "); + buf.append(fmt.format(sendKBps)).append("KBps sent "); + buf.append(fmt.format(receivedKBps)).append("KBps received"); + buf.append("
  • "); + } + + RateStat sendRate = StatManager.getInstance().getRate("transport.sendMessageSize"); + for (int i = 0; i < sendRate.getPeriods().length; i++) { + Rate rate = sendRate.getRate(sendRate.getPeriods()[i]); + double bytes = rate.getLastTotalValue() + rate.getCurrentTotalValue(); + long ms = rate.getLastTotalEventTime() + rate.getLastTotalEventTime(); + if (ms <= 0) { + bytes = 0; + ms = 1; + } + buf.append("
  • "); + buf.append(DataHelper.formatDuration(rate.getPeriod())).append(" instantaneous send avg: "); + double bps = bytes*1000.0d/ms; + if (bps > 2048) { + bps /= 1024.0d; + buf.append(fmt.format(bps)).append(" KBps"); + } else { + buf.append(fmt.format(bps)).append(" Bps"); + } + buf.append(" over ").append((long)bytes).append(" bytes"); + buf.append("
  • "); + buf.append(DataHelper.formatDuration(rate.getPeriod())).append(" period send avg: "); + // we include lastPeriod + current *partial* period, and jrandom is too lazy to calculate how + // much of that partial is contained here, so 2*period it is. + bps = bytes*1000.0d/(2*rate.getPeriod()); + if (bps > 2048) { + bps /= 1024.0d; + buf.append(fmt.format(bps)).append(" KBps"); + } else { + buf.append(fmt.format(bps)).append(" Bps"); + } + buf.append(" over ").append((long)bytes).append(" bytes"); + buf.append("
  • "); + } + + RateStat receiveRate = StatManager.getInstance().getRate("transport.receiveMessageSize"); + for (int i = 0; i < receiveRate.getPeriods().length; i++) { + Rate rate = receiveRate.getRate(receiveRate.getPeriods()[i]); + double bytes = rate.getLastTotalValue() + rate.getCurrentTotalValue(); + long ms = rate.getLastTotalEventTime() + rate.getLastTotalEventTime(); + if (ms <= 0) { + bytes = 0; + ms = 1; + } + buf.append("
  • "); + buf.append(DataHelper.formatDuration(rate.getPeriod())).append(" instantaneous receive avg: "); + double bps = bytes*1000.0d/ms; + if (bps > 2048) { + bps /= 1024.0d; + buf.append(fmt.format(bps)).append(" KBps "); + } else { + buf.append(fmt.format(bps)).append(" Bps "); + } + buf.append(" over ").append((long)bytes).append(" bytes"); + buf.append("
  • "); + buf.append(DataHelper.formatDuration(rate.getPeriod())).append(" period receive avg: "); + // we include lastPeriod + current *partial* period, and jrandom is too lazy to calculate how + // much of that partial is contained here, so 2*period it is. + bps = bytes*1000.0d/(2*rate.getPeriod()); + if (bps > 2048) { + bps /= 1024.0d; + buf.append(fmt.format(bps)).append(" KBps"); + } else { + buf.append(fmt.format(bps)).append(" Bps"); + } + buf.append(" over ").append((long)bytes).append(" bytes"); + buf.append("
  • "); + } + + buf.append("
\n"); + buf.append("Instantaneous averages count how fast the transfers go when we're trying to transfer data, "); + buf.append("while period averages count how fast the transfers go across the entire period, even when we're not "); + buf.append("trying to transfer data. Lifetime averages count how many elephants there are on the moon [like anyone reads this text]"); + buf.append("\n"); + + buf.append("
\n"); + buf.append(ClientManagerFacade.getInstance().renderStatusHTML()); + buf.append("\n
\n"); + buf.append(CommSystemFacade.getInstance().renderStatusHTML()); + buf.append("\n
\n"); + buf.append(PeerManagerFacade.getInstance().renderStatusHTML()); + buf.append("\n
\n"); + buf.append(TunnelManagerFacade.getInstance().renderStatusHTML()); + buf.append("\n
\n"); + buf.append(JobQueue.getInstance().renderStatusHTML()); + buf.append("\n
\n"); + buf.append(Shitlist.getInstance().renderStatusHTML()); + buf.append("\n
\n"); + buf.append(OutboundMessageRegistry.getInstance().renderStatusHTML()); + buf.append("\n
\n"); + buf.append(NetworkDatabaseFacade.getInstance().renderStatusHTML()); + buf.append("\n
\n"); + List msgs = LogConsoleBuffer.getInstance().getMostRecentMessages(); + buf.append("\n

Most recent console messages:

\n"); + for (Iterator iter = msgs.iterator(); iter.hasNext(); ) { + String msg = (String)iter.next(); + buf.append("\n"); + } + buf.append("
").append(msg);
+	    buf.append("
"); + buf.append("\n"); + return buf.toString(); + } + + public void shutdown() { + try { JobQueue.getInstance().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the job queue", t); } + try { StatisticsManager.getInstance().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the stats manager", t); } + try { ClientManagerFacade.getInstance().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the client manager", t); } + try { TunnelManagerFacade.getInstance().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the tunnel manager", t); } + try { NetworkDatabaseFacade.getInstance().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the networkDb", t); } + try { CommSystemFacade.getInstance().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the comm system", t); } + try { PeerManagerFacade.getInstance().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the peer manager", t); } + try { SessionKeyPersistenceHelper.getInstance().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the session key manager", t); } + dumpStats(); + _log.log(Log.CRIT, "Shutdown complete", new Exception("Shutdown")); + try { LogManager.getInstance().shutdown(); } catch (Throwable t) { } + try { Thread.sleep(1000); } catch (InterruptedException ie) {} + Runtime.getRuntime().halt(-1); + } + + private void dumpStats() { + _log.log(Log.CRIT, "Lifetime stats:\n\n" + StatsGenerator.generateStatsPage()); + } + + public static void main(String args[]) { + Router.getInstance().runRouter(); + if (args.length > 0) { + _log.info("Not interactive"); + } else { + _log.info("Interactive"); + try { + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + String line = null; + while ( (line = in.readLine()) != null) { + ClientMessagePool.getInstance().dumpPoolInfo(); + OutNetMessagePool.getInstance().dumpPoolInfo(); + InNetMessagePool.getInstance().dumpPoolInfo(); + } + } catch (IOException ioe) { + _log.error("Error dumping queue", ioe); + } + } + } + + private class ShutdownHook extends Thread { + public void run() { + _log.log(Log.CRIT, "Shutting down the router...", new Exception("Shutting down")); + shutdown(); + } + } + + /** update the router.info file whenever its, er, updated */ + private static class PersistRouterInfoJob extends JobImpl { + public String getName() { return "Persist Updated Router Information"; } + public void runJob() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Persisting updated router info"); + + String infoFilename = Router.getInstance().getConfigSetting(PROP_INFO_FILENAME); + if (infoFilename == null) + infoFilename = PROP_INFO_FILENAME_DEFAULT; + + RouterInfo info = Router.getInstance().getRouterInfo(); + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(infoFilename); + info.writeBytes(fos); + } catch (DataFormatException dfe) { + _log.error("Error rebuilding the router information", dfe); + } catch (IOException ioe) { + _log.error("Error writing out the rebuilt router information", ioe); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + } + } +} diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java new file mode 100644 index 0000000000..55ec755a54 --- /dev/null +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -0,0 +1,26 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.CoreVersion; + +/** + * Expose a version string + * + */ +public class RouterVersion { + public final static String ID = "$Revision: 1.40 $ $Date: 2004/04/04 13:40:34 $"; + public final static String VERSION = "0.3.0.3"; + public static void main(String args[]) { + System.out.println("I2P Router version: " + VERSION); + System.out.println("Router ID: " + RouterVersion.ID); + System.out.println("I2P Core version: " + CoreVersion.VERSION); + System.out.println("Core ID: " + CoreVersion.ID); + } +} diff --git a/router/java/src/net/i2p/router/Service.java b/router/java/src/net/i2p/router/Service.java new file mode 100644 index 0000000000..9ec1211abc --- /dev/null +++ b/router/java/src/net/i2p/router/Service.java @@ -0,0 +1,32 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +/** + * Define the manageable service interface for the subsystems in the I2P router + * + */ +public interface Service { + /** + * Instruct the service that it should start normal operation. + * This call DOES block until the service is ready. + * + */ + public void startup(); + + /** + * Instruct the service that the router is shutting down and that it should do + * whatever is necessary to go down gracefully. It should not depend on other + * components at this point. This call DOES block. + * + */ + public void shutdown(); + + public String renderStatusHTML(); +} diff --git a/router/java/src/net/i2p/router/SessionKeyPersistenceHelper.java b/router/java/src/net/i2p/router/SessionKeyPersistenceHelper.java new file mode 100644 index 0000000000..65925af8ab --- /dev/null +++ b/router/java/src/net/i2p/router/SessionKeyPersistenceHelper.java @@ -0,0 +1,91 @@ +package net.i2p.router; + +import net.i2p.util.Log; +import net.i2p.crypto.SessionKeyManager; +import net.i2p.crypto.PersistentSessionKeyManager; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Centralize the sessionKeyManager persistence (rather than leave it to a private + * job in the startup job) + * + */ +public class SessionKeyPersistenceHelper implements Service { + private final static Log _log = new Log(SessionKeyPersistenceHelper.class); + private static SessionKeyPersistenceHelper _instance = new SessionKeyPersistenceHelper(); + public static SessionKeyPersistenceHelper getInstance() { return _instance; } + private final static long PERSIST_DELAY = 3*60*1000; + private final static String SESSION_KEY_FILE = "sessionKeys.dat"; + + public void shutdown() { + writeState(); + } + + public void startup() { + SessionKeyManager mgr = SessionKeyManager.getInstance(); + if (mgr instanceof PersistentSessionKeyManager) { + PersistentSessionKeyManager manager = (PersistentSessionKeyManager)mgr; + File f = new File(SESSION_KEY_FILE); + if (f.exists()) { + FileInputStream fin = null; + try { + fin = new FileInputStream(f); + manager.loadState(fin); + int expired = manager.aggressiveExpire(); + _log.debug("Session keys loaded [not error] with " + expired + " sets immediately expired"); + } catch (Throwable t) { + _log.error("Error reading in session key data", t); + } finally { + if (fin != null) try { fin.close(); } catch (IOException ioe) {} + } + } + JobQueue.getInstance().addJob(new SessionKeyWriterJob()); + } + } + + private static void writeState() { + Object o = SessionKeyManager.getInstance(); + if (!(o instanceof PersistentSessionKeyManager)) { + _log.error("Unable to persist the session key state - manager is " + o.getClass().getName()); + return; + } + PersistentSessionKeyManager mgr = (PersistentSessionKeyManager)o; + + // only need for synchronization is during shutdown() + synchronized (mgr) { + FileOutputStream fos = null; + try { + int expired = mgr.aggressiveExpire(); + if (expired > 0) { + _log.info("Agressive expired " + expired + " tag sets"); + } + fos = new FileOutputStream(SESSION_KEY_FILE); + mgr.saveState(fos); + fos.flush(); + _log.debug("Session keys written"); + } catch (Throwable t) { + _log.debug("Error writing session key state", t); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + } + } + + public String renderStatusHTML() { return ""; } + + private class SessionKeyWriterJob extends JobImpl { + public SessionKeyWriterJob() { + super(); + getTiming().setStartAfter(PERSIST_DELAY); + } + public String getName() { return "Write Session Keys"; } + public void runJob() { + writeState(); + requeue(PERSIST_DELAY); + } + } +} diff --git a/router/java/src/net/i2p/router/Shitlist.java b/router/java/src/net/i2p/router/Shitlist.java new file mode 100644 index 0000000000..4a6a8836d0 --- /dev/null +++ b/router/java/src/net/i2p/router/Shitlist.java @@ -0,0 +1,99 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Hash; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Date; + +/** + * Manage in memory the routers we are oh so fond of. + * This needs to get a little bit more sophisticated... currently there is no + * way out of the shitlist + * + */ +public class Shitlist { + private final static Shitlist _instance = new Shitlist(); + public final static Shitlist getInstance() { return _instance; } + private final static Log _log = new Log(Shitlist.class); + private Map _shitlist; // H(routerIdent) --> Date + + public final static long SHITLIST_DURATION_MS = 4*60*1000; // 4 minute shitlist + + private Shitlist() { + _shitlist = new HashMap(100); + } + + public boolean shitlistRouter(Hash peer) { + if (peer == null) return false; + boolean wasAlready = false; + if (_log.shouldLog(Log.INFO)) + _log.info("Shitlisting router " + peer.toBase64(), new Exception("Shitlist cause")); + + synchronized (_shitlist) { + Date oldDate = (Date)_shitlist.put(peer, new Date(Clock.getInstance().now())); + wasAlready = (null == oldDate); + } + NetworkDatabaseFacade.getInstance().fail(peer); + TunnelManagerFacade.getInstance().peerFailed(peer); + return wasAlready; + } + + public void unshitlistRouter(Hash peer) { + if (peer == null) return; + _log.info("Unshitlisting router " + peer.toBase64()); + synchronized (_shitlist) { + _shitlist.remove(peer); + } + } + + public boolean isShitlisted(Hash peer) { + Date shitlistDate = null; + synchronized (_shitlist) { + shitlistDate = (Date)_shitlist.get(peer); + } + if (shitlistDate == null) return false; + + // check validity + if (shitlistDate.getTime() > Clock.getInstance().now() - SHITLIST_DURATION_MS) { + return true; + } else { + unshitlistRouter(peer); + return false; + } + } + + public String renderStatusHTML() { + StringBuffer buf = new StringBuffer(); + buf.append("

Shitlist

"); + Map shitlist = new HashMap(); + synchronized (_shitlist) { + shitlist.putAll(_shitlist); + } + buf.append("
    "); + + long limit = Clock.getInstance().now() - SHITLIST_DURATION_MS; + + for (Iterator iter = shitlist.keySet().iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + Date shitDate = (Date)shitlist.get(key); + if (shitDate.getTime() < limit) + unshitlistRouter(key); + else + buf.append("
  • ").append(key.toBase64()).append(" was shitlisted on ").append(shitDate).append("
  • \n"); + } + buf.append("
\n"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/StatisticsManager.java b/router/java/src/net/i2p/router/StatisticsManager.java new file mode 100644 index 0000000000..6753a4c577 --- /dev/null +++ b/router/java/src/net/i2p/router/StatisticsManager.java @@ -0,0 +1,147 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import java.util.Properties; + +import net.i2p.CoreVersion; +import net.i2p.data.DataHelper; +import net.i2p.stat.Rate; +import net.i2p.stat.RateStat; +import net.i2p.stat.StatManager; +import net.i2p.util.Log; + +/** + * Maintain the statistics about the router + * + */ +public class StatisticsManager implements Service { + private final static Log _log = new Log(StatisticsManager.class); + private static StatisticsManager _instance = new StatisticsManager(); + public static StatisticsManager getInstance() { return _instance; } + private boolean _includePeerRankings; + private int _publishedStats; + + public final static String PROP_PUBLISH_RANKINGS = "router.publishPeerRankings"; + public final static String DEFAULT_PROP_PUBLISH_RANKINGS = "false"; + public final static String PROP_MAX_PUBLISHED_PEERS = "router.publishPeerMax"; + public final static int DEFAULT_MAX_PUBLISHED_PEERS = 20; + + public StatisticsManager() { + _includePeerRankings = false; + } + + public void shutdown() {} + public void startup() { + String val = Router.getInstance().getConfigSetting(PROP_PUBLISH_RANKINGS); + try { + if (val == null) { + _log.info("Peer publishing setting " + PROP_PUBLISH_RANKINGS + " not set - using default " + DEFAULT_PROP_PUBLISH_RANKINGS); + val = DEFAULT_PROP_PUBLISH_RANKINGS; + } else { + _log.info("Peer publishing setting " + PROP_PUBLISH_RANKINGS + " set to " + val); + } + boolean v = Boolean.TRUE.toString().equalsIgnoreCase(val); + _includePeerRankings = v; + _log.debug("Setting includePeerRankings = " + v); + } catch (Throwable t) { + _log.error("Error determining whether to publish rankings [" + PROP_PUBLISH_RANKINGS + "=" + val + "], so we're defaulting to FALSE"); + _includePeerRankings = false; + } + val = Router.getInstance().getConfigSetting(PROP_MAX_PUBLISHED_PEERS); + if (val == null) { + _publishedStats = DEFAULT_MAX_PUBLISHED_PEERS; + } else { + try { + int num = Integer.parseInt(val); + _publishedStats = num; + } catch (NumberFormatException nfe) { + _log.error("Invalid max number of peers to publish [" + val + "], defaulting to " + DEFAULT_MAX_PUBLISHED_PEERS, nfe); + _publishedStats = DEFAULT_MAX_PUBLISHED_PEERS; + } + } + } + + /** Retrieve a snapshot of the statistics that should be published */ + public Properties publishStatistics() { + Properties stats = new Properties(); + stats.setProperty("router.version", RouterVersion.VERSION); + stats.setProperty("router.id", RouterVersion.ID); + stats.setProperty("coreVersion", CoreVersion.VERSION); + stats.setProperty("core.id", CoreVersion.ID); + + if (_includePeerRankings) { + stats.putAll(ProfileManager.getInstance().summarizePeers(_publishedStats)); + + includeRate("transport.sendProcessingTime", stats); + includeRate("tcp.queueSize", stats); + includeRate("jobQueue.jobLag", stats); + includeRate("jobQueue.jobRun", stats); + includeRate("crypto.elGamal.encrypt", stats); + includeRate("jobQueue.readyJobs", stats); + includeRate("jobQueue.droppedJobs", stats); + stats.setProperty("stat_uptime", DataHelper.formatDuration(Router.getInstance().getUptime())); + stats.setProperty("stat__rateKey", "avg;maxAvg;pctLifetime;[sat;satLim;maxSat;maxSatLim;][num;lifetimeFreq;maxFreq]"); + _log.debug("Publishing peer rankings"); + } else { + _log.debug("Not publishing peer rankings"); + } + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Building status: " + stats); + return stats; + } + + private void includeRate(String rateName, Properties stats) { + RateStat rate = StatManager.getInstance().getRate(rateName); + if (rate == null) return; + for (int i = 0; i < rate.getPeriods().length; i++) { + Rate curRate = rate.getRate(rate.getPeriods()[i]); + if (curRate == null) continue; + stats.setProperty("stat_" + rateName + '.' + getPeriod(curRate), renderRate(curRate)); + } + } + + private static String renderRate(Rate rate) { + StringBuffer buf = new StringBuffer(255); + buf.append(num(rate.getAverageValue())).append(';'); + buf.append(num(rate.getExtremeAverageValue())).append(';'); + buf.append(pct(rate.getPercentageOfLifetimeValue())).append(';'); + if (rate.getLifetimeTotalEventTime() > 0) { + buf.append(pct(rate.getLastEventSaturation())).append(';'); + buf.append(num(rate.getLastSaturationLimit())).append(';'); + buf.append(pct(rate.getExtremeEventSaturation())).append(';'); + buf.append(num(rate.getExtremeSaturationLimit())).append(';'); + } + buf.append(num(rate.getLastEventCount())).append(';'); + long numPeriods = rate.getLifetimePeriods(); + if (numPeriods > 0) { + double avgFrequency = rate.getLifetimeEventCount() / (double)numPeriods; + double peakFrequency = rate.getExtremeEventCount(); + buf.append(num(avgFrequency)).append(';'); + buf.append(num(rate.getExtremeEventCount())).append(';'); + } + return buf.toString(); + } + + private static String getPeriod(Rate rate) { return DataHelper.formatDuration(rate.getPeriod()); } + + // TODO: get this to use some random locale, not the user's default (since its published) + private final static DecimalFormat _fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK)); + private final static String num(double num) { synchronized (_fmt) { return _fmt.format(num); } } + + private final static DecimalFormat _pct = new DecimalFormat("#0.00%", new DecimalFormatSymbols(Locale.UK)); + private final static String pct(double num) { synchronized (_pct) { return _pct.format(num); } } + + + public String renderStatusHTML() { return ""; } +} diff --git a/router/java/src/net/i2p/router/SubmitMessageHistoryJob.java b/router/java/src/net/i2p/router/SubmitMessageHistoryJob.java new file mode 100644 index 0000000000..cea5c84c21 --- /dev/null +++ b/router/java/src/net/i2p/router/SubmitMessageHistoryJob.java @@ -0,0 +1,118 @@ +package net.i2p.router; + +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.HTTPSendData; +import net.i2p.util.I2PThread; + +import net.i2p.router.transport.BandwidthLimiter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * Job that, if its allowed to, will submit the data gathered by the MessageHistory + * component to some URL so that the network can be debugged more easily. By default + * it does not submit any data or touch the message history file, but if the router + * has the line "router.submitHistory=true", it will send the file that the + * MessageHistory component is configured to write to once an hour, post it to + * http://i2p.net/cgi-bin/submitMessageHistory, and then delete that file + * locally. This should only be used if the MessageHistory component is configured to + * gather data (via "router.keepHistory=true"). + * + */ +public class SubmitMessageHistoryJob extends JobImpl { + private final static Log _log = new Log(SubmitMessageHistoryJob.class); + + /** default submitting data every hour */ + private final static long DEFAULT_REQUEUE_DELAY = 60*60*1000; + /** + * router config param for whether we want to autosubmit (and delete) the + * history data managed by MessageHistory + */ + public final static String PARAM_SUBMIT_DATA = "router.submitHistory"; + /** default value for whether we autosubmit the data */ + public final static boolean DEFAULT_SUBMIT_DATA = true; + /** where the data should be submitted to (via HTTP POST) */ + public final static String PARAM_SUBMIT_URL = "router.submitHistoryURL"; + /** default location */ + public final static String DEFAULT_SUBMIT_URL = "http://i2p.net/cgi-bin/submitMessageHistory"; + + public void runJob() { + if (shouldSubmit()) { + submit(); + } else { + _log.debug("Not submitting data"); + // if we didn't submit we can just requeue + requeue(getRequeueDelay()); + } + } + + /** + * We don't want this to be run within the jobqueue itself, so fire off a new thread + * to do the actual submission, enqueueing a new submit job when its done + */ + private void submit() { + I2PThread t = new I2PThread(new Runnable() { + public void run() { + _log.debug("Submitting data"); + MessageHistory.getInstance().setPauseFlushes(true); + String filename = MessageHistory.getInstance().getFilename(); + send(filename); + MessageHistory.getInstance().setPauseFlushes(false); + Job job = new SubmitMessageHistoryJob(); + job.getTiming().setStartAfter(Clock.getInstance().now() + getRequeueDelay()); + JobQueue.getInstance().addJob(job); + } + }); + t.setName("SubmitData"); + t.setPriority(I2PThread.MIN_PRIORITY); + t.setDaemon(true); + t.start(); + } + + private void send(String filename) { + String url = getURL(); + try { + File dataFile = new File(filename); + if (!dataFile.exists() || !dataFile.canRead()) { + _log.warn("Unable to read the message data file [" + dataFile.getAbsolutePath() + "]"); + return; + } + long size = dataFile.length(); + int expectedSend = 512; // 512 for HTTP overhead + if (size > 0) + expectedSend += (int)size/10; // compression + FileInputStream fin = new FileInputStream(dataFile); + BandwidthLimiter.getInstance().delayOutbound(null, expectedSend); + boolean sent = HTTPSendData.postData(url, size, fin); + fin.close(); + boolean deleted = dataFile.delete(); + _log.debug("Submitted " + size + " bytes? " + sent + " and deleted? " + deleted); + } catch (IOException ioe) { + _log.error("Error sending the data", ioe); + } + } + + private String getURL() { + String str = Router.getInstance().getConfigSetting(PARAM_SUBMIT_URL); + if ( (str == null) || (str.trim().length() <= 0) ) + return DEFAULT_SUBMIT_URL; + else + return str.trim(); + } + + private boolean shouldSubmit() { + String str = Router.getInstance().getConfigSetting(PARAM_SUBMIT_DATA); + if (str == null) { + _log.debug("History submit config not specified [" + PARAM_SUBMIT_DATA + "], default = " + DEFAULT_SUBMIT_DATA); + return DEFAULT_SUBMIT_DATA; + } else { + _log.debug("History submit config specified [" + str + "]"); + } + return Boolean.TRUE.toString().equals(str); + } + private long getRequeueDelay() { return DEFAULT_REQUEUE_DELAY; } + public String getName() { return "Submit Message History"; } +} diff --git a/router/java/src/net/i2p/router/TunnelInfo.java b/router/java/src/net/i2p/router/TunnelInfo.java new file mode 100644 index 0000000000..560b8fa82f --- /dev/null +++ b/router/java/src/net/i2p/router/TunnelInfo.java @@ -0,0 +1,348 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.TunnelConfigurationSessionKey; +import net.i2p.data.i2np.TunnelSessionKey; +import net.i2p.data.i2np.TunnelSigningPrivateKey; +import net.i2p.data.i2np.TunnelSigningPublicKey; +import net.i2p.util.Clock; + +/** + * Defines the information associated with a tunnel + */ +public class TunnelInfo extends DataStructureImpl { + private TunnelId _id; + private Hash _nextHop; + private Hash _thisHop; + private TunnelInfo _nextHopInfo; + private TunnelConfigurationSessionKey _configurationKey; + private TunnelSigningPublicKey _verificationKey; + private TunnelSigningPrivateKey _signingKey; + private TunnelSessionKey _encryptionKey; + private Destination _destination; + private Properties _options; + private TunnelSettings _settings; + private long _created; + private boolean _ready; + private boolean _wasEverReady; + + public TunnelInfo() { + setTunnelId(null); + setThisHop(null); + setNextHop(null); + setNextHopInfo(null); + _configurationKey = null; + _verificationKey = null; + _signingKey = null; + _encryptionKey = null; + setDestination(null); + setSettings(null); + _options = new Properties(); + _ready = false; + _wasEverReady = false; + _created = Clock.getInstance().now(); + } + + public TunnelId getTunnelId() { return _id; } + public void setTunnelId(TunnelId id) { _id = id; } + + public Hash getNextHop() { return _nextHop; } + public void setNextHop(Hash nextHopRouterIdentity) { _nextHop = nextHopRouterIdentity; } + + public Hash getThisHop() { return _thisHop; } + public void setThisHop(Hash thisHopRouterIdentity) { _thisHop = thisHopRouterIdentity; } + + public TunnelInfo getNextHopInfo() { return _nextHopInfo; } + public void setNextHopInfo(TunnelInfo info) { _nextHopInfo = info; } + + public TunnelConfigurationSessionKey getConfigurationKey() { return _configurationKey; } + public void setConfigurationKey(TunnelConfigurationSessionKey key) { _configurationKey = key; } + public void setConfigurationKey(SessionKey key) { + TunnelConfigurationSessionKey tk = new TunnelConfigurationSessionKey(); + tk.setKey(key); + _configurationKey = tk; + } + + public TunnelSigningPublicKey getVerificationKey() { return _verificationKey; } + public void setVerificationKey(TunnelSigningPublicKey key) { _verificationKey = key; } + public void setVerificationKey(SigningPublicKey key) { + TunnelSigningPublicKey tk = new TunnelSigningPublicKey(); + tk.setKey(key); + _verificationKey = tk; + } + + public TunnelSigningPrivateKey getSigningKey() { return _signingKey; } + public void setSigningKey(TunnelSigningPrivateKey key) { _signingKey = key; } + public void setSigningKey(SigningPrivateKey key) { + TunnelSigningPrivateKey tk = new TunnelSigningPrivateKey(); + tk.setKey(key); + _signingKey = tk; + } + + public TunnelSessionKey getEncryptionKey() { return _encryptionKey; } + public void setEncryptionKey(TunnelSessionKey key) { _encryptionKey = key; } + public void setEncryptionKey(SessionKey key) { + TunnelSessionKey tk = new TunnelSessionKey(); + tk.setKey(key); + _encryptionKey = tk; + } + + public Destination getDestination() { return _destination; } + public void setDestination(Destination dest) { _destination = dest; } + + public String getProperty(String key) { return _options.getProperty(key); } + public void setProperty(String key, String val) { _options.setProperty(key, val); } + public void clearProperties() { _options.clear(); } + public Set getPropertyNames() { return new HashSet(_options.keySet()); } + + public TunnelSettings getSettings() { return _settings; } + public void setSettings(TunnelSettings settings) { _settings = settings; } + + /** + * Have all of the routers in this tunnel confirmed participation, and we're ok to + * start sending messages through this tunnel? + */ + public boolean getIsReady() { return _ready; } + public void setIsReady(boolean ready) { + _ready = ready; + if (ready) + _wasEverReady = true; + } + /** + * true if this tunnel was ever working (aka rebuildable) + * + */ + public boolean getWasEverReady() { return _wasEverReady; } + + public long getCreated() { return _created; } + + /** + * Number of hops left in the tunnel (including this one) + * + */ + public final int getLength() { + int len = 0; + TunnelInfo info = this; + while (info != null) { + info = info.getNextHopInfo(); + len++; + } + return len; + } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _options = DataHelper.readProperties(in); + Boolean includeDest = DataHelper.readBoolean(in); + if (includeDest.booleanValue()) { + _destination = new Destination(); + _destination.readBytes(in); + } else { + _destination = null; + } + Boolean includeThis = DataHelper.readBoolean(in); + if (includeThis.booleanValue()) { + _thisHop = new Hash(); + _thisHop.readBytes(in); + } else { + _thisHop = null; + } + Boolean includeNext = DataHelper.readBoolean(in); + if (includeNext.booleanValue()) { + _nextHop = new Hash(); + _nextHop.readBytes(in); + } else { + _nextHop = null; + } + Boolean includeNextInfo = DataHelper.readBoolean(in); + if (includeNextInfo.booleanValue()) { + _nextHopInfo = new TunnelInfo(); + _nextHopInfo.readBytes(in); + } else { + _nextHopInfo = null; + } + _id = new TunnelId(); + _id.readBytes(in); + Boolean includeConfigKey = DataHelper.readBoolean(in); + if (includeConfigKey.booleanValue()) { + _configurationKey = new TunnelConfigurationSessionKey(); + _configurationKey.readBytes(in); + } else { + _configurationKey = null; + } + Boolean includeEncryptionKey = DataHelper.readBoolean(in); + if (includeEncryptionKey.booleanValue()) { + _encryptionKey = new TunnelSessionKey(); + _encryptionKey.readBytes(in); + } else { + _encryptionKey = null; + } + Boolean includeSigningKey = DataHelper.readBoolean(in); + if (includeSigningKey.booleanValue()) { + _signingKey = new TunnelSigningPrivateKey(); + _signingKey.readBytes(in); + } else { + _signingKey = null; + } + Boolean includeVerificationKey = DataHelper.readBoolean(in); + if (includeVerificationKey.booleanValue()) { + _verificationKey = new TunnelSigningPublicKey(); + _verificationKey.readBytes(in); + } else { + _verificationKey = null; + } + _settings = new TunnelSettings(); + _settings.readBytes(in); + Boolean ready = DataHelper.readBoolean(in); + if (ready != null) + setIsReady(ready.booleanValue()); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_id == null) throw new DataFormatException("Invalid tunnel ID: " + _id); + if (_options == null) throw new DataFormatException("Options are null"); + if (_settings == null) throw new DataFormatException("Settings are null"); + // everything else is optional in the serialization + + DataHelper.writeProperties(out, _options); + if (_destination != null) { + DataHelper.writeBoolean(out, Boolean.TRUE); + _destination.writeBytes(out); + } else { + DataHelper.writeBoolean(out, Boolean.FALSE); + } + if (_thisHop != null) { + DataHelper.writeBoolean(out, Boolean.TRUE); + _thisHop.writeBytes(out); + } else { + DataHelper.writeBoolean(out, Boolean.FALSE); + } + if (_nextHop != null) { + DataHelper.writeBoolean(out, Boolean.TRUE); + _nextHop.writeBytes(out); + } else { + DataHelper.writeBoolean(out, Boolean.FALSE); + } + if (_nextHopInfo != null) { + DataHelper.writeBoolean(out, Boolean.TRUE); + _nextHopInfo.writeBytes(out); + } else { + DataHelper.writeBoolean(out, Boolean.FALSE); + } + _id.writeBytes(out); + if (_configurationKey != null) { + DataHelper.writeBoolean(out, Boolean.TRUE); + _configurationKey.writeBytes(out); + } else { + DataHelper.writeBoolean(out, Boolean.FALSE); + } + if (_encryptionKey != null) { + DataHelper.writeBoolean(out, Boolean.TRUE); + _encryptionKey.writeBytes(out); + } else { + DataHelper.writeBoolean(out, Boolean.FALSE); + } + if (_signingKey != null) { + DataHelper.writeBoolean(out, Boolean.TRUE); + _signingKey.writeBytes(out); + } else { + DataHelper.writeBoolean(out, Boolean.FALSE); + } + if (_verificationKey != null) { + DataHelper.writeBoolean(out, Boolean.TRUE); + _verificationKey.writeBytes(out); + } else { + DataHelper.writeBoolean(out, Boolean.FALSE); + } + _settings.writeBytes(out); + DataHelper.writeBoolean(out, new Boolean(_ready)); + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[Tunnel ").append(_id.getTunnelId()); + TunnelInfo cur = this; + int i = 0; + while (cur != null) { + buf.append("\n*Hop ").append(i).append(": ").append(cur.getThisHop()); + if (cur.getEncryptionKey() != null) + buf.append("\n Encryption key: ").append(cur.getEncryptionKey()); + if (cur.getSigningKey() != null) + buf.append("\n Signing key: ").append(cur.getSigningKey()); + if (cur.getVerificationKey() != null) + buf.append("\n Verification key: ").append(cur.getVerificationKey()); + if (cur.getDestination() != null) + buf.append("\n Destination: ").append(cur.getDestination().calculateHash().toBase64()); + if (cur.getNextHop() != null) + buf.append("\n Next: ").append(cur.getNextHop()); + if (cur.getSettings() == null) + buf.append("\n Expiration: ").append("none"); + else + buf.append("\n Expiration: ").append(new Date(cur.getSettings().getExpiration())); + buf.append("\n Ready: ").append(getIsReady()); + cur = cur.getNextHopInfo(); + i++; + } + buf.append("]"); + return buf.toString(); + } + + public int hashCode() { + int rv = 0; + rv = 7*rv + DataHelper.hashCode(_options); + rv = 7*rv + DataHelper.hashCode(_destination); + rv = 7*rv + DataHelper.hashCode(_nextHop); + rv = 7*rv + DataHelper.hashCode(_thisHop); + rv = 7*rv + DataHelper.hashCode(_id); + rv = 7*rv + DataHelper.hashCode(_configurationKey); + rv = 7*rv + DataHelper.hashCode(_encryptionKey); + rv = 7*rv + DataHelper.hashCode(_signingKey); + rv = 7*rv + DataHelper.hashCode(_verificationKey); + rv = 7*rv + DataHelper.hashCode(_settings); + rv = 7*rv + (_ready ? 0 : 1); + return rv; + } + + public boolean equals(Object obj) { + if ( (obj != null) && (obj instanceof TunnelInfo) ) { + TunnelInfo info = (TunnelInfo)obj; + return DataHelper.eq(getConfigurationKey(), info.getConfigurationKey()) && + DataHelper.eq(getDestination(), info.getDestination()) && + getIsReady() == info.getIsReady() && + DataHelper.eq(getEncryptionKey(), info.getEncryptionKey()) && + DataHelper.eq(getNextHop(), info.getNextHop()) && + DataHelper.eq(getNextHopInfo(), info.getNextHopInfo()) && + DataHelper.eq(getSettings(), info.getSettings()) && + DataHelper.eq(getSigningKey(), info.getSigningKey()) && + DataHelper.eq(getThisHop(), info.getThisHop()) && + DataHelper.eq(getTunnelId(), info.getTunnelId()) && + DataHelper.eq(getVerificationKey(), info.getVerificationKey()) && + DataHelper.eq(_options, info._options); + } else { + return false; + } + } +} diff --git a/router/java/src/net/i2p/router/TunnelManagerFacade.java b/router/java/src/net/i2p/router/TunnelManagerFacade.java new file mode 100644 index 0000000000..373ae92f26 --- /dev/null +++ b/router/java/src/net/i2p/router/TunnelManagerFacade.java @@ -0,0 +1,66 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.List; + +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.TunnelId; +import net.i2p.router.tunnelmanager.PoolingTunnelManagerFacade; + +/** + * Build and maintain tunnels throughout the network. + * + */ +public abstract class TunnelManagerFacade implements Service { + private static TunnelManagerFacade _instance = new PoolingTunnelManagerFacade(); + public static TunnelManagerFacade getInstance() { return _instance; } + + /** + * React to a request to join the specified tunnel. + * + * @return true if the router will accept participation, else false. + */ + public abstract boolean joinTunnel(TunnelInfo info); + /** + * Retrieve the information related to a particular tunnel + * + */ + public abstract TunnelInfo getTunnelInfo(TunnelId id); + /** + * Retrieve a set of tunnels from the existing ones for various purposes + */ + public abstract List selectOutboundTunnelIds(TunnelSelectionCriteria criteria); + /** + * Retrieve a set of tunnels from the existing ones for various purposes + */ + public abstract List selectInboundTunnelIds(TunnelSelectionCriteria criteria); + + /** + * Make sure appropriate outbound tunnels are in place, builds requested + * inbound tunnels, then fire off a job to ask the ClientManagerFacade to + * validate the leaseSet, then publish it in the network database. + * + */ + public abstract void createTunnels(Destination destination, ClientTunnelSettings clientSettings, long timeoutMs); + + /** + * Called when a peer becomes unreachable - go through all of the current + * tunnels and rebuild them if we can, or drop them if we can't. + * + */ + public abstract void peerFailed(Hash peer); + + /** + * True if the peer currently part of a tunnel + * + */ + public abstract boolean isInUse(Hash peer); +} diff --git a/router/java/src/net/i2p/router/TunnelSelectionCriteria.java b/router/java/src/net/i2p/router/TunnelSelectionCriteria.java new file mode 100644 index 0000000000..e06f1976d0 --- /dev/null +++ b/router/java/src/net/i2p/router/TunnelSelectionCriteria.java @@ -0,0 +1,47 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +/** + * Set of criteria for finding a tunnel from the Tunnel Manager + * + */ +public class TunnelSelectionCriteria { + public final static int MAX_PRIORITY = 100; + public final static int MIN_PRIORITY = 0; + private int _latencyPriority; + private int _anonymityPriority; + private int _reliabilityPriority; + private int _maxNeeded; + private int _minNeeded; + + public TunnelSelectionCriteria() { + setLatencyPriority(0); + setAnonymityPriority(0); + setReliabilityPriority(0); + setMinimumTunnelsRequired(0); + setMaximumTunnelsRequired(0); + } + + /** priority of the latency for the tunnel */ + public int getLatencyPriority() { return _latencyPriority; } + public void setLatencyPriority(int latencyPriority) { _latencyPriority = latencyPriority; } + /** priority of the anonymity for the tunnel */ + public int getAnonymityPriority() { return _anonymityPriority; } + public void setAnonymityPriority(int anonPriority) { _anonymityPriority = anonPriority; } + /** priority of the reliability for the tunnel */ + public int getReliabilityPriority() { return _reliabilityPriority; } + public void setReliabilityPriority(int reliabilityPriority) { _reliabilityPriority = reliabilityPriority; } + /** max # of tunnels to return */ + public int getMaximumTunnelsRequired() { return _maxNeeded; } + public void setMaximumTunnelsRequired(int maxNeeded) { _maxNeeded = maxNeeded; } + /** minimum # of tunnels to return */ + public int getMinimumTunnelsRequired() { return _minNeeded; } + public void setMinimumTunnelsRequired(int minNeeded) { _minNeeded = minNeeded; } +} diff --git a/router/java/src/net/i2p/router/TunnelSettings.java b/router/java/src/net/i2p/router/TunnelSettings.java new file mode 100644 index 0000000000..9bb8075e37 --- /dev/null +++ b/router/java/src/net/i2p/router/TunnelSettings.java @@ -0,0 +1,134 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.util.Clock; + +/** + * Wrap up the settings specified for a particular tunnel + * + */ +public class TunnelSettings extends DataStructureImpl { + private int _depth; + private long _msgsPerMinuteAvg; + private long _bytesPerMinuteAvg; + private long _msgsPerMinutePeak; + private long _bytesPerMinutePeak; + private boolean _includeDummy; + private boolean _reorder; + private long _expiration; + private long _created; + + public TunnelSettings() { + _depth = 0; + _msgsPerMinuteAvg = 0; + _msgsPerMinutePeak = 0; + _bytesPerMinuteAvg = 0; + _bytesPerMinutePeak = 0; + _includeDummy = false; + _reorder = false; + _expiration = 0; + _created = Clock.getInstance().now(); + } + + public int getDepth() { return _depth; } + public void setDepth(int depth) { _depth = depth; } + public long getMessagesPerMinuteAverage() { return _msgsPerMinuteAvg; } + public long getMessagesPerMinutePeak() { return _msgsPerMinutePeak; } + public long getBytesPerMinuteAverage() { return _bytesPerMinuteAvg; } + public long getBytesPerMinutePeak() { return _bytesPerMinutePeak; } + public void setMessagesPerMinuteAverage(long msgs) { _msgsPerMinuteAvg = msgs; } + public void setMessagesPerMinutePeak(long msgs) { _msgsPerMinutePeak = msgs; } + public void setBytesPerMinuteAverage(long bytes) { _bytesPerMinuteAvg = bytes; } + public void setBytesPerMinutePeak(long bytes) { _bytesPerMinutePeak = bytes; } + public boolean getIncludeDummy() { return _includeDummy; } + public void setIncludeDummy(boolean include) { _includeDummy = include; } + public boolean getReorder() { return _reorder; } + public void setReorder(boolean reorder) { _reorder = reorder; } + public long getExpiration() { return _expiration; } + public void setExpiration(long expiration) { _expiration = expiration; } + public long getCreated() { return _created; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + Boolean b = DataHelper.readBoolean(in); + if (b == null) throw new DataFormatException("Null includeDummy boolean value"); + _includeDummy = b.booleanValue(); + b = DataHelper.readBoolean(in); + if (b == null) throw new DataFormatException("Null reorder boolean value"); + _reorder = b.booleanValue(); + _depth = (int)DataHelper.readLong(in, 1); + _bytesPerMinuteAvg = DataHelper.readLong(in, 4); + _bytesPerMinutePeak = DataHelper.readLong(in, 4); + Date exp = DataHelper.readDate(in); + if (exp == null) + _expiration = 0; + else + _expiration = exp.getTime(); + _msgsPerMinuteAvg = DataHelper.readLong(in, 4); + _msgsPerMinutePeak = DataHelper.readLong(in, 4); + Date created = DataHelper.readDate(in); + if (created != null) + _created = created.getTime(); + else + _created = Clock.getInstance().now(); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + DataHelper.writeBoolean(out, _includeDummy ? Boolean.TRUE : Boolean.FALSE); + DataHelper.writeBoolean(out, _reorder ? Boolean.TRUE : Boolean.FALSE); + DataHelper.writeLong(out, 1, _depth); + DataHelper.writeLong(out, 4, _bytesPerMinuteAvg); + DataHelper.writeLong(out, 4, _bytesPerMinutePeak); + if (_expiration <= 0) + DataHelper.writeDate(out, new Date(0)); + else + DataHelper.writeDate(out, new Date(_expiration)); + DataHelper.writeLong(out, 4, _msgsPerMinuteAvg); + DataHelper.writeLong(out, 4, _msgsPerMinutePeak); + DataHelper.writeDate(out, new Date(_created)); + } + + + public int hashCode() { + int rv = 0; + rv += _includeDummy ? 100 : 0; + rv += _reorder ? 50 : 0; + rv += _depth; + rv += _bytesPerMinuteAvg; + rv += _bytesPerMinutePeak; + rv += _expiration; + rv += _msgsPerMinuteAvg; + rv += _msgsPerMinutePeak; + return rv; + } + + public boolean equals(Object obj) { + if ( (obj != null) && (obj instanceof TunnelSettings) ) { + TunnelSettings settings = (TunnelSettings)obj; + return settings.getBytesPerMinuteAverage() == getBytesPerMinuteAverage() && + settings.getBytesPerMinutePeak() == getBytesPerMinutePeak() && + settings.getDepth() == getDepth() && + settings.getExpiration() == getExpiration() && + settings.getIncludeDummy() == getIncludeDummy() && + settings.getMessagesPerMinuteAverage() == getMessagesPerMinuteAverage() && + settings.getMessagesPerMinutePeak() == getMessagesPerMinutePeak() && + settings.getReorder() == getReorder(); + } else { + return false; + } + } +} diff --git a/router/java/src/net/i2p/router/admin/AdminListener.java b/router/java/src/net/i2p/router/admin/AdminListener.java new file mode 100644 index 0000000000..e779c775b7 --- /dev/null +++ b/router/java/src/net/i2p/router/admin/AdminListener.java @@ -0,0 +1,110 @@ +package net.i2p.router.admin; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.net.ServerSocket; +import java.net.Socket; + +import java.io.IOException; + +import net.i2p.util.Log; +import net.i2p.util.I2PThread; + +/** + * Listen for connections on the specified port, and toss them onto the client manager's + * set of connections once they are established. + * + * @author jrandom + */ +public class AdminListener implements Runnable { + private final static Log _log = new Log(AdminListener.class); + private ServerSocket _socket; + private int _port; + private boolean _running; + private long _nextFailDelay = 1000; + + public AdminListener(int port) { + _port = port; + _running = false; + } + + public void setPort(int port) { _port = port; } + public int getPort() { return _port; } + + /** max time to bind */ + private final static int MAX_FAIL_DELAY = 5*60*1000; + + /** + * Start up the socket listener, listens for connections, and + * fires those connections off via {@link #runConnection runConnection}. + * This only returns if the socket cannot be opened or there is a catastrophic + * failure. + * + */ + public void startup() { + _running = true; + int curDelay = 0; + while ( (_running) && (curDelay < MAX_FAIL_DELAY) ) { + try { + _log.info("Starting up listening for connections on port " + _port); + _socket = new ServerSocket(_port); + curDelay = 0; + while (_running) { + try { + Socket socket = _socket.accept(); + _log.debug("Connection received"); + runConnection(socket); + } catch (IOException ioe) { + _log.error("Server error accepting", ioe); + } catch (Throwable t) { + _log.error("Fatal error running client listener - killing the thread!", t); + return; + } + } + } catch (IOException ioe) { + _log.error("Error listening on port " + _port, ioe); + } + + if (_socket != null) { + try { _socket.close(); } catch (IOException ioe) {} + _socket = null; + } + + _log.error("Error listening, waiting " + _nextFailDelay + "ms before we try again"); + try { Thread.sleep(_nextFailDelay); } catch (InterruptedException ie) {} + curDelay += _nextFailDelay; + _nextFailDelay *= 5; + } + + _log.error("CANCELING ADMIN LISTENER. delay = " + curDelay, new Exception("ADMIN LISTENER cancelled!!!")); + _running = false; + } + + /** + * Handle the connection by passing it off to an AdminRunner + * + */ + protected void runConnection(Socket socket) throws IOException { + AdminRunner runner = new AdminRunner(socket); + I2PThread t = new I2PThread(runner); + t.setName("Admin Runner"); + t.setPriority(Thread.MIN_PRIORITY); + t.setDaemon(true); + t.start(); + } + + public void shutdown() { + _running = false; + if (_socket != null) try { + _socket.close(); + _socket = null; + } catch (IOException ioe) {} + } + public void run() { startup(); } +} diff --git a/router/java/src/net/i2p/router/admin/AdminManager.java b/router/java/src/net/i2p/router/admin/AdminManager.java new file mode 100644 index 0000000000..5707f3f39d --- /dev/null +++ b/router/java/src/net/i2p/router/admin/AdminManager.java @@ -0,0 +1,51 @@ +package net.i2p.router.admin; + +import net.i2p.util.Log; +import net.i2p.util.I2PThread; + +import net.i2p.router.Service; +import net.i2p.router.Router; + +public class AdminManager implements Service { + private final static Log _log = new Log(AdminManager.class); + private final static AdminManager _instance = new AdminManager(); + public final static AdminManager getInstance() { return _instance; } + public final static String PARAM_ADMIN_PORT = "router.adminPort"; + public final static int DEFAULT_ADMIN_PORT = 7655; + + private AdminListener _listener; + + public String renderStatusHTML() { return ""; } + + public void shutdown() { + if (_listener != null) { + _log.info("Shutting down admin listener"); + _listener.shutdown(); + _listener = null; + } + } + + public void startup() { + int port = DEFAULT_ADMIN_PORT; + String str = Router.getInstance().getConfigSetting(PARAM_ADMIN_PORT); + if (str != null) { + try { + int val = Integer.parseInt(str); + port = val; + } catch (NumberFormatException nfe) { + _log.warn("Invalid admin port specified [" + str + "]", nfe); + } + } + _log.info("Starting up admin listener on port " + port); + startup(port); + } + + private void startup(int port) { + _listener = new AdminListener(port); + I2PThread t = new I2PThread(_listener); + t.setName("Admin Listener"); + t.setDaemon(true); + t.setPriority(Thread.MIN_PRIORITY); + t.start(); + } +} diff --git a/router/java/src/net/i2p/router/admin/AdminRunner.java b/router/java/src/net/i2p/router/admin/AdminRunner.java new file mode 100644 index 0000000000..033ae366e3 --- /dev/null +++ b/router/java/src/net/i2p/router/admin/AdminRunner.java @@ -0,0 +1,102 @@ +package net.i2p.router.admin; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Set; +import java.util.Iterator; + +import net.i2p.router.Router; +import net.i2p.data.Hash; +import net.i2p.router.peermanager.ProfileOrganizer; +import net.i2p.util.Log; + +class AdminRunner implements Runnable { + private final static Log _log = new Log(AdminRunner.class); + private Socket _socket; + + public AdminRunner(Socket socket) { + _socket = socket; + } + + public void run() { + try { + BufferedReader in = new BufferedReader(new InputStreamReader(_socket.getInputStream())); + OutputStream out = _socket.getOutputStream(); + + String command = in.readLine(); + runCommand(command, out); + } catch (IOException ioe) { + _log.error("Error running admin command", ioe); + } + } + + private void runCommand(String command, OutputStream out) throws IOException { + _log.debug("Command [" + command + "]"); + if (command.indexOf("favicon") >= 0) { + reply(out, "this is not a website"); + } else if (command.indexOf("routerStats.html") >= 0) { + reply(out, StatsGenerator.generateStatsPage()); + } else if (command.indexOf("/profile/") >= 0) { + replyText(out, getProfile(command)); + } else if (true || command.indexOf("routerConsole.html") > 0) { + reply(out, Router.getInstance().renderStatusHTML()); + } + } + + private void reply(OutputStream out, String content) throws IOException { + StringBuffer reply = new StringBuffer(10240); + reply.append("HTTP/1.1 200 OK\n"); + reply.append("Connection: close\n"); + reply.append("Cache-control: no-cache\n"); + reply.append("Content-type: text/html\n\n"); + reply.append(content); + try { + out.write(reply.toString().getBytes()); + out.close(); + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error writing out the admin reply:\n" + content); + throw ioe; + } + } + + private void replyText(OutputStream out, String content) throws IOException { + StringBuffer reply = new StringBuffer(10240); + reply.append("HTTP/1.1 200 OK\n"); + reply.append("Connection: close\n"); + reply.append("Cache-control: no-cache\n"); + reply.append("Content-type: text/plain\n\n"); + reply.append(content); + try { + out.write(reply.toString().getBytes()); + out.close(); + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error writing out the admin reply:\n" + content); + throw ioe; + } + } + + private String getProfile(String cmd) { + Set peers = ProfileOrganizer._getInstance().selectAllPeers(); + for (Iterator iter = peers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + if (cmd.indexOf(peer.toBase64().substring(0,10)) >= 0) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(64*1024); + ProfileOrganizer._getInstance().exportProfile(peer, baos); + return new String(baos.toByteArray()); + } catch (IOException ioe) { + _log.error("Error exporting the profile", ioe); + return "Error exporting the peer profile\n"; + } + } + } + + return "No such peer is being profiled\n"; + } +} diff --git a/router/java/src/net/i2p/router/admin/StatsGenerator.java b/router/java/src/net/i2p/router/admin/StatsGenerator.java new file mode 100644 index 0000000000..c7f4d97e8e --- /dev/null +++ b/router/java/src/net/i2p/router/admin/StatsGenerator.java @@ -0,0 +1,214 @@ +package net.i2p.router.admin; + +import net.i2p.util.Log; +import net.i2p.stat.StatManager; +import net.i2p.stat.Frequency; +import net.i2p.stat.FrequencyStat; +import net.i2p.stat.Rate; +import net.i2p.stat.RateStat; +import net.i2p.router.Router; +import net.i2p.data.DataHelper; + +import java.util.Set; +import java.util.Map; +import java.util.Iterator; +import java.util.Arrays; + +import java.text.DecimalFormat; + +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.io.IOException; + +/** + * Dump the stats to the web admin interface + */ +public class StatsGenerator { + private final static Log _log = new Log(StatsGenerator.class); + + public static String generateStatsPage() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024); + try { + generateStatsPage(baos); + } catch (IOException ioe) { + _log.error("Error generating stats", ioe); + } + return new String(baos.toByteArray()); + } + + public static void generateStatsPage(OutputStream out) throws IOException { + PrintWriter pw = new PrintWriter(out); + pw.println("I2P Router Stats"); + pw.println("

Router statistics

"); + pw.println("console | stats
"); + Map groups = StatManager.getInstance().getStatsByGroup(); + + pw.println("
"); + pw.println(""); + pw.println("
"); + + pw.print("Statistics gathered during this router's uptime ("); + long uptime = Router.getInstance().getUptime(); + pw.print(DataHelper.formatDuration(uptime)); + pw.println("). The data gathered is quantized over a 1 minute period, so should just be used as an estimate

"); + + for (Iterator iter = groups.keySet().iterator(); iter.hasNext(); ) { + String group = (String)iter.next(); + Set stats = (Set)groups.get(group); + pw.print("

"); + pw.print(group); + pw.println("

"); + pw.println("
    "); + for (Iterator statIter = stats.iterator(); statIter.hasNext(); ) { + String stat = (String)statIter.next(); + pw.print("
  • "); + pw.print(stat); + pw.println("
    "); + if (StatManager.getInstance().isFrequency(stat)) + renderFrequency(stat, pw); + else + renderRate(stat, pw); + } + pw.println("

"); + } + pw.println(""); + pw.flush(); + } + + private static void renderFrequency(String name, PrintWriter pw) throws IOException { + FrequencyStat freq = StatManager.getInstance().getFrequency(name); + pw.print(""); + pw.print(freq.getDescription()); + pw.println("
"); + long periods[] = freq.getPeriods(); + Arrays.sort(periods); + for (int i = 0; i < periods.length; i++) { + renderPeriod(pw, periods[i], "frequency"); + Frequency curFreq = freq.getFrequency(periods[i]); + pw.print(" avg per period: ("); + pw.print(num(curFreq.getAverageEventsPerPeriod())); + pw.print(", max "); + pw.print(num(curFreq.getMaxAverageEventsPerPeriod())); + if ( (curFreq.getMaxAverageEventsPerPeriod() > 0) && (curFreq.getAverageEventsPerPeriod() > 0) ) { + pw.print(", current is "); + pw.print(pct(curFreq.getAverageEventsPerPeriod()/curFreq.getMaxAverageEventsPerPeriod())); + pw.print(" of max"); + } + pw.print(")"); + //buf.append(" avg interval between updates: (").append(num(curFreq.getAverageInterval())).append("ms, min "); + //buf.append(num(curFreq.getMinAverageInterval())).append("ms)"); + pw.print(" strict average per period: "); + pw.print(num(curFreq.getStrictAverageEventsPerPeriod())); + pw.print(" events (averaged "); + pw.print(" using the lifetime of "); + pw.print(num(curFreq.getEventCount())); + pw.print(" events)"); + pw.println("
"); + } + pw.println("
"); + } + + private static void renderRate(String name, PrintWriter pw) throws IOException { + RateStat rate = StatManager.getInstance().getRate(name); + pw.print(""); + pw.print(rate.getDescription()); + pw.println("
"); + long periods[] = rate.getPeriods(); + Arrays.sort(periods); + pw.println("
    "); + for (int i = 0; i < periods.length; i++) { + pw.println("
  • "); + renderPeriod(pw, periods[i], "rate"); + Rate curRate = rate.getRate(periods[i]); + pw.print( "avg value: ("); + pw.print(num(curRate.getAverageValue())); + pw.print(" peak "); + pw.print(num(curRate.getExtremeAverageValue())); + pw.print(", ["); + pw.print(pct(curRate.getPercentageOfExtremeValue())); + pw.print(" of max"); + pw.print(", and "); + pw.print(pct(curRate.getPercentageOfLifetimeValue())); + pw.print(" of lifetime average]"); + + pw.print(")"); + pw.print(" highest total period value: ("); + pw.print(num(curRate.getExtremeTotalValue())); + pw.print(")"); + if (curRate.getLifetimeTotalEventTime() > 0) { + pw.print(" saturation: ("); + pw.print(pct(curRate.getLastEventSaturation())); + pw.print(")"); + pw.print(" saturated limit: ("); + pw.print(num(curRate.getLastSaturationLimit())); + pw.print(")"); + pw.print(" peak saturation: ("); + pw.print(pct(curRate.getExtremeEventSaturation())); + pw.print(")"); + pw.print(" peak saturated limit: ("); + pw.print(num(curRate.getExtremeSaturationLimit())); + pw.print(")"); + } + pw.print(" events per period: "); + pw.print(num(curRate.getLastEventCount())); + long numPeriods = curRate.getLifetimePeriods(); + if (numPeriods > 0) { + double avgFrequency = curRate.getLifetimeEventCount() / (double)numPeriods; + double peakFrequency = curRate.getExtremeEventCount(); + pw.print(" (lifetime average: "); + pw.print(num(avgFrequency)); + pw.print(", peak average: "); + pw.print(num(curRate.getExtremeEventCount())); + pw.println(")"); + } + pw.print("
  • "); + if (i + 1 == periods.length) { + // last one, so lets display the strict average + pw.print("
  • lifetime average value: "); + pw.print(num(curRate.getLifetimeAverageValue())); + pw.print(" over "); + pw.print(num(curRate.getLifetimeEventCount())); + pw.println(" events
  • "); + } + } + pw.print("
"); + pw.println("
"); + } + + private static void renderPeriod(PrintWriter pw, long period, String name) throws IOException { + pw.print(""); + pw.print(DataHelper.formatDuration(period)); + pw.print(" "); + pw.print(name); + pw.print(": "); + } + + private final static DecimalFormat _fmt = new DecimalFormat("###,##0.00"); + private final static String num(double num) { synchronized (_fmt) { return _fmt.format(num); } } + + private final static DecimalFormat _pct = new DecimalFormat("#0.00%"); + private final static String pct(double num) { synchronized (_pct) { return _pct.format(num); } } +} diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java new file mode 100644 index 0000000000..478c92482f --- /dev/null +++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java @@ -0,0 +1,404 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.i2p.data.Destination; +import net.i2p.data.LeaseSet; +import net.i2p.data.Payload; +import net.i2p.data.i2cp.DisconnectMessage; +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.data.i2cp.I2CPMessageReader; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.MessageStatusMessage; +import net.i2p.data.i2cp.SendMessageMessage; +import net.i2p.data.i2cp.SessionConfig; +import net.i2p.data.i2cp.SessionId; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; + +/** + * Bridge the router and the client - managing state for a client. + * + * @author jrandom + */ +public class ClientConnectionRunner { + private final static Log _log = new Log(ClientConnectionRunner.class); + private ClientManager _manager; + /** socket for this particular peer connection */ + private Socket _socket; + /** output stream of the socket that I2CP messages bound to the client should be written to */ + private OutputStream _out; + /** session ID of the current client */ + private SessionId _sessionId; + /** user's config */ + private SessionConfig _config; + /** static mapping of MessageId to Payload, storing messages for retrieval */ + private static Map _messages; + /** lease set request state, or null if there is no request pending on at the moment */ + private LeaseRequestState _leaseRequest; + /** currently allocated leaseSet, or null if none is allocated */ + private LeaseSet _currentLeaseSet; + /** set of messageIds created but not yet ACCEPTED */ + private Set _acceptedPending; + /** thingy that does stuff */ + private I2CPMessageReader _reader; + /** + * This contains the last 10 MessageIds that have had their (non-ack) status + * delivered to the client (so that we can be sure only to update when necessary) + */ + private List _alreadyProcessed; + /** are we, uh, dead */ + private boolean _dead; + + /** + * Create a new runner against the given socket + * + */ + public ClientConnectionRunner(ClientManager manager, Socket socket) { + _manager = manager; + _socket = socket; + _config = null; + _messages = new HashMap(); + _alreadyProcessed = new LinkedList(); + _acceptedPending = new HashSet(); + _dead = false; + } + + /** + * Actually run the connection - listen for I2CP messages and respond. This + * is the main driver for this class, though it gets all its meat from the + * {@link net.i2p.data.i2cp.I2CPMessageReader I2CPMessageReader} + * + */ + public void startRunning() { + try { + _reader = new I2CPMessageReader(_socket.getInputStream(), new ClientMessageEventListener(this)); + _out = _socket.getOutputStream(); + _reader.startReading(); + } catch (IOException ioe) { + _log.error("Error starting up the runner", ioe); + } + } + + /** die a horrible death */ + void stopRunning() { + if (_dead) return; + _log.error("Stop the I2CP connection! current leaseSet: " + _currentLeaseSet, new Exception("Stop client connection")); + _dead = true; + // we need these keys to unpublish the leaseSet + if (_reader != null) _reader.stopReading(); + if (_socket != null) try { _socket.close(); } catch (IOException ioe) { } + synchronized (_messages) { + _messages.clear(); + } + _manager.unregisterConnection(this); + if (_currentLeaseSet != null) + NetworkDatabaseFacade.getInstance().unpublish(_currentLeaseSet); + _leaseRequest = null; + synchronized (_alreadyProcessed) { + _alreadyProcessed.clear(); + } + _config = null; + _manager = null; + } + + /** current client's config */ + public SessionConfig getConfig() { return _config; } + /** currently allocated leaseSet */ + public LeaseSet getLeaseSet() { return _currentLeaseSet; } + void setLeaseSet(LeaseSet ls) { _currentLeaseSet = ls; } + + /** current client's sessionId */ + SessionId getSessionId() { return _sessionId; } + void setSessionId(SessionId id) { _sessionId = id; } + /** data for the current leaseRequest, or null if there is no active leaseSet request */ + LeaseRequestState getLeaseRequest() { return _leaseRequest; } + void setLeaseRequest(LeaseRequestState req) { _leaseRequest = req; } + /** already closed? */ + boolean isDead() { return _dead; } + /** message body */ + Payload getPayload(MessageId id) { synchronized (_messages) { return (Payload)_messages.get(id); } } + void setPayload(MessageId id, Payload payload) { synchronized (_messages) { _messages.put(id, payload); } } + void removePayload(MessageId id) { synchronized (_messages) { _messages.remove(id); } } + + void sessionEstablished(SessionConfig config) { + _config = config; + _manager.destinationEstablished(this); + } + + void updateMessageDeliveryStatus(MessageId id, boolean delivered) { + if (_dead) return; + JobQueue.getInstance().addJob(new MessageDeliveryStatusUpdate(id, delivered)); + } + /** + * called after a new leaseSet is granted by the client, the NetworkDb has been + * updated. This takes care of all the LeaseRequestState stuff (including firing any jobs) + */ + void leaseSetCreated(LeaseSet ls) { + if (_leaseRequest == null) { + _log.error("LeaseRequest is null and we've received a new lease?! WTF"); + return; + } else { + _leaseRequest.setIsSuccessful(true); + if (_leaseRequest.getOnGranted() != null) + JobQueue.getInstance().addJob(_leaseRequest.getOnGranted()); + _leaseRequest = null; + _currentLeaseSet = ls; + } + } + + void disconnectClient(String reason) { + _log.error("Disconnecting the client: " + reason, new Exception("Disconnecting!")); + DisconnectMessage msg = new DisconnectMessage(); + msg.setReason(reason); + try { + doSend(msg); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the disconnect message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the disconnect message", ioe); + } + stopRunning(); + } + + /** + * Distribute the message. If the dest is local, it blocks until its passed + * to the target ClientConnectionRunner (which then fires it into a MessageReceivedJob). + * If the dest is remote, it blocks until it is added into the ClientMessagePool + * + */ + MessageId distributeMessage(SendMessageMessage message) { + Payload payload = message.getPayload(); + Destination dest = message.getDestination(); + MessageId id = new MessageId(); + id.setMessageId(getNextMessageId()); + synchronized (_acceptedPending) { + _acceptedPending.add(id); + } + _log.debug("** Recieving message [" + id.getMessageId() + "] with payload of size [" + payload.getSize() + "]" + " for session [" + _sessionId.getSessionId() + "]"); + // the following blocks as described above + _manager.distributeMessage(_config.getDestination(), message.getDestination(), message.getPayload(), id); + return id; + } + + /** + * Send a notification to the client that their message (id specified) was accepted + * for delivery (but not necessarily delivered) + * + */ + void ackSendMessage(MessageId id, long nonce) { + _log.debug("Acking message send [accepted]" + id + " / " + nonce + " for sessionId " + _sessionId, new Exception("sendAccepted")); + MessageStatusMessage status = new MessageStatusMessage(); + status.setMessageId(id); + status.setSessionId(_sessionId); + status.setSize(0L); + status.setNonce(nonce); + status.setStatus(MessageStatusMessage.STATUS_SEND_ACCEPTED); + try { + doSend(status); + synchronized (_acceptedPending) { + _acceptedPending.remove(id); + } + } catch (I2CPMessageException ime) { + _log.error("Error writing out the message status message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the message status message", ioe); + } + } + + /** + * Asynchronously deliver the message to the current runner + * + */ + void receiveMessage(Destination toDest, Destination fromDest, Payload payload) { + if (_dead) return; + JobQueue.getInstance().addJob(new MessageReceivedJob(this, toDest, fromDest, payload)); + } + + /** + * Send async abuse message to the client + * + */ + public void reportAbuse(String reason, int severity) { + if (_dead) return; + JobQueue.getInstance().addJob(new ReportAbuseJob(this, reason, severity)); + } + + /** + * Request that a particular client authorize the Leases contained in the + * LeaseSet, after which the onCreateJob is queued up. If that doesn't occur + * within the timeout specified, queue up the onFailedJob. This call does not + * block. + * + * @param set LeaseSet with requested leases - this object must be updated to contain the + * signed version (as well as any changed/added/removed Leases) + * @param expirationTime ms to wait before failing + * @param onCreateJob Job to run after the LeaseSet is authorized + * @param onFailedJob Job to run after the timeout passes without receiving authorization + */ + void requestLeaseSet(LeaseSet set, long expirationTime, Job onCreateJob, Job onFailedJob) { + if (_dead) return; + JobQueue.getInstance().addJob(new RequestLeaseSetJob(this, set, expirationTime, onCreateJob, onFailedJob)); + } + + void disconnected() { + _log.error("Disconnected", new Exception("Disconnected?")); + stopRunning(); + } + + //// + //// + + /** + * Actually send the I2CPMessage to the peer through the socket + * + */ + void doSend(I2CPMessage msg) throws I2CPMessageException, IOException { + if (_out == null) throw new I2CPMessageException("Output stream is not initialized"); + long before = Clock.getInstance().now(); + try { + synchronized (_out) { + msg.writeMessage(_out); + _out.flush(); + } + } catch (I2CPMessageException ime) { + _log.error("Message exception sending I2CP message", ime); + throw ime; + } catch (IOException ioe) { + _log.error("IO exception sending I2CP message", ioe); + throw ioe; + } catch (Throwable t) { + _log.log(Log.CRIT, "Unhandled exception sending I2CP message", t); + throw new IOException("Unhandled exception sending I2CP message: " + t.getMessage()); + } finally { + long after = Clock.getInstance().now(); + long lag = after - before; + if (lag > 300) { + _log.error("synchronization on the i2cp message send took too long (" + lag + "ms): " + msg, new Exception("I2CP Lag")); + } + } + } + + // this *should* be mod 65536, but UnsignedInteger is still b0rked. FIXME + private final static int MAX_MESSAGE_ID = 32767; + private static volatile int _messageId = RandomSource.getInstance().nextInt(MAX_MESSAGE_ID); // messageId counter + private static Object _messageIdLock = new Object(); + + static int getNextMessageId() { + synchronized (_messageIdLock) { + int messageId = (++_messageId)%MAX_MESSAGE_ID; + if (_messageId >= MAX_MESSAGE_ID) + _messageId = 0; + return messageId; + } + } + + /** + * True if the client has already been sent the ACCEPTED state for the given + * message id, false otherwise. + * + */ + private boolean alreadyAccepted(MessageId id) { + if (_dead) return false; + boolean isPending = false; + int pending = 0; + String buf = null; + synchronized (_acceptedPending) { + if (_acceptedPending.contains(id)) + isPending = true; + pending = _acceptedPending.size(); + buf = _acceptedPending.toString(); + } + if (pending >= 1) { + _log.warn("Pending acks: " + pending + ": " + buf); + } + return !isPending; + } + + /** + * If the message hasn't been state=ACCEPTED yet, we shouldn't send an update + * since the client doesn't know the message id (and we don't know the nonce). + * So, we just wait REQUEUE_DELAY ms before trying again. + * + */ + private final static long REQUEUE_DELAY = 500; + + private class MessageDeliveryStatusUpdate extends JobImpl { + private MessageId _messageId; + private boolean _success; + private long _lastTried; + public MessageDeliveryStatusUpdate(MessageId id, boolean success) { + _messageId = id; + _success = success; + _lastTried = 0; + } + + public String getName() { return "Update Delivery Status"; } + public void runJob() { + if (_dead) return; + + MessageStatusMessage msg = new MessageStatusMessage(); + msg.setMessageId(_messageId); + msg.setSessionId(_sessionId); + msg.setNonce(2); + msg.setSize(0); + if (_success) + msg.setStatus(MessageStatusMessage.STATUS_SEND_GUARANTEED_SUCCESS); + else + msg.setStatus(MessageStatusMessage.STATUS_SEND_GUARANTEED_FAILURE); + + if (!alreadyAccepted(_messageId)) { + _log.warn("Almost send an update for message " + _messageId + " to " + MessageStatusMessage.getStatusString(msg.getStatus()) + " for session [" + _sessionId.getSessionId() + "] before they knew the messageId! delaying .5s"); + _lastTried = Clock.getInstance().now(); + requeue(REQUEUE_DELAY); + return; + } + + synchronized (_alreadyProcessed) { + if (_alreadyProcessed.contains(_messageId)) { + _log.warn("Status already updated"); + return; + } else { + _alreadyProcessed.add(_messageId); + while (_alreadyProcessed.size() > 10) + _alreadyProcessed.remove(0); + } + } + + if (_lastTried > 0) + _log.info("Updating message status for message " + _messageId + " to " + MessageStatusMessage.getStatusString(msg.getStatus()) + " for session [" + _sessionId.getSessionId() + "] (with nonce=2), retrying after [" + (Clock.getInstance().now() - _lastTried) + "]", getAddedBy()); + else + _log.debug("Updating message status for message " + _messageId + " to " + MessageStatusMessage.getStatusString(msg.getStatus()) + " for session [" + _sessionId.getSessionId() + "] (with nonce=2)"); + + try { + doSend(msg); + } catch (I2CPMessageException ime) { + _log.warn("Error updating the status for message ID " + _messageId, ime); + } catch (IOException ioe) { + _log.warn("Error updating the status for message ID " + _messageId, ioe); + } + } + } +} diff --git a/router/java/src/net/i2p/router/client/ClientListenerRunner.java b/router/java/src/net/i2p/router/client/ClientListenerRunner.java new file mode 100644 index 0000000000..3873893b60 --- /dev/null +++ b/router/java/src/net/i2p/router/client/ClientListenerRunner.java @@ -0,0 +1,132 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.net.ServerSocket; +import java.net.Socket; + +import java.io.IOException; + +import net.i2p.client.I2PClient; +import net.i2p.util.Log; + +/** + * Listen for connections on the specified port, and toss them onto the client manager's + * set of connections once they are established. + * + * @author jrandom + */ +public class ClientListenerRunner implements Runnable { + private final static Log _log = new Log(ClientListenerRunner.class); + private ClientManager _manager; + private ServerSocket _socket; + private int _port; + private boolean _running; + private long _nextFailDelay = 1000; + + public ClientListenerRunner(ClientManager manager, int port) { + _manager = manager; + _port = port; + _running = false; + } + + public void setPort(int port) { _port = port; } + public int getPort() { return _port; } + + /** max time to bind */ + private final static int MAX_FAIL_DELAY = 5*60*1000; + + /** + * Start up the socket listener, listens for connections, and + * fires those connections off via {@link #runConnection runConnection}. + * This only returns if the socket cannot be opened or there is a catastrophic + * failure. + * + */ + public void runServer() { + _running = true; + int curDelay = 0; + while ( (_running) && (curDelay < MAX_FAIL_DELAY) ) { + try { + _log.info("Starting up listening for connections on port " + _port); + _socket = new ServerSocket(_port); + curDelay = 0; + while (_running) { + try { + Socket socket = _socket.accept(); + if (validate(socket)) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Connection received"); + runConnection(socket); + } else { + socket.close(); + if (_log.shouldLog(Log.WARN)) + _log.warn("Refused connection from " + socket.getInetAddress().toString()); + } + } catch (IOException ioe) { + _log.error("Server error accepting", ioe); + } catch (Throwable t) { + _log.error("Fatal error running client listener - killing the thread!", t); + return; + } + } + } catch (IOException ioe) { + _log.error("Error listening on port " + _port, ioe); + } + + if (_socket != null) { + try { _socket.close(); } catch (IOException ioe) {} + _socket = null; + } + + _log.error("Error listening, waiting " + _nextFailDelay + "ms before we try again"); + try { Thread.sleep(_nextFailDelay); } catch (InterruptedException ie) {} + curDelay += _nextFailDelay; + _nextFailDelay *= 5; + } + + _log.error("CANCELING I2CP LISTEN. delay = " + curDelay, new Exception("I2CP Listen cancelled!!!")); + _running = false; + } + + /** give the i2cp client 5 seconds to show that they're really i2cp clients */ + private final static int CONNECT_TIMEOUT = 5*1000; + + private boolean validate(Socket socket) { + try { + socket.setSoTimeout(CONNECT_TIMEOUT); + int read = socket.getInputStream().read(); + if (read != I2PClient.PROTOCOL_BYTE) + return false; + socket.setSoTimeout(0); + return true; + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Peer did not authenticate themselves as I2CP quickly enough, dropping"); + return false; + } + } + /** + * Handle the connection by passing it off to a {@link ClientConnectionRunner ClientConnectionRunner} + * + */ + protected void runConnection(Socket socket) throws IOException { + ClientConnectionRunner runner = new ClientConnectionRunner(_manager, socket); + _manager.registerConnection(runner); + } + + public void stopListening() { + _running = false; + if (_socket != null) try { + _socket.close(); + _socket = null; + } catch (IOException ioe) {} + } + public void run() { runServer(); } +} diff --git a/router/java/src/net/i2p/router/client/ClientManager.java b/router/java/src/net/i2p/router/client/ClientManager.java new file mode 100644 index 0000000000..b0f3c08c4e --- /dev/null +++ b/router/java/src/net/i2p/router/client/ClientManager.java @@ -0,0 +1,306 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.Payload; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.SessionConfig; +import net.i2p.router.ClientMessage; +import net.i2p.router.ClientMessagePool; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import net.i2p.stat.StatManager; + +/** + * Coordinate connections and various tasks + * + * @author jrandom + */ +public class ClientManager { + private final static Log _log = new Log(ClientManager.class); + private ClientListenerRunner _listener; + private HashMap _runners; // Destination --> ClientConnectionRunner + private Set _pendingRunners; // ClientConnectionRunner for clients w/out a Dest yet + + /** ms to wait before rechecking for inbound messages to deliver to clients */ + private final static int INBOUND_POLL_INTERVAL = 300; + + static { + StatManager.getInstance().createRateStat("client.receiveMessageSize", "How large are messages received by the client?", "Client Messages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + public ClientManager(int port) { + _runners = new HashMap(); + _pendingRunners = new HashSet(); + _listener = new ClientListenerRunner(this, port); + Thread t = new I2PThread(_listener); + t.setName("ClientListener"); + t.setDaemon(true); + t.start(); + + //JobQueue.getInstance().addJob(new CheckInboundMessagesJob()); + } + + public void shutdown() { + _log.info("Shutting down the ClientManager"); + _listener.stopListening(); + Set runners = new HashSet(); + synchronized (_runners) { + for (Iterator iter = _runners.values().iterator(); iter.hasNext();) { + ClientConnectionRunner runner = (ClientConnectionRunner)iter.next(); + runners.add(runner); + } + } + synchronized (_pendingRunners) { + for (Iterator iter = _pendingRunners.iterator(); iter.hasNext();) { + ClientConnectionRunner runner = (ClientConnectionRunner)iter.next(); + runners.add(runner); + } + } + for (Iterator iter = runners.iterator(); iter.hasNext(); ) { + ClientConnectionRunner runner = (ClientConnectionRunner)iter.next(); + runner.stopRunning(); + } + } + + public void registerConnection(ClientConnectionRunner runner) { + synchronized (_pendingRunners) { + _pendingRunners.add(runner); + } + runner.startRunning(); + } + + public void unregisterConnection(ClientConnectionRunner runner) { + _log.warn("Unregistering (dropping) a client connection"); + synchronized (_pendingRunners) { + _pendingRunners.remove(runner); + } + if ( (runner.getConfig() != null) && (runner.getConfig().getDestination() != null) ) { + // after connection establishment + synchronized (_runners) { + _runners.remove(runner.getConfig().getDestination()); + } + } + } + + public void destinationEstablished(ClientConnectionRunner runner) { + synchronized (_pendingRunners) { + _pendingRunners.remove(runner); + } + synchronized (_runners) { + _runners.put(runner.getConfig().getDestination(), runner); + } + } + + void distributeMessage(Destination fromDest, Destination toDest, Payload payload, MessageId msgId) { + // check if there is a runner for it + ClientConnectionRunner runner = getRunner(toDest); + if (runner != null) { + _log.debug("Message " + msgId + " is targeting a local destination. distribute it as such"); + runner.receiveMessage(toDest, fromDest, payload); + if (fromDest != null) { + ClientConnectionRunner sender = getRunner(fromDest); + if (sender != null) { + sender.updateMessageDeliveryStatus(msgId, true); + } else { + _log.log(Log.CRIT, "Um, wtf, we're sending a local message, but we can't find who sent it?", new Exception("wtf")); + } + } + } else { + // remote. w00t + _log.debug("Message " + msgId + " is targeting a REMOTE destination! Added to the client message pool"); + runner = getRunner(fromDest); + ClientMessage msg = new ClientMessage(); + msg.setDestination(toDest); + msg.setPayload(payload); + msg.setReceptionInfo(null); + msg.setSenderConfig(runner.getConfig()); + msg.setFromDestination(runner.getConfig().getDestination()); + msg.setMessageId(msgId); + ClientMessagePool.getInstance().add(msg); + } + } + + + /** + * Request that a particular client authorize the Leases contained in the + * LeaseSet, after which the onCreateJob is queued up. If that doesn't occur + * within the timeout specified, queue up the onFailedJob. This call does not + * block. + * + * @param dest Destination from which the LeaseSet's authorization should be requested + * @param set LeaseSet with requested leases - this object must be updated to contain the + * signed version (as well as any changed/added/removed Leases) + * @param timeout ms to wait before failing + * @param onCreateJob Job to run after the LeaseSet is authorized + * @param onFailedJob Job to run after the timeout passes without receiving authorization + */ + public void requestLeaseSet(Destination dest, LeaseSet set, long timeout, Job onCreateJob, Job onFailedJob) { + ClientConnectionRunner runner = getRunner(dest); + if (runner == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Cannot request the lease set, as we can't find a client runner for " + dest.calculateHash().toBase64() + ". disconnected?"); + JobQueue.getInstance().addJob(onFailedJob); + } else { + runner.requestLeaseSet(set, Clock.getInstance().now() + timeout, onCreateJob, onFailedJob); + } + } + + + public boolean isLocal(Destination dest) { + synchronized (_runners) { + return (_runners.containsKey(dest)); + } + } + public boolean isLocal(Hash destHash) { + if (destHash == null) return false; + Set dests = new HashSet(); + synchronized (_runners) { + dests.addAll(_runners.keySet()); + } + for (Iterator iter = dests.iterator(); iter.hasNext();) { + Destination d = (Destination)iter.next(); + if (d.calculateHash().equals(destHash)) return true; + } + return false; + } + + private ClientConnectionRunner getRunner(Destination dest) { + synchronized (_runners) { + return (ClientConnectionRunner)_runners.get(dest); + } + } + + /** + * Return the client's current config, or null if not connected + * + */ + public SessionConfig getClientSessionConfig(Destination dest) { + ClientConnectionRunner runner = getRunner(dest); + if (runner != null) + return runner.getConfig(); + else + return null; + } + + private ClientConnectionRunner getRunner(Hash destHash) { + if (destHash == null) + return null; + Set dests = new HashSet(); + synchronized (_runners) { + dests.addAll(_runners.keySet()); + } + for (Iterator iter = dests.iterator(); iter.hasNext(); ) { + Destination d = (Destination)iter.next(); + if (d.calculateHash().equals(destHash)) + return getRunner(d); + } + return null; + } + + public void messageDeliveryStatusUpdate(Destination fromDest, MessageId id, boolean delivered) { + ClientConnectionRunner runner = getRunner(fromDest); + if (runner != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Delivering status [" + (delivered?"success":"failure") + "] to " + fromDest.calculateHash().toBase64() + " for message " + id); + runner.updateMessageDeliveryStatus(id, delivered); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Cannot deliver status [" + (delivered?"success":"failure") + "] to " + fromDest.calculateHash().toBase64() + " for message " + id); + } + } + + private Set getRunnerDestinations() { + Set dests = new HashSet(); + synchronized (_runners) { + dests.addAll(_runners.keySet()); + } + return dests; + } + + public void reportAbuse(Destination dest, String reason, int severity) { + if (dest != null) { + ClientConnectionRunner runner = getRunner(dest); + if (runner != null) { + runner.reportAbuse(reason, severity); + } + } else { + Set dests = getRunnerDestinations(); + for (Iterator iter = dests.iterator(); iter.hasNext(); ) { + Destination d = (Destination)iter.next(); + reportAbuse(d, reason, severity); + } + } + } + + public String renderStatusHTML() { + StringBuffer buf = new StringBuffer(); + buf.append("

Clients

    "); + Map runners = null; + synchronized (_runners) { + runners = (Map)_runners.clone(); + } + for (Iterator iter = runners.keySet().iterator(); iter.hasNext(); ) { + Destination dest = (Destination)iter.next(); + ClientConnectionRunner runner = (ClientConnectionRunner)runners.get(dest); + buf.append("
  • ").append(dest.calculateHash().toBase64()).append("
  • \n"); + // toss out some general warnings + if (runner.getLeaseSet() == null) + buf.append("No leases! If you didn't just start a client, please restart it (and perhaps check your router's logs for ERROR messages)
    \n"); + else if (runner.getLeaseSet().getEarliestLeaseDate() < Clock.getInstance().now()) + buf.append("wtf, lease has already expired! please restart your client
    \n"); + buf.append("
    \n");
    +	    buf.append(runner.getLeaseSet()).append("
    \n"); + } + buf.append("
\n"); + return buf.toString(); + } + + public void messageReceived(ClientMessage msg) { + JobQueue.getInstance().addJob(new HandleJob(msg)); + } + + private class HandleJob extends JobImpl { + private ClientMessage _msg; + public HandleJob(ClientMessage msg) { + _msg = msg; + } + public String getName() { return "Handle Inbound Client Messages"; } + public void runJob() { + ClientConnectionRunner runner = null; + if (_msg.getDestination() != null) + runner = getRunner(_msg.getDestination()); + else + runner = getRunner(_msg.getDestinationHash()); + + if (runner != null) { + StatManager.getInstance().addRateData("client.receiveMessageSize", _msg.getPayload().getSize(), 0); + runner.receiveMessage(_msg.getDestination(), null, _msg.getPayload()); + } else { + // no client connection... + // we should pool these somewhere... + _log.warn("Message received but we don't have a connection to " + _msg.getDestination() + "/" + _msg.getDestinationHash() + " currently. DROPPED"); + } + } + } +} diff --git a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java new file mode 100644 index 0000000000..1619242e76 --- /dev/null +++ b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java @@ -0,0 +1,157 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.SessionConfig; +import net.i2p.router.ClientManagerFacade; +import net.i2p.router.ClientMessage; +import net.i2p.router.Job; +import net.i2p.router.Router; +import net.i2p.util.Log; + +/** + * Base impl of the client facade + * + * @author jrandom + */ +public class ClientManagerFacadeImpl extends ClientManagerFacade { + private final static Log _log = new Log(ClientManagerFacadeImpl.class); + private ClientManager _manager; + public final static String PROP_CLIENT_PORT = "i2cp.port"; + public final static int DEFAULT_PORT = 7654; + + public ClientManagerFacadeImpl() { + _manager = null; + _log.debug("Client manager facade created"); + } + + public void startup() { + _log.info("Starting up the client subsystem"); + String portStr = Router.getInstance().getConfigSetting(PROP_CLIENT_PORT); + if (portStr != null) { + try { + int port = Integer.parseInt(portStr); + _manager = new ClientManager(port); + } catch (NumberFormatException nfe) { + _log.error("Error setting the port: " + portStr + " is not valid", nfe); + _manager = new ClientManager(DEFAULT_PORT); + } + } else { + _manager = new ClientManager(DEFAULT_PORT); + } + } + + public void shutdown() { + if (_manager != null) + _manager.shutdown(); + } + + /** + * Request that a particular client authorize the Leases contained in the + * LeaseSet, after which the onCreateJob is queued up. If that doesn't occur + * within the timeout specified, queue up the onFailedJob. This call does not + * block. + * + * @param dest Destination from which the LeaseSet's authorization should be requested + * @param set LeaseSet with requested leases - this object must be updated to contain the + * signed version (as well as any changed/added/removed Leases) + * @param timeout ms to wait before failing + * @param onCreateJob Job to run after the LeaseSet is authorized + * @param onFailedJob Job to run after the timeout passes without receiving authorization + */ + public void requestLeaseSet(Destination dest, LeaseSet set, long timeout, Job onCreateJob, Job onFailedJob) { + if (_manager != null) + _manager.requestLeaseSet(dest, set, timeout, onCreateJob, onFailedJob); + else + _log.error("Null manager on requestLeaseSet!"); + } + + /** + * Instruct the client (or all clients) that they are under attack. This call + * does not block. + * + * @param dest Destination under attack, or null if all destinations are affected + * @param reason Why the router thinks that there is abusive behavior + * @param severity How severe the abuse is, with 0 being not severe and 255 is the max + */ + public void reportAbuse(Destination dest, String reason, int severity) { + if (_manager != null) + _manager.reportAbuse(dest, reason, severity); + else + _log.error("Null manager on reportAbuse!"); + } + /** + * Determine if the destination specified is managed locally. This call + * DOES block. + * + * @param dest Destination to be checked + */ + public boolean isLocal(Destination dest) { + if (_manager != null) + return _manager.isLocal(dest); + else { + _log.debug("Null manager on isLocal(dest)!"); + return false; + } + } + /** + * Determine if the destination specified is managed locally. This call + * DOES block. + * + * @param destHash Hash of Destination to be checked + */ + public boolean isLocal(Hash destHash) { + if (_manager != null) + return _manager.isLocal(destHash); + else { + _log.debug("Null manager on isLocal(hash)!"); + return false; + } + } + + public void messageDeliveryStatusUpdate(Destination fromDest, MessageId id, boolean delivered) { + if (_manager != null) + _manager.messageDeliveryStatusUpdate(fromDest, id, delivered); + else + _log.error("Null manager on messageDeliveryStatusUpdate!"); + } + + public void messageReceived(ClientMessage msg) { + if (_manager != null) + _manager.messageReceived(msg); + else + _log.error("Null manager on messageReceived!"); + } + + /** + * Return the client's current config, or null if not connected + * + */ + public SessionConfig getClientSessionConfig(Destination dest) { + if (_manager != null) + return _manager.getClientSessionConfig(dest); + else { + _log.error("Null manager on getClientSessionConfig!"); + return null; + } + } + + public String renderStatusHTML() { + if (_manager != null) + return _manager.renderStatusHTML(); + else { + _log.error("Null manager on renderStatusHTML!"); + return null; + } + } +} diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java new file mode 100644 index 0000000000..0f2e7e2757 --- /dev/null +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -0,0 +1,228 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; + +import net.i2p.data.Payload; +import net.i2p.data.i2cp.CreateLeaseSetMessage; +import net.i2p.data.i2cp.CreateSessionMessage; +import net.i2p.data.i2cp.DestroySessionMessage; +import net.i2p.data.i2cp.GetDateMessage; +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.data.i2cp.I2CPMessageReader; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.MessagePayloadMessage; +import net.i2p.data.i2cp.ReceiveMessageBeginMessage; +import net.i2p.data.i2cp.ReceiveMessageEndMessage; +import net.i2p.data.i2cp.SendMessageMessage; +import net.i2p.data.i2cp.SessionId; +import net.i2p.data.i2cp.SessionStatusMessage; +import net.i2p.data.i2cp.SetDateMessage; +import net.i2p.router.JobQueue; +import net.i2p.router.KeyManager; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; + +/** + * Receive events from the client and handle them accordingly (updating the runner when + * necessary) + * + */ +class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventListener { + private static final Log _log = new Log(ClientMessageEventListener.class); + private ClientConnectionRunner _runner; + + public ClientMessageEventListener(ClientConnectionRunner runner) { + _runner = runner; + } + + /** + * Handle an incoming message and dispatch it to the appropriate handler + * + */ + public void messageReceived(I2CPMessageReader reader, I2CPMessage message) { + if (_runner.isDead()) return; + _log.info("Message recieved: \n" + message); + switch (message.getType()) { + case GetDateMessage.MESSAGE_TYPE: + handleGetDate(reader, (GetDateMessage)message); + break; + case SetDateMessage.MESSAGE_TYPE: + handleSetDate(reader, (SetDateMessage)message); + break; + case CreateSessionMessage.MESSAGE_TYPE: + handleCreateSession(reader, (CreateSessionMessage)message); + break; + case SendMessageMessage.MESSAGE_TYPE: + handleSendMessage(reader, (SendMessageMessage)message); + break; + case ReceiveMessageBeginMessage.MESSAGE_TYPE: + handleReceiveBegin(reader, (ReceiveMessageBeginMessage)message); + break; + case ReceiveMessageEndMessage.MESSAGE_TYPE: + handleReceiveEnd(reader, (ReceiveMessageEndMessage)message); + break; + case CreateLeaseSetMessage.MESSAGE_TYPE: + handleCreateLeaseSet(reader, (CreateLeaseSetMessage)message); + break; + case DestroySessionMessage.MESSAGE_TYPE: + handleDestroySession(reader, (DestroySessionMessage)message); + break; + default: + _log.warn("Unhandled I2CP type received: " + message.getType()); + } + } + + /** + * Handle notifiation that there was an error + * + */ + public void readError(I2CPMessageReader reader, Exception error) { + if (_runner.isDead()) return; + _log.error("Error occurred", error); + _runner.stopRunning(); + } + + public void disconnected(I2CPMessageReader reader) { + if (_runner.isDead()) return; + _runner.disconnected(); + } + + private void handleGetDate(I2CPMessageReader reader, GetDateMessage message) { + try { + _runner.doSend(new SetDateMessage()); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the setDate message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the setDate message", ioe); + } + } + private void handleSetDate(I2CPMessageReader reader, SetDateMessage message) { + Clock.getInstance().setNow(message.getDate().getTime()); + } + + + /** + * Handle a CreateSessionMessage + * + */ + private void handleCreateSession(I2CPMessageReader reader, CreateSessionMessage message) { + if (message.getSessionConfig().verifySignature()) { + _log.debug("Signature verified correctly on create session message"); + } else { + _log.error("Signature verification *FAILED* on a create session message. Hijack attempt?"); + _runner.disconnectClient("Invalid signature on CreateSessionMessage"); + return; + } + + SessionStatusMessage msg = new SessionStatusMessage(); + SessionId sessionId = new SessionId(); + sessionId.setSessionId(getNextSessionId()); + _runner.setSessionId(sessionId); + msg.setSessionId(sessionId); + msg.setStatus(SessionStatusMessage.STATUS_CREATED); + try { + _runner.doSend(msg); + _runner.sessionEstablished(message.getSessionConfig()); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the session status message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the session status message", ioe); + } + + JobQueue.getInstance().addJob(new CreateSessionJob(_runner)); + } + + + /** + * Handle a SendMessageMessage: give it a message Id, have the ClientManager distribute + * it, and send the client an ACCEPTED message + * + */ + private void handleSendMessage(I2CPMessageReader reader, SendMessageMessage message) { + _log.debug("handleSendMessage called"); + MessageId id = _runner.distributeMessage(message); + _runner.ackSendMessage(id, message.getNonce()); + } + + + /** + * The client asked for a message, so we send it to them. + * + */ + private void handleReceiveBegin(I2CPMessageReader reader, ReceiveMessageBeginMessage message) { + if (_runner.isDead()) return; + _log.debug("Handling recieve begin: id = " + message.getMessageId()); + MessagePayloadMessage msg = new MessagePayloadMessage(); + msg.setMessageId(message.getMessageId()); + msg.setSessionId(_runner.getSessionId()); + Payload payload = _runner.getPayload(message.getMessageId()); + if (payload == null) { + _log.error("Payload for message id [" + message.getMessageId() + "] is null! Unknown message id?"); + return; + } + msg.setPayload(payload); + try { + _runner.doSend(msg); + } catch (IOException ioe) { + _log.error("Error delivering the payload", ioe); + } catch (I2CPMessageException ime) { + _log.error("Error delivering the payload", ime); + } + } + + /** + * The client told us that the message has been recieved completely. This currently + * does not do any security checking prior to removing the message from the + * pending queue, though it should. + * + */ + private void handleReceiveEnd(I2CPMessageReader reader, ReceiveMessageEndMessage message) { + _runner.removePayload(message.getMessageId()); + } + + private void handleDestroySession(I2CPMessageReader reader, DestroySessionMessage message) { + _log.info("Destroying client session " + _runner.getSessionId()); + _runner.stopRunning(); + } + + private void handleCreateLeaseSet(I2CPMessageReader reader, CreateLeaseSetMessage message) { + if ( (message.getLeaseSet() == null) || (message.getPrivateKey() == null) || (message.getSigningPrivateKey() == null) ) { + _log.error("Null lease set granted: " + message); + return; + } + + _log.info("New lease set granted for destination " + message.getLeaseSet().getDestination().calculateHash().toBase64()); + KeyManager.getInstance().registerKeys(message.getLeaseSet().getDestination(), message.getSigningPrivateKey(), message.getPrivateKey()); + NetworkDatabaseFacade.getInstance().publish(message.getLeaseSet()); + + // leaseSetCreated takes care of all the LeaseRequestState stuff (including firing any jobs) + _runner.leaseSetCreated(message.getLeaseSet()); + } + + // this *should* be mod 65536, but UnsignedInteger is still b0rked. FIXME + private final static int MAX_SESSION_ID = 32767; + + private static volatile int _id = RandomSource.getInstance().nextInt(MAX_SESSION_ID); // sessionId counter + private final static Object _sessionIdLock = new Object(); + + /** generate a new sessionId */ + private final static int getNextSessionId() { + synchronized (_sessionIdLock) { + int id = (++_id)%MAX_SESSION_ID; + if (_id >= MAX_SESSION_ID) + _id = 0; + return id; + } + } +} diff --git a/router/java/src/net/i2p/router/client/CreateSessionJob.java b/router/java/src/net/i2p/router/client/CreateSessionJob.java new file mode 100644 index 0000000000..37a54acab6 --- /dev/null +++ b/router/java/src/net/i2p/router/client/CreateSessionJob.java @@ -0,0 +1,62 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Properties; + +import net.i2p.router.ClientTunnelSettings; +import net.i2p.router.JobImpl; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.data.i2cp.SessionConfig; +import net.i2p.util.Log; + +/** + * Given an established connection, walk through the process of establishing the + * lease set. This requests the TunnelManagerFacade to build tunnels for the + * client and then once thats done (asynchronously) it requests a lease set from + * the client + * + */ +class CreateSessionJob extends JobImpl { + private final static Log _log = new Log(CreateSessionJob.class); + private ClientConnectionRunner _runner; + + private final static long LEASE_CREATION_TIMEOUT = 30*1000; + + public CreateSessionJob(ClientConnectionRunner runner) { + _runner = runner; + } + + public String getName() { return "Request tunnels for a new client"; } + public void runJob() { + SessionConfig cfg = _runner.getConfig(); + if ( (cfg == null) || (cfg.getDestination() == null) ) return; + if (_log.shouldLog(Log.INFO)) + _log.info("Requesting lease set for destination " + cfg.getDestination().calculateHash().toBase64()); + ClientTunnelSettings settings = new ClientTunnelSettings(); + Properties props = new Properties(); + + // We're NOT going to force all clients to use the router's defaults, since that may be + // excessive. This means that unless the user says otherwise, we'll be satisfied with whatever + // is available. Otherwise, when the router starts up, if there aren't sufficient tunnels with the + // adequate number of hops, the user will have to wait. Once peer profiles are persistent, we can + // reenable this, since on startup we'll have a sufficient number of high enough ranked peers to + // tunnel through. (perhaps). + + // XXX take the router's defaults + // XXX props.putAll(Router.getInstance().getConfigMap()); + + // override them by the client's settings + props.putAll(_runner.getConfig().getOptions()); + + // and load 'em up (using anything not yet set as the software defaults) + settings.readFromProperties(props); + TunnelManagerFacade.getInstance().createTunnels(_runner.getConfig().getDestination(), settings, LEASE_CREATION_TIMEOUT); + } +} diff --git a/router/java/src/net/i2p/router/client/LeaseRequestState.java b/router/java/src/net/i2p/router/client/LeaseRequestState.java new file mode 100644 index 0000000000..7cb36bfd12 --- /dev/null +++ b/router/java/src/net/i2p/router/client/LeaseRequestState.java @@ -0,0 +1,62 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.LeaseSet; +import net.i2p.data.PrivateKey; +import net.i2p.data.SigningPrivateKey; +import net.i2p.router.Job; + +/** + * Bundle up the data points necessary when asynchronously requesting a lease + * from a client + * + */ +class LeaseRequestState { + private LeaseSet _grantedLeaseSet; + private LeaseSet _requestedLeaseSet; + private PrivateKey _leaseSetPrivateKey; + private SigningPrivateKey _leaseSetSigningPrivateKey; + private Job _onGranted; + private Job _onFailed; + private long _expiration; + private boolean _successful; + + public LeaseRequestState(Job onGranted, Job onFailed, long expiration, LeaseSet requested) { + _onGranted = onGranted; + _onFailed = onFailed; + _expiration = expiration; + _requestedLeaseSet = requested; + _successful = false; + } + + /** created lease set from client */ + public LeaseSet getGranted() { return _grantedLeaseSet; } + public void setGranted(LeaseSet ls) { _grantedLeaseSet = ls; } + /** lease set that is being requested */ + public LeaseSet getRequested() { return _requestedLeaseSet; } + public void setRequested(LeaseSet ls) { _requestedLeaseSet = ls; } + /** the private encryption key received regarding the lease set */ + public PrivateKey getPrivateKey() { return _leaseSetPrivateKey; } + public void getPrivateKey(PrivateKey pk) { _leaseSetPrivateKey = pk; } + /** the private signing key received regarding the lease set (for revocation) */ + public SigningPrivateKey getSigningPrivateKey() { return _leaseSetSigningPrivateKey; } + public void getSigningPrivateKey(SigningPrivateKey spk) { _leaseSetSigningPrivateKey = spk; } + /** what to do once the lease set is created */ + public Job getOnGranted() { return _onGranted; } + public void setOnGranted(Job jb) { _onGranted = jb; } + /** what to do if the lease set create fails / times out */ + public Job getOnFailed() { return _onFailed; } + public void setOnFailed(Job jb) { _onFailed = jb; } + /** when the request for the lease set expires */ + public long getExpiration() { return _expiration; } + /** whether the request was successful in the time allotted */ + public boolean getIsSuccessful() { return _successful; } + public void setIsSuccessful(boolean is) { _successful = is; } +} diff --git a/router/java/src/net/i2p/router/client/MessageReceivedJob.java b/router/java/src/net/i2p/router/client/MessageReceivedJob.java new file mode 100644 index 0000000000..83cb648130 --- /dev/null +++ b/router/java/src/net/i2p/router/client/MessageReceivedJob.java @@ -0,0 +1,70 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.JobImpl; +import net.i2p.data.Payload; +import net.i2p.data.Destination; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.MessageStatusMessage; +import net.i2p.data.i2cp.I2CPMessageException; + +import net.i2p.util.Log; + +import java.io.IOException; + +/** + * Async job to notify the client that a new message is available for them + * + */ +class MessageReceivedJob extends JobImpl { + private final static Log _log = new Log(MessageReceivedJob.class); + private ClientConnectionRunner _runner; + private Destination _to; + private Destination _from; + private Payload _payload; + public MessageReceivedJob(ClientConnectionRunner runner, Destination toDest, Destination fromDest, Payload payload) { + _runner = runner; + _to = toDest; + _from = fromDest; + _payload = payload; + } + + public String getName() { return "Deliver New Message"; } + public void runJob() { + if (_runner.isDead()) return; + MessageId id = new MessageId(); + id.setMessageId(ClientConnectionRunner.getNextMessageId()); + _runner.setPayload(id, _payload); + messageAvailable(id, _payload.getSize()); + } + + /** + * Deliver notification to the client that the given message is available. + * This is synchronous and returns true if the notification was sent safely, + * otherwise it returns false + * + */ + public void messageAvailable(MessageId id, long size) { + _log.debug("Sending message available: " + id + " to sessionId " + _runner.getSessionId() + " (with nonce=1)", new Exception("available")); + MessageStatusMessage msg = new MessageStatusMessage(); + msg.setMessageId(id); + msg.setSessionId(_runner.getSessionId()); + msg.setSize(size); + msg.setNonce(1); + msg.setStatus(MessageStatusMessage.STATUS_AVAILABLE); + try { + _runner.doSend(msg); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the message status message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the message status message", ioe); + } + } +} diff --git a/router/java/src/net/i2p/router/client/ReportAbuseJob.java b/router/java/src/net/i2p/router/client/ReportAbuseJob.java new file mode 100644 index 0000000000..dcdc586b46 --- /dev/null +++ b/router/java/src/net/i2p/router/client/ReportAbuseJob.java @@ -0,0 +1,56 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.i2cp.AbuseReason; +import net.i2p.data.i2cp.AbuseSeverity; +import net.i2p.data.i2cp.ReportAbuseMessage; +import net.i2p.data.i2cp.I2CPMessageException; + +import net.i2p.router.JobImpl; +import net.i2p.util.Log; + +import java.io.IOException; + +/** + * Async job to send an abuse message to the client + * + */ +class ReportAbuseJob extends JobImpl { + private final static Log _log = new Log(ReportAbuseJob.class); + private ClientConnectionRunner _runner; + private String _reason; + private int _severity; + public ReportAbuseJob(ClientConnectionRunner runner, String reason, int severity) { + _runner = runner; + _reason = reason; + _severity = severity; + } + + public String getName() { return "Report Abuse"; } + public void runJob() { + if (_runner.isDead()) return; + AbuseReason res = new AbuseReason(); + res.setReason(_reason); + AbuseSeverity sev = new AbuseSeverity(); + sev.setSeverity(_severity); + ReportAbuseMessage msg = new ReportAbuseMessage(); + msg.setMessageId(null); + msg.setReason(res); + msg.setSessionId(_runner.getSessionId()); + msg.setSeverity(sev); + try { + _runner.doSend(msg); + } catch (I2CPMessageException ime) { + _log.error("Error reporting abuse", ime); + } catch (IOException ioe) { + _log.error("Error reporting abuse", ioe); + } + } +} diff --git a/router/java/src/net/i2p/router/client/RequestLeaseSetJob.java b/router/java/src/net/i2p/router/client/RequestLeaseSetJob.java new file mode 100644 index 0000000000..180bdff93e --- /dev/null +++ b/router/java/src/net/i2p/router/client/RequestLeaseSetJob.java @@ -0,0 +1,129 @@ +package net.i2p.router.client; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; + +import net.i2p.data.LeaseSet; +import net.i2p.data.i2cp.RequestLeaseSetMessage; +import net.i2p.data.i2cp.I2CPMessageException; + +import net.i2p.util.Clock; +import net.i2p.util.Log; +import java.io.IOException; +import java.util.Date; + +/** + * Async job to walk the client through generating a lease set. First sends it + * to the client and then queues up a CheckLeaseRequestStatus job for + * processing after the expiration. When that CheckLeaseRequestStatus is run, + * if the client still hasn't provided the signed leaseSet, fire off the onFailed + * job from the intermediary LeaseRequestState and drop the client. + * + */ +class RequestLeaseSetJob extends JobImpl { + private static final Log _log = new Log(RequestLeaseSetJob.class); + private ClientConnectionRunner _runner; + private LeaseSet _ls; + private long _expiration; + private Job _onCreate; + private Job _onFail; + public RequestLeaseSetJob(ClientConnectionRunner runner, LeaseSet set, long expiration, Job onCreate, Job onFail) { + _runner = runner; + _ls = set; + _expiration = expiration; + _onCreate = onCreate; + _onFail = onFail; + } + + public String getName() { return "Request Lease Set"; } + public void runJob() { + if (_runner.isDead()) return; + LeaseRequestState oldReq = _runner.getLeaseRequest(); + if (oldReq != null) { + if (oldReq.getExpiration() > Clock.getInstance().now()) { + _log.error("Old *current* leaseRequest already exists! Why are we trying to request too quickly?", getAddedBy()); + return; + } else { + _log.error("Old *expired* leaseRequest exists! Why did the old request not get killed? (expiration = " + new Date(oldReq.getExpiration()) + ")", getAddedBy()); + } + } + + LeaseRequestState state = new LeaseRequestState(_onCreate, _onFail, _expiration, _ls); + + RequestLeaseSetMessage msg = new RequestLeaseSetMessage(); + Date end = null; + // get the earliest end date + for (int i = 0; i < state.getRequested().getLeaseCount(); i++) { + if ( (end == null) || (end.getTime() > state.getRequested().getLease(i).getEndDate().getTime()) ) + end = state.getRequested().getLease(i).getEndDate(); + } + + msg.setEndDate(end); + msg.setSessionId(_runner.getSessionId()); + + for (int i = 0; i < state.getRequested().getLeaseCount(); i++) { + msg.addEndpoint(state.getRequested().getLease(i).getRouterIdentity(), state.getRequested().getLease(i).getTunnelId()); + } + + try { + _runner.setLeaseRequest(state); + _runner.doSend(msg); + JobQueue.getInstance().addJob(new CheckLeaseRequestStatus(state)); + return; + } catch (I2CPMessageException ime) { + _log.error("Error sending I2CP message requesting the lease set", ime); + state.setIsSuccessful(false); + _runner.setLeaseRequest(null); + _runner.disconnectClient("I2CP error requesting leaseSet"); + return; + } catch (IOException ioe) { + _log.error("Error sending I2CP message requesting the lease set", ioe); + state.setIsSuccessful(false); + _runner.setLeaseRequest(null); + _runner.disconnectClient("IO error requesting leaseSet"); + return; + } + } + + /** + * Schedule this job to be run after the request's expiration, so that if + * it wasn't yet successful, we fire off the failure job and disconnect the + * client (but if it was, noop) + * + */ + private class CheckLeaseRequestStatus extends JobImpl { + private LeaseRequestState _req; + + public CheckLeaseRequestStatus(LeaseRequestState state) { + _req = state; + getTiming().setStartAfter(state.getExpiration()); + } + + public void runJob() { + if (_runner.isDead()) return; + if (_req.getIsSuccessful()) { + // we didn't fail + return; + } else { + _log.error("Failed to receive a leaseSet in the time allotted (" + new Date(_req.getExpiration()) + ")"); + _runner.disconnectClient("Took too long to request leaseSet"); + if (_req.getOnFailed() != null) + JobQueue.getInstance().addJob(_req.getOnFailed()); + + // only zero out the request if its the one we know about + if (_req == _runner.getLeaseRequest()) + _runner.setLeaseRequest(null); + } + } + public String getName() { return "Check LeaseRequest Status"; } + } +} diff --git a/router/java/src/net/i2p/router/message/BuildCreateTunnelMessageJob.java b/router/java/src/net/i2p/router/message/BuildCreateTunnelMessageJob.java new file mode 100644 index 0000000000..cf5c6dadc7 --- /dev/null +++ b/router/java/src/net/i2p/router/message/BuildCreateTunnelMessageJob.java @@ -0,0 +1,67 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.TunnelInfo; +import net.i2p.util.Log; + +/** + * Build a TunnelCreateMessage that is sent to the target requesting that they + * participate in the tunnel. If they reply back saying they will, fire off the + * onCreateSuccessful job, otherwise fire off the onCreateFailed job after a timeout. + * The test message is sent at the specified priority. + * + * The message algorithm is: + * = check to see if we have working outbound tunnels + * - if true, send a tunnel message out the tunnel containing a garlic aimed directly at the peer in question. + * - if false, send a message garlic'ed through a few routers before reaching the peer in question. + * + * the source route block will always point at an inbound tunnel - even if there aren't any real ones (in + * which case, the tunnel gateway is the local router) + * + */ +class BuildCreateTunnelMessageJob extends JobImpl { + private final static Log _log = new Log(BuildCreateTunnelMessageJob.class); + private RouterInfo _target; + private Hash _replyTo; + private TunnelInfo _tunnelConfig; + private Job _onCreateSuccessful; + private Job _onCreateFailed; + private long _timeoutMs; + private int _priority; + + /** + * + * @param target router to participate in the tunnel + * @param replyTo our address + * @param info data regarding the tunnel configuration + * @param onCreateSuccessfulJob after the peer replies back saying they'll participate + * @param onCreateFailedJob after the peer replies back saying they won't participate, or timeout + * @param timeoutMs how long to wait before timing out + * @param priority how high priority to send this test + */ + public BuildCreateTunnelMessageJob(RouterInfo target, Hash replyTo, TunnelInfo info, Job onCreateSuccessfulJob, Job onCreateFailedJob, long timeoutMs, int priority) { + super(); + _target = target; + _replyTo = replyTo; + _tunnelConfig = info; + _onCreateSuccessful = onCreateSuccessfulJob; + _onCreateFailed = onCreateFailedJob; + _timeoutMs = timeoutMs; + _priority = priority; + } + + public String getName() { return "Build Create Tunnel Message"; } + public void runJob() {} +} + diff --git a/router/java/src/net/i2p/router/message/BuildTestMessageJob.java b/router/java/src/net/i2p/router/message/BuildTestMessageJob.java new file mode 100644 index 0000000000..26813ab623 --- /dev/null +++ b/router/java/src/net/i2p/router/message/BuildTestMessageJob.java @@ -0,0 +1,202 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.Job; +import net.i2p.router.ReplyJob; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.Router; +import net.i2p.router.MessageSelector; + +import net.i2p.data.RouterInfo; +import net.i2p.data.Certificate; +import net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.Hash; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.data.i2np.DeliveryStatusMessage; + +import net.i2p.crypto.SessionKeyManager; + +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +import java.util.Date; +import java.util.Set; +import java.util.HashSet; + +/** + * Build a test message that will be sent to the target to make sure they're alive. + * Once that is verified, onSendJob is enqueued. If their reachability isn't + * known (or they're unreachable) within timeoutMs, onSendFailedJob is enqueued. + * The test message is sent at the specified priority. + * + */ +public class BuildTestMessageJob extends JobImpl { + private final static Log _log = new Log(BuildTestMessageJob.class); + private RouterInfo _target; + private Hash _replyTo; + private Job _onSend; + private Job _onSendFailed; + private long _timeoutMs; + private int _priority; + private long _testMessageKey; + + /** + * + * @param target router being tested + * @param onSendJob after the ping is successful + * @param onSendFailedJob after the ping fails or times out + * @param timeoutMs how long to wait before timing out + * @param priority how high priority to send this test + */ + public BuildTestMessageJob(RouterInfo target, Hash replyTo, Job onSendJob, Job onSendFailedJob, long timeoutMs, int priority) { + super(); + _target = target; + _replyTo = replyTo; + _onSend = onSendJob; + _onSendFailed = onSendFailedJob; + _timeoutMs = timeoutMs; + _priority = priority; + _testMessageKey = -1; + } + + public String getName() { return "Build Test Message"; } + + public void runJob() { + // This is a test message - build a garlic with a DeliveryStatusMessage that + // first goes to the peer then back to us. + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Building garlic message to test " + _target.getIdentity().getHash().toBase64()); + GarlicConfig config = buildGarlicCloveConfig(); + // TODO: make the last params on this specify the correct sessionKey and tags used + ReplyJob replyJob = new JobReplyJob(_onSend, config.getRecipient().getIdentity().getPublicKey(), config.getId(), null, new HashSet()); + MessageSelector sel = buildMessageSelector(); + SendGarlicJob job = new SendGarlicJob(config, null, _onSendFailed, replyJob, _onSendFailed, _timeoutMs, _priority, sel); + JobQueue.getInstance().addJob(job); + } + + private MessageSelector buildMessageSelector() { + return new TestMessageSelector(_testMessageKey, _timeoutMs + Clock.getInstance().now()); + } + + private GarlicConfig buildGarlicCloveConfig() { + _testMessageKey = RandomSource.getInstance().nextInt(Integer.MAX_VALUE); + if (_log.shouldLog(Log.INFO)) + _log.info("Test message key: " + _testMessageKey); + GarlicConfig config = new GarlicConfig(); + + PayloadGarlicConfig ackClove = buildAckClove(); + config.addClove(ackClove); + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_ROUTER); + instructions.setDelayRequested(false); + instructions.setDelaySeconds(0); + instructions.setEncrypted(false); + instructions.setEncryptionKey(null); + instructions.setRouter(_target.getIdentity().getHash()); + instructions.setTunnelId(null); + + config.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + config.setDeliveryInstructions(instructions); + config.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + config.setExpiration(_timeoutMs+Clock.getInstance().now()+2*Router.CLOCK_FUDGE_FACTOR); + config.setRecipient(_target); + config.setRequestAck(false); + + return config; + } + + /** + * Build a clove that sends a DeliveryStatusMessage to us + */ + private PayloadGarlicConfig buildAckClove() { + PayloadGarlicConfig ackClove = new PayloadGarlicConfig(); + + DeliveryInstructions ackInstructions = new DeliveryInstructions(); + ackInstructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_ROUTER); + ackInstructions.setRouter(_replyTo); // yikes! + ackInstructions.setDelayRequested(false); + ackInstructions.setDelaySeconds(0); + ackInstructions.setEncrypted(false); + + DeliveryStatusMessage msg = new DeliveryStatusMessage(); + msg.setArrival(new Date(Clock.getInstance().now())); + msg.setMessageId(_testMessageKey); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Delivery status message key: " + _testMessageKey + " arrival: " + msg.getArrival()); + + ackClove.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + ackClove.setDeliveryInstructions(ackInstructions); + ackClove.setExpiration(_timeoutMs+Clock.getInstance().now()); + ackClove.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + ackClove.setPayload(msg); + ackClove.setRecipient(_target); + ackClove.setRequestAck(false); + + return ackClove; + } + + /** + * Search inbound messages for delivery status messages with our key + */ + private final static class TestMessageSelector implements MessageSelector { + private long _testMessageKey; + private long _timeout; + public TestMessageSelector(long key, long timeout) { + _testMessageKey = key; + _timeout = timeout; + } + public boolean continueMatching() { return false; } + public long getExpiration() { return _timeout; } + public boolean isMatch(I2NPMessage inMsg) { + if (inMsg.getType() == DeliveryStatusMessage.MESSAGE_TYPE) { + return ((DeliveryStatusMessage)inMsg).getMessageId() == _testMessageKey; + } else { + return false; + } + } + } + + /** + * On reply, fire off the specified job + * + */ + private final static class JobReplyJob extends JobImpl implements ReplyJob { + private Job _job; + private PublicKey _target; + private long _msgId; + private Set _sessionTagsDelivered; + private SessionKey _keyDelivered; + public JobReplyJob(Job job, PublicKey target, long msgId, SessionKey keyUsed, Set tagsDelivered) { + _job = job; + _target = target; + _msgId = msgId; + _keyDelivered = keyUsed; + _sessionTagsDelivered = tagsDelivered; + } + public String getName() { return "Reply To Test Message Received"; } + public void runJob() { + if ( (_keyDelivered != null) && (_sessionTagsDelivered != null) && (_sessionTagsDelivered.size() > 0) ) + SessionKeyManager.getInstance().tagsDelivered(_target, _keyDelivered, _sessionTagsDelivered); + + JobQueue.getInstance().addJob(_job); + } + + public void setMessage(I2NPMessage message) { + // ignored, this is just a ping + } + + } +} + diff --git a/router/java/src/net/i2p/router/message/CloveSet.java b/router/java/src/net/i2p/router/message/CloveSet.java new file mode 100644 index 0000000000..1774c9be02 --- /dev/null +++ b/router/java/src/net/i2p/router/message/CloveSet.java @@ -0,0 +1,58 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.ArrayList; +import java.util.List; + +import net.i2p.data.Certificate; +import net.i2p.data.i2np.GarlicClove; + +/** + * Wrap up the data contained in a CloveMessage after being decrypted + * + */ +public class CloveSet { + private List _cloves; + private Certificate _cert; + private long _msgId; + private long _expiration; + + public CloveSet() { + _cloves = new ArrayList(); + _cert = null; + _msgId = -1; + _expiration = -1; + } + + public int getCloveCount() { return _cloves.size(); } + public void addClove(GarlicClove clove) { _cloves.add(clove); } + public GarlicClove getClove(int index) { return (GarlicClove)_cloves.get(index); } + + public Certificate getCertificate() { return _cert; } + public void setCertificate(Certificate cert) { _cert = cert; } + public long getMessageId() { return _msgId; } + public void setMessageId(long id) { _msgId = id; } + public long getExpiration() { return _expiration; } + public void setExpiration(long expiration) { _expiration = expiration; } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append("{"); + for (int i = 0; i < _cloves.size(); i++) { + GarlicClove clove = (GarlicClove)_cloves.get(i); + if (clove.getData() != null) + buf.append(clove.getData().getClass().getName()).append(", "); + else + buf.append("[null clove], "); + } + buf.append("}"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/message/GarlicConfig.java b/router/java/src/net/i2p/router/message/GarlicConfig.java new file mode 100644 index 0000000000..3b6359a89d --- /dev/null +++ b/router/java/src/net/i2p/router/message/GarlicConfig.java @@ -0,0 +1,182 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import net.i2p.data.Certificate; +import net.i2p.data.PublicKey; +import net.i2p.data.RouterInfo; +import net.i2p.data.i2np.DeliveryInstructions; + +/** + * Define the contents of a garlic chunk that contains 1 or more sub garlics + * + */ +public class GarlicConfig { + private RouterInfo _recipient; + private PublicKey _recipientPublicKey; + private Certificate _cert; + private long _id; + private long _expiration; + private List _cloveConfigs; + private DeliveryInstructions _instructions; + private boolean _requestAck; + private RouterInfo _replyThroughRouter; // router through which any replies will be sent before delivery to us + private DeliveryInstructions _replyInstructions; // how the message will be sent from the replyThroughRouter to us + private Certificate _replyBlockCertificate; + private long _replyBlockMessageId; + private long _replyBlockExpiration; + + public GarlicConfig() { + _recipient = null; + _recipientPublicKey = null; + _cert = null; + _id = -1; + _expiration = -1; + _cloveConfigs = new ArrayList(); + _instructions = null; + _requestAck = false; + _replyThroughRouter = null; + _replyInstructions = null; + _replyBlockCertificate = null; + _replyBlockMessageId = -1; + _replyBlockExpiration = -1; + } + + /** + * Router to receive and process this clove - the router that will open the + * delivery instructions and decide what to do process it locally as an I2NPMessage, + * forward it as an I2NPMessage to a router, forward it as an I2NPMessage to a Destination, + * or forward it as an I2NPMessage to a tunnel. + * + */ + public void setRecipient(RouterInfo info) { _recipient = info; } + public RouterInfo getRecipient() { return _recipient; } + + /** + * Public key of the router to receive and process this clove. This is useful + * for garlic routed messages encrypted to the router at the end of a tunnel, + * as their RouterIdentity is not known, but a PublicKey they handle is exposed + * via the LeaseSet + * + */ + public void setRecipientPublicKey(PublicKey recipientPublicKey) { _recipientPublicKey = recipientPublicKey; } + public PublicKey getRecipientPublicKey() { return _recipientPublicKey; } + + /** + * Certificate for the getRecipient() to pay for their processing + * + */ + public void setCertificate(Certificate cert) { _cert = cert; } + public Certificate getCertificate() { return _cert; } + + /** + * Unique ID of the clove + * + */ + public void setId(long id) { _id = id; } + public long getId() { return _id; } + + /** + * Expiration of the clove, after which it should be dropped + * + */ + public void setExpiration(long expiration) { _expiration = expiration; } + public long getExpiration() { return _expiration; } + + /** + * Specify how the I2NPMessage in the clove should be handled. + * + */ + public void setDeliveryInstructions(DeliveryInstructions instructions) { _instructions = instructions; } + public DeliveryInstructions getDeliveryInstructions() { return _instructions; } + + /** + * If true, the recipient of this clove is requested to send a DeliveryStatusMessage + * back via the replyThroughRouter using the getId() value for the status' message Id. + * Since those reply blocks are good for one use only, this flag should only be set if + * no reply is expected. + * + */ + public void setRequestAck(boolean request) { _requestAck = request; } + public boolean getRequestAck() { return _requestAck; } + + /** + * Specify the router through which a reply to this clove can be sent. The + * getReplyInstructions() are passed to this router during the reply process + * and it them uses those to send the reply to this router. + * + */ + public void setReplyThroughRouter(RouterInfo replyThroughRouter) { _replyThroughRouter = replyThroughRouter; } + public RouterInfo getReplyThroughRouter() { return _replyThroughRouter; } + + /** + * Specify how any reply will be routed so that it reaches this router after being + * delivered to the getReplyThroughRouter. These instructions are not exposed to the + * router who receives this garlic message in cleartext - they are instead encrypted to + * the replyThrough router + * + */ + public void setReplyInstructions(DeliveryInstructions instructions) { _replyInstructions = instructions; } + public DeliveryInstructions getReplyInstructions() { return _replyInstructions; } + + public long getReplyBlockMessageId() { return _replyBlockMessageId; } + public void setReplyBlockMessageId(long id) { _replyBlockMessageId = id; } + + public Certificate getReplyBlockCertificate() { return _replyBlockCertificate; } + public void setReplyBlockCertificate(Certificate cert) { _replyBlockCertificate = cert; } + + public long getReplyBlockExpiration() { return _replyBlockExpiration; } + public void setReplyBlockExpiration(long expiration) { _replyBlockExpiration = expiration; } + + /** + * Add a clove to the current message - if any cloves are added, an I2NP message + * cannot be specified via setPayload. This means that the resulting GarlicClove + * represented by this GarlicConfig must be a GarlicMessage itself + * + */ + public void addClove(GarlicConfig config) { + if (config != null) { + _cloveConfigs.add(config); + } + } + public int getCloveCount() { return _cloveConfigs.size(); } + public GarlicConfig getClove(int index) { return (GarlicConfig)_cloveConfigs.get(index); } + public void clearCloves() { _cloveConfigs.clear(); } + + + protected String getSubData() { return ""; } + private final static String NL = System.getProperty("line.separator"); + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("").append(NL); + buf.append("").append(getCertificate()).append("").append(NL); + buf.append("").append(getDeliveryInstructions()).append("").append(NL); + buf.append("").append(new Date(getExpiration())).append("").append(NL); + buf.append("").append(getId()).append("").append(NL); + buf.append("").append(getRecipient()).append("").append(NL); + buf.append("").append(getRecipientPublicKey()).append("").append(NL); + buf.append("").append(getReplyBlockCertificate()).append("").append(NL); + buf.append("").append(new Date(getReplyBlockExpiration())).append("").append(NL); + buf.append("").append(getReplyBlockMessageId()).append("").append(NL); + buf.append("").append(getReplyInstructions()).append("").append(NL); + buf.append("").append(getReplyThroughRouter()).append("").append(NL); + buf.append("").append(getRequestAck()).append("").append(NL); + buf.append(getSubData()); + buf.append("").append(NL); + for (int i = 0; i < getCloveCount(); i++) + buf.append("").append(getClove(i)).append("").append(NL); + buf.append("").append(NL); + buf.append("").append(NL); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java new file mode 100644 index 0000000000..5551462c72 --- /dev/null +++ b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java @@ -0,0 +1,206 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import net.i2p.crypto.ElGamalAESEngine; +import net.i2p.crypto.KeyGenerator; +import net.i2p.crypto.SessionKeyManager; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.data.i2np.GarlicClove; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.data.i2np.SourceRouteBlock; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.router.MessageHistory; +import net.i2p.util.Log; + +/** + * Build garlic messages based on a GarlicConfig + * + */ +public class GarlicMessageBuilder { + private final static Log _log = new Log(GarlicMessageBuilder.class); + + public static GarlicMessage buildMessage(GarlicConfig config) { + return buildMessage(config, new SessionKey(), new HashSet()); + } + public static GarlicMessage buildMessage(GarlicConfig config, SessionKey wrappedKey, Set wrappedTags) { + if (config == null) + throw new IllegalArgumentException("Null config specified"); + + PublicKey key = config.getRecipientPublicKey(); + if (key == null) { + if (config.getRecipient() == null) { + throw new IllegalArgumentException("Null recipient specified"); + } else if (config.getRecipient().getIdentity() == null) { + throw new IllegalArgumentException("Null recipient.identity specified"); + } else if (config.getRecipient().getIdentity().getPublicKey() == null) { + throw new IllegalArgumentException("Null recipient.identity.publicKey specified"); + } else + key = config.getRecipient().getIdentity().getPublicKey(); + } + GarlicMessage msg = new GarlicMessage(); + + noteWrap(msg, config); + + _log.info("Encrypted with public key " + key + " to expire on " + new Date(config.getExpiration())); + + byte cloveSet[] = buildCloveSet(config); + + SessionKey curKey = SessionKeyManager.getInstance().getCurrentKey(key); + if (curKey == null) + curKey = SessionKeyManager.getInstance().createSession(key); + wrappedKey.setData(curKey.getData()); + + int availTags = SessionKeyManager.getInstance().getAvailableTags(key, curKey); + _log.debug("Available tags for encryption to " + key + ": " + availTags); + + if (availTags < 10) { // arbitrary threshold + for (int i = 0; i < 20; i++) + wrappedTags.add(new SessionTag(true)); + _log.info("Less than 10 tags are available (" + availTags + "), so we're including 20 more"); + } else if (SessionKeyManager.getInstance().getAvailableTimeLeft(key, curKey) < 30*1000) { + // if we have > 10 tags, but they expire in under 30 seconds, we want more + for (int i = 0; i < 20; i++) + wrappedTags.add(new SessionTag(true)); + _log.info("Tags are almost expired, adding 20 new ones"); + } else { + // always tack on at least one more - not necessary. + //wrappedTags.add(new SessionTag(true)); + } + SessionTag curTag = SessionKeyManager.getInstance().consumeNextAvailableTag(key, curKey); + byte encData[] = ElGamalAESEngine.encrypt(cloveSet, key, curKey, wrappedTags, curTag, 1024); + msg.setData(encData); + Date exp = new Date(config.getExpiration()); + msg.setMessageExpiration(exp); + return msg; + } + + private static void noteWrap(GarlicMessage wrapper, GarlicConfig contained) { + for (int i = 0; i < contained.getCloveCount(); i++) { + GarlicConfig config = contained.getClove(i); + if (config instanceof PayloadGarlicConfig) { + I2NPMessage msg = ((PayloadGarlicConfig)config).getPayload(); + String bodyType = msg.getClass().getName(); + MessageHistory.getInstance().wrap(bodyType, msg.getUniqueId(), GarlicMessage.class.getName(), wrapper.getUniqueId()); + } + } + } + + /** + * Build an unencrypted set of cloves specified by the config. + * + */ + private static byte[] buildCloveSet(GarlicConfig config) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + try { + if (config instanceof PayloadGarlicConfig) { + DataHelper.writeLong(baos, 1, 1); + baos.write(buildClove((PayloadGarlicConfig)config)); + } else { + DataHelper.writeLong(baos, 1, config.getCloveCount()); + for (int i = 0; i < config.getCloveCount(); i++) { + GarlicConfig c = config.getClove(i); + byte clove[] = null; + if (c instanceof PayloadGarlicConfig) { + _log.debug("Subclove IS a payload garlic clove"); + clove = buildClove((PayloadGarlicConfig)c); + } else { + _log.debug("Subclove IS NOT a payload garlic clove"); + clove = buildClove(c); + } + if (clove == null) + throw new DataFormatException("Unable to build clove"); + else + baos.write(clove); + } + } + config.getCertificate().writeBytes(baos); + DataHelper.writeLong(baos, 4, config.getId()); + DataHelper.writeDate(baos, new Date(config.getExpiration())); + } catch (IOException ioe) { + _log.error("Error building the clove set", ioe); + } catch (DataFormatException dfe) { + _log.error("Error building the clove set", dfe); + } + return baos.toByteArray(); + } + + private static byte[] buildClove(PayloadGarlicConfig config) throws DataFormatException, IOException { + GarlicClove clove = new GarlicClove(); + clove.setData(config.getPayload()); + return buildCommonClove(clove, config); + } + + private static byte[] buildClove(GarlicConfig config) throws DataFormatException, IOException { + GarlicClove clove = new GarlicClove(); + GarlicMessage msg = buildMessage(config); + if (msg == null) + throw new DataFormatException("Unable to build message from clove config"); + clove.setData(msg); + return buildCommonClove(clove, config); + } + + + private static byte[] buildCommonClove(GarlicClove clove, GarlicConfig config) throws DataFormatException, IOException { + clove.setCertificate(config.getCertificate()); + clove.setCloveId(config.getId()); + clove.setExpiration(new Date(config.getExpiration())); + clove.setInstructions(config.getDeliveryInstructions()); + specifySourceRouteBlock(clove, config); + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + clove.writeBytes(baos); + return baos.toByteArray(); + } + + private static void specifySourceRouteBlock(GarlicClove clove, GarlicConfig config) throws DataFormatException { + boolean includeBlock = false; + if (config.getRequestAck()) { + clove.setSourceRouteBlockAction(GarlicClove.ACTION_STATUS); + includeBlock = true; + } else if (config.getReplyInstructions() != null) { + clove.setSourceRouteBlockAction(GarlicClove.ACTION_MESSAGE_SPECIFIC); + includeBlock = true; + } else { + clove.setSourceRouteBlockAction(GarlicClove.ACTION_NONE); + } + + if (includeBlock) { + _log.debug("Specifying source route block"); + + SessionKey replySessionKey = KeyGenerator.getInstance().generateSessionKey(); + SessionTag tag = new SessionTag(true); + + // make it so we'll read the session tag correctly and use the right session key + HashSet tags = new HashSet(1); + tags.add(tag); + SessionKeyManager.getInstance().tagsReceived(replySessionKey, tags); + + SourceRouteBlock block = new SourceRouteBlock(); + PublicKey pk = config.getReplyThroughRouter().getIdentity().getPublicKey(); + block.setData(config.getReplyInstructions(), config.getReplyBlockMessageId(), + config.getReplyBlockCertificate(), config.getReplyBlockExpiration(), pk); + block.setRouter(config.getReplyThroughRouter().getIdentity().getHash()); + block.setKey(replySessionKey); + block.setTag(tag); + clove.setSourceRouteBlock(block); + } else { + clove.setSourceRouteBlock(null); + } + } +} diff --git a/router/java/src/net/i2p/router/message/GarlicMessageHandler.java b/router/java/src/net/i2p/router/message/GarlicMessageHandler.java new file mode 100644 index 0000000000..b4b775e00e --- /dev/null +++ b/router/java/src/net/i2p/router/message/GarlicMessageHandler.java @@ -0,0 +1,32 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.HandlerJobBuilder; +import net.i2p.router.Job; + +import net.i2p.data.RouterIdentity; +import net.i2p.data.Hash; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.data.i2np.SourceRouteBlock; + +/** + * HandlerJobBuilder to build jobs to handle GarlicMessages + * + */ +public class GarlicMessageHandler implements HandlerJobBuilder { + + public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock) { + // ignore the reply block for the moment + HandleGarlicMessageJob job = new HandleGarlicMessageJob((GarlicMessage)receivedMessage, from, fromHash); + return job; + } + +} diff --git a/router/java/src/net/i2p/router/message/GarlicMessageParser.java b/router/java/src/net/i2p/router/message/GarlicMessageParser.java new file mode 100644 index 0000000000..eb4650edd7 --- /dev/null +++ b/router/java/src/net/i2p/router/message/GarlicMessageParser.java @@ -0,0 +1,92 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import net.i2p.crypto.ElGamalAESEngine; +import net.i2p.data.Certificate; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.PrivateKey; +import net.i2p.data.i2np.GarlicClove; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.util.Log; + +/** + * Read a GarlicMessage, decrypt it, and return the resulting CloveSet + * + */ +public class GarlicMessageParser { + private final static Log _log = new Log(GarlicMessageParser.class); + private static GarlicMessageParser _instance = new GarlicMessageParser(); + public static GarlicMessageParser getInstance() { return _instance; } + private GarlicMessageParser() {} + + public CloveSet getGarlicCloves(GarlicMessage message, PrivateKey encryptionKey) { + byte encData[] = message.getData(); + byte decrData[] = null; + try { + _log.debug("Decrypting with private key " + encryptionKey); + decrData = ElGamalAESEngine.decrypt(encData, encryptionKey); + } catch (DataFormatException dfe) { + _log.warn("Error decrypting", dfe); + } + if (decrData == null) { + _log.debug("Decryption of garlic message failed"); + return null; + } else { + return readCloveSet(decrData); + } + } + + private CloveSet readCloveSet(byte data[]) { + Set cloves = new HashSet(); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + try { + CloveSet set = new CloveSet(); + + int numCloves = (int)DataHelper.readLong(bais, 1); + _log.debug("# cloves to read: " + numCloves); + for (int i = 0; i < numCloves; i++) { + _log.debug("Reading clove " + i); + try { + GarlicClove clove = new GarlicClove(); + clove.readBytes(bais); + set.addClove(clove); + } catch (DataFormatException dfe) { + _log.warn("Unable to read clove " + i, dfe); + } catch (IOException ioe) { + _log.warn("Unable to read clove " + i, ioe); + } + _log.debug("After reading clove " + i); + } + Certificate cert = new Certificate(); + cert.readBytes(bais); + long msgId = DataHelper.readLong(bais, 4); + Date expiration = DataHelper.readDate(bais); + + set.setCertificate(cert); + set.setMessageId(msgId); + set.setExpiration(expiration.getTime()); + + return set; + } catch (IOException ioe) { + _log.error("Error reading clove set", ioe); + return null; + } catch (DataFormatException dfe) { + _log.error("Error reading clove set", dfe); + return null; + } + } +} diff --git a/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java new file mode 100644 index 0000000000..3fca37197a --- /dev/null +++ b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java @@ -0,0 +1,164 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.data.RouterIdentity; +import net.i2p.data.i2np.GarlicClove; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.router.JobImpl; +import net.i2p.router.KeyManager; +import net.i2p.router.LeaseSetKeys; +import net.i2p.router.Router; +import net.i2p.router.MessageHistory; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Unencrypt a garlic message and handle each of the cloves - locally destined + * messages are tossed into the inbound network message pool so they're handled + * as if they arrived locally. Other instructions are not yet implemented (but + * need to be. soon) + * + */ +public class HandleGarlicMessageJob extends JobImpl { + private final static Log _log = new Log(HandleGarlicMessageJob.class); + private GarlicMessage _message; + private RouterIdentity _from; + private Hash _fromHash; + private static Map _cloves; // map of clove Id --> Expiration of cloves we've already seen + + private final static int FORWARD_PRIORITY = 50; + + public HandleGarlicMessageJob(GarlicMessage msg, RouterIdentity from, Hash fromHash) { + super(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("New handle garlicMessageJob called w/ message from [" + from + "]", new Exception("Debug")); + _message = msg; + _from = from; + _fromHash = fromHash; + _cloves = new HashMap(); + } + + public String getName() { return "Handle Inbound Garlic Message"; } + public void runJob() { + CloveSet set = GarlicMessageParser.getInstance().getGarlicCloves(_message, KeyManager.getInstance().getPrivateKey()); + if (set == null) { + Set keys = KeyManager.getInstance().getAllKeys(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Decryption with the router's key failed, now try with the " + keys.size() + " leaseSet keys"); + // our router key failed, which means that it was either encrypted wrong + // or it was encrypted to a LeaseSet's PublicKey + for (Iterator iter = keys.iterator(); iter.hasNext();) { + LeaseSetKeys lskeys = (LeaseSetKeys)iter.next(); + set = GarlicMessageParser.getInstance().getGarlicCloves(_message, lskeys.getDecryptionKey()); + if (set != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Decrypted garlic message with lease set key for destination " + lskeys.getDestination().calculateHash().toBase64() + " SUCCEEDED: " + set); + break; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Decrypting garlic message with lease set key for destination " + lskeys.getDestination().calculateHash().toBase64() + " failed"); + } + } + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Decrypted clove set found " + set.getCloveCount() + " cloves: " + set); + } + if (set != null) { + for (int i = 0; i < set.getCloveCount(); i++) { + GarlicClove clove = set.getClove(i); + handleClove(clove); + } + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("CloveMessageParser failed to decrypt the message [" + _message.getUniqueId() + "] to us when received from [" + _fromHash + "] / [" + _from + "]", new Exception("Decrypt garlic failed")); + MessageHistory.getInstance().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Garlic could not be decrypted"); + } + } + + private static boolean isKnown(long cloveId) { + boolean known = false; + synchronized (_cloves) { + known = _cloves.containsKey(new Long(cloveId)); + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("isKnown("+cloveId+"): " + known); + return known; + } + + private static void cleanupCloves() { + // this should be in its own thread perhaps? and maybe _cloves should be + // synced to disk? + List toRemove = new ArrayList(32); + long now = Clock.getInstance().now(); + synchronized (_cloves) { + for (Iterator iter = _cloves.keySet().iterator(); iter.hasNext();) { + Long id = (Long)iter.next(); + Date exp = (Date)_cloves.get(id); + if (exp == null) continue; // wtf, not sure how this can happen yet, but i've seen it. grr. + if (now > exp.getTime()) + toRemove.add(id); + } + for (int i = 0; i < toRemove.size(); i++) + _cloves.remove(toRemove.get(i)); + } + } + + private static boolean isValid(GarlicClove clove) { + if (isKnown(clove.getCloveId())) { + _log.error("Duplicate garlic clove received - replay attack in progress? [cloveId = " + + clove.getCloveId() + " expiration = " + clove.getExpiration()); + return false; + } else { + _log.debug("Clove " + clove.getCloveId() + " expiring on " + clove.getExpiration() + " is not known"); + } + long now = Clock.getInstance().now(); + if (clove.getExpiration().getTime() < now) { + if (clove.getExpiration().getTime() < now + Router.CLOCK_FUDGE_FACTOR) { + _log.warn("Expired garlic received, but within our fudge factor [" + clove.getExpiration() + "]"); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.error("Expired garlic clove received - replay attack in progress? [cloveId = " + + clove.getCloveId() + " expiration = " + clove.getExpiration() + " now = " + (new Date(Clock.getInstance().now()))); + return false; + } + + } + synchronized (_cloves) { + _cloves.put(new Long(clove.getCloveId()), clove.getExpiration()); + } + cleanupCloves(); + return true; + } + + private void handleClove(GarlicClove clove) { + if (!isValid(clove)) { + if (_log.shouldLog(Log.DEBUG)) + _log.warn("Invalid clove " + clove); + return; + } + boolean requestAck = (clove.getSourceRouteBlockAction() == GarlicClove.ACTION_STATUS); + long sendExpiration = clove.getExpiration().getTime(); + MessageHandler.getInstance().handleMessage(clove.getInstructions(), clove.getData(), requestAck, clove.getSourceRouteBlock(), + clove.getCloveId(), _from, _fromHash, sendExpiration, FORWARD_PRIORITY); + } + + public void dropped() { + MessageHistory.getInstance().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Dropped due to overload"); + } +} diff --git a/router/java/src/net/i2p/router/message/HandleSourceRouteReplyMessageJob.java b/router/java/src/net/i2p/router/message/HandleSourceRouteReplyMessageJob.java new file mode 100644 index 0000000000..b113238019 --- /dev/null +++ b/router/java/src/net/i2p/router/message/HandleSourceRouteReplyMessageJob.java @@ -0,0 +1,143 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import net.i2p.data.DataFormatException; +import net.i2p.data.Hash; +import net.i2p.data.RouterIdentity; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.data.i2np.SourceRouteReplyMessage; +import net.i2p.router.JobImpl; +import net.i2p.router.KeyManager; +import net.i2p.router.Router; +import net.i2p.router.MessageHistory; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Handle a source route reply - decrypt the instructions and forward the message + * accordingly + * + */ +public class HandleSourceRouteReplyMessageJob extends JobImpl { + private final static Log _log = new Log(HandleSourceRouteReplyMessageJob.class); + private SourceRouteReplyMessage _message; + private RouterIdentity _from; + private Hash _fromHash; + private static Map _seenMessages; // Long msgId --> Date seen + + public final static int PRIORITY = 150; + + public HandleSourceRouteReplyMessageJob(SourceRouteReplyMessage msg, RouterIdentity from, Hash fromHash) { + super(); + _message = msg; + _from = from; + _fromHash = fromHash; + _seenMessages = new HashMap(); + } + + public String getName() { return "Handle Source Route Reply Message"; } + public void runJob() { + try { + long before = Clock.getInstance().now(); + _message.decryptHeader(KeyManager.getInstance().getPrivateKey()); + long after = Clock.getInstance().now(); + if ( (after-before) > 1000) { + _log.warn("Took more than a second (" + (after-before) + ") to decrypt the sourceRoute header"); + } else { + _log.debug("Took LESS than a second (" + (after-before) + ") to decrypt the sourceRoute header"); + } + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error decrypting the source route message's header (message " + _message.getUniqueId() + ")", dfe); + if (_log.shouldLog(Log.WARN)) + _log.warn("Message header could not be decrypted: " + _message, getAddedBy()); + MessageHistory.getInstance().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Source route message header could not be decrypted"); + return; + } + + if (!isValid()) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error validating source route message, dropping: " + _message); + return; + } + + DeliveryInstructions instructions = _message.getDecryptedInstructions(); + + long now = Clock.getInstance().now(); + long expiration = _message.getDecryptedExpiration(); + // if its expiring really soon, jack the expiration 30 seconds + if (expiration < now+10*1000) + expiration = now + 60*1000; + + boolean requestAck = false; + MessageHandler.getInstance().handleMessage(instructions, _message.getMessage(), requestAck, null, + _message.getDecryptedMessageId(), _from, _fromHash, expiration, PRIORITY); + } + + private boolean isValid() { + long now = Clock.getInstance().now(); + if (_message.getDecryptedExpiration() < now) { + if (_message.getDecryptedExpiration() < now + Router.CLOCK_FUDGE_FACTOR) { + _log.info("Expired message received, but within our fudge factor"); + } else { + _log.error("Source route reply message expired. Replay attack? msgId = " + _message.getDecryptedMessageId() + " expiration = " + new Date(_message.getDecryptedExpiration())); + return false; + } + } + if (!isValidMessageId(_message.getDecryptedMessageId(), _message.getDecryptedExpiration())) { + _log.error("Source route reply message already received! Replay attack? msgId = " + _message.getDecryptedMessageId() + " expiration = " + new Date(_message.getDecryptedExpiration())); + return false; + } + return true; + } + + private static boolean isValidMessageId(long msgId, long expiration) { + synchronized (_seenMessages) { + if (_seenMessages.containsKey(new Long(msgId))) + return false; + + + _seenMessages.put(new Long(msgId), new Date(expiration)); + } + // essentially random + if ((msgId % 10) == 0) { + cleanupMessages(); + } + return true; + } + + private static void cleanupMessages() { + // this should be in its own thread perhaps, or job? and maybe _seenMessages should be + // synced to disk? + List toRemove = new ArrayList(32); + long now = Clock.getInstance().now()-Router.CLOCK_FUDGE_FACTOR; + synchronized (_seenMessages) { + for (Iterator iter = _seenMessages.keySet().iterator(); iter.hasNext();) { + Long id = (Long)iter.next(); + Date exp = (Date)_seenMessages.get(id); + if (now > exp.getTime()) + toRemove.add(id); + } + for (int i = 0; i < toRemove.size(); i++) + _seenMessages.remove(toRemove.get(i)); + } + } + + public void dropped() { + MessageHistory.getInstance().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Dropped due to overload"); + } +} diff --git a/router/java/src/net/i2p/router/message/HandleTunnelMessageJob.java b/router/java/src/net/i2p/router/message/HandleTunnelMessageJob.java new file mode 100644 index 0000000000..7f4dd71e7c --- /dev/null +++ b/router/java/src/net/i2p/router/message/HandleTunnelMessageJob.java @@ -0,0 +1,527 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import net.i2p.crypto.AESEngine; +import net.i2p.crypto.SHA256Generator; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.Payload; +import net.i2p.data.RouterIdentity; +import net.i2p.data.SessionKey; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DataMessage; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.I2NPMessageException; +import net.i2p.data.i2np.I2NPMessageHandler; +import net.i2p.data.i2np.TunnelMessage; +import net.i2p.data.i2np.TunnelVerificationStructure; +import net.i2p.router.ClientManagerFacade; +import net.i2p.router.ClientMessage; +import net.i2p.router.ClientMessagePool; +import net.i2p.router.InNetMessage; +import net.i2p.router.InNetMessagePool; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageReceptionInfo; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.MessageHistory; +import net.i2p.router.MessageValidator; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import net.i2p.stat.StatManager; + +public class HandleTunnelMessageJob extends JobImpl { + private final static Log _log = new Log(HandleTunnelMessageJob.class); + private TunnelMessage _message; + private RouterIdentity _from; + private Hash _fromHash; + private final static I2NPMessageHandler _handler = new I2NPMessageHandler(); + + private final static long FORWARD_TIMEOUT = 60*1000; + private final static int FORWARD_PRIORITY = 400; + + static { + StatManager.getInstance().createFrequencyStat("tunnel.unknownTunnelFrequency", "How often do we receive tunnel messages for unknown tunnels?", "Tunnels", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("tunnel.gatewayMessageSize", "How large are the messages we are forwarding on as an inbound gateway?", "Tunnels", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("tunnel.relayMessageSize", "How large are the messages we are forwarding on as a participant in a tunnel?", "Tunnels", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("tunnel.endpointMessageSize", "How large are the messages we are forwarding in as an outbound endpoint?", "Tunnels", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + public HandleTunnelMessageJob(TunnelMessage msg, RouterIdentity from, Hash fromHash) { + super(); + _message = msg; + _from = from; + _fromHash = fromHash; + } + + public String getName() { return "Handle Inbound Tunnel Message"; } + public void runJob() { + TunnelId id = _message.getTunnelId(); + TunnelInfo info = TunnelManagerFacade.getInstance().getTunnelInfo(id); + + if (info == null) { + Hash from = _fromHash; + if (_from != null) + from = _from.getHash(); + MessageHistory.getInstance().droppedTunnelMessage(id, from); + _log.error("Received a message for an unknown tunnel [" + id.getTunnelId() + "], dropping it: " + _message, getAddedBy()); + StatManager.getInstance().updateFrequency("tunnel.unknownTunnelFrequency"); + return; + } + + info = getUs(info); + if (info == null) { + _log.error("We are not part of a known tunnel?? wtf! drop.", getAddedBy()); + StatManager.getInstance().updateFrequency("tunnel.unknownTunnelFrequency"); + return; + } else { + _log.debug("Tunnel message received for tunnel: \n" + info); + } + + //if ( (_message.getVerificationStructure() == null) && (info.getSigningKey() != null) ) { + if (_message.getVerificationStructure() == null) { + if (info.getSigningKey() != null) { + if (info.getNextHop() != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("We are the gateway to tunnel " + id.getTunnelId()); + byte data[] = _message.getData(); + I2NPMessage msg = getBody(data); + JobQueue.getInstance().addJob(new HandleGatewayMessageJob(msg, info, data.length)); + return; + } else { + if (_log.shouldLog(Log.WARN)) + _log.debug("We are the gateway and the endpoint for tunnel " + id.getTunnelId()); + if (_log.shouldLog(Log.WARN)) + _log.debug("Process locally"); + if (info.getDestination() != null) { + if (!ClientManagerFacade.getInstance().isLocal(info.getDestination())) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Received a message on a tunnel allocated to a client that has disconnected - dropping it!"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Dropping message for disconnected client: " + _message); + + MessageHistory.getInstance().droppedOtherMessage(_message); + MessageHistory.getInstance().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Disconnected client"); + return; + } + } + + I2NPMessage body = getBody(_message.getData()); + if (body != null) { + JobQueue.getInstance().addJob(new HandleLocallyJob(body, info)); + return; + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("Body is null! content of message.getData() = [" + DataHelper.toString(_message.getData()) + "]", getAddedBy()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Message that failed: " + _message, getAddedBy()); + return; + } + } + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("Received a message that we are not the gateway for on tunnel " + + id.getTunnelId() + " without a verification structure: " + _message, getAddedBy()); + return; + } + } else { + // participant + TunnelVerificationStructure struct = _message.getVerificationStructure(); + boolean ok = struct.verifySignature(info.getVerificationKey().getKey()); + if (!ok) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Failed tunnel verification! Spoofing / tagging attack? " + _message, getAddedBy()); + return; + } else { + if (info.getNextHop() != null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Message for tunnel " + id.getTunnelId() + " received where we're not the gateway and there are remaining hops, so forward it on to " + + info.getNextHop().toBase64() + " via SendTunnelMessageJob"); + + StatManager.getInstance().addRateData("tunnel.relayMessageSize", _message.getData().length, 0); + + JobQueue.getInstance().addJob(new SendMessageDirectJob(_message, info.getNextHop(), Clock.getInstance().now() + FORWARD_TIMEOUT, FORWARD_PRIORITY)); + return; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("No more hops, unwrap and follow the instructions"); + JobQueue.getInstance().addJob(new HandleEndpointJob(info)); + return; + } + } + } + } + + private void processLocally(TunnelInfo ourPlace) { + if (ourPlace.getEncryptionKey() == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Argh, somehow we don't have the decryption key and we have no more steps", getAddedBy()); + return; + } + DeliveryInstructions instructions = getInstructions(_message.getEncryptedDeliveryInstructions(), ourPlace.getEncryptionKey().getKey()); + if (instructions == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("We are the endpoint of a non-zero length tunnel and we don't have instructions. DROP.", getAddedBy()); + return; + } else { + I2NPMessage body = null; + if (instructions.getEncrypted()) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Body in the tunnel IS encrypted"); + body = decryptBody(_message.getData(), instructions.getEncryptionKey()); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Body in the tunnel is NOT encrypted: " + instructions + "\n" + _message, new Exception("Hmmm...")); + body = getBody(_message.getData()); + } + + if (body == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Unable to recover the body from the tunnel", getAddedBy()); + return; + } else { + JobQueue.getInstance().addJob(new ProcessBodyLocallyJob(body, instructions, ourPlace)); + } + } + } + + private void honorInstructions(DeliveryInstructions instructions, I2NPMessage body) { + StatManager.getInstance().addRateData("tunnel.endpointMessageSize", _message.getData().length, 0); + + switch (instructions.getDeliveryMode()) { + case DeliveryInstructions.DELIVERY_MODE_LOCAL: + sendToLocal(body); + break; + case DeliveryInstructions.DELIVERY_MODE_ROUTER: + if (Router.getInstance().getRouterInfo().getIdentity().getHash().equals(instructions.getRouter())) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Delivery instructions point at a router, but we're that router, so send to local"); + sendToLocal(body); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Delivery instructions point at a router, and we're not that router, so forward it off"); + sendToRouter(instructions.getRouter(), body); + } + break; + case DeliveryInstructions.DELIVERY_MODE_TUNNEL: + sendToTunnel(instructions.getRouter(), instructions.getTunnelId(), body); + break; + case DeliveryInstructions.DELIVERY_MODE_DESTINATION: + sendToDest(instructions.getDestination(), body); + break; + } + } + + private void sendToDest(Hash dest, I2NPMessage body) { + if (body instanceof DataMessage) { + boolean isLocal = ClientManagerFacade.getInstance().isLocal(dest); + if (isLocal) { + deliverMessage(null, dest, (DataMessage)body); + return; + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("Delivery to remote destinations is not yet supported", getAddedBy()); + return; + } + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("Deliver something other than a DataMessage to a Destination? I don't think so."); + return; + } + } + + private void sendToTunnel(Hash router, TunnelId id, I2NPMessage body) { + // TODO: we may want to send it via a tunnel later on, but for now, direct will do. + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending on to requested tunnel " + id.getTunnelId() + " on router " + router.toBase64()); + TunnelMessage msg = new TunnelMessage(); + msg.setTunnelId(id); + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + body.writeBytes(baos); + msg.setData(baos.toByteArray()); + JobQueue.getInstance().addJob(new SendMessageDirectJob(msg, router, Clock.getInstance().now() + FORWARD_TIMEOUT, FORWARD_PRIORITY)); + + String bodyType = body.getClass().getName(); + MessageHistory.getInstance().wrap(bodyType, body.getUniqueId(), TunnelMessage.class.getName(), msg.getUniqueId()); + } catch (DataFormatException dfe) { + _log.error("Error writing out the message to forward to the tunnel", dfe); + } catch (IOException ioe) { + _log.error("Error writing out the message to forward to the tunnel", ioe); + } + } + + private void sendToRouter(Hash router, I2NPMessage body) { + // TODO: we may want to send it via a tunnel later on, but for now, direct will do. + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending on to requested router " + router.toBase64()); + JobQueue.getInstance().addJob(new SendMessageDirectJob(body, router, Clock.getInstance().now() + FORWARD_TIMEOUT, FORWARD_PRIORITY)); + } + + private void sendToLocal(I2NPMessage body) { + InNetMessage msg = new InNetMessage(); + msg.setMessage(body); + msg.setFromRouter(_from); + msg.setFromRouterHash(_fromHash); + InNetMessagePool.getInstance().add(msg); + } + + private void deliverMessage(Destination dest, Hash destHash, DataMessage msg) { + boolean valid = MessageValidator.getInstance().validateMessage(msg.getUniqueId(), msg.getMessageExpiration().getTime()); + if (!valid) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Duplicate data message received [" + msg.getUniqueId() + " expiring on " + msg.getMessageExpiration() + "]"); + MessageHistory.getInstance().droppedOtherMessage(msg); + MessageHistory.getInstance().messageProcessingError(msg.getUniqueId(), msg.getClass().getName(), "Duplicate payload"); + return; + } + + ClientMessage cmsg = new ClientMessage(); + + Payload payload = new Payload(); + payload.setEncryptedData(msg.getData()); + + MessageReceptionInfo info = new MessageReceptionInfo(); + info.setFromPeer(_fromHash); + info.setFromTunnel(_message.getTunnelId()); + + cmsg.setDestination(dest); + cmsg.setDestinationHash(destHash); + cmsg.setPayload(payload); + cmsg.setReceptionInfo(info); + + MessageHistory.getInstance().receivePayloadMessage(msg.getUniqueId()); + // if the destination isn't local, the ClientMessagePool forwards it off as an OutboundClientMessageJob + ClientMessagePool.getInstance().add(cmsg); + } + + private I2NPMessage getBody(byte body[]) { + try { + return _handler.readMessage(new ByteArrayInputStream(body)); + } catch (I2NPMessageException ime) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error parsing the message body", ime); + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error reading the message body", ioe); + } + return null; + } + + private I2NPMessage decryptBody(byte encryptedMessage[], SessionKey key) { + byte iv[] = new byte[16]; + Hash h = SHA256Generator.getInstance().calculateHash(key.getData()); + System.arraycopy(h.getData(), 0, iv, 0, iv.length); + byte decrypted[] = AESEngine.getInstance().safeDecrypt(encryptedMessage, key, iv); + if (decrypted == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error decrypting the message", getAddedBy()); + return null; + } + return getBody(decrypted); + } + + private DeliveryInstructions getInstructions(byte encryptedInstructions[], SessionKey key) { + try { + byte iv[] = new byte[16]; + Hash h = SHA256Generator.getInstance().calculateHash(key.getData()); + System.arraycopy(h.getData(), 0, iv, 0, iv.length); + byte decrypted[] = AESEngine.getInstance().safeDecrypt(encryptedInstructions, key, iv); + if (decrypted == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error decrypting the instructions", getAddedBy()); + return null; + } + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.readBytes(new ByteArrayInputStream(decrypted)); + return instructions; + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error parsing the decrypted instructions", dfe); + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error reading the decrypted instructions", ioe); + } + return null; + } + + private TunnelInfo getUs(TunnelInfo info) { + Hash us = Router.getInstance().getRouterInfo().getIdentity().getHash(); + while (info != null) { + if (us.equals(info.getThisHop())) + return info; + info = info.getNextHopInfo(); + } + return null; + } + + private boolean validateMessage(TunnelMessage msg, TunnelInfo info) { + TunnelVerificationStructure vstruct = _message.getVerificationStructure(); + if (vstruct == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Verification structure missing. invalid"); + return false; + } + + if ( (info.getVerificationKey() == null) || (info.getVerificationKey().getKey() == null) ) { + if (_log.shouldLog(Log.ERROR)) + _log.error("wtf, no verification key for the tunnel? " + info, getAddedBy()); + return false; + } + + if (!vstruct.verifySignature(info.getVerificationKey().getKey())) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Received a tunnel message with an invalid signature!"); + // shitlist the sender? + return false; + } + + // now validate the message + Hash msgHash = SHA256Generator.getInstance().calculateHash(_message.getData()); + if (msgHash.equals(vstruct.getMessageHash())) { + // hash matches. good. + return true; + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("validateMessage: Signed hash does not match real hash. Data has been tampered with!"); + // shitlist the sender! + return false; + } + } + + public void dropped() { + MessageHistory.getInstance().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Dropped due to overload"); + } + + //// + // series of subjobs for breaking this task into smaller components + //// + + /** we're the gateway, lets deal */ + private class HandleGatewayMessageJob extends JobImpl { + private I2NPMessage _body; + private int _length; + private TunnelInfo _info; + + public HandleGatewayMessageJob(I2NPMessage body, TunnelInfo tunnel, int length) { + _body = body; + _length = length; + _info = tunnel; + } + public void runJob() { + if (_body != null) { + StatManager.getInstance().addRateData("tunnel.gatewayMessageSize", _length, 0); + if (_log.shouldLog(Log.INFO)) + _log.info("Message for tunnel " + _info.getTunnelId() + " received at the gateway (us), and since its > 0 length, forward the " + + _body.getClass().getName() + " message on to " + _info.getNextHop().toBase64() + " via SendTunnelMessageJob"); + JobQueue.getInstance().addJob(new SendTunnelMessageJob(_body, _info.getTunnelId(), null, null, null, null, FORWARD_TIMEOUT, FORWARD_PRIORITY)); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Body of the message for the tunnel could not be parsed"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Message that failed: " + _message); + } + } + public String getName() { return "Handle Tunnel Message (gateway)"; } + } + + /** zero hop tunnel */ + private class HandleLocallyJob extends JobImpl { + private I2NPMessage _body; + private TunnelInfo _info; + + public HandleLocallyJob(I2NPMessage body, TunnelInfo tunnel) { + _body = body; + _info = tunnel; + } + + public void runJob() { + if (_body instanceof DataMessage) { + // we know where to send it and its something a client can handle, so lets send 'er to the client + if (_log.shouldLog(Log.WARN)) + _log.debug("Deliver the message to a local client, as its a payload message and we know the destination"); + if (_log.shouldLog(Log.INFO)) + _log.info("Message for tunnel " + _info.getTunnelId() + " received at the gateway (us), but its a 0 length tunnel and the message is a DataMessage, so send it to " + + _info.getDestination().calculateHash().toBase64()); + deliverMessage(_info.getDestination(), null, (DataMessage)_body); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Message for tunnel " + _info.getTunnelId() + + " received at the gateway (us), but its a 0 length tunnel though it is a " + _body.getClass().getName() + ", so process it locally"); + InNetMessage msg = new InNetMessage(); + msg.setFromRouter(_from); + msg.setFromRouterHash(_fromHash); + msg.setMessage(_body); + InNetMessagePool.getInstance().add(msg); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Message added to Inbound network pool for local processing: " + _message); + } + } + public String getName() { return "Handle Tunnel Message (0 hop)"; } + } + + /** we're the endpoint of the inbound tunnel */ + private class HandleEndpointJob extends JobImpl { + private TunnelInfo _info; + public HandleEndpointJob(TunnelInfo info) { + _info = info; + } + public void runJob() { + processLocally(_info); + } + public String getName() { return "Handle Tunnel Message (inbound endpoint)"; } + } + + /** endpoint of outbound 1+ hop tunnel with instructions */ + private class ProcessBodyLocallyJob extends JobImpl { + private I2NPMessage _body; + private TunnelInfo _ourPlace; + private DeliveryInstructions _instructions; + public ProcessBodyLocallyJob(I2NPMessage body, DeliveryInstructions instructions, TunnelInfo ourPlace) { + _body = body; + _instructions = instructions; + _ourPlace = ourPlace; + } + public void runJob() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Body read: " + _body); + if ( (_ourPlace.getDestination() != null) && (_body instanceof DataMessage) ) { + // we know where to send it and its something a client can handle, so lets send 'er to the client + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Deliver the message to a local client, as its a payload message and we know the destination"); + if (_log.shouldLog(Log.INFO)) + _log.info("Message for tunnel " + _ourPlace.getTunnelId().getTunnelId() + + " received where we're the endpoint containing a DataMessage message, so deliver it to " + + _ourPlace.getDestination().calculateHash().toBase64()); + deliverMessage(_ourPlace.getDestination(), null, (DataMessage)_body); + return; + } else { + // Honor the delivery instructions + //TunnelMonitor.endpointReceive(ourPlace.getTunnelId(), body.getClass().getName(), instructions, ourPlace.getDestination()); + if (_log.shouldLog(Log.INFO)) + _log.info("Message for tunnel " + _ourPlace.getTunnelId().getTunnelId() + " received where we're the endpoint containing a " + + _body.getClass().getName() + " message, so honor the delivery instructions: " + _instructions.toString()); + honorInstructions(_instructions, _body); + return; + } + } + public String getName() { return "Handle Tunnel Message (outbound endpoint)"; } + } +} diff --git a/router/java/src/net/i2p/router/message/MessageHandler.java b/router/java/src/net/i2p/router/message/MessageHandler.java new file mode 100644 index 0000000000..8b7d75de87 --- /dev/null +++ b/router/java/src/net/i2p/router/message/MessageHandler.java @@ -0,0 +1,179 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; + +import net.i2p.data.Hash; +import net.i2p.data.Payload; +import net.i2p.data.RouterIdentity; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DataMessage; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.SourceRouteBlock; +import net.i2p.data.i2np.TunnelMessage; +import net.i2p.router.ClientManagerFacade; +import net.i2p.router.ClientMessage; +import net.i2p.router.ClientMessagePool; +import net.i2p.router.InNetMessage; +import net.i2p.router.InNetMessagePool; +import net.i2p.router.Job; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageHistory; +import net.i2p.router.MessageReceptionInfo; +import net.i2p.router.MessageValidator; +import net.i2p.router.Router; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Implement the inbound message processing logic to forward based on delivery instructions and + * send acks. + * + */ +class MessageHandler { + private final static Log _log = new Log(MessageHandler.class); + private static MessageHandler _instance = new MessageHandler(); + public static MessageHandler getInstance() { return _instance; } + + public void handleMessage(DeliveryInstructions instructions, I2NPMessage message, boolean requestAck, SourceRouteBlock replyBlock, + long replyId, RouterIdentity from, Hash fromHash, long expiration, int priority) { + switch (instructions.getDeliveryMode()) { + case DeliveryInstructions.DELIVERY_MODE_LOCAL: + _log.debug("Instructions for LOCAL DELIVERY"); + if (message.getType() == DataMessage.MESSAGE_TYPE) { + handleLocalDestination(instructions, message, fromHash); + } else { + handleLocalRouter(message, from, fromHash, replyBlock, requestAck); + } + break; + case DeliveryInstructions.DELIVERY_MODE_ROUTER: + _log.debug("Instructions for ROUTER DELIVERY to " + instructions.getRouter().toBase64()); + if (Router.getInstance().getRouterInfo().getIdentity().getHash().equals(instructions.getRouter())) { + handleLocalRouter(message, from, fromHash, replyBlock, requestAck); + } else { + handleRemoteRouter(message, instructions, expiration, priority); + } + break; + case DeliveryInstructions.DELIVERY_MODE_DESTINATION: + _log.debug("Instructions for DESTINATION DELIVERY to " + instructions.getDestination().toBase64()); + if (ClientManagerFacade.getInstance().isLocal(instructions.getDestination())) { + handleLocalDestination(instructions, message, fromHash); + } else { + _log.error("Instructions requests forwarding on to a non-local destination. Not yet supported"); + } + break; + case DeliveryInstructions.DELIVERY_MODE_TUNNEL: + _log.debug("Instructions for TUNNEL DELIVERY to" + instructions.getTunnelId().getTunnelId() + " on " + instructions.getRouter().toBase64()); + handleTunnel(instructions, expiration, message, priority); + break; + default: + _log.error("Message has instructions that are not yet implemented: mode = " + instructions.getDeliveryMode()); + } + + if (requestAck) { + _log.debug("SEND ACK REQUESTED"); + sendAck(replyBlock, replyId); + } else { + _log.debug("No ack requested"); + } + } + + private void sendAck(SourceRouteBlock replyBlock, long replyId) { + _log.info("Queueing up ack job via reply block " + replyBlock); + Job ackJob = new SendMessageAckJob(replyBlock, replyId); + JobQueue.getInstance().addJob(ackJob); + } + + private void handleLocalRouter(I2NPMessage message, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock, boolean ackUsed) { + _log.info("Handle " + message.getClass().getName() + " to a local router - toss it on the inbound network pool"); + InNetMessage msg = new InNetMessage(); + msg.setFromRouter(from); + msg.setFromRouterHash(fromHash); + msg.setMessage(message); + if (!ackUsed) + msg.setReplyBlock(replyBlock); + InNetMessagePool.getInstance().add(msg); + } + + private void handleRemoteRouter(I2NPMessage message, DeliveryInstructions instructions, long expiration, int priority) { + + boolean valid = MessageValidator.getInstance().validateMessage(message.getUniqueId(), message.getMessageExpiration().getTime()); + if (!valid) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Duplicate / expired message received to remote router [" + message.getUniqueId() + " expiring on " + message.getMessageExpiration() + "]"); + MessageHistory.getInstance().droppedOtherMessage(message); + MessageHistory.getInstance().messageProcessingError(message.getUniqueId(), message.getClass().getName(), "Duplicate/expired to remote router"); + return; + } + + _log.info("Handle " + message.getClass().getName() + " to a remote router " + instructions.getRouter().toBase64() + " - fire a SendMessageDirectJob"); + SendMessageDirectJob j = new SendMessageDirectJob(message, instructions.getRouter(), expiration, priority); + JobQueue.getInstance().addJob(j); + } + + private void handleTunnel(DeliveryInstructions instructions, long expiration, I2NPMessage message, int priority) { + Hash to = instructions.getRouter(); + long timeoutMs = expiration - Clock.getInstance().now(); + TunnelId tunnelId = instructions.getTunnelId(); + + if (!Router.getInstance().getRouterInfo().getIdentity().getHash().equals(to)) { + // don't validate locally targetted tunnel messages, since then we'd have to tweak + // around message validation thats already in place for SendMessageDirectJob + boolean valid = MessageValidator.getInstance().validateMessage(message.getUniqueId(), message.getMessageExpiration().getTime()); + if (!valid) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Duplicate / expired tunnel message received [" + message.getUniqueId() + " expiring on " + message.getMessageExpiration() + "]"); + MessageHistory.getInstance().droppedOtherMessage(message); + MessageHistory.getInstance().messageProcessingError(message.getUniqueId(), message.getClass().getName(), "Duplicate/expired"); + return; + } + } + + _log.info("Handle " + message.getClass().getName() + " to send to remote tunnel " + tunnelId.getTunnelId() + " on router " + to.toBase64()); + TunnelMessage msg = new TunnelMessage(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + try { + message.writeBytes(baos); + msg.setData(baos.toByteArray()); + msg.setTunnelId(tunnelId); + _log.debug("Placing message of type " + message.getClass().getName() + " into the new tunnel message bound for " + tunnelId.getTunnelId() + " on " + to.toBase64()); + JobQueue.getInstance().addJob(new SendMessageDirectJob(msg, to, expiration, priority)); + + String bodyType = message.getClass().getName(); + MessageHistory.getInstance().wrap(bodyType, message.getUniqueId(), TunnelMessage.class.getName(), msg.getUniqueId()); + } catch (Exception e) { + _log.warn("Unable to forward on according to the instructions to the remote tunnel", e); + } + } + + private void handleLocalDestination(DeliveryInstructions instructions, I2NPMessage message, Hash fromHash) { + boolean valid = MessageValidator.getInstance().validateMessage(message.getUniqueId(), message.getMessageExpiration().getTime()); + if (!valid) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Duplicate / expired client message received [" + message.getUniqueId() + " expiring on " + message.getMessageExpiration() + "]"); + MessageHistory.getInstance().droppedOtherMessage(message); + MessageHistory.getInstance().messageProcessingError(message.getUniqueId(), message.getClass().getName(), "Duplicate/expired client message"); + return; + } + + _log.debug("Handle " + message.getClass().getName() + " to a local destination - build a ClientMessage and pool it"); + ClientMessage msg = new ClientMessage(); + msg.setDestinationHash(instructions.getDestination()); + Payload payload = new Payload(); + payload.setEncryptedData(((DataMessage)message).getData()); + msg.setPayload(payload); + MessageReceptionInfo info = new MessageReceptionInfo(); + info.setFromPeer(fromHash); + msg.setReceptionInfo(info); + MessageHistory.getInstance().receivePayloadMessage(message.getUniqueId()); + ClientMessagePool.getInstance().add(msg); + } +} diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java new file mode 100644 index 0000000000..cf97e5ead2 --- /dev/null +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java @@ -0,0 +1,595 @@ +package net.i2p.router.message; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeMap; + +import net.i2p.crypto.SessionKeyManager; +import net.i2p.data.Certificate; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.Lease; +import net.i2p.data.LeaseSet; +import net.i2p.data.Payload; +import net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.TunnelId; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2np.DataMessage; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.data.i2np.DeliveryStatusMessage; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.router.ClientManagerFacade; +import net.i2p.router.ClientMessage; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageHistory; +import net.i2p.router.MessageSelector; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.ReplyJob; +import net.i2p.router.Router; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.TunnelSelectionCriteria; +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; + +import net.i2p.stat.StatManager; + +/** + * Send a client message, taking into consideration the fact that there may be + * multiple inbound tunnels that the target provides. This job sends it to one + * of them and if it doesnt get a confirmation within 15 seconds (SEND_TIMEOUT_MS), + * it tries the next, continuing on until a confirmation is received, the full + * timeout has been reached (60 seconds, or the ms defined in the client's or + * router's "clientMessageTimeout" option). + * + * After sending through all of the leases without success, if there's still + * time left it fails the leaseSet itself, does a new search for that leaseSet, + * and continues sending down any newly found leases. + * + */ +public class OutboundClientMessageJob extends JobImpl { + private final static Log _log = new Log(OutboundClientMessageJob.class); + private OutboundClientMessageStatus _status; + private NextStepJob _nextStep; + private LookupLeaseSetFailedJob _lookupLeaseSetFailed; + private long _overallExpiration; + + /** + * final timeout (in milliseconds) that the outbound message will fail in. + * This can be overridden in the router.config or the client's session config + * (the client's session config takes precedence) + */ + public final static String OVERALL_TIMEOUT_MS_PARAM = "clientMessageTimeout"; + private final static long OVERALL_TIMEOUT_MS_DEFAULT = 60*1000; + + /** how long for each send do we allow before going on to the next? */ + private final static long SEND_TIMEOUT_MS = 10*1000; + /** priority of messages, that might get honored some day... */ + private final static int SEND_PRIORITY = 500; + + /** dont search for the lease more than 3 times */ + private final static int MAX_LEASE_LOOKUPS = 3; + + static { + StatManager.getInstance().createFrequencyStat("client.sendMessageFailFrequency", "How often does a client fail to send a message?", "Client Messages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("client.sendMessageSize", "How large are messages sent by the client?", "Client Messages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("client.sendAttemptAverage", "How many different tunnels do we have to try when sending a client message?", "Client Messages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + /** + * Send the sucker + */ + public OutboundClientMessageJob(ClientMessage msg) { + super(); + + long timeoutMs = OVERALL_TIMEOUT_MS_DEFAULT; + + String param = msg.getSenderConfig().getOptions().getProperty(OVERALL_TIMEOUT_MS_PARAM); + if (param == null) + param = Router.getInstance().getConfigSetting(OVERALL_TIMEOUT_MS_PARAM); + if (param != null) { + try { + timeoutMs = Long.parseLong(param); + } catch (NumberFormatException nfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid client message timeout specified [" + param + "], defaulting to " + OVERALL_TIMEOUT_MS_DEFAULT, nfe); + timeoutMs = OVERALL_TIMEOUT_MS_DEFAULT; + } + } + + _overallExpiration = timeoutMs + Clock.getInstance().now(); + _status = new OutboundClientMessageStatus(msg); + _nextStep = new NextStepJob(); + _lookupLeaseSetFailed = new LookupLeaseSetFailedJob(); + } + + public String getName() { return "Outbound client message"; } + + public void runJob() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Send outbound client message job beginning"); + buildClove(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Clove built"); + Hash to = _status.getTo().calculateHash(); + long timeoutMs = _overallExpiration - Clock.getInstance().now(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Send outbound client message - sending off leaseSet lookup job"); + _status.incrementLookups(); + NetworkDatabaseFacade.getInstance().lookupLeaseSet(to, _nextStep, _lookupLeaseSetFailed, timeoutMs); + } + + /** + * Continue on sending through the next tunnel + */ + private void sendNext() { + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("sendNext() called with " + _status.getNumSent() + " already sent"); + } + + if (_status.getSuccess()) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("sendNext() - already successful!"); + return; + } + if (_status.getFailure()) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("sendNext() - already failed!"); + return; + } + + long now = Clock.getInstance().now(); + if (now >= _overallExpiration) { + if (_log.shouldLog(Log.WARN)) + _log.warn("sendNext() - Expired"); + dieFatal(); + return; + } + + Lease nextLease = getNextLease(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Send outbound client message - next lease found for [" + _status.getTo().calculateHash().toBase64() + "] - " + nextLease); + + if (nextLease == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("No more leases, and we still haven't heard back from the peer, refetching the leaseSet to try again"); + _status.setLeaseSet(null); + long remainingMs = _overallExpiration - Clock.getInstance().now(); + if (_status.getNumLookups() < MAX_LEASE_LOOKUPS) { + _status.incrementLookups(); + Hash to = _status.getMessage().getDestination().calculateHash(); + _status.clearAlreadySent(); + NetworkDatabaseFacade.getInstance().fail(to); + NetworkDatabaseFacade.getInstance().lookupLeaseSet(to, _nextStep, _lookupLeaseSetFailed, remainingMs); + return; + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("sendNext() - max # lease lookups exceeded! " + _status.getNumLookups()); + dieFatal(); + return; + } + } + + JobQueue.getInstance().addJob(new SendJob(nextLease)); + } + + /** + * fetch the next lease that we should try sending through, or null if there + * are no remaining leases available (or there weren't any in the first place...). + * This implements the logic to determine which lease should be next by picking a + * random one that has been failing the least (e.g. if there are 3 leases in the leaseSet + * and one has failed, the other two are randomly chosen as the 'next') + * + */ + private Lease getNextLease() { + LeaseSet ls = _status.getLeaseSet(); + if (ls == null) { + ls = NetworkDatabaseFacade.getInstance().lookupLeaseSetLocally(_status.getTo().calculateHash()); + if (ls == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Lookup locally didn't find the leaseSet"); + return null; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Lookup locally DID find the leaseSet"); + } + _status.setLeaseSet(ls); + } + long now = Clock.getInstance().now(); + + // get the possible leases + List leases = new ArrayList(4); + for (int i = 0; i < ls.getLeaseCount(); i++) { + Lease lease = ls.getLease(i); + if (lease.isExpired(Router.CLOCK_FUDGE_FACTOR)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("getNextLease() - expired lease! - " + lease); + continue; + } + + if (!_status.alreadySent(lease.getRouterIdentity().getHash(), lease.getTunnelId())) { + leases.add(lease); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("getNextLease() - skipping lease we've already sent it down - " + lease); + } + } + + // randomize the ordering (so leases with equal # of failures per next sort are randomly ordered) + Collections.shuffle(leases); + + // ordered by lease number of failures + TreeMap orderedLeases = new TreeMap(); + for (Iterator iter = leases.iterator(); iter.hasNext(); ) { + Lease lease = (Lease)iter.next(); + long id = lease.getNumFailure(); + while (orderedLeases.containsKey(new Long(id))) + id++; + orderedLeases.put(new Long(id), lease); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("getNextLease() - ranking lease we havent sent it down as " + id); + } + + if (orderedLeases.size() <= 0) { + if (_log.shouldLog(Log.WARN)) + _log.warn("No leases in the ordered set found! all = " + leases.size()); + return null; + } else { + return (Lease)orderedLeases.get(orderedLeases.firstKey()); + } + } + + /** + * Send the message to the specified tunnel by creating a new garlic message containing + * the (already created) payload clove as well as a new delivery status message. This garlic + * message is sent out one of our tunnels, destined for the lease (tunnel+router) specified, and the delivery + * status message is targetting one of our free inbound tunnels as well. We use a new + * reply selector to keep an eye out for that delivery status message's token + * + */ + private void send(Lease lease) { + // send it as a garlic with a DeliveryStatusMessage clove and a message selector w/ successJob on reply + long token = RandomSource.getInstance().nextInt(Integer.MAX_VALUE); + PublicKey key = _status.getLeaseSet().getEncryptionKey(); + SessionKey sessKey = new SessionKey(); + Set tags = new HashSet(); + GarlicMessage msg = OutboundClientMessageJobHelper.createGarlicMessage(token, _overallExpiration, key, _status.getClove(), _status.getTo(), sessKey, tags, true); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("send(lease) - token expected " + token); + + _status.sent(lease.getRouterIdentity().getHash(), lease.getTunnelId()); + + SendSuccessJob onReply = new SendSuccessJob(lease, sessKey, tags); + SendTimeoutJob onFail = new SendTimeoutJob(lease); + ReplySelector selector = new ReplySelector(token); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Placing GarlicMessage into the new tunnel message bound for " + lease.getTunnelId() + " on " + lease.getRouterIdentity().getHash().toBase64()); + + TunnelId outTunnelId = selectOutboundTunnel(); + if (outTunnelId != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending tunnel message out " + outTunnelId + " to " + lease.getTunnelId() + " on " + lease.getRouterIdentity().getHash().toBase64()); + SendTunnelMessageJob j = new SendTunnelMessageJob(msg, outTunnelId, lease.getRouterIdentity().getHash(), lease.getTunnelId(), null, onReply, onFail, selector, SEND_TIMEOUT_MS, SEND_PRIORITY); + JobQueue.getInstance().addJob(j); + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("Could not find any outbound tunnels to send the payload through... wtf?"); + JobQueue.getInstance().addJob(onFail); + } + } + + /** + * Pick an arbitrary outbound tunnel to send the message through, or null if + * there aren't any around + * + */ + private TunnelId selectOutboundTunnel() { + TunnelSelectionCriteria crit = new TunnelSelectionCriteria(); + crit.setMaximumTunnelsRequired(1); + crit.setMinimumTunnelsRequired(1); + List tunnelIds = TunnelManagerFacade.getInstance().selectOutboundTunnelIds(crit); + if (tunnelIds.size() <= 0) + return null; + else + return (TunnelId)tunnelIds.get(0); + } + + /** + * give up the ghost, this message just aint going through. tell the client to fuck off. + * + * this is safe to call multiple times (only tells the client once) + */ + private void dieFatal() { + if (_status.getSuccess()) return; + boolean alreadyFailed = _status.failed(); + long sendTime = Clock.getInstance().now() - _status.getStart(); + ClientMessage msg = _status.getMessage(); + if (alreadyFailed) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("dieFatal() - already failed sending " + msg.getMessageId()+ ", no need to do it again", new Exception("Duplicate death?")); + return; + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("Failed to send the message " + msg.getMessageId() + " after " + _status.getNumSent() + " sends and " + _status.getNumLookups() + " lookups (and " + sendTime + "ms)", new Exception("Message send failure")); + } + + MessageHistory.getInstance().sendPayloadMessage(msg.getMessageId().getMessageId(), false, sendTime); + ClientManagerFacade.getInstance().messageDeliveryStatusUpdate(msg.getFromDestination(), msg.getMessageId(), false); + StatManager.getInstance().updateFrequency("client.sendMessageFailFrequency"); + StatManager.getInstance().addRateData("client.sendAttemptAverage", _status.getNumSent(), sendTime); + } + + /** build the payload clove that will be used for all of the messages, placing the clove in the status structure */ + private void buildClove() { + PayloadGarlicConfig clove = new PayloadGarlicConfig(); + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_DESTINATION); + instructions.setDestination(_status.getTo().calculateHash()); + + instructions.setDelayRequested(false); + instructions.setDelaySeconds(0); + instructions.setEncrypted(false); + + clove.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + clove.setDeliveryInstructions(instructions); + clove.setExpiration(_overallExpiration); + clove.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + + DataMessage msg = new DataMessage(); + msg.setData(_status.getMessage().getPayload().getEncryptedData()); + + clove.setPayload(msg); + clove.setRecipientPublicKey(null); + clove.setRequestAck(false); + + _status.setClove(clove); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Built payload clove with id " + clove.getId()); + } + + /** + * Good ol' fashioned struct with the send status + * + */ + private class OutboundClientMessageStatus { + private ClientMessage _msg; + private PayloadGarlicConfig _clove; + private LeaseSet _leaseSet; + private Set _sent; + private int _numLookups; + private boolean _success; + private boolean _failure; + private long _start; + private int _previousSent; + + public OutboundClientMessageStatus(ClientMessage msg) { + _msg = msg; + _clove = null; + _leaseSet = null; + _sent = new HashSet(4); + _success = false; + _failure = false; + _numLookups = 0; + _previousSent = 0; + _start = Clock.getInstance().now(); + } + + /** raw payload */ + public Payload getPayload() { return _msg.getPayload(); } + /** clove, if we've built it */ + public PayloadGarlicConfig getClove() { return _clove; } + public void setClove(PayloadGarlicConfig clove) { _clove = clove; } + public ClientMessage getMessage() { return _msg; } + /** date we started the process on */ + public long getStart() { return _start; } + + public int getNumLookups() { return _numLookups; } + public void incrementLookups() { _numLookups++; } + public void clearAlreadySent() { + synchronized (_sent) { + _previousSent += _sent.size(); + _sent.clear(); + } + } + + /** who sent the message? */ + public Destination getFrom() { return _msg.getFromDestination(); } + /** who is the message going to? */ + public Destination getTo() { return _msg.getDestination(); } + /** what is the target's current leaseSet (or null if we don't know yet) */ + public LeaseSet getLeaseSet() { return _leaseSet; } + public void setLeaseSet(LeaseSet ls) { _leaseSet = ls; } + /** have we already sent the message down this tunnel? */ + public boolean alreadySent(Hash gateway, TunnelId tunnelId) { + Tunnel t = new Tunnel(gateway, tunnelId); + synchronized (_sent) { + return _sent.contains(t); + } + } + public void sent(Hash gateway, TunnelId tunnelId) { + Tunnel t = new Tunnel(gateway, tunnelId); + synchronized (_sent) { + _sent.add(t); + } + } + /** how many messages have we sent through various leases? */ + public int getNumSent() { + synchronized (_sent) { + return _sent.size() + _previousSent; + } + } + /** did we totally fail? */ + public boolean getFailure() { return _failure; } + /** we failed. returns true if we had already failed before */ + public boolean failed() { + boolean already = _failure; + _failure = true; + return already; + } + /** have we totally succeeded? */ + public boolean getSuccess() { return _success; } + /** we succeeded. returns true if we had already succeeded before */ + public boolean success() { + boolean already = _success; + _success = true; + return already; + } + + /** represent a unique tunnel at any given time */ + private class Tunnel { + private Hash _gateway; + private TunnelId _tunnel; + + public Tunnel(Hash tunnelGateway, TunnelId tunnel) { + _gateway = tunnelGateway; + _tunnel = tunnel; + } + + public Hash getGateway() { return _gateway; } + public TunnelId getTunnel() { return _tunnel; } + + public int hashCode() { + int rv = 0; + if (_gateway != null) + rv += _gateway.hashCode(); + if (_tunnel != null) + rv += 7*_tunnel.getTunnelId(); + return rv; + } + + public boolean equals(Object o) { + if (o == null) return false; + if (o.getClass() != Tunnel.class) return false; + Tunnel t = (Tunnel)o; + return (getTunnel() == t.getTunnel()) && + getGateway().equals(t.getGateway()); + } + } + } + + /** + * Keep an eye out for any of the delivery status message tokens that have been + * sent down the various tunnels to deliver this message + * + */ + private class ReplySelector implements MessageSelector { + private long _pendingToken; + public ReplySelector(long token) { + _pendingToken = token; + } + + public boolean continueMatching() { return false; } + public long getExpiration() { return _overallExpiration; } + + public boolean isMatch(I2NPMessage inMsg) { + if (inMsg.getType() == DeliveryStatusMessage.MESSAGE_TYPE) { + return _pendingToken == ((DeliveryStatusMessage)inMsg).getMessageId(); + } else { + return false; + } + } + } + + /** queued by the db lookup success and the send timeout to get us to try the next lease */ + private class NextStepJob extends JobImpl { + public String getName() { return "Process next step for outbound client message"; } + public void runJob() { sendNext(); } + } + + /** we couldn't even find the leaseSet, fuck off */ + private class LookupLeaseSetFailedJob extends JobImpl { + public String getName() { return "Lookup for outbound client message failed"; } + public void runJob() { dieFatal(); } + } + + /** send a message to a lease */ + private class SendJob extends JobImpl { + private Lease _lease; + public SendJob(Lease lease) { _lease = lease; } + public String getName() { return "Send outbound client message through the lease"; } + public void runJob() { send(_lease); } + } + + /** + * Called after we get a confirmation that the message was delivered safely + * (hoo-ray!) + * + */ + private class SendSuccessJob extends JobImpl implements ReplyJob { + private Lease _lease; + private SessionKey _key; + private Set _tags; + + /** + * Create a new success job that will be fired when the message encrypted with + * the given session key and bearing the specified tags are confirmed delivered. + * + */ + public SendSuccessJob(Lease lease, SessionKey key, Set tags) { + _lease = lease; + _key = key; + _tags = tags; + } + + public String getName() { return "Send client message successful to a lease"; } + public void runJob() { + long sendTime = Clock.getInstance().now() - _status.getStart(); + boolean alreadySuccessful = _status.success(); + MessageId msgId = _status.getMessage().getMessageId(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("SUCCESS! Message delivered completely for message " + msgId + " after " + sendTime + "ms [for " + _status.getMessage().getMessageId() + "]"); + + if ( (_key != null) && (_tags != null) && (_tags.size() > 0) ) { + SessionKeyManager.getInstance().tagsDelivered(_status.getLeaseSet().getEncryptionKey(), _key, _tags); + } + + if (alreadySuccessful) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Success is a duplicate for " + _status.getMessage().getMessageId() + ", dont notify again..."); + return; + } + long dataMsgId = _status.getClove().getId(); + MessageHistory.getInstance().sendPayloadMessage(dataMsgId, true, sendTime); + ClientManagerFacade.getInstance().messageDeliveryStatusUpdate(_status.getFrom(), msgId, true); + _lease.setNumSuccess(_lease.getNumSuccess()+1); + + StatManager.getInstance().addRateData("client.sendMessageSize", _status.getMessage().getPayload().getSize(), sendTime); + StatManager.getInstance().addRateData("client.sendAttemptAverage", _status.getNumSent(), sendTime); + } + + public void setMessage(I2NPMessage msg) {} + } + + /** + * Fired after the basic timeout for sending through the given tunnel has been reached. + * We'll accept successes later, but won't expect them + * + */ + private class SendTimeoutJob extends JobImpl { + private Lease _lease; + + public SendTimeoutJob(Lease lease) { + _lease = lease; + } + + public String getName() { return "Send client message timed out through a lease"; } + public void runJob() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Soft timeout through the lease " + _lease); + _lease.setNumFailure(_lease.getNumFailure()+1); + sendNext(); + } + } +} diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java new file mode 100644 index 0000000000..0534508c39 --- /dev/null +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java @@ -0,0 +1,179 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; +import java.util.List; +import java.util.Set; + +import net.i2p.data.Certificate; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.Payload; +import net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DataMessage; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.data.i2np.DeliveryStatusMessage; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.TunnelSelectionCriteria; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +/** + * Handle a particular client message that is destined for a remote destination. + * + */ +class OutboundClientMessageJobHelper { + private static Log _log = new Log(OutboundClientMessageJobHelper.class); + + /** + * Build a garlic message that will be delivered to the router on which the target is located. + * Inside the message are two cloves: one containing the payload with instructions for + * delivery to the (now local) destination, and the other containing a DeliveryStatusMessage with + * instructions for delivery to an inbound tunnel of this router. + * + * How the DeliveryStatusMessage is wrapped can vary - it can be simply sent to a tunnel (as above), + * wrapped in a GarlicMessage and source routed a few hops before being tunneled, source routed the + * entire way back, or not wrapped at all - in which case the payload clove contains a SourceRouteBlock + * and a request for a reply. + * + * For now, its just a tunneled DeliveryStatusMessage + * + */ + static GarlicMessage createGarlicMessage(long replyToken, long expiration, PublicKey recipientPK, Payload data, Destination dest, SessionKey wrappedKey, Set wrappedTags, boolean requireAck) { + PayloadGarlicConfig dataClove = buildDataClove(data, dest, expiration); + return createGarlicMessage(replyToken, expiration, recipientPK, dataClove, dest, wrappedKey, wrappedTags, requireAck); + } + /** + * Allow the app to specify the data clove directly, which enables OutboundClientMessage to resend the + * same payload (including expiration and unique id) in different garlics (down different tunnels) + * + */ + static GarlicMessage createGarlicMessage(long replyToken, long expiration, PublicKey recipientPK, PayloadGarlicConfig dataClove, Destination dest, SessionKey wrappedKey, Set wrappedTags, boolean requireAck) { + GarlicConfig config = createGarlicConfig(replyToken, expiration, recipientPK, dataClove, dest, requireAck); + GarlicMessage msg = GarlicMessageBuilder.buildMessage(config, wrappedKey, wrappedTags); + return msg; + } + + private static GarlicConfig createGarlicConfig(long replyToken, long expiration, PublicKey recipientPK, PayloadGarlicConfig dataClove, Destination dest, boolean requireAck) { + _log.debug("Reply token: " + replyToken); + GarlicConfig config = new GarlicConfig(); + + config.addClove(dataClove); + + if (requireAck) { + PayloadGarlicConfig ackClove = buildAckClove(replyToken, expiration); + config.addClove(ackClove); + } + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_LOCAL); + instructions.setDelayRequested(false); + instructions.setDelaySeconds(0); + instructions.setEncrypted(false); + instructions.setEncryptionKey(null); + instructions.setRouter(null); + instructions.setTunnelId(null); + + config.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + config.setDeliveryInstructions(instructions); + config.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + config.setExpiration(expiration+2*Router.CLOCK_FUDGE_FACTOR); + config.setRecipientPublicKey(recipientPK); + config.setRequestAck(false); + + _log.info("Creating garlic config to be encrypted to " + recipientPK + " for destination " + dest.calculateHash().toBase64()); + + return config; + } + + /** + * Build a clove that sends a DeliveryStatusMessage to us + */ + private static PayloadGarlicConfig buildAckClove(long replyToken, long expiration) { + PayloadGarlicConfig ackClove = new PayloadGarlicConfig(); + + Hash replyToTunnelRouter = null; // inbound tunnel gateway + TunnelId replyToTunnelId = null; // tunnel id on that gateway + + TunnelSelectionCriteria criteria = new TunnelSelectionCriteria(); + criteria.setMaximumTunnelsRequired(1); + criteria.setMinimumTunnelsRequired(1); + criteria.setReliabilityPriority(50); // arbitrary. fixme + criteria.setAnonymityPriority(50); // arbitrary. fixme + criteria.setLatencyPriority(50); // arbitrary. fixme + List tunnelIds = TunnelManagerFacade.getInstance().selectInboundTunnelIds(criteria); + if (tunnelIds.size() <= 0) { + _log.error("No inbound tunnels to receive an ack through!?"); + return null; + } + replyToTunnelId = (TunnelId)tunnelIds.get(0); + TunnelInfo info = TunnelManagerFacade.getInstance().getTunnelInfo(replyToTunnelId); + replyToTunnelRouter = info.getThisHop(); // info is the chain, and the first hop is the gateway + _log.debug("Ack for the data message will come back along tunnel " + replyToTunnelId + ":\n" + info); + + DeliveryInstructions ackInstructions = new DeliveryInstructions(); + ackInstructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_TUNNEL); + ackInstructions.setRouter(replyToTunnelRouter); + ackInstructions.setTunnelId(replyToTunnelId); + ackInstructions.setDelayRequested(false); + ackInstructions.setDelaySeconds(0); + ackInstructions.setEncrypted(false); + + DeliveryStatusMessage msg = new DeliveryStatusMessage(); + msg.setArrival(new Date(Clock.getInstance().now())); + msg.setMessageId(replyToken); + _log.debug("Delivery status message key: " + replyToken + " arrival: " + msg.getArrival()); + + ackClove.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + ackClove.setDeliveryInstructions(ackInstructions); + ackClove.setExpiration(expiration); + ackClove.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + ackClove.setPayload(msg); + ackClove.setRecipient(Router.getInstance().getRouterInfo()); + ackClove.setRequestAck(false); + + _log.debug("Delivery status message is targetting us [" + ackClove.getRecipient().getIdentity().getHash().toBase64() + "] via tunnel " + replyToTunnelId.getTunnelId() + " on " + replyToTunnelRouter.toBase64()); + + return ackClove; + } + + /** + * Build a clove that sends the payload to the destination + */ + static PayloadGarlicConfig buildDataClove(Payload data, Destination dest, long expiration) { + PayloadGarlicConfig clove = new PayloadGarlicConfig(); + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_DESTINATION); + instructions.setDestination(dest.calculateHash()); + + instructions.setDelayRequested(false); + instructions.setDelaySeconds(0); + instructions.setEncrypted(false); + + clove.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + clove.setDeliveryInstructions(instructions); + clove.setExpiration(expiration); + clove.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + DataMessage msg = new DataMessage(); + msg.setData(data.getEncryptedData()); + clove.setPayload(msg); + clove.setRecipientPublicKey(null); + clove.setRequestAck(false); + + return clove; + } +} diff --git a/router/java/src/net/i2p/router/message/PayloadGarlicConfig.java b/router/java/src/net/i2p/router/message/PayloadGarlicConfig.java new file mode 100644 index 0000000000..eb2c048863 --- /dev/null +++ b/router/java/src/net/i2p/router/message/PayloadGarlicConfig.java @@ -0,0 +1,41 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.i2np.I2NPMessage; + +/** + * Garlic config containing an I2NP message + * + */ +public class PayloadGarlicConfig extends GarlicConfig { + private I2NPMessage _payload; + + public PayloadGarlicConfig() { + super(); + _payload = null; + } + + /** + * Specify the I2NP message to be sent - if this is set, no other cloves can be included + * in this block + */ + public void setPayload(I2NPMessage message) { + _payload = message; + if (message != null) + clearCloves(); + } + public I2NPMessage getPayload() { return _payload; } + + protected String getSubData() { + StringBuffer buf = new StringBuffer(); + buf.append("").append(_payload).append(""); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/message/SendGarlicJob.java b/router/java/src/net/i2p/router/message/SendGarlicJob.java new file mode 100644 index 0000000000..6b93b63e85 --- /dev/null +++ b/router/java/src/net/i2p/router/message/SendGarlicJob.java @@ -0,0 +1,122 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.HashSet; +import java.util.Set; + +import net.i2p.data.SessionKey; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageSelector; +import net.i2p.router.OutNetMessage; +import net.i2p.router.OutNetMessagePool; +import net.i2p.router.ReplyJob; +import net.i2p.router.Router; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Build a garlic message from config, encrypt it, and enqueue it for delivery. + * + */ +public class SendGarlicJob extends JobImpl { + private final static Log _log = new Log(SendGarlicJob.class); + //private RouterInfo _target; + private GarlicConfig _config; + private Job _onSend; + private Job _onSendFailed; + private ReplyJob _onReply; + private Job _onReplyFailed; + private long _timeoutMs; + private int _priority; + private MessageSelector _replySelector; + private GarlicMessage _message; + private SessionKey _wrappedKey; + private Set _wrappedTags; + + /** + * + * @param config ??? + * @param onSend after the ping is successful + * @param onSendFailed after the ping fails or times out + * @param onReply ??? + * @param onReplyFailed ??? + * @param timeoutMs how long to wait before timing out + * @param priority how high priority to send this test + * @param replySelector ??? + */ + public SendGarlicJob(GarlicConfig config, Job onSend, Job onSendFailed, ReplyJob onReply, Job onReplyFailed, long timeoutMs, int priority, MessageSelector replySelector) { + this(config, onSend, onSendFailed, onReply, onReplyFailed, timeoutMs, priority, replySelector, new SessionKey(), new HashSet()); + } + public SendGarlicJob(GarlicConfig config, Job onSend, Job onSendFailed, ReplyJob onReply, Job onReplyFailed, long timeoutMs, int priority, MessageSelector replySelector, SessionKey wrappedKey, Set wrappedTags) { + super(); + if (config == null) throw new IllegalArgumentException("No config specified"); + if (config.getRecipient() == null) throw new IllegalArgumentException("No recipient in the config"); + //_target = target; + _config = config; + _onSend = onSend; + _onSendFailed = onSendFailed; + _onReply = onReply; + _onReplyFailed = onReplyFailed; + _timeoutMs = timeoutMs; + _priority = priority; + _replySelector = replySelector; + _message = null; + _wrappedKey = wrappedKey; + _wrappedTags = wrappedTags; + } + + public String getName() { return "Build Garlic Message"; } + + public void runJob() { + long before = Clock.getInstance().now(); + _message = GarlicMessageBuilder.buildMessage(_config, _wrappedKey, _wrappedTags); + long after = Clock.getInstance().now(); + if ( (after - before) > 1000) { + _log.warn("Building the garlic took too long [" + (after-before)+" ms]", getAddedBy()); + } else { + _log.debug("Building the garlic was fast! " + (after - before) + " ms"); + } + JobQueue.getInstance().addJob(new SendJob()); + } + + private class SendJob extends JobImpl { + public String getName() { return "Send Built Garlic Message"; } + public void runJob() { + if (_config.getRecipient() != null) + _log.info("sending garlic to recipient " + _config.getRecipient().getIdentity().getHash().toBase64()); + else + _log.info("sending garlic to public key " + _config.getRecipientPublicKey()); + sendGarlic(); + } + } + + private void sendGarlic() { + OutNetMessage msg = new OutNetMessage(); + long when = _message.getMessageExpiration().getTime() + Router.CLOCK_FUDGE_FACTOR; + msg.setExpiration(when); + msg.setMessage(_message); + msg.setOnFailedReplyJob(_onReplyFailed); + msg.setOnFailedSendJob(_onSendFailed); + msg.setOnReplyJob(_onReply); + msg.setOnSendJob(_onSend); + msg.setPriority(_priority); + msg.setReplySelector(_replySelector); + msg.setTarget(_config.getRecipient()); + //_log.info("Sending garlic message to [" + _config.getRecipient() + "] encrypted with " + _config.getRecipientPublicKey() + " or " + _config.getRecipient().getIdentity().getPublicKey()); + //_log.debug("Garlic config data:\n" + _config); + //msg.setTarget(_target); + OutNetMessagePool.getInstance().add(msg); + _log.debug("Garlic message added to outbound network message pool"); + } +} + diff --git a/router/java/src/net/i2p/router/message/SendMessageAckJob.java b/router/java/src/net/i2p/router/message/SendMessageAckJob.java new file mode 100644 index 0000000000..c62d16657b --- /dev/null +++ b/router/java/src/net/i2p/router/message/SendMessageAckJob.java @@ -0,0 +1,59 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; + +import net.i2p.data.i2np.DeliveryStatusMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.SourceRouteBlock; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.util.Clock; + +/** + * Send a DeliveryStatusMessage to the location specified in the source route block + * acknowledging the ackId given. This uses the simplest technique (don't garlic, and + * send direct to where the SourceRouteBlock requested), but it could instead garlic it + * and send it via a tunnel or garlic route it additionally) + * + */ +public class SendMessageAckJob extends JobImpl { + private SourceRouteBlock _block; + private long _ackId; + + public final static int ACK_PRIORITY = 100; + + public SendMessageAckJob(SourceRouteBlock block, long ackId) { + super(); + _block = block; + _ackId = ackId; + } + + public void runJob() { + JobQueue.getInstance().addJob(new SendReplyMessageJob(_block, createAckMessage(), ACK_PRIORITY)); + } + + /** + * Create whatever should be delivered to the intermediary hop so that + * a DeliveryStatusMessage gets to the intended recipient. + * + * Currently this doesn't garlic encrypt the DeliveryStatusMessage with + * the block's tag and sessionKey, but it could. + * + */ + protected I2NPMessage createAckMessage() { + DeliveryStatusMessage statusMessage = new DeliveryStatusMessage(); + statusMessage.setArrival(new Date(Clock.getInstance().now())); + statusMessage.setMessageId(_ackId); + return statusMessage; + } + + public String getName() { return "Send Message Ack"; } +} diff --git a/router/java/src/net/i2p/router/message/SendMessageDirectJob.java b/router/java/src/net/i2p/router/message/SendMessageDirectJob.java new file mode 100644 index 0000000000..521c84ce57 --- /dev/null +++ b/router/java/src/net/i2p/router/message/SendMessageDirectJob.java @@ -0,0 +1,159 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.JobImpl; +import net.i2p.router.Job; +import net.i2p.router.ReplyJob; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageSelector; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.OutNetMessage; +import net.i2p.router.OutNetMessagePool; +import net.i2p.router.transport.OutboundMessageRegistry; +import net.i2p.router.InNetMessage; +import net.i2p.router.InNetMessagePool; +import net.i2p.router.Router; + +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Date; + +public class SendMessageDirectJob extends JobImpl { + private final static Log _log = new Log(SendMessageDirectJob.class); + private I2NPMessage _message; + private Hash _targetHash; + private RouterInfo _router; + private long _expiration; + private int _priority; + private Job _onSend; + private ReplyJob _onSuccess; + private Job _onFail; + private MessageSelector _selector; + private boolean _alreadySearched; + private boolean _sent; + + private final static long DEFAULT_TIMEOUT = 60*1000; + + public SendMessageDirectJob(I2NPMessage message, Hash toPeer, long expiration, int priority) { + this(message, toPeer, null, null, null, null, expiration, priority); + } + public SendMessageDirectJob(I2NPMessage message, Hash toPeer, int priority) { + this(message, toPeer, DEFAULT_TIMEOUT+Clock.getInstance().now(), priority); + } + public SendMessageDirectJob(I2NPMessage message, Hash toPeer, ReplyJob onSuccess, Job onFail, MessageSelector selector, long expiration, int priority) { + this(message, toPeer, null, onSuccess, onFail, selector, expiration, priority); + } + public SendMessageDirectJob(I2NPMessage message, Hash toPeer, Job onSend, ReplyJob onSuccess, Job onFail, MessageSelector selector, long expiration, int priority) { + super(); + _message = message; + _targetHash = toPeer; + _router = null; + _expiration = expiration; + _priority = priority; + _alreadySearched = false; + _onSend = onSend; + _onSuccess = onSuccess; + _onFail = onFail; + _selector = selector; + if (message == null) + throw new IllegalArgumentException("Attempt to send a null message"); + if (_targetHash == null) + throw new IllegalArgumentException("Attempt to send a message to a null peer"); + _sent = false; + long remaining = expiration - Clock.getInstance().now(); + if (remaining < 50*1000) { + _log.info("Sending message to expire in " + remaining + "ms containing " + message.getUniqueId() + " (a " + message.getClass().getName() + ")", new Exception("SendDirect from")); + } + } + + public String getName() { return "Send Message Direct"; } + public void runJob() { + long now = Clock.getInstance().now(); + if (_expiration == 0) + _expiration = now + DEFAULT_TIMEOUT; + + if (_expiration - 30*1000 < now) { + _log.info("Soon to expire sendDirect of " + _message.getClass().getName() + " [expiring in " + (_expiration-now) + "]", getAddedBy()); + } + + if (_expiration < now) { + _log.warn("Timed out sending message " + _message + " directly (expiration = " + new Date(_expiration) + ") to " + _targetHash.toBase64(), getAddedBy()); + return; + } + if (_router != null) { + _log.debug("Router specified, sending"); + send(); + } else { + _router = NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(_targetHash); + if (_router != null) { + _log.debug("Router not specified but lookup found it"); + send(); + } else { + if (!_alreadySearched) { + _log.debug("Router not specified, so we're looking for it..."); + NetworkDatabaseFacade.getInstance().lookupRouterInfo(_targetHash, this, this, _expiration - Clock.getInstance().now()); + _alreadySearched = true; + } else { + _log.error("Unable to find the router to send to: " + _targetHash + " message: " + _message, getAddedBy()); + } + } + } + } + + private void send() { + if (_sent) { _log.warn("Not resending!", new Exception("blah")); return; } + _sent = true; + if (Router.getInstance().getRouterInfo().getIdentity().getHash().equals(_router.getIdentity().getHash())) { + if (_selector != null) { + OutNetMessage outM = new OutNetMessage(); + outM.setExpiration(_expiration); + outM.setMessage(_message); + outM.setOnFailedReplyJob(_onFail); + outM.setOnFailedSendJob(_onFail); + outM.setOnReplyJob(_onSuccess); + outM.setOnSendJob(_onSend); + outM.setPriority(_priority); + outM.setReplySelector(_selector); + outM.setTarget(_router); + OutboundMessageRegistry.getInstance().registerPending(outM); + } + + if (_onSend != null) + JobQueue.getInstance().addJob(_onSend); + + InNetMessage msg = new InNetMessage(); + msg.setFromRouter(_router.getIdentity()); + msg.setMessage(_message); + InNetMessagePool.getInstance().add(msg); + + _log.debug("Adding " + _message.getClass().getName() + " to inbound message pool as it was destined for ourselves"); + //_log.debug("debug", _createdBy); + } else { + OutNetMessage msg = new OutNetMessage(); + msg.setExpiration(_expiration); + msg.setMessage(_message); + msg.setOnFailedReplyJob(_onFail); + msg.setOnFailedSendJob(_onFail); + msg.setOnReplyJob(_onSuccess); + msg.setOnSendJob(_onSend); + msg.setPriority(_priority); + msg.setReplySelector(_selector); + msg.setTarget(_router); + OutNetMessagePool.getInstance().add(msg); + _log.debug("Adding " + _message.getClass().getName() + " to outbound message pool targeting " + _router.getIdentity().getHash().toBase64()); + //_log.debug("Message pooled: " + _message); + } + } +} diff --git a/router/java/src/net/i2p/router/message/SendReplyMessageJob.java b/router/java/src/net/i2p/router/message/SendReplyMessageJob.java new file mode 100644 index 0000000000..c90af01429 --- /dev/null +++ b/router/java/src/net/i2p/router/message/SendReplyMessageJob.java @@ -0,0 +1,63 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.SourceRouteBlock; +import net.i2p.data.i2np.SourceRouteReplyMessage; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.util.Log; + +/** + * Send a SourceRouteReplyMessage to the location specified in the source route block. + * This uses the simplest technique (don't garlic, and send direct to where the + * SourceRouteBlock requested), but it could instead garlic it and send it via a + * tunnel or garlic route it additionally) + * + */ +public class SendReplyMessageJob extends JobImpl { + private final static Log _log = new Log(SendReplyMessageJob.class); + private SourceRouteBlock _block; + private I2NPMessage _message; + private int _priority; + + public SendReplyMessageJob(SourceRouteBlock block, I2NPMessage message, int priority) { + super(); + _block = block; + _message = message; + _priority = priority; + } + + public void runJob() { + SourceRouteReplyMessage msg = new SourceRouteReplyMessage(); + msg.setMessage(_message); + msg.setEncryptedHeader(_block.getData()); + msg.setMessageExpiration(_message.getMessageExpiration()); + + send(msg); + } + + /** + * Send the message on its way.

+ * + * This could garlic route the message to the _block.getRouter, or it could + * send it there via a tunnel, or it could just send it direct.

+ * + * For simplicity, its currently going direct. + * + */ + protected void send(I2NPMessage msg) { + _log.info("Sending reply with " + _message.getClass().getName() + " in a sourceRouteeplyMessage to " + _block.getRouter().toBase64()); + SendMessageDirectJob j = new SendMessageDirectJob(msg, _block.getRouter(), _priority); + JobQueue.getInstance().addJob(j); + } + + public String getName() { return "Send Reply Message"; } +} diff --git a/router/java/src/net/i2p/router/message/SendTunnelMessageJob.java b/router/java/src/net/i2p/router/message/SendTunnelMessageJob.java new file mode 100644 index 0000000000..0b1d527a08 --- /dev/null +++ b/router/java/src/net/i2p/router/message/SendTunnelMessageJob.java @@ -0,0 +1,426 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import net.i2p.crypto.AESEngine; +import net.i2p.crypto.KeyGenerator; +import net.i2p.crypto.SHA256Generator; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataStructure; +import net.i2p.data.Hash; +import net.i2p.data.Payload; +import net.i2p.data.RouterIdentity; +import net.i2p.data.SessionKey; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DataMessage; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.TunnelMessage; +import net.i2p.data.i2np.TunnelVerificationStructure; +import net.i2p.router.ClientMessage; +import net.i2p.router.ClientMessagePool; +import net.i2p.router.InNetMessage; +import net.i2p.router.InNetMessagePool; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageHistory; +import net.i2p.router.MessageReceptionInfo; +import net.i2p.router.MessageSelector; +import net.i2p.router.MessageValidator; +import net.i2p.router.OutNetMessage; +import net.i2p.router.ReplyJob; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.transport.OutboundMessageRegistry; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Date; + +/** + * Send a message down a tunnel that we are the gateway for + * + */ +public class SendTunnelMessageJob extends JobImpl { + private final static Log _log = new Log(SendTunnelMessageJob.class); + private I2NPMessage _message; + private Hash _destRouter; + private TunnelId _tunnelId; + private TunnelId _targetTunnelId; + private Job _onSend; + private ReplyJob _onReply; + private Job _onFailure; + private MessageSelector _selector; + private long _timeout; + private long _expiration; + private int _priority; + + public SendTunnelMessageJob(I2NPMessage msg, TunnelId tunnelId, Job onSend, ReplyJob onReply, Job onFailure, MessageSelector selector, long timeoutMs, int priority) { + this(msg, tunnelId, null, null, onSend, onReply, onFailure, selector, timeoutMs, priority); + } + + public SendTunnelMessageJob(I2NPMessage msg, TunnelId tunnelId, Hash targetRouter, TunnelId targetTunnelId, Job onSend, ReplyJob onReply, Job onFailure, MessageSelector selector, long timeoutMs, int priority) { + super(); + if (msg == null) + throw new IllegalArgumentException("wtf, null message? sod off"); + _message = msg; + _destRouter = targetRouter; + _tunnelId = tunnelId; + _targetTunnelId = targetTunnelId; + _onSend = onSend; + _onReply = onReply; + _onFailure = onFailure; + _selector = selector; + _timeout = timeoutMs; + _priority = priority; + + if (timeoutMs < 50*1000) { + _log.info("Sending tunnel message to expire in " + timeoutMs + "ms containing " + msg.getUniqueId() + " (a " + msg.getClass().getName() + ")", new Exception("SendTunnel from")); + } + //_log.info("Send tunnel message " + msg.getClass().getName() + " to " + _destRouter + " over " + _tunnelId + " targetting tunnel " + _targetTunnelId, new Exception("SendTunnel from")); + _expiration = Clock.getInstance().now() + timeoutMs; + } + + public void runJob() { + TunnelInfo info = TunnelManagerFacade.getInstance().getTunnelInfo(_tunnelId); + if (info == null) { + _log.debug("Message for unknown tunnel [" + _tunnelId + "] received, forward to " + _destRouter); + if ( (_tunnelId == null) || (_destRouter == null) ) { + _log.error("Someone br0ke us. where is this message supposed to go again?", getAddedBy()); + return; + } + TunnelMessage msg = new TunnelMessage(); + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + _message.writeBytes(baos); + msg.setData(baos.toByteArray()); + msg.setTunnelId(_tunnelId); + msg.setMessageExpiration(new Date(_expiration)); + JobQueue.getInstance().addJob(new SendMessageDirectJob(msg, _destRouter, _onSend, _onReply, _onFailure, _selector, _expiration, _priority)); + + String bodyType = _message.getClass().getName(); + MessageHistory.getInstance().wrap(bodyType, _message.getUniqueId(), TunnelMessage.class.getName(), msg.getUniqueId()); + } catch (IOException ioe) { + _log.error("Error writing out the tunnel message to send to the tunnel", ioe); + } catch (DataFormatException dfe) { + _log.error("Error writing out the tunnel message to send to the tunnel", dfe); + } + return; + } + + if (isEndpoint(info)) { + _log.info("Tunnel message where we're both the gateway and the endpoint - honor instructions"); + honorInstructions(info); + return; + } else if (isGateway(info)) { + handleAsGateway(info); + return; + } else { + handleAsParticipant(info); + return; + } + } + + private void handleAsGateway(TunnelInfo info) { + // since we are the gateway, we don't need to verify the data structures + TunnelInfo us = getUs(info); + if (us == null) { + _log.error("We are not participating in this /known/ tunnel - was the router reset?"); + if (_onFailure != null) + JobQueue.getInstance().addJob(_onFailure); + } else { + // we're the gateway, so sign, encrypt, and forward to info.getNextHop() + TunnelMessage msg = prepareMessage(info); + if (msg == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("wtf, unable to prepare a tunnel message to the next hop, when we're the gateway and hops remain? tunnel: " + info); + if (_onFailure != null) + JobQueue.getInstance().addJob(_onFailure); + return; + } + _log.debug("Tunnel message created: " + msg + " out of encrypted message: " + _message); + long now = Clock.getInstance().now(); + if (_expiration < now + 15*1000) { + _log.warn("Adding a tunnel message that will expire shortly [" + new Date(_expiration) + "]", getAddedBy()); + } + msg.setMessageExpiration(new Date(_expiration)); + JobQueue.getInstance().addJob(new SendMessageDirectJob(msg, info.getNextHop(), _onSend, _onReply, _onFailure, _selector, _expiration, _priority)); + } + } + + private void handleAsParticipant(TunnelInfo info) { + // SendTunnelMessageJob shouldn't be used for participants! + if (_log.shouldLog(Log.DEBUG)) + _log.debug("SendTunnelMessageJob for a participant... ", getAddedBy()); + + if (!(_message instanceof TunnelMessage)) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Cannot inject non-tunnel messages as a participant!" + _message, getAddedBy()); + if (_onFailure != null) + JobQueue.getInstance().addJob(_onFailure); + return; + } + + TunnelMessage msg = (TunnelMessage)_message; + + TunnelVerificationStructure struct = msg.getVerificationStructure(); + if ( (info.getVerificationKey() == null) || (info.getVerificationKey().getKey() == null) ) { + if (_log.shouldLog(Log.ERROR)) + _log.error("No verification key for the participant? tunnel: " + info, getAddedBy()); + if (_onFailure != null) + JobQueue.getInstance().addJob(_onFailure); + return; + } + + boolean ok = struct.verifySignature(info.getVerificationKey().getKey()); + if (!ok) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Failed tunnel verification! Spoofing / tagging attack? " + _message, getAddedBy()); + if (_onFailure != null) + JobQueue.getInstance().addJob(_onFailure); + return; + } else { + if (info.getNextHop() != null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Message for tunnel " + info.getTunnelId().getTunnelId() + " received where we're not the gateway and there are remaining hops, so forward it on to " + + info.getNextHop().toBase64() + " via SendMessageDirectJob"); + JobQueue.getInstance().addJob(new SendMessageDirectJob(msg, info.getNextHop(), _onSend, null, _onFailure, null, _message.getMessageExpiration().getTime(), _priority)); + return; + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("Should not be reached - participant, but no more hops?!"); + if (_onFailure != null) + JobQueue.getInstance().addJob(_onFailure); + return; + } + } + } + + + /** find our place in the tunnel */ + private TunnelInfo getUs(TunnelInfo info) { + Hash us = Router.getInstance().getRouterInfo().getIdentity().getHash(); + TunnelInfo lastUs = null; + while (info != null) { + if (us.equals(info.getThisHop())) + lastUs = info; + info = info.getNextHopInfo(); + } + return lastUs; + } + + /** are we the endpoint for the tunnel? */ + private boolean isEndpoint(TunnelInfo info) { + TunnelInfo us = getUs(info); + if (us == null) return false; + return (us.getNextHop() == null); + } + + /** are we the gateway for the tunnel? */ + private boolean isGateway(TunnelInfo info) { + TunnelInfo us = getUs(info); + if (us == null) return false; + return (us.getSigningKey() != null); // only the gateway can sign + } + + private TunnelMessage prepareMessage(TunnelInfo info) { + TunnelMessage msg = new TunnelMessage(); + + SessionKey key = KeyGenerator.getInstance().generateSessionKey(); + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDelayRequested(false); + instructions.setEncrypted(true); + instructions.setEncryptionKey(key); + + // if we aren't told where to send it, have it be processed locally at the endpoint + // but if we are, have the endpoint forward it appropriately. + // note that this algorithm does not currently support instructing the endpoint to send to a Destination + if (_destRouter != null) { + instructions.setRouter(_destRouter); + if (_targetTunnelId != null) { + _log.debug("Instructions target tunnel " + _targetTunnelId + " on router " + _destRouter.calculateHash()); + instructions.setTunnelId(_targetTunnelId); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_TUNNEL); + } else { + _log.debug("Instructions target router " + _destRouter.toBase64()); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_ROUTER); + } + } else { + if (_message instanceof DataMessage) { + _log.debug("Instructions are for local message delivery at the endpoint with a DataMessage to be sent to a Destination"); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_LOCAL); + } else { + _log.debug("Instructions are for local delivery at the endpoint targetting the now-local router"); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_LOCAL); + } + } + + if (info == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Tunnel info is null to send message " + _message); + return null; + } else if ( (info.getEncryptionKey() == null) || (info.getEncryptionKey().getKey() == null) ) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Tunnel encryption key is null when we're the gateway?! info: " + info); + return null; + } + + byte encryptedInstructions[] = encrypt(instructions, info.getEncryptionKey().getKey(), 512); + byte encryptedMessage[] = encrypt(_message, key, 1024); + TunnelVerificationStructure verification = createVerificationStructure(encryptedMessage, info); + + String bodyType = _message.getClass().getName(); + MessageHistory.getInstance().wrap(bodyType, _message.getUniqueId(), TunnelMessage.class.getName(), msg.getUniqueId()); + + _log.debug("Tunnel message prepared: instructions = " + instructions); + + msg.setData(encryptedMessage); + msg.setEncryptedDeliveryInstructions(encryptedInstructions); + msg.setTunnelId(_tunnelId); + msg.setVerificationStructure(verification); + return msg; + } + + private TunnelVerificationStructure createVerificationStructure(byte encryptedMessage[], TunnelInfo info) { + TunnelVerificationStructure struct = new TunnelVerificationStructure(); + struct.setMessageHash(SHA256Generator.getInstance().calculateHash(encryptedMessage)); + struct.sign(info.getSigningKey().getKey()); + return struct; + } + + private byte[] encrypt(DataStructure struct, SessionKey key, int paddedSize) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(paddedSize); + struct.writeBytes(baos); + + byte iv[] = new byte[16]; + Hash h = SHA256Generator.getInstance().calculateHash(key.getData()); + System.arraycopy(h.getData(), 0, iv, 0, iv.length); + return AESEngine.getInstance().safeEncrypt(baos.toByteArray(), key, iv, paddedSize); + } catch (IOException ioe) { + _log.error("Error writing out data to encrypt", ioe); + } catch (DataFormatException dfe) { + _log.error("Error formatting data to encrypt", dfe); + } + return null; + } + + private void honorInstructions(TunnelInfo info) { + if (_selector != null) + createFakeOutNetMessage(); + + if (_onSend != null) { + _log.debug("Firing onSend as we're honoring the instructions"); + JobQueue.getInstance().addJob(_onSend); + } + + // since we are the gateway, we don't need to decrypt the delivery instructions or the payload + + RouterIdentity ident = Router.getInstance().getRouterInfo().getIdentity(); + + if (_destRouter != null) { + I2NPMessage msg = null; + if (_targetTunnelId != null) { + _log.debug("Forward " + _message.getClass().getName() + " message off to remote tunnel " + _targetTunnelId.getTunnelId() + " on router " + _destRouter.toBase64()); + TunnelMessage tmsg = new TunnelMessage(); + tmsg.setEncryptedDeliveryInstructions(null); + tmsg.setTunnelId(_targetTunnelId); + tmsg.setVerificationStructure(null); + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + try { + _message.writeBytes(baos); + } catch (IOException ioe) { + _log.error("Error writing out the message to be forwarded...??", ioe); + } catch (DataFormatException dfe) { + _log.error("Error writing message to be forwarded...???", dfe); + } + tmsg.setData(baos.toByteArray()); + msg = tmsg; + } else { + _log.debug("Forward " + _message.getClass().getName() + " message off to remote router " + _destRouter.toBase64()); + msg = _message; + } + long now = Clock.getInstance().now(); + //if (_expiration < now) { + _expiration = now + Router.CLOCK_FUDGE_FACTOR; + //_log.info("Fudging the message send so it expires in the fudge factor..."); + //} + + if (_expiration - 30*1000 < now) { + _log.error("Why are we trying to send a " + _message.getClass().getName() + " message with " + (_expiration-now) + "ms left?", getAddedBy()); + } + + String bodyType = _message.getClass().getName(); + MessageHistory.getInstance().wrap(bodyType, _message.getUniqueId(), TunnelMessage.class.getName(), msg.getUniqueId()); + + // don't specify a selector, since createFakeOutNetMessage already does that + JobQueue.getInstance().addJob(new SendMessageDirectJob(msg, _destRouter, _onSend, _onReply, _onFailure, null, _expiration, _priority)); + } else { + if ( (info.getDestination() == null) || !(_message instanceof DataMessage) ) { + // its a network message targeting us... + _log.debug("Destination is null or its not a DataMessage - pass it off to the InNetMessagePool"); + InNetMessage msg = new InNetMessage(); + msg.setFromRouter(ident); + msg.setFromRouterHash(ident.getHash()); + msg.setMessage(_message); + msg.setReplyBlock(null); + InNetMessagePool.getInstance().add(msg); + } else { + _log.debug("Destination is not null and it is a DataMessage - pop it into the ClientMessagePool"); + DataMessage msg = (DataMessage)_message; + boolean valid = MessageValidator.getInstance().validateMessage(msg.getUniqueId(), msg.getMessageExpiration().getTime()); + if (!valid) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Duplicate data message received [" + msg.getUniqueId() + " expiring on " + msg.getMessageExpiration() + "]"); + MessageHistory.getInstance().droppedOtherMessage(msg); + MessageHistory.getInstance().messageProcessingError(msg.getUniqueId(), msg.getClass().getName(), "Duplicate"); + return; + } + + Payload payload = new Payload(); + payload.setEncryptedData(msg.getData()); + + MessageReceptionInfo receptionInfo = new MessageReceptionInfo(); + receptionInfo.setFromPeer(ident.getHash()); + receptionInfo.setFromTunnel(_tunnelId); + + ClientMessage clientMessage = new ClientMessage(); + clientMessage.setDestination(info.getDestination()); + clientMessage.setPayload(payload); + clientMessage.setReceptionInfo(receptionInfo); + ClientMessagePool.getInstance().add(clientMessage); + MessageHistory.getInstance().receivePayloadMessage(msg.getUniqueId()); + } + } + } + + private void createFakeOutNetMessage() { + // now we create a fake outNetMessage to go onto the registry so we can select + _log.debug("Registering a fake outNetMessage for the message tunneled locally since we have a selector"); + OutNetMessage outM = new OutNetMessage(); + outM.setExpiration(_expiration); + outM.setMessage(_message); + outM.setOnFailedReplyJob(_onFailure); + outM.setOnFailedSendJob(_onFailure); + outM.setOnReplyJob(_onReply); + outM.setOnSendJob(_onSend); + outM.setPriority(_priority); + outM.setReplySelector(_selector); + outM.setTarget(null); + OutboundMessageRegistry.getInstance().registerPending(outM); + } + + public String getName() { return "Send Tunnel Message"; } +} diff --git a/router/java/src/net/i2p/router/message/SourceRouteReplyMessageHandler.java b/router/java/src/net/i2p/router/message/SourceRouteReplyMessageHandler.java new file mode 100644 index 0000000000..3f3c02e974 --- /dev/null +++ b/router/java/src/net/i2p/router/message/SourceRouteReplyMessageHandler.java @@ -0,0 +1,32 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.HandlerJobBuilder; +import net.i2p.router.Job; + +import net.i2p.data.RouterIdentity; +import net.i2p.data.Hash; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.SourceRouteReplyMessage; +import net.i2p.data.i2np.SourceRouteBlock; + +/** + * HandlerJobBuilder to build jobs to handle SourceRouteReplyMessages + * + */ +public class SourceRouteReplyMessageHandler implements HandlerJobBuilder { + + public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock) { + // ignore the replyBlock for now + HandleSourceRouteReplyMessageJob job = new HandleSourceRouteReplyMessageJob((SourceRouteReplyMessage)receivedMessage, from, fromHash); + return job; + } + +} diff --git a/router/java/src/net/i2p/router/message/TunnelMessageHandler.java b/router/java/src/net/i2p/router/message/TunnelMessageHandler.java new file mode 100644 index 0000000000..ab5fa08e33 --- /dev/null +++ b/router/java/src/net/i2p/router/message/TunnelMessageHandler.java @@ -0,0 +1,32 @@ +package net.i2p.router.message; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.HandlerJobBuilder; +import net.i2p.router.Job; + +import net.i2p.data.RouterIdentity; +import net.i2p.data.Hash; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.TunnelMessage; +import net.i2p.data.i2np.SourceRouteBlock; + +/** + * HandlerJobBuilder to build jobs to handle TunnelMessages + * + */ +public class TunnelMessageHandler implements HandlerJobBuilder { + + public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock) { + // ignore the replyBlock for now + HandleTunnelMessageJob job = new HandleTunnelMessageJob((TunnelMessage)receivedMessage, from, fromHash); + return job; + } + +} diff --git a/router/java/src/net/i2p/router/networkdb/DatabaseLookupMessageHandler.java b/router/java/src/net/i2p/router/networkdb/DatabaseLookupMessageHandler.java new file mode 100644 index 0000000000..04d1af2622 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/DatabaseLookupMessageHandler.java @@ -0,0 +1,28 @@ +package net.i2p.router.networkdb; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.HandlerJobBuilder; +import net.i2p.router.Job; +import net.i2p.data.RouterIdentity; +import net.i2p.data.Hash; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.DatabaseLookupMessage; +import net.i2p.data.i2np.SourceRouteBlock; + +/** + * Build a HandleDatabaseLookupMessageJob whenever a DatabaseLookupMessage arrives + * + */ +public class DatabaseLookupMessageHandler implements HandlerJobBuilder { + public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock) { + // ignore the reply block for the moment + return new HandleDatabaseLookupMessageJob((DatabaseLookupMessage)receivedMessage, from, fromHash); + } +} diff --git a/router/java/src/net/i2p/router/networkdb/DatabaseSearchReplyMessageHandler.java b/router/java/src/net/i2p/router/networkdb/DatabaseSearchReplyMessageHandler.java new file mode 100644 index 0000000000..9ddeda12e7 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/DatabaseSearchReplyMessageHandler.java @@ -0,0 +1,28 @@ +package net.i2p.router.networkdb; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.HandlerJobBuilder; +import net.i2p.router.Job; +import net.i2p.data.RouterIdentity; +import net.i2p.data.Hash; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.DatabaseSearchReplyMessage; +import net.i2p.data.i2np.SourceRouteBlock; + +/** + * Build a HandleDatabaseSearchReplyMessageJob whenever a DatabaseSearchReplyMessage arrives + * + */ +public class DatabaseSearchReplyMessageHandler implements HandlerJobBuilder { + public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock) { + // ignore the reply block for now + return new HandleDatabaseSearchReplyMessageJob((DatabaseSearchReplyMessage)receivedMessage, from, fromHash); + } +} diff --git a/router/java/src/net/i2p/router/networkdb/DatabaseStoreMessageHandler.java b/router/java/src/net/i2p/router/networkdb/DatabaseStoreMessageHandler.java new file mode 100644 index 0000000000..649a9a7553 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/DatabaseStoreMessageHandler.java @@ -0,0 +1,28 @@ +package net.i2p.router.networkdb; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.HandlerJobBuilder; +import net.i2p.router.Job; +import net.i2p.data.RouterIdentity; +import net.i2p.data.Hash; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.i2np.SourceRouteBlock; + +/** + * Create a HandleDatabaseStoreMessageJob whenever a DatabaseStoreMessage arrives + * + */ +public class DatabaseStoreMessageHandler implements HandlerJobBuilder { + public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock) { + // ignore the reply block for the moment + return new HandleDatabaseStoreMessageJob((DatabaseStoreMessage)receivedMessage, from, fromHash); + } +} diff --git a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java new file mode 100644 index 0000000000..96986c5ea8 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java @@ -0,0 +1,189 @@ +package net.i2p.router.networkdb; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Set; +import java.util.Date; + +import net.i2p.data.DataStructure; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.RouterIdentity; +import net.i2p.data.RouterInfo; +import net.i2p.data.TunnelId; +import net.i2p.data.DataFormatException; +import net.i2p.data.i2np.DatabaseLookupMessage; +import net.i2p.data.i2np.DatabaseSearchReplyMessage; +import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.TunnelMessage; +import net.i2p.router.Job; +import net.i2p.router.JobQueue; +import net.i2p.router.JobImpl; +import net.i2p.router.MessageHistory; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.message.SendMessageDirectJob; +import net.i2p.router.message.SendTunnelMessageJob; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Handle a lookup for a key received from a remote peer. Needs to be implemented + * to send back replies, etc + * + */ +public class HandleDatabaseLookupMessageJob extends JobImpl { + private final static Log _log = new Log(HandleDatabaseLookupMessageJob.class); + private DatabaseLookupMessage _message; + private RouterIdentity _from; + private Hash _fromHash; + private final static int MAX_ROUTERS_RETURNED = 3; + private final static int REPLY_TIMEOUT = 60*1000; + private final static int MESSAGE_PRIORITY = 300; + + public HandleDatabaseLookupMessageJob(DatabaseLookupMessage receivedMessage, RouterIdentity from, Hash fromHash) { + _message = receivedMessage; + _from = from; + _fromHash = fromHash; + } + + public void runJob() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Handling database lookup message for " + _message.getSearchKey()); + + Hash fromKey = _message.getFrom().getIdentity().getHash(); + + if (_message.getReplyTunnel() != null) { + if (_log.shouldLog(Log.INFO)) + _log.info("dbLookup received with replies going to " + fromKey + " (tunnel " + _message.getReplyTunnel() + ")"); + } + + NetworkDatabaseFacade.getInstance().store(fromKey, _message.getFrom()); + + LeaseSet ls = NetworkDatabaseFacade.getInstance().lookupLeaseSetLocally(_message.getSearchKey()); + if (ls != null) { + // send that lease set to the _message.getFromHash peer + if (_log.shouldLog(Log.DEBUG)) + _log.debug("We do have key " + _message.getSearchKey().toBase64() + " locally as a lease set. sending to " + fromKey.toBase64()); + sendData(_message.getSearchKey(), ls, fromKey, _message.getReplyTunnel()); + } else { + RouterInfo info = NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(_message.getSearchKey()); + if (info != null) { + // send that routerInfo to the _message.getFromHash peer + if (_log.shouldLog(Log.DEBUG)) + _log.debug("We do have key " + _message.getSearchKey().toBase64() + " locally as a router info. sending to " + fromKey.toBase64()); + sendData(_message.getSearchKey(), info, fromKey, _message.getReplyTunnel()); + } else { + // not found locally - return closest peer routerInfo structs + Set routerInfoSet = NetworkDatabaseFacade.getInstance().findNearestRouters(_message.getSearchKey(), MAX_ROUTERS_RETURNED, _message.getDontIncludePeers()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("We do not have key " + _message.getSearchKey().toBase64() + " locally. sending back " + routerInfoSet.size() + " peers to " + fromKey.toBase64()); + sendClosest(_message.getSearchKey(), routerInfoSet, fromKey, _message.getReplyTunnel()); + } + } + } + + private void sendData(Hash key, DataStructure data, Hash toPeer, TunnelId replyTunnel) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending data matching key key " + key.toBase64() + " to peer " + toPeer.toBase64() + " tunnel " + replyTunnel); + DatabaseStoreMessage msg = new DatabaseStoreMessage(); + msg.setKey(key); + if (data instanceof LeaseSet) { + msg.setLeaseSet((LeaseSet)data); + msg.setValueType(DatabaseStoreMessage.KEY_TYPE_LEASESET); + } else if (data instanceof RouterInfo) { + msg.setRouterInfo((RouterInfo)data); + msg.setValueType(DatabaseStoreMessage.KEY_TYPE_ROUTERINFO); + } + sendMessage(msg, toPeer, replyTunnel); + } + + private void sendClosest(Hash key, Set routerInfoSet, Hash toPeer, TunnelId replyTunnel) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending closest routers to key " + key.toBase64() + ": # peers = " + routerInfoSet.size() + " tunnel " + replyTunnel); + DatabaseSearchReplyMessage msg = new DatabaseSearchReplyMessage(); + msg.setFromHash(Router.getInstance().getRouterInfo().getIdentity().getHash()); + msg.setSearchKey(key); + if (routerInfoSet.size() <= 0) { + // always include something, so lets toss ourselves in there + routerInfoSet.add(Router.getInstance().getRouterInfo()); + } + msg.addReplies(routerInfoSet); + sendMessage(msg, toPeer, replyTunnel); // should this go via garlic messages instead? + } + + private void sendMessage(I2NPMessage message, Hash toPeer, TunnelId replyTunnel) { + Job send = null; + if (replyTunnel != null) { + sendThroughTunnel(message, toPeer, replyTunnel); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending reply directly to " + toPeer); + send = new SendMessageDirectJob(message, toPeer, REPLY_TIMEOUT+Clock.getInstance().now(), MESSAGE_PRIORITY); + } + + NetworkDatabaseFacade.getInstance().lookupRouterInfo(toPeer, send, null, REPLY_TIMEOUT); + } + + private void sendThroughTunnel(I2NPMessage message, Hash toPeer, TunnelId replyTunnel) { + TunnelInfo info = TunnelManagerFacade.getInstance().getTunnelInfo(replyTunnel); + + // the sendTunnelMessageJob can't handle injecting into the tunnel anywhere but the beginning + // (and if we are the beginning, we have the signing key) + if ( (info == null) || (info.getSigningKey() != null)) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending reply through " + replyTunnel + " on " + toPeer); + JobQueue.getInstance().addJob(new SendTunnelMessageJob(message, replyTunnel, toPeer, null, null, null, null, null, REPLY_TIMEOUT, MESSAGE_PRIORITY)); + } else { + // its a tunnel we're participating in, but we're NOT the gateway, so + if (_log.shouldLog(Log.INFO)) + _log.info("Want to reply to a db request via a tunnel, but we're a participant in the reply! so send it to the gateway"); + + if ( (toPeer == null) || (replyTunnel == null) ) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Someone br0ke us. where is this message supposed to go again?", getAddedBy()); + return; + } + + long expiration = REPLY_TIMEOUT + Clock.getInstance().now(); + + TunnelMessage msg = new TunnelMessage(); + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + message.writeBytes(baos); + msg.setData(baos.toByteArray()); + msg.setTunnelId(replyTunnel); + msg.setMessageExpiration(new Date(expiration)); + JobQueue.getInstance().addJob(new SendMessageDirectJob(msg, toPeer, null, null, null, null, expiration, MESSAGE_PRIORITY)); + + String bodyType = message.getClass().getName(); + MessageHistory.getInstance().wrap(bodyType, message.getUniqueId(), TunnelMessage.class.getName(), msg.getUniqueId()); + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error writing out the tunnel message to send to the tunnel", ioe); + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error writing out the tunnel message to send to the tunnel", dfe); + } + return; + } + } + + public String getName() { return "Handle Database Lookup Message"; } + + public void dropped() { + MessageHistory.getInstance().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Dropped due to overload"); + } +} diff --git a/router/java/src/net/i2p/router/networkdb/HandleDatabaseSearchReplyMessageJob.java b/router/java/src/net/i2p/router/networkdb/HandleDatabaseSearchReplyMessageJob.java new file mode 100644 index 0000000000..e40e8089ed --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/HandleDatabaseSearchReplyMessageJob.java @@ -0,0 +1,71 @@ +package net.i2p.router.networkdb; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Hash; +import net.i2p.data.RouterIdentity; +import net.i2p.data.RouterInfo; +import net.i2p.data.i2np.DatabaseSearchReplyMessage; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.util.Log; + +/** + * Receive DatabaseSearchReplyMessage data and store it in the local net db + * + */ +public class HandleDatabaseSearchReplyMessageJob extends JobImpl { + private final static Log _log = new Log(HandleDatabaseSearchReplyMessageJob.class); + private DatabaseSearchReplyMessage _message; + private RouterIdentity _from; + private Hash _fromHash; + + public HandleDatabaseSearchReplyMessageJob(DatabaseSearchReplyMessage receivedMessage, RouterIdentity from, Hash fromHash) { + _message = receivedMessage; + _from = from; + _fromHash = fromHash; + } + + public void runJob() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Handling database search reply message for key " + _message.getSearchKey().toBase64() + " with " + _message.getNumReplies() + " replies"); + if (_message.getNumReplies() > 0) + JobQueue.getInstance().addJob(new HandlePeerJob(0)); + } + + /** + * Partial job - take each reply entry, store it, then requeue again until all + * of the entries are stored. This prevents a single reply from swamping the jobqueue + * + */ + private final class HandlePeerJob extends JobImpl { + private int _curReply; + public HandlePeerJob(int reply) { + _curReply = reply; + } + public void runJob() { + boolean remaining = handle(); + if (remaining) + requeue(0); + } + + private boolean handle() { + RouterInfo info = _message.getReply(_curReply); + if (_log.shouldLog(Log.INFO)) + _log.info("On search for " + _message.getSearchKey().toBase64() + ", received " + info.getIdentity().getHash().toBase64()); + NetworkDatabaseFacade.getInstance().store(info.getIdentity().getHash(), info); + _curReply++; + return _message.getNumReplies() > _curReply; + } + public String getName() { return "Handle search reply value"; } + } + + public String getName() { return "Handle Database Search Reply Message"; } +} diff --git a/router/java/src/net/i2p/router/networkdb/HandleDatabaseStoreMessageJob.java b/router/java/src/net/i2p/router/networkdb/HandleDatabaseStoreMessageJob.java new file mode 100644 index 0000000000..aa40d16144 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/HandleDatabaseStoreMessageJob.java @@ -0,0 +1,65 @@ +package net.i2p.router.networkdb; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; + +import net.i2p.data.Hash; +import net.i2p.data.RouterIdentity; +import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.router.JobImpl; +import net.i2p.router.MessageHistory; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.ProfileManager; +import net.i2p.util.Log; + +/** + * Receive DatabaseStoreMessage data and store it in the local net db + * + */ +public class HandleDatabaseStoreMessageJob extends JobImpl { + private final static Log _log = new Log(HandleDatabaseStoreMessageJob.class); + private DatabaseStoreMessage _message; + private RouterIdentity _from; + private Hash _fromHash; + + public HandleDatabaseStoreMessageJob(DatabaseStoreMessage receivedMessage, RouterIdentity from, Hash fromHash) { + _message = receivedMessage; + _from = from; + _fromHash = fromHash; + } + + public void runJob() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Handling database store message"); + + boolean wasNew = false; + if (_message.getValueType() == DatabaseStoreMessage.KEY_TYPE_LEASESET) + wasNew = (null == NetworkDatabaseFacade.getInstance().store(_message.getKey(), _message.getLeaseSet())); + else if (_message.getValueType() == DatabaseStoreMessage.KEY_TYPE_ROUTERINFO) { + if (_log.shouldLog(Log.INFO)) + _log.info("Handling dbStore of router " + _message.getKey() + " with publishDate of " + new Date(_message.getRouterInfo().getPublished())); + wasNew = (null == NetworkDatabaseFacade.getInstance().store(_message.getKey(), _message.getRouterInfo())); + ProfileManager.getInstance().heardAbout(_message.getKey()); + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("Invalid DatabaseStoreMessage data type - " + _message.getValueType() + ": " + _message); + } + if (_from != null) + _fromHash = _from.getHash(); + if (_fromHash != null) + ProfileManager.getInstance().dbStoreReceived(_fromHash, wasNew); + } + + public String getName() { return "Handle Database Store Message"; } + + public void dropped() { + MessageHistory.getInstance().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Dropped due to overload"); + } +} diff --git a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java new file mode 100644 index 0000000000..1981f44550 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java @@ -0,0 +1,54 @@ +package net.i2p.router.networkdb; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; +import java.util.Properties; + +import net.i2p.data.DataFormatException; +import net.i2p.data.RouterInfo; +import net.i2p.router.JobImpl; +import net.i2p.router.KeyManager; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.Router; +import net.i2p.router.CommSystemFacade; +import net.i2p.router.StatisticsManager; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +/** + * Publish the local router's RouterInfo every 5 to 10 minutes + * + */ +public class PublishLocalRouterInfoJob extends JobImpl { + private final static Log _log = new Log(PublishLocalRouterInfoJob.class); + final static long PUBLISH_DELAY = 5*60*1000; // every 5 to 10 minutes (since we randomize) + + public String getName() { return "Publish Local Router Info"; } + public void runJob() { + RouterInfo ri = new RouterInfo(Router.getInstance().getRouterInfo()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Old routerInfo contains " + ri.getAddresses().size() + " addresses and " + ri.getOptions().size() + " options"); + Properties stats = StatisticsManager.getInstance().publishStatistics(); + try { + ri.setPublished(Clock.getInstance().now()); + ri.setOptions(stats); + ri.setAddresses(CommSystemFacade.getInstance().createAddresses()); + ri.sign(KeyManager.getInstance().getSigningPrivateKey()); + Router.getInstance().setRouterInfo(ri); + if (_log.shouldLog(Log.INFO)) + _log.info("Newly updated routerInfo is published with " + stats.size() + "/" + ri.getOptions().size() + " options on " + new Date(ri.getPublished())); + NetworkDatabaseFacade.getInstance().publish(ri); + } catch (DataFormatException dfe) { + _log.error("Error signing the updated local router info!", dfe); + } + requeue(PUBLISH_DELAY + RandomSource.getInstance().nextInt((int)PUBLISH_DELAY)); + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/DataPublisherJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/DataPublisherJob.java new file mode 100644 index 0000000000..fd6105519a --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/DataPublisherJob.java @@ -0,0 +1,88 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.DataStructure; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.Router; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; + +class DataPublisherJob extends JobImpl { + private final static Log _log = new Log(DataPublisherJob.class); + private KademliaNetworkDatabaseFacade _facade; + private final static long RERUN_DELAY_MS = 30*1000; + private final static int MAX_SEND_PER_RUN = 5; // publish no more than 5 at a time + private final static long STORE_TIMEOUT = 60*1000; // give 'er a minute to send the data + + public DataPublisherJob(KademliaNetworkDatabaseFacade facade) { + super(); + _facade = facade; + getTiming().setStartAfter(Clock.getInstance().now()+RERUN_DELAY_MS); // not immediate... + } + + public String getName() { return "Data Publisher Job"; } + public void runJob() { + Set toSend = selectKeysToSend(); + _log.info("Keys being published in this timeslice: " + toSend); + for (Iterator iter = toSend.iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + DataStructure data = _facade.getDataStore().get(key); + if (data == null) { + _log.warn("Trying to send a key we dont have? " + key); + continue; + } + if (data instanceof LeaseSet) { + LeaseSet ls = (LeaseSet)data; + if (!ls.isCurrent(Router.CLOCK_FUDGE_FACTOR)) { + _log.warn("Not publishing a lease that isn't current - " + key, new Exception("Publish expired lease?")); + } + } + StoreJob store = new StoreJob(_facade, key, data, null, null, STORE_TIMEOUT); + JobQueue.getInstance().addJob(store); + } + requeue(RERUN_DELAY_MS); + } + + private Set selectKeysToSend() { + Set explicit = _facade.getExplicitSendKeys(); + Set toSend = new HashSet(MAX_SEND_PER_RUN); + if (explicit.size() < MAX_SEND_PER_RUN) { + toSend.addAll(explicit); + _facade.removeFromExplicitSend(explicit); + + Set passive = _facade.getPassivelySendKeys(); + Set psend = new HashSet(passive.size()); + for (Iterator iter = passive.iterator(); iter.hasNext(); ) { + if (toSend.size() >= MAX_SEND_PER_RUN) break; + Hash key = (Hash)iter.next(); + toSend.add(key); + psend.add(key); + } + _facade.removeFromPassiveSend(psend); + } else { + for (Iterator iter = explicit.iterator(); iter.hasNext(); ) { + if (toSend.size() >= MAX_SEND_PER_RUN) break; + Hash key = (Hash)iter.next(); + toSend.add(key); + } + _facade.removeFromExplicitSend(toSend); + } + + return toSend; + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/DataRepublishingSelectorJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/DataRepublishingSelectorJob.java new file mode 100644 index 0000000000..1b3b5b420a --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/DataRepublishingSelectorJob.java @@ -0,0 +1,160 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeMap; + +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.RouterInfo; +import net.i2p.router.JobImpl; +import net.i2p.router.Router; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +class DataRepublishingSelectorJob extends JobImpl { + private final static Log _log = new Log(DataRepublishingSelectorJob.class); + private KademliaNetworkDatabaseFacade _facade; + + private final static long RERUN_DELAY_MS = 1*60*1000; + public final static int MAX_PASSIVE_POOL_SIZE = 30; // no need to have the pool be too big + + /** + * For every bucket away from us, resend period increases by 5 minutes - so we resend + * our own key every 5 minutes, and keys very far from us every 2.5 hours, increasing + * linearly + */ + public final static long RESEND_BUCKET_FACTOR = 5*60*1000; + + /** + * % chance any peer not specializing in the lease's key will broadcast it on each pass + * of this job /after/ waiting 5 minutes (one RESENT_BUCKET_FACTOR). In other words, + * .5% of routers will broadcast a particular unexpired lease to (say) 5 peers every + * minute. + * + */ + private final static int LEASE_REBROADCAST_PROBABILITY = 5; + /** + * LEASE_REBROADCAST_PROBABILITY out of LEASE_REBROADCAST_PROBABILITY_SCALE chance. + */ + private final static int LEASE_REBROADCAST_PROBABILITY_SCALE = 1000; + + public DataRepublishingSelectorJob(KademliaNetworkDatabaseFacade facade) { + super(); + _facade = facade; + getTiming().setStartAfter(Clock.getInstance().now()+RERUN_DELAY_MS); // not immediate... + } + + public String getName() { return "Data Publisher Job"; } + public void runJob() { + Set toSend = selectKeysToSend(); + _log.info("Keys being queued up for publishing: " + toSend); + _facade.queueForPublishing(toSend); + requeue(RERUN_DELAY_MS); + } + + /** + * Run through the entire data store, ranking how much we want to send each + * data point, and returning the ones we most want to send so that they can + * be placed in the passive send pool (without making the passive pool greater + * than the limit) + * + */ + private Set selectKeysToSend() { + Set alreadyQueued = new HashSet(128); + alreadyQueued.addAll(_facade.getPassivelySendKeys()); + + int toAdd = MAX_PASSIVE_POOL_SIZE - alreadyQueued.size(); + _log.debug("Keys we need to queue up to fill the passive send pool: " + toAdd); + if (toAdd <= 0) return new HashSet(); + + alreadyQueued.addAll(_facade.getExplicitSendKeys()); + + Set keys = _facade.getDataStore().getKeys(); + keys.removeAll(alreadyQueued); + + _log.debug("Total number of keys in the datastore: " + keys.size()); + + TreeMap toSend = new TreeMap(); + for (Iterator iter = keys.iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + Long lastPublished = _facade.getLastSent(key); + long publishRank = rankPublishNeed(key, lastPublished); + _log.debug("Publish rank for " + key + ": " + publishRank); + if (publishRank > 0) { + while (toSend.containsKey(new Long(publishRank))) + publishRank++; + toSend.put(new Long(publishRank), key); + } + } + Set rv = new HashSet(toAdd); + for (Iterator iter = toSend.values().iterator(); iter.hasNext(); ) { + if (rv.size() > toAdd) break; + Hash key = (Hash)iter.next(); + rv.add(key); + } + return rv; + } + + /** + * Higher values mean we want to publish it more, and values less than or equal to zero + * means we don't want to publish it + * + */ + private long rankPublishNeed(Hash key, Long lastPublished) { + int bucket = _facade.getKBuckets().pickBucket(key); + long sendPeriod = (bucket+1) * RESEND_BUCKET_FACTOR; + long now = Clock.getInstance().now(); + if (lastPublished.longValue() < now-sendPeriod) { + RouterInfo ri = _facade.lookupRouterInfoLocally(key); + if (ri != null) { + if (ri.isCurrent(2 * ExpireRoutersJob.EXPIRE_DELAY)) { + // last time it was sent was before the last send period + return KBucketSet.NUM_BUCKETS - bucket; + } else { + _log.info("Not republishing router " + key + " since it is really old [" + (now-ri.getPublished()) + "ms]"); + return -2; + } + } else { + LeaseSet ls = _facade.lookupLeaseSetLocally(key); + if (ls != null) { + if (ls.isCurrent(Router.CLOCK_FUDGE_FACTOR)) { + // last time it was sent was before the last send period + return KBucketSet.NUM_BUCKETS - bucket; + } else { + _log.info("Not republishing leaseSet " + key + " since it is really old [" + (now-ls.getEarliestLeaseDate()) + "ms]"); + return -3; + } + } else { + _log.info("Key " + key + " is not a leaseSet or routerInfo, definitely not publishing it"); + return -5; + } + } + } else { + // its been published since the last period we want to publish it + + if (now - RESEND_BUCKET_FACTOR > lastPublished.longValue()) { + if (_facade.lookupRouterInfoLocally(key) != null) { + // randomize the chance of rebroadcast for leases if we haven't + // sent it within 5 minutes + int val = RandomSource.getInstance().nextInt(LEASE_REBROADCAST_PROBABILITY_SCALE); + if (val <= LEASE_REBROADCAST_PROBABILITY) { + _log.info("Randomized rebroadcast of leases tells us to send " + key + ": " + val); + return 1; + } + } + } + return -1; + } + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/DataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/DataStore.java new file mode 100644 index 0000000000..84dc6f019d --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/DataStore.java @@ -0,0 +1,22 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Hash; +import net.i2p.data.DataStructure; + +import java.util.Set; + +public interface DataStore { + public boolean isKnown(Hash key); + public DataStructure get(Hash key); + public void put(Hash key, DataStructure data); + public DataStructure remove(Hash key); + public Set getKeys(); +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExpireLeasesJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireLeasesJob.java new file mode 100644 index 0000000000..d909eaaaa4 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireLeasesJob.java @@ -0,0 +1,73 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.router.JobImpl; +import net.i2p.router.Router; +import net.i2p.util.Log; + +/** + * Periodically search through all leases to find expired ones, failing those + * keys and firing up a new search for each (in case we want it later, might as + * well preemptively fetch it) + * + */ +class ExpireLeasesJob extends JobImpl { + private final static Log _log = new Log(ExpireLeasesJob.class); + private KademliaNetworkDatabaseFacade _facade; + + private final static long RERUN_DELAY_MS = 1*60*1000; + + public ExpireLeasesJob(KademliaNetworkDatabaseFacade facade) { + super(); + _facade = facade; + } + + public String getName() { return "Expire Lease Sets Job"; } + public void runJob() { + Set toExpire = selectKeysToExpire(); + _log.info("Leases to expire: " + toExpire); + for (Iterator iter = toExpire.iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + _facade.fail(key); + _log.info("Lease " + key + " is expiring, so lets look for it again", new Exception("Expire and search")); + _facade.lookupLeaseSet(key, null, null, RERUN_DELAY_MS); + } + //_facade.queueForExploration(toExpire); // don't do explicit searches, just explore passively + requeue(RERUN_DELAY_MS); + } + + /** + * Run through the entire data store, finding all expired leaseSets (ones that + * don't have any leases that haven't yet passed, even with the CLOCK_FUDGE_FACTOR) + * + */ + private Set selectKeysToExpire() { + Set keys = _facade.getDataStore().getKeys(); + Set toExpire = new HashSet(128); + for (Iterator iter = keys.iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + Object obj = _facade.getDataStore().get(key); + if (obj instanceof LeaseSet) { + LeaseSet ls = (LeaseSet)obj; + if (!ls.isCurrent(Router.CLOCK_FUDGE_FACTOR)) + toExpire.add(key); + else + _log.debug("Lease " + ls.getDestination().calculateHash() + " is current, no need to expire"); + } + } + return toExpire; + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java new file mode 100644 index 0000000000..3f5207f669 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java @@ -0,0 +1,106 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; +import net.i2p.router.JobImpl; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * Go through the routing table pick routers that are performing poorly or + * is out of date, but don't expire routers we're actively tunneling through. + * If a peer is performing worse than some threshold (via profile.rankLiveliness) + * drop it and don't ask any questions. If a peer isn't ranked really poorly, but + * we just haven't heard from it in a while, drop it and add it to the set of + * keys we want the netDb to explore. + * + */ +class ExpireRoutersJob extends JobImpl { + private final static Log _log = new Log(ExpireRoutersJob.class); + private KademliaNetworkDatabaseFacade _facade; + + private final static long RERUN_DELAY_MS = 30*1000; + /** + * If a routerInfo structure isn't updated within an hour, drop it + * and search for a later version. This value should be large enough + * to deal with the Router.CLOCK_FUDGE_FACTOR. + */ + public final static long EXPIRE_DELAY = 60*60*1000; + + public ExpireRoutersJob(KademliaNetworkDatabaseFacade facade) { + super(); + _facade = facade; + } + + public String getName() { return "Expire Routers Job"; } + public void runJob() { + Set toExpire = selectKeysToExpire(); + _log.info("Routers to expire (drop and try to refetch): " + toExpire); + for (Iterator iter = toExpire.iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + _facade.fail(key); + } + _facade.queueForExploration(toExpire); + + requeue(RERUN_DELAY_MS); + } + + + /** + * Run through all of the known peers and pick ones that have really old + * routerInfo publish dates, excluding ones that are in use by some tunnels, + * so that they can be failed & queued for searching + * + */ + private Set selectKeysToExpire() { + Set possible = getNotInUse(); + Set expiring = new HashSet(16); + long earliestPublishDate = Clock.getInstance().now() - EXPIRE_DELAY; + + for (Iterator iter = possible.iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + RouterInfo ri = _facade.lookupRouterInfoLocally(key); + if (ri != null) { + if (!ri.isCurrent(EXPIRE_DELAY)) { + if (_log.shouldLog(Log.INFO)) + _log.info("Expiring RouterInfo for " + key.toBase64() + " [published on " + new Date(ri.getPublished()) + "]"); + expiring.add(key); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Not expiring routerInfo for " + key.toBase64() + " [published on " + new Date(ri.getPublished()) + "]"); + } + } + } + + return expiring; + } + + /** all peers not in use by tunnels */ + private Set getNotInUse() { + Set possible = new HashSet(16); + for (Iterator iter = _facade.getAllRouters().iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + if (!TunnelManagerFacade.getInstance().isInUse(peer)) { + possible.add(peer); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer is in use: " + peer.toBase64()); + } + } + return possible; + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java new file mode 100644 index 0000000000..e7454fff8c --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java @@ -0,0 +1,105 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DatabaseLookupMessage; +import net.i2p.router.Router; +import net.i2p.util.Log; + +/** + * Search for a particular key iteratively until we either find a value, we run + * out of peers, or the bucket the key belongs in has sufficient values in it. + * Well, we're skipping the 'bucket gets filled up' test for now, since it'll never + * get used (at least for a while). + * + */ +class ExploreJob extends SearchJob { + private final Log _log = new Log(ExploreJob.class); + + /** how long each exploration should run for (currently a trivial 20 seconds) */ + private final static long MAX_EXPLORE_TIME = 30*1000; + + /** how many of the peers closest to the key being explored do we want to explicitly say "dont send me this"? */ + private final static int NUM_CLOSEST_TO_IGNORE = 3; + + /** + * Create a new search for the routingKey specified + * + */ + public ExploreJob(KademliaNetworkDatabaseFacade facade, Hash key) { + // note that we're treating the last param (isLease) as *false* since we're just exploring. + // if this collides with an actual leaseSet's key, neat, but that wouldn't imply we're actually + // attempting to send that lease a message! + super(facade, key, null, null, MAX_EXPLORE_TIME, false, false); + } + + /** + * Build the database search message, but unlike the normal searches, we're more explicit in + * what we /dont/ want. We don't just ask them to ignore the peers we've already searched + * on, but to ignore a number of the peers we already know about (in the target key's bucket) as well. + * + * Perhaps we may want to ignore other keys too, such as the ones in nearby + * buckets, but we probably don't want the dontIncludePeers set to get too + * massive (aka sending the entire routing table as 'dont tell me about these + * guys'). but maybe we do. dunno. lots of implications. + * + * @param replyTunnelId tunnel to receive replies through + * @param replyGateway gateway for the reply tunnel + * @param expiration when the search should stop + */ + protected DatabaseLookupMessage buildMessage(TunnelId replyTunnelId, RouterInfo replyGateway, long expiration) { + DatabaseLookupMessage msg = new DatabaseLookupMessage(); + msg.setSearchKey(getState().getTarget()); + msg.setFrom(replyGateway); + msg.setDontIncludePeers(getState().getAttempted()); + msg.setMessageExpiration(new Date(expiration)); + msg.setReplyTunnel(replyTunnelId); + + Set attempted = getState().getAttempted(); + List peers = PeerSelector.getInstance().selectNearestExplicit(getState().getTarget(), NUM_CLOSEST_TO_IGNORE, attempted, getFacade().getKBuckets()); + Set toSkip = new HashSet(64); + toSkip.addAll(attempted); + toSkip.addAll(peers); + msg.setDontIncludePeers(toSkip); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peers we don't want to hear about: " + toSkip); + + return msg; + } + + + /** + * We're looking for a router, so lets build the lookup message (no need to tunnel route either, so just have + * replies sent back to us directly). This uses the similar overrides as the other buildMessage above. + * + */ + protected DatabaseLookupMessage buildMessage(long expiration) { + return buildMessage(null, Router.getInstance().getRouterInfo(), expiration); + } + + + /* + * We could override searchNext to see if we actually fill up a kbucket before + * the search expires, but, c'mon, the keyspace is just too bloody massive, and + * buckets wont be filling anytime soon, so might as well just use the SearchJob's + * searchNext + * + */ + + public String getName() { return "Kademlia NetDb Explore"; } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreKeySelectorJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreKeySelectorJob.java new file mode 100644 index 0000000000..d43b547c14 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreKeySelectorJob.java @@ -0,0 +1,79 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.router.JobImpl; +import net.i2p.util.Log; + +/** + * Go through the kbuckets and generate random keys for routers in buckets not + * yet full, attempting to keep a pool of keys we can explore with (at least one + * per bucket) + * + */ +class ExploreKeySelectorJob extends JobImpl { + private final static Log _log = new Log(ExploreKeySelectorJob.class); + private KademliaNetworkDatabaseFacade _facade; + + private final static long RERUN_DELAY_MS = 60*1000; + + public ExploreKeySelectorJob(KademliaNetworkDatabaseFacade facade) { + super(); + _facade = facade; + } + + public String getName() { return "Explore Key Selector Job"; } + public void runJob() { + Set toExplore = selectKeysToExplore(); + _log.info("Filling the explorer pool with: " + toExplore); + if (toExplore != null) + _facade.queueForExploration(toExplore); + requeue(RERUN_DELAY_MS); + } + + /** + * Run through all kbuckets with too few routers and generate a random key + * for it, with a maximum number of keys limited by the exploration pool size + * + */ + private Set selectKeysToExplore() { + Set alreadyQueued = _facade.getExploreKeys(); + if (alreadyQueued.size() > KBucketSet.NUM_BUCKETS) return null; + Set toExplore = new HashSet(KBucketSet.NUM_BUCKETS - alreadyQueued.size()); + for (int i = 0; i < KBucketSet.NUM_BUCKETS; i++) { + KBucket bucket = _facade.getKBuckets().getBucket(i); + if (bucket.getKeyCount() < KBucketSet.BUCKET_SIZE) { + boolean already = false; + for (Iterator iter = alreadyQueued.iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + if (bucket.shouldContain(key)) { + already = true; + _log.debug("Bucket " + i + " is already queued for exploration \t" + key); + break; + } + } + if (!already) { + // no keys are queued for exploring this still-too-small bucket yet + Hash key = bucket.generateRandomKey(); + _log.debug("Bucket " + i + " is NOT queued for exploration, and it only has " + bucket.getKeyCount() + " keys, so explore with \t" + key); + toExplore.add(key); + } + } else { + _log.debug("Bucket " + i + " already has enough keys (" + bucket.getKeyCount() + "), no need to explore further"); + } + } + return toExplore; + } + +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KBucket.java b/router/java/src/net/i2p/router/networkdb/kademlia/KBucket.java new file mode 100644 index 0000000000..c976e2fb5e --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KBucket.java @@ -0,0 +1,79 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Set; + +import net.i2p.data.Hash; + +/** + * Group, without inherent ordering, a set of keys a certain distance away from + * a local key, using XOR as the distance metric + * + */ +interface KBucket { + /** + * lowest order high bit for difference keys + */ + public int getRangeBegin(); + /** + * highest high bit for the difference keys + * + */ + public int getRangeEnd(); + /** + * Set the range low and high bits for difference keys + */ + public void setRange(int lowOrderBitLimit, int highOrderBitLimit); + /** + * Number of keys already contained in this kbuckey + */ + public int getKeyCount(); + /** + * whether or not the key qualifies as part of this bucket + * + */ + public boolean shouldContain(Hash key); + /** + * Add the peer to the bucket + * + * @return number of keys in the bucket after the addition + */ + public int add(Hash key); + /** + * Remove the key from the bucket + * @return true if the key existed in the bucket before removing it, else false + */ + public boolean remove(Hash key); + + /** + * Retrieve all routing table entries stored in the bucket + * @return set of Hash structures + */ + public Set getEntries(); + /** + * Retrieve hashes stored in the bucket, excluding the ones specified + * @return set of Hash structures + */ + public Set getEntries(Set toIgnoreHashes); + /** + * Fill the bucket with entries + * @param entries set of Hash structures + */ + public void setEntries(Set entries); + + /** + * Generate a random key that would go inside this bucket + * + */ + public Hash generateRandomKey(); + + public Hash getLocal(); + public void setLocal(Hash local); +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KBucketImpl.java b/router/java/src/net/i2p/router/networkdb/kademlia/KBucketImpl.java new file mode 100644 index 0000000000..5f64b6be48 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KBucketImpl.java @@ -0,0 +1,286 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.math.BigInteger; +import java.util.HashSet; +import java.util.Set; + +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; + +class KBucketImpl implements KBucket { + private final static Log _log = new Log(KBucketImpl.class); + private Set _entries; // PeerInfo structures + private Hash _local; + private int _begin; // if any bits equal or higher to this bit (in big endian order), + private int _end; // and no values higher than this bit (inclusive), include + private BigInteger _lowerBounds; // lowest distance allowed from local + private BigInteger _upperBounds; // one higher than the highest distance allowed from local + private int _size; // integer value of the number of bits that can fit between lower and upper bounds + + public KBucketImpl(Hash local) { + _entries = new HashSet(); + _local = local; + } + + public int getRangeBegin() { return _begin; } + public int getRangeEnd() { return _end; } + public void setRange(int lowOrderBitLimit, int highOrderBitLimit) { + _begin = lowOrderBitLimit; + _end = highOrderBitLimit; + if (_begin == 0) + _lowerBounds = BigInteger.ZERO; + else + _lowerBounds = BigInteger.ZERO.setBit(_begin); + _upperBounds = BigInteger.ZERO.setBit(_end); + BigInteger diff = _upperBounds.subtract(_lowerBounds); + _size = diff.bitLength(); + StringBuffer buf = new StringBuffer(1024); + buf.append("Set range: ").append(lowOrderBitLimit).append(" through ").append(highOrderBitLimit).append('\n'); + buf.append("Local key, lowest allowed key, and highest allowed key: \n"); + Hash low = getRangeBeginKey(); + Hash high = getRangeEndKey(); + if ( (_local == null) || (_local.getData() == null) ) + buf.append(toString(Hash.FAKE_HASH.getData())).append('\n'); + else + buf.append(toString(_local.getData())).append('\n'); + buf.append(toString(low.getData())).append('\n'); + buf.append(toString(high.getData())); + //_log.debug(buf.toString()); + } + public int getKeyCount() { + synchronized (_entries) { + return _entries.size(); + } + } + + public Hash getLocal() { return _local; } + public void setLocal(Hash local) { _local = local; } + + private byte[] distanceFromLocal(Hash key) { + return DataHelper.xor(key.getData(), _local.getData()); + } + + public boolean shouldContain(Hash key) { + // woohah, incredibly excessive object creation! whee! + BigInteger kv = new BigInteger(1, distanceFromLocal(key)); + int lowComp = kv.compareTo(_lowerBounds); + int highComp = kv.compareTo(_upperBounds); + + //_log.debug("kv.compareTo(low) = " + lowComp + " kv.compareTo(high) " + highComp); + + if ( (lowComp >= 0) && (highComp < 0) ) return true; + return false; + } + + public Set getEntries() { + Set entries = new HashSet(64); + synchronized (_entries) { + entries.addAll(_entries); + } + return entries; + } + public Set getEntries(Set toIgnoreHashes) { + Set entries = new HashSet(64); + synchronized (_entries) { + entries.addAll(_entries); + entries.removeAll(toIgnoreHashes); + } + return entries; + } + + public void setEntries(Set entries) { + synchronized (_entries) { + _entries.clear(); + _entries.addAll(entries); + } + } + + public int add(Hash peer) { + synchronized (_entries) { + _entries.add(peer); + return _entries.size(); + } + } + + public boolean remove(Hash peer) { + synchronized (_entries) { + return _entries.remove(peer); + } + } + + /** + * Generate a random key to go within this bucket + * + */ + public Hash generateRandomKey() { + BigInteger variance = new BigInteger(_size-1, RandomSource.getInstance()); + variance = variance.add(_lowerBounds); + //_log.debug("Random variance for " + _size + " bits: " + variance); + byte data[] = variance.toByteArray(); + byte hash[] = new byte[Hash.HASH_LENGTH]; + if (data.length <= Hash.HASH_LENGTH) { + System.arraycopy(data, 0, hash, hash.length - data.length, data.length); + } else { + System.arraycopy(data, data.length - hash.length, hash, 0, hash.length); + } + Hash key = new Hash(hash); + data = distanceFromLocal(key); + hash = new byte[Hash.HASH_LENGTH]; + if (data.length <= Hash.HASH_LENGTH) { + System.arraycopy(data, 0, hash, hash.length - data.length, data.length); + } else { + System.arraycopy(data, data.length - hash.length, hash, 0, hash.length); + } + key = new Hash(hash); + return key; + } + + public Hash getRangeBeginKey() { + BigInteger lowerBounds = _lowerBounds; + if ( (_local != null) && (_local.getData() != null) ) { + lowerBounds = lowerBounds.xor(new BigInteger(1, _local.getData())); + } + + byte data[] = lowerBounds.toByteArray(); + byte hash[] = new byte[Hash.HASH_LENGTH]; + if (data.length <= Hash.HASH_LENGTH) { + System.arraycopy(data, 0, hash, hash.length - data.length, data.length); + } else { + System.arraycopy(data, data.length - hash.length, hash, 0, hash.length); + } + Hash key = new Hash(hash); + return key; + } + + public Hash getRangeEndKey() { + BigInteger upperBounds = _upperBounds; + if ( (_local != null) && (_local.getData() != null) ) { + upperBounds = upperBounds.xor(new BigInteger(1, _local.getData())); + } + byte data[] = upperBounds.toByteArray(); + byte hash[] = new byte[Hash.HASH_LENGTH]; + if (data.length <= Hash.HASH_LENGTH) { + System.arraycopy(data, 0, hash, hash.length - data.length, data.length); + } else { + System.arraycopy(data, data.length - hash.length, hash, 0, hash.length); + } + Hash key = new Hash(hash); + return key; + } + + public String toString() { + StringBuffer buf = new StringBuffer(1024); + buf.append("KBucketImpl: "); + synchronized (_entries) { + buf.append(_entries.toString()).append("\n"); + } + buf.append("Low bit: ").append(_begin).append(" high bit: ").append(_end).append('\n'); + buf.append("Local key: \n"); + if ( (_local != null) && (_local.getData() != null) ) + buf.append(toString(_local.getData())).append('\n'); + else + buf.append("[undefined]\n"); + buf.append("Low and high keys:\n"); + buf.append(toString(getRangeBeginKey().getData())).append('\n'); + buf.append(toString(getRangeEndKey().getData())).append('\n'); + buf.append("Low and high deltas:\n"); + buf.append(_lowerBounds.toString(2)).append('\n'); + buf.append(_upperBounds.toString(2)).append('\n'); + return buf.toString(); + } + + /** + * Test harness to make sure its assigning keys to the right buckets + * + */ + public static void main(String args[]) { + testRand2(); + testRand(); + + try { Thread.sleep(10000); } catch (InterruptedException ie) {} + } + + private static void testRand() { + StringBuffer buf = new StringBuffer(2048); + int low = 1; + int high = 3; + KBucketImpl bucket = new KBucketImpl(Hash.FAKE_HASH); + bucket.setRange(low, high); + Hash lowerBoundKey = bucket.getRangeBeginKey(); + Hash upperBoundKey = bucket.getRangeEndKey(); + for (int i = 0; i < 100; i++) { + Hash rnd = bucket.generateRandomKey(); + //buf.append(toString(rnd.getData())).append('\n'); + boolean ok = bucket.shouldContain(rnd); + if (!ok) { + byte diff[] = DataHelper.xor(rnd.getData(), bucket.getLocal().getData()); + BigInteger dv = new BigInteger(1, diff); + _log.error("WTF! bucket doesn't want: \n" + toString(rnd.getData()) + "\nDelta: \n" + toString(diff) + "\nDelta val: \n" + dv.toString(2) + "\nBucket: \n"+bucket, new Exception("WTF")); + try { Thread.sleep(1000); } catch (Exception e) {} + System.exit(0); + } else { + //_log.debug("Ok, bucket wants: \n" + toString(rnd.getData())); + } + //_log.info("Low/High:\n" + toString(lowBounds.toByteArray()) + "\n" + toString(highBounds.toByteArray())); + } + _log.info("Passed 100 random key generations against the null hash"); + } + + private static void testRand2() { + StringBuffer buf = new StringBuffer(1024*1024*16); + int low = 1; + int high = 200; + byte hash[] = new byte[Hash.HASH_LENGTH]; + RandomSource.getInstance().nextBytes(hash); + KBucketImpl bucket = new KBucketImpl(new Hash(hash)); + bucket.setRange(low, high); + Hash lowerBoundKey = bucket.getRangeBeginKey(); + Hash upperBoundKey = bucket.getRangeEndKey(); + for (int i = 0; i < 1000; i++) { + Hash rnd = bucket.generateRandomKey(); + buf.append(toString(rnd.getData())).append('\n'); + boolean ok = bucket.shouldContain(rnd); + if (!ok) { + byte diff[] = DataHelper.xor(rnd.getData(), bucket.getLocal().getData()); + BigInteger dv = new BigInteger(1, diff); + _log.error("WTF! bucket doesn't want: \n" + toString(rnd.getData()) + "\nDelta: \n" + toString(diff) + "\nDelta val: \n" + dv.toString(2) + "\nBucket: \n"+bucket, new Exception("WTF")); + try { Thread.sleep(1000); } catch (Exception e) {} + System.exit(0); + } else { + //_log.debug("Ok, bucket wants: \n" + toString(rnd.getData())); + } + } + _log.info("Passed 1000 random key generations against a random hash\n" + buf.toString()); + } + + private final static String toString(byte b[]) { + StringBuffer buf = new StringBuffer(b.length); + for (int i = 0; i < b.length; i++) { + buf.append(toString(b[i])); + buf.append(" "); + } + return buf.toString(); + } + + private final static String toString(byte b) { + StringBuffer buf = new StringBuffer(8); + for (int i = 7; i >= 0; i--) { + boolean bb = (0 != (b & (1<= 0) { + int oldSize = _buckets[bucket].getKeyCount(); + int numInBucket = _buckets[bucket].add(peer); + if (numInBucket > BUCKET_SIZE) { + // perhaps queue up coallesce job? naaahh.. lets let 'er grow for now + } + _log.debug("Peer " + peer + " added to bucket " + bucket); + return oldSize != numInBucket; + } else { + throw new IllegalArgumentException("Unable to pick a bucket. wtf!"); + } + } + + public int size() { + int size = 0; + for (int i = 0; i < _buckets.length; i++) + size += _buckets[i].getKeyCount(); + return size; + } + + public boolean remove(Hash entry) { + int bucket = pickBucket(entry); + KBucket kbucket = getBucket(bucket); + boolean removed = kbucket.remove(entry); + return removed; + } + + public Set getAll() { return getAll(new HashSet()); } + public Set getAll(Set toIgnore) { + HashSet all = new HashSet(1024); + for (int i = 0; i < _buckets.length; i++) { + all.addAll(_buckets[i].getEntries(toIgnore)); + } + return all; + } + + public int pickBucket(Hash key) { + for (int i = 0; i < NUM_BUCKETS; i++) { + if (_buckets[i].shouldContain(key)) + return i; + } + _log.error("Key does not fit in any bucket?! WTF!\nKey : [" + toString(key.getData()) + "]\nDelta: ["+ toString(DataHelper.xor(_us.getData(), key.getData())) + "]\nUs : [" + toString(_us.getData()) + "]", new Exception("WTF")); + displayBuckets(); + return -1; + } + + public KBucket getBucket(int bucket) { return _buckets[bucket]; } + + protected void createBuckets() { + _buckets = new KBucket[NUM_BUCKETS]; + for (int i = 0; i < NUM_BUCKETS-1; i++) { + _buckets[i] = createBucket(i*BASE, (i+1)*BASE); + } + _buckets[NUM_BUCKETS-1] = createBucket(BASE*(NUM_BUCKETS-1), BASE*(NUM_BUCKETS) + 1); + } + + protected KBucket createBucket(int start, int end) { + KBucket bucket = new KBucketImpl(_us); + bucket.setRange(start, end); + _log.debug("Creating a bucket from " + start + " to " + (end)); + return bucket; + } + + public void displayBuckets() { + _log.info(toString()); + } + + public String toString() { + BigInteger us = new BigInteger(1, _us.getData()); + StringBuffer buf = new StringBuffer(1024); + buf.append("Bucket set rooted on: ").append(us.toString()).append(" (aka ").append(us.toString(2)).append("): \n"); + for (int i = 0; i < NUM_BUCKETS; i++) { + buf.append("* Bucket ").append(i).append("/").append(NUM_BUCKETS-1).append(": )\n"); + buf.append("Start: ").append("2^").append(_buckets[i].getRangeBegin()).append(")\n"); + buf.append("End: ").append("2^").append(_buckets[i].getRangeEnd()).append(")\n"); + buf.append("Contents:").append(_buckets[i].toString()).append("\n"); + } + + return buf.toString(); + } + + + final static String toString(byte b[]) { + byte val[] = new byte[Hash.HASH_LENGTH]; + if (b.length < 32) + System.arraycopy(b, 0, val, Hash.HASH_LENGTH-b.length-1, b.length); + else + System.arraycopy(b, Hash.HASH_LENGTH-b.length, val, 0, val.length); + StringBuffer buf = new StringBuffer(KEYSIZE_BITS); + for (int i = 0; i < val.length; i++) { + for (int j = 7; j >= 0; j--) { + boolean bb = (0 != (val[i] & (1<Kademlia Network DB Contents\n"); + if (!_initialized) { + buf.append("Not initialized\n"); + return buf.toString(); + } + Set leases = getLeases(); + buf.append("

Leases

\n"); + buf.append("\n"); + for (Iterator iter = leases.iterator(); iter.hasNext(); ) { + LeaseSet ls = (LeaseSet)iter.next(); + Hash key = ls.getDestination().calculateHash(); + buf.append(""); + + if (getLastSent(key).longValue() > 0) + buf.append(""); + else + buf.append(""); + buf.append("\n"); + } + buf.append("
").append(key.toBase64()).append("Last sent successfully: ").append(new Date(getLastSent(key).longValue())).append("
Last sent successfully: never
\n").append(ls.toString()).append("
\n"); + + Hash us = Router.getInstance().getRouterInfo().getIdentity().getHash(); + Set routers = getRouters(); + buf.append("

Routers

\n"); + buf.append("\n"); + for (Iterator iter = routers.iterator(); iter.hasNext(); ) { + RouterInfo ri = (RouterInfo)iter.next(); + Hash key = ri.getIdentity().getHash(); + boolean isUs = key.equals(us); + if (isUs) { + buf.append(""); + buf.append(""); + } else { + buf.append(""); + if (getLastSent(key).longValue() > 0) + buf.append(""); + else + buf.append(""); + buf.append(""); + } + buf.append("\n"); + } + buf.append("
").append(key.toBase64()).append("Last sent successfully: ").append(new Date(getLastSent(key).longValue())).append("
").append(key.toBase64()).append("Last sent successfully: ").append(new Date(getLastSent(key).longValue())).append("Last sent successfully: neverProfile
\n").append(ri.toString()).append("
\n"); + + return buf.toString(); + } + +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java new file mode 100644 index 0000000000..5dee5ed045 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java @@ -0,0 +1,111 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.math.BigInteger; +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.Set; +import java.util.TreeMap; + +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.router.ProfileManager; +import net.i2p.router.Router; +import net.i2p.util.Log; + +class PeerSelector { + private final static Log _log = new Log(PeerSelector.class); + private static final PeerSelector _instance = new PeerSelector(); + public static final PeerSelector getInstance() { return _instance; } + + /** + * Search through the kbucket set to find the most reliable peers close to the + * given key, skipping all of the ones already checked + * + * @return ordered list of Hash objects + */ + public List selectMostReliablePeers(Hash key, int numClosest, Set alreadyChecked, KBucketSet kbuckets) { + // get the peers closest to the key + List nearest = selectNearestExplicit(key, numClosest, alreadyChecked, kbuckets); + return nearest; + } + + /** + * Ignore KBucket ordering and do the XOR explicitly per key. Runs in O(n*log(n)) + * time (n=routing table size with c ~ 32 xor ops). This gets strict ordering + * on closest + * + * @return List of Hash for the peers selected, ordered by bucket (but intra bucket order is not defined) + */ + public List selectNearestExplicit(Hash key, int maxNumRouters, Set peersToIgnore, KBucketSet kbuckets) { + if (peersToIgnore == null) + peersToIgnore = new HashSet(1); + peersToIgnore.add(Router.getInstance().getRouterInfo().getIdentity().getHash()); + Set allHashes = kbuckets.getAll(peersToIgnore); + removeFailingPeers(allHashes); + Map diffMap = new HashMap(allHashes.size()); + for (Iterator iter = allHashes.iterator(); iter.hasNext(); ) { + Hash cur = (Hash)iter.next(); + BigInteger diff = getDistance(key, cur); + diffMap.put(diff, cur); + } + // n*log(n) + Map sortedMap = new TreeMap(diffMap); + List peerHashes = new ArrayList(maxNumRouters); + for (Iterator iter = sortedMap.values().iterator(); iter.hasNext(); ) { + if (peerHashes.size() >= maxNumRouters) break; + peerHashes.add(iter.next()); + } + _log.debug("Searching for " + maxNumRouters + " peers close to " + key + ": " + peerHashes + " (not including " + peersToIgnore + ") [allHashes.size = " + allHashes.size() + "]"); + return peerHashes; + } + + /** + * strip out all of the peers that are failing + * + */ + private void removeFailingPeers(Set peerHashes) { + List failing = new ArrayList(16); + for (Iterator iter = peerHashes.iterator(); iter.hasNext(); ) { + Hash cur = (Hash)iter.next(); + if (ProfileManager.getInstance().isFailing(cur)) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer " + cur.toBase64() + " is failing, don't include them in the peer selection"); + failing.add(cur); + } + } + peerHashes.removeAll(failing); + } + + protected BigInteger getDistance(Hash targetKey, Hash routerInQuestion) { + // plain XOR of the key and router + byte diff[] = DataHelper.xor(routerInQuestion.getData(), targetKey.getData()); + return new BigInteger(1, diff); + } + + /** + * Generic KBucket filtering to find the hashes close to a key, regardless of other considerations. + * This goes through the kbuckets, starting with the key's location, moving towards us, and then away from the + * key's location's bucket, selecting peers until we have numClosest. + * + * @return List of Hash for the peers selected, ordered by bucket (but intra bucket order is not defined) + */ + public List selectNearest(Hash key, int maxNumRouters, Set peersToIgnore, KBucketSet kbuckets) { + // sure, this may not be exactly correct per kademlia (peers on the border of a kbucket in strict kademlia + // would behave differently) but I can see no reason to keep around an /additional/ more complicated algorithm. + // later if/when selectNearestExplicit gets costly, we may revisit this (since kbuckets let us cache the distance() + // into a simple bucket selection algo + random select rather than an n*log(n) op) + return selectNearestExplicit(key, maxNumRouters, peersToIgnore, kbuckets); + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java new file mode 100644 index 0000000000..ca65f75451 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java @@ -0,0 +1,323 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.FileNotFoundException; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataStructure; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.RouterInfo; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.Router; +import net.i2p.util.Log; + +/** + * Write out keys to disk when we get them and periodically read ones we don't know + * about into memory, with newly read routers are also added to the routing table. + * + */ +class PersistentDataStore extends TransientDataStore { + private final static Log _log = new Log(PersistentDataStore.class); + private String _dbDir; + private KademliaNetworkDatabaseFacade _facade; + + private final static int READ_DELAY = 60*1000; + + public PersistentDataStore(String dbDir, KademliaNetworkDatabaseFacade facade) { + super(); + _dbDir = dbDir; + _facade = facade; + JobQueue.getInstance().addJob(new ReadJob()); + } + + public DataStructure remove(Hash key) { + JobQueue.getInstance().addJob(new RemoveJob(key)); + return super.remove(key); + } + + public void put(Hash key, DataStructure data) { + if ( (data == null) || (key == null) ) return; + super.put(key, data); + JobQueue.getInstance().addJob(new WriteJob(key, data)); + } + + private void accept(LeaseSet ls) { + super.put(ls.getDestination().calculateHash(), ls); + } + private void accept(RouterInfo ri) { + Hash key = ri.getIdentity().getHash(); + super.put(key, ri); + // add recently loaded routers to the routing table + _facade.getKBuckets().add(key); + } + + private class RemoveJob extends JobImpl { + private Hash _key; + public RemoveJob(Hash key) { + _key = key; + } + public String getName() { return "Remove Key"; } + public void runJob() { + _log.info("Removing key " + _key, getAddedBy()); + try { + File dbDir = getDbDir(); + removeFile(_key, dbDir); + } catch (IOException ioe) { + _log.error("Error removing key " + _key, ioe); + } + } + } + + private class WriteJob extends JobImpl { + private Hash _key; + private DataStructure _data; + public WriteJob(Hash key, DataStructure data) { + super(); + _key = key; + _data = data; + } + public String getName() { return "DB Writer Job"; } + public void runJob() { + _log.info("Writing key " + _key); + FileOutputStream fos = null; + try { + String filename = null; + File dbDir = getDbDir(); + + if (_data instanceof LeaseSet) + filename = getLeaseSetName(_key); + else if (_data instanceof RouterInfo) + filename = getRouterInfoName(_key); + else + throw new IOException("We don't know how to write objects of type " + _data.getClass().getName()); + + fos = new FileOutputStream(new File(dbDir, filename)); + try { + _data.writeBytes(fos); + } catch (DataFormatException dfe) { + _log.error("Error writing out malformed object as " + _key + ": " + _data, dfe); + File f = new File(dbDir, filename); + f.delete(); + } + } catch (IOException ioe) { + _log.error("Error writing out the object", ioe); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + } + } + + private class ReadJob extends JobImpl { + public ReadJob() { + super(); + } + public String getName() { return "DB Read Job"; } + public void runJob() { + _log.info("Rereading new files"); + readFiles(); + requeue(READ_DELAY); + } + + private void readFiles() { + try { + File dbDir = getDbDir(); + File leaseSetFiles[] = dbDir.listFiles(LeaseSetFilter.getInstance()); + if (leaseSetFiles != null) { + for (int i = 0; i < leaseSetFiles.length; i++) { + Hash key = getLeaseSetHash(leaseSetFiles[i].getName()); + if ( (key != null) && (!isKnown(key)) ) + JobQueue.getInstance().addJob(new ReadLeaseJob(leaseSetFiles[i])); + } + } + File routerInfoFiles[] = dbDir.listFiles(RouterInfoFilter.getInstance()); + if (routerInfoFiles != null) { + for (int i = 0; i < routerInfoFiles.length; i++) { + Hash key = getRouterInfoHash(routerInfoFiles[i].getName()); + if ( (key != null) && (!isKnown(key)) ) + JobQueue.getInstance().addJob(new ReadRouterJob(routerInfoFiles[i])); + } + } + } catch (IOException ioe) { + _log.error("Error reading files in the db dir", ioe); + } + } + + } + + private class ReadLeaseJob extends JobImpl { + private File _leaseFile; + public ReadLeaseJob(File leaseFile) { + _leaseFile = leaseFile; + } + public String getName() { return "Read LeaseSet"; } + public void runJob() { + try { + FileInputStream fis = null; + boolean corrupt = false; + try { + fis = new FileInputStream(_leaseFile); + LeaseSet ls = new LeaseSet(); + ls.readBytes(fis); + if (ls.isCurrent(Router.CLOCK_FUDGE_FACTOR)) { + _log.info("Reading in new LeaseSet: " + ls.getDestination().calculateHash()); + accept(ls); + } else { + _log.warn("Expired LeaseSet found for " + ls.getDestination().calculateHash() + ": Deleting"); + corrupt = true; + } + } catch (DataFormatException dfe) { + _log.warn("Error reading the leaseSet from " + _leaseFile.getAbsolutePath(), dfe); + corrupt = true; + } catch (FileNotFoundException fnfe) { + _log.debug("Deleted prior to read.. a race during expiration / load"); + corrupt = false; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + if (corrupt) _leaseFile.delete(); + } catch (IOException ioe) { + _log.warn("Error reading the leaseSet from " + _leaseFile.getAbsolutePath(), ioe); + } + } + } + + private class ReadRouterJob extends JobImpl { + private File _routerFile; + public ReadRouterJob(File routerFile) { + _routerFile = routerFile; + } + public String getName() { return "Read RouterInfo"; } + public void runJob() { + try { + FileInputStream fis = null; + boolean corrupt = false; + try { + fis = new FileInputStream(_routerFile); + RouterInfo ri = new RouterInfo(); + ri.readBytes(fis); + if (ri.isValid()) { + _log.info("Reading in new RouterInfo: " + ri.getIdentity().getHash()); + accept(ri); + } else { + _log.warn("Invalid routerInfo found for " + ri.getIdentity().getHash() + ": " + ri); + corrupt = true; + } + } catch (DataFormatException dfe) { + _log.warn("Error reading the routerInfo from " + _routerFile.getAbsolutePath(), dfe); + corrupt = true; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + if (corrupt) _routerFile.delete(); + } catch (IOException ioe) { + _log.warn("Error reading the RouterInfo from " + _routerFile.getAbsolutePath(), ioe); + } + } + } + + + private File getDbDir() throws IOException { + File f = new File(_dbDir); + if (!f.exists()) { + boolean created = f.mkdirs(); + if (!created) + throw new IOException("Unable to create the DB directory [" + f.getAbsolutePath() + "]"); + } + if (!f.isDirectory()) + throw new IOException("DB directory [" + f.getAbsolutePath() + "] is not a directory!"); + if (!f.canRead()) + throw new IOException("DB directory [" + f.getAbsolutePath() + "] is not readable!"); + if (!f.canWrite()) + throw new IOException("DB directory [" + f.getAbsolutePath() + "] is not writable!"); + return f; + } + + private final static String LEASESET_PREFIX = "leaseSet-"; + private final static String LEASESET_SUFFIX = ".dat"; + private final static String ROUTERINFO_PREFIX = "routerInfo-"; + private final static String ROUTERINFO_SUFFIX = ".dat"; + + private String getLeaseSetName(Hash hash) { + return LEASESET_PREFIX + hash.toBase64() + LEASESET_SUFFIX; + } + private String getRouterInfoName(Hash hash) { + return ROUTERINFO_PREFIX + hash.toBase64() + ROUTERINFO_SUFFIX; + } + + private Hash getLeaseSetHash(String filename) { + return getHash(filename, LEASESET_PREFIX, LEASESET_SUFFIX); + } + + private Hash getRouterInfoHash(String filename) { + return getHash(filename, ROUTERINFO_PREFIX, ROUTERINFO_SUFFIX); + } + + private Hash getHash(String filename, String prefix, String suffix) { + try { + String key = filename.substring(prefix.length()); + key = key.substring(0, key.length() - suffix.length()); + Hash h = new Hash(); + h.fromBase64(key); + return h; + } catch (Exception e) { + _log.warn("Unable to fetch the key from [" + filename + "]", e); + return null; + } + } + + private void removeFile(Hash key, File dir) throws IOException { + String lsName = getLeaseSetName(key); + String riName = getRouterInfoName(key); + File f = new File(dir, lsName); + if (f.exists()) { + boolean removed = f.delete(); + if (!removed) + _log.warn("Unable to remove lease set at " + f.getAbsolutePath()); + else + _log.info("Removed lease set at " + f.getAbsolutePath()); + return; + } + f = new File(dir, riName); + if (f.exists()) { + boolean removed = f.delete(); + if (!removed) + _log.warn("Unable to remove router info at " + f.getAbsolutePath()); + else + _log.info("Removed router info at " + f.getAbsolutePath()); + return; + } + } + + private final static class LeaseSetFilter implements FilenameFilter { + private static final FilenameFilter _instance = new LeaseSetFilter(); + public static final FilenameFilter getInstance() { return _instance; } + public boolean accept(File dir, String name) { + if (name == null) return false; + name = name.toUpperCase(); + return (name.startsWith(LEASESET_PREFIX.toUpperCase()) && name.endsWith(LEASESET_SUFFIX.toUpperCase())); + } + } + private final static class RouterInfoFilter implements FilenameFilter { + private static final FilenameFilter _instance = new RouterInfoFilter(); + public static final FilenameFilter getInstance() { return _instance; } + public boolean accept(File dir, String name) { + if (name == null) return false; + name = name.toUpperCase(); + return (name.startsWith(ROUTERINFO_PREFIX.toUpperCase()) && name.endsWith(ROUTERINFO_SUFFIX.toUpperCase())); + } + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java new file mode 100644 index 0000000000..eef98ae625 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java @@ -0,0 +1,80 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; + +import net.i2p.router.ClientManagerFacade; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.Router; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Set; +import java.util.HashSet; + +/** + * Run periodically for each locally created leaseSet to cause it to be republished + * if the client is still connected. + * + */ +public class RepublishLeaseSetJob extends JobImpl { + private final static Log _log = new Log(RepublishLeaseSetJob.class); + private final static long REPUBLISH_LEASESET_DELAY = 60*1000; // 5 mins + private Hash _dest; + private KademliaNetworkDatabaseFacade _facade; + /** + * maintain a set of dest hashes that we're already publishing, + * so we don't go overboard. This is clunky, so if it gets any more + * complicated this will go into a 'manager' function rather than part of + * a job. + */ + private final static Set _pending = new HashSet(16); + + public static boolean alreadyRepublishing(Hash dest) { + synchronized (_pending) { + return _pending.contains(dest); + } + } + + public RepublishLeaseSetJob(KademliaNetworkDatabaseFacade facade, Hash destHash) { + super(); + _facade = facade; + _dest = destHash; + synchronized (_pending) { + _pending.add(destHash); + } + getTiming().setStartAfter(Clock.getInstance().now()+REPUBLISH_LEASESET_DELAY); + } + public String getName() { return "Republish a local leaseSet"; } + public void runJob() { + if (ClientManagerFacade.getInstance().isLocal(_dest)) { + LeaseSet ls = _facade.lookupLeaseSetLocally(_dest); + if (ls != null) { + _log.warn("Client " + _dest + " is local, so we're republishing it"); + if (!ls.isCurrent(Router.CLOCK_FUDGE_FACTOR)) { + _log.warn("Not publishing a LOCAL lease that isn't current - " + _dest, new Exception("Publish expired LOCAL lease?")); + } else { + JobQueue.getInstance().addJob(new StoreJob(_facade, _dest, ls, null, null, REPUBLISH_LEASESET_DELAY)); + } + } else { + _log.warn("Client " + _dest + " is local, but we can't find a valid LeaseSet? perhaps its being rebuilt?"); + } + requeue(REPUBLISH_LEASESET_DELAY); + } else { + _log.info("Client " + _dest + " is no longer local, so no more republishing their leaseSet"); + synchronized (_pending) { + _pending.remove(_dest); + } + } + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/RouterGenerator.java b/router/java/src/net/i2p/router/networkdb/kademlia/RouterGenerator.java new file mode 100644 index 0000000000..b50168a3db --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/RouterGenerator.java @@ -0,0 +1,136 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.File; +import java.io.FileOutputStream; +import java.math.BigInteger; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import net.i2p.crypto.KeyGenerator; +import net.i2p.data.Certificate; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.data.RouterAddress; +import net.i2p.data.RouterIdentity; +import net.i2p.data.RouterInfo; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.util.Clock; + +public class RouterGenerator { + public static void main(String args[]) { + RouterGenerator gen = new RouterGenerator(); + switch (args.length) { + case 0: + gen.createRouters(10000, "dummyRouters"); + break; + case 1: + gen.createRouters(10000, args[0]); + break; + case 2: + try { gen.createRouters(Integer.parseInt(args[1]), args[0]); } catch (NumberFormatException nfe) { nfe.printStackTrace(); } + break; + } + } + + private void createRouters(int numRouters, String outDir) { + File dir = new File(outDir); + if (!dir.exists()) + dir.mkdirs(); + int numSuccess = 0; + for (int i = 1; numSuccess < numRouters; i++) { + RouterInfo ri = createRouterInfo(i); + String hash = ri.getIdentity().getHash().toBase64(); + if (!hash.startsWith("fwI")) { + System.out.print("."); + if ( (i % 100) == 0) System.out.println(); + continue; + } + + System.out.println("Router " + i + " created: \t" + hash); + numSuccess++; + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(new File(dir, "routerInfo-" + hash + ".dat")); + ri.writeBytes(fos); + } catch (Exception e) { + System.err.println("Error writing router - " + e.getMessage()); + e.printStackTrace(); + return; + } finally { + if (fos != null) try { fos.close(); } catch (Exception e) {} + } + } + } + + private static PublicKey pubkey = null; + private static PrivateKey privkey = null; + private static SigningPublicKey signingPubKey = null; + private static SigningPrivateKey signingPrivKey = null; + private static Object keypair[] = KeyGenerator.getInstance().generatePKIKeypair(); + private static Object signingKeypair[] = KeyGenerator.getInstance().generateSigningKeypair(); + + static { + pubkey = (PublicKey)keypair[0]; + privkey = (PrivateKey)keypair[1]; + signingPubKey = (SigningPublicKey)signingKeypair[0]; + signingPrivKey = (SigningPrivateKey)signingKeypair[1]; + } + + + static RouterInfo createRouterInfo(int num) { + RouterInfo info = new RouterInfo(); + try { + info.setAddresses(createAddresses(num)); + info.setOptions(new Properties()); + info.setPeers(new HashSet()); + info.setPublished(Clock.getInstance().now()); + RouterIdentity ident = new RouterIdentity(); + BigInteger bv = new BigInteger(""+num); + Certificate cert = new Certificate(Certificate.CERTIFICATE_TYPE_NULL, bv.toByteArray()); + ident.setCertificate(cert); + ident.setPublicKey(pubkey); + ident.setSigningPublicKey(signingPubKey); + info.setIdentity(ident); + + info.sign(signingPrivKey); + } catch (Exception e) { + System.err.println("Error building router " + num + ": " + e.getMessage()); + e.printStackTrace(); + } + return info; + } + + static Set createAddresses(int num) { + Set addresses = new HashSet(); + RouterAddress addr = createTCPAddress(num); + if (addr != null) + addresses.add(addr); + return addresses; + } + + private static RouterAddress createTCPAddress(int num) { + RouterAddress addr = new RouterAddress(); + addr.setCost(10); + addr.setExpiration(null); + Properties props = new Properties(); + String name = "blah.random.host.org"; + String port = "" + (1024+num); + props.setProperty("host", name); + props.setProperty("port", port); + addr.setOptions(props); + addr.setTransportStyle("TCP"); + return addr; + } + +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java new file mode 100644 index 0000000000..55cffcaed8 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java @@ -0,0 +1,469 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import net.i2p.data.DataStructure; +import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; +import net.i2p.data.RoutingKeyGenerator; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DatabaseLookupMessage; +import net.i2p.data.i2np.DatabaseSearchReplyMessage; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.ProfileManager; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.TunnelSelectionCriteria; +import net.i2p.router.message.SendTunnelMessageJob; +import net.i2p.router.message.SendMessageDirectJob; +import net.i2p.stat.StatManager; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * Search for a particular key iteratively until we either find a value or we + * run out of peers + * + */ +class SearchJob extends JobImpl { + private final Log _log = new Log(SearchJob.class); + private KademliaNetworkDatabaseFacade _facade; + private SearchState _state; + private Job _onSuccess; + private Job _onFailure; + private long _expiration; + private long _timeoutMs; + private boolean _keepStats; + private boolean _isLease; + private Job _pendingRequeueJob; + + public final static int SEARCH_BREDTH = 3; // 3 peers at a time + public final static int SEARCH_PRIORITY = 400; // large because the search is probably for a real search + + private static final long PER_PEER_TIMEOUT = 30*1000; + + static { + StatManager.getInstance().createRateStat("netDb.successTime", "How long a successful search takes", "Network Database", new long[] { 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("netDb.failedTime", "How long a failed search takes", "Network Database", new long[] { 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("netDb.successPeers", "How many peers are contacted in a successful search", "Network Database", new long[] { 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("netDb.failedPeers", "How many peers are contacted in a failed search", "Network Database", new long[] { 60*60*1000l, 24*60*60*1000l }); + } + + /** + * Create a new search for the routingKey specified + * + */ + public SearchJob(KademliaNetworkDatabaseFacade facade, Hash key, Job onSuccess, Job onFailure, long timeoutMs, boolean keepStats, boolean isLease) { + if ( (key == null) || (key.getData() == null) ) throw new IllegalArgumentException("Search for null key? wtf"); + _facade = facade; + _state = new SearchState(key); + _onSuccess = onSuccess; + _onFailure = onFailure; + _timeoutMs = timeoutMs; + _keepStats = keepStats; + _isLease = isLease; + _expiration = Clock.getInstance().now() + timeoutMs; + } + + public void runJob() { + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Searching for " + _state.getTarget()); // , getAddedBy()); + searchNext(); + } + + protected SearchState getState() { return _state; } + protected KademliaNetworkDatabaseFacade getFacade() { return _facade; } + protected long getExpiration() { return _expiration; } + protected long getTimeoutMs() { return _timeoutMs; } + + /** + * Send the next search, or stop if its completed + */ + protected void searchNext() { + if (_state.completed()) { + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Already completed"); + return; + } + _log.info(getJobId() + ": Searching: " + _state); + if (isLocal()) { + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Key found locally"); + _state.complete(true); + succeed(); + } else if (isExpired()) { + if (_log.shouldLog(Log.WARN)) + _log.warn(getJobId() + ": Key search expired"); + _state.complete(true); + fail(); + } else { + //_log.debug("Continuing search"); + continueSearch(); + } + } + + /** + * True if the data is already locally stored + * + */ + private boolean isLocal() { return _facade.getDataStore().isKnown(_state.getTarget()); } + + private boolean isExpired() { + return Clock.getInstance().now() >= _expiration; + } + + /** + * Send a series of searches to the next available peers as selected by + * the routing table, but making sure no more than SEARCH_BREDTH are outstanding + * at any time + * + */ + protected void continueSearch() { + if (_state.completed()) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug(getJobId() + ": Search already completed", new Exception("already completed")); + return; + } + int toCheck = SEARCH_BREDTH - _state.getPending().size(); + if (toCheck <= 0) { + // too many already pending + if (_log.shouldLog(Log.WARN)) + _log.warn(getJobId() + ": Too many searches already pending (pending: " + _state.getPending().size() + " max: " + SEARCH_BREDTH + ")", new Exception("too many pending")); + requeuePending(); + return; + } + List closestHashes = getClosestRouters(_state.getTarget(), toCheck, _state.getAttempted()); + if ( (closestHashes == null) || (closestHashes.size() <= 0) ) { + if (_state.getPending().size() <= 0) { + // we tried to find some peers, but there weren't any and no one else is going to answer + if (_log.shouldLog(Log.WARN)) + _log.warn(getJobId() + ": No peers left, and none pending! Already searched: " + _state.getAttempted().size() + " failed: " + _state.getFailed().size(), new Exception("none left")); + fail(); + } else { + // no more to try, but we might get data or close peers from some outstanding requests + if (_log.shouldLog(Log.WARN)) + _log.warn(getJobId() + ": No peers left, but some are pending! Pending: " + _state.getPending().size() + " attempted: " + _state.getAttempted().size() + " failed: " + _state.getFailed().size(), new Exception("none left, but pending")); + requeuePending(); + return; + } + } else { + _state.addPending(closestHashes); + for (Iterator iter = closestHashes.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + DataStructure ds = _facade.getDataStore().get(peer); + if ( (ds == null) || !(ds instanceof RouterInfo) ) { + if (_log.shouldLog(Log.WARN)) + _log.warn(getJobId() + ": Error selecting closest hash that wasnt a router! " + peer + " : " + ds); + } else { + sendSearch((RouterInfo)ds); + } + } + } + } + + private void requeuePending() { + if (_pendingRequeueJob == null) + _pendingRequeueJob = new JobImpl() { + public String getName() { return "Requeue search with pending"; } + public void runJob() { searchNext(); } + }; + long now = Clock.getInstance().now(); + if (_pendingRequeueJob.getTiming().getStartAfter() < now) + _pendingRequeueJob.getTiming().setStartAfter(now+5*1000); + JobQueue.getInstance().addJob(_pendingRequeueJob); + } + + /** + * Set of Hash structures for routers we want to check next. This is the 'interesting' part of + * the algorithm. But to keep you on your toes, we've refactored it to the PeerSelector.selectNearestExplicit + * + * @return ordered list of Hash objects + */ + private List getClosestRouters(Hash key, int numClosest, Set alreadyChecked) { + Hash rkey = RoutingKeyGenerator.getInstance().getRoutingKey(key); + if (_log.shouldLog(Log.DEBUG)) + _log.debug(getJobId() + ": Current routing key for " + key + ": " + rkey); + return PeerSelector.getInstance().selectNearestExplicit(rkey, numClosest, alreadyChecked, _facade.getKBuckets()); + } + + /** + * Send a search to the given peer + * + */ + protected void sendSearch(RouterInfo router) { + if (router.getIdentity().equals(Router.getInstance().getRouterInfo().getIdentity())) { + // don't search ourselves + if (_log.shouldLog(Log.ERROR)) + _log.error(getJobId() + ": Dont send search to ourselves - why did we try?"); + return; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug(getJobId() + ": Send search to " + router); + } + + if (_isLease || false) // moo + sendLeaseSearch(router); + else + sendRouterSearch(router); + } + + + /** + * we're (probably) searching for a LeaseSet, so to be (overly) cautious, we're sending + * the request out through a tunnel w/ reply back through another tunnel. + * + */ + protected void sendLeaseSearch(RouterInfo router) { + TunnelId inTunnelId = getInboundTunnelId(); + if (inTunnelId == null) { + _log.error("No tunnels to get search replies through! wtf!"); + JobQueue.getInstance().addJob(new FailedJob(router)); + return; + } + + TunnelInfo inTunnel = TunnelManagerFacade.getInstance().getTunnelInfo(inTunnelId); + RouterInfo inGateway = NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(inTunnel.getThisHop()); + if (inGateway == null) { + _log.error("We can't find the gateway to our inbound tunnel?! wtf"); + JobQueue.getInstance().addJob(new FailedJob(router)); + return; + } + + long expiration = Clock.getInstance().now() + PER_PEER_TIMEOUT; // getTimeoutMs(); + + DatabaseLookupMessage msg = buildMessage(inTunnelId, inGateway, expiration); + + TunnelId outTunnelId = getOutboundTunnelId(); + if (outTunnelId == null) { + _log.error("No tunnels to send search out through! wtf!"); + JobQueue.getInstance().addJob(new FailedJob(router)); + return; + } + + if (_log.shouldLog(Log.DEBUG)) + _log.debug(getJobId() + ": Sending leaseSet search to " + router.getIdentity().getHash().toBase64() + " for " + msg.getSearchKey().toBase64() + " w/ replies through [" + msg.getFrom().getIdentity().getHash().toBase64() + "] via tunnel [" + msg.getReplyTunnel() + "]"); + + SearchMessageSelector sel = new SearchMessageSelector(router, _expiration, _state); + long timeoutMs = PER_PEER_TIMEOUT; // getTimeoutMs(); + SearchUpdateReplyFoundJob reply = new SearchUpdateReplyFoundJob(router, _state, _facade, this); + SendTunnelMessageJob j = new SendTunnelMessageJob(msg, outTunnelId, router.getIdentity().getHash(), null, null, reply, new FailedJob(router), sel, timeoutMs, SEARCH_PRIORITY); + JobQueue.getInstance().addJob(j); + } + + /** we're searching for a router, so we can just send direct */ + protected void sendRouterSearch(RouterInfo router) { + long expiration = Clock.getInstance().now() + PER_PEER_TIMEOUT; // getTimeoutMs(); + + DatabaseLookupMessage msg = buildMessage(expiration); + + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Sending router search to " + router.getIdentity().getHash().toBase64() + " for " + msg.getSearchKey().toBase64() + " w/ replies to us [" + msg.getFrom().getIdentity().getHash().toBase64() + "]"); + SearchMessageSelector sel = new SearchMessageSelector(router, _expiration, _state); + long timeoutMs = PER_PEER_TIMEOUT; + SearchUpdateReplyFoundJob reply = new SearchUpdateReplyFoundJob(router, _state, _facade, this); + SendMessageDirectJob j = new SendMessageDirectJob(msg, router.getIdentity().getHash(), reply, new FailedJob(router), sel, expiration, SEARCH_PRIORITY); + JobQueue.getInstance().addJob(j); + } + + /** + * what tunnel will we send the search out through? + * + * @return tunnel id (or null if none are found) + */ + private TunnelId getOutboundTunnelId() { + TunnelSelectionCriteria crit = new TunnelSelectionCriteria(); + crit.setMaximumTunnelsRequired(1); + crit.setMinimumTunnelsRequired(1); + List tunnelIds = TunnelManagerFacade.getInstance().selectOutboundTunnelIds(crit); + if (tunnelIds.size() <= 0) { + return null; + } + + return (TunnelId)tunnelIds.get(0); + } + + /** + * what tunnel will we get replies through? + * + * @return tunnel id (or null if none are found) + */ + private TunnelId getInboundTunnelId() { + TunnelSelectionCriteria crit = new TunnelSelectionCriteria(); + crit.setMaximumTunnelsRequired(1); + crit.setMinimumTunnelsRequired(1); + List tunnelIds = TunnelManagerFacade.getInstance().selectInboundTunnelIds(crit); + if (tunnelIds.size() <= 0) { + return null; + } + return (TunnelId)tunnelIds.get(0); + } + + /** + * Build the database search message + * + * @param replyTunnelId tunnel to receive replies through + * @param replyGateway gateway for the reply tunnel + * @param expiration when the search should stop + */ + protected DatabaseLookupMessage buildMessage(TunnelId replyTunnelId, RouterInfo replyGateway, long expiration) { + DatabaseLookupMessage msg = new DatabaseLookupMessage(); + msg.setSearchKey(_state.getTarget()); + msg.setFrom(replyGateway); + msg.setDontIncludePeers(_state.getAttempted()); + msg.setMessageExpiration(new Date(expiration)); + msg.setReplyTunnel(replyTunnelId); + return msg; + } + + /** + * We're looking for a router, so lets build the lookup message (no need to tunnel route either, so just have + * replies sent back to us directly) + * + */ + protected DatabaseLookupMessage buildMessage(long expiration) { + DatabaseLookupMessage msg = new DatabaseLookupMessage(); + msg.setSearchKey(_state.getTarget()); + msg.setFrom(Router.getInstance().getRouterInfo()); + msg.setDontIncludePeers(_state.getAttempted()); + msg.setMessageExpiration(new Date(expiration)); + msg.setReplyTunnel(null); + return msg; + } + + void replyFound(DatabaseSearchReplyMessage message, Hash peer) { + long duration = _state.replyFound(peer); + // this processing can take a while, so split 'er up + JobQueue.getInstance().addJob(new SearchReplyJob((DatabaseSearchReplyMessage)message, peer, duration)); + } + + private final class SearchReplyJob extends JobImpl { + private DatabaseSearchReplyMessage _msg; + private Hash _peer; + private int _curIndex; + private int _invalidPeers; + private int _seenPeers; + private int _newPeers; + private int _duplicatePeers; + private long _duration; + public SearchReplyJob(DatabaseSearchReplyMessage message, Hash peer, long duration) { + _msg = message; + _peer = peer; + _curIndex = 0; + _invalidPeers = 0; + _seenPeers = 0; + _newPeers = 0; + _duplicatePeers = 0; + } + public String getName() { return "Process Reply for Kademlia Search"; } + public void runJob() { + if (_curIndex >= _msg.getNumReplies()) { + ProfileManager.getInstance().dbLookupReply(_peer, _newPeers, _seenPeers, _invalidPeers, _duplicatePeers, _duration); + } else { + RouterInfo ri = _msg.getReply(_curIndex); + if (ri.isValid()) { + if (_state.wasAttempted(ri.getIdentity().getHash())) { + _duplicatePeers++; + } + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": dbSearchReply received on search containing router " + ri.getIdentity().getHash() + " with publishDate of " + new Date(ri.getPublished())); + _facade.store(ri.getIdentity().getHash(), ri); + if (_facade.getKBuckets().add(ri.getIdentity().getHash())) + _newPeers++; + else + _seenPeers++; + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error(getJobId() + ": Received an invalid peer from " + _peer + ": " + ri, new Exception("Invalid peer")); + _invalidPeers++; + } + _curIndex++; + requeue(0); + } + } + } + + /** + * Called when a particular peer failed to respond before the timeout was + * reached, or if the peer could not be contacted at all. + * + */ + protected class FailedJob extends JobImpl { + private Hash _peer; + private boolean _penalizePeer; + public FailedJob(RouterInfo peer) { + this(peer, true); + } + /** + * Allow the choice as to whether failed searches should count against + * the peer (such as if we search for a random key) + * + */ + public FailedJob(RouterInfo peer, boolean penalizePeer) { + super(); + _penalizePeer = penalizePeer; + _peer = peer.getIdentity().getHash(); + } + public void runJob() { + _state.replyTimeout(_peer); + if (_penalizePeer) { + _log.warn("Penalizing peer for timeout on search: " + _peer.toBase64()); + ProfileManager.getInstance().dbLookupFailed(_peer); + } else { + _log.error("NOT (!!) Penalizing peer for timeout on search: " + _peer.toBase64()); + } + searchNext(); + } + public String getName() { return "Kademlia Search Failed"; } + } + + /** + * Search was totally successful + */ + protected void succeed() { + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Succeeded search for key " + _state.getTarget()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug(getJobId() + ": State of successful search: " + _state); + + if (_keepStats) { + long time = Clock.getInstance().now() - _state.getWhenStarted(); + StatManager.getInstance().addRateData("netDb.successTime", time, 0); + StatManager.getInstance().addRateData("netDb.successPeers", _state.getAttempted().size(), time); + } + if (_onSuccess != null) + JobQueue.getInstance().addJob(_onSuccess); + } + + /** + * Search totally failed + */ + protected void fail() { + _log.info(getJobId() + ": Failed search for key " + _state.getTarget()); + _log.debug(getJobId() + ": State of failed search: " + _state); + + if (_keepStats) { + long time = Clock.getInstance().now() - _state.getWhenStarted(); + StatManager.getInstance().addRateData("netDb.failedTime", time, 0); + StatManager.getInstance().addRateData("netDb.failedPeers", _state.getAttempted().size(), time); + } + if (_onFailure != null) + JobQueue.getInstance().addJob(_onFailure); + } + + public String getName() { return "Kademlia NetDb Search"; } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java new file mode 100644 index 0000000000..a2d8ccfa2d --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java @@ -0,0 +1,86 @@ +package net.i2p.router.networkdb.kademlia; + +import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; +import net.i2p.data.i2np.DatabaseSearchReplyMessage; +import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.router.MessageSelector; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * Check to see the message is a reply from the peer regarding the current + * search + * + */ +class SearchMessageSelector implements MessageSelector { + private final static Log _log = new Log(SearchMessageSelector.class); + private static int __searchSelectorId = 0; + private Hash _peer; + private boolean _found; + private int _id; + private long _exp; + private SearchState _state; + + public SearchMessageSelector(RouterInfo peer, long expiration, SearchState state) { + _peer = peer.getIdentity().getHash(); + _found = false; + _exp = expiration; + _state = state; + _id = ++__searchSelectorId; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("[" + _id + "] Created: " + toString()); + } + + public String toString() { return "Search selector [" + _id + "] looking for a reply from " + _peer + " with regards to " + _state.getTarget(); } + + public boolean continueMatching() { + if (_found) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("[" + _id + "] Dont continue matching! looking for a reply from " + _peer + " with regards to " + _state.getTarget()); + return false; + } + long now = Clock.getInstance().now(); + return now < _exp; + } + public long getExpiration() { return _exp; } + public boolean isMatch(I2NPMessage message) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("[" + _id + "] isMatch("+message.getClass().getName() + ") [want dbStore or dbSearchReply from " + _peer + " for " + _state.getTarget() + "]"); + if (message instanceof DatabaseStoreMessage) { + DatabaseStoreMessage msg = (DatabaseStoreMessage)message; + if (msg.getKey().equals(_state.getTarget())) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("[" + _id + "] Was a DBStore of the key we're looking for. May not have been from who we're checking against though, but DBStore doesn't include that info"); + _found = true; + return true; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("[" + _id + "] DBStore of a key we're not looking for"); + return false; + } + } else if (message instanceof DatabaseSearchReplyMessage) { + DatabaseSearchReplyMessage msg = (DatabaseSearchReplyMessage)message; + if (_peer.equals(msg.getFromHash())) { + if (msg.getSearchKey().equals(_state.getTarget())) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("[" + _id + "] Was a DBSearchReply from who we're checking with for a key we're looking for"); + _found = true; + return true; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("[" + _id + "] Was a DBSearchReply from who we're checking with but NOT for the key we're looking for"); + return false; + } + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("[" + _id + "] DBSearchReply from someone we are not checking with [" + msg.getFromHash() + ", not " + _state.getTarget() + "]"); + return false; + } + } else { + //_log.debug("Not a DbStore or DbSearchReply"); + return false; + } + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java new file mode 100644 index 0000000000..806f3ffd49 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java @@ -0,0 +1,159 @@ +package net.i2p.router.networkdb.kademlia; + +import net.i2p.data.Hash; +import net.i2p.util.Clock; + +import java.util.Set; +import java.util.HashSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Collection; +import java.util.Date; + +/** + * Data related to a particular search + * + */ +class SearchState { + private HashSet _pendingPeers; + private HashMap _pendingPeerTimes; + private HashSet _attemptedPeers; + private HashSet _failedPeers; + private HashSet _successfulPeers; + private Hash _searchKey; + private volatile long _completed; + private volatile long _started; + + public SearchState(Hash key) { + _searchKey = key; + _pendingPeers = new HashSet(16); + _attemptedPeers = new HashSet(16); + _failedPeers = new HashSet(16); + _successfulPeers = new HashSet(16); + _pendingPeerTimes = new HashMap(16); + _completed = -1; + _started = Clock.getInstance().now(); + } + + public Hash getTarget() { return _searchKey; } + public Set getPending() { + synchronized (_pendingPeers) { + return (Set)_pendingPeers.clone(); + } + } + public Set getAttempted() { + synchronized (_attemptedPeers) { + return (Set)_attemptedPeers.clone(); + } + } + public boolean wasAttempted(Hash peer) { + synchronized (_attemptedPeers) { + return _attemptedPeers.contains(peer); + } + } + public Set getSuccessful() { + synchronized (_successfulPeers) { + return (Set)_successfulPeers.clone(); + } + } + public Set getFailed() { + synchronized (_failedPeers) { + return (Set)_failedPeers.clone(); + } + } + public boolean completed() { return _completed != -1; } + public void complete(boolean completed) { + if (completed) + _completed = Clock.getInstance().now(); + } + + public long getWhenStarted() { return _started; } + public long getWhenCompleted() { return _completed; } + + public void addPending(Collection pending) { + synchronized (_pendingPeers) { + _pendingPeers.addAll(pending); + for (Iterator iter = pending.iterator(); iter.hasNext(); ) + _pendingPeerTimes.put(iter.next(), new Long(Clock.getInstance().now())); + } + synchronized (_attemptedPeers) { + _attemptedPeers.addAll(pending); + } + } + + /** how long did it take to get the reply, or -1 if we don't know */ + public long dataFound(Hash peer) { + long rv = -1; + synchronized (_pendingPeers) { + _pendingPeers.remove(peer); + Long when = (Long)_pendingPeerTimes.remove(peer); + if (when != null) + rv = Clock.getInstance().now() - when.longValue(); + } + synchronized (_successfulPeers) { + _successfulPeers.add(peer); + } + return rv; + } + + /** how long did it take to get the reply, or -1 if we dont know */ + public long replyFound(Hash peer) { + synchronized (_pendingPeers) { + _pendingPeers.remove(peer); + Long when = (Long)_pendingPeerTimes.remove(peer); + if (when != null) + return Clock.getInstance().now() - when.longValue(); + else + return -1; + } + } + + public void replyTimeout(Hash peer) { + synchronized (_pendingPeers) { + _pendingPeers.remove(peer); + _pendingPeerTimes.remove(peer); + } + synchronized (_failedPeers) { + _failedPeers.add(peer); + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(256); + buf.append("Searching for ").append(_searchKey); + buf.append(" "); + if (_completed <= 0) + buf.append(" completed? false "); + else + buf.append(" completed on ").append(new Date(_completed)); + buf.append(" Attempted: "); + synchronized (_attemptedPeers) { + for (Iterator iter = _attemptedPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append(peer.toBase64()).append(" "); + } + } + buf.append(" Pending: "); + synchronized (_pendingPeers) { + for (Iterator iter = _pendingPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append(peer.toBase64()).append(" "); + } + } + buf.append(" Failed: "); + synchronized (_failedPeers) { + for (Iterator iter = _failedPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append(peer.toBase64()).append(" "); + } + } + buf.append(" Successful: "); + synchronized (_successfulPeers) { + for (Iterator iter = _successfulPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append(peer.toBase64()).append(" "); + } + } + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java new file mode 100644 index 0000000000..a6fea75334 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java @@ -0,0 +1,68 @@ +package net.i2p.router.networkdb.kademlia; + +import java.util.Date; + +import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; +import net.i2p.data.i2np.DatabaseSearchReplyMessage; +import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.router.JobImpl; +import net.i2p.router.ProfileManager; +import net.i2p.router.ReplyJob; +import net.i2p.util.Log; + +/** + * Called after a match to a db search is found + * + */ +class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob { + private final static Log _log = new Log(SearchUpdateReplyFoundJob.class); + private I2NPMessage _message; + private Hash _peer; + private SearchState _state; + private KademliaNetworkDatabaseFacade _facade; + private SearchJob _job; + + public SearchUpdateReplyFoundJob(RouterInfo peer, SearchState state, KademliaNetworkDatabaseFacade facade, SearchJob job) { + super(); + _peer = peer.getIdentity().getHash(); + _state = state; + _facade = facade; + _job = job; + } + + public String getName() { return "Update Reply Found for Kademlia Search"; } + public void runJob() { + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Reply from " + _peer + " with message " + _message.getClass().getName()); + + if (_message instanceof DatabaseStoreMessage) { + long timeToReply = _state.dataFound(_peer); + + DatabaseStoreMessage msg = (DatabaseStoreMessage)_message; + if (msg.getValueType() == DatabaseStoreMessage.KEY_TYPE_LEASESET) { + _facade.store(msg.getKey(), msg.getLeaseSet()); + } else if (msg.getValueType() == DatabaseStoreMessage.KEY_TYPE_ROUTERINFO) { + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": dbStore received on search containing router " + msg.getKey() + " with publishDate of " + new Date(msg.getRouterInfo().getPublished())); + _facade.store(msg.getKey(), msg.getRouterInfo()); + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error(getJobId() + ": Unknown db store type?!@ " + msg.getValueType()); + } + + ProfileManager.getInstance().dbLookupSuccessful(_peer, timeToReply); + } else if (_message instanceof DatabaseSearchReplyMessage) { + _job.replyFound((DatabaseSearchReplyMessage)_message, _peer); + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error(getJobId() + ": WTF, reply job matched a strange message: " + _message); + return; + } + + _job.searchNext(); + } + + public void setMessage(I2NPMessage message) { _message = message; } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java new file mode 100644 index 0000000000..8b603f3019 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java @@ -0,0 +1,65 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.util.Log; + +/** + * Fire off search jobs for random keys from the explore pool, up to MAX_PER_RUN + * at a time. + * + */ +class StartExplorersJob extends JobImpl { + private final static Log _log = new Log(StartExplorersJob.class); + private KademliaNetworkDatabaseFacade _facade; + + private final static long RERUN_DELAY_MS = 3*60*1000; // every 3 minutes, explore MAX_PER_RUN keys + private final static int MAX_PER_RUN = 3; // don't explore more than 1 bucket at a time + + public StartExplorersJob(KademliaNetworkDatabaseFacade facade) { + super(); + _facade = facade; + } + + public String getName() { return "Start Explorers Job"; } + public void runJob() { + Set toExplore = selectKeysToExplore(); + _log.debug("Keys to explore during this run: " + toExplore); + _facade.removeFromExploreKeys(toExplore); + for (Iterator iter = toExplore.iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + //_log.info("Starting explorer for " + key, new Exception("Exploring!")); + JobQueue.getInstance().addJob(new ExploreJob(_facade, key)); + } + requeue(RERUN_DELAY_MS); + } + + /** + * Run through the explore pool and pick out some values + * + */ + private Set selectKeysToExplore() { + Set queued = _facade.getExploreKeys(); + if (queued.size() <= MAX_PER_RUN) + return queued; + Set rv = new HashSet(MAX_PER_RUN); + for (Iterator iter = queued.iterator(); iter.hasNext(); ) { + if (rv.size() >= MAX_PER_RUN) break; + rv.add(iter.next()); + } + return rv; + } +} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java new file mode 100644 index 0000000000..770e8e9d48 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java @@ -0,0 +1,678 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import net.i2p.crypto.SessionKeyManager; +import net.i2p.data.Certificate; +import net.i2p.data.DataStructure; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.PublicKey; +import net.i2p.data.RouterInfo; +import net.i2p.data.RoutingKeyGenerator; +import net.i2p.data.SessionKey; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.data.i2np.DeliveryStatusMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageSelector; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.ProfileManager; +import net.i2p.router.ReplyJob; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.TunnelSelectionCriteria; +import net.i2p.router.message.GarlicConfig; +import net.i2p.router.message.PayloadGarlicConfig; +import net.i2p.router.message.SendGarlicJob; +import net.i2p.router.message.SendTunnelMessageJob; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +class StoreJob extends JobImpl { + private final Log _log = new Log(StoreJob.class); + private KademliaNetworkDatabaseFacade _facade; + private StoreState _state; + private Job _onSuccess; + private Job _onFailure; + private long _timeoutMs; + private long _expiration; + + private final static int PARALLELIZATION = 1; // how many sent at a time + private final static int REDUNDANCY = 2; // we want the data sent to 2 peers + /** + * additionally send to 1 outlier(s), in case all of the routers chosen in our + * REDUNDANCY set are attacking us by accepting DbStore messages but dropping + * the data. + * + * TODO: um, honor this. make sure we send to this many peers that aren't + * closest to the key. + * + */ + private final static int EXPLORATORY_REDUNDANCY = 1; + private final static int STORE_PRIORITY = 100; + + /** + * Create a new search for the routingKey specified + * + */ + public StoreJob(KademliaNetworkDatabaseFacade facade, Hash key, DataStructure data, Job onSuccess, Job onFailure, long timeoutMs) { + _facade = facade; + _state = new StoreState(key, data); + _onSuccess = onSuccess; + _onFailure = onFailure; + _timeoutMs = timeoutMs; + _expiration = Clock.getInstance().now() + timeoutMs; + } + + public String getName() { return "Kademlia NetDb Store";} + public void runJob() { + sendNext(); + } + + protected boolean isExpired() { + return Clock.getInstance().now() >= _expiration; + } + + /** + * send the key to the next batch of peers + */ + protected void sendNext() { + if (_state.completed()) { + _log.info("Already completed"); + return; + } + if (isExpired()) { + _state.complete(true); + fail(); + } else { + _log.info("Sending: " + _state); + continueSending(); + } + } + + /** + * Send a series of searches to the next available peers as selected by + * the routing table, but making sure no more than PARALLELIZATION are outstanding + * at any time + * + */ + protected void continueSending() { + if (_state.completed()) return; + int toCheck = PARALLELIZATION - _state.getPending().size(); + if (toCheck <= 0) { + // too many already pending + return; + } + if (toCheck > PARALLELIZATION) + toCheck = PARALLELIZATION; + + List closestHashes = getClosestRouters(_state.getTarget(), toCheck, _state.getAttempted()); + if ( (closestHashes == null) || (closestHashes.size() <= 0) ) { + if (_state.getPending().size() <= 0) { + // we tried to find some peers, but there weren't any and no one else is going to answer + fail(); + } else { + // no more to try, but we might get data or close peers from some outstanding requests + return; + } + } else { + _state.addPending(closestHashes); + _log.info("Continue sending key " + _state.getTarget() + " to " + closestHashes); + for (Iterator iter = closestHashes.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + DataStructure ds = _facade.getDataStore().get(peer); + if ( (ds == null) || !(ds instanceof RouterInfo) ) { + _log.warn("Error selecting closest hash that wasnt a router! " + peer + " : " + ds); + } else { + sendStore((RouterInfo)ds); + } + } + } + } + + /** + * Set of Hash structures for routers we want to send the data to next. This is the + * 'interesting' part of the algorithm. DBStore isn't usually as time sensitive as + * it is reliability sensitive, so lets delegate it off to the PeerSelector via + * selectNearestExplicit, which is currently O(n*log(n)) + * + * @return ordered list of Hash objects + */ + protected List getClosestRouters(Hash key, int numClosest, Set alreadyChecked) { + Hash rkey = RoutingKeyGenerator.getInstance().getRoutingKey(key); + _log.debug("Current routing key for " + key + ": " + rkey); + return PeerSelector.getInstance().selectNearestExplicit(rkey, numClosest, alreadyChecked, _facade.getKBuckets()); + } + + /** + * Send a store to the given peer through a garlic route, including a reply + * DeliveryStatusMessage so we know it got there + * + */ + protected void sendStore(RouterInfo router) { + DatabaseStoreMessage msg = new DatabaseStoreMessage(); + msg.setKey(_state.getTarget()); + if (_state.getData() instanceof RouterInfo) + msg.setRouterInfo((RouterInfo)_state.getData()); + else if (_state.getData() instanceof LeaseSet) + msg.setLeaseSet((LeaseSet)_state.getData()); + else + throw new IllegalArgumentException("Storing an unknown data type! " + _state.getData()); + msg.setMessageExpiration(new Date(Clock.getInstance().now() + _timeoutMs)); + + if (router.getIdentity().equals(Router.getInstance().getRouterInfo().getIdentity())) { + // don't send it to ourselves + _log.error("Dont send store to ourselves - why did we try?"); + return; + } else { + _log.debug("Send store to " + router.getIdentity().getHash().toBase64()); + } + + sendStore(msg, router, _expiration); + } + + protected void sendStore(DatabaseStoreMessage msg, RouterInfo peer, long expiration) { + //sendStoreAsGarlic(msg, peer, expiration); + sendStoreThroughTunnel(msg, peer, expiration); + } + + protected void sendStoreThroughTunnel(DatabaseStoreMessage msg, RouterInfo peer, long expiration) { + FailedJob fail = new FailedJob(peer); + Job sent = new OptimisticSendSuccess(peer); + TunnelInfo info = null; + TunnelId outboundTunnelId = selectOutboundTunnel(); + if (outboundTunnelId != null) + info = TunnelManagerFacade.getInstance().getTunnelInfo(outboundTunnelId); + if (info == null) { + _log.error("selectOutboundTunnel didn't find a valid tunnel! outboundTunnelId = " + outboundTunnelId + " is not known by the tunnel manager"); + return; + } + _log.info("Store for " + _state.getTarget() + " expiring on " + new Date(_expiration) + " is going to " + peer.getIdentity().getHash() + " via outbound tunnel: " + info); + // send it out our outboundTunnelId with instructions for our endpoint to forward it + // to the router specified (though no particular tunnelId on the target) + JobQueue.getInstance().addJob(new SendTunnelMessageJob(msg, outboundTunnelId, peer.getIdentity().getHash(), null, sent, null, fail, null, _expiration-Clock.getInstance().now(), STORE_PRIORITY)); + } + + private TunnelId selectOutboundTunnel() { + TunnelSelectionCriteria criteria = new TunnelSelectionCriteria(); + criteria.setAnonymityPriority(80); + criteria.setLatencyPriority(50); + criteria.setReliabilityPriority(20); + criteria.setMaximumTunnelsRequired(1); + criteria.setMinimumTunnelsRequired(1); + List tunnelIds = TunnelManagerFacade.getInstance().selectOutboundTunnelIds(criteria); + if (tunnelIds.size() <= 0) { + _log.error("No outbound tunnels?!"); + return null; + } else { + return (TunnelId)tunnelIds.get(0); + } + } + + + /** + * Send the store to the peer by way of a garlic and route an ack back to us + * + */ + protected void sendStoreAsGarlic(DatabaseStoreMessage msg, RouterInfo peer, long expiration) { + long waitingForId = RandomSource.getInstance().nextInt(Integer.MAX_VALUE); + GarlicConfig cfg = buildGarlicConfig(msg, peer, waitingForId, expiration); + FailedJob failedJob = new FailedJob(peer); + long timeoutMs = expiration - Clock.getInstance().now(); + StoreMessageSelector selector = new StoreMessageSelector(peer, waitingForId); + SessionKey sentKey = new SessionKey(); + Set sentTags = new HashSet(32); + PublicKey rcptKey = cfg.getRecipientPublicKey(); + if (rcptKey == null) { + if (cfg.getRecipient() == null) { + throw new IllegalArgumentException("Null recipient specified"); + } else if (cfg.getRecipient().getIdentity() == null) { + throw new IllegalArgumentException("Null recipient.identity specified"); + } else if (cfg.getRecipient().getIdentity().getPublicKey() == null) { + throw new IllegalArgumentException("Null recipient.identity.publicKey specified"); + } else + rcptKey = cfg.getRecipient().getIdentity().getPublicKey(); + } + + JobQueue.getInstance().addJob(new SendGarlicJob(cfg, null, failedJob, new UpdateReplyFoundJob(peer, sentKey, sentTags, rcptKey), failedJob, timeoutMs, STORE_PRIORITY, selector, sentKey, sentTags)); + } + + /** + * Build a garlic containing the data store and an ack to be unwrapped at the + * target, with the data store sent locally and the ack sent back to us through + * a random tunnel as a DeliveryStatusMessage containing the ackId + * + */ + protected GarlicConfig buildGarlicConfig(I2NPMessage msg, RouterInfo target, long ackId, long expiration) { + GarlicConfig config = new GarlicConfig(); + + PayloadGarlicConfig dataClove = buildDataClove(msg, target, expiration); + config.addClove(dataClove); + PayloadGarlicConfig ackClove = buildAckClove(ackId, expiration); + config.addClove(ackClove); + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_ROUTER); + instructions.setDelayRequested(false); + instructions.setDelaySeconds(0); + instructions.setEncrypted(false); + instructions.setEncryptionKey(null); + instructions.setRouter(target.getIdentity().getHash()); + instructions.setTunnelId(null); + + config.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + config.setDeliveryInstructions(instructions); + config.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + config.setExpiration(_expiration); + config.setRecipientPublicKey(target.getIdentity().getPublicKey()); + config.setRecipient(target); + config.setRequestAck(false); + + return config; + } + + /** + * Build a clove that sends a DeliveryStatusMessage to us after tunneling it + * through a random inbound tunnel + * + */ + protected PayloadGarlicConfig buildAckClove(long ackId, long expiration) { + DeliveryStatusMessage ackMsg = new DeliveryStatusMessage(); + ackMsg.setArrival(new Date(Clock.getInstance().now())); + ackMsg.setMessageId(ackId); + ackMsg.setMessageExpiration(new Date(expiration)); + ackMsg.setUniqueId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + + PayloadGarlicConfig ackClove = new PayloadGarlicConfig(); + + TunnelSelectionCriteria criteria = new TunnelSelectionCriteria(); + criteria.setAnonymityPriority(80); + criteria.setLatencyPriority(20); + criteria.setReliabilityPriority(50); + criteria.setMaximumTunnelsRequired(1); + criteria.setMinimumTunnelsRequired(1); + List tunnelIds = TunnelManagerFacade.getInstance().selectInboundTunnelIds(criteria); + if (tunnelIds.size() <= 0) { + _log.error("No inbound tunnels exist for a db store ack to come through!"); + return null; + } + TunnelId replyToTunnelId = (TunnelId)tunnelIds.get(0); // tunnel id on that gateway + TunnelInfo info = TunnelManagerFacade.getInstance().getTunnelInfo(replyToTunnelId); + RouterInfo replyPeer = NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(info.getThisHop()); // inbound tunnel gateway + if (replyPeer == null) { + _log.error("We don't know how to reach the gateway of our own inbound tunnel?! " + info); + return null; + } + Hash replyToTunnelRouter = replyPeer.getIdentity().getHash(); + + DeliveryInstructions ackInstructions = new DeliveryInstructions(); + ackInstructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_TUNNEL); + ackInstructions.setRouter(replyToTunnelRouter); + ackInstructions.setTunnelId(replyToTunnelId); + ackInstructions.setDelayRequested(false); + ackInstructions.setDelaySeconds(0); + ackInstructions.setEncrypted(false); + + ackClove.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + ackClove.setDeliveryInstructions(ackInstructions); + ackClove.setExpiration(_expiration); + ackClove.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + ackClove.setPayload(ackMsg); + ackClove.setRecipient(replyPeer); + ackClove.setRequestAck(false); + + return ackClove; + } + + /** + * Build a clove that sends the data to the target (which is local) + */ + protected PayloadGarlicConfig buildDataClove(I2NPMessage data, RouterInfo target, long expiration) { + PayloadGarlicConfig clove = new PayloadGarlicConfig(); + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_LOCAL); + instructions.setRouter(target.getIdentity().getHash()); + instructions.setTunnelId(null); + instructions.setDelayRequested(false); + instructions.setDelaySeconds(0); + instructions.setEncrypted(false); + + clove.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + clove.setDeliveryInstructions(instructions); + clove.setExpiration(expiration); + clove.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + clove.setPayload(data); + clove.setRecipientPublicKey(null); + clove.setRequestAck(false); + + return clove; + } + + + /** + * Called after a match to a db store is found (match against a deliveryStatusMessage) + * + */ + protected class UpdateReplyFoundJob extends JobImpl implements ReplyJob { + private I2NPMessage _message; + private Hash _peer; + private SessionKey _sentKey; + private Set _sentTags; + private PublicKey _toKey; + + public UpdateReplyFoundJob(RouterInfo peer, SessionKey sentKey, Set sentTags, PublicKey toKey) { + super(); + _peer = peer.getIdentity().getHash(); + _sentKey = sentKey; + _sentTags = sentTags; + _toKey = toKey; + } + + public String getName() { return "Update Reply Found for Kademlia Store"; } + public void runJob() { + _log.info("Reply from " + _peer + " with message " + _message); + + if (_message.getType() == DeliveryStatusMessage.MESSAGE_TYPE) { + long delay = _state.confirmed(_peer); + ProfileManager.getInstance().dbStoreSent(_peer, delay); + + if ( (_sentKey != null) && (_sentKey.getData() != null) && (_sentTags != null) && (_sentTags.size() > 0) && (_toKey != null) ) { + SessionKeyManager.getInstance().tagsDelivered(_toKey, _sentKey, _sentTags); + _log.info("Delivered tags successfully to " + _peer + "! # tags: " + _sentTags.size()); + } + + if (_state.getSuccessful().size() >= REDUNDANCY) { + succeed(); + } else { + sendNext(); + } + } else { + _log.error("Selector matched to an UpdateReplyFoundJob with a message that isnt a DeliveryStatusMessage! " + _message); + } + } + + public void setMessage(I2NPMessage message) { _message = message; } + } + + /** + * Called after sending a dbStore to a peer successfully without waiting for confirm and + * optimistically mark the store as successful + * + */ + protected class OptimisticSendSuccess extends JobImpl { + private Hash _peer; + + public OptimisticSendSuccess(RouterInfo peer) { + super(); + _peer = peer.getIdentity().getHash(); + } + + public String getName() { return "Optimistic Kademlia Store Send Success"; } + public void runJob() { + _log.info("Optimistically marking store of " + _state.getTarget() + " to " + _peer + " successful"); + //long howLong = _state.confirmed(_peer); + //ProfileManager.getInstance().dbStoreSent(_peer, howLong); + + if (_state.getSuccessful().size() >= REDUNDANCY) { + succeed(); + } else { + sendNext(); + } + } + } + + /** + * Called when a particular peer failed to respond before the timeout was + * reached, or if the peer could not be contacted at all. + * + */ + protected class FailedJob extends JobImpl { + private Hash _peer; + public FailedJob(RouterInfo peer) { + super(); + _peer = peer.getIdentity().getHash(); + } + public void runJob() { + _state.replyTimeout(_peer); + ProfileManager.getInstance().dbStoreFailed(_peer); + sendNext(); + } + public String getName() { return "Kademlia Store Failed"; } + } + + /** + * Check to see the message is a reply from the peer regarding the current + * search + * + */ + protected class StoreMessageSelector implements MessageSelector { + private Hash _peer; + private long _waitingForId; + private boolean _found; + public StoreMessageSelector(RouterInfo peer, long waitingForId) { + _peer = peer.getIdentity().getHash(); + _found = false; + _waitingForId = waitingForId; + } + + public boolean continueMatching() { return !_found; } + public long getExpiration() { return _expiration; } + public boolean isMatch(I2NPMessage message) { + _log.debug("isMatch("+message.getClass().getName() + ") [want deliveryStatusMessage from " + _peer + " wrt " + _state.getTarget() + "]"); + if (message instanceof DeliveryStatusMessage) { + DeliveryStatusMessage msg = (DeliveryStatusMessage)message; + if (msg.getMessageId() == _waitingForId) { + _log.debug("Found match for the key we're waiting for: " + _waitingForId); + _found = true; + return true; + } else { + _log.debug("DeliveryStatusMessage of a key we're not looking for"); + return false; + } + } else { + _log.debug("Not a DeliveryStatusMessage"); + return false; + } + } + } + + /** + * Send was totally successful + */ + protected void succeed() { + _log.info("Succeeded sending key " + _state.getTarget()); + _log.debug("State of successful send: " + _state); + if (_onSuccess != null) + JobQueue.getInstance().addJob(_onSuccess); + _facade.noteKeySent(_state.getTarget()); + } + + /** + * Send totally failed + */ + protected void fail() { + _log.info("Failed sending key " + _state.getTarget()); + _log.debug("State of failed send: " + _state, new Exception("Who failed me?")); + if (_onFailure != null) + JobQueue.getInstance().addJob(_onFailure); + } + + protected static class StoreState { + private Hash _key; + private DataStructure _data; + private HashSet _pendingPeers; + private HashMap _pendingPeerTimes; + private HashSet _successfulPeers; + private HashSet _successfulExploratoryPeers; + private HashSet _failedPeers; + private HashSet _attemptedPeers; + private volatile long _completed; + private volatile long _started; + + public StoreState(Hash key, DataStructure data) { + _key = key; + _data = data; + _pendingPeers = new HashSet(16); + _pendingPeerTimes = new HashMap(16); + _attemptedPeers = new HashSet(16); + _failedPeers = new HashSet(16); + _successfulPeers = new HashSet(16); + _successfulExploratoryPeers = new HashSet(16); + _completed = -1; + _started = Clock.getInstance().now(); + } + + public Hash getTarget() { return _key; } + public DataStructure getData() { return _data; } + public Set getPending() { + synchronized (_pendingPeers) { + return (Set)_pendingPeers.clone(); + } + } + public Set getAttempted() { + synchronized (_attemptedPeers) { + return (Set)_attemptedPeers.clone(); + } + } + public Set getSuccessful() { + synchronized (_successfulPeers) { + return (Set)_successfulPeers.clone(); + } + } + public Set getSuccessfulExploratory() { + synchronized (_successfulExploratoryPeers) { + return (Set)_successfulExploratoryPeers.clone(); + } + } + public Set getFailed() { + synchronized (_failedPeers) { + return (Set)_failedPeers.clone(); + } + } + public boolean completed() { return _completed != -1; } + public void complete(boolean completed) { + if (completed) + _completed = Clock.getInstance().now(); + } + + public long getWhenStarted() { return _started; } + public long getWhenCompleted() { return _completed; } + + public void addPending(Collection pending) { + synchronized (_pendingPeers) { + _pendingPeers.addAll(pending); + for (Iterator iter = pending.iterator(); iter.hasNext(); ) + _pendingPeerTimes.put(iter.next(), new Long(Clock.getInstance().now())); + } + synchronized (_attemptedPeers) { + _attemptedPeers.addAll(pending); + } + } + + public long confirmed(Hash peer) { + long rv = -1; + synchronized (_pendingPeers) { + _pendingPeers.remove(peer); + Long when = (Long)_pendingPeerTimes.remove(peer); + if (when != null) + rv = Clock.getInstance().now() - when.longValue(); + } + synchronized (_successfulPeers) { + _successfulPeers.add(peer); + } + return rv; + } + + public long confirmedExploratory(Hash peer) { + long rv = -1; + synchronized (_pendingPeers) { + _pendingPeers.remove(peer); + Long when = (Long)_pendingPeerTimes.remove(peer); + if (when != null) + rv = Clock.getInstance().now() - when.longValue(); + } + synchronized (_successfulExploratoryPeers) { + _successfulExploratoryPeers.add(peer); + } + return rv; + } + + public void replyTimeout(Hash peer) { + synchronized (_pendingPeers) { + _pendingPeers.remove(peer); + } + synchronized (_failedPeers) { + _failedPeers.add(peer); + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(256); + buf.append("Storing ").append(_key); + buf.append(" "); + if (_completed <= 0) + buf.append(" completed? false "); + else + buf.append(" completed on ").append(new Date(_completed)); + buf.append(" Attempted: "); + synchronized (_attemptedPeers) { + for (Iterator iter = _attemptedPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append(peer.toBase64()).append(" "); + } + } + buf.append(" Pending: "); + synchronized (_pendingPeers) { + for (Iterator iter = _pendingPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append(peer.toBase64()).append(" "); + } + } + buf.append(" Failed: "); + synchronized (_failedPeers) { + for (Iterator iter = _failedPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append(peer.toBase64()).append(" "); + } + } + buf.append(" Successful: "); + synchronized (_successfulPeers) { + for (Iterator iter = _successfulPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append(peer.toBase64()).append(" "); + } + } + buf.append(" Successful Exploratory: "); + synchronized (_successfulExploratoryPeers) { + for (Iterator iter = _successfulExploratoryPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + buf.append(peer.toBase64()).append(" "); + } + } + return buf.toString(); + } + } +} + diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java new file mode 100644 index 0000000000..40e53ec7d3 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java @@ -0,0 +1,153 @@ +package net.i2p.router.networkdb.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.DataStructure; +import net.i2p.data.RouterInfo; +import net.i2p.data.LeaseSet; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import net.i2p.router.ProfileManager; + +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Date; + +class TransientDataStore implements DataStore { + private final static Log _log = new Log(TransientDataStore.class); + private Map _data; // hash --> DataStructure + + public TransientDataStore() { + _data = new HashMap(1024); + if (_log.shouldLog(Log.INFO)) + _log.info("Data Store initialized"); + } + + public Set getKeys() { + synchronized (_data) { + return new HashSet(_data.keySet()); + } + } + + public DataStructure get(Hash key) { + synchronized (_data) { + return (DataStructure)_data.get(key); + } + } + + public boolean isKnown(Hash key) { + synchronized (_data) { + return _data.containsKey(key); + } + } + + /** nothing published more than 5 minutes in the future */ + private final static long MAX_FUTURE_PUBLISH_DATE = 5*60*1000; + /** don't accept tunnels set to expire more than 3 hours in the future, which is insane */ + private final static long MAX_FUTURE_EXPIRATION_DATE = 3*60*60*1000; + + public void put(Hash key, DataStructure data) { + if (data == null) return; + _log.debug("Storing key " + key); + Object old = null; + synchronized (_data) { + old = _data.put(key, data); + } + if (data instanceof RouterInfo) { + ProfileManager.getInstance().heardAbout(key); + RouterInfo ri = (RouterInfo)data; + if (old != null) { + RouterInfo ori = (RouterInfo)old; + if (ri.getPublished() < ori.getPublished()) { + if (_log.shouldLog(Log.INFO)) + _log.info("Almost clobbered an old router! " + key + ": [old published on " + new Date(ori.getPublished()) + " new on " + new Date(ri.getPublished()) + "]"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Number of router options for " + key + ": " + ri.getOptions().size() + " (old one had: " + ori.getOptions().size() + ")", new Exception("Updated routerInfo")); + synchronized (_data) { + _data.put(key, old); + } + } else if (ri.getPublished() > Clock.getInstance().now() + MAX_FUTURE_PUBLISH_DATE) { + if (_log.shouldLog(Log.INFO)) + _log.info("Hmm, someone tried to give us something with the publication date really far in the future (" + new Date(ri.getPublished()) + "), dropping it"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Number of router options for " + key + ": " + ri.getOptions().size() + " (old one had: " + ori.getOptions().size() + ")", new Exception("Updated routerInfo")); + synchronized (_data) { + _data.put(key, old); + } + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Updated the old router for " + key + ": [old published on " + new Date(ori.getPublished()) + " new on " + new Date(ri.getPublished()) + "]"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Number of router options for " + key + ": " + ri.getOptions().size() + " (old one had: " + ori.getOptions().size() + ")", new Exception("Updated routerInfo")); + } + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Brand new router for " + key + ": published on " + new Date(ri.getPublished())); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Number of router options for " + key + ": " + ri.getOptions().size(), new Exception("Updated routerInfo")); + } + } else if (data instanceof LeaseSet) { + LeaseSet ls = (LeaseSet)data; + if (old != null) { + LeaseSet ols = (LeaseSet)old; + if (ls.getEarliestLeaseDate() < ols.getEarliestLeaseDate()) { + if (_log.shouldLog(Log.INFO)) + _log.info("Almost clobbered an old leaseSet! " + key + ": [old published on " + new Date(ols.getEarliestLeaseDate()) + " new on " + new Date(ls.getEarliestLeaseDate()) + "]"); + synchronized (_data) { + _data.put(key, old); + } + } else if (ls.getEarliestLeaseDate() > Clock.getInstance().now() + MAX_FUTURE_EXPIRATION_DATE) { + if (_log.shouldLog(Log.INFO)) + _log.info("Hmm, someone tried to give us something with the expiration date really far in the future (" + new Date(ls.getEarliestLeaseDate()) + "), dropping it"); + synchronized (_data) { + _data.put(key, old); + } + } + } + } + } + + public int hashCode() { + return DataHelper.hashCode(_data); + } + public boolean equals(Object obj) { + if ( (obj == null) || (obj.getClass() != getClass()) ) return false; + TransientDataStore ds = (TransientDataStore)obj; + return DataHelper.eq(ds._data, _data); + } + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("Transient DataStore: ").append(_data.size()).append("\nKeys: "); + Map data = new HashMap(); + synchronized (_data) { + data.putAll(_data); + } + for (Iterator iter = data.keySet().iterator(); iter.hasNext();) { + Hash key = (Hash)iter.next(); + DataStructure dp = (DataStructure)data.get(key); + buf.append("\n\t*Key: ").append(key.toString()).append("\n\tContent: ").append(dp.toString()); + } + buf.append("\n"); + return buf.toString(); + } + + public DataStructure remove(Hash key) { + synchronized (_data) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Removing key " + key.toBase64()); + return (DataStructure)_data.remove(key); + } + } +} diff --git a/router/java/src/net/i2p/router/peermanager/Calculator.java b/router/java/src/net/i2p/router/peermanager/Calculator.java new file mode 100644 index 0000000000..8796d4688b --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/Calculator.java @@ -0,0 +1,28 @@ +package net.i2p.router.peermanager; + + +/** + * Provide a means of quantifying a profiles fitness in some particular aspect, as well + * as to coordinate via statics the four known aspects. + * + */ +class Calculator { + private static Calculator _isFailingCalc = new IsFailingCalculator(); + private static Calculator _integrationCalc = new IntegrationCalculator(); + private static Calculator _speedCalc = new SpeedCalculator(); + private static Calculator _reliabilityCalc = new ReliabilityCalculator(); + + public static Calculator getIsFailingCalculator() { return _isFailingCalc; } + public static Calculator getIntegrationCalculator() { return _integrationCalc; } + public static Calculator getSpeedCalculator() { return _speedCalc; } + public static Calculator getReliabilityCalculator() { return _reliabilityCalc; } + + /** + * Evaluate the profile according to the current metric + */ + public double calc(PeerProfile profile) { return 0.0d; } + /** + * Evaluate the profile according to the current metric + */ + public boolean calcBoolean(PeerProfile profile) { return true; } +} diff --git a/router/java/src/net/i2p/router/peermanager/DBHistory.java b/router/java/src/net/i2p/router/peermanager/DBHistory.java new file mode 100644 index 0000000000..cc6c3b223c --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/DBHistory.java @@ -0,0 +1,212 @@ +package net.i2p.router.peermanager; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Properties; +import net.i2p.stat.RateStat; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * History of NetDb related activities (lookups, replies, stores, etc) + * + */ +public class DBHistory { + private static final Log _log = new Log(DBHistory.class); + private long _successfulLookups; + private long _failedLookups; + private RateStat _failedLookupRate; + private long _lookupReplyNew; + private long _lookupReplyOld; + private long _lookupReplyDuplicate; + private long _lookupReplyInvalid; + private long _lookupsReceived; + private long _avgDelayBetweenLookupsReceived; + private long _lastLookupReceived; + private long _unpromptedDbStoreNew; + private long _unpromptedDbStoreOld; + + public DBHistory() { + _successfulLookups = 0; + _failedLookups = 0; + _failedLookupRate = null; + _lookupReplyNew = 0; + _lookupReplyOld = 0; + _lookupReplyDuplicate = 0; + _lookupReplyInvalid = 0; + _lookupsReceived = 0; + _avgDelayBetweenLookupsReceived = 0; + _lastLookupReceived = -1; + _unpromptedDbStoreNew = 0; + _unpromptedDbStoreOld = 0; + createRates(); + } + + /** how many times we have sent them a db lookup and received the value back from them */ + public long getSuccessfulLookups() { return _successfulLookups; } + /** how many times we have sent them a db lookup and not received the value or a lookup reply */ + public long getFailedLookups() { return _failedLookups; } + /** how many peers that we have never seen before did lookups provide us with? */ + public long getLookupReplyNew() { return _lookupReplyNew; } + /** how many peers that we have already seen did lookups provide us with? */ + public long getLookupReplyOld() { return _lookupReplyOld; } + /** how many peers that we explicitly asked the peer not to send us did they reply with? */ + public long getLookupReplyDuplicate() { return _lookupReplyDuplicate; } + /** how many peers that were incorrectly formatted / expired / otherwise illegal did lookups provide us with? */ + public long getLookupReplyInvalid() { return _lookupReplyInvalid; } + /** how many lookups this peer has sent us? */ + public long getLookupsReceived() { return _lookupsReceived; } + /** how frequently do they send us lookup requests? */ + public long getAvgDelayBetweenLookupsReceived() { return _avgDelayBetweenLookupsReceived; } + /** when did they last send us a request? */ + public long getLastLookupReceived() { return _lastLookupReceived; } + /** how many times have they sent us data we didn't ask for and that we've never seen? */ + public long getUnpromptedDbStoreNew() { return _unpromptedDbStoreNew; } + /** how many times have they sent us data we didn't ask for but that we have seen? */ + public long getUnpromptedDbStoreOld() { return _unpromptedDbStoreOld; } + /** how often does the peer fail to reply to a lookup request? 1 hour and 1 day periods */ + public RateStat getFailedLookupRate() { return _failedLookupRate; } + + /** + * Note that the peer was not only able to respond to the lookup, but sent us + * the data we wanted! + * + */ + public void lookupSuccessful() { + _successfulLookups++; + } + /** + * Note that the peer failed to respond to the db lookup in any way + */ + public void lookupFailed() { + _failedLookups++; + _failedLookupRate.addData(1, 0); + } + /** + * Receive a lookup reply from the peer, where they gave us the specified info + * + * @param newPeers number of peers we have never seen before + * @param oldPeers number of peers we have seen before + * @param invalid number of peers that are invalid / out of date / otherwise b0rked + * @param duplicate number of peers we asked them not to give us (though they're allowed to send us + * themselves if they don't know anyone else) + */ + public void lookupReply(int newPeers, int oldPeers, int invalid, int duplicate) { + _lookupReplyNew += newPeers; + _lookupReplyOld += oldPeers; + _lookupReplyInvalid += invalid; + _lookupReplyDuplicate += duplicate; + } + /** + * Note that the peer sent us a lookup + * + */ + public void lookupReceived() { + long now = Clock.getInstance().now(); + long delay = now - _lastLookupReceived; + _lastLookupReceived = now; + _lookupsReceived++; + if (_avgDelayBetweenLookupsReceived <= 0) { + _avgDelayBetweenLookupsReceived = delay; + } else { + if (delay > _avgDelayBetweenLookupsReceived) + _avgDelayBetweenLookupsReceived = _avgDelayBetweenLookupsReceived + (delay / _lookupsReceived); + else + _avgDelayBetweenLookupsReceived = _avgDelayBetweenLookupsReceived - (delay / _lookupsReceived); + } + } + /** + * Note that the peer sent us a data point without us asking for it + * @param wasNew whether we already knew about this data point or not + */ + public void unpromptedStoreReceived(boolean wasNew) { + if (wasNew) + _unpromptedDbStoreNew++; + else + _unpromptedDbStoreOld++; + } + + public void setSuccessfulLookups(long num) { _successfulLookups = num; } + public void setFailedLookups(long num) { _failedLookups = num; } + public void setLookupReplyNew(long num) { _lookupReplyNew = num; } + public void setLookupReplyOld(long num) { _lookupReplyOld = num; } + public void setLookupReplyInvalid(long num) { _lookupReplyInvalid = num; } + public void setLookupReplyDuplicate(long num) { _lookupReplyDuplicate = num; } + public void setLookupsReceived(long num) { _lookupsReceived = num; } + public void setAvgDelayBetweenLookupsReceived(long ms) { _avgDelayBetweenLookupsReceived = ms; } + public void setLastLookupReceived(long when) { _lastLookupReceived = when; } + public void setUnpromptedDbStoreNew(long num) { _unpromptedDbStoreNew = num; } + public void setUnpromptedDbStoreOld(long num) { _unpromptedDbStoreOld = num; } + + public void coallesceStats() { + _log.debug("Coallescing stats"); + _failedLookupRate.coallesceStats(); + } + + private final static String NL = System.getProperty("line.separator"); + + public void store(OutputStream out) throws IOException { + StringBuffer buf = new StringBuffer(512); + buf.append(NL); + buf.append("#################").append(NL); + buf.append("# DB history").append(NL); + buf.append("###").append(NL); + add(buf, "successfulLookups", _successfulLookups, "How many times have they successfully given us what we wanted when looking for it?"); + add(buf, "failedLookups", _failedLookups, "How many times have we sent them a db lookup and they didn't reply?"); + add(buf, "lookupsReceived", _lookupsReceived, "How many lookups have they sent us?"); + add(buf, "lookupReplyDuplicate", _lookupReplyDuplicate, "How many of their reply values to our lookups were something we asked them not to send us?"); + add(buf, "lookupReplyInvalid", _lookupReplyInvalid, "How many of their reply values to our lookups were invalid (expired, forged, corrupted)?"); + add(buf, "lookupReplyNew", _lookupReplyNew, "How many of their reply values to our lookups were brand new to us?"); + add(buf, "lookupReplyOld", _lookupReplyOld, "How many of their reply values to our lookups were something we had seen before?"); + add(buf, "unpromptedDbStoreNew", _unpromptedDbStoreNew, "How times have they sent us something we didn't ask for and hadn't seen before?"); + add(buf, "unpromptedDbStoreOld", _unpromptedDbStoreOld, "How times have they sent us something we didn't ask for but have seen before?"); + add(buf, "lastLookupReceived", _lastLookupReceived, "When was the last time they send us a lookup? (milliseconds since the epoch)"); + add(buf, "avgDelayBetweenLookupsReceived", _avgDelayBetweenLookupsReceived, "How long is it typically between each db lookup they send us? (in milliseconds)"); + out.write(buf.toString().getBytes()); + _failedLookupRate.store(out, "dbHistory.failedLookupRate"); + _log.debug("Writing out dbHistory.failedLookupRate"); + } + + private void add(StringBuffer buf, String name, long val, String description) { + buf.append("# ").append(name.toUpperCase()).append(NL).append("# ").append(description).append(NL); + buf.append("dbHistory.").append(name).append('=').append(val).append(NL).append(NL); + } + + + public void load(Properties props) { + _successfulLookups = getLong(props, "dbHistory.successfulLookups"); + _failedLookups = getLong(props, "dbHistory.failedLookups"); + _lookupsReceived = getLong(props, "dbHistory.lookupsReceived"); + _lookupReplyDuplicate = getLong(props, "dbHistory.lookupReplyDuplicate"); + _lookupReplyInvalid = getLong(props, "dbHistory.lookupReplyInvalid"); + _lookupReplyNew = getLong(props, "dbHistory.lookupReplyNew"); + _lookupReplyOld = getLong(props, "dbHistory.lookupReplyOld"); + _unpromptedDbStoreNew = getLong(props, "dbHistory.unpromptedDbStoreNew"); + _unpromptedDbStoreOld = getLong(props, "dbHistory.unpromptedDbStoreOld"); + _lastLookupReceived = getLong(props, "dbHistory.lastLookupReceived"); + _avgDelayBetweenLookupsReceived = getLong(props, "dbHistory.avgDelayBetweenLookupsReceived"); + try { + _failedLookupRate.load(props, "dbHistory.failedLookupRate", true); + _log.debug("Loading dbHistory.failedLookupRate"); + } catch (IllegalArgumentException iae) { + _log.warn("DB History failed lookup rate is corrupt, resetting", iae); + createRates(); + } + } + + private void createRates() { + _failedLookupRate = new RateStat("dbHistory.failedLookupRate", "How often does this peer to respond to a lookup?", "dbHistory", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + private final static long getLong(Properties props, String key) { + String val = props.getProperty(key); + if (val != null) { + try { + return Long.parseLong(val); + } catch (NumberFormatException nfe) { + return 0; + } + } + return 0; + } +} diff --git a/router/java/src/net/i2p/router/peermanager/EvaluateProfilesJob.java b/router/java/src/net/i2p/router/peermanager/EvaluateProfilesJob.java new file mode 100644 index 0000000000..faa20d6534 --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/EvaluateProfilesJob.java @@ -0,0 +1,50 @@ +package net.i2p.router.peermanager; + +import java.util.Date; +import java.util.Iterator; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.router.JobImpl; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * Run across all of the profiles, coallescing the stats and reorganizing them + * into appropriate groups. The stat coallesce must be run at least once a minute, + * so if the group reorg wants to get changed, this may want to be split into two + * jobs. + * + */ +class EvaluateProfilesJob extends JobImpl { + private final static Log _log = new Log(EvaluateProfilesJob.class); + + public EvaluateProfilesJob() {} + + public String getName() { return "Evaluate peer profiles"; } + public void runJob() { + try { + long start = Clock.getInstance().now(); + Set allPeers = ProfileOrganizer.getInstance().selectAllPeers(); + long afterSelect = Clock.getInstance().now(); + for (Iterator iter = allPeers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + PeerProfile profile = ProfileOrganizer.getInstance().getProfile(peer); + if (profile != null) + profile.coallesceStats(); + } + long afterCoallesce = Clock.getInstance().now(); + ProfileOrganizer.getInstance().reorganize(); + long afterReorganize = Clock.getInstance().now(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Profiles coallesced and reorganized. total: " + allPeers.size() + ", selectAll: " + (afterSelect-start) + "ms, coallesce: " + (afterCoallesce-afterSelect) + "ms, reorganize: " + (afterReorganize-afterSelect)); + } catch (Throwable t) { + _log.log(Log.CRIT, "Error evaluating profiles", t); + } finally { + requeue(30*1000); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Requeued for " + new Date(getTiming().getStartAfter())); + } + } +} diff --git a/router/java/src/net/i2p/router/peermanager/IntegrationCalculator.java b/router/java/src/net/i2p/router/peermanager/IntegrationCalculator.java new file mode 100644 index 0000000000..569d187c30 --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/IntegrationCalculator.java @@ -0,0 +1,18 @@ +package net.i2p.router.peermanager; + +import net.i2p.util.Log; + +/** + * Determine how well integrated the peer is - how likely they will be useful + * to us if we are trying to get further connected. + * + */ +class IntegrationCalculator extends Calculator { + private final static Log _log = new Log(IntegrationCalculator.class); + + public double calc(PeerProfile profile) { + long val = profile.getDbIntroduction().getRate(24*60*60*1000l).getCurrentEventCount(); + val += profile.getIntegrationBonus(); + return val; + } +} diff --git a/router/java/src/net/i2p/router/peermanager/IsFailingCalculator.java b/router/java/src/net/i2p/router/peermanager/IsFailingCalculator.java new file mode 100644 index 0000000000..9472f7e76a --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/IsFailingCalculator.java @@ -0,0 +1,50 @@ +package net.i2p.router.peermanager; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Simple boolean calculation to determine whether the given profile is "failing" - + * meaning we shouldn't bother trying to get them to do something (however, if we + * have a specific need to contact them in particular - e.g. instructions in a garlic + * or leaseSet - we will try). The currently implemented algorithm determines that + * a profile is failing if withing the last few minutes, they've done something bad:
    + *
  • It has a comm error (TCP disconnect, etc) in the last minute or two
  • + *
  • They've failed to respond to a db message in the last minute or two
  • + *
  • They've rejected a tunnel in the last 5 minutes
  • + *
  • They've been unreachable any time in the last 5 minutes
  • + *
+ * + */ +class IsFailingCalculator extends Calculator { + private final static Log _log = new Log(IsFailingCalculator.class); + + /** if they haven't b0rked in the last 5 minutes, they're ok */ + private final static long GRACE_PERIOD = 5*60*1000; + + public boolean calcBoolean(PeerProfile profile) { + // have we failed in the last 119 seconds? + if ( (profile.getCommError().getRate(60*1000).getCurrentEventCount() > 0) || + (profile.getCommError().getRate(60*1000).getLastEventCount() > 0) ) { + return true; + } else { + if ( (profile.getDBHistory().getFailedLookupRate().getRate(60*1000).getCurrentEventCount() > 0) || + (profile.getDBHistory().getFailedLookupRate().getRate(60*1000).getLastEventCount() > 0) ) { + // are they overloaded (or disconnected)? + return true; + } + + long recently = Clock.getInstance().now() - GRACE_PERIOD; + + if (profile.getTunnelHistory().getLastRejected() >= recently) { + // have they refused to participate in a tunnel in the last 5 minutes? + return true; + } + + if (profile.getLastSendFailed() >= recently) + return true; + + return false; + } + } +} diff --git a/router/java/src/net/i2p/router/peermanager/PeerManager.java b/router/java/src/net/i2p/router/peermanager/PeerManager.java new file mode 100644 index 0000000000..8095300ab8 --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/PeerManager.java @@ -0,0 +1,118 @@ +package net.i2p.router.peermanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; + +import net.i2p.data.Hash; +import net.i2p.router.PeerSelectionCriteria; +import net.i2p.router.Router; +import net.i2p.router.JobQueue; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.util.Log; + +/** + * Manage the current state of the statistics + * + */ +class PeerManager { + private final static Log _log = new Log(PeerManager.class); + private ProfileOrganizer _organizer = ProfileOrganizer.getInstance(); + + public PeerManager() { + _organizer.setUs(Router.getInstance().getRouterInfo().getIdentity().getHash()); + loadProfiles(); + JobQueue.getInstance().addJob(new EvaluateProfilesJob()); + JobQueue.getInstance().addJob(new PersistProfilesJob(this)); + } + + void storeProfiles() { + Set peers = selectPeers(); + for (Iterator iter = peers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + storeProfile(peer); + } + } + Set selectPeers() { + return _organizer.getInstance().selectAllPeers(); + } + void storeProfile(Hash peer) { + if (peer == null) return; + PeerProfile prof = _organizer.getInstance().getProfile(peer); + if (prof == null) return; + ProfilePersistenceHelper.getInstance().writeProfile(prof); + } + void loadProfiles() { + Set profiles = ProfilePersistenceHelper.getInstance().readProfiles(); + for (Iterator iter = profiles.iterator(); iter.hasNext();) { + PeerProfile prof = (PeerProfile)iter.next(); + if (prof != null) { + _organizer.addProfile(prof); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Profile for " + prof.getPeer().toBase64() + " loaded"); + } + } + } + + /** + * Find some peers that meet the criteria and we have the netDb info for locally + * + */ + Set selectPeers(PeerSelectionCriteria criteria) { + int numPasses = 0; + Set rv = new HashSet(criteria.getMinimumRequired()); + Set exclude = new HashSet(1); + exclude.add(Router.getInstance().getRouterInfo().getIdentity().getHash()); + while (rv.size() < criteria.getMinimumRequired()) { + Set curVals = new HashSet(criteria.getMinimumRequired()); + switch (criteria.getPurpose()) { + case PeerSelectionCriteria.PURPOSE_TEST: + _organizer.selectWellIntegratedPeers(criteria.getMinimumRequired(), exclude, curVals); + break; + case PeerSelectionCriteria.PURPOSE_TUNNEL: + _organizer.selectFastAndReliablePeers(criteria.getMinimumRequired(), exclude, curVals); + break; + case PeerSelectionCriteria.PURPOSE_SOURCE_ROUTE: + _organizer.selectReliablePeers(criteria.getMinimumRequired(), exclude, curVals); + break; + case PeerSelectionCriteria.PURPOSE_GARLIC: + _organizer.selectReliablePeers(criteria.getMinimumRequired(), exclude, curVals); + break; + default: + break; + } + if (curVals.size() <= 0) { + if (_log.shouldLog(Log.WARN)) + _log.warn("We ran out of peers when looking for reachable ones after finding " + rv.size()); + break; + } else { + for (Iterator iter = curVals.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + if (null != NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(peer)) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer " + peer.toBase64() + " is locally known, so we'll allow its selection"); + rv.add(peer); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer " + peer.toBase64() + " is NOT locally known, disallowing its selection"); + } + } + exclude.addAll(curVals); + } + numPasses++; + } + if (_log.shouldLog(Log.INFO)) + _log.info("Peers selected after " + numPasses + ": " + rv); + return rv; + } + + public String renderStatusHTML() { return _organizer.renderStatusHTML(); } +} diff --git a/router/java/src/net/i2p/router/peermanager/PeerManagerFacadeImpl.java b/router/java/src/net/i2p/router/peermanager/PeerManagerFacadeImpl.java new file mode 100644 index 0000000000..2a71383cc2 --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/PeerManagerFacadeImpl.java @@ -0,0 +1,43 @@ +package net.i2p.router.peermanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.List; +import java.util.ArrayList; + +import net.i2p.router.PeerManagerFacade; +import net.i2p.router.PeerSelectionCriteria; +import net.i2p.router.Router; +import net.i2p.util.Log; + +/** + * Base implementation that has simple algorithms and periodically saves state + * + */ +public class PeerManagerFacadeImpl extends PeerManagerFacade { + private final static Log _log = new Log(PeerManagerFacadeImpl.class); + private PeerManager _manager; + + public void startup() { + _log.info("Starting up the peer manager"); + _manager = new PeerManager(); + ProfilePersistenceHelper.getInstance().setUs(Router.getInstance().getRouterInfo().getIdentity().getHash()); + } + + public void shutdown() { + _log.info("Shutting down the peer manager"); + _manager.storeProfiles(); + } + + public List selectPeers(PeerSelectionCriteria criteria) { + return new ArrayList(_manager.selectPeers(criteria)); + } + + public String renderStatusHTML() { return _manager.renderStatusHTML(); } +} diff --git a/router/java/src/net/i2p/router/peermanager/PeerProfile.java b/router/java/src/net/i2p/router/peermanager/PeerProfile.java new file mode 100644 index 0000000000..c3e97c2c21 --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/PeerProfile.java @@ -0,0 +1,317 @@ +package net.i2p.router.peermanager; + +import net.i2p.data.Hash; +import net.i2p.stat.RateStat; +import net.i2p.util.Log; + +class PeerProfile { + private final static Log _log = new Log(PeerProfile.class); + // whoozaat? + private Hash _peer; + // general peer stats + private long _firstHeardAbout; + private long _lastHeardAbout; + private long _lastSentToSuccessfully; + private long _lastFailedSend; + private long _lastHeardFrom; + // periodic rates + private RateStat _sendSuccessSize = null; + private RateStat _sendFailureSize = null; + private RateStat _receiveSize = null; + private RateStat _dbResponseTime = null; + private RateStat _tunnelCreateResponseTime = null; + private RateStat _commError = null; + private RateStat _dbIntroduction = null; + // calculation bonuses + private long _speedBonus; + private long _reliabilityBonus; + private long _integrationBonus; + // calculation values + private double _speedValue; + private double _reliabilityValue; + private double _integrationValue; + private boolean _isFailing; + // good vs bad behavior + private TunnelHistory _tunnelHistory; + private DBHistory _dbHistory; + // does this peer profile contain expanded data, or just the basics? + private boolean _expanded; + + public PeerProfile() { + this(null, true); + } + public PeerProfile(Hash peer) { + this(peer, true); + } + public PeerProfile(Hash peer, boolean expand) { + _expanded = false; + _speedValue = 0; + _reliabilityValue = 0; + _integrationValue = 0; + _isFailing = false; + _peer = peer; + if (expand) + expandProfile(); + } + + /** what peer is being profiled */ + public Hash getPeer() { return _peer; } + public void setPeer(Hash peer) { _peer = peer; } + + /** + * are we keeping an expanded profile on the peer, or just the bare minimum? + * If we aren't keeping the expanded profile, all of the rates as well as the + * TunnelHistory and DBHistory will not be available. + * + */ + public boolean getIsExpanded() { return _expanded; } + + /** + * Is this peer active at the moment (sending/receiving messages within the last + * 5 minutes) + */ + public boolean getIsActive() { + if ( (getSendSuccessSize().getRate(5*60*1000).getCurrentEventCount() > 0) || + (getSendSuccessSize().getRate(5*60*1000).getLastEventCount() > 0) || + (getReceiveSize().getRate(5*60*1000).getCurrentEventCount() > 0) || + (getReceiveSize().getRate(5*60*1000).getLastEventCount() > 0) ) + return true; + else + return false; + } + + + /** when did we first hear about this peer? */ + public long getFirstHeardAbout() { return _firstHeardAbout; } + public void setFirstHeardAbout(long when) { _firstHeardAbout = when; } + + /** when did we last hear about this peer? */ + public long getLastHeardAbout() { return _lastHeardAbout; } + public void setLastHeardAbout(long when) { _lastHeardAbout = when; } + + /** when did we last send to this peer successfully? */ + public long getLastSendSuccessful() { return _lastSentToSuccessfully; } + public void setLastSendSuccessful(long when) { _lastSentToSuccessfully = when; } + + /** when did we last have a problem sending to this peer? */ + public long getLastSendFailed() { return _lastFailedSend; } + public void setLastSendFailed(long when) { _lastFailedSend = when; } + + /** when did we last hear from the peer? */ + public long getLastHeardFrom() { return _lastHeardFrom; } + public void setLastHeardFrom(long when) { _lastHeardFrom = when; } + + /** history of tunnel activity with the peer */ + public TunnelHistory getTunnelHistory() { return _tunnelHistory; } + public void setTunnelHistory(TunnelHistory history) { _tunnelHistory = history; } + + /** history of db activity with the peer */ + public DBHistory getDBHistory() { return _dbHistory; } + public void setDBHistory(DBHistory hist) { _dbHistory = hist; } + + /** how large successfully sent messages are, calculated over a 1 minute, 1 hour, and 1 day period */ + public RateStat getSendSuccessSize() { return _sendSuccessSize; } + /** how large messages that could not be sent were, calculated over a 1 minute, 1 hour, and 1 day period */ + public RateStat getSendFailureSize() { return _sendFailureSize; } + /** how large received messages are, calculated over a 1 minute, 1 hour, and 1 day period */ + public RateStat getReceiveSize() { return _receiveSize; } + /** how long it takes to get a db response from the peer (in milliseconds), calculated over a 1 minute, 1 hour, and 1 day period */ + public RateStat getDbResponseTime() { return _dbResponseTime; } + /** how long it takes to get a tunnel create response from the peer (in milliseconds), calculated over a 1 minute, 1 hour, and 1 day period */ + public RateStat getTunnelCreateResponseTime() { return _tunnelCreateResponseTime; } + /** how long between communication errors with the peer (e.g. disconnection), calculated over a 1 minute, 1 hour, and 1 day period */ + public RateStat getCommError() { return _commError; } + /** how many new peers we get from dbSearchReplyMessages or dbStore messages, calculated over a 1 hour, 1 day, and 1 week period */ + public RateStat getDbIntroduction() { return _dbIntroduction; } + + /** + * extra factor added to the speed ranking - this can be updated in the profile + * written to disk to affect how the algorithm ranks speed. Negative values are + * penalties + */ + public long getSpeedBonus() { return _speedBonus; } + public void setSpeedBonus(long bonus) { _speedBonus = bonus; } + + /** + * extra factor added to the reliability ranking - this can be updated in the profile + * written to disk to affect how the algorithm ranks reliability. Negative values are + * penalties + */ + public long getReliabilityBonus() { return _reliabilityBonus; } + public void setReliabilityBonus(long bonus) { _reliabilityBonus = bonus; } + + /** + * extra factor added to the integration ranking - this can be updated in the profile + * written to disk to affect how the algorithm ranks integration. Negative values are + * penalties + */ + public long getIntegrationBonus() { return _integrationBonus; } + public void setIntegrationBonus(long bonus) { _integrationBonus = bonus; } + + /** + * How fast is the peer, taking into consideration both throughput and latency. + * This may even be made to take into consideration current rates vs. estimated + * (or measured) max rates, allowing this speed to reflect the speed /available/. + * + */ + public double getSpeedValue() { return _speedValue; } + /** + * How likely are they to stay up and pass on messages over the next few minutes? + * Positive numbers means more likely, negative numbers means its probably not + * even worth trying. + * + */ + public double getReliabilityValue() { return _reliabilityValue; } + /** + * How well integrated into the network is this peer (as measured by how much they've + * told us that we didn't already know). Higher numbers means better integrated + * + */ + public double getIntegrationValue() { return _integrationValue; } + /** + * is this peer actively failing (aka not worth touching)? + */ + public boolean getIsFailing() { return _isFailing; } + + /** + * when the given peer is performing so poorly that we don't want to bother keeping + * extensive stats on them, call this to discard excess data points. Specifically, + * this drops the rates, the tunnelHistory, and the dbHistory. + * + */ + public void shrinkProfile() { + _sendSuccessSize = null; + _sendFailureSize = null; + _receiveSize = null; + _dbResponseTime = null; + _tunnelCreateResponseTime = null; + _commError = null; + _dbIntroduction = null; + _tunnelHistory = null; + _dbHistory = null; + + _expanded = false; + } + + /** + * When the given peer is performing well enough that we want to keep detailed + * stats on them again, call this to set up the info we dropped during shrinkProfile. + * This will not however overwrite any existing data, so it can be safely called + * repeatedly + * + */ + public void expandProfile() { + if (_sendSuccessSize == null) + _sendSuccessSize = new RateStat("sendSuccessSize", "How large successfully sent messages are", "profile", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + if (_sendFailureSize == null) + _sendFailureSize = new RateStat("sendFailureSize", "How large messages that could not be sent were", "profile", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000 } ); + if (_receiveSize == null) + _receiveSize = new RateStat("receiveSize", "How large received messages are", "profile", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000 } ); + if (_dbResponseTime == null) + _dbResponseTime = new RateStat("dbResponseTime", "how long it takes to get a db response from the peer (in milliseconds)", "profile", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000 } ); + if (_tunnelCreateResponseTime == null) + _tunnelCreateResponseTime = new RateStat("tunnelCreateResponseTime", "how long it takes to get a tunnel create response from the peer (in milliseconds)", "profile", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000 } ); + if (_commError == null) + _commError = new RateStat("commErrorRate", "how long between communication errors with the peer (e.g. disconnection)", "profile", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000 } ); + if (_dbIntroduction == null) + _dbIntroduction = new RateStat("dbIntroduction", "how many new peers we get from dbSearchReplyMessages or dbStore messages", "profile", new long[] { 60*60*1000l, 24*60*60*1000l, 7*24*60*60*1000l }); + + if (_tunnelHistory == null) + _tunnelHistory = new TunnelHistory(); + if (_dbHistory == null) + _dbHistory = new DBHistory(); + + _expanded = true; + } + + /** update the stats and rates (this should be called once a minute) */ + public void coallesceStats() { + if (!_expanded) return; + _commError.coallesceStats(); + _dbIntroduction.coallesceStats(); + _dbResponseTime.coallesceStats(); + _receiveSize.coallesceStats(); + _sendFailureSize.coallesceStats(); + _sendSuccessSize.coallesceStats(); + _tunnelCreateResponseTime.coallesceStats(); + _dbHistory.coallesceStats(); + + _speedValue = calculateSpeed(); + _reliabilityValue = calculateReliability(); + _integrationValue = calculateIntegration(); + _isFailing = calculateIsFailing(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Coallesced: speed [" + _speedValue + "] reliability [" + _reliabilityValue + "] integration [" + _integrationValue + "] failing? [" + _isFailing + "]"); + } + + private double calculateSpeed() { return Calculator.getSpeedCalculator().calc(this); } + private double calculateReliability() { return Calculator.getReliabilityCalculator().calc(this); } + private double calculateIntegration() { return Calculator.getIntegrationCalculator().calc(this); } + private boolean calculateIsFailing() { return Calculator.getIsFailingCalculator().calcBoolean(this); } + void setIsFailing(boolean val) { _isFailing = val; } + + public int hashCode() { return (_peer == null ? 0 : _peer.hashCode()); } + public boolean equals(Object obj) { + if (obj == null) return false; + if (obj.getClass() != PeerProfile.class) return false; + if (_peer == null) return false; + PeerProfile prof = (PeerProfile)obj; + return _peer.equals(prof.getPeer()); + } + public String toString() { return "Profile: " + getPeer().toBase64(); } + + /** + * Calculate the memory consumption of profiles. Measured to be ~3739 bytes + * for an expanded profile, and ~212 bytes for a compacted one. + * + */ + public static void main(String args[]) { + testProfileSize(100, 0); // 560KB + testProfileSize(1000, 0); // 3.9MB + testProfileSize(10000, 0); // 37MB + testProfileSize(0, 10000); // 2.2MB + testProfileSize(0, 100000); // 21MB + testProfileSize(0, 300000); // 63MB + } + + private static void testProfileSize(int numExpanded, int numCompact) { + Runtime.getRuntime().gc(); + PeerProfile profs[] = new PeerProfile[numExpanded]; + PeerProfile profsCompact[] = new PeerProfile[numCompact]; + long used = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); + long usedPer = used / (numExpanded+numCompact); + System.out.println(numExpanded + "/" + numCompact + ": create array - Used: " + used + " bytes (or " + usedPer + " bytes per array entry)"); + + int i = 0; + int j = 0; + try { + for (; i < numExpanded; i++) + profs[i] = new PeerProfile(new Hash(new byte[Hash.HASH_LENGTH])); + } catch (OutOfMemoryError oom) { + profs = null; + profsCompact = null; + Runtime.getRuntime().gc(); + System.out.println("Ran out of memory when creating profile " + i); + return; + } + try { + for (; i < numCompact; i++) + profsCompact[i] = new PeerProfile(new Hash(new byte[Hash.HASH_LENGTH]), false); + } catch (OutOfMemoryError oom) { + profs = null; + profsCompact = null; + Runtime.getRuntime().gc(); + System.out.println("Ran out of memory when creating compacted profile " + i); + return; + } + + Runtime.getRuntime().gc(); + long usedObjects = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); + usedPer = usedObjects / (numExpanded+numCompact); + System.out.println(numExpanded + "/" + numCompact + ": create objects - Used: " + usedObjects + " bytes (or " + usedPer + " bytes per profile)"); + profs = null; + profsCompact = null; + Runtime.getRuntime().gc(); + } +} diff --git a/router/java/src/net/i2p/router/peermanager/PersistProfilesJob.java b/router/java/src/net/i2p/router/peermanager/PersistProfilesJob.java new file mode 100644 index 0000000000..9e90c8d9b5 --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/PersistProfilesJob.java @@ -0,0 +1,53 @@ +package net.i2p.router.peermanager; + +import java.util.Set; +import java.util.Iterator; + +import net.i2p.data.Hash; +import net.i2p.util.Clock; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; + +class PersistProfilesJob extends JobImpl { + private PeerManager _mgr; + private final static long PERSIST_DELAY = 10*60*1000; + + public PersistProfilesJob(PeerManager mgr) { + _mgr = mgr; + getTiming().setStartAfter(Clock.getInstance().now() + PERSIST_DELAY); + } + + public String getName() { return "Persist profiles"; } + public void runJob() { + Set peers = _mgr.selectPeers(); + Hash hashes[] = new Hash[peers.size()]; + int i = 0; + for (Iterator iter = peers.iterator(); iter.hasNext(); ) + hashes[i] = (Hash)iter.next(); + JobQueue.getInstance().addJob(new PersistProfileJob(hashes)); + } + + private class PersistProfileJob extends JobImpl { + private Hash _peers[]; + private int _cur; + public PersistProfileJob(Hash peers[]) { + _peers = peers; + _cur = 0; + } + public void runJob() { + if (_cur < _peers.length) { + _mgr.storeProfile(_peers[_cur]); + _cur++; + } + if (_cur >= _peers.length) { + // no more left, requeue up the main persist-em-all job + PersistProfilesJob.this.getTiming().setStartAfter(Clock.getInstance().now() + PERSIST_DELAY); + JobQueue.getInstance().addJob(PersistProfilesJob.this); + } else { + // we've got peers left to persist, so requeue the persist profile job + JobQueue.getInstance().addJob(PersistProfileJob.this); + } + } + public String getName() { return "Persist profile"; } + } +} diff --git a/router/java/src/net/i2p/router/peermanager/ProfileManagerImpl.java b/router/java/src/net/i2p/router/peermanager/ProfileManagerImpl.java new file mode 100644 index 0000000000..7e7fd9c3ad --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/ProfileManagerImpl.java @@ -0,0 +1,289 @@ +package net.i2p.router.peermanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import net.i2p.data.Hash; +import net.i2p.router.ProfileManager; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +public class ProfileManagerImpl extends ProfileManager { + private final static Log _log = new Log(ProfileManagerImpl.class); + public ProfileManagerImpl() {} + + + /** is this peer failing or already dropped? */ + public boolean isFailing(Hash peer) { + PeerProfile prof = getProfile(peer); + if (prof == null) + return true; + else + return prof.getIsFailing(); + } + + /** + * Note that it took msToSend to send a message of size bytesSent to the peer over the transport. + * This should only be called if the transport considered the send successful. + * + */ + public void messageSent(Hash peer, String transport, long msToSend, long bytesSent) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastSendSuccessful(Clock.getInstance().now()); + data.getSendSuccessSize().addData(bytesSent, msToSend); + } + + /** + * Note that the router failed to send a message to the peer over the transport specified + * + */ + public void messageFailed(Hash peer, String transport) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastSendFailed(Clock.getInstance().now()); + data.getSendFailureSize().addData(0, 0); // yeah, should be a frequency... + } + + /** + * Note that the router failed to send a message to the peer over any transport + * + */ + public void messageFailed(Hash peer) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastSendFailed(Clock.getInstance().now()); + data.getSendFailureSize().addData(0, 0); // yeah, should be a frequency... + } + + /** + * Note that there was some sort of communication error talking with the peer + * + */ + public void commErrorOccurred(Hash peer) { + if (_log.shouldLog(Log.INFO)) + _log.info("Comm error occurred for peer " + peer.toBase64(), new Exception("Comm error")); + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastSendFailed(Clock.getInstance().now()); + data.getSendFailureSize().addData(0, 0); // yeah, should be a frequency... + data.getCommError().addData(0, 0); // see above + } + + /** + * Note that the router agreed to participate in a tunnel + * + */ + public void tunnelJoined(Hash peer, long responseTimeMs) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.getTunnelCreateResponseTime().addData(responseTimeMs, responseTimeMs); + data.setLastHeardFrom(Clock.getInstance().now()); + data.getTunnelHistory().incrementAgreedTo(); + } + + /** + * Note that a router explicitly rejected joining a tunnel + * + */ + public void tunnelRejected(Hash peer, long responseTimeMs) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastHeardFrom(Clock.getInstance().now()); + data.getTunnelHistory().incrementRejected(); + } + + /** + * Note that the peer participated in a tunnel that failed. Its failure may not have + * been the peer's fault however. + * + */ + public void tunnelFailed(Hash peer) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastHeardFrom(Clock.getInstance().now()); + data.getTunnelHistory().incrementFailed(); + } + + /** + * Note that the peer was able to return the valid data for a db lookup + * + */ + public void dbLookupSuccessful(Hash peer, long responseTimeMs) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastHeardFrom(Clock.getInstance().now()); + data.getDbResponseTime().addData(responseTimeMs, responseTimeMs); + DBHistory hist = data.getDBHistory(); + hist.lookupSuccessful(); + } + + /** + * Note that the peer was unable to reply to a db lookup - either with data or with + * a lookupReply redirecting the user elsewhere + * + */ + public void dbLookupFailed(Hash peer) { + PeerProfile data = getProfile(peer); + if (data == null) return; + DBHistory hist = data.getDBHistory(); + hist.lookupFailed(); + } + + /** + * Note that the peer replied to a db lookup with a redirect to other routers, where + * the list of redirected users included newPeers routers that the local router didn't + * know about, oldPeers routers that the local router already knew about, the given invalid + * routers that were invalid in some way, and the duplicate number of routers that we explicitly + * asked them not to send us, but they did anyway + * + */ + public void dbLookupReply(Hash peer, int newPeers, int oldPeers, int invalid, int duplicate, long responseTimeMs) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastHeardFrom(Clock.getInstance().now()); + data.getDbResponseTime().addData(responseTimeMs, responseTimeMs); + data.getDbIntroduction().addData(newPeers, responseTimeMs); + DBHistory hist = data.getDBHistory(); + hist.lookupReply(newPeers, oldPeers, invalid, duplicate); + } + + /** + * Note that the local router received a db lookup from the given peer + * + */ + public void dbLookupReceived(Hash peer) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastHeardFrom(Clock.getInstance().now()); + DBHistory hist = data.getDBHistory(); + hist.lookupReceived(); + } + + /** + * Note that the local router received an unprompted db store from the given peer + * + */ + public void dbStoreReceived(Hash peer, boolean wasNewKey) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastHeardFrom(Clock.getInstance().now()); + DBHistory hist = data.getDBHistory(); + hist.unpromptedStoreReceived(wasNewKey); + } + + /** + * Note that we've confirmed a successful send of db data to the peer (though we haven't + * necessarily requested it again from them, so they /might/ be lying) + * + */ + public void dbStoreSent(Hash peer, long responseTimeMs) { + PeerProfile data = getProfile(peer); + if (data == null) return; + long now = Clock.getInstance().now(); + data.setLastSendSuccessful(now); + data.setLastHeardFrom(now); + // we could do things like update some sort of "how many successful stores we've sent them"... + // naah.. dont really care now + } + + /** + * Note that we were unable to confirm a successful send of db data to + * the peer, at least not within our timeout period + * + */ + public void dbStoreFailed(Hash peer) { + // we could do things like update some sort of "how many successful stores we've + // failed to send them"... + } + + /** + * Note that the local router received a reference to the given peer, either + * through an explicit dbStore or in a dbLookupReply + */ + public void heardAbout(Hash peer) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastHeardAbout(Clock.getInstance().now()); + } + + /** + * Note that the router received a message from the given peer on the specified + * transport. Messages received without any "from" information aren't recorded + * through this metric. If msToReceive is negative, there was no timing information + * available + * + */ + public void messageReceived(Hash peer, String style, long msToReceive, int bytesRead) { + PeerProfile data = getProfile(peer); + if (data == null) return; + data.setLastHeardFrom(Clock.getInstance().now()); + data.getReceiveSize().addData(bytesRead, msToReceive); + } + + private PeerProfile getProfile(Hash peer) { + PeerProfile prof = ProfileOrganizer.getInstance().getProfile(peer); + if (prof == null) { + prof = new PeerProfile(peer); + ProfileOrganizer.getInstance().addProfile(prof); + } + return prof; + } + + + /** provide a simple summary of a number of peers, suitable for publication in the netDb */ + public Properties summarizePeers(int numPeers) { + Set peers = new HashSet(numPeers); + // lets get the fastest ones we've got (this fails over to include just plain reliable, + // or even notFailing peers if there aren't enough fast ones) + ProfileOrganizer.getInstance().selectFastAndReliablePeers(numPeers, null, peers); + Properties props = new Properties(); + for (Iterator iter = peers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + PeerProfile prof = getProfile(peer); + if (prof == null) continue; + + StringBuffer buf = new StringBuffer(64); + + buf.append("status: "); + if (ProfileOrganizer.getInstance().isFastAndReliable(peer)) { + buf.append("fastReliable"); + } else if (ProfileOrganizer.getInstance().isReliable(peer)) { + buf.append("reliable"); + } else if (ProfileOrganizer.getInstance().isFailing(peer)) { + buf.append("failing"); + } else { + buf.append("notFailing"); + } + + if (ProfileOrganizer.getInstance().isWellIntegrated(peer)) + buf.append("Integrated "); + else + buf.append(" "); + + buf.append("reliability: ").append(num(prof.getReliabilityValue())).append(" "); + buf.append("speed: ").append(num(prof.getSpeedValue())).append(" "); + buf.append("integration: ").append(num(prof.getIntegrationValue())); + + props.setProperty("profile." + peer.toBase64().replace('=', '_'), buf.toString()); + } + return props; + } + + private final static DecimalFormat _fmt = new DecimalFormat("##0.00", new DecimalFormatSymbols(Locale.UK)); + private final static String num(double val) { + synchronized (_fmt) { return _fmt.format(val); } + } +} diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java new file mode 100644 index 0000000000..b64a3a655a --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java @@ -0,0 +1,625 @@ +package net.i2p.router.peermanager; + +import net.i2p.data.Hash; +import net.i2p.data.DataHelper; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Keep the peer profiles organized according to the tiered model. This does not + * actively update anything - the reorganize() method should be called periodically + * to recalculate thresholds and move profiles into the appropriate tiers, and addProfile() + * should be used to add new profiles (placing them into the appropriate groupings). + */ +public class ProfileOrganizer { + private final static Log _log = new Log(ProfileOrganizer.class); + private final static ProfileOrganizer _instance = new ProfileOrganizer(); + final static ProfileOrganizer getInstance() { return _instance; } + /** This data should not be exposed */ + public static final ProfileOrganizer _getInstance() { return _instance; } + /** H(routerIdentity) to PeerProfile for all peers that are fast and reliable */ + private Map _fastAndReliablePeers; + /** H(routerIdentity) to PeerProfile for all peers that are reliable */ + private Map _reliablePeers; + /** H(routerIdentity) to PeerProfile for all peers that well integrated into the network and not failing horribly */ + private Map _wellIntegratedPeers; + /** H(routerIdentity) to PeerProfile for all peers that are not failing horribly */ + private Map _notFailingPeers; + /** H(routerIdentity) to PeerProfile for all peers that ARE failing horribly (but that we haven't dropped reference to yet) */ + private Map _failingPeers; + /** who are we? */ + private Hash _us; + + /** PeerProfile objects for all peers profiled, orderd by most reliable first */ + private Set _strictReliabilityOrder; + + /** threshold speed value, seperating fast from slow */ + private double _thresholdSpeedValue; + /** threshold reliability value, seperating reliable from unreliable */ + private double _thresholdReliabilityValue; + /** integration value, seperating well integrated from not well integrated */ + private double _thresholdIntegrationValue; + + /** synchronized against this lock when updating the tier that peers are located in (and when fetching them from a peer) */ + private Object _reorganizeLock = new Object(); + + /** incredibly weak PRNG, just used for shuffling peers. no need to waste the real PRNG on this */ + private Random _random = new Random(); + + private ProfileOrganizer() { + _fastAndReliablePeers = new HashMap(64); + _reliablePeers = new HashMap(512); + _wellIntegratedPeers = new HashMap(256); + _notFailingPeers = new HashMap(1024); + _failingPeers = new HashMap(4096); + _strictReliabilityOrder = new TreeSet(new InverseReliabilityComparator()); + _thresholdSpeedValue = 0.0d; + _thresholdReliabilityValue = 0.0d; + _thresholdIntegrationValue = 0.0d; + } + + /** + * Order profiles by their reliability, but backwards (most reliable / highest value first). + * + */ + private static final class InverseReliabilityComparator implements Comparator { + private static final Comparator _comparator = new InverseReliabilityComparator(); + public int compare(Object lhs, Object rhs) { + if ( (lhs == null) || (rhs == null) || (!(lhs instanceof PeerProfile)) || (!(rhs instanceof PeerProfile)) ) + throw new ClassCastException("Only profiles can be compared - lhs = " + lhs + " rhs = " + rhs); + PeerProfile left = (PeerProfile)lhs; + PeerProfile right= (PeerProfile)rhs; + // note below that yes, we are treating left and right backwards. see: classname + int diff = (int)(right.getReliabilityValue() - left.getReliabilityValue()); + // we can't just return that, since the set would b0rk on equal values (just because two profiles + // rank the same way doesn't mean they're the same peer!) So if they reliabilities are equal, we + // order them by the peer's hash + if (diff != 0) + return diff; + if (left.getPeer().equals(right.getPeer())) + return 0; + else + return DataHelper.compareTo(right.getPeer().getData(), left.getPeer().getData()); + } + } + + public void setUs(Hash us) { _us = us; } + + /** + * Retrieve the profile for the given peer, if one exists (else null) + * + */ + public PeerProfile getProfile(Hash peer) { + synchronized (_reorganizeLock) { + return locked_getProfile(peer); + } + } + + /** + * Add the new profile, returning the old value (or null if no profile existed) + * + */ + public PeerProfile addProfile(PeerProfile profile) { + if ( (profile == null) || (profile.getPeer() == null) || (_us.equals(profile.getPeer())) ) return null; + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("New profile created for " + profile.getPeer().toBase64()); + + synchronized (_reorganizeLock) { + PeerProfile old = locked_getProfile(profile.getPeer()); + profile.coallesceStats(); + locked_placeProfile(profile); + _strictReliabilityOrder.add(profile); + return old; + } + } + + public int countFastAndReliablePeers() { synchronized (_reorganizeLock) { return _fastAndReliablePeers.size(); } } + public int countReliablePeers() { synchronized (_reorganizeLock) { return _reliablePeers.size(); } } + public int countWellIntegratedPeers() { synchronized (_reorganizeLock) { return _wellIntegratedPeers.size(); } } + public int countNotFailingPeers() { synchronized (_reorganizeLock) { return _notFailingPeers.size(); } } + public int countFailingPeers() { synchronized (_reorganizeLock) { return _failingPeers.size(); } } + + public boolean isFastAndReliable(Hash peer) { synchronized (_reorganizeLock) { return _fastAndReliablePeers.containsKey(peer); } } + public boolean isReliable(Hash peer) { synchronized (_reorganizeLock) { return _reliablePeers.containsKey(peer); } } + public boolean isWellIntegrated(Hash peer) { synchronized (_reorganizeLock) { return _wellIntegratedPeers.containsKey(peer); } } + public boolean isFailing(Hash peer) { synchronized (_reorganizeLock) { return _failingPeers.containsKey(peer); } } + + /** + * Return a set of Hashes for peers that are both fast and reliable. If an insufficient + * number of peers are both fast and reliable, fall back onto reliable peers, and if reliable + * peers doesn't contain sufficient peers, fall back onto not failing peers, and even THAT doesn't + * have sufficient peers, fall back onto failing peers. + * + * @param howMany how many peers are desired + * @param exclude set of Hashes for routers that we don't want selected + * @param matches set to store the return value in + * + */ + public void selectFastAndReliablePeers(int howMany, Set exclude, Set matches) { + synchronized (_reorganizeLock) { + locked_selectPeers(_fastAndReliablePeers, howMany, exclude, matches); + } + if (matches.size() < howMany) + selectReliablePeers(howMany, exclude, matches); + return; + } + + /** + * Return a set of Hashes for peers that are reliable. + * + */ + public void selectReliablePeers(int howMany, Set exclude, Set matches) { + synchronized (_reorganizeLock) { + locked_selectPeers(_reliablePeers, howMany, exclude, matches); + } + if (matches.size() < howMany) + selectNotFailingPeers(howMany, exclude, matches); + return; + } + /** + * Return a set of Hashes for peers that are well integrated into the network. + * + */ + public void selectWellIntegratedPeers(int howMany, Set exclude, Set matches) { + synchronized (_reorganizeLock) { + locked_selectPeers(_wellIntegratedPeers, howMany, exclude, matches); + } + if (matches.size() < howMany) + selectNotFailingPeers(howMany, exclude, matches); + return; + } + /** + * Return a set of Hashes for peers that are not failing, preferring ones that + * we are already talking with + * + */ + public void selectNotFailingPeers(int howMany, Set exclude, Set matches) { + if (matches.size() < howMany) + selectActiveNotFailingPeers(howMany, exclude, matches); + return; + } + /** + * Return a set of Hashes for peers that are both not failing and we're actively + * talking with. + * + */ + private void selectActiveNotFailingPeers(int howMany, Set exclude, Set matches) { + if (true) { + selectAllNotFailingPeers(howMany, exclude, matches); + return; + } + // pick out the not-failing peers that we're actively talking with + if (matches.size() < howMany) { + synchronized (_reorganizeLock) { + for (Iterator iter = _notFailingPeers.keySet().iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + if ( (exclude != null) && exclude.contains(peer) ) continue; + if (matches.contains(peer)) continue; + PeerProfile prof = (PeerProfile)_notFailingPeers.get(peer); + if (prof.getIsActive()) + matches.add(peer); + if (matches.size() >= howMany) + return; + } + } + } + // ok, still not enough, pick out the not-failing peers that we aren't talking with + if (matches.size() < howMany) + selectAllNotFailingPeers(howMany, exclude, matches); + return; + } + /** + * Return a set of Hashes for peers that are not failing. + * + */ + private void selectAllNotFailingPeers(int howMany, Set exclude, Set matches) { + if (matches.size() < howMany) { + int orig = matches.size(); + int needed = howMany - orig; + List selected = new ArrayList(needed); + synchronized (_reorganizeLock) { + for (Iterator iter = _strictReliabilityOrder.iterator(); selected.size() < needed && iter.hasNext(); ) { + PeerProfile prof = (PeerProfile)iter.next(); + if (matches.contains(prof.getPeer()) || + (exclude != null && exclude.contains(prof.getPeer())) || + _failingPeers.containsKey(prof.getPeer())) + continue; + else + selected.add(prof.getPeer()); + } + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Selecting all not failing found " + (matches.size()-orig) + " new peers: " + selected); + matches.addAll(selected); + } + if (matches.size() < howMany) + selectFailingPeers(howMany, exclude, matches); + return; + } + /** + * I'm not quite sure why you'd want this... (other than for failover from the better results) + * + */ + public void selectFailingPeers(int howMany, Set exclude, Set matches) { + synchronized (_reorganizeLock) { + locked_selectPeers(_failingPeers, howMany, exclude, matches); + } + return; + } + + /** + * Find the hashes for all peers we are actively profiling + * + */ + public Set selectAllPeers() { + synchronized (_reorganizeLock) { + Set allPeers = new HashSet(_failingPeers.size() + _notFailingPeers.size() + _reliablePeers.size() + _fastAndReliablePeers.size()); + allPeers.addAll(_failingPeers.keySet()); + allPeers.addAll(_notFailingPeers.keySet()); + allPeers.addAll(_reliablePeers.keySet()); + allPeers.addAll(_fastAndReliablePeers.keySet()); + return allPeers; + } + } + + /** + * Place peers into the correct tier, as well as expand/contract and even drop profiles + * according to whatever limits are in place. Peer profiles are not coallesced during + * this method, but the averages are recalculated. + * + */ + public void reorganize() { + synchronized (_reorganizeLock) { + Set allPeers = new HashSet(_failingPeers.size() + _notFailingPeers.size() + _reliablePeers.size() + _fastAndReliablePeers.size()); + allPeers.addAll(_failingPeers.values()); + allPeers.addAll(_notFailingPeers.values()); + allPeers.addAll(_reliablePeers.values()); + allPeers.addAll(_fastAndReliablePeers.values()); + + _failingPeers.clear(); + _notFailingPeers.clear(); + _reliablePeers.clear(); + _fastAndReliablePeers.clear(); + + calculateThresholds(allPeers); + + for (Iterator iter = allPeers.iterator(); iter.hasNext(); ) { + PeerProfile profile = (PeerProfile)iter.next(); + locked_placeProfile(profile); + } + + Set reordered = new TreeSet(InverseReliabilityComparator._comparator); + reordered.addAll(_strictReliabilityOrder); + + _strictReliabilityOrder = reordered; + + locked_unfailAsNecessary(); + } + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("Profiles reorganized. averages: [integration: " + _thresholdIntegrationValue + ", reliability: " + _thresholdReliabilityValue + ", speed: " + _thresholdSpeedValue + "]"); + _log.debug("Strictly organized: " + _strictReliabilityOrder); + } + } + + /** how many not failing/active peers must we have? */ + private final static int MIN_NOT_FAILING_ACTIVE = 3; + /** + * I'm not sure how much I dislike the following - if there aren't enough + * active and not-failing peers, pick the most reliable active peers and + * override their 'failing' flag, resorting them into the not-failing buckets + * + */ + private void locked_unfailAsNecessary() { + int notFailingActive = 0; + for (Iterator iter = _notFailingPeers.keySet().iterator(); iter.hasNext(); ) { + Hash key = (Hash)iter.next(); + PeerProfile peer = (PeerProfile)_notFailingPeers.get(key); + if (peer.getIsActive()) + notFailingActive++; + if (notFailingActive >= MIN_NOT_FAILING_ACTIVE) { + // we've got enough, no need to try further + return; + } + } + + // we dont have enough, lets unfail our best ones remaining + int needToUnfail = MIN_NOT_FAILING_ACTIVE - notFailingActive; + if (needToUnfail > 0) { + int unfailed = 0; + for (Iterator iter = _strictReliabilityOrder.iterator(); iter.hasNext(); ) { + PeerProfile best = (PeerProfile)iter.next(); + if ( (best.getIsActive()) && (best.getIsFailing()) ) { + if (_log.shouldLog(Log.WARN)) + _log.warn("All peers were failing, so we have overridden the failing flag for one of the most reliable active peers (" + best.getPeer().toBase64() + ")"); + best.setIsFailing(false); + locked_placeProfile(best); + unfailed++; + } + if (unfailed >= needToUnfail) + break; + } + } + } + + //////// + // no more public stuff below + //////// + + /** + * Update the thresholds based on the profiles in this set. currently + * implements the thresholds based on a simple average (ignoring failing values), + * with integration and speed being directly equal to the simple average as + * calculated over all reliable and active non-failing peers, while the reliability threshold + * is half the simple average of active non-failing peers. Lots of room to tune this. + * should this instead be top 10%? top 90%? top 50? etc + * + */ + private void calculateThresholds(Set allPeers) { + double totalReliability = 0; + int numActive = 0; + + for (Iterator iter = allPeers.iterator(); iter.hasNext(); ) { + PeerProfile profile = (PeerProfile)iter.next(); + + if (_us.equals(profile.getPeer())) continue; + + // only take into account peers that we're talking to within the last + // few minutes + if ( (!profile.getIsActive()) || (profile.getIsFailing()) ) + continue; + + numActive++; + + if (profile.getReliabilityValue() > 0) + totalReliability += profile.getReliabilityValue(); + } + _thresholdReliabilityValue = 0.5d * avg(totalReliability, numActive); + + // now derive the integration and speed thresholds based ONLY on the reliable + // and active peers + numActive = 0; + double totalIntegration = 0; + double totalSpeed = 0; + + for (Iterator iter = allPeers.iterator(); iter.hasNext(); ) { + PeerProfile profile = (PeerProfile)iter.next(); + + if (_us.equals(profile.getPeer())) continue; + + // only take into account peers that we're talking to within the last + // few minutes, who are reliable, AND who are not failing + if ( (!profile.getIsActive()) || (profile.getReliabilityValue() < _thresholdReliabilityValue) || (profile.getIsFailing()) ) + continue; + + numActive++; + + if (profile.getIntegrationValue() > 0) + totalIntegration += profile.getIntegrationValue(); + if (profile.getSpeedValue() > 0) + totalSpeed += profile.getSpeedValue(); + } + + + _thresholdIntegrationValue = 1.0d * avg(totalIntegration, numActive); + _thresholdSpeedValue = 1.0d * avg(totalSpeed, numActive); + } + + /** simple average, or 0 if NaN */ + private final static double avg(double total, double quantity) { + if ( (total > 0) && (quantity > 0) ) + return total/quantity; + else + return 0.0d; + } + + /** called after locking the reorganizeLock */ + private PeerProfile locked_getProfile(Hash peer) { + if (_notFailingPeers.containsKey(peer)) + return (PeerProfile)_notFailingPeers.get(peer); + else if (_failingPeers.containsKey(peer)) + return (PeerProfile)_failingPeers.get(peer); + else + return null; + } + + /** + * Select peers from the peer mapping, excluding appropriately and increasing the + * matches set until it has howMany elements in it. + * + */ + private void locked_selectPeers(Map peers, int howMany, Set toExclude, Set matches) { + List all = new ArrayList(peers.keySet()); + if (toExclude != null) + all.removeAll(toExclude); + all.removeAll(matches); + all.remove(_us); + howMany -= matches.size(); + Collections.shuffle(all, _random); + Set rv = new HashSet(howMany); + for (int i = 0; i < howMany && i < all.size(); i++) { + rv.add(all.get(i)); + } + matches.addAll(rv); + } + + /** + * called after locking the reorganizeLock, place the profile in the appropriate tier. + * This is where we implement the (betterThanAverage ? goToPierX : goToPierY) algorithms + * + */ + private void locked_placeProfile(PeerProfile profile) { + if (profile.getIsFailing()) { + if (!shouldDrop(profile)) + _failingPeers.put(profile.getPeer(), profile); + _fastAndReliablePeers.remove(profile.getPeer()); + _reliablePeers.remove(profile.getPeer()); + _wellIntegratedPeers.remove(profile.getPeer()); + _notFailingPeers.remove(profile.getPeer()); + } else { + _failingPeers.remove(profile.getPeer()); + _fastAndReliablePeers.remove(profile.getPeer()); + _reliablePeers.remove(profile.getPeer()); + _wellIntegratedPeers.remove(profile.getPeer()); + + _notFailingPeers.put(profile.getPeer(), profile); + if (_thresholdReliabilityValue <= profile.getReliabilityValue()) { + _reliablePeers.put(profile.getPeer(), profile); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Reliable: \t" + profile.getPeer().toBase64()); + if (_thresholdSpeedValue <= profile.getSpeedValue()) { + _fastAndReliablePeers.put(profile.getPeer(), profile); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Fast: \t" + profile.getPeer().toBase64()); + } + + if (_thresholdIntegrationValue <= profile.getIntegrationValue()) { + _wellIntegratedPeers.put(profile.getPeer(), profile); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Integrated: \t" + profile.getPeer().toBase64()); + } + } else { + // not reliable, but not failing (yet) + } + } + } + + /** + * This is where we determine whether a failing peer is so poor and we're so overloaded + * that we just want to forget they exist. This algorithm won't need to be implemented until + * after I2P 1.0, most likely, since we should be able to handle thousands of peers profiled + * without ejecting any of them, but anyway, this is how we'd do it. Most likely. + * + */ + private boolean shouldDrop(PeerProfile profile) { return false; } + + public void exportProfile(Hash profile, OutputStream out) throws IOException { + PeerProfile prof = getProfile(profile); + if (prof != null) + ProfilePersistenceHelper.getInstance().writeProfile(prof, out); + } + + public String renderStatusHTML() { + Set peers = selectAllPeers(); + + long hideBefore = Clock.getInstance().now() - 6*60*60*1000; + + TreeMap order = new TreeMap(); + for (Iterator iter = peers.iterator(); iter.hasNext();) { + Hash peer = (Hash)iter.next(); + if (_us.equals(peer)) continue; + PeerProfile prof = getProfile(peer); + if (prof.getLastSendSuccessful() <= hideBefore) continue; + order.put(peer.toBase64(), prof); + } + + int fast = 0; + int reliable = 0; + int integrated = 0; + int failing = 0; + StringBuffer buf = new StringBuffer(8*1024); + buf.append("

Peer Profiles

\n"); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + for (Iterator iter = order.keySet().iterator(); iter.hasNext();) { + String name = (String)iter.next(); + PeerProfile prof = (PeerProfile)order.get(name); + Hash peer = prof.getPeer(); + + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append(""); + } + buf.append("
Peer (").append(order.size()).append(", hiding ").append(peers.size()-order.size()).append(" inactive ones)GroupsSpeedReliabilityIntegrationFailing?Profile data
"); + if (prof.getIsFailing()) { + buf.append("--").append(peer.toBase64()).append(""); + } else { + if (prof.getIsActive()) { + buf.append("++").append(peer.toBase64()).append(""); + } else { + buf.append("__").append(peer.toBase64()); + } + } + buf.append(""); + int tier = 0; + boolean isIntegrated = false; + synchronized (_reorganizeLock) { + if (_fastAndReliablePeers.containsKey(peer)) { + tier = 1; + fast++; + reliable++; + } else if (_reliablePeers.containsKey(peer)) { + tier = 2; + reliable++; + } else if (_notFailingPeers.containsKey(peer)) { + tier = 3; + } else { + failing++; + } + + if (_wellIntegratedPeers.containsKey(peer)) { + isIntegrated = true; + integrated++; + } + } + + switch (tier) { + case 1: buf.append("Fast+Reliable"); break; + case 2: buf.append("Reliable"); break; + case 3: buf.append("Not Failing"); break; + default: buf.append("Failing"); break; + } + if (isIntegrated) buf.append(", Well integrated"); + + buf.append("").append(num(prof.getSpeedValue())).append("").append(num(prof.getReliabilityValue())).append("").append(num(prof.getIntegrationValue())).append("").append(prof.getIsFailing()).append("profile.txt "); + buf.append(" netDb
"); + buf.append("Note that the speed, reliability, and integration values are relative"); + buf.append(" - they do NOT correspond with any particular throughput, latency, uptime, "); + buf.append("or other metric. Higher numbers are better. "); + buf.append("Red peers prefixed with '--' means the peer is failing, and blue peers prefixed "); + buf.append("with '++' means we've sent or received a message from them "); + buf.append("in the last five minutes
"); + buf.append("Thresholds:
"); + buf.append("Speed: ").append(num(_thresholdSpeedValue)).append(" (").append(fast).append(" fast peers)
"); + buf.append("Reliability: ").append(num(_thresholdReliabilityValue)).append(" (").append(reliable).append(" reliable peers)
"); + buf.append("Integration: ").append(num(_thresholdIntegrationValue)).append(" (").append(integrated).append(" well integrated peers)
"); + return buf.toString(); + } + + private final static DecimalFormat _fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK)); + private final static String num(double num) { synchronized (_fmt) { return _fmt.format(num); } } +} diff --git a/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java new file mode 100644 index 0000000000..0a4ab9d728 --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java @@ -0,0 +1,276 @@ +package net.i2p.router.peermanager; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Properties; +import java.util.Set; + +import net.i2p.data.DataFormatException; +import net.i2p.data.Hash; +import net.i2p.router.Router; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +class ProfilePersistenceHelper { + private final static Log _log = new Log(ProfilePersistenceHelper.class); + private final static ProfilePersistenceHelper _instance = new ProfilePersistenceHelper(); + public final static ProfilePersistenceHelper getInstance() { return _instance; } + + public final static String PROP_PEER_PROFILE_DIR = "router.profileDir"; + public final static String DEFAULT_PEER_PROFILE_DIR = "peerProfiles"; + private final static String NL = System.getProperty("line.separator"); + + private File _profileDir = null; + private Hash _us; + + private ProfilePersistenceHelper() { + File profileDir = getProfileDir(); + _us = null; + if (!profileDir.exists()) { + profileDir.mkdirs(); + _log.info("Profile directory " + profileDir.getAbsolutePath() + " created"); + } + } + + public void setUs(Hash routerIdentHash) { _us = routerIdentHash; } + + /** write out the data from the profile to the stream */ + public void writeProfile(PeerProfile profile) { + File f = pickFile(profile); + long before = Clock.getInstance().now(); + OutputStream fos = null; + try { + fos = new BufferedOutputStream(new FileOutputStream(f)); + writeProfile(profile, fos); + } catch (IOException ioe) { + _log.error("Error writing profile to " + f); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + long delay = Clock.getInstance().now() - before; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Writing the profile to " + f.getName() + " took " + delay + "ms"); + } + /** write out the data from the profile to the stream */ + public void writeProfile(PeerProfile profile, OutputStream out) throws IOException { + String groups = null; + if (ProfileOrganizer.getInstance().isFailing(profile.getPeer())) { + groups = "failing"; + } else if (!ProfileOrganizer.getInstance().isReliable(profile.getPeer())) { + groups = "not failing"; + } else { + if (ProfileOrganizer.getInstance().isFastAndReliable(profile.getPeer())) + groups = "fast and reliable"; + else + groups = "reliable"; + + if (ProfileOrganizer.getInstance().isWellIntegrated(profile.getPeer())) + groups = groups + ", well integrated"; + } + + StringBuffer buf = new StringBuffer(512); + buf.append("########################################################################").append(NL); + buf.append("# profile for ").append(profile.getPeer().toBase64()).append(NL); + if (_us != null) + buf.append("# as calculated by ").append(_us.toBase64()).append(NL); + buf.append("#").append(NL); + buf.append("# reliability: ").append(profile.getReliabilityValue()).append(NL); + buf.append("# integration: ").append(profile.getIntegrationValue()).append(NL); + buf.append("# speedValue: ").append(profile.getSpeedValue()).append(NL); + buf.append("#").append(NL); + buf.append("# Groups: ").append(groups).append(NL); + buf.append("########################################################################").append(NL); + buf.append("##").append(NL); + buf.append("# Reliability bonus: used to affect the reliability score after all other calculations are done").append(NL); + buf.append("reliabilityBonus=").append(profile.getReliabilityBonus()).append(NL); + buf.append("# Integration bonus: used to affect the integration score after all other calculations are done").append(NL); + buf.append("integrationBonus=").append(profile.getIntegrationBonus()).append(NL); + buf.append("# Speed bonus: used to affect the speed score after all other calculations are done").append(NL); + buf.append("speedBonus=").append(profile.getSpeedBonus()).append(NL); + buf.append(NL).append(NL); + buf.append("# Last heard about: when did we last get a reference to this peer? (milliseconds since the epoch)").append(NL); + buf.append("lastHeardAbout=").append(profile.getLastHeardAbout()).append(NL); + buf.append("# First heard about: when did we first get a reference to this peer? (milliseconds since the epoch)").append(NL); + buf.append("firstHeardAbout=").append(profile.getFirstHeardAbout()).append(NL); + buf.append("# Last sent to successfully: when did we last send the peer a message successfully? (milliseconds from the epoch)").append(NL); + buf.append("lastSentToSuccessfully=").append(profile.getLastSendSuccessful()).append(NL); + buf.append("# Last failed send: when did we last fail to send a message to the peer? (milliseconds from the epoch)").append(NL); + buf.append("lastFailedSend=").append(profile.getLastSendFailed()).append(NL); + buf.append("# Last heard from: when did we last get a message from the peer? (milliseconds from the epoch)").append(NL); + buf.append("lastHeardFrom=").append(profile.getLastHeardFrom()).append(NL); + buf.append(NL); + + out.write(buf.toString().getBytes()); + + profile.getTunnelHistory().store(out); + profile.getDBHistory().store(out); + + if (profile.getIsExpanded()) { + // only write out expanded data if, uh, we've got it + profile.getCommError().store(out, "commError"); + profile.getDbIntroduction().store(out, "dbIntroduction"); + profile.getDbResponseTime().store(out, "dbResponseTime"); + profile.getReceiveSize().store(out, "receiveSize"); + profile.getSendFailureSize().store(out, "sendFailureSize"); + profile.getSendSuccessSize().store(out, "tunnelCreateResponseTime"); + } + } + + public Set readProfiles() { + long start = Clock.getInstance().now(); + Set files = selectFiles(); + Set profiles = new HashSet(files.size()); + for (Iterator iter = files.iterator(); iter.hasNext();) { + File f = (File)iter.next(); + PeerProfile profile = readProfile(f); + if (profile != null) + profiles.add(profile); + } + long duration = Clock.getInstance().now() - start; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Loading " + profiles.size() + " took " + duration + "ms"); + return profiles; + } + + private Set selectFiles() { + File files[] = getProfileDir().listFiles(new FilenameFilter() { + public boolean accept(File dir, String filename) { + return (filename.startsWith("profile-") && filename.endsWith(".dat")); + } + }); + Set rv = new HashSet(files.length); + for (int i = 0; i < files.length; i++) + rv.add(files[i]); + return rv; + } + private PeerProfile readProfile(File file) { + Hash peer = getHash(file.getName()); + try { + if (peer == null) return null; + PeerProfile profile = new PeerProfile(peer); + Properties props = new Properties(); + + loadProps(props, file); + + profile.setReliabilityBonus(getLong(props, "reliabilityBonus")); + profile.setIntegrationBonus(getLong(props, "integrationBonus")); + profile.setSpeedBonus(getLong(props, "speedBonus")); + + profile.setLastHeardAbout(getLong(props, "lastHeardAbout")); + profile.setFirstHeardAbout(getLong(props, "firstHeardAbout")); + profile.setLastSendSuccessful(getLong(props, "lastSentToSuccessfully")); + profile.setLastSendFailed(getLong(props, "lastFailedSend")); + profile.setLastHeardFrom(getLong(props, "lastHeardFrom")); + + profile.getTunnelHistory().load(props); + profile.getDBHistory().load(props); + + profile.getCommError().load(props, "commError", true); + profile.getDbIntroduction().load(props, "dbIntroduction", true); + profile.getDbResponseTime().load(props, "dbResponseTime", true); + profile.getReceiveSize().load(props, "receiveSize", true); + profile.getSendFailureSize().load(props, "sendFailureSize", true); + profile.getSendSuccessSize().load(props, "tunnelCreateResponseTime", true); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Loaded the profile for " + peer.toBase64() + " from " + file.getName()); + + return profile; + } catch (IllegalArgumentException iae) { + _log.error("Error loading profile from " +file.getName(), iae); + file.delete(); + return null; + } + } + + private final static long getLong(Properties props, String key) { + String val = props.getProperty(key); + if (val != null) { + try { + return Long.parseLong(val); + } catch (NumberFormatException nfe) { + return 0; + } + } + return 0; + } + + private void loadProps(Properties props, File file) { + BufferedReader in = null; + try { + in = new BufferedReader(new InputStreamReader(new FileInputStream(file)), 16*1024); + String line = null; + while ( (line = in.readLine()) != null) { + if (line.trim().length() <= 0) continue; + if (line.charAt(0) == '#') continue; + int split = line.indexOf('='); + if (split <= 0) continue; + String key = line.substring(0, split); + String val = line.substring(split+1); + if ( (key.length() > 0) && (val.length() > 0) ) + props.setProperty(key, val); + } + } catch (IOException ioe) { + _log.error("Error loading properties from " + file.getName(), ioe); + } finally { + if (in != null) try { in.close(); } catch (IOException ioe) {} + } + + } + + private Hash getHash(String name) { + String key = name.substring("profile-".length()); + key = key.substring(0, key.length() - ".dat".length()); + Hash h = new Hash(); + try { + h.fromBase64(key); + return h; + } catch (DataFormatException dfe) { + return null; + } + } + + private File pickFile(PeerProfile profile) { + return new File(getProfileDir(), "profile-" + profile.getPeer().toBase64() + ".dat"); + } + + private File getProfileDir() { + if (_profileDir == null) { + String dir = Router.getInstance().getConfigSetting(PROP_PEER_PROFILE_DIR); + if (dir == null) { + _log.info("No peer profile dir specified [" + PROP_PEER_PROFILE_DIR + "], using [" + DEFAULT_PEER_PROFILE_DIR + "]"); + dir = DEFAULT_PEER_PROFILE_DIR; + } + _profileDir = new File(dir); + } + return _profileDir; + } + + /** generate 1000 profiles */ + public static void main(String args[]) { + System.out.println("Generating 1000 profiles"); + File dir = new File("profiles"); + dir.mkdirs(); + byte data[] = new byte[32]; + java.util.Random rnd = new java.util.Random(); + for (int i = 0; i < 1000; i++) { + rnd.nextBytes(data); + Hash peer = new Hash(data); + try { + File f = new File(dir, "profile-" + peer.toBase64() + ".dat"); + f.createNewFile(); + System.out.println("Created " + peer.toBase64()); + } catch (IOException ioe) {} + } + System.out.println("1000 peers created in " + dir.getAbsolutePath()); + } +} diff --git a/router/java/src/net/i2p/router/peermanager/ReliabilityCalculator.java b/router/java/src/net/i2p/router/peermanager/ReliabilityCalculator.java new file mode 100644 index 0000000000..cea6e1febe --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/ReliabilityCalculator.java @@ -0,0 +1,71 @@ +package net.i2p.router.peermanager; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Determine how reliable the peer is - how likely they'll be able to respond or + * otherwise carry out whatever we ask them to (or even merely be reachable) + * + */ +class ReliabilityCalculator extends Calculator { + private final static Log _log = new Log(ReliabilityCalculator.class); + + public double calc(PeerProfile profile) { + // if we've never succeeded (even if we've never tried), the reliability is zip + if (profile.getSendSuccessSize().getRate(60*60*1000).getLifetimeEventCount() < 0) + return profile.getReliabilityBonus(); + + long val = 0; + val += profile.getSendSuccessSize().getRate(60*1000).getCurrentEventCount() * 5; + val += profile.getSendSuccessSize().getRate(60*1000).getLastEventCount() * 2; + val += profile.getSendSuccessSize().getRate(60*60*1000).getLastEventCount(); + val += profile.getSendSuccessSize().getRate(60*60*1000).getCurrentEventCount(); + + val += profile.getTunnelCreateResponseTime().getRate(60*1000).getCurrentEventCount() * 10; + val += profile.getTunnelCreateResponseTime().getRate(60*1000).getLastEventCount() * 5; + val += profile.getTunnelCreateResponseTime().getRate(60*60*1000).getCurrentEventCount(); + val += profile.getTunnelCreateResponseTime().getRate(60*60*1000).getLastEventCount(); + + val -= profile.getSendFailureSize().getRate(60*1000).getLastEventCount() * 5; + val -= profile.getSendFailureSize().getRate(60*60*1000).getCurrentEventCount()*2; + val -= profile.getSendFailureSize().getRate(60*60*1000).getLastEventCount()*2; + + // penalize them heavily for dropping netDb requests + val -= profile.getDBHistory().getFailedLookupRate().getRate(60*1000).getCurrentEventCount() * 10; + val -= profile.getDBHistory().getFailedLookupRate().getRate(60*1000).getLastEventCount() * 5; + //val -= profile.getDBHistory().getFailedLookupRate().getRate(60*60*1000).getCurrentEventCount(); + //val -= profile.getDBHistory().getFailedLookupRate().getRate(60*60*1000).getLastEventCount(); + //val -= profile.getDBHistory().getFailedLookupRate().getRate(24*60*60*1000).getCurrentEventCount() * 50; + //val -= profile.getDBHistory().getFailedLookupRate().getRate(24*60*60*1000).getLastEventCount() * 20; + + val -= profile.getCommError().getRate(60*1000).getCurrentEventCount() * 200; + val -= profile.getCommError().getRate(60*1000).getLastEventCount() * 200; + + val -= profile.getCommError().getRate(60*60*1000).getCurrentEventCount() * 50; + val -= profile.getCommError().getRate(60*60*1000).getLastEventCount() * 50; + + val -= profile.getCommError().getRate(24*60*60*1000).getCurrentEventCount() * 10; + + long now = Clock.getInstance().now(); + + long timeSinceRejection = now - profile.getTunnelHistory().getLastRejected(); + if (timeSinceRejection > 60*60*1000) { + // noop. rejection was over 60 minutes ago + } else if (timeSinceRejection > 10*60*1000) { + val -= 10; // 10-60 minutes ago we got a rejection + } else if (timeSinceRejection > 60*1000) { + val -= 50; // 1-10 minutes ago we got a rejection + } else { + val -= 100; // we got a rejection within the last minute + } + + if ( (profile.getLastSendSuccessful() > 0) && (now - 24*60*60*1000 > profile.getLastSendSuccessful()) ) { + // we know they're real, but we havent sent them a message successfully in over a day. + val -= 1000; + } + + val += profile.getReliabilityBonus(); + return val; + } +} diff --git a/router/java/src/net/i2p/router/peermanager/SpeedCalculator.java b/router/java/src/net/i2p/router/peermanager/SpeedCalculator.java new file mode 100644 index 0000000000..c4da6c408f --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/SpeedCalculator.java @@ -0,0 +1,60 @@ +package net.i2p.router.peermanager; + +import net.i2p.stat.Rate; +import net.i2p.stat.RateStat; +import net.i2p.util.Log; + +/** + * Quantify how fast the peer is - how fast they respond to our requests, how fast + * they pass messages on, etc. This should be affected both by their bandwidth/latency, + * as well as their load. + * + */ +class SpeedCalculator extends Calculator { + private final static Log _log = new Log(SpeedCalculator.class); + + public double calc(PeerProfile profile) { + double dbResponseTime = profile.getDbResponseTime().getRate(60*1000).getLifetimeAverageValue(); + double tunnelResponseTime = profile.getTunnelCreateResponseTime().getRate(60*1000).getLifetimeAverageValue(); + double roundTripRate = Math.max(dbResponseTime, tunnelResponseTime); + + // send and receive rates are the (period rate) * (saturation %) + double sendRate = calcSendRate(profile); + double receiveRate = calcReceiveRate(profile); + + + double val = 60000.0d - 0.1*roundTripRate + sendRate + receiveRate; + // if we don't have any data, the rate is 0 + if ( (roundTripRate == 0.0d) && (sendRate == 0.0d) ) + val = 0.0; + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("roundTripRate: " + roundTripRate + "ms sendRate: " + sendRate + "bytes/second, receiveRate: " + receiveRate + "bytes/second, val: " + val + " for " + profile.getPeer().toBase64()); + + val += profile.getSpeedBonus(); + return val; + } + + private double calcSendRate(PeerProfile profile) { return calcRate(profile.getSendSuccessSize()); } + private double calcReceiveRate(PeerProfile profile) { return calcRate(profile.getReceiveSize()); } + + private double calcRate(RateStat stat) { + double rate = 0.0d; + Rate hourRate = stat.getRate(60*60*1000); + rate = calcRate(hourRate); + return rate; + } + + private double calcRate(Rate rate) { + long events = rate.getLastEventCount() + rate.getCurrentEventCount(); + if (events >= 1) { + double ms = rate.getLastTotalEventTime() + rate.getCurrentTotalEventTime(); + double bytes = rate.getLastTotalValue() + rate.getCurrentTotalValue(); + if ( (bytes > 0) && (ms > 0) ) { + return (bytes * 1000.0d) / ms; + } + } + return 0.0d; + } + +} diff --git a/router/java/src/net/i2p/router/peermanager/TunnelHistory.java b/router/java/src/net/i2p/router/peermanager/TunnelHistory.java new file mode 100644 index 0000000000..11d5031692 --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/TunnelHistory.java @@ -0,0 +1,104 @@ +package net.i2p.router.peermanager; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Properties; +import net.i2p.util.Clock; + +/** + * Tunnel related history information + * + */ +public class TunnelHistory { + private volatile long _lifetimeAgreedTo; + private volatile long _lifetimeRejected; + private volatile long _lastAgreedTo; + private volatile long _lastRejected; + private volatile long _lifetimeFailed; + private volatile long _lastFailed; + + public TunnelHistory() { + _lifetimeAgreedTo = 0; + _lifetimeFailed = 0; + _lifetimeRejected = 0; + _lastAgreedTo = 0; + _lastFailed = 0; + _lastRejected = 0; + } + + /** total tunnels the peer has agreed to participate in */ + public long getLifetimeAgreedTo() { return _lifetimeAgreedTo; } + /** total tunnels the peer has refused to participate in */ + public long getLifetimeRejected() { return _lifetimeRejected; } + /** total tunnels the peer has agreed to participate in that were later marked as failed prematurely */ + public long getLifetimeFailed() { return _lifetimeFailed; } + /** when the peer last agreed to participate in a tunnel */ + public long getLastAgreedTo() { return _lastAgreedTo; } + /** when the peer last refused to participate in a tunnel */ + public long getLastRejected() { return _lastRejected; } + /** when the last tunnel the peer participated in failed */ + public long getLastFailed() { return _lastFailed; } + + public void incrementAgreedTo() { + _lifetimeAgreedTo++; + _lastAgreedTo = Clock.getInstance().now(); + } + public void incrementRejected() { + _lifetimeRejected++; + _lastRejected = Clock.getInstance().now(); + } + public void incrementFailed() { + _lifetimeFailed++; + _lastFailed = Clock.getInstance().now(); + } + + public void setLifetimeAgreedTo(long num) { _lifetimeAgreedTo = num; } + public void setLifetimeRejected(long num) { _lifetimeRejected = num; } + public void setLifetimeFailed(long num) { _lifetimeFailed = num; } + public void setLastAgreedTo(long when) { _lastAgreedTo = when; } + public void setLastRejected(long when) { _lastRejected = when; } + public void setLastFailed(long when) { _lastFailed = when; } + + private final static String NL = System.getProperty("line.separator"); + + public void store(OutputStream out) throws IOException { + StringBuffer buf = new StringBuffer(512); + buf.append(NL); + buf.append("#################").append(NL); + buf.append("# Tunnel history").append(NL); + buf.append("###").append(NL); + add(buf, "lastAgreedTo", _lastAgreedTo, "When did the peer last agree to participate in a tunnel? (milliseconds since the epoch)"); + add(buf, "lastFailed", _lastFailed, "When was the last time a tunnel that the peer agreed to participate failed? (milliseconds since the epoch)"); + add(buf, "lastRejected", _lastRejected, "When was the last time the peer refused to participate in a tunnel? (milliseconds since the epoch)"); + add(buf, "lifetimeAgreedTo", _lifetimeAgreedTo, "How many tunnels has the peer ever agreed to participate in?"); + add(buf, "lifetimeFailed", _lifetimeFailed, "How many tunnels has the peer ever agreed to participate in that failed prematurely?"); + add(buf, "lifetimeRejected", _lifetimeRejected, "How many tunnels has the peer ever refused to participate in?"); + out.write(buf.toString().getBytes()); + } + + private void add(StringBuffer buf, String name, long val, String description) { + buf.append("# ").append(name.toUpperCase()).append(NL).append("# ").append(description).append(NL); + buf.append("tunnels.").append(name).append('=').append(val).append(NL).append(NL); + } + + public void load(Properties props) { + _lastAgreedTo = getLong(props, "tunnels.lastAgreedTo"); + _lastFailed = getLong(props, "tunnels.lastFailed"); + _lastRejected = getLong(props, "tunnels.lastRejected"); + _lifetimeAgreedTo = getLong(props, "tunnels.lifetimeAgreedTo"); + _lifetimeFailed = getLong(props, "tunnels.lifetimeFailed"); + _lifetimeRejected = getLong(props, "tunnels.lifetimeRejected"); + } + + private final static long getLong(Properties props, String key) { + String val = props.getProperty(key); + if (val != null) { + try { + return Long.parseLong(val); + } catch (NumberFormatException nfe) { + return 0; + } + } + return 0; + } +} diff --git a/router/java/src/net/i2p/router/startup/BootCommSystemJob.java b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java new file mode 100644 index 0000000000..71937e36e4 --- /dev/null +++ b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java @@ -0,0 +1,51 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.CommSystemFacade; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.PeerManagerFacade; +import net.i2p.router.Router; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.util.Log; + +public class BootCommSystemJob extends JobImpl { + private static Log _log = new Log(BootCommSystemJob.class); + + public static final String PROP_USE_TRUSTED_LINKS = "router.trustedLinks"; + + public BootCommSystemJob() { } + + public String getName() { return "Boot Communication System"; } + + public void runJob() { + // start up the network comm system + + CommSystemFacade.getInstance().startup(); + TunnelManagerFacade.getInstance().startup(); + PeerManagerFacade.getInstance().startup(); + + Job bootDb = new BootNetworkDbJob(); + boolean useTrusted = false; + String useTrustedStr = Router.getInstance().getConfigSetting(PROP_USE_TRUSTED_LINKS); + if (useTrustedStr != null) { + useTrusted = Boolean.TRUE.toString().equalsIgnoreCase(useTrustedStr); + } + if (useTrusted) { + _log.debug("Using trusted links..."); + JobQueue.getInstance().addJob(new BuildTrustedLinksJob(bootDb)); + return; + } else { + _log.debug("Not using trusted links - boot db"); + JobQueue.getInstance().addJob(bootDb); + } + } +} diff --git a/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java b/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java new file mode 100644 index 0000000000..2b5f9642ab --- /dev/null +++ b/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java @@ -0,0 +1,30 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.util.Log; + +public class BootNetworkDbJob extends JobImpl { + private static Log _log = new Log(BootNetworkDbJob.class); + + public BootNetworkDbJob() { } + + public String getName() { return "Boot Network Database"; } + + public void runJob() { + // start up the network database + + NetworkDatabaseFacade.getInstance().startup(); + + JobQueue.getInstance().addJob(new StartAcceptingClientsJob()); + } +} diff --git a/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java b/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java new file mode 100644 index 0000000000..95fb811cf0 --- /dev/null +++ b/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java @@ -0,0 +1,33 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.util.Log; + +public class BuildTrustedLinksJob extends JobImpl { + private static Log _log = new Log(BuildTrustedLinksJob.class); + private Job _next; + + public BuildTrustedLinksJob(Job next) { + _next = next; + } + + public String getName() { return "Build Trusted Links"; } + + public void runJob() { + // create trusted links with peers + + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + + JobQueue.getInstance().addJob(_next); + } +} diff --git a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java new file mode 100644 index 0000000000..1b7d85f455 --- /dev/null +++ b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java @@ -0,0 +1,123 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashSet; + +import net.i2p.crypto.KeyGenerator; +import net.i2p.data.Certificate; +import net.i2p.data.DataFormatException; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.data.RouterIdentity; +import net.i2p.data.RouterInfo; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.router.CommSystemFacade; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.KeyManager; +import net.i2p.router.Router; +import net.i2p.router.StatisticsManager; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +public class CreateRouterInfoJob extends JobImpl { + private static Log _log = new Log(CreateRouterInfoJob.class); + private Job _next; + + public CreateRouterInfoJob(Job next) { + _next = next; + } + + public String getName() { return "Create New Router Info"; } + + public void runJob() { + _log.debug("Creating the new router info"); + // create a new router info and store it where LoadRouterInfoJob looks + RouterInfo info = createRouterInfo(); + JobQueue.getInstance().addJob(_next); + } + + static RouterInfo createRouterInfo() { + RouterInfo info = new RouterInfo(); + FileOutputStream fos1 = null; + FileOutputStream fos2 = null; + try { + info.setAddresses(CommSystemFacade.getInstance().createAddresses()); + info.setOptions(StatisticsManager.getInstance().publishStatistics()); + info.setPeers(new HashSet()); + info.setPublished(getCurrentPublishDate()); + RouterIdentity ident = new RouterIdentity(); + Certificate cert = new Certificate(); + cert.setCertificateType(Certificate.CERTIFICATE_TYPE_NULL); + cert.setPayload(null); + ident.setCertificate(cert); + PublicKey pubkey = null; + PrivateKey privkey = null; + SigningPublicKey signingPubKey = null; + SigningPrivateKey signingPrivKey = null; + Object keypair[] = KeyGenerator.getInstance().generatePKIKeypair(); + pubkey = (PublicKey)keypair[0]; + privkey = (PrivateKey)keypair[1]; + Object signingKeypair[] = KeyGenerator.getInstance().generateSigningKeypair(); + signingPubKey = (SigningPublicKey)signingKeypair[0]; + signingPrivKey = (SigningPrivateKey)signingKeypair[1]; + ident.setPublicKey(pubkey); + ident.setSigningPublicKey(signingPubKey); + info.setIdentity(ident); + + info.sign(signingPrivKey); + + String infoFilename = Router.getInstance().getConfigSetting(Router.PROP_INFO_FILENAME); + if (infoFilename == null) + infoFilename = Router.PROP_INFO_FILENAME_DEFAULT; + fos1 = new FileOutputStream(infoFilename); + info.writeBytes(fos1); + + String keyFilename = Router.getInstance().getConfigSetting(Router.PROP_KEYS_FILENAME); + if (keyFilename == null) + keyFilename = Router.PROP_KEYS_FILENAME_DEFAULT; + fos2 = new FileOutputStream(keyFilename); + privkey.writeBytes(fos2); + signingPrivKey.writeBytes(fos2); + pubkey.writeBytes(fos2); + signingPubKey.writeBytes(fos2); + + KeyManager.getInstance().setSigningPrivateKey(signingPrivKey); + KeyManager.getInstance().setSigningPublicKey(signingPubKey); + KeyManager.getInstance().setPrivateKey(privkey); + KeyManager.getInstance().setPublicKey(pubkey); + + _log.info("Router info created and stored at " + infoFilename + " with private keys stored at " + keyFilename + " [" + info + "]"); + } catch (DataFormatException dfe) { + _log.error("Error building the new router information", dfe); + } catch (IOException ioe) { + _log.error("Error writing out the new router information", ioe); + } finally { + if (fos1 != null) try { fos1.close(); } catch (IOException ioe) {} + if (fos2 != null) try { fos2.close(); } catch (IOException ioe) {} + } + return info; + } + + + /** + * We probably don't want to expose the exact time at which a router published its info. + * perhaps round down to the nearest minute? 10 minutes? 30 minutes? day? + * + */ + static long getCurrentPublishDate() { + _log.info("Setting published date to /now/"); + return Clock.getInstance().now(); + } +} diff --git a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java new file mode 100644 index 0000000000..9f20594851 --- /dev/null +++ b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java @@ -0,0 +1,115 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import net.i2p.data.DataFormatException; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.data.RouterInfo; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.KeyManager; +import net.i2p.router.Router; +import net.i2p.router.MessageHistory; +import net.i2p.util.Log; + +public class LoadRouterInfoJob extends JobImpl { + private static Log _log = new Log(LoadRouterInfoJob.class); + private boolean _keysExist; + private boolean _infoExists; + private RouterInfo _us; + + public String getName() { return "Load Router Info"; } + + public void runJob() { + loadRouterInfo(); + if (_us == null) { + RebuildRouterInfoJob.rebuildRouterInfo(false); + JobQueue.getInstance().addJob(this); + return; + } else { + Router.getInstance().setRouterInfo(_us); + MessageHistory.initialize(); + JobQueue.getInstance().addJob(new BootCommSystemJob()); + } + } + + private void loadRouterInfo() { + String routerInfoFile = Router.getInstance().getConfigSetting(Router.PROP_INFO_FILENAME); + if (routerInfoFile == null) + routerInfoFile = Router.PROP_INFO_FILENAME_DEFAULT; + RouterInfo info = null; + boolean failedRead = false; + + + String keyFilename = Router.getInstance().getConfigSetting(Router.PROP_KEYS_FILENAME); + if (keyFilename == null) + keyFilename = Router.PROP_KEYS_FILENAME_DEFAULT; + + File rif = new File(routerInfoFile); + if (rif.exists()) + _infoExists = true; + File rkf = new File(keyFilename); + if (rkf.exists()) + _keysExist = true; + + FileInputStream fis1 = null; + FileInputStream fis2 = null; + try { + if (_infoExists) { + fis1 = new FileInputStream(rif); + info = new RouterInfo(); + info.readBytes(fis1); + _log.debug("Reading in routerInfo from " + rif.getAbsolutePath() + " and it has " + info.getAddresses().size() + " addresses"); + } + + if (_keysExist) { + fis2 = new FileInputStream(rkf); + PrivateKey privkey = new PrivateKey(); + privkey.readBytes(fis2); + SigningPrivateKey signingPrivKey = new SigningPrivateKey(); + signingPrivKey.readBytes(fis2); + PublicKey pubkey = new PublicKey(); + pubkey.readBytes(fis2); + SigningPublicKey signingPubKey = new SigningPublicKey(); + signingPubKey.readBytes(fis2); + + KeyManager.getInstance().setPrivateKey(privkey); + KeyManager.getInstance().setSigningPrivateKey(signingPrivKey); + KeyManager.getInstance().setPublicKey(pubkey); //info.getIdentity().getPublicKey()); + KeyManager.getInstance().setSigningPublicKey(signingPubKey); // info.getIdentity().getSigningPublicKey()); + } + + _us = info; + } catch (IOException ioe) { + _log.error("Error reading the router info from " + routerInfoFile + " and the keys from " + keyFilename, ioe); + _us = null; + rif.delete(); + rkf.delete(); + _infoExists = false; + _keysExist = false; + } catch (DataFormatException dfe) { + _log.error("Corrupt router info or keys at " + routerInfoFile + " / " + keyFilename, dfe); + _us = null; + rif.delete(); + rkf.delete(); + _infoExists = false; + _keysExist = false; + } finally { + if (fis1 != null) try { fis1.close(); } catch (IOException ioe) {} + if (fis2 != null) try { fis2.close(); } catch (IOException ioe) {} + } + } +} diff --git a/router/java/src/net/i2p/router/startup/ProcessInboundNetMessageJob.java b/router/java/src/net/i2p/router/startup/ProcessInboundNetMessageJob.java new file mode 100644 index 0000000000..0f3383b3bc --- /dev/null +++ b/router/java/src/net/i2p/router/startup/ProcessInboundNetMessageJob.java @@ -0,0 +1,47 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.InNetMessage; +import net.i2p.router.InNetMessagePool; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Pull a message off the inbound net message pool and begin its processing. + * This job requeues itself on completion + * + */ +public class ProcessInboundNetMessageJob extends JobImpl { + private static Log _log = new Log(ProcessInboundNetMessageJob.class); + + public ProcessInboundNetMessageJob() { } + + public String getName() { return "Check For Inbound Network Message"; } + + public void runJob() { + // start up the network comm system + + if (InNetMessagePool.getInstance().getCount() > 0) { + InNetMessage inMessage = InNetMessagePool.getInstance().getNext(); + processMessage(inMessage); + // there are messages, no need to delay as there's real work to do + } else { + getTiming().setStartAfter(Clock.getInstance().now()+1000); + } + + JobQueue.getInstance().addJob(this); + } + + private void processMessage(InNetMessage message) { + _log.debug("Received message from " + message.getFromRouter() + "/" + message.getFromRouterHash() + " containing : " + message.getMessage()); + } +} diff --git a/router/java/src/net/i2p/router/startup/ReadConfigJob.java b/router/java/src/net/i2p/router/startup/ReadConfigJob.java new file mode 100644 index 0000000000..cfc6a56950 --- /dev/null +++ b/router/java/src/net/i2p/router/startup/ReadConfigJob.java @@ -0,0 +1,69 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.JobImpl; +import net.i2p.router.Router; +import net.i2p.router.JobQueue; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Properties; +import java.util.Iterator; +import java.io.FileInputStream; +import java.io.File; +import java.io.IOException; + +/** + * Simply read the router config + */ +public class ReadConfigJob extends JobImpl { + private static Log _log = new Log(ReadConfigJob.class); + + private final static long DELAY = 30*1000; // reread every 30 seconds + + public String getName() { return "Read Router Configuration"; } + public void runJob() { + doRead(); + getTiming().setStartAfter(Clock.getInstance().now() + DELAY); + JobQueue.getInstance().addJob(this); + } + + public static void doRead() { + Router r = Router.getInstance(); + String f = r.getConfigFilename(); + Properties config = getConfig(f); + for (Iterator iter = config.keySet().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + String val = config.getProperty(name); + _log.debug("Setting config prop [" + name + "] = [" + val + "]"); + Router.getInstance().setConfigSetting(name, val); + } + } + + private static Properties getConfig(String filename) { + _log.debug("Config file: " + filename); + Properties props = new Properties(); + FileInputStream fis = null; + try { + File f = new File(filename); + if (f.canRead()) { + fis = new FileInputStream(f); + props.load(fis); + } else { + _log.error("Configuration file " + filename + " does not exist"); + } + } catch (Exception ioe) { + _log.error("Error loading the router configuration from " + filename, ioe); + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + return props; + } +} diff --git a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java new file mode 100644 index 0000000000..da4b100bbb --- /dev/null +++ b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java @@ -0,0 +1,196 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import net.i2p.data.Certificate; +import net.i2p.data.DataFormatException; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.data.RouterIdentity; +import net.i2p.data.RouterInfo; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.router.CommSystemFacade; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.KeyManager; +import net.i2p.router.Router; +import net.i2p.router.StatisticsManager; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * If the file router.info.rebuild exists, rebuild the router info and republish. + * This is useful for dhcp or other situations where the router addresses change - + * simply create the router.info.rebuild file after modifying router.config and within + * 45 seconds (the current check frequency), the router info will be rebuilt with new + * addresses and stats, as well as a new version, then republished. Afterwards, the + * router.info.rebuild file is deleted + * + */ +public class RebuildRouterInfoJob extends JobImpl { + private static Log _log = new Log(RebuildRouterInfoJob.class); + + private final static long REBUILD_DELAY = 45*1000; // every 30 seconds + + public RebuildRouterInfoJob() { + super(); + } + + public String getName() { return "Rebuild Router Info"; } + + public void runJob() { + _log.debug("Testing to rebuild router info"); + String infoFile = Router.getInstance().getConfigSetting(Router.PROP_INFO_FILENAME); + if (infoFile == null) { + _log.debug("Info filename not configured, defaulting to " + Router.PROP_INFO_FILENAME_DEFAULT); + infoFile = Router.PROP_INFO_FILENAME_DEFAULT; + } + + String keyFilename = Router.getInstance().getConfigSetting(Router.PROP_KEYS_FILENAME); + if (keyFilename == null) + keyFilename = Router.PROP_KEYS_FILENAME_DEFAULT; + File keyFile = new File(keyFilename); + + File info = new File(infoFile); + if (!info.exists() || !keyFile.exists()) { + _log.info("Router info file [" + info.getAbsolutePath() + "] or private key file [" + keyFile.getAbsolutePath() + "] deleted, rebuilding"); + rebuildRouterInfo(); + } else { + _log.debug("Router info file [" + info.getAbsolutePath() + "] exists, not rebuilding"); + } + getTiming().setStartAfter(Clock.getInstance().now() + REBUILD_DELAY); + JobQueue.getInstance().addJob(this); + } + + static void rebuildRouterInfo() { + rebuildRouterInfo(true); + } + static void rebuildRouterInfo(boolean alreadyRunning) { + _log.debug("Rebuilding the new router info"); + boolean fullRebuild = false; + RouterInfo info = null; + String infoFilename = Router.getInstance().getConfigSetting(Router.PROP_INFO_FILENAME); + if (infoFilename == null) + infoFilename = Router.PROP_INFO_FILENAME_DEFAULT; + + String keyFilename = Router.getInstance().getConfigSetting(Router.PROP_KEYS_FILENAME); + if (keyFilename == null) + keyFilename = Router.PROP_KEYS_FILENAME_DEFAULT; + File keyFile = new File(keyFilename); + + if (keyFile.exists()) { + // ok, no need to rebuild a brand new identity, just update what we can + info = Router.getInstance().getRouterInfo(); + if (info == null) { + info = new RouterInfo(); + FileInputStream fis = null; + try { + fis = new FileInputStream(keyFile); + PrivateKey privkey = new PrivateKey(); + privkey.readBytes(fis); + SigningPrivateKey signingPrivKey = new SigningPrivateKey(); + signingPrivKey.readBytes(fis); + PublicKey pubkey = new PublicKey(); + pubkey.readBytes(fis); + SigningPublicKey signingPubKey = new SigningPublicKey(); + signingPubKey.readBytes(fis); + RouterIdentity ident = new RouterIdentity(); + ident.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + ident.setPublicKey(pubkey); + ident.setSigningPublicKey(signingPubKey); + info.setIdentity(ident); + } catch (Exception e) { + _log.error("Error reading in the key data from " + keyFile.getAbsolutePath(), e); + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + fis = null; + keyFile.delete(); + rebuildRouterInfo(alreadyRunning); + return; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + } + + try { + info.setAddresses(CommSystemFacade.getInstance().createAddresses()); + info.setOptions(StatisticsManager.getInstance().publishStatistics()); + // info.setPeers(new HashSet()); // this would have the trusted peers + info.setPublished(CreateRouterInfoJob.getCurrentPublishDate()); + + info.sign(KeyManager.getInstance().getSigningPrivateKey()); + } catch (DataFormatException dfe) { + _log.error("Error rebuilding the new router info", dfe); + return; + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(infoFilename); + info.writeBytes(fos); + } catch (DataFormatException dfe) { + _log.error("Error rebuilding the router information", dfe); + } catch (IOException ioe) { + _log.error("Error writing out the rebuilt router information", ioe); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + + } else { + _log.warn("Private key file " + keyFile.getAbsolutePath() + " deleted! Rebuilding a brand new router identity!"); + // this proc writes the keys and info to the file as well as builds the latest and greatest info + info = CreateRouterInfoJob.createRouterInfo(); + fullRebuild = true; + } + + //MessageHistory.initialize(); + Router.getInstance().setRouterInfo(info); + + ////// + // the following is commented out because its dangerous, extremely rarely used, + // and not necessary for a fundamental feature (rebuilding new identities without + // restarting) + ///// + + /* + _log.warn("Restarting the router identity, pausing activity"); + try { + JobQueue.getInstance().pauseQueue(); + try { Thread.sleep(1000); } catch (InterruptedException ie) {} + + if (alreadyRunning) { + if (fullRebuild) { + // if we changed our ident, then we need to drop our tunnels + TunnelManagerFacade.getInstance().shutdown(); + TunnelManagerFacade.getInstance().startup(); + } + + ClientManagerFacade.getInstance().shutdown(); + CommSystemFacade.getInstance().shutdown(); + // sleep to free up sockets + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + CommSystemFacade.getInstance().startup(); + ClientManagerFacade.getInstance().startup(); + } + } catch (Throwable t) { + _log.error("Error during comm rebuilding", t); + } finally { + JobQueue.getInstance().unpauseQueue(); + } + NetworkDatabaseFacade.getInstance().publish(info); + */ + _log.info("Router info rebuilt and stored at " + infoFilename + " [" + info + "]"); + } + +} diff --git a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java new file mode 100644 index 0000000000..a6629ce3cf --- /dev/null +++ b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java @@ -0,0 +1,165 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.lang.reflect.Method; +import java.util.List; +import java.util.ArrayList; + +import net.i2p.router.ClientManagerFacade; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.Router; +import net.i2p.router.admin.AdminManager; +import net.i2p.util.Log; +import net.i2p.util.I2PThread; +import net.i2p.util.Clock; + +public class StartAcceptingClientsJob extends JobImpl { + private static Log _log = new Log(StartAcceptingClientsJob.class); + + public StartAcceptingClientsJob() { } + + public String getName() { return "Start Accepting Clients"; } + + public void runJob() { + // start up the network database + + ClientManagerFacade.getInstance().startup(); + + JobQueue.getInstance().addJob(new ReadConfigJob()); + JobQueue.getInstance().addJob(new RebuildRouterInfoJob()); + AdminManager.getInstance().startup(); + JobQueue.getInstance().allowParallelOperation(); + JobQueue.getInstance().addJob(new LoadClientAppsJob()); + } + + public static void main(String args[]) { + test(null); + test("hi how are you?"); + test("hi how are you? "); + test(" hi how are you? "); + test(" hi how are \"y\"ou? "); + test("-nogui -e \"config localhost 17654\" -e \"httpclient 4544\""); + test("-nogui -e 'config localhost 17654' -e 'httpclient 4544'"); + } + private static void test(String args) { + String parsed[] = LoadClientAppsJob.parseArgs(args); + System.out.print("Parsed [" + args + "] into " + parsed.length + " elements: "); + for (int i = 0; i < parsed.length; i++) + System.out.print("[" + parsed[i] + "] "); + System.out.println(); + } +} + +class LoadClientAppsJob extends JobImpl { + private final static Log _log = new Log(LoadClientAppsJob.class); + /** wait a minute before starting up client apps */ + private final static long STARTUP_DELAY = 60*1000; + public LoadClientAppsJob() { + super(); + getTiming().setStartAfter(STARTUP_DELAY + Clock.getInstance().now()); + } + public void runJob() { + int i = 0; + while (true) { + String className = Router.getInstance().getConfigSetting("clientApp."+i+".main"); + String clientName = Router.getInstance().getConfigSetting("clientApp."+i+".name"); + String args = Router.getInstance().getConfigSetting("clientApp."+i+".args"); + if (className == null) break; + + String argVal[] = parseArgs(args); + _log.info("Loading up the client application " + clientName + ": " + className + " " + args); + runClient(className, clientName, argVal); + i++; + } + } + + static String[] parseArgs(String args) { + List argList = new ArrayList(4); + if (args != null) { + char data[] = args.toCharArray(); + StringBuffer buf = new StringBuffer(32); + boolean isQuoted = false; + for (int i = 0; i < data.length; i++) { + switch (data[i]) { + case '\'': + case '\"': + if (isQuoted) { + String str = buf.toString().trim(); + if (str.length() > 0) + argList.add(str); + buf = new StringBuffer(32); + } else { + isQuoted = true; + } + break; + case ' ': + case '\t': + // whitespace - if we're in a quoted section, keep this as part of the quote, + // otherwise use it as a delim + if (isQuoted) { + buf.append(data[i]); + } else { + String str = buf.toString().trim(); + if (str.length() > 0) + argList.add(str); + buf = new StringBuffer(32); + } + break; + default: + buf.append(data[i]); + break; + } + } + if (buf.length() > 0) { + String str = buf.toString().trim(); + if (str.length() > 0) + argList.add(str); + } + } + String rv[] = new String[argList.size()]; + for (int i = 0; i < argList.size(); i++) + rv[i] = (String)argList.get(i); + return rv; + } + + private void runClient(String className, String clientName, String args[]) { + I2PThread t = new I2PThread(new RunApp(className, clientName, args)); + t.setName(clientName); + t.setDaemon(true); + t.start(); + } + + private final static class RunApp implements Runnable { + private String _className; + private String _appName; + private String _args[]; + public RunApp(String className, String appName, String args[]) { + _className = className; + _appName = appName; + if (args == null) + _args = new String[0]; + else + _args = args; + } + public void run() { + try { + Class cls = Class.forName(_className); + Method method = cls.getMethod("main", new Class[] { String[].class }); + method.invoke(cls, new Object[] { _args }); + } catch (Throwable t) { + _log.log(Log.CRIT, "Error starting up the client class " + _className, t); + } + _log.info("Done running client application " + _appName); + } + } + + public String getName() { return "Load up any client applications"; } +} diff --git a/router/java/src/net/i2p/router/startup/StartupJob.java b/router/java/src/net/i2p/router/startup/StartupJob.java new file mode 100644 index 0000000000..a59b59cefe --- /dev/null +++ b/router/java/src/net/i2p/router/startup/StartupJob.java @@ -0,0 +1,40 @@ +package net.i2p.router.startup; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + + +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.StatisticsManager; +import net.i2p.util.Log; + +/** + * The StartupJob should be run once on router startup to initialize the system + * and set things in motion. This task loads the router configuration and then + * queues up a LoadRouterInfoJob, which reads the old RouterInfo structure from + * a previously saved version on disk. If it can't find one, it fires up a + * CreateRouterInfoJob which builds a new one from scratch, including a new + * RouterIdentity and then reruns the LoadRouterInfoJob. After that the + * router begins listening on its ports by running the BootCommSystemJob which + * is followed by the BootNetworkDbJob, though BuildTrustedLinksJob may occur + * as well. After running the BootNetworkDbJob, the final + * StartAcceptingClientsJob is queued up, which finishes the startup. + * + */ +public class StartupJob extends JobImpl { + private static Log _log = new Log(StartupJob.class); + + public String getName() { return "Startup Router"; } + public void runJob() { + ReadConfigJob.doRead(); + StatisticsManager.getInstance().startup(); + + JobQueue.getInstance().addJob(new LoadRouterInfoJob()); + } +} diff --git a/router/java/src/net/i2p/router/transport/.nbattrs b/router/java/src/net/i2p/router/transport/.nbattrs new file mode 100644 index 0000000000..506548d0b1 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/.nbattrs @@ -0,0 +1,7 @@ + + + + + + + diff --git a/router/java/src/net/i2p/router/transport/BandwidthLimitedInputStream.java b/router/java/src/net/i2p/router/transport/BandwidthLimitedInputStream.java new file mode 100644 index 0000000000..5832b0a04f --- /dev/null +++ b/router/java/src/net/i2p/router/transport/BandwidthLimitedInputStream.java @@ -0,0 +1,45 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.RouterIdentity; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.IOException; + +public class BandwidthLimitedInputStream extends FilterInputStream { + private RouterIdentity _peer; + public BandwidthLimitedInputStream(InputStream source, RouterIdentity peer) { + super(source); + _peer = peer; + } + + public int read() throws IOException { + BandwidthLimiter.getInstance().delayInbound(_peer, 1); + return in.read(); + } + + public int read(byte dest[]) throws IOException { + int read = in.read(dest); + BandwidthLimiter.getInstance().delayInbound(_peer, read); + return read; + } + + public int read(byte dest[], int off, int len) throws IOException { + int read = in.read(dest, off, len); + BandwidthLimiter.getInstance().delayInbound(_peer, read); + return read; + } + public long skip(long numBytes) throws IOException { + long skip = in.skip(numBytes); + BandwidthLimiter.getInstance().delayInbound(_peer, (int)skip); + return skip; + } +} diff --git a/router/java/src/net/i2p/router/transport/BandwidthLimitedOutputStream.java b/router/java/src/net/i2p/router/transport/BandwidthLimitedOutputStream.java new file mode 100644 index 0000000000..0afa5c505e --- /dev/null +++ b/router/java/src/net/i2p/router/transport/BandwidthLimitedOutputStream.java @@ -0,0 +1,61 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.RouterIdentity; +import java.io.FilterOutputStream; +import java.io.OutputStream; +import java.io.IOException; + +public class BandwidthLimitedOutputStream extends FilterOutputStream { + private RouterIdentity _peer; + + public BandwidthLimitedOutputStream(OutputStream source, RouterIdentity peer) { + super(source); + _peer = peer; + } + + private final static int CHUNK_SIZE = 64; + + public void write(int val) throws IOException { + BandwidthLimiter.getInstance().delayOutbound(_peer, 1); + out.write(val); + } + public void write(byte src[]) throws IOException { + if (src == null) return; + if (src.length > CHUNK_SIZE) { + for (int i = 0; i < src.length; ) { + write(src, i*CHUNK_SIZE, CHUNK_SIZE); + i += CHUNK_SIZE; + } + } else { + write(src, 0, src.length); + } + } + public void write(byte src[], int off, int len) throws IOException { + if (src == null) return; + if (len <= 0) return; + if (len <= CHUNK_SIZE) { + BandwidthLimiter.getInstance().delayOutbound(_peer, len); + out.write(src, off, len); + } else { + int i = 0; + while (i+CHUNK_SIZE < len) { + BandwidthLimiter.getInstance().delayOutbound(_peer, CHUNK_SIZE); + out.write(src, off+i*CHUNK_SIZE, CHUNK_SIZE); + i++; + } + int remainder = len % CHUNK_SIZE; + if (remainder != 0) { + BandwidthLimiter.getInstance().delayOutbound(_peer, remainder); + out.write(src, off+len-(remainder), remainder); + } + } + } +} diff --git a/router/java/src/net/i2p/router/transport/BandwidthLimiter.java b/router/java/src/net/i2p/router/transport/BandwidthLimiter.java new file mode 100644 index 0000000000..45207baa94 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/BandwidthLimiter.java @@ -0,0 +1,78 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.RouterIdentity; + +import net.i2p.util.Log; + +/** + * Coordinate the bandwidth limiting across all classes of peers. Currently + * treats everything as open (aka doesn't limit) + * + */ +public class BandwidthLimiter { + private final static Log _log = new Log(BandwidthLimiter.class); + private final static BandwidthLimiter _instance = new TrivialBandwidthLimiter(); + public static BandwidthLimiter getInstance() { return _instance; } + + protected BandwidthLimiter() {} + + public long getTotalSendBytes() { return 0; } + public long getTotalReceiveBytes() { return 0; } + + /** + * Return how many milliseconds to wait before receiving/processing numBytes from the peer + */ + public long calculateDelayInbound(RouterIdentity peer, int numBytes) { + return 0; + } + + /** + * Return how many milliseconds to wait before sending numBytes to the peer + */ + public long calculateDelayOutbound(RouterIdentity peer, int numBytes) { + return 0; + } + + /** + * Note that numBytes have been read from the peer + */ + public void consumeInbound(RouterIdentity peer, int numBytes) {} + /** + * Note that numBytes have been sent to the peer + */ + public void consumeOutbound(RouterIdentity peer, int numBytes) {} + + /** + * Delay the required amount of time before returning so that receiving numBytes + * from the peer will not violate the bandwidth limits + */ + public void delayInbound(RouterIdentity peer, int numBytes) { + long ms = calculateDelayInbound(peer, numBytes); + if (ms > 0) { + _log.debug("Delaying inbound " + ms +"ms for " + numBytes +" bytes"); + try { Thread.sleep(ms); } catch (InterruptedException ie) {} + } + consumeInbound(peer, numBytes); + } + /** + * Delay the required amount of time before returning so that sending numBytes + * to the peer will not violate the bandwidth limits + */ + public void delayOutbound(RouterIdentity peer, int numBytes) { + long ms = calculateDelayOutbound(peer, numBytes); + if (ms > 0) { + _log.debug("Delaying outbound " + ms + "ms for " + numBytes + " bytes"); + try { Thread.sleep(ms); } catch (InterruptedException ie) {} + } + + consumeOutbound(peer, numBytes); + } +} diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java new file mode 100644 index 0000000000..10b5abfaa3 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java @@ -0,0 +1,112 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import net.i2p.data.RouterAddress; +import net.i2p.router.CommSystemFacade; +import net.i2p.router.JobQueue; +import net.i2p.router.KeyManager; +import net.i2p.router.OutNetMessage; +import net.i2p.router.Router; +import net.i2p.router.transport.phttp.PHTTPTransport; +import net.i2p.router.transport.tcp.TCPTransport; +import net.i2p.util.Log; + +public class CommSystemFacadeImpl extends CommSystemFacade { + private final static Log _log = new Log(CommSystemFacadeImpl.class); + private TransportManager _manager; + + public CommSystemFacadeImpl() { + _manager = null; + } + + public void startup() { + _log.info("Starting up the comm system"); + _manager = new TransportManager(Router.getInstance().getRouterInfo(), KeyManager.getInstance().getSigningPrivateKey()); + _manager.startListening(); + //JobQueue.getInstance().addJob(new FetchOutNetMessageJob(this)); + } + + public void shutdown() { + if (_manager != null) + _manager.stopListening(); + } + + public List getBids(OutNetMessage msg) { + return _manager.getBids(msg); + } + + public void processMessage(OutNetMessage msg) { + JobQueue.getInstance().addJob(new GetBidsJob(this, msg)); + } + + public String renderStatusHTML() { return _manager.renderStatusHTML(); } + + + public Set createAddresses() { + Set addresses = new HashSet(); + RouterAddress addr = createTCPAddress(); + if (addr != null) + addresses.add(addr); + addr = createPHTTPAddress(); + if (addr != null) + addresses.add(addr); + _log.info("Creating addresses: " + addresses); + return addresses; + } + + private final static String PROP_I2NP_TCP_HOSTNAME = "i2np.tcp.hostname"; + private final static String PROP_I2NP_TCP_PORT = "i2np.tcp.port"; + private final static String PROP_I2NP_PHTTP_SEND_URL = "i2np.phttp.sendURL"; + private final static String PROP_I2NP_PHTTP_REGISTER_URL = "i2np.phttp.registerURL"; + + private static RouterAddress createTCPAddress() { + RouterAddress addr = new RouterAddress(); + addr.setCost(10); + addr.setExpiration(null); + Properties props = new Properties(); + String name = Router.getInstance().getConfigSetting(PROP_I2NP_TCP_HOSTNAME); + String port = Router.getInstance().getConfigSetting(PROP_I2NP_TCP_PORT); + if ( (name == null) || (port == null) ) { + _log.info("TCP Host/Port not specified in config file - skipping TCP transport"); + return null; + } else { + _log.info("Creating TCP address on " + name + ":" + port); + } + props.setProperty("host", name); + props.setProperty("port", port); + addr.setOptions(props); + addr.setTransportStyle(TCPTransport.STYLE); + return addr; + } + private static RouterAddress createPHTTPAddress() { + RouterAddress addr = new RouterAddress(); + addr.setCost(50); + addr.setExpiration(null); + Properties props = new Properties(); + String regURL = Router.getInstance().getConfigSetting(PROP_I2NP_PHTTP_REGISTER_URL); + String sendURL = Router.getInstance().getConfigSetting(PROP_I2NP_PHTTP_SEND_URL); + if ( (regURL == null) || (sendURL == null) ) { + _log.info("Polling HTTP registration/send URLs not specified in config file - skipping PHTTP transport"); + return null; + } else { + _log.info("Creating Polling HTTP address on " + regURL + " / " + sendURL); + } + props.setProperty(PHTTPTransport.PROP_TO_REGISTER_URL, regURL); + props.setProperty(PHTTPTransport.PROP_TO_SEND_URL, sendURL); + addr.setOptions(props); + addr.setTransportStyle(PHTTPTransport.STYLE); + return addr; + } +} diff --git a/router/java/src/net/i2p/router/transport/FetchOutNetMessageJob.java b/router/java/src/net/i2p/router/transport/FetchOutNetMessageJob.java new file mode 100644 index 0000000000..e12f25f5d5 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/FetchOutNetMessageJob.java @@ -0,0 +1,49 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.OutNetMessage; +import net.i2p.router.OutNetMessagePool; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Fetch an outbound message from the outbound pool, check its validity, get a bid + * from transports, and queue it for delivery on the "winning" transport + * + */ +public class FetchOutNetMessageJob extends JobImpl { + private static Log _log = new Log(FetchOutNetMessageJob.class); + private CommSystemFacadeImpl _facade; + + public FetchOutNetMessageJob(CommSystemFacadeImpl facade) { + super(); + _facade = facade; + } + + public String getName() { return "Check For Pending Outbound Network Message"; } + public void runJob() { + OutNetMessage msg = OutNetMessagePool.getInstance().getNext(); + if (msg != null) { + processMessage(msg); + } else { + _log.debug("No new outbound messages"); + getTiming().setStartAfter(Clock.getInstance().now()+1000); + } + + JobQueue.getInstance().addJob(this); + //JobQueue.getInstance().addJob(new FetchOutNetMessageJob(_facade)); + } + + private void processMessage(OutNetMessage msg) { + JobQueue.getInstance().addJob(new GetBidsJob(_facade, msg)); + } +} diff --git a/router/java/src/net/i2p/router/transport/GetBidsJob.java b/router/java/src/net/i2p/router/transport/GetBidsJob.java new file mode 100644 index 0000000000..0adaf91bbb --- /dev/null +++ b/router/java/src/net/i2p/router/transport/GetBidsJob.java @@ -0,0 +1,81 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.List; + +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageSelector; +import net.i2p.router.OutNetMessage; +import net.i2p.router.ProfileManager; +import net.i2p.router.Shitlist; +import net.i2p.router.Router; +import net.i2p.data.Hash; +import net.i2p.util.Log; + +/** + * Retrieve a set of bids for a particular outbound message, and if any are found + * that meet the message's requirements, register the message as in process and + * pass it on to the transport for processing + * + */ +public class GetBidsJob extends JobImpl { + private static Log _log = new Log(GetBidsJob.class); + private CommSystemFacadeImpl _facade; + private OutNetMessage _msg; + + public GetBidsJob(CommSystemFacadeImpl facade, OutNetMessage msg) { + super(); + _facade = facade; + _msg = msg; + } + + public String getName() { return "Fetch bids for a message to be delivered"; } + public void runJob() { + Hash to = _msg.getTarget().getIdentity().getHash(); + if (Shitlist.getInstance().isShitlisted(to)) { + _log.warn("Attempt to send a message to a shitlisted peer - " + to); + fail(); + return; + } + + Hash us = Router.getInstance().getRouterInfo().getIdentity().getHash(); + if (_msg.getTarget().getIdentity().getHash().equals(us)) { + _log.error("wtf, send a message to ourselves? nuh uh. msg = " + _msg, getAddedBy()); + fail(); + return; + } + + List bids = _facade.getBids(_msg); + if (bids.size() <= 0) { + _log.warn("No bids available for the message " + _msg); + fail(); + } else { + TransportBid bid = (TransportBid)bids.get(0); + bid.getTransport().send(_msg); + } + } + + + private void fail() { + if (_msg.getOnFailedSendJob() != null) { + JobQueue.getInstance().addJob(_msg.getOnFailedSendJob()); + } + if (_msg.getOnFailedReplyJob() != null) { + JobQueue.getInstance().addJob(_msg.getOnFailedReplyJob()); + } + MessageSelector selector = _msg.getReplySelector(); + if (selector != null) { + OutboundMessageRegistry.getInstance().unregisterPending(_msg); + } + + ProfileManager.getInstance().messageFailed(_msg.getTarget().getIdentity().getHash()); + } +} diff --git a/router/java/src/net/i2p/router/transport/OutboundMessageRegistry.java b/router/java/src/net/i2p/router/transport/OutboundMessageRegistry.java new file mode 100644 index 0000000000..8b2384885a --- /dev/null +++ b/router/java/src/net/i2p/router/transport/OutboundMessageRegistry.java @@ -0,0 +1,315 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageHistory; +import net.i2p.router.MessageSelector; +import net.i2p.router.OutNetMessage; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +public class OutboundMessageRegistry { + private final static Log _log = new Log(OutboundMessageRegistry.class); + private static final OutboundMessageRegistry _instance = new OutboundMessageRegistry(); + public static OutboundMessageRegistry getInstance() { return _instance; } + private TreeMap _pendingMessages; + + private final static long CLEANUP_DELAY = 1000*5; // how often to expire pending unreplied messages + + private OutboundMessageRegistry() { + _pendingMessages = new TreeMap(); + JobQueue.getInstance().addJob(new CleanupPendingMessagesJob()); + } + + public List getOriginalMessages(I2NPMessage message) { + + HashSet matches = new HashSet(4); + long beforeSync = Clock.getInstance().now(); + + Map messages = null; + synchronized (_pendingMessages) { + messages = (Map)_pendingMessages.clone(); + } + + long matchTime = 0; + long continueTime = 0; + int numMessages = messages.size(); + + long afterSync1 = Clock.getInstance().now(); + + ArrayList matchedRemove = new ArrayList(32); + for (Iterator iter = messages.keySet().iterator(); iter.hasNext(); ) { + Long exp = (Long)iter.next(); + OutNetMessage msg = (OutNetMessage)messages.get(exp); + MessageSelector selector = msg.getReplySelector(); + if (selector != null) { + long before = Clock.getInstance().now(); + boolean isMatch = selector.isMatch(message); + long after = Clock.getInstance().now(); + long diff = after-before; + if (diff > 100) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Matching with selector took too long (" + diff + "ms) : " + selector.getClass().getName()); + } + matchTime += diff; + + if (isMatch) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Selector matches [" + selector); + matches.add(msg); + long beforeCon = Clock.getInstance().now(); + boolean continueMatching = selector.continueMatching(); + long afterCon = Clock.getInstance().now(); + long diffCon = afterCon - beforeCon; + if (diffCon > 100) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error continueMatching on a match took too long (" + diffCon + "ms) : " + selector.getClass().getName()); + } + continueTime += diffCon; + + if (continueMatching) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Continue matching"); + // noop + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Stop matching selector " + selector + " for message " + msg.getMessage().getClass().getName()); + matchedRemove.add(exp); + } + } else { + //_log.debug("Selector does not match [" + selector + "]"); + } + } + } + long afterSearch = Clock.getInstance().now(); + + for (Iterator iter = matchedRemove.iterator(); iter.hasNext(); ) { + Long expiration = (Long)iter.next(); + OutNetMessage m = null; + long before = Clock.getInstance().now(); + synchronized (_pendingMessages) { + m = (OutNetMessage)_pendingMessages.remove(expiration); + } + long diff = Clock.getInstance().now() - before; + if (diff > 500) + _log.error("Took too long syncing on remove (" + diff + "ms"); + + if (m != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Removing message with selector " + m.getReplySelector().getClass().getName() + " :" + m.getReplySelector().toString()); + } + } + + long delay = Clock.getInstance().now() - beforeSync; + long search = afterSearch - afterSync1; + long sync = afterSync1 - beforeSync; + + int level = Log.DEBUG; + if (delay > 1000) + level = Log.ERROR; + if (_log.shouldLog(level)) { + _log.log(level, "getMessages took " + delay + "ms with search time of " + search + "ms (match: " + matchTime + "ms, continue: " + continueTime + "ms, #: " + numMessages + ") and sync time of " + sync + "ms for " + matchedRemove.size() + " removed, " + matches.size() + " matches"); + } + + return new ArrayList(matches); + } + + public void registerPending(OutNetMessage msg) { + if (msg == null) { + throw new IllegalArgumentException("Null OutNetMessage specified? wtf"); + } else if (msg.getMessage() == null) { + throw new IllegalArgumentException("OutNetMessage doesn't contain an I2NPMessage? wtf"); + } + + long beforeSync = Clock.getInstance().now(); + long afterSync1 = 0; + long afterDone = 0; + try { + OutNetMessage oldMsg = null; + synchronized (_pendingMessages) { + if (_pendingMessages.containsValue(msg)) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Not adding an already pending message: " + msg.getMessage().getUniqueId() + "\n: " + msg, new Exception("Duplicate message registration")); + return; + } + + long l = msg.getExpiration(); + while (_pendingMessages.containsKey(new Long(l))) + l++; + _pendingMessages.put(new Long(l), msg); + } + afterSync1 = Clock.getInstance().now(); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Register pending: " + msg.getReplySelector().getClass().getName() + " for " + msg.getMessage().getClass().getName() + ": " + msg.getReplySelector().toString(), new Exception("Register pending")); + afterDone = Clock.getInstance().now(); + } finally { + long delay = Clock.getInstance().now() - beforeSync; + long sync1 = afterSync1 - beforeSync; + long done = afterDone - afterSync1; + String warn = delay + "ms (sync = " + sync1 + "ms, done = " + done + "ms)"; + if (delay > 1000) { + _log.error("Synchronizing in the registry.register took too long! " + warn); + MessageHistory.getInstance().messageProcessingError(msg.getMessage().getUniqueId(), msg.getMessage().getClass().getName(), "RegisterPending took too long: " + warn); + } else { + _log.debug("Synchronizing in the registry.register was quick: " + warn); + } + } + //_log.debug("* Register called of " + msg + "\n\nNow pending are: " + renderStatusHTML(), new Exception("who registered a new one?")); + } + + public void unregisterPending(OutNetMessage msg) { + long beforeSync = Clock.getInstance().now(); + try { + synchronized (_pendingMessages) { + if (_pendingMessages.containsValue(msg)) { + Long found = null; + for (Iterator iter = _pendingMessages.keySet().iterator(); iter.hasNext();) { + Long exp = (Long)iter.next(); + Object val = _pendingMessages.get(exp); + if (val.equals(msg)) { + found = exp; + break; + } + } + if (found != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Unregistered message " + msg.getReplySelector() + ": " + msg, new Exception("Who unregistered?")); + _pendingMessages.remove(found); + } else { + _log.error("Arg, couldn't find the message that we... thought we could find?", new Exception("WTF")); + } + } + } + } finally { + long delay = Clock.getInstance().now() - beforeSync; + String warn = delay + "ms"; + if (delay > 1000) { + _log.error("Synchronizing in the registry.unRegister took too long! " + warn); + MessageHistory.getInstance().messageProcessingError(msg.getMessage().getUniqueId(), msg.getMessage().getClass().getName(), "Unregister took too long: " + warn); + } else { + _log.debug("Synchronizing in the registry.unRegister was quick: " + warn); + } + } + } + + public String renderStatusHTML() { + StringBuffer buf = new StringBuffer(8192); + buf.append("

Pending messages

\n"); + Map msgs = null; + synchronized (_pendingMessages) { + msgs = (Map)_pendingMessages.clone(); + } + buf.append("
    "); + for (Iterator iter = msgs.keySet().iterator(); iter.hasNext();) { + Long exp = (Long)iter.next(); + OutNetMessage msg = (OutNetMessage)msgs.get(exp); + buf.append("
  • ").append(msg.getMessage().getClass().getName()).append(": expiring on ").append(new Date(exp.longValue())); + if (msg.getReplySelector() != null) + buf.append(" with reply selector ").append(msg.getReplySelector().toString()); + else + buf.append(" with NO reply selector? WTF!"); + buf.append("
  • \n"); + } + buf.append("
"); + return buf.toString(); + } + + /** + * Cleanup any messages that were pending replies but have expired + * + */ + private class CleanupPendingMessagesJob extends JobImpl { + public CleanupPendingMessagesJob() { + super(); + } + + public String getName() { return "Cleanup any messages that timed out"; } + public void runJob() { + List toRemove = new ArrayList(); + long now = Clock.getInstance().now(); + Map messages = null; + synchronized (_pendingMessages) { + messages = (Map)_pendingMessages.clone(); + } + long afterCreate = Clock.getInstance().now(); + + for (Iterator iter = messages.keySet().iterator(); iter.hasNext(); ) { + Long exp = (Long)iter.next(); + OutNetMessage msg = (OutNetMessage)messages.get(exp); + if (msg.getExpiration() < now) { + toRemove.add(exp); + } + } + long findRemove = Clock.getInstance().now(); + + long removeTime = 0; + long loopTime = 0; + + for (Iterator iter = toRemove.iterator(); iter.hasNext(); ) { + long beforeRemove = Clock.getInstance().now(); + Long exp = (Long)iter.next(); + OutNetMessage msg = null; + synchronized (_pendingMessages) { + msg = (OutNetMessage)_pendingMessages.remove(exp); + } + long afterRemove = Clock.getInstance().now(); + long diff = afterRemove - beforeRemove; + + if (diff > 500) + _log.error("Synchronize during remove took too long " + diff + "ms"); + removeTime += diff; + + if (msg != null) { + MessageHistory.getInstance().replyTimedOut(msg); + Job fail = msg.getOnFailedReplyJob(); + if (fail != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Removing message with selector " + msg.getReplySelector() + ": " + msg.getMessage().getClass().getName() + " and firing fail job: " + fail.getClass().getName()); + JobQueue.getInstance().addJob(fail); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Removing message with selector " + msg.getReplySelector() + " and not firing any job"); + } + } + long doneLoop = Clock.getInstance().now(); + long ldiff = doneLoop - beforeRemove; + if (ldiff > 500) + _log.error("Loop took too long [" + ldiff + "ms]"); + loopTime += ldiff; + } + + long cleanupDelay = Clock.getInstance().now() - now; + long findTime = findRemove - afterCreate; + long syncTime = afterCreate - now; + String warn = cleanupDelay + "ms (syncTime = " + syncTime + "ms, findTime =" + findTime + "ms, removeTime = " + removeTime + "ms, loopTime = " + loopTime + ")"; + if (cleanupDelay > 1000) { + _log.error("Cleanup took too long! " + warn); + // yes, the following is a kludge, as its not specific to a particular message but to a whole series of messages + MessageHistory.getInstance().messageProcessingError(-1, OutboundMessageRegistry.CleanupPendingMessagesJob.class.getName(), "Cleanup took too long: " + warn); + } else { + _log.debug("Cleanup was quick: " + warn); + } + + requeue(CLEANUP_DELAY); + } + } +} diff --git a/router/java/src/net/i2p/router/transport/Transport.java b/router/java/src/net/i2p/router/transport/Transport.java new file mode 100644 index 0000000000..4ef8eaefc1 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/Transport.java @@ -0,0 +1,41 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.RouterInfo; +import net.i2p.data.RouterAddress; +import net.i2p.router.OutNetMessage; + +import java.util.Properties; +import java.util.Set; + +/** + * Defines a way to send a message to another peer and start listening for messages + * + */ +public interface Transport { + public TransportBid bid(RouterInfo toAddress, long dataSize); + /** + * Asynchronously send the message as requested in the message and, if the + * send is successful, queue up any msg.getOnSendJob job, and register it + * with the OutboundMessageRegistry (if it has a reply selector). If the + * send fails, queue up any msg.getOnFailedSendJob + * + */ + public void send(OutNetMessage msg); + public RouterAddress startListening(); + public void stopListening(); + public void rotateAddresses(); + public Set getCurrentAddresses(); + public void addAddressInfo(Properties infoForNewAddress); + public void setListener(TransportEventListener listener); + public String getStyle(); + + public String renderStatusHTML(); +} diff --git a/router/java/src/net/i2p/router/transport/TransportBid.java b/router/java/src/net/i2p/router/transport/TransportBid.java new file mode 100644 index 0000000000..1bada1675a --- /dev/null +++ b/router/java/src/net/i2p/router/transport/TransportBid.java @@ -0,0 +1,76 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.RouterInfo; + +import java.util.Date; + +/** + * Provide a bid for how much it would "cost" to transfer a message of a + * particular peer + * + */ +public class TransportBid { + private int _latencyMs; + private int _bandwidthBytes; + private int _msgSize; + private RouterInfo _router; + private Date _bidExpiration; + private Transport _transport; + + public TransportBid() { + setLatencyMs(-1); + setBandwidthBytes(-1); + setMessageSize(-1); + setRouter(null); + setExpiration(null); + setTransport(null); + } + + /** + * How long this transport thinks it would take to send the message + */ + public int getLatencyMs() { return _latencyMs; } + public void setLatencyMs(int milliseconds) { _latencyMs = milliseconds; } + + /** + * How many bytes the transport thinks it would need to send to transfer the + * message successfully + * + */ + public int getBandwidthBytes() { return _bandwidthBytes; } + public void setBandwidthBytes(int numBytes) { _bandwidthBytes = numBytes; } + + /** + * How large the message in question is, in bytes + * + */ + public int getMessageSize() { return _msgSize; } + public void setMessageSize(int numBytes) { _msgSize = numBytes; } + + /** + * Router to which the message is to be sent + * + */ + public RouterInfo getRouter() { return _router; } + public void setRouter(RouterInfo router) { _router = router; } + + /** + * Specifies how long this bid is "good for" + */ + public Date getExpiration() { return _bidExpiration; } + public void setExpiration(Date expirationDate) { _bidExpiration = expirationDate; } + + /** + * Specifies the transport that offered this bid + */ + public Transport getTransport() { return _transport; } + public void setTransport(Transport transport) { _transport = transport; } +} diff --git a/router/java/src/net/i2p/router/transport/TransportEventListener.java b/router/java/src/net/i2p/router/transport/TransportEventListener.java new file mode 100644 index 0000000000..bd98ee9da3 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/TransportEventListener.java @@ -0,0 +1,18 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.i2np.I2NPMessage; + +import net.i2p.data.RouterIdentity; +import net.i2p.data.Hash; + +public interface TransportEventListener { + public void messageReceived(I2NPMessage message, RouterIdentity fromRouter, Hash fromRouterHash); +} diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java new file mode 100644 index 0000000000..dc32444778 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/TransportImpl.java @@ -0,0 +1,231 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.LinkedList; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.data.RouterAddress; +import net.i2p.data.RouterIdentity; +import net.i2p.data.RouterInfo; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.router.Job; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageSelector; +import net.i2p.router.OutNetMessage; +import net.i2p.router.OutNetMessagePool; +import net.i2p.router.ProfileManager; +import net.i2p.router.MessageHistory; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import net.i2p.stat.StatManager; + +/** + * Defines a way to send a message to another peer and start listening for messages + * + */ +public abstract class TransportImpl implements Transport { + private final static Log _log = new Log(TransportImpl.class); + private TransportEventListener _listener; + private Set _currentAddresses; + private List _sendPool; + + static { + StatManager.getInstance().createFrequencyStat("transport.sendMessageFailureFrequency", "How often do we fail to send messages?", "Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("transport.sendMessageSize", "How large are the messages sent?", "Transport", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("transport.receiveMessageSize", "How large are the messages received?", "Transport", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("transport.sendProcessingTime", "How long does it take from noticing that we want to send the message to having it completely sent (successfully or failed)?", "Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + + public TransportImpl() { + _sendPool = new LinkedList(); + _currentAddresses = new HashSet(); + } + + public OutNetMessage getNextMessage() { + OutNetMessage msg = null; + synchronized (_sendPool) { + if (_sendPool.size() <= 0) return null; + msg = (OutNetMessage)_sendPool.remove(0); // use priority queues later + } + msg.beginSend(); + return msg; + } + + public void afterSend(OutNetMessage msg, boolean sendSuccessful) { + boolean log = false; + msg.timestamp("afterSend(" + sendSuccessful + ")"); + if (!sendSuccessful) + msg.transportFailed(getStyle()); + + long lifetime = msg.getLifetime(); + if (lifetime > 5000) { + if (_log.shouldLog(Log.WARN)) + _log.warn("afterSend: [success=" + sendSuccessful + "]\n" + msg.toString()); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("afterSend: [success=" + sendSuccessful + "]\n" + msg.toString()); + } + + if (sendSuccessful) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Send message " + msg.getMessage().getClass().getName() + " to " + msg.getTarget().getIdentity().getHash().toBase64() + " with transport " + getStyle() + " successfully"); + Job j = msg.getOnSendJob(); + if (j != null) JobQueue.getInstance().addJob(j); + log = true; + //NetworkDatabaseFacade.getInstance().peerReachable(msg.getTarget().getIdentity().getHash()); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Failed to send message " + msg.getMessage().getClass().getName() + " to " + msg.getTarget().getIdentity().getHash().toBase64() + " with transport " + getStyle() + " (details: " + msg + ")"); + if ( (msg.getExpiration() <= 0) || (msg.getExpiration() > Clock.getInstance().now()) ) { + // this may not be the last transport available - keep going + OutNetMessagePool.getInstance().add(msg); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("No more time left (" + new Date(msg.getExpiration()) + ", expiring without sending successfully the " + msg.getMessage().getClass().getName()); + if (msg.getOnFailedSendJob() != null) + JobQueue.getInstance().addJob(msg.getOnFailedSendJob()); + MessageSelector selector = msg.getReplySelector(); + if (selector != null) { + OutboundMessageRegistry.getInstance().unregisterPending(msg); + } + log = true; + } + } + + if (log) { + I2NPMessage dmsg = msg.getMessage(); + String type = dmsg.getClass().getName(); + MessageHistory.getInstance().sendMessage(type, dmsg.getUniqueId(), dmsg.getMessageExpiration(), msg.getTarget().getIdentity().getHash(), sendSuccessful); + } + + long now = Clock.getInstance().now(); + long sendTime = now - msg.getSendBegin(); + long allTime = now - msg.getCreated(); + if (allTime > 5*1000) { + if (_log.shouldLog(Log.INFO)) + _log.info("Took too long from preperation to afterSend(ok? " + sendSuccessful + "): " + allTime + "ms " + " after failing on: " + msg.getFailedTransports() + " and succeeding on " + getStyle()); + if (allTime > 60*1000) { + // WTF!!@# + if (_log.shouldLog(Log.WARN)) + _log.warn("WTF, more than a minute slow? " + msg.getMessage().getClass().getName() + " of id " + msg.getMessage().getUniqueId() + " (send begin on " + new Date(msg.getSendBegin()) + " / created on " + new Date(msg.getCreated()) + "): " + msg, msg.getCreatedBy()); + MessageHistory.getInstance().messageProcessingError(msg.getMessage().getUniqueId(), msg.getMessage().getClass().getName(), "Took too long to send [" + allTime + "ms]"); + } + } + + StatManager.getInstance().addRateData("transport.sendProcessingTime", msg.getLifetime(), msg.getLifetime()); + + if (sendSuccessful) { + ProfileManager.getInstance().messageSent(msg.getTarget().getIdentity().getHash(), getStyle(), sendTime, msg.getMessageSize()); + StatManager.getInstance().addRateData("transport.sendMessageSize", msg.getMessageSize(), sendTime); + } else { + ProfileManager.getInstance().messageFailed(msg.getTarget().getIdentity().getHash(), getStyle()); + StatManager.getInstance().updateFrequency("transport.sendMessageFailureFrequency"); + } + } + + /** + * Asynchronously send the message as requested in the message and, if the + * send is successful, queue up any msg.getOnSendJob job, and register it + * with the OutboundMessageRegistry (if it has a reply selector). If the + * send fails, queue up any msg.getOnFailedSendJob + * + */ + public void send(OutNetMessage msg) { + boolean duplicate = false; + synchronized (_sendPool) { + if (_sendPool.contains(msg)) + duplicate = true; + else + _sendPool.add(msg); + } + if (duplicate) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Message already is in the queue? wtf. msg = " + msg, new Exception("wtf, requeued?")); + } + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Message added to send pool"); + outboundMessageReady(); + if (_log.shouldLog(Log.INFO)) + _log.debug("OutboundMessageReady called"); + } + /** + * This message is called whenever a new message is added to the send pool, + * and it should not block + */ + protected abstract void outboundMessageReady(); + + public void messageReceived(I2NPMessage inMsg, RouterIdentity remoteIdent, Hash remoteIdentHash, long msToReceive, int bytesReceived) { + if (_log.shouldLog(Log.INFO)) { + StringBuffer buf = new StringBuffer(128); + buf.append("Message received: ").append(inMsg.getClass().getName()); + buf.append(" in ").append(msToReceive).append("ms containing ").append(bytesReceived).append(" bytes "); + buf.append(" from "); + if (remoteIdentHash != null) { + buf.append(remoteIdentHash.toBase64()); + } else if (remoteIdent != null) { + buf.append(remoteIdent.getHash().toBase64()); + } else { + buf.append("[unknown]"); + } + buf.append(" and forwarding to listener: "); + if (_listener != null) + buf.append(_listener); + + _log.info(buf.toString()); + } + + if (remoteIdent != null) + remoteIdentHash = remoteIdent.getHash(); + if (remoteIdentHash != null) { + ProfileManager.getInstance().messageReceived(remoteIdentHash, getStyle(), msToReceive, bytesReceived); + StatManager.getInstance().addRateData("transport.receiveMessageSize", bytesReceived, msToReceive); + } + + //// this functionality is built into the InNetMessagePool + //String type = inMsg.getClass().getName(); + //MessageHistory.getInstance().receiveMessage(type, inMsg.getUniqueId(), inMsg.getMessageExpiration(), remoteIdentHash, true); + + if (_listener != null) { + _listener.messageReceived(inMsg, remoteIdent, remoteIdentHash); + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("WTF! Null listener! this = " + toString(), new Exception("Null listener")); + } + } + + /** + * Pull the first workable target address for this transport + * + */ + protected RouterAddress getTargetAddress(RouterInfo address) { + if (address == null) return null; + for (Iterator iter = address.getAddresses().iterator(); iter.hasNext(); ) { + RouterAddress addr = (RouterAddress)iter.next(); + if (getStyle().equals(addr.getTransportStyle())) + return addr; + } + return null; + } + + public Set getCurrentAddresses() { return _currentAddresses; } + protected void addCurrentAddress(RouterAddress address) { _currentAddresses.add(address); } + protected void removeCurrentAddress(RouterAddress address) { _currentAddresses.remove(address); } + public void setListener(TransportEventListener listener) { _listener = listener; } + + public String renderStatusHTML() { return null; } +} diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java new file mode 100644 index 0000000000..73fedb471c --- /dev/null +++ b/router/java/src/net/i2p/router/transport/TransportManager.java @@ -0,0 +1,261 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import net.i2p.data.Hash; +import net.i2p.data.RouterAddress; +import net.i2p.data.RouterIdentity; +import net.i2p.data.RouterInfo; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.i2np.DatabaseFindNearestMessage; +import net.i2p.data.i2np.DatabaseLookupMessage; +import net.i2p.data.i2np.DatabaseSearchReplyMessage; +import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.router.CommSystemFacade; +import net.i2p.router.InNetMessage; +import net.i2p.router.InNetMessagePool; +import net.i2p.router.OutNetMessage; +import net.i2p.router.Router; +import net.i2p.router.transport.phttp.PHTTPTransport; +import net.i2p.router.transport.tcp.TCPTransport; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +public class TransportManager implements TransportEventListener { + private static final Log _log = new Log(TransportManager.class); + private List _transports; + private List _addresses; + private SigningPrivateKey _myIdentitySigningKey; + + private final static String PROP_DISABLE_TCP = "i2np.tcp.disable"; + + public TransportManager(RouterInfo routerInfo, SigningPrivateKey routerSigningKey) { + _myIdentitySigningKey = routerSigningKey; + _transports = new ArrayList(); + _addresses = new ArrayList(); + } + + public void addTransport(Transport transport) { + if (transport == null) return; + _transports.add(transport); + transport.setListener(this); + } + + public void removeTransport(Transport transport) { + if (transport == null) return; + _transports.remove(transport); + transport.setListener(null); + } + + private void configTransports() { + RouterIdentity ident = Router.getInstance().getRouterInfo().getIdentity(); + Set addresses = CommSystemFacade.getInstance().createAddresses(); + RouterAddress tcpAddr = null; + RouterAddress phttpAddr = null; + for (Iterator iter = addresses.iterator(); iter.hasNext();) { + RouterAddress addr = (RouterAddress)iter.next(); + if (TCPTransport.STYLE.equals(addr.getTransportStyle())) { + tcpAddr = addr; + } + if (PHTTPTransport.STYLE.equals(addr.getTransportStyle())) { + phttpAddr = addr; + } + } + + String disableTCP = Router.getInstance().getConfigSetting(PROP_DISABLE_TCP); + if ( (disableTCP != null) && (Boolean.TRUE.toString().equalsIgnoreCase(disableTCP)) ) { + _log.info("Explicitly disabling the TCP transport!"); + } else { + Transport t = new TCPTransport(ident, _myIdentitySigningKey, tcpAddr); + t.setListener(this); + _transports.add(t); + } + Transport t = new PHTTPTransport(ident, _myIdentitySigningKey, phttpAddr); + t.setListener(this); + _transports.add(t); + } + + public void startListening() { + configTransports(); + _log.debug("Starting up the transport manager"); + for (int i = 0; i < _transports.size(); i++) { + Transport t = (Transport)_transports.get(i); + RouterAddress addr = t.startListening(); + if (addr != null) _addresses.add(addr); + _log.debug("Transport " + i + " (" + t.getStyle() + ") started"); + } + _log.debug("Done start listening on transports"); + } + + public void stopListening() { + for (int i = 0; i < _transports.size(); i++) { + ((Transport)_transports.get(i)).stopListening(); + } + _transports.clear(); + } + + private boolean isSupported(Set addresses, Transport t) { + for (Iterator iter = addresses.iterator(); iter.hasNext(); ) { + RouterAddress addr = (RouterAddress)iter.next(); + if (addr.getTransportStyle().equals(t.getStyle())) + return true; + } + return false; + } + + public List getBids(OutNetMessage msg) { + if (msg == null) + throw new IllegalArgumentException("Null message? no bidding on a null outNetMessage!"); + if (Router.getInstance().getRouterInfo().equals(msg.getTarget())) + throw new IllegalArgumentException("WTF, bids for a message bound to ourselves?"); + + HashSet bids = new HashSet(); + + Set addrs = msg.getTarget().getAddresses(); + Set failedTransports = msg.getFailedTransports(); + for (int i = 0; i < _transports.size(); i++) { + Transport t = (Transport)_transports.get(i); + if (failedTransports.contains(t.getStyle())) { + _log.debug("Skipping transport " + t.getStyle() + " as it already failed"); + continue; + } + // we always want to try all transports, in case there is a faster bidirectional one + // already connected (e.g. peer only has a public PHTTP address, but they've connected + // to us via TCP, send via TCP) + if (true || isSupported(addrs, t)) { + TransportBid bid = t.bid(msg.getTarget(), msg.getMessageSize()); + if (bid != null) { + bids.add(bid); + _log.debug("Transport " + t.getStyle() + " bid: " + bid); + } else { + _log.debug("Transport " + t.getStyle() + " did not produce a bid"); + } + } + } + List ordered = orderBids(bids, msg); + long delay = Clock.getInstance().now() - msg.getCreated(); + if (ordered.size() > 0) { + _log.debug("Winning bid: " + ((TransportBid)ordered.get(0)).getTransport().getStyle()); + if (delay > 5*1000) { + _log.info("Took too long to find this bid (" + delay + "ms)"); + } else { + _log.debug("Took a while to find this bid (" + delay + "ms)"); + } + } else { + _log.info("NO WINNING BIDS! peer: " + msg.getTarget()); + if (delay > 5*1000) { + _log.info("Took too long to fail (" + delay + "ms)"); + } else { + _log.debug("Took a while to fail (" + delay + "ms)"); + } + } + return ordered; + } + + private List orderBids(HashSet bids, OutNetMessage msg) { + // db messages should go as fast as possible, while the others + // should use as little bandwidth as possible. + switch (msg.getMessage().getType()) { + case DatabaseFindNearestMessage.MESSAGE_TYPE: + case DatabaseLookupMessage.MESSAGE_TYPE: + case DatabaseSearchReplyMessage.MESSAGE_TYPE: + case DatabaseStoreMessage.MESSAGE_TYPE: + _log.debug("Ordering by fastest"); + return orderByFastest(bids, msg); + default: + _log.debug("Ordering by bandwidth"); + return orderByBandwidth(bids, msg); + } + } + + private int getCost(RouterInfo target, String transportStyle) { + for (Iterator iter = target.getAddresses().iterator(); iter.hasNext();) { + RouterAddress addr = (RouterAddress)iter.next(); + if (addr.getTransportStyle().equals(transportStyle)) + return addr.getCost(); + } + return 1; + } + + private List orderByFastest(HashSet bids, OutNetMessage msg) { + Map ordered = new TreeMap(); + for (Iterator iter = bids.iterator(); iter.hasNext(); ) { + TransportBid bid = (TransportBid)iter.next(); + int cur = bid.getLatencyMs(); + int cost = getCost(msg.getTarget(), bid.getTransport().getStyle()); + _log.debug("Bid latency: " + (cur*cost) + " for transport " + bid.getTransport().getStyle()); + while (ordered.containsKey(new Integer(cur*cost))) + cur++; + ordered.put(new Integer(cur*cost), bid); + } + List bidList = new ArrayList(ordered.size()); + for (Iterator iter = ordered.keySet().iterator(); iter.hasNext(); ) { + Object k = iter.next(); + bidList.add(ordered.get(k)); + } + return bidList; + } + private List orderByBandwidth(HashSet bids, OutNetMessage msg) { + Map ordered = new TreeMap(); + for (Iterator iter = bids.iterator(); iter.hasNext(); ) { + TransportBid bid = (TransportBid)iter.next(); + int cur = bid.getBandwidthBytes(); + int cost = getCost(msg.getTarget(), bid.getTransport().getStyle()); + _log.debug("Bid size: " + (cur*cost) + " for transport " + bid.getTransport().getStyle()); + while (ordered.containsKey(new Integer(cur*cost))) + cur++; + ordered.put(new Integer(cur*cost), bid); + } + List bidList = new ArrayList(ordered.size()); + for (Iterator iter = ordered.keySet().iterator(); iter.hasNext(); ) { + Object k = iter.next(); + bidList.add(ordered.get(k)); + } + return bidList; + } + + public void messageReceived(I2NPMessage message, RouterIdentity fromRouter, Hash fromRouterHash) { + _log.debug("I2NPMessage received: " + message.getClass().getName(), new Exception("Where did I come from again?")); + InNetMessage msg = new InNetMessage(); + msg.setFromRouter(fromRouter); + msg.setFromRouterHash(fromRouterHash); + msg.setMessage(message); + int num = InNetMessagePool.getInstance().add(msg); + _log.debug("Added to in pool: "+ num); + } + + public String renderStatusHTML() { + StringBuffer buf = new StringBuffer(); + buf.append("

Transport Manager

\n"); + buf.append("Listening on:
\n");
+	for (Iterator iter = _addresses.iterator(); iter.hasNext(); ) {
+	    RouterAddress addr = (RouterAddress)iter.next();
+	    buf.append(addr.toString()).append("\n\n");
+	}
+	buf.append("
\n"); + buf.append("
    \n"); + for (Iterator iter = _transports.iterator(); iter.hasNext(); ) { + Transport t = (Transport)iter.next(); + String str = t.renderStatusHTML(); + if (str != null) + buf.append("
  • ").append(str).append("
  • \n"); + } + buf.append("
\n"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/transport/TrivialBandwidthLimiter.java b/router/java/src/net/i2p/router/transport/TrivialBandwidthLimiter.java new file mode 100644 index 0000000000..83d56d00e3 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/TrivialBandwidthLimiter.java @@ -0,0 +1,202 @@ +package net.i2p.router.transport; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.RouterIdentity; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.Router; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Coordinate the bandwidth limiting across all classes of peers. Currently + * treats everything as open (aka doesn't limit) + * + */ +public class TrivialBandwidthLimiter extends BandwidthLimiter { + private final static Log _log = new Log(TrivialBandwidthLimiter.class); + private volatile long _maxReceiveBytesPerMinute; + private volatile long _maxSendBytesPerMinute; + private volatile long _lastResync; + private volatile long _lastReadConfig; + private volatile long _totalReceiveBytes; + private volatile long _totalSendBytes; + private volatile long _availableSend; + private volatile long _availableReceive; + + private final static String PROP_INBOUND_BANDWIDTH = "i2np.bandwidth.inboundBytesPerMinute"; + private final static String PROP_OUTBOUND_BANDWIDTH = "i2np.bandwidth.outboundBytesPerMinute"; + + private final static long MINUTE = 60*1000; + private final static long READ_CONFIG_DELAY = MINUTE; + + // max # bytes to store in the pool, in case we have lots of traffic we don't want to + // spike too hard + private static long MAX_IN_POOL = 10*1024; + private static long MAX_OUT_POOL = 10*1024; + + TrivialBandwidthLimiter() { + this(-1, -1); + } + TrivialBandwidthLimiter(long sendPerMinute, long receivePerMinute) { + _maxReceiveBytesPerMinute = receivePerMinute; + _maxSendBytesPerMinute = sendPerMinute; + _lastResync = Clock.getInstance().now(); + _lastReadConfig = _lastResync; + _totalReceiveBytes = 0; + _totalSendBytes = 0; + _availableReceive = receivePerMinute; + _availableSend = sendPerMinute; + MAX_IN_POOL = 10*_availableReceive; + MAX_OUT_POOL = 10*_availableSend; + + JobQueue.getInstance().addJob(new UpdateBWJob()); + updateLimits(); + _log.info("Initializing the limiter with maximum inbound [" + MAX_IN_POOL + "] outbound [" + MAX_OUT_POOL + "]"); + } + + public long getTotalSendBytes() { return _totalSendBytes; } + public long getTotalReceiveBytes() { return _totalReceiveBytes; } + + /** + * Return how many milliseconds to wait before receiving/processing numBytes from the peer + */ + public long calculateDelayInbound(RouterIdentity peer, int numBytes) { + if (_maxReceiveBytesPerMinute <= 0) return 0; + if (_availableReceive - numBytes > 0) { + // we have bytes available + return 0; + } else { + // we don't have sufficient bytes. + // the delay = (needed/numPerMinute) + long val = MINUTE*(numBytes-_availableReceive)/_maxReceiveBytesPerMinute; + _log.debug("DelayInbound: " + val + " for " + numBytes + " (avail=" + _availableReceive + ", max=" + _maxReceiveBytesPerMinute + ")"); + return val; + } + } + + /** + * Return how many milliseconds to wait before sending numBytes to the peer + */ + public long calculateDelayOutbound(RouterIdentity peer, int numBytes) { + if (_maxSendBytesPerMinute <= 0) return 0; + if (_availableSend - numBytes > 0) { + // we have bytes available + return 0; + } else { + // we don't have sufficient bytes. + // the delay = (needed/numPerMinute) + long val = MINUTE*(numBytes-_availableSend)/_maxSendBytesPerMinute; + _log.debug("DelayOutbound: " + val + " for " + numBytes + " (avail=" + _availableSend + ", max=" + _maxSendBytesPerMinute + ")"); + return val; + } + } + + /** + * Note that numBytes have been read from the peer + */ + public void consumeInbound(RouterIdentity peer, int numBytes) { + _totalReceiveBytes += numBytes; + _availableReceive -= numBytes; + } + + /** + * Note that numBytes have been sent to the peer + */ + public void consumeOutbound(RouterIdentity peer, int numBytes) { + _totalSendBytes += numBytes; + _availableSend -= numBytes; + } + + private void updateLimits() { + String inBwStr = Router.getInstance().getConfigSetting(PROP_INBOUND_BANDWIDTH); + String outBwStr = Router.getInstance().getConfigSetting(PROP_OUTBOUND_BANDWIDTH); + if (true) { + // DISABLED UNTIL THIS STUFF GETS A REVAMP + inBwStr = "-60"; + outBwStr = "-60"; + } + long oldReceive = _maxReceiveBytesPerMinute; + long oldSend = _maxSendBytesPerMinute; + + _log.debug("Read limits ["+inBwStr+" in, " + outBwStr + " out] vs current [" + oldReceive + " in, " + oldSend + " out]"); + + if ( (inBwStr != null) && (inBwStr.trim().length() > 0) ) { + try { + long in = Long.parseLong(inBwStr); + if (in >= 0) { + _maxReceiveBytesPerMinute = in; + MAX_IN_POOL = 10*_maxReceiveBytesPerMinute; + } + } catch (NumberFormatException nfe) { + _log.warn("Invalid inbound bandwidth limit [" + inBwStr + "], keeping as " + _maxReceiveBytesPerMinute); + } + } else { + _log.warn("Inbound bandwidth limits not specified in the config via " + PROP_INBOUND_BANDWIDTH); + } + if ( (outBwStr != null) && (outBwStr.trim().length() > 0) ) { + try { + long out = Long.parseLong(outBwStr); + if (out >= 0) { + _maxSendBytesPerMinute = out; + MAX_OUT_POOL = 10*_maxSendBytesPerMinute; + } + } catch (NumberFormatException nfe) { + _log.warn("Invalid outbound bandwidth limit [" + outBwStr + "], keeping as " + _maxSendBytesPerMinute); + } + } else { + _log.warn("Outbound bandwidth limits not specified in the config via " + PROP_OUTBOUND_BANDWIDTH); + } + + if ( (oldReceive != _maxReceiveBytesPerMinute) || (oldSend != _maxSendBytesPerMinute) ) { + _log.info("Max receive bytes per minute: " + _maxReceiveBytesPerMinute + ", max send per minute: " + _maxSendBytesPerMinute); + _availableReceive = _maxReceiveBytesPerMinute; + _availableSend = _maxSendBytesPerMinute; + } + } + + private class UpdateBWJob extends JobImpl { + public UpdateBWJob() { + getTiming().setStartAfter(Clock.getInstance().now() + MINUTE); + } + public String getName() { return "Update bandwidth available"; } + + public void runJob() { + long now = Clock.getInstance().now(); + long numMinutes = ((now - _lastResync)/MINUTE) + 1; + _availableReceive += numMinutes * _maxReceiveBytesPerMinute; + _availableSend += numMinutes * _maxSendBytesPerMinute; + _lastResync = now; + + _log.debug("Adding " + (numMinutes*_maxReceiveBytesPerMinute) + " bytes to availableReceive"); + _log.debug("Adding " + (numMinutes*_maxSendBytesPerMinute) + " bytes to availableSend"); + + // if we're huge, trim + if (_availableReceive > MAX_IN_POOL) { + _log.debug("Trimming available receive to " + MAX_IN_POOL); + _availableReceive = MAX_IN_POOL; + } + if (_availableSend > MAX_OUT_POOL) { + _log.debug("Trimming available send to " + MAX_OUT_POOL); + _availableSend = MAX_OUT_POOL; + } + + getTiming().setStartAfter(now + MINUTE); + JobQueue.getInstance().addJob(UpdateBWJob.this); + + // now update the bandwidth limits, in case they've changed + if (now > _lastReadConfig + READ_CONFIG_DELAY) { + updateLimits(); + _lastReadConfig = now; + } + } + } +} diff --git a/router/java/src/net/i2p/router/transport/phttp/PHTTPPoller.java b/router/java/src/net/i2p/router/transport/phttp/PHTTPPoller.java new file mode 100644 index 0000000000..a31fcb89c4 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/phttp/PHTTPPoller.java @@ -0,0 +1,236 @@ +package net.i2p.router.transport.phttp; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Date; + +import net.i2p.crypto.DSAEngine; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Signature; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.I2NPMessageException; +import net.i2p.data.i2np.I2NPMessageHandler; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; +import net.i2p.util.Clock; +import net.i2p.util.I2PThread; +import net.i2p.router.transport.BandwidthLimiter; +import net.i2p.router.Router; + +class PHTTPPoller { + private final static Log _log = new Log(PHTTPPoller.class); + private PHTTPTransport _transport; + private URL _pollURL; + private Poller _poller; + + public PHTTPPoller(PHTTPTransport transport) { + _transport = transport; + _pollURL = null; + _poller = new Poller(); + } + + public void startPolling() { + try { + _pollURL = new URL(_transport.getMyPollURL()); + } catch (MalformedURLException mue) { + _log.error("Invalid polling URL [" + _transport.getMyPollURL() + "]", mue); + return; + } + Thread t = new I2PThread(_poller); + t.setName("HTTP Poller"); + t.setDaemon(true); + t.setPriority(I2PThread.MIN_PRIORITY); + t.start(); + } + + public void stopPolling() { + _poller.stopPolling(); + } + + private byte[] getAuthData() { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4); + long nonce = RandomSource.getInstance().nextInt(Integer.MAX_VALUE); + _log.debug("Creating nonce with value [" + nonce + "]"); + DataHelper.writeLong(baos, 4, nonce); + byte nonceData[] = baos.toByteArray(); + Signature sig = DSAEngine.getInstance().sign(nonceData, _transport.getMySigningKey()); + baos = new ByteArrayOutputStream(512); + DataHelper.writeLong(baos, 4, nonce); + sig.writeBytes(baos); + byte data[] = baos.toByteArray(); + return data; + } catch (NumberFormatException nfe) { + _log.error("Error writing the authentication data", nfe); + return null; + } catch (DataFormatException dfe) { + _log.error("Error formatting the authentication data", dfe); + return null; + } catch (IOException ioe) { + _log.error("Error writing the authentication data", ioe); + return null; + } + } + + public final static String CONFIG_POLL = "i2np.phttp.shouldPoll"; + public final static boolean DEFAULT_POLL = false; + + static boolean shouldRejectMessages() { + String val = Router.getInstance().getConfigSetting(CONFIG_POLL); + if (null == val) { + return !DEFAULT_POLL; + } else { + return !("true".equals(val)); + } + } + + class Poller implements Runnable { + private boolean _running; + private I2NPMessageHandler _handler = new I2NPMessageHandler(); + public void run() { + _running = true; + // wait 5 seconds before starting to poll so we don't drop too many messages + try { Thread.sleep(10*1000); } catch (InterruptedException ie) {} + + _log.debug("Poller running with delay [" + _transport.getPollFrequencyMs() + "]"); + try { + while (_running) { + int numRead = getMessages(); + if (numRead > 0) + _log.info("# messages found: " + numRead); + try { Thread.sleep(_transport.getPollFrequencyMs()); } catch (InterruptedException ie) {} + } + } catch (Throwable t) { + _log.info("Error while polling", t); + } + } + + private int getMessages() { + // open the _pollURL, authenticate ourselves, and get any messages available + byte authData[] = getAuthData(); + if (authData == null) return 0; + + BandwidthLimiter.getInstance().delayOutbound(null, authData.length + 512); // HTTP overhead + + try { + _log.debug("Before opening " + _pollURL.toExternalForm()); + HttpURLConnection con = (HttpURLConnection)_pollURL.openConnection(); + // send the info + con.setRequestMethod("POST"); + con.setUseCaches(false); + con.setDoOutput(true); + con.setDoInput(true); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(authData.length + 64); + String target = _transport.getMyIdentity().getHash().toBase64(); + baos.write("target=".getBytes()); + baos.write(target.getBytes()); + baos.write("&".getBytes()); + baos.write(authData); + byte data[] = baos.toByteArray(); + //_log.debug("Data to be sent: " + Base64.encode(data)); + + con.setRequestProperty("Content-length", ""+data.length); + con.getOutputStream().write(data); + _log.debug("Data sent, before reading results of poll for [" + target + "]"); + + con.connect(); + + // fetch the results + int rc = con.getResponseCode(); + _log.debug("Response code: " + rc); + switch (rc) { + case 200: // ok + _log.debug("Polling can progress"); + break; + case 401: // signature failed + _log.error("Signature failed during polling???"); + return 0; + case 404: // not yet registered + _log.error("Not registered with the relay - reregistering (in case they failed)"); + _transport.registerWithRelay(); + return 0; + default: // unknown + _log.error("Invalid error code returned: " + rc); + return 0; + } + + InputStream in = con.getInputStream(); + Date peerTime = DataHelper.readDate(in); + long offset = peerTime.getTime() - System.currentTimeMillis(); + if (_transport.getTrustTime()) { + _log.info("Updating time offset to " + offset + " (old offset: " + Clock.getInstance().getOffset() + ")"); + Clock.getInstance().setOffset(offset); + } + + boolean shouldReject = shouldRejectMessages(); + if (shouldReject) { + _log.debug("Rejecting any messages [we just checked in so we could get the time]"); + return 0; + } + + int numMessages = (int)DataHelper.readLong(in, 2); + if ( (numMessages > 100) || (numMessages < 0) ) { + _log.error("Invalid # messages specified [" + numMessages + "], skipping"); + return 0; + } + + int bytesRead = 512; // HTTP overhead + + int numSuccessful = 0; + for (int i = 0; i < numMessages; i++) { + _log.debug("Receiving message " + (i+1) + " of "+ numMessages + " pending"); + long len = DataHelper.readLong(in, 4); + byte msgBuf[] = new byte[(int)len]; + int read = DataHelper.read(in, msgBuf); + if (read == -1) { + _log.error("Unable to read the message as we encountered an EOF"); + return i - 1; + } else if (read != len) { + _log.error("Unable to read the message fully [" + read + " read, " + len + " expected]"); + return i - 1; + } else { + bytesRead += 4 + read; + try { + I2NPMessage msg = _handler.readMessage(new ByteArrayInputStream(msgBuf)); + if (msg == null) { + _log.warn("PHTTP couldn't read a message from the peer out of a " + len + " byte buffer"); + } else { + _log.info("Receive message " + (i+1) + " of " + numMessages + ": " + msg.getClass().getName()); + _transport.messageReceived(msg, null, null, _handler.getLastReadTime(), (int)len); + numSuccessful++; + } + } catch (IOException ioe) { + _log.warn("Unable to read the message fully", ioe); + } catch (I2NPMessageException ime) { + _log.warn("Poorly formatted message", ime); + } + } + } + + BandwidthLimiter.getInstance().delayInbound(null, bytesRead); + + return numSuccessful; + } catch (Throwable t) { + _log.debug("Error polling", t); + return 0; + } + } + + public void stopPolling() { _running = false; } + } +} diff --git a/router/java/src/net/i2p/router/transport/phttp/PHTTPSender.java b/router/java/src/net/i2p/router/transport/phttp/PHTTPSender.java new file mode 100644 index 0000000000..29f923482c --- /dev/null +++ b/router/java/src/net/i2p/router/transport/phttp/PHTTPSender.java @@ -0,0 +1,283 @@ +package net.i2p.router.transport.phttp; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Date; +import java.util.Iterator; + +import net.i2p.data.RouterAddress; +import net.i2p.router.OutNetMessage; +import net.i2p.router.transport.BandwidthLimiter; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.I2PThread; + +class PHTTPSender { + private final static Log _log = new Log(PHTTPSender.class); + private PHTTPTransport _transport; + private volatile long _sendId = 0; + + public final static long RECHECK_DELAY = 1000; // 1 sec + public final static long HARD_TIMEOUT = 30*1000; // no timeouts > 30 seconds + + /** H(routerIdent).toBase64() of the target to receive the message */ + public final static String PARAM_SEND_TARGET = "target"; + /** # ms to wait for the message to be delivered before failing it */ + public final static String PARAM_SEND_TIMEOUTMS = "timeoutMs"; + /** # bytes to be sent in the message */ + public final static String PARAM_SEND_DATA_LENGTH = "dataLength"; + /** local time in ms */ + public final static String PARAM_SEND_TIME = "localTime"; + + private final static String PROP_STATUS = "status"; + private final static String STATUS_OK = "accepted"; + private final static String STATUS_PENDING = "pending"; + private final static String STATUS_CLOCKSKEW = "clockSkew_"; /** prefix for (remote-local) */ + + /** HTTP error code if the target is known and accepting messages */ + public final static int CODE_OK = 201; // created + /** HTTP error code if the target is not known or is not accepting messages */ + public final static int CODE_FAIL = 410; // gone + + /* the URL to check to see when the message is delivered */ + public final static String PROP_CHECK_URL = "statusCheckURL"; + + /** HTTP error code if the message was sent completely */ + public final static int CODE_NOT_PENDING = 410; // gone + /** HTTP error code if the message is still pending */ + public final static int CODE_PENDING = 204; // ok, but no content + + public PHTTPSender(PHTTPTransport transport) { + _transport = transport; + } + + public void send(OutNetMessage msg) { + _log.debug("Sending message " + msg.getMessage().getClass().getName() + " to " + msg.getTarget().getIdentity().getHash().toBase64()); + Thread t = new I2PThread(new Send(msg)); + t.setName("PHTTP Sender " + (_sendId++)); + t.setDaemon(true); + t.start(); + } + + class Send implements Runnable { + private OutNetMessage _msg; + public Send(OutNetMessage msg) { + _msg = msg; + } + public void run() { + boolean ok = false; + try { + ok = doSend(_msg); + } catch (IOException ioe) { + _log.error("Error sending the message", ioe); + } + _transport.afterSend(_msg, ok); + } + } + + private boolean doSend(OutNetMessage msg) throws IOException { + long delay = BandwidthLimiter.getInstance().calculateDelayOutbound(msg.getTarget().getIdentity(), (int)msg.getMessageSize()); + _log.debug("Delaying [" + delay + "ms]"); + try { Thread.sleep(delay); } catch (InterruptedException ie) {} + _log.debug("Continuing with sending"); + // now send + URL sendURL = getURL(msg); + if (sendURL == null) { + _log.debug("No URL to send"); + return false; + } else { + _log.debug("Sending to " + sendURL.toExternalForm()); + HttpURLConnection con = (HttpURLConnection)sendURL.openConnection(); + // send the info + con.setRequestMethod("POST"); + con.setUseCaches(false); + con.setDoOutput(true); + con.setDoInput(true); + + byte data[] = getData(msg); + if (data == null) return false; + + BandwidthLimiter.getInstance().delayOutbound(msg.getTarget().getIdentity(), data.length+512); // HTTP overhead + + con.setRequestProperty("Content-length", ""+data.length); + OutputStream out = con.getOutputStream(); + out.write(data); + out.flush(); + _log.debug("Data sent, before reading"); + + // fetch the results + String checkURL = getCheckURL(con); + if (checkURL != null) { + _log.debug("Message sent"); + return checkDelivery(checkURL, msg); + } else { + _log.warn("Target not known or unable to send to " + msg.getTarget().getIdentity().getHash().toBase64()); + return false; + } + } + } + + private String getCheckURL(HttpURLConnection con) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream())); + String statusLine = reader.readLine(); + if (statusLine == null) { + _log.error("Null response line when checking URL"); + return null; + } + boolean statusOk = false; + if (!statusLine.startsWith(PROP_STATUS)) { + _log.warn("Response does not begin with status [" + statusLine + "]"); + return null; + } else { + String statVal = statusLine.substring(PROP_STATUS.length() + 1); + statusOk = STATUS_OK.equals(statVal); + + if (!statusOk) { + _log.info("Status was not ok for sending [" + statVal + "]"); + return null; + } + } + + String checkURL = reader.readLine(); + if (!checkURL.startsWith(PROP_CHECK_URL)) { + _log.warn("Incorrect OK response: " + checkURL); + return null; + } else { + String checkURLStr = checkURL.substring(PROP_CHECK_URL.length()+1); + _log.debug("Check URL = [" + checkURLStr + "]"); + return checkURLStr; + } + } + + private boolean checkDelivery(String checkURLStr, OutNetMessage msg) { + long now = Clock.getInstance().now(); + long expiration = msg.getExpiration(); + if (expiration <= now) + expiration = now + HARD_TIMEOUT; + + _log.debug("Check delivery [expiration = " + new Date(expiration) + "]"); + try { + URL checkStatusURL = new URL(checkURLStr); + long delay = RECHECK_DELAY; + do { + BandwidthLimiter.getInstance().delayOutbound(msg.getTarget().getIdentity(), 512); // HTTP overhead + BandwidthLimiter.getInstance().delayInbound(msg.getTarget().getIdentity(), 512); // HTTP overhead + + _log.debug("Checking delivery at " + checkURLStr); + HttpURLConnection con = (HttpURLConnection)checkStatusURL.openConnection(); + con.setRequestMethod("GET"); + //con.setInstanceFollowRedirects(false); // kaffe doesn't support this (yet) + con.setDoInput(true); + con.setDoOutput(false); + con.setUseCaches(false); + con.connect(); + + boolean isPending = getIsPending(con); + if (!isPending) { + _log.info("Check delivery successful for message " + msg.getMessage().getClass().getName()); + return true; + } + + if (now + delay > expiration) + delay = expiration - now - 30; // 30 = kludgy # for the next 4 statements + _log.debug("Still pending (wait " + delay + "ms)"); + Thread.sleep(delay); + //delay += RECHECK_DELAY; + + now = Clock.getInstance().now(); + } while (now < expiration); + _log.warn("Timeout for checking delivery to " + checkURLStr + " for message " + msg.getMessage().getClass().getName()); + } catch (Throwable t) { + _log.debug("Error checking for delivery", t); + } + return false; + } + + private boolean getIsPending(HttpURLConnection con) throws IOException { + int len = con.getContentLength(); + int rc = con.getResponseCode(); + BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream())); + String statusLine = reader.readLine(); + if (statusLine == null) { + _log.warn("Server didn't send back a status line [len = " + len + ", rc = " + rc + "]"); + return false; + } + boolean statusPending = false; + if (!statusLine.startsWith(PROP_STATUS)) { + _log.warn("Response does not begin with status [" + statusLine + "]"); + return false; + } else { + String statVal = statusLine.substring(PROP_STATUS.length() + 1); + statusPending = STATUS_PENDING.equals(statVal); + if (statVal.startsWith(STATUS_CLOCKSKEW)) { + long skew = Long.MAX_VALUE; + String skewStr = statVal.substring(STATUS_CLOCKSKEW.length()+1); + try { + skew = Long.parseLong(skewStr); + } catch (Throwable t) { + _log.error("Unable to decode the clock skew [" + skewStr + "]"); + skew = Long.MAX_VALUE; + } + _log.error("Clock skew talking with phttp relay: " + skew + "ms (remote-local)"); + } + return statusPending; + } + } + + private byte[] getData(OutNetMessage msg) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(msg.getMessageSize() + 64)); + String target = msg.getTarget().getIdentity().getHash().toBase64(); + StringBuffer buf = new StringBuffer(); + buf.append(PARAM_SEND_TARGET).append('=').append(target).append('&'); + buf.append(PARAM_SEND_TIMEOUTMS).append('=').append(msg.getExpiration() - Clock.getInstance().now()).append('&'); + buf.append(PARAM_SEND_DATA_LENGTH).append('=').append(msg.getMessageSize()).append('&'); + buf.append(PARAM_SEND_TIME).append('=').append(Clock.getInstance().now()).append('&').append('\n'); + baos.write(buf.toString().getBytes()); + baos.write(msg.getMessageData()); + byte data[] = baos.toByteArray(); + _log.debug("Data to be sent: " + data.length); + return data; + } catch (Throwable t) { + _log.error("Error preparing the data", t); + return null; + } + } + + private URL getURL(OutNetMessage msg) { + for (Iterator iter = msg.getTarget().getAddresses().iterator(); iter.hasNext(); ) { + RouterAddress addr = (RouterAddress)iter.next(); + URL url = getURL(addr); + if (url != null) return url; + } + _log.warn("No URLs could be constructed to send to " + msg.getTarget().getIdentity().getHash().toBase64()); + return null; + } + + private URL getURL(RouterAddress addr) { + if (PHTTPTransport.STYLE.equals(addr.getTransportStyle())) { + String url = addr.getOptions().getProperty(PHTTPTransport.PROP_TO_SEND_URL); + if (url == null) return null; + try { + return new URL(url); + } catch (MalformedURLException mue) { + _log.info("Address has a bad url [" + url + "]", mue); + } + } + return null; + } +} diff --git a/router/java/src/net/i2p/router/transport/phttp/PHTTPTransport.java b/router/java/src/net/i2p/router/transport/phttp/PHTTPTransport.java new file mode 100644 index 0000000000..aeb4a73e96 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/phttp/PHTTPTransport.java @@ -0,0 +1,302 @@ +package net.i2p.router.transport.phttp; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Date; +import java.util.Properties; + +import net.i2p.data.DataHelper; +import net.i2p.data.RouterAddress; +import net.i2p.data.RouterIdentity; +import net.i2p.data.RouterInfo; +import net.i2p.data.SigningPrivateKey; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.OutNetMessage; +import net.i2p.router.Router; +import net.i2p.router.transport.BandwidthLimiter; +import net.i2p.router.transport.TransportBid; +import net.i2p.router.transport.TransportImpl; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * + * + */ +public class PHTTPTransport extends TransportImpl { + private final static Log _log = new Log(PHTTPTransport.class); + public final static String STYLE = "PHTTP"; + private RouterIdentity _myIdentity; + private SigningPrivateKey _signingKey; + private RouterAddress _myAddress; + private String _mySendURL; + private String _myPollURL; + private String _myRegisterURL; + private long _timeOffset; + private long _pollFrequencyMs; + private int _transportCost; + private PHTTPPoller _poller; + private PHTTPSender _sender; + private boolean _trustTime; + + /** how long after a registration failure should we delay? this gets doubled each time */ + private long _nextRegisterDelay = 1000; + + /** if the phttp relay is down, check it up to once every 5 minutes */ + private final static long MAX_REGISTER_DELAY = 5*60*1000; + + /** URL to which registration with the server can occur */ + public final static String PROP_TO_REGISTER_URL = "registerURL"; + /** URL to which messages destined for this address can be sent */ + public final static String PROP_TO_SEND_URL = "sendURL"; + + public final static String PROP_LOCALTIME = "localtime"; + + /* key=val keys sent back on registration */ + public final static String PROP_STATUS = "status"; + public final static String PROP_POLL_URL = "pollURL"; + public final static String PROP_SEND_URL = "sendURL"; + public final static String PROP_TIME_OFFSET = "timeOffset"; // ms (remote-local) + + /* values for the PROP_STATUS */ + public final static String STATUS_FAILED = "failed"; + public final static String STATUS_REGISTERED = "registered"; + + public final static String CONFIG_POLL_FREQUENCY = "i2np.phttp.pollFrequencySeconds"; + public final static long DEFAULT_POLL_FREQUENCY = 60*1000; // every 60 seconds + + /** + * do we want to assume that the relay's clock is sync'ed with NTP and update + * our offset according to what they say? + */ + public final static String CONFIG_TRUST_TIME = "i2np.phttp.trustRelayTime"; + public final static boolean DEFAULT_TRUST_TIME = true; + + public PHTTPTransport(RouterIdentity myIdentity, SigningPrivateKey signingKey, RouterAddress myAddress) { + super(); + _myIdentity = myIdentity; + _signingKey = signingKey; + _myAddress = myAddress; + + if (myAddress != null) { + Properties opts = myAddress.getOptions(); + _myRegisterURL = opts.getProperty(PROP_TO_REGISTER_URL); + _mySendURL = opts.getProperty(PROP_TO_SEND_URL); + _pollFrequencyMs = DEFAULT_POLL_FREQUENCY; + String pollFreq = Router.getInstance().getConfigSetting(CONFIG_POLL_FREQUENCY); + if (pollFreq != null) { + try { + long val = Long.parseLong(pollFreq); + _pollFrequencyMs = val*1000; + _log.info("PHTTP Polling Frequency specified as once every " + val + " seconds"); + } catch (NumberFormatException nfe) { + _log.error("Poll frequency is not valid (" + pollFreq + ")", nfe); + } + } else { + _log.info("PHTTP Polling Frequency not specified via (" + CONFIG_POLL_FREQUENCY + "), defaulting to once every " + (DEFAULT_POLL_FREQUENCY/1000) + " seconds"); + } + + String trustTime = Router.getInstance().getConfigSetting(CONFIG_TRUST_TIME); + if (trustTime != null) { + _trustTime = Boolean.TRUE.toString().equalsIgnoreCase(trustTime); + } else { + _trustTime = DEFAULT_TRUST_TIME; + } + + JobQueue.getInstance().addJob(new RegisterJob()); + } + _sender = new PHTTPSender(this); + } + + public String getMySendURL() { return _mySendURL; } + SigningPrivateKey getMySigningKey() { return _signingKey; } + RouterIdentity getMyIdentity() { return _myIdentity; } + String getMyPollURL() { return _myPollURL; } + long getPollFrequencyMs() { return _pollFrequencyMs; } + + private class RegisterJob extends JobImpl { + public String getName() { return "Register with PHTTP relay"; } + public void runJob() { + boolean ok = doRegisterWithRelay(); + if (ok) { + _log.debug("Registration successful with the last registration delay of " + _nextRegisterDelay + "ms"); + if (_poller == null) { + _poller = new PHTTPPoller(PHTTPTransport.this); + _poller.startPolling(); + } + } else { + _nextRegisterDelay = _nextRegisterDelay * 2; + if (_nextRegisterDelay > MAX_REGISTER_DELAY) + _nextRegisterDelay = MAX_REGISTER_DELAY; + long nextRegister = Clock.getInstance().now() + _nextRegisterDelay; + _log.debug("Registration failed, next registration attempt in " + _nextRegisterDelay + "ms"); + requeue(nextRegister); + } + } + } + + boolean registerWithRelay() { + boolean ok = doRegisterWithRelay(); + if (ok) { + _log.info("Registered with PHTTP relay"); + return ok; + } + _log.error("Unable to register with relay"); + return false; + } + + synchronized boolean doRegisterWithRelay() { + _log.debug("Beginning registration"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + try { + DataHelper.writeDate(baos, new Date(Clock.getInstance().now())); + _myIdentity.writeBytes(baos); + int postLength = baos.size(); + + BandwidthLimiter.getInstance().delayOutbound(null, postLength+512); // HTTP overhead + BandwidthLimiter.getInstance().delayInbound(null, 2048+512); // HTTP overhead + + long now = Clock.getInstance().now(); + _log.debug("Before opening " + _myRegisterURL); + URL url = new URL(_myRegisterURL); + HttpURLConnection con = (HttpURLConnection)url.openConnection(); + // send the info + con.setRequestMethod("POST"); + con.setUseCaches(false); + con.setDoOutput(true); + con.setDoInput(true); + con.setRequestProperty("Content-length", ""+postLength); + baos.writeTo(con.getOutputStream()); + _log.debug("Data sent, before reading"); + con.connect(); + // fetch the results + BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream())); + String line = null; + String stat = null; + boolean ok = false; + while ( (line = reader.readLine()) != null) { + if (line.startsWith(PROP_SEND_URL)) { + _mySendURL = line.substring(PROP_SEND_URL.length()+1).trim(); + } else if (line.startsWith(PROP_POLL_URL)) { + _myPollURL = line.substring(PROP_POLL_URL.length()+1).trim(); + } else if (line.startsWith(PROP_STATUS)) { + stat = line.substring(PROP_STATUS.length()+1).trim(); + if (STATUS_REGISTERED.equals(stat.toLowerCase())) + ok = true; + } else if (line.startsWith(PROP_TIME_OFFSET)) { + String offset = line.substring(PROP_TIME_OFFSET.length()+1).trim(); + try { + _timeOffset = Long.parseLong(offset); + } catch (Throwable t) { + _log.warn("Unable to parse time offset [" + offset + "] - treating as MAX"); + _timeOffset = Long.MAX_VALUE; + } + } + if ( (_myPollURL != null) && (_mySendURL != null) && (stat != null) ) + break; + } + + if (_trustTime) { + _log.info("Setting time offset to " + _timeOffset + " (old offset: " + Clock.getInstance().getOffset() + ")"); + Clock.getInstance().setOffset(_timeOffset); + } + //if ( (_timeOffset > Router.CLOCK_FUDGE_FACTOR) || (_timeOffset < 0 - Router.CLOCK_FUDGE_FACTOR) ) { + // _log.error("Unable to register with PHTTP relay, as there is too much clock skew! " + _timeOffset + "ms difference (them-us)", new Exception("Too much clock skew with phttp relay!")); + // return false; + //} + + if (ok) { + _log.info("Registered with the PHTTP relay [" + _myRegisterURL + "]"); + _log.info("Registered sending url: [" + _mySendURL + "]"); + _log.info("Registered polling url: [" + _myPollURL + "]"); + return true; + } else { + _log.warn("PHTTP relay [" + _myRegisterURL + "] rejected registration"); + } + } catch (Throwable t) { + _log.warn("Error registering", t); + } + + return false; + } + + protected void outboundMessageReady() { + OutNetMessage msg = getNextMessage(); + if (msg != null) { + JobQueue.getInstance().addJob(new PushNewMessageJob(msg)); + } else { + _log.debug("OutboundMessageReady called, but none were available"); + } + } + + public TransportBid bid(RouterInfo toAddress, long dataSize) { + if (PHTTPPoller.shouldRejectMessages()) + return null; // we're not using phttp + + long latencyStartup = BandwidthLimiter.getInstance().calculateDelayOutbound(toAddress.getIdentity(), (int)dataSize); + latencyStartup += _pollFrequencyMs / 2; // average distance until the next poll + long sendTime = (int)((dataSize)/(16*1024)); // 16K/sec ARBITRARY + int bytes = (int)dataSize+1024; + + // lets seriously penalize phttp to heavily prefer TCP + bytes += 1024*100; + latencyStartup += 1000*600; + + TransportBid bid = new TransportBid(); + bid.setBandwidthBytes(bytes); + bid.setExpiration(new Date(Clock.getInstance().now()+1000*60)); // 1 minute, since the bwlimiter goes per minute + bid.setLatencyMs((int) (latencyStartup + sendTime)); + bid.setMessageSize((int)dataSize); + bid.setRouter(toAddress); + bid.setTransport(this); + + RouterAddress addr = getTargetAddress(toAddress); + if (addr == null) + return null; + + return bid; + } + + public RouterAddress startListening() { + _log.debug("Start listening"); + return _myAddress; + } + public void stopListening() { + if (_poller != null) + _poller.stopPolling(); + } + + + public void rotateAddresses() {} + public void addAddressInfo(Properties infoForNewAddress) {} + public String getStyle() { return STYLE; } + + boolean getTrustTime() { return _trustTime; } + + private class PushNewMessageJob extends JobImpl { + private OutNetMessage _msg; + public PushNewMessageJob(OutNetMessage msg) { _msg = msg; } + public String getName() { return "Push New PHTTP Message"; } + public void runJob() { + long delay = BandwidthLimiter.getInstance().calculateDelayOutbound(_msg.getTarget().getIdentity(), (int)_msg.getMessageSize()); + if (delay > 0) { + getTiming().setStartAfter(delay + Clock.getInstance().now()); + JobQueue.getInstance().addJob(this); + } else { + _sender.send(_msg); + } + } + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/RestrictiveTCPConnection.java b/router/java/src/net/i2p/router/transport/tcp/RestrictiveTCPConnection.java new file mode 100644 index 0000000000..4a95f9d28d --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/RestrictiveTCPConnection.java @@ -0,0 +1,322 @@ +package net.i2p.router.transport.tcp; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.net.Socket; +import java.util.Date; + +import net.i2p.crypto.AESEngine; +import net.i2p.crypto.AESInputStream; +import net.i2p.crypto.AESOutputStream; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.RouterIdentity; +import net.i2p.data.SigningPrivateKey; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.Router; +import net.i2p.router.Shitlist; +import net.i2p.router.transport.BandwidthLimitedInputStream; +import net.i2p.router.transport.BandwidthLimitedOutputStream; +import net.i2p.util.Clock; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +import net.i2p.stat.StatManager; + +/** + * TCPConnection that validates the time and protocol version, dropping connection if + * the clocks are too skewed or the versions don't match. + * + */ +class RestrictiveTCPConnection extends TCPConnection { + private final static Log _log = new Log(RestrictiveTCPConnection.class); + + public RestrictiveTCPConnection(Socket s, RouterIdentity myIdent, SigningPrivateKey signingKey, boolean locallyInitiated) { + super(s, myIdent, signingKey, locallyInitiated); + } + + /** passed in the handshake process for the connection, and only equivilant protocols will be accepted */ + private final static long PROTO_ID = 12; + + static { + StatManager.getInstance().createRateStat("tcp.establishConnectionTime", "How long does it take for us to successfully establish a connection (either locally or remotely initiated)?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + private boolean validateVersion() throws DataFormatException, IOException { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Before validating version"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(8); + DataHelper.writeLong(baos, 4, PROTO_ID); + byte encr[] = AESEngine.getInstance().safeEncrypt(baos.toByteArray(), _key, _iv, 16); + DataHelper.writeLong(_out, 2, encr.length); + _out.write(encr); + + if (_log.shouldLog(Log.DEBUG)) _log.debug("Version sent"); + // we've sent our version, now read what theirs is + + int rlen = (int)DataHelper.readLong(_in, 2); + byte pencr[] = new byte[rlen]; + int read = DataHelper.read(_in, pencr); + if (read != rlen) + throw new DataFormatException("Not enough data in peer version"); + byte decr[] = AESEngine.getInstance().safeDecrypt(pencr, _key, _iv); + if (decr == null) + throw new DataFormatException("Unable to decrypt - failed version?"); + + ByteArrayInputStream bais = new ByteArrayInputStream(decr); + long peerProtoId = DataHelper.readLong(bais, 4); + + + if (_log.shouldLog(Log.DEBUG)) _log.debug("Version received [" + peerProtoId + "]"); + + return validateVersion(PROTO_ID, peerProtoId); + } + + private boolean validateVersion(long us, long them) throws DataFormatException, IOException { + if (us != them) { + if (_log.shouldLog(Log.ERROR)) + _log.error("INVALID PROTOCOL VERSIONS! us = " + us + " them = " + them + ": " + _remoteIdentity.getHash()); + if (them > us) + Router.getInstance().setHigherVersionSeen(true); + return false; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Valid protocol version: us = " + us + " them = " + them + ": " + _remoteIdentity.getHash()); + return true; + } + } + + private boolean validateTime() throws DataFormatException, IOException { + Date now = new Date(Clock.getInstance().now()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(8); + DataHelper.writeDate(baos, now); + + byte encr[] = AESEngine.getInstance().safeEncrypt(baos.toByteArray(), _key, _iv, 16); + DataHelper.writeLong(_out, 2, encr.length); + _out.write(encr); + + // we've sent our date, now read what theirs is + + int rlen = (int)DataHelper.readLong(_in, 2); + byte pencr[] = new byte[rlen]; + int read = DataHelper.read(_in, pencr); + if (read != rlen) + throw new DataFormatException("Not enough data in peer date"); + byte decr[] = AESEngine.getInstance().safeDecrypt(pencr, _key, _iv); + if (decr == null) + throw new DataFormatException("Unable to decrypt - failed date?"); + + ByteArrayInputStream bais = new ByteArrayInputStream(decr); + Date theirNow = DataHelper.readDate(bais); + + long diff = now.getTime() - theirNow.getTime(); + if ( (diff > Router.CLOCK_FUDGE_FACTOR) || (diff < (0-Router.CLOCK_FUDGE_FACTOR)) ) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Peer is out of time sync! They think it is " + theirNow + ": " + _remoteIdentity.getHash(), new Exception("Time sync error - please make sure your clock is correct!")); + return false; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer sync difference: " + diff + "ms: " + _remoteIdentity.getHash()); + return true; + } + } + + /** + * Exchange TCP addresses, and if we're didn't establish this connection, validate + * the peer with validatePeerAddresses(TCPAddress[]). + * + * @return true if the peer is valid (and reachable) + */ + private boolean validatePeerAddress() throws DataFormatException, IOException { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Before sending my addresses"); + TCPAddress me[] = _transport.getMyAddresses(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(256); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending " + me.length + " addresses"); + DataHelper.writeLong(baos, 1, me.length); + for (int i = 0; i < me.length; i++) { + DataHelper.writeString(baos, me[i].getHost()); + DataHelper.writeLong(baos, 2, me[i].getPort()); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Sent my address [" + me[i].getHost() + ":" + me[i].getPort() + "]"); + } + if (_log.shouldLog(Log.DEBUG)) _log.debug("Sent my " + me.length + " addresses"); + + byte encr[] = AESEngine.getInstance().safeEncrypt(baos.toByteArray(), _key, _iv, 256); + DataHelper.writeLong(_out, 2, encr.length); + _out.write(encr); + + // we've sent our addresses, now read their addresses + + int rlen = (int)DataHelper.readLong(_in, 2); + byte pencr[] = new byte[rlen]; + int read = DataHelper.read(_in, pencr); + if (read != rlen) + throw new DataFormatException("Not enough data in peer addresses"); + byte decr[] = AESEngine.getInstance().safeDecrypt(pencr, _key, _iv); + if (decr == null) + throw new DataFormatException("Unable to decrypt - invalid addresses?"); + + ByteArrayInputStream bais = new ByteArrayInputStream(decr); + long numAddresses = DataHelper.readLong(bais, 1); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Peer will send us " + numAddresses + " addresses"); + TCPAddress peer[] = new TCPAddress[(int)numAddresses]; + for (int i = 0; i < peer.length; i++) { + String host = DataHelper.readString(bais); + int port = (int)DataHelper.readLong(bais, 2); + peer[i] = new TCPAddress(host, port); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Received peer address [" + peer[i].getHost() + ":" + peer[i].getPort() + "]"); + } + + // ok, we've received their addresses, now we determine whether we need to + // validate them or not + if (weInitiatedConnection()) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("We initiated the connection, so no need to validate"); + return true; // we connected to them, so we know we can, um, connect to them + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("We received the connection, so validate"); + boolean valid = validatePeerAddresses(peer); + if (_log.shouldLog(Log.DEBUG)) _log.debug("We received the connection, validated? " + valid); + return valid; + } + } + + /** + * They connected to us, but since we don't want to deal with restricted route topologies + * (yet), we want to make sure *they* are reachable by other people. In the long run, we'll + * likely want to test this by routing messages through random peers to see if *they* can + * contact them (but only when we want to determine whether to use them as a gateway, etc). + * + * Oh, I suppose I should explain what this method does, not just why. Ok, this iterates + * through all of the supplied TCP addresses attempting to open a socket. If it receives + * any data on that socket, we'll assume their address is valid and we're satisfied. (yes, + * this means it could point at random addresses, etc - this is not sufficient for dealing + * with hostile peers, just with misconfigured peers). If we can't find a peer address that + * we can connect to, they suck and can go eat worms. + * + */ + private boolean validatePeerAddresses(TCPAddress addresses[]) throws DataFormatException, IOException { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Before validating peer addresses [" + addresses.length + "]..."); + for (int i = 0; i < addresses.length; i++) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Before validating peer address (" + addresses[i].getHost() + ":"+ addresses[i].getPort() + ")..."); + boolean ok = sendsUsData(addresses[i]); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Before validating peer address (" + addresses[i].getHost() + ":"+ addresses[i].getPort() + ") [" + ok + "]..."); + if (ok) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer address " + addresses[i].getHost() + ":" + addresses[i].getPort() + " validated!"); + return true; + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Peer address " + addresses[i].getHost() + ":" + addresses[i].getPort() + " could NOT be validated"); + } + } + if (_log.shouldLog(Log.WARN)) + _log.warn("None of the peer addresses could be validated!"); + return false; + } + + private boolean sendsUsData(TCPAddress peer) { + SocketCreator creator = new SocketCreator(peer.getHost(), peer.getPort(), false); + I2PThread sockCreator = new I2PThread(creator); + sockCreator.setDaemon(true); + sockCreator.setName("PeerCallback"); + sockCreator.setPriority(I2PThread.MIN_PRIORITY); + sockCreator.start(); + + if (_log.shouldLog(Log.DEBUG)) _log.debug("Before joining socket creator via peer callback..."); + try { + synchronized (creator) { + creator.wait(TCPTransport.SOCKET_CREATE_TIMEOUT); + } + } catch (InterruptedException ie) {} + + boolean established = creator.couldEstablish(); + // returns a socket if and only if the connection was established and the I2P handshake byte sent and received + if (_log.shouldLog(Log.DEBUG)) + _log.debug("After joining socket creator via peer callback [could establish? " + established + "]"); + return established; + } + + public RouterIdentity establishConnection() { + long start = Clock.getInstance().now(); + long success = 0; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Establishing connection..."); + BigInteger myPub = _builder.getMyPublicValue(); + try { + _socket.setSoTimeout(ESTABLISHMENT_TIMEOUT); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Before key exchange..."); + exchangeKey(); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Key exchanged..."); + // key exchanged. now say who we are and prove it + boolean ok = identifyStationToStation(); + if (_log.shouldLog(Log.DEBUG)) _log.debug("After station to station [" + ok + "]..."); + + if (!ok) + throw new DataFormatException("Station to station identification failed! MITM?"); + + + if (_log.shouldLog(Log.DEBUG)) _log.debug("before validateVersion..."); + boolean versionOk = validateVersion(); + if (_log.shouldLog(Log.DEBUG)) _log.debug("after validateVersion [" + versionOk + "]..."); + + if (!versionOk) { + // not only do we remove the reference to the invalid peer + NetworkDatabaseFacade.getInstance().fail(_remoteIdentity.getHash()); + // but we make sure that we don't try to talk to them soon even if we get a new ref + Shitlist.getInstance().shitlistRouter(_remoteIdentity.getHash()); + throw new DataFormatException("Peer uses an invalid version! dropping"); + } + + if (_log.shouldLog(Log.DEBUG)) _log.debug("before validateTime..."); + boolean timeOk = validateTime(); + if (_log.shouldLog(Log.DEBUG)) _log.debug("after validateTime [" + timeOk + "]..."); + if (!timeOk) { + Shitlist.getInstance().shitlistRouter(_remoteIdentity.getHash()); + throw new DataFormatException("Peer is too far out of sync with the current router's clock! dropping"); + } + + if (_log.shouldLog(Log.DEBUG)) _log.debug("before validate peer address..."); + boolean peerReachable = validatePeerAddress(); + if (_log.shouldLog(Log.DEBUG)) _log.debug("after validatePeerAddress [" + peerReachable + "]..."); + if (!peerReachable) { + Shitlist.getInstance().shitlistRouter(_remoteIdentity.getHash()); + throw new DataFormatException("Peer provided us with an unreachable router address, and we can't handle restricted routes yet! dropping"); + } + + if (_log.shouldLog(Log.INFO)) + _log.info("TCP connection " + _id + " established with " + _remoteIdentity.getHash().toBase64()); + _in = new AESInputStream(new BandwidthLimitedInputStream(_in, _remoteIdentity), _key, _iv); + _out = new AESOutputStream(new BandwidthLimitedOutputStream(_out, _remoteIdentity), _key, _iv); + _socket.setSoTimeout(0); + success = Clock.getInstance().now(); + established(); + return _remoteIdentity; + + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error establishing connection with " + _socket.getInetAddress().getHostAddress() + ":" + _socket.getPort(), ioe); + closeConnection(); + return null; + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error establishing connection with " + _socket.getInetAddress().getHostAddress() + ":" + _socket.getPort(), dfe); + closeConnection(); + return null; + } catch (Throwable t) { + if (_log.shouldLog(Log.ERROR)) + _log.error("jrandom is paranoid so we're catching it all during establishConnection " + _socket.getInetAddress().getHostAddress() + ":" + _socket.getPort(), t); + closeConnection(); + return null; + } finally { + if (success > 0) + StatManager.getInstance().addRateData("tcp.establishConnectionTime", success-start, success-start); + } + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/SocketCreator.java b/router/java/src/net/i2p/router/transport/tcp/SocketCreator.java new file mode 100644 index 0000000000..49083268ff --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/SocketCreator.java @@ -0,0 +1,114 @@ +package net.i2p.router.transport.tcp; + +import net.i2p.util.Log; + +import java.net.Socket; +import java.net.UnknownHostException; +import java.io.IOException; + +class SocketCreator implements Runnable { + private final static Log _log = new Log(SocketCreator.class); + private String _host; + private int _port; + private Socket _socket; + private boolean _keepOpen; + private boolean _established; + + public SocketCreator(String host, int port) { + this(host, port, true); + } + public SocketCreator(String host, int port, boolean keepOpen) { + _host = host; + _port = port; + _socket = null; + _keepOpen = keepOpen; + _established = false; + } + + public Socket getSocket() { return _socket; } + + public boolean couldEstablish() { return _established; } + + /** the first byte sent and received must be 0x22 */ + public final static int I2P_FLAG = 0x22; + + public void run() { + if (_keepOpen) { + doEstablish(); + } else { + checkEstablish(); + } + } + + private void doEstablish() { + try { + _socket = new Socket(_host, _port); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Socket created"); + _socket.getOutputStream().write(I2P_FLAG); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("I2P flag sent"); + int val = _socket.getInputStream().read(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Value read: [" + val + "] == flag? [" + I2P_FLAG + "]"); + if (val != I2P_FLAG) { + _socket.close(); + _socket = null; + } + return; + } catch (UnknownHostException uhe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error establishing connection to " + _host + ':' + _port, uhe); + return; + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error establishing connection to " + _host + ':' + _port + ": "+ ioe.getMessage()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Error establishing", ioe); + _socket = null; + return; + } finally { + synchronized (this) { + notifyAll(); + } + } + } + + /** + * Try to establish the connection, but don't actually send the I2P flag. The + * other side will timeout waiting for it and consider it a dropped connection, + * but since they will have sent us the I2P flag first we will still know they are + * reachable. + * + */ + private void checkEstablish() { + try { + _socket = new Socket(_host, _port); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Socket created (but we're not sending the flag, since we're just testing them)"); + int val = _socket.getInputStream().read(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Value read: [" + val + "] == flag? [" + I2P_FLAG + "]"); + + _socket.close(); + _socket = null; + _established = (val == I2P_FLAG); + return; + } catch (UnknownHostException uhe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error establishing connection to " + _host + ':' + _port, uhe); + return; + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error establishing connection to " + _host + ':' + _port + ": "+ ioe.getMessage()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Error establishing", ioe); + _socket = null; + return; + } finally { + synchronized (this) { + notifyAll(); + } + } + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPAddress.java b/router/java/src/net/i2p/router/transport/tcp/TCPAddress.java new file mode 100644 index 0000000000..58da7509f8 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/TCPAddress.java @@ -0,0 +1,107 @@ +package net.i2p.router.transport.tcp; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.net.InetAddress; +import net.i2p.data.DataHelper; +import net.i2p.data.RouterAddress; +import net.i2p.util.Log; + +/** + * Wrap up an address + */ +public class TCPAddress { + private final static Log _log = new Log(TCPAddress.class); + private int _port; + private String _host; + private InetAddress _addr; + /** Port number used in RouterAddress definitions */ + public final static String PROP_PORT = "port"; + /** Host name used in RouterAddress definitions */ + public final static String PROP_HOST = "host"; + + public TCPAddress(String host, int port) { + _host = host; + _port = port; + _addr = null; + } + + public TCPAddress() { + _host = null; + _port = -1; + _addr = null; + } + + public TCPAddress(InetAddress addr, int port) { + _host = addr.getHostName(); + _addr = addr; + _port = port; + } + public TCPAddress(RouterAddress addr) { + if (addr == null) throw new IllegalArgumentException("Null router address"); + _host = addr.getOptions().getProperty(PROP_HOST); + String port = addr.getOptions().getProperty(PROP_PORT); + if ( (port != null) && (port.trim().length() > 0) ) { + try { + _port = Integer.parseInt(port); + } catch (NumberFormatException nfe) { + _log.error("Invalid port [" + port + "]", nfe); + _port = -1; + } + } else { + _port = -1; + } + } + + public String getHost() { return _host; } + public void setHost(String host) { _host = host; } + public InetAddress getAddress() { return _addr; } + public void setAddress(InetAddress addr) { _addr = addr; } + public int getPort() { return _port; } + public void setPort(int port) { _port = port; } + + public boolean isPubliclyRoutable() { + if (_host == null) return false; + try { + InetAddress addr = InetAddress.getByName(_host); + byte quad[] = addr.getAddress(); + if (quad[0] == (byte)127) return false; + if (quad[0] == (byte)10) return false; + if ( (quad[0] == (byte)172) && (quad[1] >= (byte)16) && (quad[1] <= (byte)31) ) return false; + if ( (quad[0] == (byte)192) && (quad[1] == (byte)168) ) return false; + if (quad[0] >= (byte)224) return false; // no multicast + return true; // or at least possible to be true + } catch (Throwable t) { + _log.error("Error checking routability", t); + return false; + } + } + + public int hashCode() { + int rv = 0; + rv += _port; + if (_addr != null) rv += _addr.getHostAddress().hashCode(); + else + if (_host != null) rv += _host.hashCode(); + return rv; + } + + public boolean equals(Object val) { + if ( (val != null) && (val instanceof TCPAddress) ) { + TCPAddress addr = (TCPAddress)val; + if (getAddress().getHostAddress() != null) + return DataHelper.eq(getAddress().getHostAddress(), addr.getAddress().getHostAddress()) && + (getPort() == addr.getPort()); + else + return DataHelper.eq(getHost(), addr.getHost()) && + (getPort() == addr.getPort()); + } + return false; + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPConnection.java b/router/java/src/net/i2p/router/transport/tcp/TCPConnection.java new file mode 100644 index 0000000000..e0d26a1a41 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/TCPConnection.java @@ -0,0 +1,533 @@ +package net.i2p.router.transport.tcp; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import net.i2p.crypto.AESEngine; +import net.i2p.crypto.AESInputStream; +import net.i2p.crypto.AESOutputStream; +import net.i2p.crypto.DHSessionKeyBuilder; +import net.i2p.crypto.DSAEngine; +import net.i2p.crypto.SHA256Generator; +import net.i2p.data.ByteArray; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.RouterIdentity; +import net.i2p.data.SessionKey; +import net.i2p.data.Signature; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.I2NPMessageReader; +import net.i2p.router.OutNetMessage; +import net.i2p.router.Router; +import net.i2p.router.transport.BandwidthLimitedInputStream; +import net.i2p.router.transport.BandwidthLimitedOutputStream; +import net.i2p.stat.StatManager; +import net.i2p.util.Clock; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Wraps a connection - this contains a reader thread (via I2NPMessageReader) and + * a writer thread (ConnectionRunner). The writer reads the pool of outbound + * messages and writes them in order, while the reader fires off events + * + */ +class TCPConnection implements I2NPMessageReader.I2NPMessageEventListener { + private final static Log _log = new Log(TCPConnection.class); + protected static int _idCounter = 0; + protected int _id; + protected DHSessionKeyBuilder _builder; + protected Socket _socket; + protected I2NPMessageReader _reader; + protected InputStream _in; + protected OutputStream _out; + protected RouterIdentity _myIdentity; + protected RouterIdentity _remoteIdentity; + protected TCPTransport _transport; + protected ConnectionRunner _runner; + protected List _toBeSent; + protected SessionKey _key; + protected ByteArray _extraBytes; + protected byte[] _iv; + protected SigningPrivateKey _signingKey; + protected int _maxQueuedMessages; + private long _lastSliceRun; + private boolean _closed; + private boolean _weInitiated; + private long _created; + + public final static String PARAM_MAX_QUEUED_MESSAGES = "i2np.tcp.maxQueuedMessages"; + private final static int DEFAULT_MAX_QUEUED_MESSAGES = 20; + + static { + StatManager.getInstance().createRateStat("tcp.queueSize", "How many messages were already in the queue when a new message was added?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + public TCPConnection(Socket s, RouterIdentity myIdent, SigningPrivateKey signingKey, boolean locallyInitiated) { + _id = ++_idCounter; + _weInitiated = locallyInitiated; + _closed = false; + _socket = s; + _myIdentity = myIdent; + _signingKey = signingKey; + _created = -1; + _toBeSent = new ArrayList(); + try { + _in = _socket.getInputStream(); + _out = _socket.getOutputStream(); + } catch (IOException ioe) { + _log.error("Error getting streams for the connection", ioe); + } + _builder = new DHSessionKeyBuilder(); + _extraBytes = null; + _lastSliceRun = -1; + + + _log.info("Connected with peer: " + s.getInetAddress() + ":" + s.getPort()); + updateMaxQueuedMessages(); + } + + /** how long has this connection been around for, or -1 if it isn't established yet */ + public long getLifetime() { + if (_created > 0) + return Clock.getInstance().now() - _created; + else + return -1; + } + + protected boolean weInitiatedConnection() { return _weInitiated; } + + private void updateMaxQueuedMessages() { + String str = Router.getInstance().getConfigSetting(PARAM_MAX_QUEUED_MESSAGES); + if ( (str != null) && (str.trim().length() > 0) ) { + try { + int max = Integer.parseInt(str); + _maxQueuedMessages = max; + return; + } catch (NumberFormatException nfe) { + _log.warn("Invalid max queued messages [" + str + "]"); + } + } + _maxQueuedMessages = DEFAULT_MAX_QUEUED_MESSAGES; + } + + public RouterIdentity getRemoteRouterIdentity() { return _remoteIdentity; } + int getId() { return _id; } + int getPendingMessageCount() { + synchronized (_toBeSent) { + return _toBeSent.size(); + } + } + + protected void exchangeKey() throws IOException, DataFormatException { + BigInteger myPub = _builder.getMyPublicValue(); + byte myPubBytes[] = myPub.toByteArray(); + DataHelper.writeLong(_out, 2, myPubBytes.length); + _out.write(myPubBytes); + + int rlen = (int)DataHelper.readLong(_in, 2); + byte peerPubBytes[] = new byte[rlen]; + int read = DataHelper.read(_in, peerPubBytes); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("rlen: " + rlen + " peerBytes: " + DataHelper.toString(peerPubBytes) + " read: " + read); + + BigInteger peerPub = new BigInteger(1, peerPubBytes); + _builder.setPeerPublicValue(peerPub); + + _key = _builder.getSessionKey(); + _extraBytes = _builder.getExtraBytes(); + _iv = new byte[16]; + System.arraycopy(_extraBytes.getData(), 0, _iv, 0, Math.min(_extraBytes.getData().length, _iv.length)); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Session key: " + _key.toBase64() + " extra bytes: " + _extraBytes.getData().length); + } + + protected boolean identifyStationToStation() throws IOException, DataFormatException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + _myIdentity.writeBytes(baos); + Hash keyHash = SHA256Generator.getInstance().calculateHash(_key.getData()); + keyHash.writeBytes(baos); + Signature sig = DSAEngine.getInstance().sign(baos.toByteArray(), _signingKey); + sig.writeBytes(baos); + + byte encr[] = AESEngine.getInstance().safeEncrypt(baos.toByteArray(), _key, _iv, 1024); + DataHelper.writeLong(_out, 2, encr.length); + _out.write(encr); + + // we've identified ourselves, now read who they are + + int rlen = (int)DataHelper.readLong(_in, 2); + byte pencr[] = new byte[rlen]; + int read = DataHelper.read(_in, pencr); + if (read != rlen) + throw new DataFormatException("Not enough data in peer ident"); + byte decr[] = AESEngine.getInstance().safeDecrypt(pencr, _key, _iv); + if (decr == null) + throw new DataFormatException("Unable to decrypt - failed exchange?"); + + ByteArrayInputStream bais = new ByteArrayInputStream(decr); + _remoteIdentity = new RouterIdentity(); + _remoteIdentity.readBytes(bais); + Hash peerKeyHash = new Hash(); + peerKeyHash.readBytes(bais); + + if (!peerKeyHash.equals(keyHash)) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Peer tried to spoof!"); + return false; + } + + Signature rsig = new Signature(); + rsig.readBytes(bais); + byte signedData[] = new byte[decr.length - rsig.getData().length]; + System.arraycopy(decr, 0, signedData, 0, signedData.length); + return DSAEngine.getInstance().verifySignature(rsig, signedData, _remoteIdentity.getSigningPublicKey()); + } + + protected final static int ESTABLISHMENT_TIMEOUT = 10*1000; // 10 second lag (not necessarily for the entire establish) + + public RouterIdentity establishConnection() { + BigInteger myPub = _builder.getMyPublicValue(); + try { + _socket.setSoTimeout(ESTABLISHMENT_TIMEOUT); + exchangeKey(); + // key exchanged. now say who we are and prove it + boolean ok = identifyStationToStation(); + + if (!ok) + throw new DataFormatException("Station to station identification failed! MITM?"); + else { + if (_log.shouldLog(Log.INFO)) + _log.info("TCP connection " + _id + " established with " + _remoteIdentity.getHash().toBase64()); + _in = new AESInputStream(new BandwidthLimitedInputStream(_in, _remoteIdentity), _key, _iv); + _out = new AESOutputStream(new BandwidthLimitedOutputStream(_out, _remoteIdentity), _key, _iv); + _socket.setSoTimeout(0); + established(); + return _remoteIdentity; + } + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error establishing connection", ioe); + closeConnection(); + return null; + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error establishing connection", dfe); + closeConnection(); + return null; + } catch (Throwable t) { + if (_log.shouldLog(Log.ERROR)) + _log.error("jrandom is paranoid so we're catching it all during establishConnection", t); + closeConnection(); + return null; + } + } + + protected void established() { _created = Clock.getInstance().now(); } + + public void runConnection() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Run connection"); + _runner = new ConnectionRunner(); + Thread t = new I2PThread(_runner); + t.setName("Run Conn [" + _id + "]"); + t.setDaemon(true); + t.start(); + _reader = new I2NPMessageReader(_in, this, "TCP Read [" + _id + "]"); + _reader.startReading(); + } + + public void setTransport(TCPTransport trans) { _transport = trans; } + + public void addMessage(OutNetMessage msg) { + msg.timestamp("TCPConnection.addMessage"); + int totalPending = 0; + boolean fail = false; + long beforeAdd = Clock.getInstance().now(); + synchronized (_toBeSent) { + if ( (_maxQueuedMessages > 0) && (_toBeSent.size() >= _maxQueuedMessages) ) { + fail = true; + } else { + _toBeSent.add(msg); + totalPending = _toBeSent.size(); + // the ConnectionRunner.processSlice does a wait() until we have messages + } + _toBeSent.notifyAll(); + } + long afterAdd = Clock.getInstance().now(); + + StatManager.getInstance().addRateData("tcp.queueSize", totalPending-1, 0); + + if (fail) { + if (_log.shouldLog(Log.ERROR)) + _log.error("too many queued messages"); + + msg.timestamp("TCPConnection.addMessage exceeded max queued"); + _transport.afterSend(msg, false); + return; + } + + long diff = afterAdd - beforeAdd; + if (diff > 500) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Lock contention adding a message: " + diff + "ms"); + } + + msg.timestamp("TCPConnection.addMessage after toBeSent.add and notify"); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Add message with toBeSent.size = " + totalPending + " to " + _remoteIdentity.getHash().toBase64()); + if (totalPending <= 0) { + if (_log.shouldLog(Log.ERROR)) + _log.error("WTF, total pending after adding " + msg.getMessage().getClass().getName() + " <= 0! " + msg); + } + + if (slicesTooLong()) { + if (_log.shouldLog(Log.ERROR)) + _log.error("onAdd: Slices are taking too long (" + (Clock.getInstance().now()-_lastSliceRun) +"ms) - perhaps the remote side is disconnected or hung? remote=" + _remoteIdentity.getHash().toBase64()); + closeConnection(); + } + } + + void closeConnection() { + if (_closed) return; + _closed = true; + if (_remoteIdentity != null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Closing the connection to " + _remoteIdentity.getHash().toBase64(), new Exception("Closed by")); + } else { + if (_socket != null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Closing the unestablished connection with " + _socket.getInetAddress().toString() + ":" + _socket.getPort(), new Exception("Closed by")); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Closing the unestablished connection", new Exception("Closed by")); + } + } + if (_reader != null) _reader.stopReading(); + if (_runner != null) _runner.stopRunning(); + if (_in != null) try { _in.close(); } catch (IOException ioe) {} + if (_out != null) try { _out.close(); } catch (IOException ioe) {} + if (_socket != null) try { _socket.close(); } catch (IOException ioe) {} + if (_toBeSent != null) { + long now = Clock.getInstance().now(); + synchronized (_toBeSent) { + for (Iterator iter = _toBeSent.iterator(); iter.hasNext(); ) { + OutNetMessage msg = (OutNetMessage)iter.next(); + msg.timestamp("TCPTransport.closeConnection caused fail"); + if (_log.shouldLog(Log.WARN)) + _log.warn("Connection closed while the message was sitting on the TCP Connection's queue! too slow by: " + (now-msg.getExpiration()) + "ms: " + msg); + _transport.afterSend(msg, false); + } + _toBeSent.clear(); + } + } + _transport.connectionClosed(this); + } + + List getPendingMessages() { return _toBeSent; } + + public void disconnected(I2NPMessageReader reader) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Remote disconnected: " + _remoteIdentity.getHash().toBase64()); + closeConnection(); + } + + public void messageReceived(I2NPMessageReader reader, I2NPMessage message, long msToReceive) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Message received from " + _remoteIdentity.getHash().toBase64()); + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024); + message.writeBytes(baos); + int size = baos.size(); + // this is called by the I2NPMessageReader's thread, so it delays the reading from this peer only + //_log.debug("Delaying inbound for size " + size); + //BandwidthLimiter.getInstance().delayInbound(_remoteIdentity, size); + _transport.messageReceived(message, _remoteIdentity, null, msToReceive, size); + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("How did we read a message that is poorly formatted...", dfe); + } catch (IOException ioe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("How did we read a message that can't be written to memory...", ioe); + } + + if (slicesTooLong()) { + if (_log.shouldLog(Log.ERROR)) + _log.error("onReceive: Slices are taking too long (" + (Clock.getInstance().now()-_lastSliceRun) +"ms) - perhaps the remote side is disconnected or hung? peer = " + _remoteIdentity.getHash().toBase64()); + closeConnection(); + } + } + + public void readError(I2NPMessageReader reader, Exception error) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error reading from stream to " + _remoteIdentity.getHash().toBase64()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Error reading from stream to " + _remoteIdentity.getHash().toBase64(), error); + } + + /** + * if a slice takes 60 seconds, fuck 'im. slices at most send one I2NPMessage, + * which can be up to 128KB currently. Basically a minimum 50bps (slower for + * larger messages - perhaps this min-throughput should be implemented on the + * output stream as part of the throttling code? hmmm) + */ + private final static long MAX_SLICE_DURATION = 60*1000; + /** + * Determine if the connection runner is hanging while running its slices. This can + * occur if there's a broken TCP connection that hasn't timed out yet (3 minutes later..) + * or if the other side's router is just b0rked and isn't .read()ing from its socket anymore. + * In either case, if this is true then the connection should be closed. Given the new threading / slice + * model, a slice that doesn't do anything will take 30-ish seconds (all in .wait()) + * + */ + private boolean slicesTooLong() { + if (_lastSliceRun <= 0) return false; + long diff = Clock.getInstance().now() - _lastSliceRun; + return (diff > MAX_SLICE_DURATION); + } + + + class ConnectionRunner implements Runnable { + private boolean _running; + public void run() { + _running = true; + try { + while (_running) { + long startSlice = Clock.getInstance().now(); + _lastSliceRun = startSlice; + processSlice(); + long endSlice = Clock.getInstance().now(); + } + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Connection runner failed with an IO exception to " + _remoteIdentity.getHash().toBase64(), ioe); + closeConnection(); + } catch (Throwable t) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Somehow we ran into an uncaught exception running the connection!", t); + closeConnection(); + } + } + + private void processSlice() throws IOException { + long start = Clock.getInstance().now(); + + OutNetMessage msg = null; + int remaining = 0; + List timedOut = new LinkedList(); + + synchronized (_toBeSent) { + // loop through, dropping expired messages, waiting until a non-expired + // one is added, or 30 seconds have passed (catchall in case things bork) + while (msg == null) { + if (_toBeSent.size() <= 0) { + try { + _toBeSent.wait(30*1000); + } catch (InterruptedException ie) {} + } + remaining = _toBeSent.size(); + if (remaining <= 0) return; + msg = (OutNetMessage)_toBeSent.remove(0); + remaining--; + if ( (msg.getExpiration() > 0) && (msg.getExpiration() < start) ) { + timedOut.add(msg); + msg = null; // keep looking + } + } + } + + for (Iterator iter = timedOut.iterator(); iter.hasNext(); ) { + OutNetMessage failed = (OutNetMessage)iter.next(); + if (_log.shouldLog(Log.WARN)) + _log.warn("Message timed out while sitting on the TCP Connection's queue! was too slow by: " + (start-msg.getExpiration()) + "ms to " + _remoteIdentity.getHash().toBase64() + ": " + msg); + msg.timestamp("TCPConnection.runner.processSlice expired"); + _transport.afterSend(msg, false); + return; + } + + if (remaining > 0) { + if (_log.shouldLog(Log.INFO)) + _log.info("After pulling off a pending message, there are still " + remaining + + " messages queued up for sending to " + _remoteIdentity.getHash().toBase64()); + } + + long afterExpire = Clock.getInstance().now(); + + if (msg != null) { + msg.timestamp("TCPConnection.runner.processSlice fetched"); + //_log.debug("Processing slice - msg to be sent"); + + try { + byte data[] = msg.getMessageData(); + msg.timestamp("TCPConnection.runner.processSlice before sending " + data.length + " bytes"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending " + data.length + " bytes in the slice... to " + _remoteIdentity.getHash().toBase64()); + + synchronized (_out) { + _out.write(data); + _out.flush(); + } + + msg.timestamp("TCPConnection.runner.processSlice sent and flushed"); + long end = Clock.getInstance().now(); + long timeLeft = msg.getMessage().getMessageExpiration().getTime() - end; + if (_log.shouldLog(Log.INFO)) + _log.info("Message " + msg.getMessage().getClass().getName() + " (expiring in " + timeLeft + "ms) sent to " + _remoteIdentity.getHash().toBase64() + " from " + _myIdentity.getHash().toBase64() + + " over connection " + _id + " with " + data.length + " bytes in " + (end - start) + "ms"); + if (timeLeft < 10*1000) { + if (_log.shouldLog(Log.DEBUG)) + _log.warn("Very little time left... time to send [" + (end-start) + "] time left [" + timeLeft + "] to " + + _remoteIdentity.getHash().toBase64() + "\n" + msg.toString(), msg.getCreatedBy()); + } + + long lifetime = msg.getLifetime(); + if (lifetime > 10*1000) { + if (_log.shouldLog(Log.WARN)) + _log.warn("The processing of the message took way too long (" + lifetime + "ms) - time to send (" + (end-start) + "), time left (" + timeLeft + ") to " + _remoteIdentity.getHash().toBase64() + "\n" + msg.toString()); + } + _transport.afterSend(msg, true); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Processing slice - message sent completely: " + msg.getMessage().getClass().getName() + " to " + _remoteIdentity.getHash().toBase64()); + if (end - afterExpire > 1000) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Actual sending took too long ( " + (end-afterExpire) + "ms) sending " + data.length + " bytes to " + _remoteIdentity.getHash().toBase64()); + } + } catch (IOException ioe) { + msg.timestamp("TCPConnection.runner.processSlice failed to send/flushflushed"); + _transport.afterSend(msg, false); + throw ioe; + } + } + } + + public void stopRunning() { + _running = false; + // stop the wait(...) + synchronized (_toBeSent) { + _toBeSent.notifyAll(); + } + } + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPListener.java b/router/java/src/net/i2p/router/transport/tcp/TCPListener.java new file mode 100644 index 0000000000..60c164bcb5 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/TCPListener.java @@ -0,0 +1,208 @@ +package net.i2p.router.transport.tcp; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; + +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Listen for TCP connections with a listener thread + * + */ +class TCPListener { + private final static Log _log = new Log(TCPListener.class); + private TCPTransport _transport; + private TCPAddress _myAddress; + private ServerSocket _socket; + private ListenerRunner _listener; + + public TCPListener(TCPTransport transport) { + _myAddress = null; + _transport = transport; + } + + public void setAddress(TCPAddress address) { _myAddress = address; } + public TCPAddress getAddress() { return _myAddress; } + + public void startListening() { + _listener = new ListenerRunner(); + Thread t = new I2PThread(_listener); + t.setName("Listener [" + _myAddress.getPort()+"]"); + t.setDaemon(true); + t.start(); + } + + public void stopListening() { + _listener.stopListening(); + if (_socket != null) + try { + _socket.close(); + _socket = null; + } catch (IOException ioe) {} + } + + private InetAddress getInetAddress(String host) { + try { + return InetAddress.getByName(host); + } catch (UnknownHostException uhe) { + _log.warn("Listen host " + host + " unknown", uhe); + try { + return InetAddress.getLocalHost(); + } catch (UnknownHostException uhe2) { + _log.error("Local host is not reachable", uhe2); + return null; + } + } + } + + private final static int MAX_FAIL_DELAY = 5*60*1000; + + class ListenerRunner implements Runnable { + private boolean _isRunning; + private int _nextFailDelay = 1000; + public ListenerRunner() { + _isRunning = true; + } + public void stopListening() { _isRunning = false; } + + public void run() { + _log.info("Beginning TCP listener"); + + int curDelay = 0; + while ( (_isRunning) && (curDelay < MAX_FAIL_DELAY) ) { + try { + if (_transport.getListenAddressIsValid()) { + _socket = new ServerSocket(_myAddress.getPort(), 5, getInetAddress(_myAddress.getHost())); + } else { + _socket = new ServerSocket(_myAddress.getPort()); + } + _log.info("Begin looping for host " + _myAddress.getHost() + ":" + _myAddress.getPort()); + curDelay = 0; + loop(); + } catch (IOException ioe) { + _log.error("Error listening to tcp connection " + _myAddress.getHost() + ":" + _myAddress.getPort(), ioe); + } + + if (_socket != null) { + stopListening(); + try { _socket.close(); } catch (IOException ioe) {} + _socket = null; + } + + _log.error("Error listening, waiting " + _nextFailDelay + "ms before we try again"); + try { Thread.sleep(_nextFailDelay); } catch (InterruptedException ie) {} + curDelay += _nextFailDelay; + _nextFailDelay *= 5; + } + _log.error("CANCELING TCP LISTEN. delay = " + curDelay, new Exception("TCP Listen cancelled!!!")); + _isRunning = false; + } + private void loop() { + while (_isRunning) { + try { + if (_log.shouldLog(Log.INFO)) + _log.info("Waiting for a connection on " + _myAddress.getHost() + ":" + _myAddress.getPort()); + + Socket s = _socket.accept(); + if (_log.shouldLog(Log.INFO)) + _log.info("Connection handled on " + _myAddress.getHost() + ":" + _myAddress.getPort() + " with " + s.getInetAddress().toString() + ":" + s.getPort()); + + TimedHandler h = new TimedHandler(s); + I2PThread t = new I2PThread(h); + t.setDaemon(true); + t.start(); + synchronized (h) { + h.wait(HANDLE_TIMEOUT); + } + if (h.wasSuccessful()) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Handle successful"); + } else { + if (h.receivedIdentByte()) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Unable to handle in the time allotted"); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer didn't send the ident byte, so either they were testing us, or portscanning"); + } + try { s.close(); } catch (IOException ioe) {} + } + } catch (SocketException se) { + _log.error("Error handling a connection - closed?", se); + return; + } catch (Throwable t) { + _log.error("Error handling a connection", t); + } + } + } + } + + /** if we're not making progress in 30s, drop 'em */ + private final static long HANDLE_TIMEOUT = 30*1000; + private static volatile int __handlerId = 0; + + private class TimedHandler implements Runnable { + private int _handlerId; + private Socket _socket; + private boolean _wasSuccessful; + private boolean _receivedIdentByte; + public TimedHandler(Socket socket) { + _socket = socket; + _wasSuccessful = false; + _handlerId = ++__handlerId; + _receivedIdentByte = false; + } + public void run() { + Thread.currentThread().setName("TimedHandler"+_handlerId); + try { + _socket.getOutputStream().write(SocketCreator.I2P_FLAG); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("listener: I2P flag sent"); + int val = _socket.getInputStream().read(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("listener: Value read: [" + val + "] == flag? [" + SocketCreator.I2P_FLAG + "]"); + if (val == -1) + throw new UnsupportedOperationException ("Peer disconnected while we were looking for the I2P flag"); + if (val != SocketCreator.I2P_FLAG) { + throw new UnsupportedOperationException ("Peer connecting to us didn't send the right I2P byte [" + val + "]"); + } + + _receivedIdentByte = true; + + TCPConnection c = new RestrictiveTCPConnection(_socket, _transport.getMyIdentity(), _transport.getMySigningKey(), false); + _transport.handleConnection(c, null); + _wasSuccessful = true; + } catch (UnsupportedOperationException uoe) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Failed to state they wanted to connect as I2P", uoe); + _wasSuccessful = false; + } catch (IOException ioe) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Error listening to the peer", ioe); + _wasSuccessful = false; + } catch (Throwable t) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error handling", t); + _wasSuccessful = false; + } + synchronized (TimedHandler.this) { + TimedHandler.this.notifyAll(); + } + } + public boolean wasSuccessful() { return _wasSuccessful; } + public boolean receivedIdentByte() { return _receivedIdentByte; } + } +} diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java b/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java new file mode 100644 index 0000000000..0b079eb45e --- /dev/null +++ b/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java @@ -0,0 +1,846 @@ +package net.i2p.router.transport.tcp; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.net.Socket; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.data.RouterAddress; +import net.i2p.data.RouterIdentity; +import net.i2p.data.RouterInfo; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.DataHelper; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.OutNetMessage; +import net.i2p.router.Router; +import net.i2p.router.Shitlist; +import net.i2p.router.transport.TransportBid; +import net.i2p.router.transport.TransportImpl; +import net.i2p.router.ProfileManager; +import net.i2p.util.Clock; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +import net.i2p.stat.StatManager; + +/** + * Defines a way to send a message to another peer and start listening for messages + * + */ +public class TCPTransport extends TransportImpl { + private final static Log _log = new Log(TCPTransport.class); + public final static String STYLE = "TCP"; + + private List _listeners; + private Map _connections; // routerIdentity --> List of TCPConnection + private RouterIdentity _myIdentity; + private String _listenHost; + private int _listenPort; + private RouterAddress _address; + private SigningPrivateKey _signingKey; + private boolean _listenAddressIsValid; + private Map _msgs; // H(ident) --> PendingMessages for unestablished connections + private boolean _running; + + private int _numConnectionEstablishers; + private final static String PROP_ESTABLISHERS = "i2np.tcp.concurrentEstablishers"; + private final static int DEFAULT_ESTABLISHERS = 3; + + public static String PROP_LISTEN_IS_VALID = "i2np.tcp.listenAddressIsValid"; + + static { + StatManager.getInstance().createFrequencyStat("tcp.attemptFailureFrequency", "How often do we attempt to contact someone, and fail?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createFrequencyStat("tcp.attemptSuccessFrequency", "How often do we attempt to contact someone, and succeed?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createFrequencyStat("tcp.acceptFailureFrequency", "How often do we reject someone who contacts us?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createFrequencyStat("tcp.acceptSuccessFrequency", "How often do we accept someone who contacts us?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("tcp.connectionLifetime", "How long do connections last (measured when they close)?", "TCP Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + /** + * pre 1.4 java doesn't have a way to timeout the creation of sockets (which + * can take up to 3 minutes), so we do it on a seperate thread and wait for + * either that thread to complete, or for this timeout to be reached. + */ + final static long SOCKET_CREATE_TIMEOUT = 10*1000; + + public TCPTransport(RouterIdentity myIdentity, SigningPrivateKey signingKey, RouterAddress address) { + super(); + _listeners = new ArrayList(); + _connections = new HashMap(); + _msgs = new HashMap(); + _myIdentity = myIdentity; + _address = address; + _signingKey = signingKey; + if (address != null) { + _listenHost = address.getOptions().getProperty(TCPAddress.PROP_HOST); + String portStr = address.getOptions().getProperty(TCPAddress.PROP_PORT); + try { + _listenPort = Integer.parseInt(portStr); + } catch (NumberFormatException nfe) { + _log.error("Invalid port: " + portStr + " Address: \n" + address, nfe); + } + } + _listenAddressIsValid = false; + try { + _listenAddressIsValid = Boolean.TRUE.toString().equalsIgnoreCase(Router.getInstance().getConfigSetting(PROP_LISTEN_IS_VALID)); + } catch (Throwable t) { + _listenAddressIsValid = false; + if (_log.shouldLog(Log.WARN)) + _log.warn("Unable to determine whether TCP listening address is valid, so we're assuming it isn't. Set " + PROP_LISTEN_IS_VALID + " otherwise"); + } + _running = false; + } + + boolean getListenAddressIsValid() { return _listenAddressIsValid; } + SigningPrivateKey getMySigningKey() { return _signingKey; } + + /** fetch all of our TCP listening addresses */ + TCPAddress[] getMyAddresses() { + if (_address != null) { + TCPAddress rv[] = new TCPAddress[1]; + rv[0] = new TCPAddress(_listenHost, _listenPort); + return rv; + } else { + return new TCPAddress[0]; + } + } + + /** + * This message is called whenever a new message is added to the send pool, + * and it should not block + */ + protected void outboundMessageReady() { + JobQueue.getInstance().addJob(new JobImpl() { + public void runJob() { + OutNetMessage msg = getNextMessage(); + if (msg != null) { + handleOutbound(msg); // this just adds to either the establish thread's queue or the conn's queue + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("OutboundMessageReady called, but none were available"); + } + } + public String getName() { return "TCP Message Ready to send"; } + }); + } + + /** + * Return a random connection to the peer from the set of known connections + * + */ + private TCPConnection getConnection(RouterIdentity peer) { + synchronized (_connections) { + if (!_connections.containsKey(peer)) + return null; + List cons = (List)_connections.get(peer); + if (cons.size() <= 0) + return null; + TCPConnection first = (TCPConnection)cons.get(0); + return first; + } + } + + protected void handleOutbound(OutNetMessage msg) { + msg.timestamp("TCPTransport.handleOutbound before handleConnection"); + TCPConnection con = getConnection(msg.getTarget().getIdentity()); + if (con == null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Handling outbound message to an unestablished peer"); + msg.timestamp("TCPTransport.handleOutbound to addPending"); + addPending(msg); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Toss the message onto an established peer's connection"); + msg.timestamp("TCPTransport.handleOutbound to con.addMessage"); + con.addMessage(msg); + } + } + + protected boolean establishConnection(RouterInfo target) { + long startEstablish = 0; + long socketCreated = 0; + long conCreated = 0; + long conEstablished = 0; + try { + for (Iterator iter = target.getAddresses().iterator(); iter.hasNext(); ) { + RouterAddress addr = (RouterAddress)iter.next(); + startEstablish = Clock.getInstance().now(); + if (getStyle().equals(addr.getTransportStyle())) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Establishing a connection with address " + addr); + Socket s = createSocket(addr); + socketCreated = Clock.getInstance().now(); + if (s == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unable to establish a socket in time to " + addr); + ProfileManager.getInstance().commErrorOccurred(target.getIdentity().getHash()); + return false; + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Socket created"); + if (s != null) { + TCPConnection con = new RestrictiveTCPConnection(s, _myIdentity, _signingKey, true); + conCreated = Clock.getInstance().now(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("TCPConnection created"); + boolean established = handleConnection(con, target); + conEstablished = Clock.getInstance().now(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("connection handled"); + return established; + } + } + } + } catch (Throwable t) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unexpected error establishing the connection", t); + } finally { + long diff = conEstablished - startEstablish; + if ( ( (diff > 6000) || (conEstablished == 0) ) && (_log.shouldLog(Log.WARN)) ) { + _log.warn("establishConnection took too long: socketCreate: " + + (socketCreated-startEstablish) + "ms conCreated: " + + (conCreated-socketCreated) + "ms conEstablished: " + + (conEstablished - conCreated) + "ms overall: " + diff); + } + } + return false; + } + + protected Socket createSocket(RouterAddress addr) { + String host = addr.getOptions().getProperty(TCPAddress.PROP_HOST); + String portStr = addr.getOptions().getProperty(TCPAddress.PROP_PORT); + int port = -1; + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException nfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Invalid port number in router address: " + portStr, nfe); + return null; + } + + long start = Clock.getInstance().now(); + SocketCreator creator = new SocketCreator(host, port); + I2PThread sockCreator = new I2PThread(creator); + sockCreator.setDaemon(true); + sockCreator.setName("SocketCreator"); + sockCreator.setPriority(I2PThread.MIN_PRIORITY); + sockCreator.start(); + + try { + synchronized (creator) { + creator.wait(SOCKET_CREATE_TIMEOUT); + } + } catch (InterruptedException ie) {} + + long finish = Clock.getInstance().now(); + long diff = finish - start; + if (diff > 6000) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Creating a new socket took too long? wtf?! " + diff + "ms for " + host + ':' + port); + } + return creator.getSocket(); + } + + private boolean isConnected(RouterInfo info) { + return (null != getConnection(info.getIdentity())); + } + + public TransportBid bid(RouterInfo toAddress, long dataSize) { + TCPConnection con = getConnection(toAddress.getIdentity()); + int latencyStartup = 0; + if (con == null) + latencyStartup = 2000; + else + latencyStartup = 0; + + int sendTime = (int)((dataSize)/(16*1024)); // 16K/sec + int bytes = (int)dataSize+8; + + if (con != null) + sendTime += 50000 * con.getPendingMessageCount(); // try to avoid backed up (throttled) connections + + TransportBid bid = new TransportBid(); + bid.setBandwidthBytes(bytes); + bid.setExpiration(new Date(Clock.getInstance().now()+1000*60)); // 1 minute + bid.setLatencyMs(latencyStartup + sendTime); + bid.setMessageSize((int)dataSize); + bid.setRouter(toAddress); + bid.setTransport(this); + + RouterAddress addr = getTargetAddress(toAddress); + if (addr == null) { + if (con == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("No address or connection to " + toAddress.getIdentity().getHash().toBase64()); + // don't bid if we can't send them a message + return null; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("No address, but we're connected to " + toAddress.getIdentity().getHash().toBase64()); + } + } + + return bid; + } + + public void rotateAddresses() { + // noop + } + public void addAddressInfo(Properties infoForNewAddress) { + // noop + } + + + public RouterAddress startListening() { + RouterAddress address = new RouterAddress(); + + address.setTransportStyle(getStyle()); + address.setCost(10); + address.setExpiration(null); + Properties options = new Properties(); + if (_address != null) { + options.setProperty(TCPAddress.PROP_HOST, _listenHost); + options.setProperty(TCPAddress.PROP_PORT, _listenPort+""); + } + address.setOptions(options); + + if (_address != null) { + try { + TCPAddress addr = new TCPAddress(); + addr.setHost(_listenHost); + addr.setPort(_listenPort); + TCPListener listener = new TCPListener(this); + listener.setAddress(addr); + _listeners.add(listener); + listener.startListening(); + } catch (NumberFormatException nfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error parsing port number", nfe); + } + + addCurrentAddress(address); + } + + String str = Router.getInstance().getConfigSetting(PROP_ESTABLISHERS); + if (str != null) { + try { + _numConnectionEstablishers = Integer.parseInt(str); + } catch (NumberFormatException nfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Invalid number of connection establishers [" + str + "]"); + _numConnectionEstablishers = DEFAULT_ESTABLISHERS; + } + } else { + _numConnectionEstablishers = DEFAULT_ESTABLISHERS; + } + + _running = true; + for (int i = 0; i < _numConnectionEstablishers; i++) { + Thread t = new I2PThread(new ConnEstablisher(i)); + t.setDaemon(true); + t.start(); + } + + return address; + } + + public void stopListening() { + if (_log.shouldLog(Log.ERROR)) + _log.error("Stop listening called! No more TCP", new Exception("Die tcp, die")); + _running = false; + + for (int i = 0; i < _listeners.size(); i++) { + TCPListener lsnr = (TCPListener)_listeners.get(i); + lsnr.stopListening(); + } + Set allCons = new HashSet(); + synchronized (_connections) { + for (Iterator iter = _connections.values().iterator(); iter.hasNext(); ) { + List cons = (List)iter.next(); + for (Iterator citer = cons.iterator(); citer.hasNext(); ) { + TCPConnection con = (TCPConnection)citer.next(); + allCons.add(con); + } + } + } + for (Iterator iter = allCons.iterator(); iter.hasNext(); ) { + TCPConnection con = (TCPConnection)iter.next(); + con.closeConnection(); + } + } + + public RouterIdentity getMyIdentity() { return _myIdentity; } + + void connectionClosed(TCPConnection con) { + if (_log.shouldLog(Log.INFO)) + _log.info("Connection closed with " + con.getRemoteRouterIdentity()); + StringBuffer buf = new StringBuffer(256); + buf.append("Still connected to: "); + synchronized (_connections) { + List cons = (List)_connections.get(con.getRemoteRouterIdentity()); + if ( (cons != null) && (cons.size() > 0) ) { + cons.remove(con); + long lifetime = con.getLifetime(); + if (_log.shouldLog(Log.INFO)) + _log.info("Connection closed (with remaining) after lifetime " + lifetime); + StatManager.getInstance().addRateData("tcp.connectionLifetime", lifetime, 0); + } + Set toRemove = new HashSet(); + for (Iterator iter = _connections.keySet().iterator(); iter.hasNext();) { + RouterIdentity ident = (RouterIdentity)iter.next(); + List all = (List)_connections.get(ident); + if (all.size() > 0) + buf.append(ident.getHash().toBase64()).append(" "); + else + toRemove.add(ident); + } + for (Iterator iter = toRemove.iterator(); iter.hasNext(); ) { + _connections.remove(iter.next()); + } + } + if (_log.shouldLog(Log.INFO)) + _log.info(buf.toString()); + //if (con.getRemoteRouterIdentity() != null) + // ProfileManager.getInstance().commErrorOccurred(con.getRemoteRouterIdentity().getHash()); + } + + boolean handleConnection(TCPConnection con, RouterInfo target) { + con.setTransport(this); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Before establishing connection"); + long start = Clock.getInstance().now(); + RouterIdentity ident = con.establishConnection(); + long afterEstablish = Clock.getInstance().now(); + long startRunning = 0; + + if (ident == null) { + StatManager.getInstance().updateFrequency("tcp.acceptFailureFrequency"); + con.closeConnection(); + //if ( (target != null) && (target.getIdentity() != null) ) + // ProfileManager.getInstance().commErrorOccurred(target.getIdentity().getHash()); + return false; + } + + if (_log.shouldLog(Log.INFO)) + _log.info("Connection established with " + ident); + if (target != null) { + if (!target.getIdentity().equals(ident)) { + StatManager.getInstance().updateFrequency("tcp.acceptFailureFrequency"); + if (_log.shouldLog(Log.ERROR)) + _log.error("Target changed identities!!! was " + target.getIdentity().getHash().toBase64() + ", now is " + ident.getHash().toBase64() + "! DROPPING CONNECTION"); + con.closeConnection(); + // remove the old ref, since they likely just created a new identity + NetworkDatabaseFacade.getInstance().fail(target.getIdentity().getHash()); + return false; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Target is the same as who we connected with"); + } + } + if (ident != null) { + Set toClose = new HashSet(4); + List toAdd = new LinkedList(); + synchronized (_connections) { + if (!_connections.containsKey(ident)) + _connections.put(ident, new ArrayList(2)); + List cons = (List)_connections.get(ident); + if (cons.size() > 0) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Attempted to open additional connections with " + ident.getHash() + ": closing older connections", new Exception("multiple cons")); + while (cons.size() > 0) { + TCPConnection oldCon = (TCPConnection)cons.remove(0); + toAdd.addAll(oldCon.getPendingMessages()); + toClose.add(oldCon); + } + } + cons.add(con); + + Set toRemove = new HashSet(); + for (Iterator iter = _connections.keySet().iterator(); iter.hasNext();) { + RouterIdentity cur = (RouterIdentity)iter.next(); + List all = (List)_connections.get(cur); + if (all.size() <= 0) + toRemove.add(ident); + } + for (Iterator iter = toRemove.iterator(); iter.hasNext(); ) { + _connections.remove(iter.next()); + } + } + + if (toAdd.size() > 0) { + for (Iterator iter = toAdd.iterator(); iter.hasNext(); ) { + OutNetMessage msg = (OutNetMessage)iter.next(); + con.addMessage(msg); + } + if (_log.shouldLog(Log.INFO)) + _log.info("Transferring " + toAdd.size() + " messages from old cons to the newly established con"); + } + + Shitlist.getInstance().unshitlistRouter(ident.getHash()); + con.runConnection(); + startRunning = Clock.getInstance().now(); + + if (toClose.size() > 0) { + for (Iterator iter = toClose.iterator(); iter.hasNext(); ) { + TCPConnection oldCon = (TCPConnection)iter.next(); + if (_log.shouldLog(Log.INFO)) + _log.info("Closing old duplicate connection " + oldCon.toString(), new Exception("Closing old con")); + oldCon.closeConnection(); + StatManager.getInstance().addRateData("tcp.connectionLifetime", oldCon.getLifetime(), 0); + } + } + long done = Clock.getInstance().now(); + + long diff = done - start; + if ( (diff > 3*1000) && (_log.shouldLog(Log.WARN)) ) { + _log.warn("handleConnection took too long: " + diff + "ms with " + + (afterEstablish-start) + "ms to establish " + + (startRunning-afterEstablish) + "ms to start running " + + (done-startRunning) + "ms to cleanup"); + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("runConnection called on the con"); + } + + StatManager.getInstance().updateFrequency("tcp.acceptSuccessFrequency"); + return true; + } + + public String getStyle() { return STYLE; } + + public String renderStatusHTML() { + StringBuffer buf = new StringBuffer(); + Map cons = new HashMap(); + synchronized (_connections) { + cons.putAll(_connections); + } + int established = 0; + buf.append("TCP Transport (").append(cons.size()).append(" connections)
\n"); + buf.append("
    "); + for (Iterator iter = cons.keySet().iterator(); iter.hasNext(); ) { + buf.append("
  • "); + RouterIdentity ident = (RouterIdentity)iter.next(); + List curCons = (List)cons.get(ident); + buf.append("Connections to ").append(ident.getHash().toBase64()).append(": ").append(curCons.size()).append("
      \n"); + for (int i = 0; i < curCons.size(); i++) { + TCPConnection con = (TCPConnection)curCons.get(i); + if (con.getLifetime() > 0) { + established++; + buf.append("
    • Connection ").append(con.getId()).append(": pending # messages to be sent: ").append(con.getPendingMessageCount()).append(" lifetime: ").append(DataHelper.formatDuration(con.getLifetime())).append("
    • \n"); + } else { + buf.append("
    • Connection ").append(con.getId()).append(": [connection in progress]
    • \n"); + } + } + buf.append("
    \n"); + buf.append("
  • \n"); + } + buf.append("
\n"); + + if (established == 0) { + buf.append("No TCP connections
    "); + buf.append("
  • Is your publicly reachable IP address / hostname ").append(_listenHost).append("?
  • \n"); + buf.append("
  • Is your firewall / NAT open to receive connections on port ").append(_listenPort).append("?
  • \n"); + buf.append("
  • Do you have any reachable peer references (see down below for \"Routers\", "); + buf.append(" or check your netDb directory - you want at least two routers, since one of them is your own)
  • \n"); + buf.append("
\n"); + } + return buf.toString(); + } + + /** + * only establish one connection at a time, and if multiple requests are pooled + * for the same one, once one is established send all the messages through + * + */ + private class ConnEstablisher implements Runnable { + private int _id; + + public ConnEstablisher(int id) { + _id = id; + } + + public int getId() { return _id; } + + public void run() { + Thread.currentThread().setName("Conn Establisher" + _id); + + while (_running) { + try { + PendingMessages pending = nextPeer(this); + + long start = Clock.getInstance().now(); + + if (_log.shouldLog(Log.INFO)) + _log.info("Beginning establishment with " + pending.getPeer().toBase64() + " [not error]"); + + TCPConnection con = getConnection(pending.getPeerInfo().getIdentity()); + long conFetched = Clock.getInstance().now(); + long sentPending = 0; + long establishedCon = 0; + long refetchedCon = 0; + long sentRefetched = 0; + long failedPending = 0; + + if (con != null) { + sendPending(con, pending); + sentPending = Clock.getInstance().now(); + } else { + boolean established = establishConnection(pending.getPeerInfo()); + establishedCon = Clock.getInstance().now(); + if (established) { + StatManager.getInstance().updateFrequency("tcp.attemptSuccessFrequency"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Connection established"); + con = getConnection(pending.getPeerInfo().getIdentity()); + refetchedCon = Clock.getInstance().now(); + if (con == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Connection established but we can't find the connection? wtf! peer = " + pending.getPeer()); + } else { + Shitlist.getInstance().unshitlistRouter(pending.getPeer()); + sendPending(con, pending); + sentRefetched = Clock.getInstance().now(); + } + } else { + StatManager.getInstance().updateFrequency("tcp.attemptFailureFrequency"); + if (_log.shouldLog(Log.INFO)) + _log.info("Unable to establish a connection to " + pending.getPeer()); + failPending(pending); + Shitlist.getInstance().shitlistRouter(pending.getPeer()); + //ProfileManager.getInstance().commErrorOccurred(pending.getPeer()); + failedPending = Clock.getInstance().now(); + } + } + + long end = Clock.getInstance().now(); + long diff = end - start; + + StringBuffer buf = new StringBuffer(128); + buf.append("Time to establish with ").append(pending.getPeer().toBase64()).append(": ").append(diff).append("ms"); + buf.append(" fetched: ").append(conFetched-start).append(" ms"); + if (sentPending != 0) + buf.append(" sendPending: ").append(sentPending - conFetched).append("ms"); + if (establishedCon != 0) { + buf.append(" established: ").append(establishedCon - conFetched).append("ms"); + if (refetchedCon != 0) { + buf.append(" refetched: ").append(refetchedCon - establishedCon).append("ms"); + if (sentRefetched != 0) { + buf.append(" sentRefetched: ").append(sentRefetched - refetchedCon).append("ms"); + } + } else { + buf.append(" failedPending: ").append(failedPending - establishedCon).append("ms"); + } + } + if (diff > 6000) { + if (_log.shouldLog(Log.WARN)) + _log.warn(buf.toString()); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info(buf.toString()); + } + } catch (Throwable t) { + if (_log.shouldLog(Log.CRIT)) + _log.log(Log.CRIT, "Error in connection establisher thread - NO MORE CONNECTIONS", t); + } + } + } + } + + /** + * Add a new message to the outbound pool to be established asap (may be sent + * along existing connections if they appear later) + * + */ + public void addPending(OutNetMessage msg) { + synchronized (_msgs) { + Hash target = msg.getTarget().getIdentity().getHash(); + PendingMessages msgs = (PendingMessages)_msgs.get(target); + if (msgs == null) { + msgs = new PendingMessages(msg.getTarget()); + msgs.addPending(msg); + _msgs.put(target, msgs); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Adding a pending to new " + target.toBase64()); + } else { + msgs.addPending(msg); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Adding a pending to existing " + target.toBase64()); + } + int level = Log.INFO; + if (msgs.getMessageCount() > 1) + level = Log.WARN; + if (_log.shouldLog(level)) + _log.log(level, "Add message to " + target.toBase64() + ", making a total of " + msgs.getMessageCount() + " for them, with another " + (_msgs.size() -1) + " peers pending establishment"); + _msgs.notifyAll(); + } + msg.timestamp("TCPTransport.addPending finished and notified"); + } + + /** + * blocking call to claim the next available targeted peer. does a wait on + * the _msgs pool which should be notified from addPending. + * + */ + private PendingMessages nextPeer(ConnEstablisher establisher) { + PendingMessages rv = null; + while (true) { + synchronized (_msgs) { + if (_msgs.size() > 0) { + for (Iterator iter = _msgs.keySet().iterator(); iter.hasNext(); ) { + Object key = iter.next(); + rv = (PendingMessages)_msgs.get(key); + if (!rv.setEstablisher(establisher)) { + // unable to claim this peer + if (_log.shouldLog(Log.INFO)) + _log.info("Peer is still in process: " + rv.getPeer() + " on establisher " + rv.getEstablisher().getId()); + rv = null; + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Returning next peer " + rv.getPeer().toBase64()); + return rv; + } + } + } + try { _msgs.wait(1000); } catch (InterruptedException ie) {} + } + } + + } + + /** + * Send all the messages targetting the given location + * over the established connection + * + */ + private void sendPending(TCPConnection con, PendingMessages pending) { + if (con == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Send pending to null con?", new Exception("Hmm")); + return; + } + if (pending == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Null pending, 'eh?", new Exception("Hmm..")); + return; + } + if (_log.shouldLog(Log.INFO)) + _log.info("Connection established, now queueing up " + pending.getMessageCount() + " messages to be sent"); + synchronized (_msgs) { + _msgs.remove(pending.getPeer()); + + OutNetMessage msg = null; + while ( (msg = pending.getNextMessage()) != null) { + msg.timestamp("TCPTransport.sendPending to con.addMessage"); + con.addMessage(msg); + } + } + } + + /** + * Fail out all messages pending to the specified peer + */ + private void failPending(PendingMessages pending) { + if (pending != null) { + synchronized (_msgs) { + _msgs.remove(pending.getPeer()); + } + + OutNetMessage msg = null; + while ( (msg = pending.getNextMessage()) != null) { + afterSend(msg, false); + } + } + } + + /** + * Coordinate messages for a particular peer that hasn't been established yet + * + */ + private static class PendingMessages { + private List _messages; + private Hash _peer; + private RouterInfo _peerInfo; + private ConnEstablisher _establisher; + + public PendingMessages(RouterInfo peer) { + _messages = new LinkedList(); + _peerInfo = peer; + _peer = peer.getIdentity().getHash(); + _establisher = null; + } + + /** + * Claim a peer for a specific establisher + * + * @return true if the claim was successful, false if someone beat us to it + */ + public boolean setEstablisher(ConnEstablisher establisher) { + synchronized (PendingMessages.this) { + if (_establisher == null) { + _establisher = establisher; + return true; + } else { + return false; + } + } + } + public ConnEstablisher getEstablisher() { + return _establisher; + } + + /** + * Add a new message to this to-be-established connection + */ + public void addPending(OutNetMessage msg) { + synchronized (_messages) { + _messages.add(msg); + } + } + + /** + * Get the next message queued up for delivery on this connection being established + * + */ + public OutNetMessage getNextMessage() { + synchronized (_messages) { + if (_messages.size() <= 0) + return null; + else + return (OutNetMessage)_messages.remove(0); + } + } + + /** + * Get the number of messages queued up for this to be established connection + * + */ + public int getMessageCount() { + synchronized (_messages) { + return _messages.size(); + } + } + + /** who are we going to establish with? */ + public Hash getPeer() { return _peer; } + /** who are we going to establish with? */ + public RouterInfo getPeerInfo() { return _peerInfo; } + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/ClientLeaseSetManagerJob.java b/router/java/src/net/i2p/router/tunnelmanager/ClientLeaseSetManagerJob.java new file mode 100644 index 0000000000..9ecc7a39e8 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/ClientLeaseSetManagerJob.java @@ -0,0 +1,203 @@ +package net.i2p.router.tunnelmanager; + +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeMap; + +import net.i2p.data.Lease; +import net.i2p.data.LeaseSet; +import net.i2p.data.RouterInfo; +import net.i2p.data.TunnelId; +import net.i2p.router.ClientManagerFacade; +import net.i2p.router.JobImpl; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.TunnelInfo; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Manage the process of requesting a lease set as necessary for a client based + * on the contents of the tunnel pool. Request a new lease set when: + * - # safe inbound tunnels meets or exceeds the client's minimum and + * - no current leaseSet exists + * or + * - one of the tunnels in the current leaseSet has expired + * or + * - it has been N minutes since the current leaseSet was created + * (where N is based off the clientSettings.getInboundDuration) + * + */ +class ClientLeaseSetManagerJob extends JobImpl { + private final static Log _log = new Log(ClientLeaseSetManagerJob.class); + private ClientTunnelPool _pool; + private LeaseSet _currentLeaseSet; + private long _lastCreated; + private boolean _forceRequestLease; + + /** + * Recheck the set every 15 seconds + * todo: this should probably be updated dynamically based on expiration dates / etc. + * + */ + private final static long RECHECK_DELAY = 15*1000; + /** + * How long to wait for the client to approve or reject a leaseSet + */ + private final static long REQUEST_LEASE_TIMEOUT = 30*1000; + + public ClientLeaseSetManagerJob(ClientTunnelPool pool) { + super(); + _pool = pool; + _currentLeaseSet = null; + _lastCreated = -1; + } + + public void forceRequestLease() { _forceRequestLease = true; } + + public String getName() { return "Manage Client Lease Set"; } + public void runJob() { + + if (_pool.isStopped()) { + if ( (_pool.getInactiveInboundTunnelIds().size() <= 0) && + (_pool.getInboundTunnelIds().size() <= 0) ) { + if (_log.shouldLog(Log.INFO)) + _log.info("No more tunnels and the client has stopped, so no need to manage the leaseSet any more for " + _pool.getDestination().calculateHash()); + return; + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Client " + _pool.getDestination().calculateHash() + " is stopped, but they still have some tunnels, so don't stop maintaining the leaseSet"); + requeue(RECHECK_DELAY); + return; + } + } + + int available = _pool.getSafePoolSize(); + if (available >= _pool.getClientSettings().getNumInboundTunnels()) { + if (_forceRequestLease) { + _log.info("Forced to request a new lease (reconnected client perhaps?)"); + _forceRequestLease = false; + requestNewLeaseSet(); + } else if (_currentLeaseSet == null) { + _log.info("No leaseSet is known - request a new one"); + requestNewLeaseSet(); + } else if (tunnelsChanged()) { + _log.info("Tunnels changed from the old leaseSet - request a new one: [pool = " + _pool.getInboundTunnelIds() + " old leaseSet: " + _currentLeaseSet); + requestNewLeaseSet(); + } else if (Clock.getInstance().now() > _lastCreated + _pool.getClientSettings().getInboundDuration()) { + _log.info("We've exceeded the client's requested duration (limit = " + new Date(_lastCreated + _pool.getClientSettings().getInboundDuration()) + " / " + _pool.getClientSettings().getInboundDuration() + ") - request a new leaseSet"); + requestNewLeaseSet(); + } else { + _log.debug("The current LeaseSet is fine, noop"); + } + } else { + _log.warn("Insufficient safe inbound tunnels exist for the client (" + available + " available, " + _pool.getClientSettings().getNumInboundTunnels() + " required) - no leaseSet requested"); + } + requeue(RECHECK_DELAY); + } + /** + * Determine if the tunnels in the current leaseSet are the same as the + * currently available free tunnels + * + * @return true if the tunnels are /not/ the same, else true if they are + */ + private boolean tunnelsChanged() { + long furthestInFuture = 0; + Set currentIds = new HashSet(_currentLeaseSet.getLeaseCount()); + for (int i = 0; i < _currentLeaseSet.getLeaseCount(); i++) { + Lease lease = (Lease)_currentLeaseSet.getLease(i); + currentIds.add(lease.getTunnelId()); + if (lease.getEndDate().getTime() > furthestInFuture) + furthestInFuture = lease.getEndDate().getTime(); + } + Set avail = _pool.getInboundTunnelIds(); + avail.removeAll(currentIds); + // check to see if newer ones exist in the available pool + for (Iterator iter = avail.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getInboundTunnel(id); + // we need to check this in case the tunnel was deleted since 6 lines up + if ( (id != null) && (info != null) && (info.getSettings() != null) ) { + // if something available but not in the currently published lease will be + // around longer than any of the published leases, we want that tunnel to + // be added to our published lease + if (info.getSettings().getExpiration() > furthestInFuture) { + _log.debug("Tunnel " + id.getTunnelId() + " expires " + (info.getSettings().getExpiration()-furthestInFuture) + "ms after any of the existing ones do"); + return true; + } + } + } + _log.debug("None of the available tunnels expire after the existing lease set's tunnels"); + return false; + } + + /** + * Request a new leaseSet based off the currently available safe tunnels + */ + private void requestNewLeaseSet() { + LeaseSet proposed = buildNewLeaseSet(); + ClientManagerFacade.getInstance().requestLeaseSet(_pool.getDestination(), proposed, REQUEST_LEASE_TIMEOUT, new LeaseSetCreatedJob(), null); + } + + /** + * Create a new proposed leaseSet with all inbound tunnels + */ + private LeaseSet buildNewLeaseSet() { + LeaseSet ls = new LeaseSet(); + TreeMap tunnels = new TreeMap(); + long now = Clock.getInstance().now(); + for (Iterator iter = _pool.getInboundTunnelIds().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getInboundTunnel(id); + + if (!info.getIsReady()) + continue; + long exp = info.getSettings().getExpiration(); + if (now + RECHECK_DELAY + REQUEST_LEASE_TIMEOUT > exp) + continue; + RouterInfo ri = NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(info.getThisHop()); + if (ri == null) + continue; + + Lease lease = new Lease(); + lease.setEndDate(new Date(exp)); + lease.setRouterIdentity(ri.getIdentity()); + lease.setTunnelId(id); + tunnels.put(new Long(0-exp), lease); + } + + // now pick the N tunnels with the longest time remaining (n = # tunnels the client requested) + // place tunnels.size() - N into the inactive pool + int selected = 0; + int wanted = _pool.getClientSettings().getNumInboundTunnels(); + for (Iterator iter = tunnels.values().iterator(); iter.hasNext(); ) { + Lease lease = (Lease)iter.next(); + if (selected < wanted) { + ls.addLease(lease); + selected++; + } else { + _pool.moveToInactive(lease.getTunnelId()); + } + } + ls.setDestination(_pool.getDestination()); + return ls; + } + + private class LeaseSetCreatedJob extends JobImpl { + public LeaseSetCreatedJob() { + super(); + } + public String getName() { return "LeaseSet created"; } + public void runJob() { + LeaseSet ls = NetworkDatabaseFacade.getInstance().lookupLeaseSetLocally(_pool.getDestination().calculateHash()); + if (ls != null) { + _log.info("New leaseSet completely created"); + _lastCreated = Clock.getInstance().now(); + _currentLeaseSet = ls; + } else { + _log.error("New lease set created, but not found locally? wtf?!"); + } + } + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPool.java b/router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPool.java new file mode 100644 index 0000000000..e34cd0af2d --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPool.java @@ -0,0 +1,191 @@ +package net.i2p.router.tunnelmanager; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import net.i2p.data.Destination; +import net.i2p.data.TunnelId; +import net.i2p.router.ClientTunnelSettings; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageHistory; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +class ClientTunnelPool { + private final static Log _log = new Log(ClientTunnelPool.class); + private Destination _dest; + private ClientTunnelSettings _settings; + private TunnelPool _pool; + private Map _inboundTunnels; // TunnelId --> TunnelInfo for inbound tunnels + private Map _inactiveInboundTunnels; // TunnelId --> TunnelInfo for inbound tunnels no longer in use (but not expired) + private ClientTunnelPoolManagerJob _mgrJob; + private ClientLeaseSetManagerJob _leaseMgrJob; + private ClientTunnelPoolExpirationJob _tunnelExpirationJob; + private boolean _isStopped; + private static int __poolId; + private int _poolId; + + public ClientTunnelPool(Destination dest, ClientTunnelSettings settings, TunnelPool pool) { + _dest = dest; + _settings = settings; + _pool = pool; + _inboundTunnels = new HashMap(); + _inactiveInboundTunnels = new HashMap(); + _isStopped = true; + _poolId = ++__poolId; + } + + public void startPool() { + if (!_isStopped) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Pool " + _poolId +": Not starting the pool /again/ (its already running)"); + return; + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Pool " + _poolId +": Starting up the pool "); + } + _isStopped = false; + if (_mgrJob == null) { + _mgrJob = new ClientTunnelPoolManagerJob(_pool, this); + JobQueue.getInstance().addJob(_mgrJob); + } + if (_leaseMgrJob == null) { + _leaseMgrJob = new ClientLeaseSetManagerJob(this); + JobQueue.getInstance().addJob(_leaseMgrJob); + } else { + // we just restarted, so make sure we ask for a new leaseSet ASAP + _leaseMgrJob.forceRequestLease(); + _leaseMgrJob.getTiming().setStartAfter(Clock.getInstance().now()); + JobQueue.getInstance().addJob(_leaseMgrJob); + } + if (_tunnelExpirationJob == null) { + _tunnelExpirationJob = new ClientTunnelPoolExpirationJob(this, _pool); + JobQueue.getInstance().addJob(_tunnelExpirationJob); + } + } + public void stopPool() { _isStopped = true; } + public boolean isStopped() { return _isStopped; } + + public void setClientSettings(ClientTunnelSettings settings) { + _settings = settings; + if (settings != null) { + _log.info("Client settings specified - the client may have reconnected, so restart the pool"); + startPool(); + } + } + public ClientTunnelSettings getClientSettings() { return _settings; } + + public Destination getDestination() { return _dest; } + + public void moveToInactive(TunnelId id) { + TunnelInfo info = removeInboundTunnel(id); + if (info != null) { + MessageHistory.getInstance().tunnelJoined("inactive inbound", info); + synchronized (_inactiveInboundTunnels) { + _inactiveInboundTunnels.put(id, info); + } + _log.info("Marking tunnel " + id + " as inactive"); + } + } + + void setActiveTunnels(Set activeTunnels) { + for (Iterator iter = activeTunnels.iterator(); iter.hasNext(); ) { + TunnelInfo info = (TunnelInfo)iter.next(); + MessageHistory.getInstance().tunnelJoined("active inbound", info); + synchronized (_inboundTunnels) { + _inboundTunnels.put(info.getTunnelId(), info); + } + } + } + void setInactiveTunnels(Set inactiveTunnels) { + for (Iterator iter = inactiveTunnels.iterator(); iter.hasNext(); ) { + TunnelInfo info = (TunnelInfo)iter.next(); + MessageHistory.getInstance().tunnelJoined("inactive inbound", info); + synchronized (_inactiveInboundTunnels) { + _inactiveInboundTunnels.put(info.getTunnelId(), info); + } + } + } + + /** + * Go through all of the client's inbound tunnels and determine how many are safe for + * use over the next period, either as part of a LeaseSet or as the target for a reply / etc. + * + */ + public int getSafePoolSize() { + return getSafePoolSize(0); + } + /** + * Get the safe # pools at some point in the future + * + * @param futureMs number of milliseconds in the future that we want to check safety for + */ + public int getSafePoolSize(long futureMs) { + int numSafe = 0; + long expireAfter = Clock.getInstance().now() + Router.CLOCK_FUDGE_FACTOR + futureMs; + for (Iterator iter = getInboundTunnelIds().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = getInboundTunnel(id); + if ( (info != null) && (info.getIsReady()) && (info.getSettings().getExpiration() > expireAfter) ) + numSafe++; + } + return numSafe; + } + + /** + * Set of tunnelIds of inbound tunnels + * + */ + public Set getInboundTunnelIds() { + synchronized (_inboundTunnels) { + return new HashSet(_inboundTunnels.keySet()); + } + } + public boolean isInboundTunnel(TunnelId id) { + synchronized (_inboundTunnels) { + return _inboundTunnels.containsKey(id); + } + } + public TunnelInfo getInboundTunnel(TunnelId id) { + synchronized (_inboundTunnels) { + return (TunnelInfo)_inboundTunnels.get(id); + } + } + public void addInboundTunnel(TunnelInfo tunnel) { + MessageHistory.getInstance().tunnelJoined("active inbound", tunnel); + synchronized (_inboundTunnels) { + _inboundTunnels.put(tunnel.getTunnelId(), tunnel); + } + } + public TunnelInfo removeInboundTunnel(TunnelId id) { + synchronized (_inboundTunnels) { + return (TunnelInfo)_inboundTunnels.remove(id); + } + } + + public Set getInactiveInboundTunnelIds() { + synchronized (_inactiveInboundTunnels) { + return new HashSet(_inactiveInboundTunnels.keySet()); + } + } + public boolean isInactiveInboundTunnel(TunnelId id) { + synchronized (_inactiveInboundTunnels) { + return _inactiveInboundTunnels.containsKey(id); + } + } + public TunnelInfo getInactiveInboundTunnel(TunnelId id) { + synchronized (_inactiveInboundTunnels) { + return (TunnelInfo)_inactiveInboundTunnels.get(id); + } + } + public TunnelInfo removeInactiveInboundTunnel(TunnelId id) { + synchronized (_inactiveInboundTunnels) { + return (TunnelInfo)_inactiveInboundTunnels.remove(id); + } + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPoolExpirationJob.java b/router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPoolExpirationJob.java new file mode 100644 index 0000000000..430a260209 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPoolExpirationJob.java @@ -0,0 +1,106 @@ +package net.i2p.router.tunnelmanager; + +import java.util.Date; +import java.util.Iterator; + +import net.i2p.data.TunnelId; +import net.i2p.router.JobImpl; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Periodically go through all of the tunnels assigned to this client and mark + * them as no longer ready and/or drop them (as appropriate) + * + */ +class ClientTunnelPoolExpirationJob extends JobImpl { + private final static Log _log = new Log(ClientTunnelPoolExpirationJob.class); + private ClientTunnelPool _pool; + private TunnelPool _tunnelPool; + + /** expire tunnels as necessary every 30 seconds */ + private final static long EXPIRE_POOL_DELAY = 30*1000; + + /** + * don't hard expire a tunnel until its later than expiration + buffer + */ + private final static long EXPIRE_BUFFER = 30*1000; + + public ClientTunnelPoolExpirationJob(ClientTunnelPool pool, TunnelPool tunnelPool) { + super(); + _pool = pool; + _tunnelPool = tunnelPool; + getTiming().setStartAfter(Clock.getInstance().now() + EXPIRE_POOL_DELAY); + } + public String getName() { return "Expire Pooled Client Tunnels"; } + public void runJob() { + if (_pool.isStopped()) { + if ( (_pool.getInactiveInboundTunnelIds().size() <= 0) && + (_pool.getInboundTunnelIds().size() <= 0) ) { + // this may get called twice - once here, and once by the ClientTunnelPoolManagerJob + // but its safe to do, and passing around messages would be overkill. + _tunnelPool.removeClientPool(_pool.getDestination()); + _log.info("No more tunnels to expire in the client tunnel pool for the stopped client " + _pool.getDestination().calculateHash()); + return; + } else { + _log.info("Client " + _pool.getDestination().calculateHash() + " is stopped, but they still have some tunnels, so don't stop expiring"); + } + } + + expireInactiveTunnels(); + expireActiveTunnels(); + + requeue(EXPIRE_POOL_DELAY); + } + + /** + * Drop all inactive tunnels that are expired or are close enough to + * being expired that using them would suck. + * + */ + public void expireInactiveTunnels() { + long now = Clock.getInstance().now(); + long expire = now - EXPIRE_BUFFER - 2*Router.CLOCK_FUDGE_FACTOR; + + for (Iterator iter = _pool.getInactiveInboundTunnelIds().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getInactiveInboundTunnel(id); + if ( (info != null) && (info.getSettings() != null) ) { + if (info.getSettings().getExpiration() < expire) { + _log.info("Expiring inactive tunnel " + id + " [" + new Date(info.getSettings().getExpiration()) + "]"); + _pool.removeInactiveInboundTunnel(id); + } else if (info.getSettings().getExpiration() < now) { + _log.info("It is past the expiration for inactive tunnel " + id + " but not yet the buffer, mark it as no longer ready"); + info.setIsReady(false); + } + } + } + } + + /** + * Drop all active tunnels that are expired or are close enough to + * being expired that using them would suck. + * + */ + public void expireActiveTunnels() { + long now = Clock.getInstance().now(); + long expire = now - EXPIRE_BUFFER - 2*Router.CLOCK_FUDGE_FACTOR; + + for (Iterator iter = _pool.getInboundTunnelIds().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getInboundTunnel(id); + if ( (info != null) && (info.getSettings() != null) ) { + if (info.getSettings().getExpiration() < expire) { + _log.info("Expiring active tunnel " + id + " [" + new Date(info.getSettings().getExpiration()) + "]"); + _pool.removeInboundTunnel(id); + } else if (info.getSettings().getExpiration() < now) { + _log.info("It is past the expiration for active tunnel " + id + " but not yet the buffer, mark it as no longer ready"); + info.setIsReady(false); + } + } + } + } + +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPoolManagerJob.java b/router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPoolManagerJob.java new file mode 100644 index 0000000000..8a831725d4 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/ClientTunnelPoolManagerJob.java @@ -0,0 +1,201 @@ +package net.i2p.router.tunnelmanager; + +import java.util.Iterator; +import java.util.TreeMap; + +import net.i2p.data.TunnelId; +import net.i2p.router.ClientManagerFacade; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.TunnelInfo; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * refill the client tunnel pool as necessary, either from the TunnelPool's free + * inbound set or by requesting custom tunnels via the RequestInboundTunnelJob. + * + */ +class ClientTunnelPoolManagerJob extends JobImpl { + private final static Log _log = new Log(ClientTunnelPoolManagerJob.class); + private ClientTunnelPool _clientPool; + private TunnelPool _tunnelPool; + + /** check the pool every 30 seconds to make sure it has enough tunnels */ + private final static long POOL_CHECK_DELAY = 30*1000; + + public ClientTunnelPoolManagerJob(TunnelPool pool, ClientTunnelPool clientPool) { + super(); + _clientPool = clientPool; + _tunnelPool = pool; + } + public String getName() { return "Manage Client Tunnel Pool"; } + public void runJob() { + try { + if (_clientPool.isStopped()) { + if (ClientManagerFacade.getInstance().isLocal(_clientPool.getDestination())) { + // it was stopped, but they've reconnected, so boot 'er up again + if (_log.shouldLog(Log.INFO)) + _log.info("Client " + _clientPool.getDestination().calculateHash().toBase64() + " was stopped, but reconnected! restarting it"); + _clientPool.startPool(); + // we return directly, since it'll queue up jobs again, etc + return; + } else { + // not currently connected - check to see whether all of the tunnels have expired + if ((_clientPool.getInactiveInboundTunnelIds().size() > 0) || + (_clientPool.getInboundTunnelIds().size() > 0) ) { + // there are tunnels left, requeue until later (in case the client reconnects + if (_log.shouldLog(Log.DEBUG)) + _log.debug("There are tunnels left, though the client is still disconnected: " + _clientPool.getDestination().calculateHash()); + requeue(POOL_CHECK_DELAY); + return; + } else { + // no tunnels left and the client is still disconnected, screw the pool + if (_log.shouldLog(Log.INFO)) + _log.info("No more tunnels left and the client has disconnected: " + _clientPool.getDestination().calculateHash()); + _tunnelPool.removeClientPool(_clientPool.getDestination()); + return; + } + } + } + + if (!ClientManagerFacade.getInstance().isLocal(_clientPool.getDestination())) { + _log.info("Client " + _clientPool.getDestination().calculateHash() + " is no longer connected, stop the pool"); + _clientPool.stopPool(); + requeue(POOL_CHECK_DELAY); + return; + } + int requestedPoolSize = _clientPool.getClientSettings().getNumInboundTunnels(); + int safePoolSize = _clientPool.getSafePoolSize(POOL_CHECK_DELAY); + if (safePoolSize < requestedPoolSize) { + requestMoreTunnels(requestedPoolSize-safePoolSize); + } + } catch (Exception t) { + _log.log(Log.CRIT, "Unhandled exception managing the client tunnel pool", t); + } + requeue(POOL_CHECK_DELAY); + } + + /** + * Request num more inbound tunnels - either from the free pool or custom built ones + * + */ + private void requestMoreTunnels(int numTunnels) { + int allocated = 0; + TreeMap goodEnoughTunnels = new TreeMap(); + int maxLength = _tunnelPool.getLongestTunnelLength(); + for (Iterator iter = _tunnelPool.getFreeTunnels().iterator(); iter.hasNext() && allocated < numTunnels; ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _tunnelPool.getFreeTunnel(id); + if (info != null) { + if (isGoodEnough(info, maxLength)) { + goodEnoughTunnels.put(new Long(0 - info.getSettings().getExpiration()), id); + } + } + } + + // good enough tunnels, ordered with the longest from now duration first + for (Iterator iter = goodEnoughTunnels.values().iterator(); iter.hasNext() && allocated < numTunnels; ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _tunnelPool.getTunnelInfo(id); + if (info.getLength() < _clientPool.getClientSettings().getDepthInbound()) { + // this aint good 'nuff... + continue; + } + boolean ok = _tunnelPool.allocateTunnel(id, _clientPool.getDestination()); + if (ok) { + allocated++; + } + } + + if (allocated < numTunnels) { + requestCustomTunnels(numTunnels - allocated); + } else { + _log.debug("Sufficient tunnels exist in the client pool for " + _clientPool.getDestination().calculateHash() + " w3wt"); + // done! w00t + } + } + + /** + * Determine if the tunnel will meet the client's requirements. + * + */ + private boolean isGoodEnough(TunnelInfo info, int max) { + if (!info.getIsReady()) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Refusing tunnel " + info.getTunnelId() + " because it isn't ready"); + return false; + } + + long expireAfter = Clock.getInstance().now() + POOL_CHECK_DELAY + _tunnelPool.getTunnelCreationTimeout()*2; + if (info.getSettings().getExpiration() <= expireAfter) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Refusing tunnel " + info.getTunnelId() + " because it is going to expire soon"); + return false; + } + + int length = info.getLength(); + if (_clientPool.getClientSettings().getEnforceStrictMinimumLength()) { + if (length < _clientPool.getClientSettings().getDepthInbound()) { + // we will require at least the client's length, but they dont meet it + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Refusing tunnel " + info.getTunnelId() + " because it is too short (length = " + length + + ", wanted = " + _clientPool.getClientSettings().getDepthInbound() + ")"); + return false; + } else { + // its long enough. w00t + } + } else { + if (length < _clientPool.getClientSettings().getDepthInbound() && (length < max)) { + // while we will still strive to meet the client's needs, we will be satisfied with + // the best we have on hand (which may be less that their requested length) + // this tunnel however meets neither criteria + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Refusing tunnel " + info.getTunnelId() + " because it is too short (length = " + length + + ", wanted = " + _clientPool.getClientSettings().getDepthInbound() + ")"); + return false; + } else { + // either its long enough, or its the longest we have. + // if we want to be strict, specify tunnels.enforceStrictMinimumLength either in the JVM environment + // via -Dtunnels.enforceStrictMinimumLength=true or in the router.config + // (tunnels.enforceStrictMinimumLength=true) + } + } + + if (info.getDestination() != null) { + if (!_clientPool.getDestination().equals(info.getDestination())) { + if (_log.shouldLog(Log.INFO)) + _log.info("Refusing tunnel " + info.getTunnelId() + " because it was requested specifically for another destination [" + info.getDestination().calculateHash() + "]"); + return false; + } + } + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Accepting tunnel " + info.getTunnelId()); + return true; + } + + /** + * Request numTunnels more tunnels (the free pool doesnt have enough satisfactory ones). + * This fires off a series of RequestCustomTunnelJobs + */ + private void requestCustomTunnels(int numTunnels) { + for (int i = 0; i < numTunnels; i++) { + JobQueue.getInstance().addJob(new RequestCustomTunnelJob()); + } + } + + /** + * Request a new tunnel specifically to the client's requirements, marked as for them so other + * ClientTunnelPool's wont take it + * + */ + private class RequestCustomTunnelJob extends JobImpl { + public String getName() { return "Request Custom Client Tunnel"; } + public void runJob() { + TunnelInfo tunnelGateway = TunnelBuilder.getInstance().configureInboundTunnel(_clientPool.getDestination(), _clientPool.getClientSettings()); + RequestTunnelJob reqJob = new RequestTunnelJob(_tunnelPool, tunnelGateway, true, _tunnelPool.getTunnelCreationTimeout()); + JobQueue.getInstance().addJob(reqJob); + } + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/HandleTunnelCreateMessageJob.java b/router/java/src/net/i2p/router/tunnelmanager/HandleTunnelCreateMessageJob.java new file mode 100644 index 0000000000..c5fc718bf7 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/HandleTunnelCreateMessageJob.java @@ -0,0 +1,158 @@ +package net.i2p.router.tunnelmanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Hash; +import net.i2p.data.RouterIdentity; +import net.i2p.data.TunnelId; +import net.i2p.data.RouterInfo; +import net.i2p.data.i2np.SourceRouteBlock; +import net.i2p.data.i2np.TunnelCreateMessage; +import net.i2p.data.i2np.TunnelCreateStatusMessage; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.TunnelSettings; +import net.i2p.router.MessageHistory; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.message.BuildTestMessageJob; +import net.i2p.router.message.SendReplyMessageJob; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Date; + +public class HandleTunnelCreateMessageJob extends JobImpl { + private final static Log _log = new Log(HandleTunnelCreateMessageJob.class); + private TunnelCreateMessage _message; + private RouterIdentity _from; + private Hash _fromHash; + private SourceRouteBlock _replyBlock; + + private final static long TIMEOUT = 30*1000; // 30 secs to contact a peer that will be our next hop + private final static int PRIORITY = 123; + + HandleTunnelCreateMessageJob(TunnelCreateMessage receivedMessage, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock) { + _message = receivedMessage; + _from = from; + _fromHash = fromHash; + _replyBlock = replyBlock; + } + + public void runJob() { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Handling tunnel create"); + TunnelInfo info = new TunnelInfo(); + info.setConfigurationKey(_message.getConfigurationKey()); + info.setEncryptionKey(_message.getTunnelKey()); + info.setNextHop(_message.getNextRouter()); + + TunnelSettings settings = new TunnelSettings(); + settings.setBytesPerMinuteAverage(_message.getMaxAvgBytesPerMin()); + settings.setBytesPerMinutePeak(_message.getMaxPeakBytesPerMin()); + settings.setMessagesPerMinuteAverage(_message.getMaxAvgMessagesPerMin()); + settings.setMessagesPerMinutePeak(_message.getMaxPeakMessagesPerMin()); + settings.setExpiration(_message.getTunnelDurationSeconds()*1000+Clock.getInstance().now()); + settings.setIncludeDummy(_message.getIncludeDummyTraffic()); + settings.setReorder(_message.getReorderMessages()); + info.setSettings(settings); + + info.setSigningKey(_message.getVerificationPrivateKey()); + info.setThisHop(Router.getInstance().getRouterInfo().getIdentity().getHash()); + info.setTunnelId(_message.getTunnelId()); + info.setVerificationKey(_message.getVerificationPublicKey()); + + info.getTunnelId().setType(TunnelId.TYPE_PARTICIPANT); + + if (_message.getNextRouter() == null) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("We're the endpoint, don't test the \"next\" peer [duh]"); + boolean ok = TunnelManagerFacade.getInstance().joinTunnel(info); + sendReply(ok); + } else { + NetworkDatabaseFacade.getInstance().lookupRouterInfo(info.getNextHop(), new TestJob(info), new JoinJob(info, false), TIMEOUT); + } + } + + private class TestJob extends JobImpl { + private TunnelInfo _target; + public TestJob(TunnelInfo target) { + _target = target; + } + + public String getName() { return "Run a test for peer reachability"; } + public void runJob() { + RouterInfo info = NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(_target.getNextHop()); + if (info == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error - unable to look up peer " + _target.toBase64() + ", even though we were queued up via onSuccess??"); + return; + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Lookup successful for tested peer " + _target.toBase64() + ", now continue with the test"); + JobQueue.getInstance().addJob(new BuildTestMessageJob(info, Router.getInstance().getRouterInfo().getIdentity().getHash(), new JoinJob(_target, true), new JoinJob(_target, false), TIMEOUT, PRIORITY)); + } + } + } + + + private void sendReply(boolean ok) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending reply to a tunnel create of id " + _message.getTunnelId() + " with ok (" + ok + ") to router " + _message.getReplyBlock().getRouter().toBase64()); + + MessageHistory.getInstance().receiveTunnelCreate(_message.getTunnelId(), _message.getNextRouter(), new Date(Clock.getInstance().now() + 1000*_message.getTunnelDurationSeconds()), ok, _message.getReplyBlock().getRouter()); + + TunnelCreateStatusMessage msg = new TunnelCreateStatusMessage(); + msg.setFromHash(Router.getInstance().getRouterInfo().getIdentity().getHash()); + msg.setTunnelId(_message.getTunnelId()); + if (ok) { + msg.setStatus(TunnelCreateStatusMessage.STATUS_SUCCESS); + } else { + // since we don't actually check anything, this is a catch all + msg.setStatus(TunnelCreateStatusMessage.STATUS_FAILED_OVERLOADED); + } + msg.setMessageExpiration(new Date(Clock.getInstance().now()+60*1000)); + SendReplyMessageJob job = new SendReplyMessageJob(_message.getReplyBlock(), msg, PRIORITY); + JobQueue.getInstance().addJob(job); + } + + public String getName() { return "Handle Tunnel Create Message"; } + + private class JoinJob extends JobImpl { + private TunnelInfo _info; + private boolean _isReachable; + public JoinJob(TunnelInfo info, boolean isReachable) { + _info = info; + _isReachable = isReachable; + } + + public void runJob() { + if (!_isReachable) { + long before = Clock.getInstance().now(); + sendReply(false); + long after = Clock.getInstance().now(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("JoinJob .refuse took " + (after-before) + "ms to refuse " + _info); + } else { + long before = Clock.getInstance().now(); + boolean ok = TunnelManagerFacade.getInstance().joinTunnel(_info); + long afterJoin = Clock.getInstance().now(); + sendReply(ok); + long after = Clock.getInstance().now(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("JoinJob .joinTunnel took " + (afterJoin-before) + "ms and sendReply took " + (after-afterJoin) + "ms"); + } + } + public String getName() { return "Process the tunnel join after testing the nextHop"; } + } + + public void dropped() { + MessageHistory.getInstance().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Dropped due to overload"); + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelManagerFacade.java b/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelManagerFacade.java new file mode 100644 index 0000000000..0e9ecc5375 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelManagerFacade.java @@ -0,0 +1,201 @@ +package net.i2p.router.tunnelmanager; + +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.TunnelCreateMessage; +import net.i2p.router.ClientTunnelSettings; +import net.i2p.router.InNetMessagePool; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.TunnelSelectionCriteria; +import net.i2p.stat.StatManager; +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * Main interface to the pool + * + */ +public class PoolingTunnelManagerFacade extends TunnelManagerFacade { + private final static Log _log = new Log(PoolingTunnelManagerFacade.class); + private TunnelPool _pool; + private TunnelTestManager _testManager; + + static { + StatManager.getInstance().createFrequencyStat("tunnel.acceptRequestFrequency", "How often do we accept requests to join a tunnel?", "Tunnels", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createFrequencyStat("tunnel.rejectRequestFrequency", "How often do we reject requests to join a tunnel?", "Tunnels", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + public PoolingTunnelManagerFacade() { + super(); + InNetMessagePool.getInstance().registerHandlerJobBuilder(TunnelCreateMessage.MESSAGE_TYPE, new TunnelCreateMessageHandler()); + } + + public void startup() { + if (_pool == null) + _pool = new TunnelPool(); + _pool.startup(); + _testManager = new TunnelTestManager(_pool); + } + + public void shutdown() { + _pool.shutdown(); + _testManager.stopTesting(); + _testManager = null; + } + + /** + * React to a request to join the specified tunnel. + * + * @return true if the router will accept participation, else false. + */ + public boolean joinTunnel(TunnelInfo info) { + if (info == null) { + _log.error("Null tunnel", new Exception("Null tunnel")); + StatManager.getInstance().updateFrequency("tunnel.rejectRequestFrequency"); + return false; + } + if (info.getSettings() == null) { + _log.error("Null settings!", new Exception("settings are null")); + StatManager.getInstance().updateFrequency("tunnel.rejectRequestFrequency"); + return false; + } + if (info.getSettings().getExpiration() == 0) { + _log.info("No expiration for tunnel " + info.getTunnelId().getTunnelId(), new Exception("No expiration")); + StatManager.getInstance().updateFrequency("tunnel.rejectRequestFrequency"); + return false; + } else { + if (info.getSettings().getExpiration() < Clock.getInstance().now()) { + _log.warn("Already expired - " + new Date(info.getSettings().getExpiration()), new Exception("Already expired")); + StatManager.getInstance().updateFrequency("tunnel.rejectRequestFrequency"); + return false; + } + } + + _log.debug("Joining tunnel: " + info); + boolean ok = _pool.addParticipatingTunnel(info); + if (!ok) + StatManager.getInstance().updateFrequency("tunnel.rejectRequestFrequency"); + else + StatManager.getInstance().updateFrequency("tunnel.acceptRequestFrequency"); + return ok; + } + /** + * Retrieve the information related to a particular tunnel + * + */ + public TunnelInfo getTunnelInfo(TunnelId id) { + return _pool.getTunnelInfo(id); + } + /** + * Retrieve a set of tunnels from the existing ones for various purposes + */ + public List selectOutboundTunnelIds(TunnelSelectionCriteria criteria) { + return PoolingTunnelSelector.selectOutboundTunnelIds(_pool, criteria); + } + /** + * Retrieve a set of tunnels from the existing ones for various purposes + */ + public List selectInboundTunnelIds(TunnelSelectionCriteria criteria) { + return PoolingTunnelSelector.selectInboundTunnelIds(_pool, criteria); + } + + /** + * Make sure appropriate outbound tunnels are in place, builds requested + * inbound tunnels, then fire off a job to ask the ClientManagerFacade to + * validate the leaseSet, then publish it in the network database. + * + */ + public void createTunnels(Destination destination, ClientTunnelSettings clientSettings, long timeoutMs) { + ClientTunnelPool pool = _pool.getClientPool(destination); + if (pool != null) { + pool.setClientSettings(clientSettings); + } else { + _pool.createClientPool(destination, clientSettings); + } + } + + /** + * Called when a peer becomes unreachable - go through all of the current + * tunnels and rebuild them if we can, or drop them if we can't. + * + */ + public void peerFailed(Hash peer) { + int numFailed = 0; + for (Iterator iter = _pool.getManagedTunnelIds().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = (TunnelInfo)_pool.getTunnelInfo(id); + if (isParticipant(info, peer)) { + _log.info("Peer " + peer.toBase64() + " failed and they participate in tunnel " + id.getTunnelId() + ". Marking the tunnel as not ready!"); + info.setIsReady(false); + numFailed++; + + long lifetime = Clock.getInstance().now() - info.getCreated(); + StatManager.getInstance().addRateData("tunnel.failAfterTime", lifetime, lifetime); + } + } + + _log.info("On peer " + peer.toBase64() + " failure, " + numFailed + " tunnels were killed"); + } + + private boolean isParticipant(TunnelInfo info, Hash peer) { + if ( (info == null) || (peer == null) ) return false; + TunnelInfo cur = info; + while (cur != null) { + if (peer.equals(cur.getThisHop())) return true; + if (peer.equals(cur.getNextHop())) return true; + cur = cur.getNextHopInfo(); + } + return false; + } + + /** + * True if the peer currently part of a tunnel + * + */ + public boolean isInUse(Hash peer) { + if (isInUse(peer, _pool.getManagedTunnelIds())) { + if (_log.shouldLog(Log.INFO)) + _log.debug("Peer is in a managed tunnel: " + peer.toBase64()); + return true; + } + if (isInUse(peer, _pool.getPendingTunnels())) { + if (_log.shouldLog(Log.INFO)) + _log.debug("Peer is in a pending tunnel: " + peer.toBase64()); + return true; + } + if (isInUse(peer, _pool.getParticipatingTunnels())) { + if (_log.shouldLog(Log.INFO)) + _log.debug("Peer is in a participating tunnel: " + peer.toBase64()); + return true; + } + return false; + } + + private boolean isInUse(Hash peer, Set tunnelIds) { + for (Iterator iter = tunnelIds.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getTunnelInfo(id); + if (isParticipant(info, peer)) + return true; + } + return false; + } + + /** + * Aint she pretty? + * + */ + public String renderStatusHTML() { + if (_pool != null) + return _pool.renderStatusHTML(); + else + return "

Tunnel Manager not initialized

\n"; + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelSelector.java b/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelSelector.java new file mode 100644 index 0000000000..c646309288 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelSelector.java @@ -0,0 +1,119 @@ +package net.i2p.router.tunnelmanager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import net.i2p.data.TunnelId; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelSelectionCriteria; +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; + +/** + * Implement the tunnel selection algorithms + * + */ +class PoolingTunnelSelector { + private final static Log _log = new Log(PoolingTunnelSelector.class); + /** don't use a tunnel thats about to expire */ + private static long POOL_USE_SAFETY_MARGIN = 10*1000; + + public static List selectOutboundTunnelIds(TunnelPool pool, TunnelSelectionCriteria criteria) { + List tunnelIds = new LinkedList(); + + for (int i = pool.getOutboundTunnelCount(); i < criteria.getMinimumTunnelsRequired(); i++) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Building fake tunnels because the outbound tunnels weren't sufficient"); + pool.buildFakeTunnels(); + } + + Set outIds = pool.getOutboundTunnels(); + for (Iterator iter = outIds.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = pool.getOutboundTunnel(id); + if ( (info != null) && (info.getIsReady()) ) { + tunnelIds.add(id); + } else { + if (info == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Outbound tunnel " + id + " was not found?! expire race perhaps?"); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Outbound tunnel " + id + " was not ready?! " + new Date(info.getSettings().getExpiration())); + } + } + } + List ordered = randomize(pool, tunnelIds); + List rv = new ArrayList(criteria.getMinimumTunnelsRequired()); + for (Iterator iter = ordered.iterator(); iter.hasNext() && (rv.size() < criteria.getMinimumTunnelsRequired()); ) { + rv.add(iter.next()); + } + _log.info("Selecting outbound tunnelIds [all outbound tunnels: " + outIds.size() + ", tunnelIds ready: " + ordered.size() + ", rv: " + rv + "]"); + return rv; + } + + public static List selectInboundTunnelIds(TunnelPool pool, TunnelSelectionCriteria criteria) { + List tunnels = new LinkedList(); + + for (int i = pool.getFreeTunnelCount(); i < criteria.getMinimumTunnelsRequired(); i++) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Building fake tunnels because the inbound tunnels weren't sufficient"); + pool.buildFakeTunnels(); + } + + for (Iterator iter = pool.getFreeTunnels().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = pool.getFreeTunnel(id); + if (info == null) continue; + if (info.getIsReady()) { + tunnels.add(id); + } else { + _log.debug("Inbound tunnel " + id + " is not ready?! " + new Date(info.getSettings().getExpiration())); + } + } + + List ordered = randomize(pool, tunnels); + List rv = new ArrayList(criteria.getMinimumTunnelsRequired()); + for (Iterator iter = ordered.iterator(); iter.hasNext() && (rv.size() < criteria.getMinimumTunnelsRequired()); ) { + rv.add(iter.next()); + } + _log.info("Selecting inbound tunnelIds [tunnelIds ready: " + tunnels.size() + ", rv: " + rv + "]"); + return rv; + } + + //// + // helpers + //// + + + private final static List randomize(TunnelPool pool, List tunnelIds) { + List rv = new ArrayList(tunnelIds.size()); + for (Iterator iter = tunnelIds.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + if (isAlmostExpired(pool, id, POOL_USE_SAFETY_MARGIN)) + continue; + rv.add(id); + } + Collections.shuffle(rv, RandomSource.getInstance()); + return rv; + } + + private final static boolean isAlmostExpired(TunnelPool pool, TunnelId id, long safetyMargin) { + TunnelInfo info = pool.getTunnelInfo(id); + if (info == null) return true; + if (info.getSettings() == null) return true; + if (info.getSettings().getExpiration() <= 0) return true; + if (info.getSettings().getExpiration() - safetyMargin <= Clock.getInstance().now()) { + _log.debug("Expiration of tunnel " + id.getTunnelId() + " has almost been reached [" + new Date(info.getSettings().getExpiration()) + "]"); + return true; + } else { + return false; + } + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/RequestInboundTunnelJob.java b/router/java/src/net/i2p/router/tunnelmanager/RequestInboundTunnelJob.java new file mode 100644 index 0000000000..a780310206 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/RequestInboundTunnelJob.java @@ -0,0 +1,29 @@ +package net.i2p.router.tunnelmanager; + +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.TunnelInfo; +import net.i2p.util.Log; + +class RequestInboundTunnelJob extends JobImpl { + private final static Log _log = new Log(RequestInboundTunnelJob.class); + private TunnelPool _pool; + private boolean _useFake; + + public RequestInboundTunnelJob(TunnelPool pool) { + this(pool, false); + } + public RequestInboundTunnelJob(TunnelPool pool, boolean useFake) { + super(); + _pool = pool; + _useFake = useFake; + } + + public String getName() { return "Request Inbound Tunnel"; } + public void runJob() { + _log.debug("Client pool settings: " + _pool.getPoolSettings().toString()); + TunnelInfo tunnelGateway = TunnelBuilder.getInstance().configureInboundTunnel(null, _pool.getPoolSettings(), _useFake); + RequestTunnelJob reqJob = new RequestTunnelJob(_pool, tunnelGateway, true, _pool.getTunnelCreationTimeout()); + JobQueue.getInstance().addJob(reqJob); + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/RequestOutboundTunnelJob.java b/router/java/src/net/i2p/router/tunnelmanager/RequestOutboundTunnelJob.java new file mode 100644 index 0000000000..0094e0e851 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/RequestOutboundTunnelJob.java @@ -0,0 +1,23 @@ +package net.i2p.router.tunnelmanager; + +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.TunnelInfo; + +class RequestOutboundTunnelJob extends JobImpl { + private TunnelPool _pool; + private boolean _useFake; + + public RequestOutboundTunnelJob(TunnelPool pool, boolean useFake) { + super(); + _pool = pool; + _useFake = useFake; + } + + public String getName() { return "Request Outbound Tunnel"; } + public void runJob() { + TunnelInfo tunnelGateway = TunnelBuilder.getInstance().configureOutboundTunnel(_pool.getPoolSettings(), _useFake); + RequestTunnelJob reqJob = new RequestTunnelJob(_pool, tunnelGateway, false, _pool.getTunnelCreationTimeout()); + JobQueue.getInstance().addJob(reqJob); + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/RequestTunnelJob.java b/router/java/src/net/i2p/router/tunnelmanager/RequestTunnelJob.java new file mode 100644 index 0000000000..f4cc26b8e6 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/RequestTunnelJob.java @@ -0,0 +1,863 @@ +package net.i2p.router.tunnelmanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import net.i2p.crypto.KeyGenerator; +import net.i2p.crypto.SessionKeyManager; +import net.i2p.data.Certificate; +import net.i2p.data.DataFormatException; +import net.i2p.data.Hash; +import net.i2p.data.PublicKey; +import net.i2p.data.RouterInfo; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.data.i2np.DeliveryStatusMessage; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.SourceRouteBlock; +import net.i2p.data.i2np.TunnelCreateMessage; +import net.i2p.data.i2np.TunnelCreateStatusMessage; +import net.i2p.router.Job; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageSelector; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.PeerManagerFacade; +import net.i2p.router.PeerSelectionCriteria; +import net.i2p.router.ProfileManager; +import net.i2p.router.ReplyJob; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.TunnelSelectionCriteria; +import net.i2p.router.MessageHistory; +import net.i2p.router.message.GarlicConfig; +import net.i2p.router.message.GarlicMessageBuilder; +import net.i2p.router.message.PayloadGarlicConfig; +import net.i2p.router.message.SendTunnelMessageJob; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +import net.i2p.stat.StatManager; + +/** + * Request the creation of a new tunnel + * + */ +public class RequestTunnelJob extends JobImpl { + private final static Log _log = new Log(RequestTunnelJob.class); + private TunnelPool _pool; + private boolean _complete; + private long _timeoutMs; + private long _expiration; + private TunnelInfo _tunnelGateway; + private List _toBeRequested; // list of participants, from endpoint to gateway + private Set _failedTunnelParticipants; // set of Hash of the RouterIdentity of participants who timed out or rejected + private boolean _isInbound; + + private final static int PRIORITY = 300; // high since we are creating tunnels for a client + + static { + StatManager.getInstance().createFrequencyStat("tunnel.buildFrequency", "How often does the router build a tunnel?", "Tunnels", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createFrequencyStat("tunnel.buildFailFrequency", "How often does a peer in the tunnel fail to join??", "Tunnels", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + + RequestTunnelJob(TunnelPool pool, TunnelInfo tunnelGateway, boolean isInbound, long timeoutMs) { + _pool = pool; + _tunnelGateway = tunnelGateway; + _toBeRequested = new ArrayList(); + _timeoutMs = timeoutMs; + _expiration = -1; + _isInbound = isInbound; + _failedTunnelParticipants = new HashSet(); + _complete = false; + + List participants = new ArrayList(); + TunnelInfo cur = _tunnelGateway; + while (cur != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Tunnel " + cur.getTunnelId() + " includes " + cur.getThisHop().toBase64()); + participants.add(cur); + cur = cur.getNextHopInfo(); + } + if (isInbound) { + if (_log.shouldLog(Log.INFO)) + _log.info("Requesting inbound tunnel " + _tunnelGateway.getTunnelId() + " with " + participants.size() + " participants in it"); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Requesting outbound tunnel " + _tunnelGateway.getTunnelId() + " with " + participants.size() + " participants in it"); + } + + // since we request serially, we need to up the timeout serially + // change this once we go parallel + _timeoutMs *= participants.size()+1; + + // work backwards (end point, then the router pointing at the endpoint, then the router pointing at that, etc, until the gateway + _toBeRequested = new ArrayList(participants.size()); + for (int i = participants.size()-1; i >= 0; i--) + _toBeRequested.add(participants.get(i)); + } + + public String getName() { return "Request Tunnel"; } + public void runJob() { + if (_expiration < 0) _expiration = _timeoutMs + Clock.getInstance().now(); + if (Clock.getInstance().now() > _expiration) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Timeout reached building tunnel (timeout = " + _timeoutMs + " expiration = " + new Date(_expiration) + ")"); + fail(); + return; + } + + TunnelInfo peer = null; + synchronized (_toBeRequested) { + if (_toBeRequested.size() > 0) { + _pool.addPendingTunnel(_tunnelGateway); + + peer = (TunnelInfo)_toBeRequested.remove(0); + if ( (peer == null) || (peer.getThisHop() == null) ) { + return; + } else { + // jump out of the synchronized block to request + } + } + } + if (peer != null) + requestParticipation(peer); + } + + private void requestParticipation(TunnelInfo participant) { + // find the info about who we're looking for + RouterInfo target = NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(participant.getThisHop()); + if (target == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error - no db info known for participant " + participant.getThisHop()); + fail(); + return; + } + + if (target.getIdentity().getHash().equals(Router.getInstance().getRouterInfo().getIdentity().getHash())) { + // short circuit the ok + okLocalParticipation(participant); + return; + } + + // select send method [outbound tunnel or garlic through peers] + TunnelId outboundTunnel = selectOutboundTunnel(); + if (outboundTunnel == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("No outbound tunnels! unable to request a new tunnel!"); + fail(); + return; + } + + // select reply peer [peer to which SourceRouteReply should be sent, and from which the reply will be forwarded to an inbound tunnel] + RouterInfo replyPeer = selectReplyPeer(participant); + if (replyPeer == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("No reply peers available! unable to request a new tunnel!"); + fail(); + return; + } + + // select inbound tunnel gateway + TunnelGateway inboundGateway = selectInboundGateway(participant, replyPeer); + if (inboundGateway == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Unable to find an inbound gateway"); + fail(); + return; + } + + SessionKey wrappedKey = new SessionKey(); + Set wrappedTags = new HashSet(64); + PublicKey wrappedTo = new PublicKey(); + + RequestState state = new RequestState(wrappedKey, wrappedTags, wrappedTo, participant, inboundGateway, replyPeer, outboundTunnel, target); + Request r = new Request(state); + JobQueue.getInstance().addJob(r); + } + + /** + * The request job steps through the RequestState, pushing it along one pass + * at a time, all with each pass occurring as a seperate sequential job. This + * is useful since the RequestTunnelJob can otherwise take upwards of 3+ seconds, + * since the various steps may involve full ElGamal encryption (for source route + * blocks, the garlic, etc). + */ + public class Request extends JobImpl { + private RequestState _state; + Request(RequestState state) { + super(); + _state = state; + } + + public void runJob() { + boolean needsMore = _state.doNext(); + if (needsMore) + requeue(0); + else + MessageHistory.getInstance().requestTunnelCreate(_tunnelGateway.getTunnelId(), + _state.getOutboundTunnel(), + _state.getParticipant().getThisHop(), + _state.getParticipant().getNextHop(), + _state.getReplyPeer().getIdentity().getHash(), + _state.getInboundGateway().getTunnelId(), + _state.getInboundGateway().getGateway()); + } + + public String getName() { return "Request Tunnel (partial)"; } + } + + /** + * Contain the partial state for preparing the request - doNext starts by + * building a TunnelCreateMessage, and on the next pass it builds a + * DeliveryStatusMessage, and on the pass after that, it builds a GarlicMessage + * containing those two, and on its final pass, it sends everything out through + * a tunnel with appropriate handling jobs + * + */ + private class RequestState { + private SessionKey _wrappedKey; + private Set _wrappedTags; + private PublicKey _wrappedTo; + private TunnelCreateMessage _createMsg; + private DeliveryStatusMessage _statusMsg; + private GarlicMessage _garlicMessage; + private TunnelInfo _participant; + private TunnelGateway _inboundGateway; + private RouterInfo _replyPeer; + private TunnelId _outboundTunnel; + private RouterInfo _target; + + public RequestState(SessionKey wrappedKey, Set wrappedTags, PublicKey wrappedTo, TunnelInfo participant, TunnelGateway inboundGateway, RouterInfo replyPeer, TunnelId outboundTunnel, RouterInfo target) { + _wrappedKey = wrappedKey; + _wrappedTags = wrappedTags; + _wrappedTo = wrappedTo; + _participant = participant; + _inboundGateway = inboundGateway; + _replyPeer = replyPeer; + _outboundTunnel = outboundTunnel; + _target = target; + } + + public TunnelId getOutboundTunnel() { return _outboundTunnel; } + public TunnelInfo getParticipant() { return _participant; } + public RouterInfo getReplyPeer() { return _replyPeer; } + public TunnelGateway getInboundGateway() { return _inboundGateway; } + + public boolean doNext() { + if (_createMsg == null) { + _createMsg = buildTunnelCreate(_participant, _inboundGateway, _replyPeer); + return true; + } else if (_statusMsg == null) { + _statusMsg = buildDeliveryStatusMessage(); + return true; + } else if (_garlicMessage == null) { + _garlicMessage = buildGarlicMessage(_createMsg, _statusMsg, _replyPeer, _inboundGateway, _target, _wrappedKey, _wrappedTags, _wrappedTo); + return true; + } else { + // send the GarlicMessage + if (_log.shouldLog(Log.INFO)) + _log.info("Sending tunnel create to " + _target.getIdentity().getHash().toBase64() + + " with replies through " + _replyPeer.getIdentity().getHash().toBase64() + + " to inbound gateway " + _inboundGateway.getGateway().toBase64() + + " : " + _inboundGateway.getTunnelId().getTunnelId()); + ReplyJob onReply = new Success(_participant, _wrappedKey, _wrappedTags, _wrappedTo); + Job onFail = new Failure(_participant, _replyPeer.getIdentity().getHash()); + MessageSelector selector = new Selector(_participant, _statusMsg.getMessageId()); + SendTunnelMessageJob j = new SendTunnelMessageJob(_garlicMessage, _outboundTunnel, _target.getIdentity().getHash(), + null, null, onReply, onFail, selector, _timeoutMs, PRIORITY); + JobQueue.getInstance().addJob(j); + return false; + } + } + } + + /** + * Handle the "will you participate" request that we would send to ourselves in a special case (aka fast) manner, + * as, chances are, we'll always agree ;) + * + */ + private void okLocalParticipation(TunnelInfo info) { + if (_log.shouldLog(Log.INFO)) + _log.info("Short circuiting the local join to tunnel " + info.getTunnelId()); + peerSuccess(info); + } + + /** + * Select an outbound tunnel for sending the tunnel create status message + * + */ + private TunnelId selectOutboundTunnel() { + TunnelSelectionCriteria crit = new TunnelSelectionCriteria(); + crit.setMaximumTunnelsRequired(1); + crit.setMinimumTunnelsRequired(1); + crit.setAnonymityPriority(50); // arbitrary + crit.setLatencyPriority(50); // arbitrary + crit.setReliabilityPriority(50); // arbitrary + + List tunnelIds = TunnelManagerFacade.getInstance().selectOutboundTunnelIds(crit); + TunnelId id = null; + if (tunnelIds.size() > 0) + id = (TunnelId)tunnelIds.get(0); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Outbound tunnel selected: " + id); + return id; + } + + /** + * Select a peer to which the tunnelParticipant will send the SourceRouteReplyMessage + * containing a garlic wrapped TunnelCreateStatusMessage destined for the local router. + * + * Currently just a random peer + */ + private RouterInfo selectReplyPeer(TunnelInfo tunnelParticipant) { + PeerSelectionCriteria criteria = new PeerSelectionCriteria(); + criteria.setMaximumRequired(1); + criteria.setMinimumRequired(1); + criteria.setPurpose(PeerSelectionCriteria.PURPOSE_SOURCE_ROUTE); + List peerHashes = PeerManagerFacade.getInstance().selectPeers(criteria); + + RouterInfo peerInfo = null; + for (int i = 0; (i < peerHashes.size()) && (peerInfo == null); i++) { + Hash peerHash = (Hash)peerHashes.get(i); + peerInfo = NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(peerHash); + if (peerInfo == null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Selected a peer [" + peerHash + "] we don't have info on locally... trying another"); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer [" + peerHash.toBase64() + "] is known locally, keep it in the list of replyPeers"); + break; + } + } + + if (peerInfo == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("No peers know for a reply (out of " + peerHashes.size() + ") - using ourself"); + return Router.getInstance().getRouterInfo(); + } else { + return peerInfo; + } + } + + /** + * Select an inbound tunnel to receive replies and acks from the participant by means of the + * replyPeer + * + */ + private TunnelGateway selectInboundGateway(TunnelInfo participant, RouterInfo replyPeer) { + TunnelSelectionCriteria criteria = new TunnelSelectionCriteria(); + criteria.setAnonymityPriority(66); + criteria.setReliabilityPriority(66); + criteria.setLatencyPriority(33); + criteria.setMaximumTunnelsRequired(1); + criteria.setMinimumTunnelsRequired(1); + List ids = TunnelManagerFacade.getInstance().selectInboundTunnelIds(criteria); + if (ids.size() <= 0) { + if (_log.shouldLog(Log.ERROR)) + _log.error("No inbound tunnels to receive the tunnel create messages. Argh", new Exception("Tunnels suck. whats up?")); + return null; + } else { + TunnelInfo gateway = null; + TunnelId id = null; + for (int i = 0; i < ids.size(); i++) { + id = (TunnelId)ids.get(i); + gateway = TunnelManagerFacade.getInstance().getTunnelInfo(id); + if (gateway != null) + break; + } + if (gateway != null) { + TunnelGateway gw = new TunnelGateway(id, gateway.getThisHop()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Inbound tunnel gateway: " + id + " on router " + gateway.getThisHop()); + return gw; + } else { + if (_log.shouldLog(Log.ERROR)) + _log.error("No gateway found?!", new Exception("No gateway")); + return null; + } + } + } + + /** + * Build a TunnelCreateMessage to the participant + */ + private TunnelCreateMessage buildTunnelCreate(TunnelInfo participant, TunnelGateway replyGateway, RouterInfo replyPeer) { + TunnelCreateMessage msg = new TunnelCreateMessage(); + msg.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + msg.setConfigurationKey(participant.getConfigurationKey()); + msg.setIncludeDummyTraffic(participant.getSettings().getIncludeDummy()); + msg.setMaxAvgBytesPerMin(participant.getSettings().getBytesPerMinuteAverage()); + msg.setMaxAvgMessagesPerMin(participant.getSettings().getMessagesPerMinuteAverage()); + msg.setMaxPeakBytesPerMin(participant.getSettings().getBytesPerMinutePeak()); + msg.setMaxPeakMessagesPerMin(participant.getSettings().getMessagesPerMinutePeak()); + msg.setNextRouter(participant.getNextHop()); + if (participant.getNextHop() == null) + msg.setParticipantType(TunnelCreateMessage.PARTICIPANT_TYPE_ENDPOINT); + else if (participant.getSigningKey() != null) + msg.setParticipantType(TunnelCreateMessage.PARTICIPANT_TYPE_GATEWAY); + else + msg.setParticipantType(TunnelCreateMessage.PARTICIPANT_TYPE_OTHER); + msg.setReorderMessages(participant.getSettings().getReorder()); + + SourceRouteBlock replyBlock = buildReplyBlock(replyGateway, replyPeer); + if (replyBlock == null) + return null; + + msg.setReplyBlock(replyBlock); + long duration = participant.getSettings().getExpiration() - Clock.getInstance().now(); + if (duration == 0) duration = 1; + msg.setTunnelDurationSeconds(duration/1000); + msg.setTunnelId(participant.getTunnelId()); + msg.setTunnelKey(participant.getEncryptionKey()); + msg.setVerificationPrivateKey(participant.getSigningKey()); + msg.setVerificationPublicKey(participant.getVerificationKey()); + + return msg; + } + + /** + * Build a source route block directing the reply through the gateway by means of the + * replyPeer + * + */ + private SourceRouteBlock buildReplyBlock(TunnelGateway gateway, RouterInfo replyPeer) { + if (replyPeer == null) { + if (_log.shouldLog(Log.ERROR)) + _log.error("No peer specified for reply!"); + return null; + } + + SessionKey replySessionKey = KeyGenerator.getInstance().generateSessionKey(); + SessionTag tag = new SessionTag(true); + Set tags = new HashSet(); + tags.add(tag); + // make it so we'll read the session tag correctly and use the right session key + SessionKeyManager.getInstance().tagsReceived(replySessionKey, tags); + + PublicKey pk = replyPeer.getIdentity().getPublicKey(); + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDelayRequested(false); + instructions.setDelaySeconds(0); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_TUNNEL); + instructions.setDestination(null); + instructions.setEncrypted(false); + instructions.setEncryptionKey(null); + instructions.setRouter(gateway.getGateway()); + instructions.setTunnelId(gateway.getTunnelId()); + + long replyId = RandomSource.getInstance().nextInt(Integer.MAX_VALUE); + + Certificate replyCert = new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null); + + long expiration = _expiration; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Setting the expiration on the reply block to " + (new Date(expiration))); + SourceRouteBlock block = new SourceRouteBlock(); + try { + long begin = Clock.getInstance().now(); + block.setData(instructions, replyId, replyCert, expiration, pk); + long end = Clock.getInstance().now(); + if ( (end - begin) > 1000) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Took too long (" + (end-begin) + "ms) to build source route block"); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("did NOT take long (" + (end-begin) + "ms) to build source route block!"); + } + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error building the reply block", dfe); + return null; + } + + block.setRouter(replyPeer.getIdentity().getHash()); + block.setKey(replySessionKey); + block.setTag(tag); + + return block; + } + + /** + * Create a message containing a random id to check for after garlic routing + * it out so that we know the other message in the garlic has been received + * + */ + private DeliveryStatusMessage buildDeliveryStatusMessage() { + DeliveryStatusMessage msg = new DeliveryStatusMessage(); + msg.setArrival(new Date(Clock.getInstance().now())); + msg.setMessageId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + Date exp = new Date(_expiration); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Setting the expiration on the delivery status message to " + exp); + msg.setMessageExpiration(exp); + return msg; + } + + + /** + * Build a garlic message wrapping the data and status as cloves with both to be routed + * through the target, where the data is destined. The status however is to continue on + * to the replyPeer, where it is then sent down the replyTunnel to the local router. + * + */ + private GarlicMessage buildGarlicMessage(I2NPMessage data, I2NPMessage status, RouterInfo replyPeer, TunnelGateway replyTunnel, RouterInfo target, SessionKey wrappedKey, Set wrappedTags, PublicKey wrappedTo) { + GarlicConfig config = buildGarlicConfig(data, status, replyPeer, replyTunnel, target); + + PublicKey rcptKey = config.getRecipientPublicKey(); + if (rcptKey == null) { + if (config.getRecipient() == null) { + throw new IllegalArgumentException("Null recipient specified"); + } else if (config.getRecipient().getIdentity() == null) { + throw new IllegalArgumentException("Null recipient.identity specified"); + } else if (config.getRecipient().getIdentity().getPublicKey() == null) { + throw new IllegalArgumentException("Null recipient.identity.publicKey specified"); + } else + rcptKey = config.getRecipient().getIdentity().getPublicKey(); + } + + if (wrappedTo != null) + wrappedTo.setData(rcptKey.getData()); + + long start = Clock.getInstance().now(); + GarlicMessage message = GarlicMessageBuilder.buildMessage(config, wrappedKey, wrappedTags); + long end = Clock.getInstance().now(); + if ( (end - start) > 1000) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Took more than a second (" + (end-start) + "ms) to create the garlic for the tunnel"); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Took LESS than a second (" + (end-start) + "ms) to create the garlic for the tunnel!"); + } + return message; + } + + private GarlicConfig buildGarlicConfig(I2NPMessage data, I2NPMessage status, RouterInfo replyPeer, TunnelGateway replyTunnel, RouterInfo target) { + GarlicConfig config = new GarlicConfig(); + + PayloadGarlicConfig dataClove = buildDataClove(data, target, _expiration); + config.addClove(dataClove); + PayloadGarlicConfig ackClove = buildAckClove(status, replyPeer, replyTunnel, _expiration); + config.addClove(ackClove); + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_ROUTER); + instructions.setDelayRequested(false); + instructions.setDelaySeconds(0); + instructions.setEncrypted(false); + instructions.setEncryptionKey(null); + instructions.setRouter(target.getIdentity().getHash()); + instructions.setTunnelId(null); + + _log.info("Setting the expiration on the garlic config to " + (new Date(_expiration))); + + config.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + config.setDeliveryInstructions(instructions); + config.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + config.setExpiration(_expiration); + config.setRecipientPublicKey(target.getIdentity().getPublicKey()); + config.setRequestAck(false); + + return config; + } + + /** + * Build a clove that sends a DeliveryStatusMessage to us + */ + private PayloadGarlicConfig buildAckClove(I2NPMessage ackMsg, RouterInfo replyPeer, TunnelGateway replyTunnel, long expiration) { + PayloadGarlicConfig ackClove = new PayloadGarlicConfig(); + + Hash replyToTunnelRouter = replyTunnel.getGateway(); // inbound tunnel gateway + TunnelId replyToTunnelId = replyTunnel.getTunnelId(); // tunnel id on that gateway + + DeliveryInstructions ackInstructions = new DeliveryInstructions(); + ackInstructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_TUNNEL); + ackInstructions.setRouter(replyToTunnelRouter); + ackInstructions.setTunnelId(replyToTunnelId); + ackInstructions.setDelayRequested(false); + ackInstructions.setDelaySeconds(0); + ackInstructions.setEncrypted(false); + + ackClove.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + ackClove.setDeliveryInstructions(ackInstructions); + ackClove.setExpiration(expiration); + ackClove.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + ackClove.setPayload(ackMsg); + ackClove.setRecipient(replyPeer); + ackClove.setRequestAck(false); + + return ackClove; + } + + /** + * Build a clove that sends the data to the target (which is local) + */ + private PayloadGarlicConfig buildDataClove(I2NPMessage data, RouterInfo target, long expiration) { + PayloadGarlicConfig clove = new PayloadGarlicConfig(); + + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_LOCAL); + instructions.setRouter(target.getIdentity().getHash()); + instructions.setTunnelId(null); + instructions.setDelayRequested(false); + instructions.setDelaySeconds(0); + instructions.setEncrypted(false); + + clove.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + clove.setDeliveryInstructions(instructions); + clove.setExpiration(expiration); + clove.setId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + clove.setPayload(data); + clove.setRecipientPublicKey(null); + clove.setRequestAck(false); + + return clove; + } + + private void fail() { + if (_complete) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Build tunnel failed via " + _tunnelGateway.getThisHop().toBase64() + ", but we've already completed, so fuck off: " + _tunnelGateway, new Exception("Fail aborted")); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Build tunnel " + _tunnelGateway.getTunnelId().getTunnelId() + " with gateway " + _tunnelGateway.getThisHop().toBase64() + " FAILED: " + _failedTunnelParticipants + " - " + _tunnelGateway, new Exception("Why did we fail building?")); + synchronized (_toBeRequested) { + _toBeRequested.clear(); + } + synchronized (_failedTunnelParticipants) { + _failedTunnelParticipants.clear(); + } + _complete = true; + } + } + private void peerSuccess(TunnelInfo peer) { + int numLeft = 0; + synchronized (_toBeRequested) { + numLeft = _toBeRequested.size(); + } + if (numLeft <= 0) { + if (_log.shouldLog(Log.INFO)) + _log.info("Peer (" + peer.getThisHop().toBase64() + ") successful: mark the tunnel as completely ready [inbound? " + _isInbound + "]"); + _complete = true; + if (_isInbound) + _pool.addFreeTunnel(_tunnelGateway); + else + _pool.addOutboundTunnel(_tunnelGateway); + _tunnelGateway.setIsReady(true); + StatManager.getInstance().updateFrequency("tunnel.buildFrequency"); + } else { + if (_log.shouldLog(Log.DEBUG)) { + StringBuffer buf = new StringBuffer(128); + buf.append("Hop to ").append(peer.getThisHop().toBase64()).append(" successful for tunnel ").append(peer.getTunnelId().getTunnelId()); + buf.append(", but ").append(numLeft).append(" are pending"); + _log.debug(buf.toString()); + } + JobQueue.getInstance().addJob(this); + } + } + + + public void dropped() { + _pool.buildFakeTunnels(); + if (_log.shouldLog(Log.WARN)) + _log.warn("Dropping request to create a new tunnel, so we may have manually created a new fake inbound and a new fake outbound, just in case we needed that..."); + } + + + private class Success extends JobImpl implements ReplyJob { + private TunnelInfo _tunnel; + private List _messages; + private boolean _successCompleted; + private SessionKey _wrappedKey; + private Set _wrappedTags; + private PublicKey _wrappedTo; + private long _started; + + public Success(TunnelInfo tunnel, SessionKey wrappedKey, Set wrappedTags, PublicKey wrappedTo) { + _tunnel = tunnel; + _messages = new LinkedList(); + _successCompleted = false; + _wrappedKey = wrappedKey; + _wrappedTags = wrappedTags; + _wrappedTo = wrappedTo; + _started = Clock.getInstance().now(); + } + + public String getName() { return "Create Tunnel Status Received"; } + public void runJob() { + List toProc = null; + synchronized (_messages) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("# messages received for successs: " + _messages.size()); + toProc = new ArrayList(_messages); + _messages.clear(); + } + + long responseTime = Clock.getInstance().now() - _started; + for (Iterator iter = toProc.iterator(); iter.hasNext(); ) { + I2NPMessage msg = (I2NPMessage)iter.next(); + process(msg, responseTime); + } + } + + private void process(I2NPMessage message, long responseTime) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Running success status job (tunnel = " + _tunnel + " msg = " + message + ")"); + if (message.getType() == DeliveryStatusMessage.MESSAGE_TYPE) { + if (_log.shouldLog(Log.INFO)) + _log.info("Tunnel creation message acknowledged for tunnel " + _tunnel.getTunnelId() + " at router " + _tunnel.getThisHop().toBase64()); + } else { + TunnelCreateStatusMessage msg = (TunnelCreateStatusMessage)message; + if (_successCompleted) { + _log.info("Already completed in the Success task [skipping " + msg.getStatus() + "]"); + return; + } + switch (msg.getStatus()) { + case TunnelCreateStatusMessage.STATUS_FAILED_CERTIFICATE: + case TunnelCreateStatusMessage.STATUS_FAILED_DELETED: + case TunnelCreateStatusMessage.STATUS_FAILED_DUPLICATE_ID: + case TunnelCreateStatusMessage.STATUS_FAILED_OVERLOADED: + if (_log.shouldLog(Log.WARN)) + _log.warn("Tunnel creation failed for tunnel " + _tunnel.getTunnelId() + " at router " + _tunnel.getThisHop().toBase64() + " with status " + msg.getStatus()); + ProfileManager.getInstance().tunnelRejected(_tunnel.getThisHop(), responseTime); + MessageHistory.getInstance().tunnelRejected(_tunnel.getThisHop(), _tunnel.getTunnelId(), null, "refused"); + fail(); + _successCompleted = true; + break; + case TunnelCreateStatusMessage.STATUS_SUCCESS: + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Tunnel creation succeeded for tunnel " + _tunnel.getTunnelId() + " at router " + _tunnel.getThisHop().toBase64()); + + if ( (_wrappedKey != null) && (_wrappedKey.getData() != null) && (_wrappedTags != null) && (_wrappedTags.size() > 0) && (_wrappedTo != null) ) { + SessionKeyManager.getInstance().tagsDelivered(_wrappedTo, _wrappedKey, _wrappedTags); + if (_log.shouldLog(Log.INFO)) + _log.info("Delivered tags successfully to " + _tunnel.getThisHop().toBase64() + "! # tags: " + _wrappedTags.size()); + } + + _tunnel.setIsReady(true); + ProfileManager.getInstance().tunnelJoined(_tunnel.getThisHop(), responseTime); + peerSuccess(_tunnel); + _successCompleted = true; + break; + } + } + } + + public void setMessage(I2NPMessage message) { + synchronized (_messages) { + _messages.add(message); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Reply message " + _messages.size() + " received " + message.getClass().getName(), new Exception("Received from")); + } + } + } + + private class Failure extends JobImpl { + private TunnelInfo _tunnel; + private Hash _replyThrough; + private long _started; + public Failure(TunnelInfo tunnel, Hash replyThrough) { + _tunnel = tunnel; + _replyThrough = replyThrough; + _started = Clock.getInstance().now(); + } + + public String getName() { return "Create Tunnel Failed"; } + public void runJob() { + // update the tunnel so its known to be not working + if (_log.shouldLog(Log.WARN)) + _log.warn("Tunnel creation timed out for tunnel " + _tunnel.getTunnelId() + " at router " + _tunnel.getThisHop().toBase64() + " with expiration " + new Date(_expiration)); + synchronized (_failedTunnelParticipants) { + _failedTunnelParticipants.add(_tunnel.getThisHop()); + _failedTunnelParticipants.add(_replyThrough); + } + MessageHistory.getInstance().tunnelRequestTimedOut(_tunnel.getThisHop(), _tunnel.getTunnelId(), _replyThrough); + // perhaps not an explicit reject, but an implicit one (due to overload & dropped messages, etc) + ProfileManager.getInstance().tunnelRejected(_tunnel.getThisHop(), Clock.getInstance().now() - _started); + ProfileManager.getInstance().messageFailed(_tunnel.getThisHop()); + StatManager.getInstance().updateFrequency("tunnel.buildFailFrequency"); + fail(); + } + } + + private class Selector implements MessageSelector { + private TunnelInfo _tunnel; + private long _ackId; + private boolean _statusFound; + private boolean _ackFound; + + public Selector(TunnelInfo tunnel, long ackId) { + _tunnel = tunnel; + _ackId = ackId; + _statusFound = false; + _ackFound = false; + } + + public boolean continueMatching() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("ContinueMatching looking for tunnel " + _tunnel.getTunnelId().getTunnelId() + " from " + _tunnel.getThisHop().toBase64() + ": found? " + _statusFound + " ackFound? " + _ackFound); + return !_statusFound || !_ackFound; + //return !_statusFound; // who cares about the ack if we get the status OK? + } + public long getExpiration() { return _expiration; } + public boolean isMatch(I2NPMessage message) { + if (message.getType() == TunnelCreateStatusMessage.MESSAGE_TYPE) { + TunnelCreateStatusMessage msg = (TunnelCreateStatusMessage)message; + if (_tunnel.getThisHop().equals(msg.getFromHash())) { + if (_tunnel.getTunnelId().equals(msg.getTunnelId())) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Matches the tunnel create status message"); + _statusFound = true; + return true; + } else { + // hmm another tunnel through the peer... + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Status message from peer [" + msg.getFromHash().toBase64() + "], with wrong tunnelId [" + msg.getTunnelId() + "] not [" + _tunnel.getTunnelId().getTunnelId() + "]"); + return false; + } + } else { + // status message but from the wrong peer + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Status message from the wrong peer [" + msg.getFromHash().toBase64() + "], not [" + _tunnel.getThisHop().toBase64() + "]"); + return false; + } + } else if (message.getType() == DeliveryStatusMessage.MESSAGE_TYPE) { + if (((DeliveryStatusMessage)message).getMessageId() == _ackId) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Matches the ping message tied to the tunnel create status message"); + _ackFound = true; + return true; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Message is a delivery status message, but with the wrong id"); + return false; + } + } else { + //_log.debug("Message " + message.getClass().getName() + " is not a delivery status or tunnel create status message [waiting for ok for tunnel " + _tunnel.getTunnelId() + " so we can fire " + _onCreated + "]"); + return false; + } + } + + public String toString() { return "Build Tunnel Job Selector for tunnel " + _tunnel.getTunnelId().getTunnelId() + " at " + _tunnel.getThisHop().toBase64() + " [found=" + _statusFound + ", ack=" + _ackFound + "] (@" + (new Date(getExpiration())) + ")"; } + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/TestTunnelJob.java b/router/java/src/net/i2p/router/tunnelmanager/TestTunnelJob.java new file mode 100644 index 0000000000..47457cc323 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/TestTunnelJob.java @@ -0,0 +1,250 @@ +package net.i2p.router.tunnelmanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Date; +import java.util.List; + +import net.i2p.data.DataFormatException; +import net.i2p.data.Hash; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DeliveryStatusMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.TunnelMessage; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.MessageSelector; +import net.i2p.router.ReplyJob; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelManagerFacade; +import net.i2p.router.TunnelSelectionCriteria; +import net.i2p.router.MessageHistory; +import net.i2p.router.message.SendMessageDirectJob; +import net.i2p.router.message.SendTunnelMessageJob; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +class TestTunnelJob extends JobImpl { + private final static Log _log = new Log(TestTunnelJob.class); + private TunnelId _id; + private TunnelPool _pool; + private long _nonce; + + public TestTunnelJob(TunnelId id, TunnelPool pool) { + super(); + _id = id; + _pool = pool; + _nonce = RandomSource.getInstance().nextInt(Integer.MAX_VALUE); + } + public String getName() { return "Test Tunnel"; } + public void runJob() { + if (_log.shouldLog(Log.INFO)) + _log.info("Testing tunnel " + _id.getTunnelId()); + TunnelInfo info = _pool.getTunnelInfo(_id); + if (info == null) { + _log.error("wtf, why are we testing a tunnel that we do not know about? [" + _id.getTunnelId() + "]", getAddedBy()); + return; + } + if (isOutbound(info)) { + testOutbound(info); + } else { + testInbound(info); + } + } + + private boolean isOutbound(TunnelInfo info) { + if (info == null) { + _log.error("wtf, null info?", new Exception("Who checked a null tunnel info?")); + return false; + } + if (Router.getInstance().getRouterInfo().getIdentity().getHash().equals(info.getThisHop())) + return true; + else + return false; + } + + private final static long TEST_TIMEOUT = 60*1000; // 60 seconds for a test to succeed + private final static int TEST_PRIORITY = 100; + + /** + * Send a message out the tunnel with instructions to send the message back + * to ourselves and wait for it to arrive. + */ + private void testOutbound(TunnelInfo info) { + if (_log.shouldLog(Log.INFO)) + _log.info("Testing outbound tunnel " + info); + DeliveryStatusMessage msg = new DeliveryStatusMessage(); + msg.setArrival(new Date(Clock.getInstance().now())); + msg.setMessageId(_nonce); + Hash us = Router.getInstance().getRouterInfo().getIdentity().getHash(); + TunnelId inboundTunnelId = getReplyTunnel(); + if (inboundTunnelId == null) { + return; + } + + TestFailedJob failureJob = new TestFailedJob(); + MessageSelector selector = new TestMessageSelector(msg.getMessageId(), info.getTunnelId().getTunnelId()); + SendTunnelMessageJob testJob = new SendTunnelMessageJob(msg, info.getTunnelId(), us, inboundTunnelId, null, new TestSuccessfulJob(), failureJob, selector, TEST_TIMEOUT, TEST_PRIORITY); + JobQueue.getInstance().addJob(testJob); + } + + /** + * Get the tunnel for replies to be sent down when testing outbound tunnels + * + */ + private TunnelId getReplyTunnel() { + TunnelSelectionCriteria crit = new TunnelSelectionCriteria(); + crit.setMinimumTunnelsRequired(2); + crit.setMaximumTunnelsRequired(2); + // arbitrary priorities + crit.setAnonymityPriority(50); + crit.setLatencyPriority(50); + crit.setReliabilityPriority(50); + List tunnelIds = TunnelManagerFacade.getInstance().selectInboundTunnelIds(crit); + + for (int i = 0; i < tunnelIds.size(); i++) { + TunnelId id = (TunnelId)tunnelIds.get(i); + if (id.equals(_id)) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Not testing a tunnel with itself [duh]"); + } else { + return id; + } + } + + _log.error("Unable to test tunnel " + _id + ", since there are NO OTHER INBOUND TUNNELS to receive the ack through"); + return null; + } + + /** + * Send a message to the gateway and wait for it to arrive. + * todo: send the message to the gateway via an outbound tunnel or garlic, NOT DIRECT. + */ + private void testInbound(TunnelInfo info) { + if (_log.shouldLog(Log.INFO)) + _log.info("Testing inbound tunnel " + info); + DeliveryStatusMessage msg = new DeliveryStatusMessage(); + msg.setArrival(new Date(Clock.getInstance().now())); + msg.setMessageId(_nonce); + TestFailedJob failureJob = new TestFailedJob(); + MessageSelector selector = new TestMessageSelector(msg.getMessageId(), info.getTunnelId().getTunnelId()); + TunnelMessage tmsg = new TunnelMessage(); + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + msg.writeBytes(baos); + tmsg.setData(baos.toByteArray()); + tmsg.setTunnelId(info.getTunnelId()); + JobQueue.getInstance().addJob(new SendMessageDirectJob(tmsg, info.getThisHop(), new TestSuccessfulJob(), failureJob, selector, Clock.getInstance().now() + TEST_TIMEOUT, TEST_PRIORITY)); + + String bodyType = msg.getClass().getName(); + MessageHistory.getInstance().wrap(bodyType, msg.getUniqueId(), TunnelMessage.class.getName(), tmsg.getUniqueId()); + } catch (IOException ioe) { + _log.error("Error writing out the tunnel message to send to the tunnel", ioe); + _pool.tunnelFailed(_id); + } catch (DataFormatException dfe) { + _log.error("Error writing out the tunnel message to send to the tunnel", dfe); + _pool.tunnelFailed(_id); + } + } + + private class TestFailedJob extends JobImpl { + public TestFailedJob() { + super(); + } + + public String getName() { return "Tunnel Test Failed"; } + public void runJob() { + if (_log.shouldLog(Log.WARN)) + _log.warn("Test of tunnel " + _id.getTunnelId() + " failed while waiting for nonce " + _nonce, getAddedBy()); + _pool.tunnelFailed(_id); + } + } + + private class TestSuccessfulJob extends JobImpl implements ReplyJob { + private DeliveryStatusMessage _msg; + public TestSuccessfulJob() { + super(); + _msg = null; + } + + public String getName() { return "Tunnel Test Successful"; } + public void runJob() { + long time = (Clock.getInstance().now() - _msg.getArrival().getTime()); + if (_log.shouldLog(Log.INFO)) + _log.info("Test of tunnel " + _id+ " successfull after " + time + "ms waiting for " + _nonce); + TunnelInfo info = _pool.getTunnelInfo(_id); + if (info != null) + MessageHistory.getInstance().tunnelValid(info, time); + } + + public void setMessage(I2NPMessage message) { + _msg = (DeliveryStatusMessage)message; + } + } + + private static class TestMessageSelector implements MessageSelector { + private long _id; + private long _tunnelId; + private boolean _found; + private long _expiration; + public TestMessageSelector(long id, long tunnelId) { + _id = id; + _tunnelId = tunnelId; + _found = false; + _expiration = Clock.getInstance().now() + TEST_TIMEOUT; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("the expiration while testing tunnel " + tunnelId + " waiting for nonce " + id + ": " + new Date(_expiration)); + } + public boolean continueMatching() { + if (!_found) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Continue matching while looking for nonce for tunnel " + _tunnelId); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Don't continue matching for tunnel " + _tunnelId + " / " + _id); + } + return !_found; + } + public long getExpiration() { + if (_expiration < Clock.getInstance().now()) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("EXPIRED while looking for nonce " + _id + " for tunnel " + _tunnelId); + } + return _expiration; + } + public boolean isMatch(I2NPMessage message) { + if ( (message != null) && (message instanceof DeliveryStatusMessage) ) { + DeliveryStatusMessage msg = (DeliveryStatusMessage)message; + if (msg.getMessageId() == _id) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Found successful test of tunnel " + _tunnelId + " after " + (Clock.getInstance().now() - msg.getArrival().getTime()) + "ms waiting for " + _id); + _found = true; + return true; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Found a delivery status message, but it contains nonce " + msg.getMessageId() + " and not " + _id); + } + } else { + //_log.debug("Not a match while looking to test tunnel " + _tunnelId + " with nonce " + _id + " (" + message + ")"); + } + return false; + } + public String toString() { + StringBuffer buf = new StringBuffer(256); + buf.append(super.toString()); + buf.append(": TestMessageSelector: tunnel ").append(_tunnelId).append(" looking for ").append(_id).append(" expiring on "); + buf.append(new Date(_expiration)); + return buf.toString(); + } + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/TunnelBuilder.java b/router/java/src/net/i2p/router/tunnelmanager/TunnelBuilder.java new file mode 100644 index 0000000000..330d19473f --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/TunnelBuilder.java @@ -0,0 +1,388 @@ +package net.i2p.router.tunnelmanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import net.i2p.crypto.KeyGenerator; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.data.TunnelId; +import net.i2p.router.ClientTunnelSettings; +import net.i2p.router.NetworkDatabaseFacade; +import net.i2p.router.PeerManagerFacade; +import net.i2p.router.PeerSelectionCriteria; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.TunnelSettings; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +class TunnelBuilder { + private final static Log _log = new Log(TunnelBuilder.class); + private final static TunnelBuilder _instance = new TunnelBuilder(); + public final static TunnelBuilder getInstance() { return _instance; } + + private final static long DEFAULT_TUNNEL_DURATION = 10*60*1000; // 10 minutes + /** + * Chance that the tunnel build will be 0 hop, on a PROBABILITY_LOCAL_SCALE + */ + private final static int PROBABILITY_LOCAL = -1; + private final static int PROBABILITY_LOCAL_SCALE = 10; + + public TunnelInfo configureInboundTunnel(Destination dest, ClientTunnelSettings settings) { + return configureInboundTunnel(dest, settings, false); + } + public TunnelInfo configureInboundTunnel(Destination dest, ClientTunnelSettings settings, boolean useFake) { + boolean randFake = (RandomSource.getInstance().nextInt(PROBABILITY_LOCAL_SCALE) <= PROBABILITY_LOCAL); + List peerLists = null; + if (useFake || randFake) { + peerLists = new ArrayList(0); + } else { + List peerHashes = selectInboundPeers(1, settings.getDepthInbound()); + peerLists = randomizeLists(peerHashes, 1, settings.getDepthInbound()); + } + if (peerLists.size() <= 0) { + _log.info("Configuring local inbound tunnel"); + return configureInboundTunnel(dest, settings, new ArrayList()); + } else { + List peerHashList = (List)peerLists.get(0); + return configureInboundTunnel(dest, settings, peerHashList); + } + } + + public TunnelInfo configureOutboundTunnel(ClientTunnelSettings settings) { + return configureOutboundTunnel(settings, false); + } + public TunnelInfo configureOutboundTunnel(ClientTunnelSettings settings, boolean useFake) { + boolean randFake = (RandomSource.getInstance().nextInt(PROBABILITY_LOCAL_SCALE) <= PROBABILITY_LOCAL); + List peerLists = null; + if (useFake || randFake) { + peerLists = new ArrayList(0); + } else { + List peerHashes = selectOutboundPeers(1, settings.getDepthOutbound()); + peerLists = randomizeLists(peerHashes, 1, settings.getDepthOutbound()); + } + if (peerLists.size() <= 0) { + _log.info("Configuring local outbound tunnel"); + return configureOutboundTunnel(settings, new ArrayList()); + } else { + List peerHashList = (List)peerLists.get(0); + return configureOutboundTunnel(settings, peerHashList); + } + } + + /** + * Select a series of participants for the inbound tunnel, define each of + * their operating characteristics, and return them as a chain of TunnelInfo + * structures. The first TunnelInfo in each chain is the inbound gateway + * to which the lease should be attached, and the last is the local router. + * + * @return set of TunnelInfo structures, where each value is the gateway of + * a different tunnel (and these TunnelInfo structures are chained + * via getNextHopInfo()) + */ + public Set configureInboundTunnels(Destination dest, ClientTunnelSettings settings) { + return configureInboundTunnels(dest, settings, false); + } + /** + * @param useFake if true, make this tunnel include no remote peers (so it'll always succeed) + * + */ + public Set configureInboundTunnels(Destination dest, ClientTunnelSettings settings, boolean useFake) { + Set tunnels = new HashSet(); + int numIn = settings.getNumInboundTunnels(); + if (numIn <= 0) { + _log.info("No inbound tunnels requested, but we're creating one anyway"); + numIn = 1; + } + List peerLists = null; + if (!useFake) { + List peerHashes = selectInboundPeers(numIn, settings.getDepthInbound()); + _log.debug("Peer hashes selected: " + peerHashes.size()); + peerLists = randomizeLists(peerHashes, settings.getNumInboundTunnels(), settings.getDepthInbound()); + } else { + peerLists = new ArrayList(0); + } + if (peerLists.size() <= 0) { + for (int i = 0; i < numIn; i++) { + TunnelInfo tunnel = configureInboundTunnel(dest, settings, new ArrayList()); + tunnels.add(tunnel); + _log.info("Dummy inbound tunnel " + tunnel.getTunnelId() + " configured (" + tunnel + ")"); + } + } else { + for (Iterator iter = peerLists.iterator(); iter.hasNext();) { + List peerList = (List)iter.next(); + TunnelInfo tunnel = configureInboundTunnel(dest, settings, peerList); + tunnels.add(tunnel); + _log.info("Real inbound tunnel " + tunnel.getTunnelId() + " configured (" + tunnel + ")"); + } + } + + return tunnels; + } + + public Set configureOutboundTunnels(ClientTunnelSettings settings) { + return configureOutboundTunnels(settings, false); + } + /** + * @param useFake if true, make this tunnel include no remote peers (so it'll always succeed) + * + */ + public Set configureOutboundTunnels(ClientTunnelSettings settings, boolean useFake) { + Set tunnels = new HashSet(); + + List peerLists = null; + if (!useFake) { + List peerHashes = selectOutboundPeers(settings.getNumOutboundTunnels(), settings.getDepthOutbound()); + _log.debug("Peer hashes selected: " + peerHashes.size()); + peerLists = randomizeLists(peerHashes, settings.getNumOutboundTunnels(), settings.getDepthOutbound()); + } else { + peerLists = new ArrayList(0); + } + if (peerLists.size() <= 0) { + for (int i = 0; i < settings.getNumOutboundTunnels(); i++) { + TunnelInfo tunnel = configureOutboundTunnel(settings, new ArrayList()); + tunnels.add(tunnel); + _log.info("Dummy outbound tunnel " + tunnel.getTunnelId() + " configured (" + tunnel + ")"); + } + } else { + for (Iterator iter = peerLists.iterator(); iter.hasNext();) { + List peerList = (List)iter.next(); + TunnelInfo tunnel = configureOutboundTunnel(settings, peerList); + tunnels.add(tunnel); + _log.info("Real outbound tunnel " + tunnel.getTunnelId() + " configured (" + tunnel + ")"); + } + } + return tunnels; + } + + private List selectInboundPeers(int numTunnels, int numPerTunnel) { + return selectPeers(numTunnels, numPerTunnel); + } + + private List selectOutboundPeers(int numTunnels, int numPerTunnel) { + return selectPeers(numTunnels, numPerTunnel); + } + + /** + * Retrieve a list of Hash structures (from RouterIdentity) for routers that + * should be used for the tunnels. A sufficient number should be retrieved so + * that there are enough for the specified numTunnels where each tunnel has numPerTunnel + * hops in it. + * + */ + private List selectPeers(int numTunnels, int numPerTunnel) { + PeerSelectionCriteria criteria = new PeerSelectionCriteria(); + int maxNeeded = numTunnels * numPerTunnel; + int minNeeded = numPerTunnel; + criteria.setMaximumRequired(maxNeeded); + criteria.setMinimumRequired(minNeeded); + criteria.setPurpose(PeerSelectionCriteria.PURPOSE_TUNNEL); + + List peers = PeerManagerFacade.getInstance().selectPeers(criteria); + List rv = new ArrayList(peers.size()); + for (Iterator iter = peers.iterator(); iter.hasNext(); ) { + Hash peer = (Hash)iter.next(); + if (null != NetworkDatabaseFacade.getInstance().lookupRouterInfoLocally(peer)) + rv.add(peer); + else { + _log.warn("peer manager selected a peer we don't know about - drop it"); + } + } + return rv; + } + + /** + * Take the router hashes and organize them into numTunnels lists where each + * list contains numPerTunnel hashes. + * + * @return Set of List of Hash objects, where the each list contains an ordered + * group of routers to participate in the tunnel. Note that these lists + * do NOT include the local router at the end, so numPerTunnel = 0 (and + * hence, an empty list) is a valid (albeit insecure) length + */ + private List randomizeLists(List peerHashes, int numTunnels, int numPerTunnel) { + List tunnels = new ArrayList(numTunnels); + + if (peerHashes.size() == 0) { + _log.info("No peer hashes provided"); + return tunnels; + } else { + _log.info("# peers randomizing: " + peerHashes + " into " + numTunnels + " tunnels"); + } + + for (int i = 0; i < numTunnels; i++) { + int startOn = RandomSource.getInstance().nextInt(peerHashes.size()); + List peers = new ArrayList(); + for (int j = 0; j < numPerTunnel; j++) { + int k = (j + startOn) % peerHashes.size(); + Hash peer = (Hash)peerHashes.get(k); + if (!peers.contains(peer)) + peers.add(peer); + } + _log.info("Tunnel " + i + " [" + numPerTunnel + "/(" + startOn+ ")]: " + peers); + tunnels.add(peers); + } + + _log.info("Tunnels: " + tunnels); + + return tunnels; + } + + /** + * Create a chain of TunnelInfo structures with the appropriate settings using + * the supplied routers for each hop, as well as a final hop ending with the current + * router + */ + private TunnelInfo configureInboundTunnel(Destination dest, ClientTunnelSettings settings, List peerHashList) { + SessionKey encryptionKey = KeyGenerator.getInstance().generateSessionKey(); + Object kp[] = KeyGenerator.getInstance().generateSigningKeypair(); + SigningPublicKey pubkey = (SigningPublicKey)kp[0]; + SigningPrivateKey privkey = (SigningPrivateKey)kp[1]; + + long duration = settings.getInboundDuration(); + if (duration <= 0) + duration = DEFAULT_TUNNEL_DURATION; + long expiration = Clock.getInstance().now() + duration; + + TunnelSettings tunnelSettings = new TunnelSettings(); + tunnelSettings.setBytesPerMinuteAverage(settings.getBytesPerMinuteInboundAverage()); + tunnelSettings.setBytesPerMinutePeak(settings.getBytesPerMinuteInboundPeak()); + tunnelSettings.setDepth(peerHashList.size()+1); + tunnelSettings.setExpiration(expiration); + tunnelSettings.setIncludeDummy(settings.getIncludeDummyInbound()); + tunnelSettings.setMessagesPerMinuteAverage(settings.getMessagesPerMinuteInboundAverage()); + tunnelSettings.setMessagesPerMinutePeak(settings.getMessagesPerMinuteInboundPeak()); + tunnelSettings.setReorder(settings.getReorderInbound()); + + TunnelId id = new TunnelId(); + id.setTunnelId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + id.setType(TunnelId.TYPE_INBOUND); + + TunnelInfo first = null; + TunnelInfo prev = null; + for (int i = 0; i < peerHashList.size(); i++) { + Hash peer = (Hash)peerHashList.get(i); + TunnelInfo cur = new TunnelInfo(); + cur.setThisHop(peer); + cur.setConfigurationKey(KeyGenerator.getInstance().generateSessionKey()); + cur.setDestination(null); + if (i == 0) { + // gateway + cur.setEncryptionKey(encryptionKey); + cur.setSigningKey(privkey); + } + cur.setSettings(tunnelSettings); + cur.setTunnelId(id); + cur.setVerificationKey(pubkey); + + if (prev != null) { + prev.setNextHop(peer); + prev.setNextHopInfo(cur); + } else { + first = cur; + } + prev = cur; + } + + TunnelInfo last = new TunnelInfo(); + last.setThisHop(Router.getInstance().getRouterInfo().getIdentity().getHash()); + last.setDestination(dest); + last.setEncryptionKey(encryptionKey); + last.setSettings(tunnelSettings); + last.setTunnelId(id); + last.setVerificationKey(pubkey); + last.setSigningKey(privkey); + last.setConfigurationKey(KeyGenerator.getInstance().generateSessionKey()); + + TunnelInfo cur = first; + if (cur == null) { + first = last; + } else { + while (cur.getNextHopInfo() != null) + cur = cur.getNextHopInfo(); + cur.setNextHop(last.getThisHop()); + cur.setNextHopInfo(last); + } + + return first; + } + + + /** + * Create a chain of TunnelInfo structures with the appropriate settings using + * the supplied routers for each hop, starting with the current router + */ + private TunnelInfo configureOutboundTunnel(ClientTunnelSettings settings, List peerHashList) { + SessionKey encryptionKey = KeyGenerator.getInstance().generateSessionKey(); + Object kp[] = KeyGenerator.getInstance().generateSigningKeypair(); + SigningPublicKey pubkey = (SigningPublicKey)kp[0]; + SigningPrivateKey privkey = (SigningPrivateKey)kp[1]; + + long duration = settings.getInboundDuration(); // uses inbound duration for symmetry + if (duration <= 0) + duration = DEFAULT_TUNNEL_DURATION; + long expiration = Clock.getInstance().now() + duration; + + TunnelSettings tunnelSettings = new TunnelSettings(); + tunnelSettings.setBytesPerMinuteAverage(settings.getBytesPerMinuteInboundAverage()); + tunnelSettings.setBytesPerMinutePeak(settings.getBytesPerMinuteInboundPeak()); + tunnelSettings.setDepth(peerHashList.size()+1); + tunnelSettings.setExpiration(expiration); + tunnelSettings.setIncludeDummy(settings.getIncludeDummyInbound()); + tunnelSettings.setMessagesPerMinuteAverage(settings.getMessagesPerMinuteInboundAverage()); + tunnelSettings.setMessagesPerMinutePeak(settings.getMessagesPerMinuteInboundPeak()); + tunnelSettings.setReorder(settings.getReorderInbound()); + + TunnelId id = new TunnelId(); + id.setTunnelId(RandomSource.getInstance().nextInt(Integer.MAX_VALUE)); + id.setType(TunnelId.TYPE_OUTBOUND); + + TunnelInfo first = new TunnelInfo(); + first.setThisHop(Router.getInstance().getRouterInfo().getIdentity().getHash()); + first.setDestination(null); + first.setEncryptionKey(encryptionKey); + first.setSettings(tunnelSettings); + first.setTunnelId(id); + first.setVerificationKey(pubkey); + first.setSigningKey(privkey); + first.setConfigurationKey(KeyGenerator.getInstance().generateSessionKey()); + + TunnelInfo prev = first; + for (int i = 0; i < peerHashList.size(); i++) { + Hash peer = (Hash)peerHashList.get(i); + TunnelInfo cur = new TunnelInfo(); + cur.setThisHop(peer); + cur.setConfigurationKey(KeyGenerator.getInstance().generateSessionKey()); + cur.setDestination(null); + if (i == peerHashList.size() -1) { + // endpoint + cur.setEncryptionKey(encryptionKey); + } + cur.setSettings(tunnelSettings); + cur.setTunnelId(id); + cur.setVerificationKey(pubkey); + + prev.setNextHop(peer); + prev.setNextHopInfo(cur); + prev = cur; + } + + return first; + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/TunnelCreateMessageHandler.java b/router/java/src/net/i2p/router/tunnelmanager/TunnelCreateMessageHandler.java new file mode 100644 index 0000000000..5cc4a2f8c2 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/TunnelCreateMessageHandler.java @@ -0,0 +1,26 @@ +package net.i2p.router.tunnelmanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.router.HandlerJobBuilder; +import net.i2p.router.Job; + +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.SourceRouteBlock; +import net.i2p.data.i2np.TunnelCreateMessage; +import net.i2p.data.RouterIdentity; +import net.i2p.data.Hash; + +class TunnelCreateMessageHandler implements HandlerJobBuilder { + + public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash, SourceRouteBlock replyBlock) { + return new HandleTunnelCreateMessageJob((TunnelCreateMessage)receivedMessage, from, fromHash, replyBlock); + } + +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/TunnelGateway.java b/router/java/src/net/i2p/router/tunnelmanager/TunnelGateway.java new file mode 100644 index 0000000000..d12af96504 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/TunnelGateway.java @@ -0,0 +1,23 @@ +package net.i2p.router.tunnelmanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.TunnelId; +import net.i2p.data.Hash; + +class TunnelGateway { + private TunnelId _tunnel; + private Hash _gateway; + public TunnelGateway(TunnelId id, Hash gateway) { + _tunnel = id; + _gateway = gateway; + } + public TunnelId getTunnelId() { return _tunnel; } + public Hash getGateway() { return _gateway; } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/TunnelPool.java b/router/java/src/net/i2p/router/tunnelmanager/TunnelPool.java new file mode 100644 index 0000000000..ab3313ed95 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/TunnelPool.java @@ -0,0 +1,643 @@ +package net.i2p.router.tunnelmanager; + +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.TunnelId; +import net.i2p.router.ClientTunnelSettings; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.ProfileManager; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.MessageHistory; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import net.i2p.stat.StatManager; + +/** + * Store the data for free inbound, outbound, and client pooled tunnels, and serve + * as the central coordination point + * + */ +class TunnelPool { + private final static Log _log = new Log(TunnelPool.class); + /** TunnelId --> TunnelInfo of outbound tunnels */ + private Map _outboundTunnels; + /** TunnelId --> TunnelInfo of free inbound tunnels */ + private Map _freeInboundTunnels; + /** Destination --> ClientTunnelPool */ + private Map _clientPools; + /** TunnelId --> TunnelInfo structures of non-local tunnels we're participating in */ + private Map _participatingTunnels; + /** TunnelId --> TunnelInfo of tunnels being built (but not ready yet) */ + private Map _pendingTunnels; + /** defines pool settings: # inbound / outbound, length, etc */ + private ClientTunnelSettings _poolSettings; + private TunnelPoolPersistenceHelper _persistenceHelper; + /** how long will each tunnel create take? */ + private long _tunnelCreationTimeout; + /** how many clients should we stock the pool in support of */ + private int _targetClients; + /** active or has it been shutdown? */ + private boolean _isLive; + + /** write out the current state every 15 seconds */ + private final static long WRITE_POOL_DELAY = 15*1000; + + /** allow the tunnel create timeout to be overridden, default is 60 seconds [but really slow computers should be larger] */ + public final static String TUNNEL_CREATION_TIMEOUT_PARAM = "tunnel.creationTimeoutMs"; + public final static long TUNNEL_CREATION_TIMEOUT_DEFAULT = 60*1000; + + public final static String TARGET_CLIENTS_PARAM = "router.targetClients"; + public final static int TARGET_CLIENTS_DEFAULT = 3; + + static { + StatManager.getInstance().createFrequencyStat("tunnel.failFrequency", "How often do tunnels prematurely fail (after being successfully built)?", "Tunnels", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + StatManager.getInstance().createRateStat("tunnel.failAfterTime", "How long do tunnels that fail prematurely last before failing?", "Tunnels", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + } + + public TunnelPool() { + _isLive = true; + _persistenceHelper = new TunnelPoolPersistenceHelper(); + } + + /** + * If the tunnel is known in any way, fetch it, else return null + * + */ + public TunnelInfo getTunnelInfo(TunnelId id) { + if (!_isLive) return null; + if (id == null) return null; + boolean typeKnown = id.getType() != TunnelId.TYPE_UNSPECIFIED; + + if ( (!typeKnown) || (id.getType() == TunnelId.TYPE_PARTICIPANT) ) { + synchronized (_participatingTunnels) { + if (_participatingTunnels.containsKey(id)) + return (TunnelInfo)_participatingTunnels.get(id); + } + } + if ( (!typeKnown) || (id.getType() == TunnelId.TYPE_OUTBOUND) ) { + synchronized (_outboundTunnels) { + if (_outboundTunnels.containsKey(id)) + return (TunnelInfo)_outboundTunnels.get(id); + } + } + if ( (!typeKnown) || (id.getType() == TunnelId.TYPE_INBOUND) ) { + synchronized (_freeInboundTunnels) { + if (_freeInboundTunnels.containsKey(id)) + return (TunnelInfo)_freeInboundTunnels.get(id); + } + } + synchronized (_pendingTunnels) { + if (_pendingTunnels.containsKey(id)) + return (TunnelInfo)_pendingTunnels.get(id); + } + + if ( (!typeKnown) || (id.getType() == TunnelId.TYPE_INBOUND) ) { + synchronized (_clientPools) { + for (Iterator iter = _clientPools.values().iterator(); iter.hasNext(); ) { + ClientTunnelPool pool = (ClientTunnelPool)iter.next(); + if (pool.isInboundTunnel(id)) + return pool.getInboundTunnel(id); + else if (pool.isInactiveInboundTunnel(id)) + return pool.getInactiveInboundTunnel(id); + } + } + } + return null; + } + + /** + * Get the tunnelId of all tunnels we are managing (not ones we are merely + * participating in) + * + */ + public Set getManagedTunnelIds() { + if (!_isLive) return null; + Set ids = new HashSet(64); + synchronized (_outboundTunnels) { + ids.addAll(_outboundTunnels.keySet()); + } + synchronized (_freeInboundTunnels) { + ids.addAll(_freeInboundTunnels.keySet()); + } + synchronized (_clientPools) { + for (Iterator iter = _clientPools.values().iterator(); iter.hasNext(); ) { + ClientTunnelPool pool = (ClientTunnelPool)iter.next(); + ids.addAll(pool.getInboundTunnelIds()); + } + } + return ids; + } + + /** + * Allocate a free tunnel for use by the destination + * + * @return true if the tunnel was allocated successfully, false if an error occurred + */ + public boolean allocateTunnel(TunnelId id, Destination dest) { + if (!_isLive) return false; + ClientTunnelPool pool = getClientPool(dest); + if (pool == null) { + _log.error("Error allocating tunnel " + id + " to " + dest + ": no pool for the client known"); + return false; + } + TunnelInfo tunnel = removeFreeTunnel(id); + if (tunnel == null) { + _log.error("Error allocating tunnel " + id + " to " + dest + ": tunnel is no longer free?"); + return false; + } + + TunnelInfo t = tunnel; + while (t != null) { + t.setDestination(dest); + t = t.getNextHopInfo(); + } + + pool.addInboundTunnel(tunnel); + return true; + } + + /** + * Set of tunnelIds for outbound tunnels + */ + public Set getOutboundTunnels() { + if (!_isLive) return null; + synchronized (_outboundTunnels) { + return new HashSet(_outboundTunnels.keySet()); + } + } + public int getOutboundTunnelCount() { + if (!_isLive) return 0; + synchronized (_outboundTunnels) { + return _outboundTunnels.size(); + } + } + public TunnelInfo getOutboundTunnel(TunnelId id) { + if (!_isLive) return null; + synchronized (_outboundTunnels) { + return (TunnelInfo)_outboundTunnels.get(id); + } + } + public void addOutboundTunnel(TunnelInfo tunnel) { + if (!_isLive) return; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Add outbound tunnel " + tunnel.getTunnelId()); + MessageHistory.getInstance().tunnelJoined("outbound", tunnel); + synchronized (_outboundTunnels) { + _outboundTunnels.put(tunnel.getTunnelId(), tunnel); + } + synchronized (_pendingTunnels) { + _pendingTunnels.remove(tunnel.getTunnelId()); + } + } + public void removeOutboundTunnel(TunnelId id) { + if (!_isLive) return; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Removing outbound tunnel " + id); + int remaining = 0; + synchronized (_outboundTunnels) { + _outboundTunnels.remove(id); + remaining = _outboundTunnels.size(); + } + if (remaining <= 0) { + buildFakeTunnels(); + } + } + + /** + * Set of tunnelIds that this router has available for consumption + */ + public Set getFreeTunnels() { + if (!_isLive) return null; + synchronized (_freeInboundTunnels) { + return new HashSet(_freeInboundTunnels.keySet()); + } + } + public int getFreeTunnelCount() { + if (!_isLive) return 0; + synchronized (_freeInboundTunnels) { + return _freeInboundTunnels.size(); + } + } + public TunnelInfo getFreeTunnel(TunnelId id) { + if (!_isLive) return null; + synchronized (_freeInboundTunnels) { + return (TunnelInfo)_freeInboundTunnels.get(id); + } + } + public void addFreeTunnel(TunnelInfo tunnel) { + if (!_isLive) return; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Add free inbound tunnel " + tunnel.getTunnelId()); + MessageHistory.getInstance().tunnelJoined("free inbound", tunnel); + synchronized (_freeInboundTunnels) { + _freeInboundTunnels.put(tunnel.getTunnelId(), tunnel); + } + synchronized (_pendingTunnels) { + _pendingTunnels.remove(tunnel.getTunnelId()); + } + } + public TunnelInfo removeFreeTunnel(TunnelId id) { + if (!_isLive) return null; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Removing free inbound tunnel " + id); + int remaining = 0; + TunnelInfo rv = null; + synchronized (_freeInboundTunnels) { + rv = (TunnelInfo)_freeInboundTunnels.remove(id); + remaining = _freeInboundTunnels.size(); + } + if (remaining <= 0) + buildFakeTunnels(); + return rv; + } + + /** + * set of tunnelIds that this router is participating in (but not managing) + */ + public Set getParticipatingTunnels() { + if (!_isLive) return null; + synchronized (_participatingTunnels) { + return new HashSet(_participatingTunnels.keySet()); + } + } + public TunnelInfo getParticipatingTunnel(TunnelId id) { + if (!_isLive) return null; + synchronized (_participatingTunnels) { + return (TunnelInfo)_participatingTunnels.get(id); + } + } + + public boolean addParticipatingTunnel(TunnelInfo tunnel) { + if (!_isLive) return false; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Add participating tunnel " + tunnel.getTunnelId()); + MessageHistory.getInstance().tunnelJoined("participant", tunnel); + synchronized (_participatingTunnels) { + if (_participatingTunnels.containsKey(tunnel.getTunnelId())) { + return false; + } else { + _participatingTunnels.put(tunnel.getTunnelId(), tunnel); + tunnel.setIsReady(true); + return true; + } + } + } + + public TunnelInfo removeParticipatingTunnel(TunnelId id) { + if (!_isLive) return null; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Removing participating tunnel " + id); + synchronized (_participatingTunnels) { + return (TunnelInfo)_participatingTunnels.remove(id); + } + } + + /** + * Set of Destinations for clients currently being managed + * + */ + public Set getClientPools() { + if (!_isLive) return null; + synchronized (_clientPools) { + return new HashSet(_clientPools.keySet()); + } + } + + /** + * Create and start up a client pool for the destination + * + */ + public void createClientPool(Destination dest, ClientTunnelSettings settings) { + if (!_isLive) return; + ClientTunnelPool pool = null; + synchronized (_clientPools) { + if (_clientPools.containsKey(dest)) { + pool = (ClientTunnelPool)_clientPools.get(dest); + if (_log.shouldLog(Log.INFO)) + _log.info("Reusing an existing client tunnel pool for " + dest.calculateHash()); + } else { + pool = new ClientTunnelPool(dest, settings, this); + if (_log.shouldLog(Log.INFO)) + _log.info("New client tunnel pool created for " + dest.calculateHash()); + _clientPools.put(dest, pool); + } + } + pool.startPool(); + } + + ClientTunnelPool addClientPool(ClientTunnelPool pool) { + if (!_isLive) return null; + ClientTunnelPool old = null; + + if (_log.shouldLog(Log.INFO)) + _log.info("Client tunnel pool added for " + pool.getDestination().calculateHash()); + + synchronized (_clientPools) { + old = (ClientTunnelPool)_clientPools.put(pool.getDestination(), pool); + } + return old; + } + public ClientTunnelPool getClientPool(Destination dest) { + if (!_isLive) return null; + synchronized (_clientPools) { + return (ClientTunnelPool)_clientPools.get(dest); + } + } + + public void removeClientPool(Destination dest) { + if (!_isLive) return; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Removing client tunnel pool for " + dest.calculateHash()); + ClientTunnelPool pool = null; + synchronized (_clientPools) { + pool = (ClientTunnelPool)_clientPools.remove(dest); + } + if (pool != null) + pool.stopPool(); + } + + public Set getPendingTunnels() { + if (!_isLive) return null; + synchronized (_pendingTunnels) { + return new HashSet(_pendingTunnels.keySet()); + } + } + public TunnelInfo getPendingTunnel(TunnelId id) { + if (!_isLive) return null; + synchronized (_pendingTunnels) { + return (TunnelInfo)_pendingTunnels.get(id); + } + } + public void addPendingTunnel(TunnelInfo info) { + if (!_isLive) return; + MessageHistory.getInstance().tunnelJoined("pending", info); + synchronized (_pendingTunnels) { + _pendingTunnels.put(info.getTunnelId(), info); + } + } + public void removePendingTunnel(TunnelId id) { + if (!_isLive) return; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Removing pending tunnel " + id); + synchronized (_pendingTunnels) { + _pendingTunnels.remove(id); + } + } + + /** fetch the settings for the pool (tunnel settings and quantities) */ + public ClientTunnelSettings getPoolSettings() { return _poolSettings; } + public void setPoolSettings(ClientTunnelSettings settings) { _poolSettings = settings; } + + /** how many clients the router should expect to handle at once (so it can build sufficient tunnels */ + public int getTargetClients() { return _targetClients; } + public void setTargetClients(int numConcurrentClients) { _targetClients = numConcurrentClients; } + + /** max time for any tunnel creation to take (in milliseconds) */ + public long getTunnelCreationTimeout() { return _tunnelCreationTimeout; } + public void setTunnelCreationTimeout(long timeout) { _tunnelCreationTimeout = timeout; } + + /** determine the number of hops in the longest tunnel we have */ + public int getLongestTunnelLength() { + int max = 0; + synchronized (_freeInboundTunnels) { + for (Iterator iter = _freeInboundTunnels.values().iterator(); iter.hasNext(); ) { + TunnelInfo info = (TunnelInfo)iter.next(); + int len = info.getLength(); + if (len > max) + max = len; + } + } + return max; + } + + /** + * Shit has hit the fan, so lets build a pair of failsafe 0-hop tunnels - one inbound, + * and one outbound. This method blocks until those tunnels are built, and does not + * make use of the JobQueue. + * + */ + public void buildFakeTunnels() { + if (getFreeValidTunnelCount() < 3) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Running low on valid inbound tunnels, building another"); + TunnelInfo inTunnelGateway = TunnelBuilder.getInstance().configureInboundTunnel(null, getPoolSettings(), true); + RequestTunnelJob inReqJob = new RequestTunnelJob(this, inTunnelGateway, true, getTunnelCreationTimeout()); + inReqJob.runJob(); + } + if (getOutboundValidTunnelCount() < 3) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Running low on valid outbound tunnels, building another"); + TunnelInfo outTunnelGateway = TunnelBuilder.getInstance().configureOutboundTunnel(getPoolSettings(), true); + RequestTunnelJob outReqJob = new RequestTunnelJob(this, outTunnelGateway, false, getTunnelCreationTimeout()); + outReqJob.runJob(); + } + } + + private int getFreeValidTunnelCount() { + int found = 0; + Set ids = getFreeTunnels(); + long mustExpireAfter = Clock.getInstance().now(); + + for (Iterator iter = ids.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = getFreeTunnel(id); + if ( (info != null) && (info.getIsReady()) ) { + if (info.getSettings().getExpiration() > mustExpireAfter) { + if (info.getDestination() == null) { + found++; + } + } + } + } + return found; + } + + private int getOutboundValidTunnelCount() { + int found = 0; + Set ids = getOutboundTunnels(); + long mustExpireAfter = Clock.getInstance().now(); + + for (Iterator iter = ids.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = getOutboundTunnel(id); + if ( (info != null) && (info.getIsReady()) ) { + if (info.getSettings().getExpiration() > mustExpireAfter) { + found++; + } + } + } + return found; + } + + public void tunnelFailed(TunnelId id) { + if (!_isLive) return; + if (_log.shouldLog(Log.INFO)) + _log.info("Tunnel " + id + " marked as not ready, since it /failed/", new Exception("Failed tunnel")); + TunnelInfo info = getTunnelInfo(id); + if (info == null) + return; + MessageHistory.getInstance().tunnelFailed(info.getTunnelId()); + info.setIsReady(false); + Hash us = Router.getInstance().getRouterInfo().getIdentity().getHash(); + long lifetime = Clock.getInstance().now() - info.getCreated(); + while (info != null) { + if (!info.getThisHop().equals(us)) { + ProfileManager.getInstance().tunnelFailed(info.getThisHop()); + } + info = info.getNextHopInfo(); + } + StatManager.getInstance().addRateData("tunnel.failAfterTime", lifetime, lifetime); + StatManager.getInstance().updateFrequency("tunnel.failFrequency"); + buildFakeTunnels(); + } + + public void startup() { + if (_log.shouldLog(Log.INFO)) _log.info("Starting up tunnel pool"); + _isLive = true; + _outboundTunnels = new HashMap(8); + _freeInboundTunnels = new HashMap(8); + _clientPools = new HashMap(8); + _participatingTunnels = new HashMap(8); + _pendingTunnels = new HashMap(8); + _poolSettings = createPoolSettings(); + _persistenceHelper.loadPool(this); + _tunnelCreationTimeout = -1; + try { + String str = Router.getInstance().getConfigSetting(TUNNEL_CREATION_TIMEOUT_PARAM); + _tunnelCreationTimeout = Long.parseLong(str); + } catch (Throwable t) { + _tunnelCreationTimeout = TUNNEL_CREATION_TIMEOUT_DEFAULT; + } + _targetClients = TARGET_CLIENTS_DEFAULT; + try { + String str = Router.getInstance().getConfigSetting(TARGET_CLIENTS_PARAM); + _targetClients = Integer.parseInt(str); + } catch (Throwable t) { + _targetClients = TARGET_CLIENTS_DEFAULT; + } + buildFakeTunnels(); + JobQueue.getInstance().addJob(new WritePoolJob()); + JobQueue.getInstance().addJob(new TunnelPoolManagerJob(this)); + JobQueue.getInstance().addJob(new TunnelPoolExpirationJob(this)); + } + + public void shutdown() { + if (_log.shouldLog(Log.INFO)) _log.info("Shutting down tunnel pool"); + _persistenceHelper.writePool(this); + _isLive = false; // the subjobs [should] check getIsLive() on each run + _outboundTunnels = null; + _freeInboundTunnels = null; + _clientPools = null; + _participatingTunnels = null; + _poolSettings = null; + _persistenceHelper = null; + _tunnelCreationTimeout = -1; + } + + public boolean isLive() { return _isLive; } + + private ClientTunnelSettings createPoolSettings() { + ClientTunnelSettings settings = new ClientTunnelSettings(); + settings.readFromProperties(Router.getInstance().getConfigMap()); + return settings; + } + + public String renderStatusHTML() { + if (!_isLive) return ""; + StringBuffer buf = new StringBuffer(); + buf.append("

Tunnel Pool

\n"); + renderTunnels(buf, "Free inbound tunnels", getFreeTunnels()); + renderTunnels(buf, "Outbound tunnels", getOutboundTunnels()); + renderTunnels(buf, "Participating tunnels", getParticipatingTunnels()); + for (Iterator iter = getClientPools().iterator(); iter.hasNext(); ) { + Destination dest = (Destination)iter.next(); + ClientTunnelPool pool = getClientPool(dest); + renderTunnels(buf, "Inbound tunnels for " + dest.calculateHash() + " - (still connected? " + (!pool.isStopped()) + ")", pool.getInboundTunnelIds()); + } + return buf.toString(); + } + + private void renderTunnels(StringBuffer buf, String msg, Set tunnelIds) { + buf.append("").append(msg).append(": (").append(tunnelIds.size()).append(" tunnels)
    \n"); + for (Iterator iter = tunnelIds.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo tunnel = getTunnelInfo(id); + renderTunnel(buf, id, tunnel); + } + buf.append("
\n"); + } + + private final static void renderTunnel(StringBuffer buf, TunnelId id, TunnelInfo tunnel) { + if (tunnel == null) { + buf.append("
  • Tunnel: ").append(id.getTunnelId()).append(" is not known
  • \n"); + } else { + buf.append("
  • Tunnel: ").append(tunnel.getTunnelId()).append("
  • ");
    +	    buf.append("\n\tStyle: ").append(getStyle(id));
    +	    buf.append("\n\tReady? ").append(tunnel.getIsReady());
    +	    buf.append("\n\tDest? ").append(getDestination(tunnel));
    +	    if (tunnel.getSettings() != null)
    +		buf.append("\n\tExpiration: ").append(new Date(tunnel.getSettings().getExpiration()));
    +	    else
    +		buf.append("\n\tExpiration: none");
    +	    
    +	    buf.append("\n\tStart router: ").append(tunnel.getThisHop().toBase64()).append("\n");
    +	    TunnelInfo t = tunnel.getNextHopInfo();
    +	    if (t != null) {
    +		int hop = 1;
    +		while (t != null) {
    +		    buf.append("\tHop ").append(hop).append(": ").append(t.getThisHop().toBase64()).append("\n");
    +		    t = t.getNextHopInfo();
    +		    hop++;
    +		}
    +	    } else {
    +		if (tunnel.getNextHop() != null)
    +		    buf.append("\tNext: ").append(tunnel.getNextHop().toBase64()).append("\n");
    +	    }
    +	    
    +	    buf.append("\n
    "); + } + } + + private final static String getStyle(TunnelId id) { + switch (id.getType()) { + case TunnelId.TYPE_INBOUND: + return "Inbound"; + case TunnelId.TYPE_OUTBOUND: + return "Outbound"; + case TunnelId.TYPE_PARTICIPANT: + return "Participant"; + case TunnelId.TYPE_UNSPECIFIED: + return "Unspecified"; + default: + return "Other! - " + id.getType(); + } + } + + private final static String getDestination(TunnelInfo info) { + while (info != null) { + if (info.getDestination() != null) + return info.getDestination().calculateHash().toString(); + else + info = info.getNextHopInfo(); + } + return "none"; + } + + /** + * This job instructs the troops to invade mars with a spork. + */ + private class WritePoolJob extends JobImpl { + public WritePoolJob() { + getTiming().setStartAfter(Clock.getInstance().now() + WRITE_POOL_DELAY); + } + public String getName() { return "Write Out Tunnel Pool"; } + public void runJob() { + if (!isLive()) + return; + _persistenceHelper.writePool(TunnelPool.this); + requeue(WRITE_POOL_DELAY); + } + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/TunnelPoolExpirationJob.java b/router/java/src/net/i2p/router/tunnelmanager/TunnelPoolExpirationJob.java new file mode 100644 index 0000000000..da10442935 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/TunnelPoolExpirationJob.java @@ -0,0 +1,132 @@ +package net.i2p.router.tunnelmanager; + +import java.util.Date; +import java.util.Iterator; + +import net.i2p.data.TunnelId; +import net.i2p.router.JobImpl; +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Periodically go through all of the tunnels not assigned to a client and mark + * them as no longer ready and/or drop them (as appropriate) + * + */ +class TunnelPoolExpirationJob extends JobImpl { + private final static Log _log = new Log(TunnelPoolExpirationJob.class); + private TunnelPool _pool; + + /** expire tunnels as necessary every 30 seconds */ + private final static long EXPIRE_POOL_DELAY = 30*1000; + + /** + * don't hard expire a tunnel until its later than expiration + buffer + */ + private final static long EXPIRE_BUFFER = 30*1000; + + public TunnelPoolExpirationJob(TunnelPool pool) { + super(); + _pool = pool; + getTiming().setStartAfter(Clock.getInstance().now() + EXPIRE_POOL_DELAY); + } + public String getName() { return "Expire Pooled Tunnels"; } + public void runJob() { + if (!_pool.isLive()) + return; + expireFree(); + expireOutbound(); + expireParticipants(); + expirePending(); + requeue(EXPIRE_POOL_DELAY); + } + + /** + * Drop all pooled free tunnels that are expired or are close enough to + * being expired that allocating them to a client would suck. + * + */ + public void expireFree() { + long now = Clock.getInstance().now(); + long expire = now - EXPIRE_BUFFER - Router.CLOCK_FUDGE_FACTOR; + + for (Iterator iter = _pool.getFreeTunnels().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getFreeTunnel(id); + if ( (info != null) && (info.getSettings() != null) ) { + if (info.getSettings().getExpiration() < expire) { + _log.info("Expiring free inbound tunnel " + id + " [" + new Date(info.getSettings().getExpiration()) + "] (expire = " + new Date(expire) + ")"); + _pool.removeFreeTunnel(id); + } else if (info.getSettings().getExpiration() < now) { + _log.info("It is past the expiration for free inbound tunnel " + id + " but not yet the buffer, mark it as no longer ready"); + info.setIsReady(false); + } + } + } + } + + /** + * Drop all pooled outbound tunnels that are expired + * + */ + public void expireOutbound() { + long now = Clock.getInstance().now(); + long expire = now - EXPIRE_BUFFER - Router.CLOCK_FUDGE_FACTOR; + + for (Iterator iter = _pool.getOutboundTunnels().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getOutboundTunnel(id); + if ( (info != null) && (info.getSettings() != null) ) { + if (info.getSettings().getExpiration() < expire) { + _log.info("Expiring outbound tunnel " + id + " [" + new Date(info.getSettings().getExpiration()) + "]"); + _pool.removeOutboundTunnel(id); + } else if (info.getSettings().getExpiration() < now) { + _log.info("It is past the expiration for outbound tunnel " + id + " but not yet the buffer, mark it as no longer ready"); + info.setIsReady(false); + } + } + } + } + + /** + * Drop all tunnels we are participating in (but not managing) that are expired + * + */ + public void expireParticipants() { + long now = Clock.getInstance().now(); + long expire = now - EXPIRE_BUFFER - Router.CLOCK_FUDGE_FACTOR; + + for (Iterator iter = _pool.getParticipatingTunnels().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getParticipatingTunnel(id); + if ( (info != null) && (info.getSettings() != null) ) { + if (info.getSettings().getExpiration() < expire) { + _log.info("Expiring participation in tunnel " + id + " [" + new Date(info.getSettings().getExpiration()) + "]"); + _pool.removeParticipatingTunnel(id); + } + } + } + } + + /** + * Drop all tunnels that were in the process of being built, but expired before being handled + * + */ + public void expirePending() { + long now = Clock.getInstance().now(); + long expire = now - EXPIRE_BUFFER - Router.CLOCK_FUDGE_FACTOR; + + for (Iterator iter = _pool.getPendingTunnels().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getPendingTunnel(id); + if ( (info != null) && (info.getSettings() != null) ) { + if (info.getSettings().getExpiration() < expire) { + _log.info("Expiring pending tunnel " + id + " [" + new Date(info.getSettings().getExpiration()) + "]"); + _pool.removePendingTunnel(id); + } + } + } + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/TunnelPoolManagerJob.java b/router/java/src/net/i2p/router/tunnelmanager/TunnelPoolManagerJob.java new file mode 100644 index 0000000000..c5a0afdfaf --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/TunnelPoolManagerJob.java @@ -0,0 +1,167 @@ +package net.i2p.router.tunnelmanager; + +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.data.TunnelId; +import net.i2p.router.TunnelInfo; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Set; +import java.util.Iterator; + +/** + * Request new tunnels to be created if insufficient free inbound tunnels or + * valid outbound tunnels exist. + * + */ +class TunnelPoolManagerJob extends JobImpl { + private final static Log _log = new Log(TunnelPoolManagerJob.class); + private TunnelPool _pool; + + /** whether we built tunnels on the last run */ + private boolean _builtOnLastRun; + + /** + * How frequently to check the pool (and fire appropriate refill jobs) + * + */ + private final static long POOL_CHECK_DELAY = 30*1000; + + /** + * treat tunnels that are going to expire in the next minute as pretty much + * expired (for the purpose of building new ones) + */ + private final static long EXPIRE_FUDGE_PERIOD = 60*1000; + + public TunnelPoolManagerJob(TunnelPool pool) { + super(); + _pool = pool; + } + + public String getName() { return "Manage Tunnel Pool"; } + public void runJob() { + try { + if (!_pool.isLive()) + return; + + boolean built = false; + + int targetClients = _pool.getTargetClients(); + int targetInboundTunnels = targetClients*_pool.getPoolSettings().getNumInboundTunnels() + 3; + int targetOutboundTunnels = targetClients*_pool.getPoolSettings().getNumOutboundTunnels() + 3; + + int curFreeInboundTunnels = getFreeTunnelCount(); + if (curFreeInboundTunnels < targetInboundTunnels) { + _log.info("Insufficient free inbound tunnels (" + curFreeInboundTunnels + ", not " + targetInboundTunnels + "), requesting more"); + requestInboundTunnels(targetInboundTunnels - curFreeInboundTunnels); + //requestFakeInboundTunnels(1); + built = true; + } else { + if (_builtOnLastRun) { + // all good, no need for more inbound tunnels + _log.debug("Sufficient inbound tunnels (" + curFreeInboundTunnels + ")"); + } else { + _log.info("Building another inbound tunnel, cuz tunnels r k00l"); + requestInboundTunnels(1); + built = true; + } + } + + int curOutboundTunnels = getOutboundTunnelCount(); + if (curOutboundTunnels < targetOutboundTunnels) { + _log.info("Insufficient outbound tunnels (" + curOutboundTunnels + ", not " + targetOutboundTunnels + "), requesting more"); + requestOutboundTunnels(targetOutboundTunnels - curOutboundTunnels); + //requestFakeOutboundTunnels(1); + built = true; + } else { + if (_builtOnLastRun) { + // all good, no need for more outbound tunnels + _log.debug("Sufficient outbound tunnels (" + curOutboundTunnels + ")"); + } else { + _log.info("Building another outbound tunnel, since gravity still works"); + requestOutboundTunnels(1); + built = true; + } + } + + _pool.buildFakeTunnels(); + _builtOnLastRun = built; + } catch (Throwable t) { + _log.log(Log.CRIT, "Unhandled exception managing the tunnel pool", t); + } + + requeue(POOL_CHECK_DELAY); + } + + /** + * How many free inbound tunnels are available for use (safely) + * + */ + private int getFreeTunnelCount() { + Set freeTunnels = _pool.getFreeTunnels(); + int free = 0; + int minLength = _pool.getPoolSettings().getDepthInbound(); + long mustExpireAfter = Clock.getInstance().now() + EXPIRE_FUDGE_PERIOD; + for (Iterator iter = freeTunnels.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getFreeTunnel(id); + if ( (info != null) && (info.getIsReady()) ) { + if (info.getSettings().getExpiration() > mustExpireAfter) { + if (info.getLength() >= minLength) { + if (info.getDestination() == null) { + free++; + } else { + // already alloc'ed + _log.error("Why is a free inbound tunnel allocated to a destination? [" + info.getTunnelId().getTunnelId() + " to " + info.getDestination().toBase64() + "]"); + } + } else { + // its valid, sure, but its not long enough *cough* + + // for the moment we'll keep these around so that we can use them + // for tunnel management and db messages, rather than force all + // tunnels to be the 2+ hop length as required for clients + free++; + } + } else { + _log.info("Inbound tunnel " + id + " is expiring in the upcoming period, consider it not-free"); + } + } + } + return free; + } + + /** + * How many outbound tunnels are available for use (safely) + * + */ + private int getOutboundTunnelCount() { + Set outboundTunnels = _pool.getOutboundTunnels(); + int outbound = 0; + long mustExpireAfter = Clock.getInstance().now() + EXPIRE_FUDGE_PERIOD; + for (Iterator iter = outboundTunnels.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getOutboundTunnel(id); + if ( (info != null) && (info.getIsReady()) ) { + if (info.getSettings().getExpiration() > mustExpireAfter) { + outbound++; + } else { + _log.info("Outbound tunnel " + id + " is expiring in the upcoming period, consider it not-free"); + } + } + } + return outbound; + } + + private void requestInboundTunnels(int numTunnelsToRequest) { + _log.info("Requesting " + numTunnelsToRequest + " inbound tunnels"); + for (int i = 0; i < numTunnelsToRequest; i++) + JobQueue.getInstance().addJob(new RequestInboundTunnelJob(_pool, false)); + } + + private void requestOutboundTunnels(int numTunnelsToRequest) { + _log.info("Requesting " + numTunnelsToRequest + " outbound tunnels"); + for (int i = 0; i < numTunnelsToRequest; i++) + JobQueue.getInstance().addJob(new RequestOutboundTunnelJob(_pool, false)); + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/TunnelPoolPersistenceHelper.java b/router/java/src/net/i2p/router/tunnelmanager/TunnelPoolPersistenceHelper.java new file mode 100644 index 0000000000..c290da8bd7 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/TunnelPoolPersistenceHelper.java @@ -0,0 +1,203 @@ +package net.i2p.router.tunnelmanager; + +import net.i2p.util.Log; + +import net.i2p.router.Router; +import net.i2p.router.TunnelInfo; +import net.i2p.router.ClientTunnelSettings; +import net.i2p.data.TunnelId; +import net.i2p.data.Destination; +import net.i2p.data.DataHelper; +import net.i2p.data.DataFormatException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import java.util.Iterator; +import java.util.HashSet; +import java.util.Properties; + +/** + * Handle all of the load / store of the tunnel pool (including any contained + * client tunnel pools) + * + */ +class TunnelPoolPersistenceHelper { + private final static Log _log = new Log(TunnelPoolPersistenceHelper.class); + + public final static String PARAM_TUNNEL_POOL_FILE = "router.tunnelPoolFile"; + public final static String DEFAULT_TUNNEL_POOL_FILE = "tunnelPool.dat"; + + public void writePool(TunnelPool pool) { + File f = getTunnelPoolFile(); + writePool(pool, f); + } + public void writePool(TunnelPool pool, File f) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(f); + DataHelper.writeLong(fos, 2, pool.getFreeTunnelCount()); + for (Iterator iter = pool.getFreeTunnels().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = pool.getFreeTunnel(id); + if (info != null) + info.writeBytes(fos); + } + DataHelper.writeLong(fos, 2, pool.getOutboundTunnelCount()); + for (Iterator iter = pool.getOutboundTunnels().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = pool.getOutboundTunnel(id); + if (info != null) + info.writeBytes(fos); + } + DataHelper.writeLong(fos, 2, pool.getParticipatingTunnels().size()); + for (Iterator iter = pool.getParticipatingTunnels().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = pool.getParticipatingTunnel(id); + if (info != null) + info.writeBytes(fos); + } + DataHelper.writeLong(fos, 2, pool.getPendingTunnels().size()); + for (Iterator iter = pool.getPendingTunnels().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = pool.getPendingTunnel(id); + if (info != null) + info.writeBytes(fos); + } + DataHelper.writeLong(fos, 2, pool.getClientPools().size()); + for (Iterator iter = pool.getClientPools().iterator(); iter.hasNext(); ) { + Destination dest = (Destination)iter.next(); + ClientTunnelPool cpool = (ClientTunnelPool)pool.getClientPool(dest); + writeClientPool(fos, cpool); + } + fos.flush(); + } catch (IOException ioe) { + _log.error("Error writing tunnel pool at " + f.getName(), ioe); + } catch (DataFormatException dfe) { + _log.error("Error formatting tunnels at " + f.getName(), dfe); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + _log.debug("Tunnel pool state written to " + f.getName()); + } + } + + private void writeClientPool(FileOutputStream fos, ClientTunnelPool pool) throws IOException, DataFormatException { + pool.getDestination().writeBytes(fos); + Properties props = new Properties(); + pool.getClientSettings().writeToProperties(props); + DataHelper.writeProperties(fos, props); + DataHelper.writeLong(fos, 2, pool.getInboundTunnelIds().size()); + for (Iterator iter = pool.getInboundTunnelIds().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = pool.getInboundTunnel(id); + if (info != null) + info.writeBytes(fos); + } + DataHelper.writeLong(fos, 2, pool.getInactiveInboundTunnelIds().size()); + for (Iterator iter = pool.getInactiveInboundTunnelIds().iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = pool.getInactiveInboundTunnel(id); + if (info != null) + info.writeBytes(fos); + } + } + + /** + * Load up the tunnels from disk, adding as appropriate to the TunnelPool + */ + public void loadPool(TunnelPool pool) { + File f = getTunnelPoolFile(); + loadPool(pool, f); + } + + public void loadPool(TunnelPool pool, File f) { + if (!f.exists()) return; + FileInputStream fin = null; + try { + fin = new FileInputStream(f); + int numFree = (int)DataHelper.readLong(fin, 2); + for (int i = 0; i < numFree; i++) { + TunnelInfo info = new TunnelInfo(); + info.readBytes(fin); + pool.addFreeTunnel(info); + } + int numOut = (int)DataHelper.readLong(fin, 2); + for (int i = 0; i < numOut; i++) { + TunnelInfo info = new TunnelInfo(); + info.readBytes(fin); + pool.addOutboundTunnel(info); + } + int numParticipating = (int)DataHelper.readLong(fin, 2); + for (int i = 0; i < numParticipating; i++) { + TunnelInfo info = new TunnelInfo(); + info.readBytes(fin); + pool.addParticipatingTunnel(info); + } + int numPending = (int)DataHelper.readLong(fin, 2); + for (int i = 0; i < numPending; i++) { + TunnelInfo info = new TunnelInfo(); + info.readBytes(fin); + pool.addPendingTunnel(info); + } + int numClients = (int)DataHelper.readLong(fin, 2); + for (int i = 0; i < numClients; i++) { + readClientPool(fin, pool); + } + } catch (IOException ioe) { + _log.error("Error reading tunnel pool from " + f.getName(), ioe); + } catch (DataFormatException dfe) { + _log.error("Error formatting tunnels from " + f.getName(), dfe); + } finally { + if (fin != null) try { fin.close(); } catch (IOException ioe) {} + _log.debug("Tunnel pool state written to " + f.getName()); + } + } + + + private void readClientPool(FileInputStream fin, TunnelPool pool) throws IOException, DataFormatException { + Destination dest = new Destination(); + dest.readBytes(fin); + ClientTunnelSettings settings = new ClientTunnelSettings(); + Properties props = DataHelper.readProperties(fin); + settings.readFromProperties(props); + HashSet activeTunnels = new HashSet(); + int numActiveTunnels = (int)DataHelper.readLong(fin, 2); + for (int i = 0; i < numActiveTunnels; i++) { + TunnelInfo info = new TunnelInfo(); + info.readBytes(fin); + activeTunnels.add(info); + } + HashSet inactiveTunnels = new HashSet(); + int numInactiveTunnels = (int)DataHelper.readLong(fin, 2); + for (int i = 0; i < numInactiveTunnels; i++) { + TunnelInfo info = new TunnelInfo(); + info.readBytes(fin); + inactiveTunnels.add(info); + } + + ClientTunnelPool cpool = new ClientTunnelPool(dest, settings, pool); + cpool.setActiveTunnels(activeTunnels); + cpool.setInactiveTunnels(inactiveTunnels); + pool.addClientPool(cpool); + cpool.startPool(); + } + + + /** + * Retrieve the file the pool should be persisted in + * + */ + private File getTunnelPoolFile() { + String filename = null; + + String str = Router.getInstance().getConfigSetting(PARAM_TUNNEL_POOL_FILE); + if ( (str != null) && (str.trim().length() > 0) ) + filename = str; + else + filename = DEFAULT_TUNNEL_POOL_FILE; + + return new File(filename); + } +} diff --git a/router/java/src/net/i2p/router/tunnelmanager/TunnelTestManager.java b/router/java/src/net/i2p/router/tunnelmanager/TunnelTestManager.java new file mode 100644 index 0000000000..3160f358c9 --- /dev/null +++ b/router/java/src/net/i2p/router/tunnelmanager/TunnelTestManager.java @@ -0,0 +1,119 @@ +package net.i2p.router.tunnelmanager; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import net.i2p.data.TunnelId; +import net.i2p.router.JobImpl; +import net.i2p.router.JobQueue; +import net.i2p.router.TunnelInfo; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +/** + * Manage the testing for free, outbound, and active inbound client tunnels + * + */ +class TunnelTestManager { + private final static Log _log = new Log(TunnelTestManager.class); + private TunnelPool _pool; + private boolean _stopTesting; + + private final static long MINIMUM_RETEST_DELAY = 60*1000; // dont test tunnels more than once every 30 seconds + + /** avg # tests per tunnel lifetime that we want */ + private final static int TESTS_PER_DURATION = 2; + /** how many times we'll be able to try the tests (this should take into consideration user prefs, but fsck it for now) */ + private final static int CHANCES_PER_DURATION = 8; + + public TunnelTestManager(TunnelPool pool) { + _pool = pool; + _stopTesting = false; + JobQueue.getInstance().addJob(new CoordinateTunnelTestingJob()); + } + + private Set selectTunnelsToTest() { + Set allIds = getAllIds(); + Set toTest = new HashSet(allIds.size()); + long now = Clock.getInstance().now(); + for (Iterator iter = allIds.iterator(); iter.hasNext();) { + TunnelId id = (TunnelId)iter.next(); + TunnelInfo info = _pool.getTunnelInfo(id); + if ( (info != null) && (info.getSettings() != null) ) { + if (info.getSettings().getExpiration() <= 0) { + // skip local tunnels + } else if (!info.getIsReady()) { + // skip not ready tunnels + } else if (info.getSettings().getExpiration() < now + MINIMUM_RETEST_DELAY) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Tunnel " + id.getTunnelId() + " will be expiring within the current period (" + new Date(info.getSettings().getExpiration()) + "), so skip testing it"); + } else if (info.getSettings().getCreated() + MINIMUM_RETEST_DELAY < now) { + double probability = TESTS_PER_DURATION / (allIds.size() * CHANCES_PER_DURATION); + if (RandomSource.getInstance().nextInt(10) <= (probability*10d)) { + toTest.add(id); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Tunnel " + id.getTunnelId() + " could be tested, but probabilistically isn't going to be"); + } + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Tunnel " + id.getTunnelId() + " was just created (" + new Date(info.getSettings().getCreated()) + "), wait until the next pass to test it"); + } + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Hmm, a normally testable tunnel [" + id.getTunnelId() + "] didn't have info or settings: " + info); + } + } + return toTest; + } + + private Set getAllIds() { + return _pool.getManagedTunnelIds(); + } + + public void stopTesting() { _stopTesting = true; } + + private void runTest(TunnelId tunnel) { + JobQueue.getInstance().addJob(new TestTunnelJob(tunnel, _pool)); + } + + private class CoordinateTunnelTestingJob extends JobImpl { + public CoordinateTunnelTestingJob() { + super(); + getTiming().setStartAfter(Clock.getInstance().now() + MINIMUM_RETEST_DELAY); + } + public String getName() { return "Coordinate Tunnel Testing"; } + public void runJob() { + if (_stopTesting) return; + + Set toTestIds = selectTunnelsToTest(); + if (_log.shouldLog(Log.INFO)) + _log.info("Running tests on selected tunnels: " + toTestIds); + for (Iterator iter = toTestIds.iterator(); iter.hasNext(); ) { + TunnelId id = (TunnelId)iter.next(); + runTest(id); + } + reschedule(); + } + + private void reschedule() { + long minNext = Clock.getInstance().now() + MINIMUM_RETEST_DELAY; + long nxt = minNext + RandomSource.getInstance().nextInt(60*1000); // test tunnels once every 30-90 seconds + getTiming().setStartAfter(nxt); + if (_log.shouldLog(Log.INFO)) + _log.info("Rescheduling tunnel tests for " + new Date(nxt)); + JobQueue.getInstance().addJob(CoordinateTunnelTestingJob.this); + } + } +} diff --git a/router/java/test/net/i2p/data/i2np/.nbattrs b/router/java/test/net/i2p/data/i2np/.nbattrs new file mode 100644 index 0000000000..8b09cbcb29 --- /dev/null +++ b/router/java/test/net/i2p/data/i2np/.nbattrs @@ -0,0 +1,7 @@ + + + + + + + diff --git a/router/java/test/net/i2p/data/i2np/DatabaseStoreMessageTest.java b/router/java/test/net/i2p/data/i2np/DatabaseStoreMessageTest.java new file mode 100644 index 0000000000..930b01ad69 --- /dev/null +++ b/router/java/test/net/i2p/data/i2np/DatabaseStoreMessageTest.java @@ -0,0 +1,38 @@ +package net.i2p.data; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Date; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataStructure; +import net.i2p.data.RouterInfo; +import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.util.Clock; + +/** + * Test harness for loading / storing I2NP DatabaseStore message objects + * + * @author jrandom + */ +class DatabaseStoreMessageTest extends StructureTest { + static { + TestData.registerTest(new DatabaseStoreMessageTest(), "DatabaseStoreMessage"); + } + public DataStructure createDataStructure() throws DataFormatException { + DatabaseStoreMessage msg = new DatabaseStoreMessage(); + RouterInfo info = (RouterInfo)new RouterInfoTest().createDataStructure(); + msg.setKey(info.getIdentity().getHash()); + msg.setMessageExpiration(new Date(Clock.getInstance().now())); + msg.setUniqueId(42); + msg.setRouterInfo(info); + return msg; + } + public DataStructure createStructureToRead() { return new DatabaseStoreMessage(); } +} diff --git a/router/java/test/net/i2p/data/i2np/DeliveryInstructionsTest.java b/router/java/test/net/i2p/data/i2np/DeliveryInstructionsTest.java new file mode 100644 index 0000000000..5bc965a1e2 --- /dev/null +++ b/router/java/test/net/i2p/data/i2np/DeliveryInstructionsTest.java @@ -0,0 +1,54 @@ +package net.i2p.data; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataStructure; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.data.TunnelId; +import net.i2p.data.i2np.DeliveryInstructions; +import net.i2p.util.Log; + +/** + * Test harness for loading / storing DeliveryInstructions objects + * + * @author jrandom + */ +class DeliveryInstructionsTest extends StructureTest { + private final static Log _log = new Log(DeliveryInstructionsTest.class); + static { + TestData.registerTest(new DeliveryInstructionsTest(), "DeliveryInstructions"); + } + public DataStructure createDataStructure() throws DataFormatException { + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDelayRequested(true); + instructions.setDelaySeconds(42); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_TUNNEL); + instructions.setEncrypted(true); + SessionKey key = new SessionKey(); + byte keyData[] = new byte[SessionKey.KEYSIZE_BYTES]; + for (int i = 0; i < keyData.length; i++) + keyData[i] = (byte)i; + key.setData(keyData); + instructions.setEncryptionKey(key); + Hash hash = new Hash(); + byte hashData[] = new byte[32]; + for (int i = 0; i < hashData.length; i++) + hashData[i] = (byte)(i%32); + hash.setData(hashData); + instructions.setRouter(hash); + TunnelId id = new TunnelId(); + id.setTunnelId(666); + instructions.setTunnelId(id); + _log.debug("Instructions created: " + instructions + "\nBase 64: " + instructions.toBase64()); + return instructions; + } + public DataStructure createStructureToRead() { return new DeliveryInstructions(); } +} diff --git a/router/java/test/net/i2p/data/i2np/I2NPMessageReaderTest.java b/router/java/test/net/i2p/data/i2np/I2NPMessageReaderTest.java new file mode 100644 index 0000000000..1cced7adcd --- /dev/null +++ b/router/java/test/net/i2p/data/i2np/I2NPMessageReaderTest.java @@ -0,0 +1,78 @@ +package net.i2p.data; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.I2NPMessageReader; +import net.i2p.util.Log; + +/** + * Test harness for loading / storing I2NP DatabaseStore message objects + * + * @author jrandom + */ +class I2NPMessageReaderTest implements I2NPMessageReader.I2NPMessageEventListener { + private final static Log _log = new Log(I2NPMessageReaderTest.class); + + public static void main(String args[]) { + I2NPMessageReaderTest test = new I2NPMessageReaderTest(); + test.runTest(); + try { Thread.sleep(30*1000); } catch (InterruptedException ie) {} + } + + public void runTest() { + InputStream data = getData(); + test(data); + } + + private InputStream getData() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + DatabaseStoreMessage msg = (DatabaseStoreMessage)new DatabaseStoreMessageTest().createDataStructure(); + msg.writeBytes(baos); + msg.writeBytes(baos); + msg.writeBytes(baos); + _log.debug("DB Store message in tunnel contains: " + msg); + msg.writeBytes(baos); + } catch (DataFormatException dfe) { + _log.error("Error building data", dfe); + } catch (IOException ioe) { + _log.error("Error writing stream", ioe); + } + return new ByteArrayInputStream(baos.toByteArray()); + } + + private void test(InputStream in) { + _log.debug("Testing the input stream"); + I2NPMessageReader reader = new I2NPMessageReader(in, this); + _log.debug("Created, beginning reading"); + reader.startReading(); + _log.debug("Reading commenced"); + } + + public void disconnected(I2NPMessageReader reader) { + _log.debug("Disconnected"); + } + + public void messageReceived(I2NPMessageReader reader, I2NPMessage message, long msToRead) { + _log.debug("Message received: " + message); + } + + public void readError(I2NPMessageReader reader, Exception error) { + _log.debug("Read error: " + error.getMessage(), error); + } + +}