Compare commits

..

195 Commits

Author SHA1 Message Date
76fd98c346 0.2.2 2010-03-22 07:17:20 +00:00
f3bdebead6 Spelling of the plugin display name 2010-03-22 07:16:32 +00:00
d735db129a Increase max. lock time for unresponsive peers 2010-03-22 04:16:31 +00:00
903c58d7ce Use a solid background for displaying emails 2010-03-21 17:38:18 +00:00
96d51bbc4d Fix: Bootstrap nodes are shown as active since 1970 2010-03-21 17:12:03 +00:00
35050ae5c9 Don't create a N_ file if an O_ file exists for the same message ID. 2010-03-21 16:21:31 +00:00
2fa58dc2c2 * Rename I2PBoteThread.awaitShutdown() to awaitShutdownRequest(), keep DHT.awaitShutdown()
* Write peers to file on shutdown
2010-03-21 08:00:21 +00:00
f307561705 Abort lookup on shutdown request 2010-03-21 07:41:09 +00:00
d313cd89e6 Use BufferedWriter.newline() instead of System.getProperty(line.separator) 2010-03-21 07:36:22 +00:00
13174f7164 Cleanup 2010-03-21 01:27:06 +00:00
668c7c4896 Implement IndexPacketTest 2010-03-21 01:26:17 +00:00
8724648d53 Don't exit until send queue is empty (or the thread is killed) 2010-03-19 23:07:49 +00:00
707aaf0bd0 Shut down ExecutorService when done so threads exit 2010-03-19 20:43:05 +00:00
bb055ce55c Adjust text field widths, now that the CSS doesn't make them all 100% wide 2010-03-19 20:04:54 +00:00
5bf930102c * Make auto mail checking configurable
* Configuration.java:
  - Wrap Properties instead of extending it
  - Replace tabs with spaces
2010-03-19 19:55:18 +00:00
554b0e251d Add some missing translations 2010-03-15 07:07:55 +00:00
9a84bb2ae1 Put a space after Re: 2010-03-15 06:38:37 +00:00
ce6bcecff9 Increase stack size to 256 kB for mail check threads 2010-03-15 06:30:41 +00:00
d6fd4e971e PacketQueue:
* Now based on an ArrayList instead of LinkedList
 * Any synchronization issues should be gone
2010-03-15 05:28:36 +00:00
f399f8d691 Rename REFRESH_TIMEOUT to BUCKET_REFRESH_INTERVAL 2010-03-14 21:04:44 +00:00
5acebd19fe Fix a ConcurrentModificationException 2010-03-14 20:57:26 +00:00
80db993ca7 Make saveToFile() parameterless and rename to save() 2010-03-14 20:56:10 +00:00
9d59cd074c Remove some unused stuff 2010-03-14 18:42:15 +00:00
e86ef43058 German translation 2010-03-14 18:40:31 +00:00
b928c41d22 * Multi-language support
* Delete PrintAppVersionTag, replace with a JSPHelper property
2010-03-14 17:41:35 +00:00
1cc96558d5 Second bootstrap node 2010-03-05 16:28:11 +00:00
7371f835de Fix a NPE 2010-03-05 16:18:46 +00:00
e9010555a1 Fix: refilling the s-bucket broke BucketManagerTest 2010-03-04 07:05:16 +00:00
ee2f9f7471 Fix a potential IndexOutOfBoundsException 2010-03-04 07:02:11 +00:00
8c1e166007 * Use locked + unlocked peers for refilling the s-bucket
* Fix: refillSiblings() generates duplicate peers
2010-03-04 07:00:27 +00:00
030ec907f4 * add missing "}"
* replace tabs with spaces
2010-03-04 06:22:41 +00:00
e9bd3ed60e 2010-03-03 18:06:04 +00:00
e156962b24 0.2.1 2010-03-03 18:05:03 +00:00
aa6b5489f7 2010-03-03 18:00:15 +00:00
1adec9c83b Shutdown:
* Fix: NPE when stopping plugin before the application has started connecting
  * Shut down all threads so the app exits without having to kill the JVM
2010-03-03 17:59:34 +00:00
e21db2b740 store(): Include local node if not enough peers found, or if local node is closer than one of the peers found 2010-03-01 08:05:01 +00:00
5ff0f41988 * Always query the closest unqueried peer, instead of picking one randomly
* Have a list of all (not just S) peers ready at the beginning of the lookup. This fixes the "No storages found" bug (which I think would have gone away in the next release due to the higher S value).
2010-03-01 08:02:56 +00:00
302db49850 2010-03-01 06:26:52 +00:00
b11459eeba Fix: An IndexPacketDeleteRequest for an already deleted entry causes a NPE 2010-03-01 06:04:37 +00:00
1ae332a85d Partial undo of rev. 30c74967ae81dff73f62ecca085cd72a85424ffe: Don't move unresponsive siblings to a k-bucket. I don't know what I was thinking, there is no reason to do that. 2010-03-01 03:21:32 +00:00
653a162643 Layout fix 2010-03-01 02:27:06 +00:00
cf3d58920a Re-bootstrap when there are no more active peers 2010-02-28 22:44:07 +00:00
1431272618 Run pack200 on plugin jars 2010-02-28 20:52:07 +00:00
3c0fb794d9 Code simplification 2010-02-28 06:34:35 +00:00
5135d07fa4 Ant plugin target: get the app version in a platform-independent way
Ant src target: get the app version from the source like the plugin target does
2010-02-28 02:42:12 +00:00
c1aa31bdfb 2010-02-28 02:35:24 +00:00
79b2197dc0 Option to compile I2P-Bote as an I2P plugin 2010-02-26 23:32:43 +00:00
ec7e057b6d * Fix: ClosestNodesLookupTask returned too early in some cases
* Periodical s-bucket refresh
* Exponential backoff for unresponsive peers
* Lots of minor stuff
2010-02-25 02:02:00 +00:00
d6a69e237e * when a sibling times out, move it to a k-bucket and refill the s-bucket
* don't use unresponsive peers for lookups, but keep them unless there is a replacement peer
* remove some unused or redundant code
2010-02-19 05:43:44 +00:00
cc48d56682 * Bucket refresh (periodically refresh a bucket if there hasn't been a DHT lookup for a key within the bucket's range)
* New method KBucket.toString() for logging purposes
* Fix: bucket prefix was calculated wrong
2010-02-17 04:10:07 +00:00
831d263449 2010-02-16 04:47:25 +00:00
7ae9575ed2 Use fn:escapeXml for displaying email data 2010-02-16 04:46:17 +00:00
215a9ab160 Fix junit Ant task 2010-02-16 04:02:29 +00:00
5fac243e70 Ant can use web.xml directly, no need for web-template.xml 2010-02-16 03:51:54 +00:00
6b3c11322e Some cleaning up and other minor stuff 2010-02-15 06:39:58 +00:00
422eacd53d 2010-02-15 06:38:18 +00:00
947ad0014c Set character encoding in requests to UTF-8 unless the browser sets an encoding 2010-02-15 06:37:46 +00:00
b91f08fa01 Implement the Address Book feature 2010-02-15 06:31:38 +00:00
2227955eee Implement the "default identity" feature. 2010-02-12 05:30:17 +00:00
fddefc9275 Fix: Trying to edit an existing identity creates a new one 2010-02-11 05:06:23 +00:00
352df69d51 Fix NPE when navigating to network.jsp right after the 3-minute initial wait 2010-02-11 03:03:41 +00:00
82917aaaab When replying, don't add a Re: if there is one already 2010-02-10 23:13:16 +00:00
75fbcbaecf 2010-02-10 23:11:51 +00:00
45c0fff636 Display Kademlia-specific information on network.jsp while still keeping the DHT interface implementation-agnostic 2010-02-10 21:06:59 +00:00
cc846c3abe * Rename KademliaException to DhtException; in DHT, throw DhtException instead of Exception
* Drop Comparable from I2PSendQueue.ScheduledPacket
* Remove an unneccessary "throws"
* Fix some minor issues found by FindBugs:
  - Don't ignore return values of File.mkdir, File.renameTo, etc.
  - Close open files even if an exception happens
  - Use Integer.valueOf instead of the Integer constructor
  - Make some inner classes static
  - Remove an unused method from UniqueId
  - Make two fields volatile
2010-02-10 04:59:44 +00:00
a131fa6600 90k chars can't fit into 3 email packets anymore due to email encoding, so lower it to 80k chars. 2010-02-09 19:36:17 +00:00
40bd121a77 * Fix: test failed because peers[199] was the same destination as localDestination
* Comments
2010-02-09 19:13:39 +00:00
e4d317c76d Change K and S to real world values. B is still 1 for now. 2010-02-09 19:10:04 +00:00
22a0ef14b6 Return K peers in a Kademlia lookup, not S 2010-02-09 19:05:18 +00:00
f26c8116e8 Remove an incorrect log message 2010-02-09 19:01:07 +00:00
0a03d90696 New unit test BucketManagerTest 2010-02-09 04:39:36 +00:00
3d7788632e * Several Kademlia bugs fixed (bucket splitting was severely broken)
* Use all positive numbers for representing a Hash as a BigInteger
* Some refactoring of the Kademlia code, mainly in order to keep buckets sorted by last reception time without having to explicitly store a "last seen" time for each peer.
2010-02-09 04:38:55 +00:00
6df3e8b845 remove unused variable 2010-02-09 04:29:17 +00:00
39fed9ec83 remove unused import 2010-02-09 04:28:09 +00:00
e2cc37c3e3 Catch all exceptions in messageAvailable() and log them before they get caught in I2PSessionImpl which doesn\'t print a stack trace 2010-02-09 04:21:12 +00:00
b17bcbaa07 More detailed log output 2010-02-09 04:17:35 +00:00
8bdb64c39b * Remove Email.getUniqueID() and Email.setUniqueID(), only use getMessageID()/setMessageID()
* call super method in Email.updateHeaders()
* Explicitly encode subject line from UTF-8
2010-02-06 06:38:31 +00:00
ff880b25ef * Use UTF-8 in HTTP requests
* Instead of an HTTP param, use an EL variable for passing the page title to header.jsp so non-US characters don't get messed up
* Fix title on network.jsp
2010-02-06 06:27:22 +00:00
c341d47b27 2010-02-05 06:21:28 +00:00
8212edca98 * Use JavaMail
* Get rid of the FolderElement interface
2010-02-05 06:20:04 +00:00
bdabe0e19a Add some missing unit tests to AllTests.java 2010-02-04 06:18:10 +00:00
37cbc5cfd9 * Get rid of MessageId, use UniqueId instead
* Don't use the "Message-Id" header at all. The message Id is already in the
  file name (for .mail files) or in the MSID field (for packetized emails).
2010-02-04 06:16:29 +00:00
015af37a59 * Pass the whole Ant classpath to Ant tasks because JSPC seems to need a java compiler for tag files
* Partial fix for the junit task (tests still fail, but they compile now)
* Print SHA256 sum for the war file
* Delete src.zip in the "clean" task
2010-02-03 07:36:53 +00:00
38ad423353 Rename test/.../kademlia package to test/.../dht for consistency 2010-02-02 03:48:37 +00:00
b64614ea45 Version 0.2 2010-02-01 16:09:38 +00:00
c7277b944a Remove the cancel button, disable the save button 2010-02-01 16:05:04 +00:00
27e8723753 Put a tag back in that got accidentally deleted in rev. d6d2a84b2ce55918029c8362a8526ed21e254fe6 2010-02-01 07:01:56 +00:00
bdce1fc8a3 2010-02-01 06:17:36 +00:00
cf6a89a731 Prevent network.jsp from throwing an error when the app has not started connecting yet 2010-02-01 03:44:26 +00:00
e929c8fb18 Show "to" addresses in the inbox and when displaying an email 2010-02-01 03:03:07 +00:00
4e051a8111 Remove unused file 2010-02-01 02:56:51 +00:00
e5385f4017 Reverse sort order, was oldest to newest 2010-02-01 02:56:12 +00:00
146bb6f876 Auto-check for new mail periodically 2010-02-01 01:31:21 +00:00
3698843c0f Fix the delete button 2010-02-01 01:23:07 +00:00
24fb21ea2c * N_ or O_ prefix in the filename indicates if an email is new (read) or old (unread)
* Show new emails in bold, read ones in normal font
* Set an email to "old" when the user displays it
2010-01-31 21:37:41 +00:00
44d09a1b37 2010-01-31 21:37:01 +00:00
64fe831e0c * Move some code out of getElements() into the new method getFilenames()
* Add method getNumElements()
2010-01-31 01:59:12 +00:00
0bc9825a46 2010-01-31 01:45:57 +00:00
4c7f3bd029 remove redundant interface declaration 2010-01-31 01:36:54 +00:00
be58442eed Fix: wrong field name in the javascript that focuses the "public name" field on page load 2010-01-31 01:14:01 +00:00
aafcce644e IncomingPacketHandler:
* Ignore response packets whose id doesn't match the request
 * Rename addPeers to updatePeers
2010-01-30 19:44:07 +00:00
24c76dd897 * Handle invalid packets better
* Ban peers that use a protocol version other than 2
2010-01-30 10:48:29 +00:00
38171179df Remove some unnecessary code 2010-01-30 10:38:33 +00:00
808b895951 * Print local dest hash on the network status page
* List active peers and banned peers separately on the network status page
2010-01-30 10:24:49 +00:00
540b0990eb 2010-01-30 09:49:24 +00:00
f5e3b128f3 Remove unused class 2010-01-30 09:39:23 +00:00
0d12ee1240 2010-01-29 04:55:20 +00:00
d26032595e * AbstractBucket/KBucket: Replace ConcurrentSkipListSet with ConcurrentHashSet for the peer list and the replacement cache
* drop a peer after 3 consecutive non-responses
* several minor changes
2010-01-29 04:54:47 +00:00
80c66d7c23 Oops, two more files should have gone into revision 6b0ba3c712564df1061979948a109779470024f8 2010-01-27 20:13:15 +00:00
5785c5836b . 2010-01-27 07:14:00 +00:00
1c52df1713 Fix the stale counter 2010-01-27 07:13:33 +00:00
591c3bff98 . 2010-01-27 06:32:00 +00:00
28f09ac7d4 Implement reply button 2010-01-27 04:20:59 +00:00
6a059521f0 Remove debug message 2010-01-27 04:16:20 +00:00
7164b7f428 Fix: Public Name and Description headings were in the wrong order 2010-01-25 01:09:38 +00:00
471e8f4b25 Fix: left angle bracket disappears when inserting line break in the sender field 2010-01-25 01:08:09 +00:00
84eb1ded73 Make textarea one row bigger because some email destinations have more line breaks than others 2010-01-25 01:04:44 +00:00
db3429d0e0 * Include sender's Public Name in the "sender" header field
* Use a proportional font for the "new email" text box
2010-01-25 00:17:39 +00:00
ed4439ef4d Don't try to read peers.txt if it does not exist. 2010-01-24 18:24:06 +00:00
63b8bf29e8 Preserve line breaks in emails when sending and convert them to HTML for displaying emails. 2010-01-24 06:36:34 +00:00
7a09d652d1 Fix incorrect logger names 2010-01-24 02:09:04 +00:00
2ebb389900 Use POST for the "New Email" form 2010-01-23 06:07:35 +00:00
2e50916897 Add license notices 2010-01-23 05:11:16 +00:00
b40e327508 Fix unnecessary timeouts when doing DHT retrievals 2010-01-23 04:46:41 +00:00
b9534b2a2a PacketQueue: Insert packets with an earliestSendTime>0 in the right place 2010-01-23 01:05:25 +00:00
c022088190 Always send a response to retrieve requests, even if nothing found. 2010-01-23 00:52:00 +00:00
f5db9e18b5 Don't report a peer timeout if the packet hasn't been sent to the peer yet 2010-01-22 19:04:24 +00:00
87cc21dcf1 * Show peer info on the network page
* Remove some dead code
* Major refactoring of the Kademlia code:
 - Interface DhtPeer replaces the PeerInfo class
 - KademliaPeer extends Destination
 - Get rid of BanList, store ban info in KademliaPeer instead
 - Use separate classes for k-buckets and s-buckets, both extend AbstractBucket
2010-01-22 01:59:25 +00:00
d0af45125e Show peer info on network.jsp 2010-01-19 01:59:21 +00:00
39b66e85ce Put peers on the ban list if they use a wrong protocol version (has no effect yet) 2010-01-18 06:54:40 +00:00
b4072d4813 * Implement deletion of email packets from the DHT and deletion of index packet entries
* Version 2 of the network protocol
2010-01-18 06:51:27 +00:00
525cd894f5 Log dest hash, not dest key in packetReceived 2010-01-08 21:00:01 +00:00
b8dfe7f96d * Allow the X-Priority header field
* Set the "From" field when setting the "Sender" field
2010-01-08 20:43:47 +00:00
289c78108d . 2010-01-08 20:41:50 +00:00
fb9fb7844e Throw an exception if no peers were found for storing a packet 2009-12-20 04:28:19 +00:00
10e32012f9 . 2009-12-20 04:25:48 +00:00
1218c1545c Ver. 0.1.5 2009-12-20 00:08:27 +00:00
a3be21648b 0.1.5 2009-12-20 00:00:03 +00:00
7694ea3e58 Fix: redirect to noIdentities.jsp was broken 2009-12-19 23:56:10 +00:00
c6a12fc336 support deletion of emails 2009-12-19 23:24:32 +00:00
f50b0a9da1 Fix: DHT packets only got stored on one node because the StoreRequests all had the same packet Id. 2009-12-19 23:23:56 +00:00
e9554c7924 Doing a findOne for index packets didn't seem such a good idea after all because
it might return an incomplete version of the index packet when there is a more
complete version stored on another peer. So I'm reverting back to findAll.
2009-12-19 23:22:20 +00:00
424e0dfb35 support deletion of emails 2009-12-19 23:19:24 +00:00
fec9052413 new 2009-12-19 23:15:57 +00:00
cc85ef3d0b . 2009-12-19 20:41:08 +00:00
35fbaa377c Add disabled button style 2009-12-19 19:27:02 +00:00
ee941ea7a1 Move some app status related code to getStatus.jsp so it can be shared with other JSPs 2009-12-19 19:26:24 +00:00
2005a2db4d Disable the Network Status link until the I2P session is ready 2009-12-19 19:21:51 +00:00
31cd0b9bd1 Disable two buttons and one link until I2P session is ready 2009-12-19 19:20:36 +00:00
e8fc716b21 new file 2009-12-19 17:26:36 +00:00
bb33c8dcd9 remove all unimplemented HTML links 2009-12-19 08:26:51 +00:00
ff88ec13ba don't say "connected" until done bootstrapping 2009-12-19 08:21:55 +00:00
62f1850144 Remove some dead code;
in the find method, return the local result immediately if only one result was asked for
2009-12-19 07:34:38 +00:00
0e4a732c76 . 2009-12-19 07:30:39 +00:00
7d4b9d051f Just use the first index packet received, don't wait for all peers to respond 2009-12-19 07:30:21 +00:00
b3d883f7fa fix: distance can't be negative; add JavaDoc 2009-12-19 06:07:13 +00:00
7c51fd6848 Set max bucket size to k for k-buckets, and s for the s-bucket 2009-12-19 04:24:30 +00:00
02e28eb59f . 2009-12-19 03:50:39 +00:00
737237c836 synchronize the list of responses received; more log output; log dest hash, not the full dest key; use the correct key type when removing a mapping from pendingRequests 2009-12-19 03:49:56 +00:00
3d343e7949 Fix: toByteArray wrote all 387 bytes of an I2P destination instead of just the two public keys 2009-12-19 03:30:13 +00:00
278f612aa4 rename FindCloseNodesPacketTest to FindClosePeersPacketTest; add PeerListTest 2009-12-19 03:27:12 +00:00
b858cccb59 . 2009-12-19 03:24:42 +00:00
6d535e6699 New file PeerListTest.java 2009-12-19 03:24:06 +00:00
af193ec09b rename FindCloseNodesPacketTest to FindClosePeersPacketTest 2009-12-19 03:23:19 +00:00
e5d84be0db Implement equals and hashCode 2009-12-18 18:06:55 +00:00
e330c4bf22 log destination hash, not the complete dest key 2009-12-18 18:04:57 +00:00
b5191bc663 add the src target which makes a src.zip file 2009-12-17 18:38:54 +00:00
9dc72d2c70 . 2009-12-17 18:13:58 +00:00
19758eab8e add createRandomHash() 2009-12-17 18:13:20 +00:00
fb2726b0b0 Move all network and threading code out of BucketManager into KademliaDHT;
don't add self to buckets;
use a peer for bootstrapping if it initiated communication;
fix the refresh(KBucket) method which didn't do a closest nodes lookup;
other, minor changes
2009-12-17 18:12:33 +00:00
11438e3649 Move all network and threading code out of BucketManager into KademliaDHT; don't add self to buckets; make BucketManager iterable; other, minor changes 2009-12-17 17:56:50 +00:00
b03534c339 Only log the dest hash, not the multi-line output of Destination.toString(); get rid of an unnecessary if 2009-12-17 17:44:44 +00:00
f6ac9b910a Check for expired packets once a day, not every time when checking for new packets 2009-12-17 17:33:27 +00:00
ed765834fe 0.1.4 2009-12-13 07:41:58 +00:00
459075ca7f Release 0.1.4; Do the 3-minute wait in the background and support skipping the wait time. 2009-12-13 07:41:28 +00:00
47da9495fe New tag "connect"; new JSP function getNetworkStatus 2009-12-13 07:28:06 +00:00
b51733ff2f New JSP function getNetworkStatus() 2009-12-13 07:26:50 +00:00
df463fd6d0 New method isConnected() 2009-12-13 07:25:51 +00:00
d50d9a5318 Modify statusbox class for revised statusFrame.jsp 2009-12-13 07:24:11 +00:00
3b7b748e71 Fix jstllib path; don't add JSPs to war (thanks zzz) 2009-12-13 07:20:05 +00:00
d77b61e939 Link to network.jsp 2009-12-13 07:17:35 +00:00
fbba5d461b Add JSTL for displaying local I2P destination, commented out for now. 2009-12-13 07:17:03 +00:00
0fc434b6d7 Remove unused welcome-file entries 2009-12-13 07:14:46 +00:00
57be687166 New file 2009-12-13 07:12:07 +00:00
9880a54337 0.1.3 2009-12-12 07:45:52 +00:00
08629cb759 Remove unused code; other minor changes 2009-12-12 07:23:58 +00:00
4fe2c5c08d Fix: high cpu usage when sending or receiving; use sendQueue.send instead of sendQueue.sendAndWait so requests can be sent in parallel. 2009-12-12 07:08:07 +00:00
f9ee3ae653 Set DHT key in the merge constructor (fixes a NPE) 2009-12-12 07:02:50 +00:00
ab26312aa0 2009-12-12 07:00:30 +00:00
a1687cb50d bump to ver. 0.1.3; wait 3 minutes before connecting after startup; set tunnel names to "I2P-Bote"; minor changes like renaming of variables 2009-12-12 06:59:55 +00:00
76d6008daa Fix: inboxFlag was never set, which caused the "Check Mail" button not to show new emails. See also buttonFrame.jsp, rev. 1c93f3214bbc0b5ee1ef772caccf576644fe3cbd vs ed79eab91430b185d5b710b0fe8ef3afc8567cb0 2009-12-12 06:55:00 +00:00
05e93e53dd Fix: "Check Mail" button didn't show new emails, user had to reload inbox 2009-12-12 06:49:21 +00:00
1509a65bee checkMail.jsp no longer needed since buttonFrame.jsp does all the mail checking 2009-12-12 06:47:38 +00:00
ab6a498fb7 add failonerror="true" to javac tasks; change all /opt/... paths to ../i2p/... 2009-12-12 06:42:00 +00:00
bf50d5049d 2009-12-07 06:13:58 +00:00
139 changed files with 7666 additions and 2437 deletions

Binary file not shown.

View File

@ -0,0 +1,20 @@
<%@ attribute name="address" required="true" description="The email address to display" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<% jspContext.setAttribute("newline", "\n"); %>
<%-- If the address contains a name that is followed by an email destination in angle brackets, put a line break after the name --%>
<c:set var="gtHtml" value="${fn:escapeXml('<')}"/>
<c:set var="newlinePlusGt" value="${newline}${gtHtml}"/>
<c:set var="formattedAddress" value="${fn:escapeXml(address)}"/>
<c:set var="formattedAddress" value="${fn:replace(formattedAddress, gtHtml, newlinePlusGt)}"/>
<%-- if the address contains an email destination, use a textarea; otherwise just print it --%>
<c:if test="${fn:length(formattedAddress) ge 512}">
<textarea cols="64" rows="9" readonly="yes" wrap="soft" class="nobordertextarea">${formattedAddress}</textarea>
</c:if>
<c:if test="${fn:length(formattedAddress) lt 512}">
${formattedAddress}
</c:if>

View File

@ -1,2 +0,0 @@
<%@ attribute name="index" required="true" description="0-based index of the email identity" %>
test

View File

@ -0,0 +1,12 @@
<%@ attribute name="folderName" required="true" description="This is the folder directory and also the folder's display name" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<c:set var="numEmails" value="${ib:getMailFolder(folderName).numElements}"/>
<c:set var="numNew" value="${ib:getMailFolder(folderName).numNewEmails}"/>
<a href="folder.jsp?path=${folderName}" title="${numEmails} emails total, ${numNew} new">
<img src="images/folder.png"/>${folderName}
</a>
<c:if test="${numNew>0}">(${numNew})</c:if>

View File

@ -38,23 +38,138 @@
</tag>
<tag>
<name>numDhtPeers</name>
<tag-class>i2p.bote.web.PrintNumDhtPeersTag</tag-class>
<name>formatPlainText</name>
<tag-class>i2p.bote.web.FormatPlainTextTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>text</name>
<rtexprvalue>true</rtexprvalue>
<required>true</required>
</attribute>
</tag>
<tag>
<name>setEmailRead</name>
<tag-class>i2p.bote.web.SetEmailReadTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>folder</name>
<rtexprvalue>true</rtexprvalue>
<required>true</required>
</attribute>
<attribute>
<name>messageId</name>
<rtexprvalue>true</rtexprvalue>
<required>true</required>
</attribute>
<attribute>
<name>read</name>
<rtexprvalue>true</rtexprvalue>
<required>true</required>
</attribute>
</tag>
<tag>
<name>quote</name>
<tag-class>i2p.bote.web.QuoteTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>text</name>
<rtexprvalue>true</rtexprvalue>
<required>true</required>
</attribute>
</tag>
<tag>
<name>shortSenderName</name>
<tag-class>i2p.bote.web.PrintShortSenderNameTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>sender</name>
<rtexprvalue>true</rtexprvalue>
<required>true</required>
</attribute>
</tag>
<tag>
<name>connect</name>
<tag-class>i2p.bote.web.ConnectTag</tag-class>
<body-content>empty</body-content>
</tag>
<tag>
<name>numRelayPeers</name>
<tag-class>i2p.bote.web.PrintNumRelayPeersTag</tag-class>
<name>localDestination</name>
<tag-class>i2p.bote.web.LocalDestinationTag</tag-class>
<body-content>empty</body-content>
</tag>
<tag>
<name>printVersion</name>
<tag-class>i2p.bote.web.PrintAppVersionTag</tag-class>
<name>peerInfo</name>
<tag-class>i2p.bote.web.PeerInfoTag</tag-class>
<body-content>empty</body-content>
</tag>
<tag>
<name>message</name>
<description>
Same as fmt:message, except that message keys in
the "en" locale translate to the message key itself.
</description>
<tag-class>i2p.bote.web.MessageTag</tag-class>
<body-content>JSP</body-content>
<attribute>
<name>key</name>
<rtexprvalue>true</rtexprvalue>
<required>true</required>
</attribute>
<attribute>
<name>bundle</name>
<rtexprvalue>true</rtexprvalue>
<required>false</required>
</attribute>
<attribute>
<name>var</name>
<rtexprvalue>true</rtexprvalue>
<required>false</required>
</attribute>
<attribute>
<name>scope</name>
<rtexprvalue>true</rtexprvalue>
<required>false</required>
</attribute>
</tag>
<tag>
<name>param</name>
<description>
Same as fmt:param. Must be inside an ib:message tag.
</description>
<tag-class>i2p.bote.web.ParamTag</tag-class>
<body-content>scriptless</body-content>
<attribute>
<name>value</name>
<rtexprvalue>true</rtexprvalue>
<required>true</required>
</attribute>
</tag>
<tag>
<name>saveConfiguration</name>
<description>
Saves the configuration to the config file.
</description>
<tag-class>i2p.bote.web.SaveConfigurationTag</tag-class>
<body-content>empty</body-content>
</tag>
<function>
<name>getNetworkStatus</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
i2p.bote.network.NetworkStatus getNetworkStatus()
</function-signature>
</function>
<function>
<name>getIdentities</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
@ -63,22 +178,14 @@
</function-signature>
</function>
<function>
<name>getLocalDestination</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
java.lang.String getLocalDestination()
</function-signature>
</function>
<function>
<name>saveIdentity</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
boolean saveIdentity(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
boolean saveIdentity(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)
</function-signature>
</function>
<function>
<name>deleteIdentity</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
@ -87,6 +194,38 @@
</function-signature>
</function>
<function>
<name>getAddressBook</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
i2p.bote.addressbook.AddressBook getAddressBook()
</function-signature>
</function>
<function>
<name>saveContact</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
boolean saveContact(java.lang.String, java.lang.String)
</function-signature>
</function>
<function>
<name>deleteContact</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
boolean deleteContact(java.lang.String)
</function-signature>
</function>
<function>
<name>getContact</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
i2p.bote.addressbook.Contact getContact(java.lang.String)
</function-signature>
</function>
<function>
<name>isCheckingForMail</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
@ -119,9 +258,59 @@
</function-signature>
</function>
<function>
<name>getOneLocalRecipient</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
java.lang.String getOneLocalRecipient(i2p.bote.email.Email)
</function-signature>
</function>
<function>
<name>deleteEmail</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
java.lang.boolean deleteEmail(java.lang.String, java.lang.String)
</function-signature>
</function>
<function>
<name>extractName</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
java.lang.String extractName(java.lang.String)
</function-signature>
</function>
<function>
<name>extractEmailDestination</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
i2p.bote.email.EmailDestination extractEmailDestination(java.lang.String)
</function-signature>
</function>
<function>
<name>getRecipients</name>
<function-class>i2p.bote.web.JSPHelper</function-class>
<function-signature>
java.util.Map getRecipients(java.util.Map)
</function-signature>
</function>
<tag-file>
<name>printWithLineBreaks</name>
<path>/WEB-INF/tags/printWithLineBreaks.tag</path>
</tag-file>
<tag-file>
<name>folderLink</name>
<path>/WEB-INF/tags/folderLink.tag</path>
</tag-file>
<tag-file>
<name>address</name>
<path>/WEB-INF/tags/address.tag</path>
</tag-file>
</taglib>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>i2pbote</display-name>
<!-- precompiled servlets -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<listener>
<listener-class>i2p.bote.web.ServiceInitializer</listener-class>
</listener>
</web-app>

View File

@ -1,16 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>i2pbote</display-name>
<!-- precompiled servlets -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<listener>
<listener-class>i2p.bote.web.ServiceInitializer</listener-class>
</listener>
<filter>
<filter-name>CharsetFilter</filter-name>
<filter-class>i2p.bote.web.CharsetFilter</filter-class>
<init-param>
<param-name>requestEncoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharsetFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View File

@ -22,27 +22,32 @@
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:include page="header.jsp">
<jsp:param name="title" value="About I2P-Bote"/>
</jsp:include>
<ib:message key="About I2P-Bote" var="title" scope="request"/>
<jsp:include page="header.jsp"/>
<div class="main">
<h2>
I2P-Bote Version <ib:printVersion/>
<ib:message key="I2P-Bote Version {0}">
<jsp:useBean id="jspHelperBean" class="i2p.bote.web.JSPHelper"/>
<ib:param value="${jspHelperBean.appVersion}"/>
</ib:message>
</h2>
<br/>
<table><tr>
<td><strong>Author:</strong></td>
<td><strong><ib:message key="Author:"/></strong></td>
</tr><tr>
<td></td><td>HungryHobo@mail.i2p</td>
</tr><tr>
<td><strong>Contributors:</strong></td>
<td><strong><ib:message key="Contributors:"/></strong></td>
</tr><tr>
<td></td><td>Mixxy</td>
</tr><tr>
<td></td><td>zzz</td>
</tr></table>
</div>
<jsp:include page="footer.jsp"/>
<jsp:include page="footer.jsp"/>

122
WebContent/addressBook.jsp Normal file
View File

@ -0,0 +1,122 @@
<%--
Copyright (C) 2009 HungryHobo@mail.i2p
The GPG fingerprint for HungryHobo@mail.i2p is:
6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
This file is part of I2P-Bote.
I2P-Bote 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 3 of the License, or
(at your option) any later version.
I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
--%>
<%--
This page behaves differently depending on the "search" boolean parameter.
If search is true, the user can select contacts and add them as recipients.
If search is false, the user can view and edit the address book.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<ib:message key="Address Book" var="title" scope="request"/>
<jsp:include page="header.jsp"/>
<div class="main">
<c:set var="contacts" value="${ib:getAddressBook().all}"/>
<h2>
<c:if test="${!param.search}"><ib:message key="Private Address Book"/></c:if>
<c:if test="${param.search && !empty contacts}"><ib:message key="Select One or More Entries"/></c:if>
</h2>
<c:if test="${empty contacts}">
The address book is empty.
</c:if>
<div class="addressbook">
<c:if test="${param.search}">
<form action="newEmail.jsp" method="POST">
<c:forEach var="parameter" items="${param}">
<input type="hidden" name="${parameter.key}" value="${parameter.value}"/>
</c:forEach>
</c:if>
<table>
<c:if test="${!empty contacts}">
<tr>
<c:if test="${param.search}"><th style="width: 10px;"></th></c:if>
<th><ib:message key="Name"/></th>
<th><ib:message key="Email Destination"/></th>
<th style="width: 20px; padding: 0px"></th>
</tr>
</c:if>
<c:forEach items="${contacts}" var="contact" varStatus="loopStatus">
<tr>
<c:if test="${param.search}">
<td>
<input type="checkbox" name="selectedContact" value="${contact.name} &lt;${contact.destination}&gt;"/>
</td>
</c:if>
<td style="width: 100px;">
<div class="ellipsis">
<c:if test="${!param.search}">
<a href="editContact.jsp?new=false&destination=${contact.destination}&name=${contact.name}">
</c:if>
${contact.name}
<c:if test="${!param.search}">
</a>
</c:if>
</div>
</td>
<td style="width: 100px;">
<div class="ellipsis">
${contact.destination}
</div>
</td>
<td>
<c:if test="${!param.search}">
<a href="deleteContact.jsp?destination=${contact.destination}"><img src="images/delete.png" alt="<ib:message key='Delete'/>" title='<ib:message key='Delete this contact'/>'/></a>
</c:if>
</td>
</tr>
</c:forEach>
</table>
</div>
<p/>
<table>
<c:if test="${!param.search}">
<tr><td>
<form action="editContact.jsp?new=true" method="POST">
<button type="submit" value="New"><ib:message key="New Contact"/></button>
</form>
</td></tr>
</c:if>
<c:if test="${param.search}">
<tr><td>
<button type="submit" value="New"><ib:message key="Add Recipients"/></button>
</td></tr>
</c:if>
</table>
<c:if test="${param.search}">
</form>
</c:if>
</div>
<jsp:include page="footer.jsp"/>

View File

@ -26,6 +26,12 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:include page="getStatus.jsp"/>
<c:if test="${param.checkMail == 1}">
<ib:checkForMail/>
</c:if>
<c:if test="${ib:isCheckingForMail()}">
<c:set var="checkingForMail" value="true"/>
</c:if>
@ -35,12 +41,17 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="i2pbote.css" />
<c:if test="${checkingForMail}">
<meta http-equiv="refresh" content="20" />
<meta http-equiv="refresh" content="20;url=buttonFrame.jsp" />
</c:if>
</head>
<body style="background-color: transparent; margin: 0px;">
<c:set var="disable" value=""/>
<c:if test="${connStatus == DELAY}">
<c:set var="disable" value="disabled=&quot;disabled&quot;"/>
</c:if>
<table><tr>
<td>
<c:if test="${checkingForMail}">
@ -50,23 +61,34 @@
</c:if>
<c:if test="${!checkingForMail}">
<div class="checkmail">
<form action="checkMail.jsp" target="_top" method="GET">
<input type="hidden" name="path" value="Inbox"/>
<button type="submit" value="Check Mail">Check Mail</button>
<c:choose>
<c:when test="${empty ib:getIdentities().all}">
<c:set var="url" value="noIdentities.jsp"/>
<c:set var="frame" value="target=&quot;_parent&quot;"/>
</c:when>
<c:otherwise>
<c:set var="link" value="buttonFrame.jsp"/>
<c:set var="frame" value=""/>
</c:otherwise>
</c:choose>
<form action="${url}" ${frame} method="GET">
<input type="hidden" name="checkMail" value="1"/>
<button type="submit" value="<ib:message key="Check Mail"/>" ${disable}>Check Mail</button>
</form>
</div>
<c:if test="${ib:newMailReceived()}">
<script language="Javascript">
// If inbox is being displayed, reload so the new email(s) show
if (document.getElementById("inboxFlag"))
window.location.reload();
if (parent.document.getElementById('inboxFlag'))
parent.location.reload();
</script>
</c:if>
</c:if>
</td>
<td>
<form action="newEmail.jsp" target="_top" method="GET">
<button type="submit" value="New">New</button>
<button type="submit" value="<ib:message key="New"/>" ${disable}>New</button>
</form>
</td>
</tr></table>

29
WebContent/connect.jsp Normal file
View File

@ -0,0 +1,29 @@
<%--
Copyright (C) 2009 HungryHobo@mail.i2p
The GPG fingerprint for HungryHobo@mail.i2p is:
6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
This file is part of I2P-Bote.
I2P-Bote 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 3 of the License, or
(at your option) any later version.
I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<ib:connect/>
<jsp:forward page="/index.jsp"></jsp:forward>

View File

@ -26,22 +26,18 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<c:choose>
<c:when test="${empty ib:getIdentities().all}">
<jsp:forward page="noIdentities.jsp"/>
</c:when>
<c:otherwise>
<ib:checkForMail/>
<c:if test="${empty param.nextPage}">
<c:set var="param.nextPage" value="index.jsp"/>
</c:if>
<jsp:forward page="${param.nextPage}"/>
</c:otherwise>
</c:choose>
<c:set var="errorMessage" value="${ib:deleteContact(param.destination)}"/>
<p>
TODO checkMailTag, show status at the bottom (or top?),
point the Inbox link here or just check periodically and when the Check Email button is clicked?
</p>
<jsp:include page="footer.jsp"/>
<c:if test="${empty errorMessage}">
<ib:message key="The contact has been deleted from the address book." var="message"/>
<jsp:forward page="addressBook.jsp">
<jsp:param name="message" value="${message}"/>
</jsp:forward>
</c:if>
<c:if test="${!empty errorMessage}">
<jsp:include page="header.jsp"/>
<div class="main">
<ib:message key="Error"/>: ${errorMessage}
</div>
<jsp:include page="footer.jsp"/>
</c:if>

View File

@ -0,0 +1,37 @@
<%--
Copyright (C) 2009 HungryHobo@mail.i2p
The GPG fingerprint for HungryHobo@mail.i2p is:
6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
This file is part of I2P-Bote.
I2P-Bote 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 3 of the License, or
(at your option) any later version.
I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:include page="header.jsp"/>
<c:set var="deleted" value="${ib:deleteEmail(param.folder, param.messageID)}"/>
<c:if test="${deleted}">
<jsp:forward page="folder.jsp?path=Inbox"/>
</c:if>
<c:if test="${!deleted}">
<ib:message key="Error: Couldn't delete email."/>
</c:if>

View File

@ -36,7 +36,7 @@
<c:if test="${!empty errorMessage}">
<jsp:include page="header.jsp"/>
<div class="main">
Error: ${errorMessage}
<ib:message key="Error:"/> ${errorMessage}
</div>
<jsp:include page="footer.jsp"/>
</c:if>

View File

@ -0,0 +1,80 @@
<%--
Copyright (C) 2009 HungryHobo@mail.i2p
The GPG fingerprint for HungryHobo@mail.i2p is:
6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
This file is part of I2P-Bote.
I2P-Bote 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 3 of the License, or
(at your option) any later version.
I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<c:choose>
<c:when test="${param.new}">
<ib:message key="New Contact" var="title"/>
<c:set var="title" value="${title}" scope="request"/>
<ib:message key="Add" var="action"/>
<c:set var="commitAction" value="${action}"/>
</c:when>
<c:otherwise>
<ib:message key="Edit Contact" var="title"/>
<c:set var="title" value="${title}" scope="request"/>
<ib:message key="Add" var="action"/>
<c:set var="commitAction" value="${action}"/>
</c:otherwise>
</c:choose>
<jsp:include page="header.jsp"/>
<div class="main">
<form name="form" action="saveContact.jsp" method="post">
<table>
<tr>
<td>
<div style="font-weight: bold;"><ib:message key="Email Destination"/></div>
<c:if test="${empty param.destination}">
<div style="font-size: 0.8em;"><ib:message key="(required field, must be 512 characters)"/></div>
</c:if>
</td>
<td>
<input type="text" size="80" name="destination" value="${param.destination}"/>
</td>
</tr>
<tr>
<td>
<div style="font-weight: bold;"><ib:message key="Name:"/></div>
<div style="font-size: 0.8em;"><ib:message key="(required field)"/></div>
</td>
<td>
<input type="text" size="40" name="name" value="${param.name}"/>
</td>
</tr>
</table>
<input type="hidden" name="destination" value="${param.destination}"/>
<input type="submit" name="action" value="${commitAction}"/>
<input type="submit" name="action" value="<ib:message key="Cancel"/>"/>
</form>
<script type="text/javascript" language="JavaScript">
document.forms['form'].elements['name'].focus();
</script>
</div>
<jsp:include page="footer.jsp"/>

View File

@ -29,21 +29,19 @@
<c:choose>
<c:when test="${param.new}">
<c:set var="title" value="New Email Identity"/>
<c:set var="commitAction" value="Create"/>
<ib:message key="New Email Identity" var="title"/>" scope="request"/>
<ib:message key="Create" var="commitAction"/>"/>
</c:when>
<c:otherwise>
<c:set var="title" value="Edit Email Identity"/>
<c:set var="commitAction" value="Save"/>
<ib:message key="Edit Email Identity" var="title"/>
<ib:message key="Save" var="commitAction"/>
</c:otherwise>
</c:choose>
<jsp:include page="header.jsp">
<jsp:param name="title" value="${title}"/>
</jsp:include>
<jsp:include page="header.jsp"/>
<div class="errorMessage">
${param.errorMessage}
${fn:escapeXml(param.errorMessage)}
</div>
<div class="main">
@ -51,48 +49,60 @@
<table>
<tr>
<td>
<div style="font-weight: bold;">Public Name:</div>
<div style="font-size: 0.8em;">(required field, shown to recipients)</div>
<div style="font-weight: bold;"><ib:message key="Public Name:"/></div>
<div style="font-size: 0.8em;"><ib:message key="(required field, shown to recipients)"/></div>
</td>
<td>
<input type="text" size="25" name="publicName" value="${param.publicName}"/>
<input type="text" size="40" name="publicName" value="${param.publicName}"/>
</td>
</tr>
<tr>
<td>
<div style="font-weight: bold;">Description:</div>
<div style="font-size: 0.8em;">(optional, kept private)</div>
<div style="font-weight: bold;"><ib:message key="Description:"/></div>
<div style="font-size: 0.8em;"><ib:message key="(optional, kept private)"/></div>
</td>
<td>
<input type="text" size="25" name="description" value="${param.description}"/>
<input type="text" size="40" name="description" value="${param.description}"/>
</td>
</tr>
<tr>
<td>
<div style="font-weight: bold;">Email Address:</div>
<div style="font-size: 0.8em;">(optional)</div>
<div style="font-weight: bold;"><ib:message key="Email Address:"/></div>
<div style="font-size: 0.8em;"><ib:message key="(optional)"/></div>
</td>
<td>
<input type="text" size="50" name="emailAddress" value="${param.emailAddress}"/>
<input type="text" size="40" name="emailAddress" value="${param.emailAddress}"/>
</td>
</tr>
<c:if test="${!empty param.key}">
<tr>
<td style="font-weight: bold; vertical-align: top;">
Email Destination:
<ib:message key="Email Destination:"/>
</td>
<td>
<textarea cols="64" rows="8" readonly="yes" wrap="soft" style="background-color: transparent; border: none;">${param.key}</textarea>
<textarea cols="64" rows="9" readonly="yes" wrap="soft" class="destinationtextarea">${param.key}</textarea>
</td>
</tr>
<tr>
<td style="font-weight: bold; vertical-align: top;">
<ib:message key="Default Identity:"/>
</td>
<td>
<c:if test="${param.isDefault}">
<c:set var="checked" value="checked"/>
</c:if>
<input type="checkbox" name="isDefault" ${checked}/>
</td>
</tr>
</c:if>
</table>
<input type="hidden" name="key" value="${param.key}"/>
<input type="submit" name="action" value="${commitAction}"/>
<input type="submit" name="action" value="Cancel"/>
<input type="submit" name="action" value="<ib:message key='Cancel'/>"/>
</form>
<script type="text/javascript" language="JavaScript">
document.forms['form'].elements['name'].focus();
document.forms['form'].elements['publicName'].focus();
</script>
</div>

View File

@ -24,54 +24,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:include page="header.jsp">
<jsp:param name="title" value="${param.path}"/>
</jsp:include>
<c:set var="title" value="${param.path}" scope="request"/>
<jsp:include page="header.jsp"/>
<c:set var="folderName" value="Inbox"/>
<c:if test="${folderName == 'Inbox'}">
<div id="inboxFlag"></div>
</c:if>
[DEBUG MSG: This is the <b>${param.path}</b> folder.]
<div class="main">
<div class="folder">
<table>
<tr>
<th style="width: 100px;">Sender</th>
<th style="width: 150px;">Subject</th>
<th style="width: 100px;">Date</th>
<th style="width: 100px;"><ib:message key="From"/></th>
<th style="width: 100px;"><ib:message key="To"/></th>
<th style="width: 150px;"><ib:message key="Subject"/></th>
<th style="width: 100px;"><ib:message key="Date"/></th>
<th style="width: 20px;"></th>
</tr>
<c:set var="folderName" value="Inbox"/>
<c:forEach items="${ib:getMailFolder(folderName).elements}" var="email">
<tr>
<c:set var="sender" value="${email.sender}"/>
<c:if test="${empty sender}">
<c:set var="sender" value="Anonymous"/>
<ib:message key="Anonymous" var="sender"/>
</c:if>
<c:set var="date" value="${email.date}"/>
<c:set var="recipient" value="${ib:getOneLocalRecipient(email)}"/>
<c:set var="date" value="${email.sentDate}"/>
<c:if test="${empty date}">
<c:set var="date" value="${email.dateString}"/>
</c:if>
<c:if test="${empty date}">
<c:set var="date" value="Unknown"/>
<ib:message key="Unknown" var="date"/>"/>
</c:if>
<c:set var="subject" value="${email.subject}"/>
<c:if test="${empty subject}">
<c:set var="subject" value="(No subject)"/>
<ib:message key="(No subject)" var="subject"/>
</c:if>
<c:set var="mailUrl" value="showEmail.jsp?folder=${folderName}&messageID=${email.messageID}"/>
<td><div class="ellipsis"><a href="${mailUrl}">${sender}</a></div></td>
<td><div class="ellipsis"><a href="${mailUrl}">${subject}</a></div></td>
<td><a href="${mailUrl}">${date}</a></td>
<td><a href="deleteEmail.jsp?folder=${folderName}&messageID=${email.messageID}"><img src="images/delete.png" alt="Delete" title="Delete this email"/></a></td>
<c:choose>
<c:when test="${email.new}"><c:set var="fontWeight" value="bold"/></c:when>
<c:otherwise><c:set var="fontWeight" value="normal"/></c:otherwise>
</c:choose>
<td><div class="ellipsis"><a href="${mailUrl}" style="font-weight: ${fontWeight}">${fn:escapeXml(sender)}</a></div></td>
<td><div class="ellipsis"><a href="${mailUrl}" style="font-weight: ${fontWeight}">${fn:escapeXml(recipient)}</a></div></td>
<td><div class="ellipsis"><a href="${mailUrl}" style="font-weight: ${fontWeight}">${fn:escapeXml(subject)}</a></div></td>
<td><a href="${mailUrl}" style="font-weight: ${fontWeight}">${fn:escapeXml(date)}</a></td>
<td>
<a href="deleteEmail.jsp?folder=${folderName}&messageID=${email.messageID}">
<img src="images/delete.png" alt="<ib:message key='Delete'/>" title="<ib:message key='Delete this email'/>"/></a>
</td>
</tr>
</c:forEach>
</table>

36
WebContent/getStatus.jsp Normal file
View File

@ -0,0 +1,36 @@
<%--
Copyright (C) 2009 HungryHobo@mail.i2p
The GPG fingerprint for HungryHobo@mail.i2p is:
6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
This file is part of I2P-Bote.
I2P-Bote 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 3 of the License, or
(at your option) any later version.
I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<%
pageContext.setAttribute("NOT_STARTED", i2p.bote.network.NetworkStatus.NOT_STARTED, PageContext.REQUEST_SCOPE);
pageContext.setAttribute("DELAY", i2p.bote.network.NetworkStatus.DELAY, PageContext.REQUEST_SCOPE);
pageContext.setAttribute("CONNECTING", i2p.bote.network.NetworkStatus.CONNECTING, PageContext.REQUEST_SCOPE);
pageContext.setAttribute("CONNECTED", i2p.bote.network.NetworkStatus.CONNECTED, PageContext.REQUEST_SCOPE);
pageContext.setAttribute("ERROR", i2p.bote.network.NetworkStatus.ERROR, PageContext.REQUEST_SCOPE);
%>
<c:set var="connStatus" value="${ib:getNetworkStatus()}" scope="request"/>

View File

@ -24,22 +24,28 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<fmt:requestEncoding value="UTF-8"/>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="i2pbote.css" />
<link rel="icon" type="image/png" href="images/favicon.png" />
<title>${param.title} - I2P-Bote</title>
<c:if test="${!empty title}">
<title>${title} <ib:message key="- I2P-Bote"/></title>
</c:if>
</head>
<body>
<div class="titlebar" style="cursor:pointer" onclick="document.location='.'">
<div class="title">I2P-Bote</div>
<div class="title"><ib:message key="I2P-Bote"/></div>
<br/>
<div class="subtitle">Secure Distributed Email</div>
<div class="subtitle"><ib:message key="Secure Distributed Email"/></div>
</div>
<div class="menubox">
@ -47,39 +53,47 @@
</div>
<div class="menubox">
<h2>Folders</h2>
<a href="folder.jsp?path=Inbox"><img src="images/folder.png"/>Inbox</a><br/>
<a href="folder.jsp?path=Outbox"><img src="images/folder.png"/>Outbox</a><br/>
<a href="folder.jsp?path=Sent"><img src="images/folder.png"/>Sent</a><br/>
<a href="folder.jsp?path=Drafts"><img src="images/folder.png"/>Drafts</a><br/>
<a href="folder.jsp?path=Trash"><img src="images/folder.png"/>Trash</a><br/>
<h2><ib:message key="Folders"/></h2>
<ib:message key="Inbox" var="folderName"/><ib:folderLink folderName="${folderName}"/><br/>
<img src="images/folder.png"/><ib:message key="Outbox"/><br/>
<img src="images/folder.png"/><ib:message key="Sent"/><br/>
<img src="images/folder.png"/><ib:message key="Drafts"/><br/>
<img src="images/folder.png"/><ib:message key="Trash"/><br/>
<hr/>
<a href="folder.jsp?path=User_created_Folder_1"><img src="images/folder.png"/>User_created_Folder_1</a><br/>
<a href="folder.jsp?path=User_created_Folder_2"><img src="images/folder.png"/>User_created_Folder_2</a><br/>
<a href="folder.jsp?path=User_created_Folder_3"><img src="images/folder.png"/>User_created_Folder_3</a><br/>
<img src="images/folder.png"/>User_created_Folder_1<br/>
<img src="images/folder.png"/>User_created_Folder_2<br/>
<img src="images/folder.png"/>User_created_Folder_3<br/>
<hr/>
<a href=.>Manage Folders</a>
<ib:message key="Manage Folders"/>
</div>
<div class="menubox">
<h2>Addresses</h2>
<a href="identities.jsp">Identities</a><br/>
<a href=.>Private Address Book</a><br/>
<a href=.>Public Address Directory</a><br/>
<h2><ib:message key="Addresses"/></h2>
<a href="identities.jsp"><ib:message key="Identities"/></a><br/>
<a href="addressBook.jsp"><ib:message key="Address Book"/></a><br/>
Public Address Directory<br/>
</div>
<div class="menubox">
<h2>Configuration</h2>
<a href=.>Settings</a><br/>
<h2><ib:message key="Configuration"/></h2>
<a href="settings.jsp"><ib:message key="Settings"/></a><br/>
</div>
<div class="menubox">
<h2>Network Status</h2>
<h2><a href="network.jsp"><ib:message key="Network Status"/></a></h2>
<iframe src="statusFrame.jsp" width="100%" height="40px" scrolling="no" frameborder="0" allowtransparency="true"></iframe>
</div>
<div class="menubox">
<h2>Help</h2>
<a href=.>User Guide</a><br/>
<a href="about.jsp">About</a><br/>
<h2><ib:message key="Help"/></h2>
<ib:message key="User Guide"/><br/>
<a href="about.jsp"><ib:message key="About"/></a><br/>
</div>
<div class="infoMessage">
${fn:escapeXml(param.infoMessage)}
</div>
<div class="errorMessage">
${fn:escapeXml(param.errorMessage)}
</div>

View File

@ -1,12 +1,12 @@
/* Based on the router console css */
body {
margin: 15px 0 0 10px;
padding: 0em;
text-align: center;
background: #eef;
color: #000;
font: 9pt/130% "Lucida Sans Unicode", "Bitstream Vera Sans", Verdana, Tahoma, Helvetica, sans-serif;
margin: 15px 0 0 10px;
padding: 0em;
text-align: center;
background: #eef;
color: #000;
font: 9pt/130% "Lucida Sans Unicode", "Bitstream Vera Sans", Verdana, Tahoma, Helvetica, sans-serif;
}
.titlebar {
@ -79,17 +79,10 @@ body {
wordwrap: none;
}
.statusbox table {
width: 100%;
}
.statusbox td {
text-align: left;
color: #000;
.statusbox {
font-size: 8pt;
vertical-align: top;
padding: 0px 30px 0px 0px;
width: 100%;
font-weight: bold;
text-align: left;
}
.main {
@ -108,6 +101,22 @@ body {
-moz-box-shadow: inset 0px 0px 1px 0px #002;
}
.emailtext {
width: 800px;
margin: 0px 20px 20px 240px;
padding: 0 15px 15px 25px;
background: #eef;
font: 9pt/130% "Lucida Sans Unicode", "Bitstream Vera Sans", Verdana, Tahoma, Helvetica, sans-serif;
text-align: left;
color: #001;
border: 1px solid #000033;
background: #cce2ff;
-moz-border-radius: 4px;
-khtml-border-radius: 4px;
border-radius: 4px;
-moz-box-shadow: inset 0px 0px 1px 0px #002;
}
.infoMessage {
margin: 0px 20px 5px 240px;
text-align: left;
@ -183,6 +192,13 @@ button:active {
-moz-box-shadow: inset 0px 0px 0px 1px #f60;
}
button[disabled] {
border: 1px outset #999;
background: #ddf !important;
color: #666;
-moz-box-shadow: inset 0px 0px 0px 1px #999;
}
.underline {
border-bottom: 1px solid #000022;
padding: 5px 0px 5px 0px;
@ -234,6 +250,16 @@ input:active {
}
input[type=text] {
background: #eef;
color: #001;
margin: 5px;
padding: 5px;
font: bold 8pt "Lucida Sans Unicode", "Bitstream Vera Sans", Verdana, Tahoma, Helvetica, sans-serif;
border: 1px solid #001;
text-decoration: none;
}
.widetextfield {
width: 100%;
background: #eef;
color: #001;
@ -257,11 +283,33 @@ textarea {
border-radius: 4px;
-moz-border-radius: 4px;
-khtml-border-radius: 4px;
font: 8pt "Lucida Console", "DejaVu Sans Mono", Courier, mono;
font: 9pt/130% "Lucida Sans Unicode", "Bitstream Vera Sans", Verdana, Tahoma, Helvetica, sans-serif;
min-height: 100px;
border: 1px solid #001;
}
.nobordertextarea {
width: 100%;
padding: 0px;
margin: 0px;
background: #eef;
background-color: transparent;
color: #003;
font: 9pt/130% "Lucida Sans Unicode", "Bitstream Vera Sans", Verdana, Tahoma, Helvetica, sans-serif;
min-height: 100px;
border: none;
}
.destinationtextarea {
width: 100%;
background: #eef;
background-color: transparent;
color: #003;
border: none;
font: 8pt "Lucida Console", "DejaVu Sans Mono", Courier, mono;
min-height: 100px;
}
submit {
background: #f00;
color: #eef;
@ -303,6 +351,18 @@ select {
overflow-x: hidden;
}
.addressbook table {
table-layout: fixed;
width: 800px;
}
.addressbook td, .addressbook th {
vertical-align: middle;
padding: 0px 30px 0px 0px;
white-space: nowrap;
overflow-x: hidden;
}
.ellipsis
{
line-height: 1.2em;

View File

@ -26,9 +26,8 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:include page="header.jsp">
<jsp:param name="title" value="Identities"/>
</jsp:include>
<c:set var="title" value="Identities" scope="request"/>
<jsp:include page="header.jsp"/>
<div class="infoMessage">
${param.infoMessage}
@ -36,27 +35,36 @@
<div class="main">
<h2>
Email Identities
<ib:message key="Email Identities"/>
</h2>
<c:if test="${empty ib:getIdentities().all}">
No email identities are defined.
<c:set var="identities" value="${ib:getIdentities().all}"/>
<c:if test="${empty identities}">
<ib:message key="No email identities are defined."/>
</c:if>
<div class="identities">
<table>
<tr>
<th>Description</th>
<th>Public Name</th>
<th>Email Address</th>
<th>Key</th>
<th style="width: 20px; padding: 0px"></th>
</tr>
<c:forEach items="${ib:getIdentities().all}" var="identity">
<c:if test="${!empty identities}">
<tr>
<th style="width: 20px;"><ib:message key="Def."/></th>
<th><ib:message key="Public Name"/></th>
<th><ib:message key="Description"/></th>
<th><ib:message key="Email Address"/></th>
<th><ib:message key="Key"/></th>
<th style="width: 20px; padding: 0px"></th>
</tr>
</c:if>
<c:forEach items="${identities}" var="identity">
<tr>
<td style="width: 20px; text-align: right;">
<c:if test="${identity.default}">
<img src="images/asterisk.png"/>
</c:if>
</td>
<td style="width: 100px;">
<div class="ellipsis">
<a href="editIdentity.jsp?new=false&key=${identity.key}&publicName=${identity.publicName}&description=${identity.description}&emailAddress=${identity.emailAddress}">
<a href="editIdentity.jsp?new=false&key=${identity.key}&publicName=${identity.publicName}&description=${identity.description}&emailAddress=${identity.emailAddress}&isDefault=${identity.default}">
${identity.publicName}
</a>
</div>
@ -77,7 +85,7 @@
</div>
</td>
<td>
<a href="deleteIdentity.jsp?key=${identity.key}"><img src="images/delete.png" alt="Delete" title="Delete this identity"/></a>
<a href="deleteIdentity.jsp?key=${identity.key}"><img src="images/delete.png" alt="<ib:message key='Delete'/>" title="<ib:message key='Delete this identity'/>"/></a>
</td>
</tr>
</c:forEach>
@ -86,7 +94,7 @@
<p/>
<form action="editIdentity.jsp?new=true" method="POST">
<button type="submit" value="New">New Identity</button>
<button type="submit" value="<ib:message key='New'/>"><ib:message key="New Identity"/></button>
</form>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

7
WebContent/index.html Normal file
View File

@ -0,0 +1,7 @@
<html>
<head>
<meta http-equiv="refresh" content="0;url=index.jsp" />
</head>
<body>
</body>
</html>

View File

@ -22,8 +22,5 @@
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:forward page="folder.jsp?path=Inbox"/>

46
WebContent/network.jsp Normal file
View File

@ -0,0 +1,46 @@
<%--
Copyright (C) 2009 HungryHobo@mail.i2p
The GPG fingerprint for HungryHobo@mail.i2p is:
6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
This file is part of I2P-Bote.
I2P-Bote 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 3 of the License, or
(at your option) any later version.
I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:include page="getStatus.jsp"/>
<ib:message key="Network" var="title" scope="request"/>
<jsp:include page="header.jsp"/>
<div class="main">
<c:choose>
<c:when test="${connStatus==NOT_STARTED || connStatus==DELAY}">
<strong><ib:message key="Network information is not available because I2P-Bote hasn't started connecting to the network yet."/></strong>
</c:when>
<c:otherwise>
<strong><ib:message key="Local destination:"/> </strong><ib:localDestination/><p/><br/>
<ib:peerInfo/>
</c:otherwise>
</c:choose>
</div>
<jsp:include page="footer.jsp"/>

View File

@ -24,24 +24,43 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:include page="header.jsp">
<jsp:param name="title" value="New Email"/>
</jsp:include>
<c:choose>
<c:when test="${param.action eq 'send'}">
<jsp:forward page="sendEmail.jsp"/>
</c:when>
<c:when test="${fn:startsWith(param.action, 'lookup')}">
<jsp:forward page="addressBook.jsp">
<jsp:param name="search" value="true"/>
<jsp:param name="action" value=""/>
</jsp:forward>
</c:when>
</c:choose>
<ib:message key="New Email" var="title"scope="request"/>
<jsp:include page="header.jsp"/>
<div class="main">
<form action="sendEmail.jsp">
<form action="newEmail.jsp" method="post">
<table>
<tr>
<td>
From:
<ib:message key="From:"/>
</td>
<td>
<select name="sender">
<option value="anonymous">Anonymous</option>
<c:forEach items="${ib:getIdentities().all}" var="identity">
<option value="${identity.key}">
<c:set var="selected" value=""/>
<c:if test="${fn:contains(param.sender, identity.key)}">
<c:set var="selected" value=" selected"/>
</c:if>
<c:if test="${empty param.sender && identity.default}">
<c:set var="selected" value=" selected"/>
</c:if>
<option value="${identity.publicName} &lt;${identity.key}&gt;"${selected}>
${identity.publicName}
<c:if test="${!empty identity.description}"> - ${identity.description}</c:if>
</option>
@ -49,32 +68,87 @@
</select>
</td>
</tr>
<c:set var="maxRecipientIndex" value="-1"/>
<c:forEach var="parameter" items="${ib:getRecipients(param)}">
<c:set var="isRecipient" value="${fn:startsWith(parameter.key, 'recipient') and !fn:contains(parameter.key, 'Type')}"/>
<c:if test="${isRecipient && !empty parameter.value}">
<c:set var="recipientField" value="${parameter.key}"/>
<c:set var="recipient" value="${parameter.value}"/>
<c:set var="recipientIndex" value="${fn:substringAfter(recipientField, 'recipient')}"/>
<c:if test="${recipientIndex gt maxRecipientIndex}">
<c:set var="maxRecipientIndex" value="${recipientIndex}"/>
</c:if>
<tr><td>
<c:set var="recipientTypeField" value="recipientType${recipientIndex}"/>
<c:set var="recipientType" value="${param[recipientTypeField]}"/>
<select name="${recipientTypeField}">
<c:set var="toSelected" value="${recipientType eq 'to' ? ' selected' : ''}"/>
<c:set var="ccSelected" value="${recipientType eq 'cc' ? ' selected' : ''}"/>
<c:set var="bccSelected" value="${recipientType eq 'bcc' ? ' selected' : ''}"/>
<c:set var="replytoSelected" value="${recipientType eq 'replyto' ? ' selected' : ''}"/>
<option value="to"${toSelected}><ib:message key="To:"/></option>
<option value="cc"${ccSelected}><ib:message key="CC:"/></option>
<option value="bcc"${bccSelected}><ib:message key="BCC:"/></option>
<option value="replyto"${replytoSelected}><ib:message key="Reply To:"/></option>
</select>
</td><td>
<input type="text" size="80" name="${recipientField}" value="${recipient}"/>
</td></tr>
</c:if>
</c:forEach>
<c:if test="${!empty param.selectedContact}">
<c:forEach var="destination" items="${paramValues.selectedContact}">
<c:set var="maxRecipientIndex" value="${maxRecipientIndex+1}"/>
<tr><td>
<select name="recipientType${maxRecipientIndex}">
<option value="to"><ib:message key="To:"/></option>
<option value="cc"><ib:message key="CC:"/></option>
<option value="bcc"><ib:message key="BCC:"/></option>
<option value="replyto"><ib:message key="Reply To:"/></option>
</select>
</td><td>
<input type="text" size="80" name="recipient${maxRecipientIndex}" value="${destination}"/>
</td></tr>
</c:forEach>
</c:if>
<c:set var="maxRecipientIndex" value="${maxRecipientIndex+1}"/>
<tr><td>
<select name="recipientType${maxRecipientIndex}">
<option value="to"><ib:message key="To:"/></option>
<option value="cc"><ib:message key="CC:"/></option>
<option value="bcc"><ib:message key="BCC:"/></option>
<option value="replyto"><ib:message key="Reply To:"/></option>
</select>
</td><td>
<input class="widetextfield" type="text" size="80" name="recipient${maxRecipientIndex}"/>
</td></tr>
<tr>
<td>
<select name="recipientType0">
<option value="to">To:</option>
<option value="cc">CC:</option>
<option value="bcc">BCC:</option>
<option value="reply_to">Reply To:</option>
</select>
</td>
<td>
<input type="text" size="80" name="recipient0"/>
<td/>
<td style="text-align: right;">
<button type="submit" name="action" value="addRecipient" disabled="disabled">+</button>
<button type="submit" name="action" value="lookup0"><ib:message key="Addr. Book..."/></button>
</td>
</tr>
<tr>
<td valign="top"><br/>Subject:</td>
<td><input type="text" size="80" name="subject"/></td>
<td valign="top"><br/><ib:message key="Subject:"/></td>
<td><input class="widetextfield" type="text" size="80" name="subject" value="${param.subject}"/></td>
</tr>
<tr>
<td valign="top"><br/>Message:</td>
<td><textarea rows="30" cols="80" name="message"></textarea></td>
<td valign="top"><br/><ib:message key="Message:"/></td>
<td>
<textarea rows="30" cols="80" name="message"><c:if test="${!empty param.quoteMsgId}">
<%-- The following lines are not indented because the indentation would show up as blank chars on the textarea --%>
<c:set var="origEmail" value="${ib:getEmail(param.quoteMsgFolder, param.quoteMsgId)}"/>
<ib:shortSenderName sender="${origEmail.sender}"/> wrote:
<ib:quote text="${origEmail.text}"/></c:if><c:if test="${empty param.quoteMsgId}">${param.message}</c:if></textarea>
</td>
</tr>
<tr>
<td colspan=2 align="center">
<input type="submit" value="Send"/>
<input type="submit" value="Cancel"/>
<input type="submit" value="Save"/>
<td colspan=3 align="center">
<button type="submit" name="action" value="send"><ib:message key="Send"/></button>
<button type="submit" name="action" disabled="disabled"><ib:message key="Save"/></button>
</td>
</tr>
</table>

View File

@ -23,9 +23,8 @@
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<jsp:include page="header.jsp">
<jsp:param name="title" value="No Identity"/>
</jsp:include>
<c:set var="title" value="No Identity" scope="request"/>
<jsp:include page="header.jsp"/>
<div class="main">
<h2>No Email Identity Defined</h2>

View File

@ -0,0 +1,55 @@
<%--
Copyright (C) 2009 HungryHobo@mail.i2p
The GPG fingerprint for HungryHobo@mail.i2p is:
6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
This file is part of I2P-Bote.
I2P-Bote 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 3 of the License, or
(at your option) any later version.
I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<c:if test="${param.action == 'Cancel'}">
<jsp:forward page="addressBook.jsp"/>
</c:if>
<c:choose>
<c:when test="${empty param.destination}">
<ib:message key="Please fill in the Destination field." var="errorMessage"/>
</c:when>
<c:when test="${empty param.name}">
<ib:message key="Please fill in the Name field." var="errorMessage"/>
</c:when>
<c:otherwise>
<c:set var="errorMessage" value="${ib:saveContact(param.destination, param.name)}"/>
</c:otherwise>
</c:choose>
<c:if test="${empty errorMessage}">
<ib:message key="The contact has been saved." var="infoMessage"/>
<jsp:forward page="addressBook.jsp">
<jsp:param name="infoMessage" value="${infoMessage}"/>
</jsp:forward>
</c:if>
<c:if test="${!empty errorMessage}">
<jsp:forward page="editContact.jsp">
<jsp:param name="errorMessage" value="${errorMessage}"/>
</jsp:forward>
</c:if>

View File

@ -26,23 +26,23 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<c:if test="${param.action == 'Cancel'}">
<jsp:forward page="identities.jsp"/>
</c:if>
<c:choose>
<c:when test="${empty param.publicName}">
<c:set var="errorMessage" value="Please fill in the Public Name field."/>
<ib:message key="Please fill in the Public Name field." var="errorMessage"/>
</c:when>
<c:otherwise>
<c:set var="errorMessage" value="${ib:saveIdentity(param.key, param.publicName, param.description, param.emailAddress)}"/>
<c:set var="errorMessage" value="${ib:saveIdentity(param.key, param.publicName, param.description, param.emailAddress, param.isDefault=='on')}"/>
</c:otherwise>
</c:choose>
<c:if test="${empty errorMessage}">
<ib:message key="The email identity has been saved." var="infoMessage"/>
<jsp:forward page="identities.jsp">
<jsp:param name="infoMessage" value="The email identity has been saved."/>
<jsp:param name="infoMessage" value="${infoMessage}"/>
</jsp:forward>
</c:if>
<c:if test="${!empty errorMessage}">

View File

@ -25,9 +25,8 @@
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:include page="header.jsp">
<jsp:param name="title" value="New Email"/>
</jsp:include>
<ib:message key="New Email" var="title" scope="request"/>
<jsp:include page="header.jsp"/>
<ib:sendEmail sender="${param.sender}" recipient="${param.recipient0}" subject="${param.subject}" message="${param.message}" />

58
WebContent/settings.jsp Normal file
View File

@ -0,0 +1,58 @@
<%--
Copyright (C) 2009 HungryHobo@mail.i2p
The GPG fingerprint for HungryHobo@mail.i2p is:
6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
This file is part of I2P-Bote.
I2P-Bote 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 3 of the License, or
(at your option) any later version.
I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<ib:message key="Settings" var="title" scope="request"/>
<jsp:include page="header.jsp"/>
<div class="main">
<h2>
<ib:message key="Settings"/>
</h2>
<br/>
<jsp:useBean id="jspHelperBean" class="i2p.bote.web.JSPHelper"/>
<c:set var="configuration" value="${jspHelperBean.configuration}"/>
<c:if test="${param.action eq 'save'}">
<jsp:setProperty name="configuration" property="autoMailCheckEnabled" value="${param.autoMailCheckEnabled eq 'on' ? 'true' : 'false'}"/>
<jsp:setProperty name="configuration" property="mailCheckInterval" value="${param.mailCheckInterval}"/>
<ib:saveConfiguration/>
</c:if>
<form action="settings.jsp" method="post">
<input type="hidden" name="action" value="save"/>
<c:set var="autoMailCheckEnabled" value="${jspHelperBean.configuration.autoMailCheckEnabled}"/>
<c:set var="checked" value="${autoMailCheckEnabled ? ' checked' : ''}"/>
<input type="checkbox"${checked} name="autoMailCheckEnabled"/>Check for mail every
<input type="text" name="mailCheckInterval" size="3" value="${configuration.mailCheckInterval}"/> minutes
<p/>
<button type="submit"><ib:message key="Save"/></button>
</form>
</div>
<jsp:include page="footer.jsp"/>

View File

@ -29,30 +29,71 @@
<c:set var="email" value="${ib:getEmail(param.folder, param.messageID)}"/>
<jsp:include page="header.jsp">
<jsp:param name="title" value="${email.subject}"/>
</jsp:include>
<ib:setEmailRead folder="${ib:getMailFolder(param.folder)}" messageId="${param.messageID}" read="true"/>
<div class="main">
<c:set var="title" value="${email.subject}" scope="request"/>
<jsp:include page="header.jsp"/>
<div class="emailtext">
<table>
<tr>
<td valign="top"><strong>From:</strong></td>
<td valign="top"><strong><ib:message key="From:"/></strong></td>
<td>
<c:forEach var="i" begin="0" end="${fn:length(email.sender)}" step="64">
<c:if test="${i > 0}">
<br/>
</c:if>
${fn:substring(email.sender, i, i+64)}
<ib:address address="${email.sender}"/>
<c:set var="senderDestination" value="${ib:extractEmailDestination(email.sender)}"/>
<c:if test="${!empty senderDestination}">
<form action="editContact.jsp?new=true&destination=${senderDestination}&name=${ib:extractName(email.sender)}" method="POST">
<c:set var="disabled" value="${empty ib:getContact(senderDestination) ? '' : 'disabled=&quot; disabled&quot; title=&quot;The Email Destination already exists in the address book.&quot;'}"/>
<button type="submit"${disabled}><ib:message key="Add to Address Book"/></button>
</form>
</c:if>
</td>
</tr>
<tr>
<td valign="top"><strong><ib:message key="To:"/></strong></td>
<td>
<c:forEach var="recipient" varStatus="status" items="${email.allRecipients}">
<ib:address address="${recipient}"/>
<c:if test="${!status.last}">,<p/></c:if>
</c:forEach>
</td>
</tr>
<tr>
<td valign="top"><strong>Subject:</strong></td>
<td>${email.subject}</td>
<td valign="top"><strong><ib:message key="Subject:"/></strong></td>
<td>${fn:escapeXml(email.subject)}</td>
</tr>
<tr>
<td valign="top"><strong>Message:</strong></td>
<td>${email.bodyText}</td>
<td valign="top"><strong><ib:message key="Message:"/></strong></td>
<td><ib:formatPlainText text="${fn:escapeXml(email.text)}"/></td>
</tr>
<tr>
<td colspan="2">
<table><tr>
<td>
<form action="newEmail.jsp" method="post">
<button type="submit"><ib:message key="Reply"/></button>
<input type="hidden" name="sender" value="${ib:getOneLocalRecipient(email)}"/>
<input type="hidden" name="recipient0" value="${email.sender}"/>
<ib:message key="Re:" var="responsePrefix"/>
<c:set var="responsePrefix" value="${responsePrefix} "/>
<c:if test="${fn:startsWith(email.subject, responsePrefix)}">
<c:set var="responsePrefix" value=""/>
</c:if>
<input type="hidden" name="subject" value="${responsePrefix}${email.subject}"/>
<input type="hidden" name="quoteMsgFolder" value="${param.folder}"/>
<input type="hidden" name="quoteMsgId" value="${param.messageID}"/>
</form>
</td><td>
<form action="deleteEmail.jsp" method="post">
<button type="submit"><ib:message key="Delete"/></button>
<input type="hidden" name="folder" value="${param.folder}"/>
<input type="hidden" name="messageID" value="${email.messageID}"/>
</form>
</td>
</tr></table>
</td>
</tr>
</table>
</div>

View File

@ -26,6 +26,8 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ib" uri="I2pBoteTags" %>
<jsp:include page="getStatus.jsp"/>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
@ -36,11 +38,20 @@
<body style="background-color: transparent; margin: 0px;">
<div class="statusbox">
<table><tr>
<td><strong>K-Peers:</strong></td><td style="text-align: right"><ib:numDhtPeers/></td>
<%-- </tr><tr>
<td><strong>R-Peers:</strong></td><td style="text-align: right"><ib:numRelayPeers/></td> --%>
</tr></table>
<c:choose>
<c:when test="${connStatus == NOT_STARTED}"><img src="images/redsquare.png"/> <ib:message key="Not Started"/></c:when>
<c:when test="${connStatus == DELAY}"><img src="images/yellowsquare.png"/> <ib:message key="Waiting 3 Minutes..."/><br/>
<div style="text-align: center">
<form action="connect.jsp" target="_top" method="GET">
<button type="submit"><ib:message key="Connect Now"/></button>
</form>
</div>
</c:when>
<c:when test="${connStatus == CONNECTING}"><img src="images/yellowsquare.png"/> <ib:message key="Connecting..."/></c:when>
<c:when test="${connStatus == CONNECTED}"><img src="images/greensquare.png"/> <ib:message key="Connected"/></c:when>
<c:when test="${connStatus == ERROR}"><img src="images/redsquare.png"/> <ib:message key="Error"/></c:when>
<c:otherwise> <ib:message key="Unknown Status"/></c:otherwise>
</c:choose>
</div>
</body>

View File

@ -0,0 +1,10 @@
package i2p.bote.ant;
import i2p.bote.I2PBote;
public class PrintAppVersion {
public static void main(String[] args) {
System.out.println(I2PBote.getAppVersion());
}
}

240
build.xml
View File

@ -1,35 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="all" name="I2P-Bote">
<property name="i2plib" value="/opt/i2p/lib"/>
<property name="jstllib" value="WebContent/WEB-INF/lib"/>
<property name="jettylib" value="${i2plib}"/>
<property name="antlib" value="/opt/ant/lib"/>
<project basedir="." default="help" name="I2P-Bote">
<property name="i2pbase" value="../i2p"/>
<property name="i2plib" value="${i2pbase}/core/java/build"/>
<property name="jettylib" value="${i2pbase}/apps/jetty/jettylib"/>
<property name="antlib" value="${jettylib}"/>
<property name="jstllib" value="${i2pbase}/apps/susidns/src/WEB-INF/lib"/>
<property name="lib" value="WebContent/WEB-INF/lib"/>
<property name="makeplugin" value="../i2p.scripts/plugin/makeplugin.sh"/>
<path id="cp">
<pathelement path="${classpath}" />
<pathelement path="${java.class.path}" />
<pathelement location="${i2plib}/i2p.jar" />
<pathelement location="${jstllib}/jstl.jar" />
<pathelement location="${jstllib}/standard.jar" />
<pathelement location="${jettylib}/org.mortbay.jetty.jar"/>
<pathelement location="${jettylib}/jasper-compiler.jar" />
<pathelement location="${jettylib}/jasper-runtime.jar" />
<pathelement location="${jettylib}/javax.servlet.jar" />
<pathelement location="${jettylib}/commons-logging.jar" />
<pathelement location="${jettylib}/commons-el.jar" />
<pathelement location="${antlib}/ant.jar" />
<pathelement location="${jstllib}/jstl.jar" />
<pathelement location="${jstllib}/standard.jar" />
<pathelement location="${lib}/mailapi.jar" />
</path>
<target name="all" depends="clean, war" />
<target name="help">
<echo message="Useful targets:" />
<echo message=" war: Makes a .war file" />
<echo message=" plugin: Makes a I2P plugin. Only runs on Linux;" />
<echo message=" $I2P must point to an I2P installation." />
<echo message=" all: war + plugin" />
<echo message=" junit: Runs all unit tests" />
</target>
<target name="all" depends="clean, war, plugin" />
<target name="compile">
<mkdir dir="./ant_build" />
<mkdir dir="./ant_build/classes" />
<javac
srcdir="./src"
debug="true" deprecation="on" source="1.5" target="1.5"
debug="true"
deprecation="on"
destdir="./ant_build/classes"
classpathref="cp" >
<compilerarg line="" />
</javac>
classpathref="cp"
failonerror="true" />
</target>
<target name="precompilejsp">
@ -51,48 +63,220 @@
<arg value="WebContent" />
</java>
<javac debug="true" deprecation="on" source="1.5" target="1.5"
destdir="ant_build/classes" srcdir="./ant_build/jspjava"
includes="**/*.java" classpathref="jspcp">
<compilerarg line="" />
</javac>
<copy file="WebContent/WEB-INF/web-template.xml" tofile="ant_build/web.xml" />
<javac
debug="true"
deprecation="on"
destdir="ant_build/classes"
srcdir="./ant_build/jspjava"
includes="**/*.java"
classpathref="jspcp"
failonerror="true" />
<copy file="WebContent/WEB-INF/web.xml" tofile="ant_build/web.xml" />
<loadfile property="jspc.web.fragment" srcfile="ant_build/web-fragment.xml" />
<replace file="ant_build/web.xml">
<replacefilter token="&lt;!-- precompiled servlets --&gt;" value="${jspc.web.fragment}" />
</replace>
</target>
<target name="war" depends="compile,precompilejsp">
<target name="war" depends="compile,precompilejsp,bundle">
<mkdir dir="ant_build" />
<war destfile="i2pbote.war" webxml="ant_build/web.xml">
<classes dir="ant_build/classes" includes="**/*.class" />
<classes dir="src" includes="i2p/bote/network/kademlia/built-in-peers.txt" />
<fileset dir="WebContent" includes="*.jsp"/>
<fileset dir="WebContent" includes="index.html"/>
<fileset dir="WebContent" includes="*.css"/>
<fileset dir="WebContent/" includes="*.xml"/>
<webinf dir="WebContent/WEB-INF/tlds" includes="*.tld"/>
<lib file="${jstllib}/jstl.jar"/>
<lib file="${jstllib}/standard.jar"/>
<lib file="${lib}/mailapi.jar"/>
<zipfileset dir="WebContent/images" prefix="images"/>
</war>
<echo message="SHA256 sum:"/>
<exec executable="sha256sum">
<arg value="i2pbote.war"/>
</exec>
</target>
<!--
- Make two plugins, one for initial installs and one for updates.
- Only the initial install includes mailapi.jar.
- Neither includes jstl.jar or standard.jar, as any i2p version that has
- plugin support has these two jars pulled out of susidns.war and put in
- $I2P/lib. We set the classpath in webapps.config to find them.
-->
<target name="plugin" depends="pluginwar,getversion">
<mkdir dir="plugin/plugin.tmp/lib" />
<mkdir dir="plugin/plugin.tmp/console/webapps" />
<copy file="plugin/webapps.config" todir="plugin/plugin.tmp/console/" />
<!-- run i2pbote-plugin.war through pack200 -->
<move file="i2pbote-plugin.war" tofile="plugin/plugin.tmp/console/webapps/i2pbote.jar" />
<exec executable="pack200" failifexecutionfails="true">
<arg value="--no-gzip"/>
<arg value="--effort=9"/>
<arg value="--modification-time=latest"/>
<arg value="plugin/plugin.tmp/console/webapps/i2pbote.war.pack"/>
<arg value="plugin/plugin.tmp/console/webapps/i2pbote.jar"/>
</exec>
<delete file="plugin/plugin.tmp/console/webapps/i2pbote.jar" />
<delete file="plugin/plugin.tmp/lib/mailapi.jar" />
<!-- get build number -->
<buildnumber file="plugin/build.number" />
<!-- make the update xpi2p -->
<copy file="plugin/plugin.config" todir="plugin/plugin.tmp" overwrite="true" />
<exec executable="echo" osfamily="unix" failifexecutionfails="true" output="plugin/plugin.tmp/plugin.config" append="true">
<arg value="version=${version}-b${build.number}" />
</exec>
<exec executable="echo" osfamily="unix" failifexecutionfails="true" output="plugin/plugin.tmp/plugin.config" append="true">
<arg value="update-only=true" />
</exec>
<exec executable="${makeplugin}" failifexecutionfails="true" >
<arg value="plugin/plugin.tmp" />
</exec>
<move file="i2pbote.xpi2p" tofile="i2pbote-update.xpi2p" />
<!-- make the install xpi2p -->
<exec executable="pack200" failifexecutionfails="true">
<arg value="--no-gzip"/>
<arg value="--effort=9"/>
<arg value="--modification-time=latest"/>
<arg value="plugin/plugin.tmp/lib/mailapi.jar.pack"/>
<arg value="${lib}/mailapi.jar"/>
</exec>
<copy file="plugin/plugin.config" todir="plugin/plugin.tmp" overwrite="true" />
<exec executable="echo" osfamily="unix" failifexecutionfails="true" output="plugin/plugin.tmp/plugin.config" append="true">
<arg value="version=${version}-b${build.number}" />
</exec>
<exec executable="${makeplugin}" >
<arg value="plugin/plugin.tmp" />
</exec>
</target>
<!-- same as war but without the library jars -->
<target name="pluginwar" depends="compile,precompilejsp,bundle">
<mkdir dir="ant_build" />
<war destfile="i2pbote-plugin.war" webxml="ant_build/web.xml">
<classes dir="ant_build/classes" includes="**/*.class" />
<classes dir="src" includes="i2p/bote/network/kademlia/built-in-peers.txt" />
<fileset dir="WebContent" includes="index.html"/>
<fileset dir="WebContent" includes="*.css"/>
<fileset dir="WebContent/" includes="*.xml"/>
<webinf dir="WebContent/WEB-INF/tlds" includes="*.tld"/>
<zipfileset dir="WebContent/images" prefix="images"/>
</war>
</target>
<target name="bundle">
<!-- Update the messages_*.po files.
We need to supply the bat file for windows, and then change the fai
l property to true -->
<exec executable="sh" osfamily="unix" failifexecutionfails="false" >
<arg value="./bundle-messages.sh" />
</exec>
<exec executable="sh" osfamily="mac" failifexecutionfails="false" >
<arg value="./bundle-messages.sh" />
</exec>
<exec executable="cmd" osfamily="windows" failifexecutionfails="false" >
<arg value="/c" />
<arg value="bundle-messages.bat" />
</exec>
</target>
<target name="poupdate">
<!-- Update the messages_*.po files. -->
<exec executable="sh" osfamily="unix" failifexecutionfails="true" >
<arg value="./bundle-messages.sh" />
<arg value="-p" />
</exec>
<exec executable="sh" osfamily="mac" failifexecutionfails="true" >
<arg value="./bundle-messages.sh" />
<arg value="-p" />
</exec>
<exec executable="cmd" osfamily="windows" failifexecutionfails="true" >
<arg value="/c" />
<arg value="bundle-messages.bat" />
<arg value="-p" />
</exec>
</target>
<target name="junit" depends="compile">
<junit printsummary="yes">
<path id="junitcp">
<pathelement location="${lib}/junit.jar"/>
<pathelement location="./ant_build/classes"/>
<path refid="cp"/>
</path>
<javac
srcdir="./test"
debug="true"
deprecation="on"
destdir="./ant_build/classes"
classpathref="junitcp"
failonerror="true" />
<junit printsummary="off">
<classpath>
<path refid="classpath"/>
<path refid="application"/>
<path refid="junitcp"/>
</classpath>
<batchtest fork="yes">
<fileset dir="${src.dir}" includes="*Test.java"/>
<batchtest fork="true" todir="./ant_build">
<fileset dir="./ant_build/classes" includes="**/*Test.class"/>
<formatter type="plain"/>
</batchtest>
</junit>
</target>
<!-- Write the app version into ${version} -->
<target name="getversion" depends="compile">
<javac
srcdir="./ant"
debug="true"
deprecation="on"
destdir="./ant_build/classes"
classpathref="cp"
classpath="./ant_build/classes"
failonerror="true" />
<java
classname="i2p.bote.ant.PrintAppVersion"
classpathref="cp"
classpath="./ant_build/classes"
outputproperty="version"
failonerror="true"/>
<delete dir="./ant_build/classes/i2p/bote/ant" />
<echo message="I2P-Bote version is ${version}"/>
</target>
<!-- depends on compile b/c it gets the app version from I2PBote.class -->
<target name="src" depends="getversion">
<property name="subdir" value="i2pbote-${version}-src"/>
<zip destfile="src.zip">
<zipfileset dir="src" prefix="${subdir}/src"/>
<zipfileset dir="WebContent" prefix="${subdir}/WebContent"/>
<zipfileset dir="test" prefix="${subdir}/test"/>
<zipfileset dir="ant" prefix="${subdir}/ant"/>
<zipfileset dir="doc" prefix="${subdir}/doc"/>
<zipfileset file="build.xml" prefix="${subdir}"/>
<zipfileset file="history.txt" prefix="${subdir}"/>
<zipfileset file="license.txt" prefix="${subdir}"/>
</zip>
<echo message="SHA256 sum:"/>
<exec executable="sha256sum">
<arg value="src.zip"/>
</exec>
</target>
<target name="clean">
<delete dir="ant_build" />
<delete file="i2pbote.war" />
<delete file="i2pbote-plugin.war" />
<delete dir="plugin/plugin.tmp" />
<delete file="i2pbote.xpi2p" />
<delete file="i2pbote-update.xpi2p" />
<delete file="src.zip" />
</target>
</project>

92
bundle-messages.sh Executable file
View File

@ -0,0 +1,92 @@
#
# Update messages_xx.po and messages_xx.class files,
# from both java and jsp sources.
# Requires installed programs xgettext, msgfmt, msgmerge, find,
# plus everything jsp2po.sh depends on: grep, sed, awk, sort, and bash.
#
# usage:
# bundle-messages.sh (generates the resource bundle from the .po file)
# bundle-messages.sh -p (updates the .po file from the source tags, then generates the resource bundle)
#
# zzz - public domain
#
CLASS=i2p.bote.locale.Messages
TMPFILE=ant_build/javafiles.txt
export TZ=UTC
if [ "$1" = "-p" ]
then
POUPDATE=1
fi
# add ../java/ so the refs will work in the po file
JPATHS="src"
JSPPATHS="WebContent"
for i in locale/messages_*.po
do
# get language
LG=${i#locale/messages_}
LG=${LG%.po}
if [ "$POUPDATE" = "1" ]
then
# make list of java files newer than the .po file
find $JPATHS -name *.java -newer $i > $TMPFILE
find $JSPPATHS -name *.java -newer $i >> $TMPFILE
fi
if [ -s ant_build/classes/i2p/bote/locale/messages_$LG.class -a \
ant_build/classes/i2p/bote/locale/messages_$LG.class -nt $i -a \
! -s $TMPFILE ]
then
continue
fi
if [ "$POUPDATE" = "1" ]
then
echo "Updating the $i file from the tags..."
# extract strings from java and jsp files, and update messages.po files
# translate calls must be one of the forms:
# _("foo")
# _x("foo")
# To start a new translation, copy the header from an old translation to the new .po file,
# then ant distclean poupdate.
find $JPATHS -name *.java > $TMPFILE
xgettext -f $TMPFILE -F -L java --from-code=UTF-8 \
--keyword=_ --keyword=_x \
-o ${i}t
if [ $? -ne 0 ]
then
echo 'Warning - xgettext failed, not updating translations'
rm -f ${i}t
break
fi
# extract strings from jsp files
./jsp2po.sh $JSPPATHS >> ${i}t
msgmerge -U --backup=none $i ${i}t
if [ $? -ne 0 ]
then
echo 'Warning - msgmerge failed, not updating translations'
rm -f ${i}t
break
fi
rm -f ${i}t
# so we don't do this again
touch $i
fi
echo "Generating ${CLASS}_$LG ResourceBundle..."
# convert to class files in ant_build/classes
msgfmt --java --statistics -r $CLASS -l $LG -d ant_build/classes $i
if [ $? -ne 0 ]
then
echo 'Warning - msgfmt failed, not updating translations'
break
fi
done
rm -f $TMPFILE
# todo: return failure
exit 0

View File

@ -76,13 +76,43 @@ Below is a diagram of how an email packet is routed from a sender to a recipient
`-------' `-------' `---------'
The number of relays for sending or retrieving an email is user-configurable. For higher performance (but
reduced anonymity), it is possible to use no relays, no storers/fetchers, i.e. sender and recipient
talk to the storage nodes directly.
The number of relays for sending or retrieving an email is user-configurable.
TODO explain how an index packet is relayed back to the recipient, who then uses another chain to get the email packets
referenced in the index packet.
For higher performance (but reduced anonymity), it is possible to use no relays, no storers/fetchers,
i.e. sender and recipient talk to the storage nodes directly. If both sender and recipient chose not to
use relays, the diagram looks like this:
.--------.
| Sender | ----------------------------.
`--------' `\
|
V
.--------------- Kademlia DHT -------------------------.
| .---------. |
| .---------. | Storage | |
| | Storage | .---------. | Node | |
| | Node | | Storage | `---------' |
| `---------' | Node | |
| `---------' .---------. |
| .---------. .---------. | Storage | |
| | Storage | | Storage | .---------. | Node | |
| | Node | | Node | | Storage | `---------' |
| `---------' `---------' | Node | |
| `---------' |
`------------------------------------------------------'
|
|
.-----------. _'
| Recipient | <-------------------------'
`-----------'
The code is licensed under the GPL. The author can be reached at HungryHobo@mail.i2p, either in
German or English.
@ -112,7 +142,7 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
Relay Request | | | Request to a node to forward a Relay Packet
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'Y'
| VER | 1 byte | Format version (only ver. 1 is supported)
| VER | 1 byte | Protocol version
| PID | 32 bytes | Packet ID, used for delivery confirmation
| HLEN | 2 bytes | HashCash length
| HK | byte[] | HashCash token (HLEN bytes)
@ -122,7 +152,7 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
Fetch Request | | | Request to a chain end point to fetch emails
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'T'
| VER | 1 byte | Format version (only ver. 1 is supported)
| VER | 1 byte | Protocol version
| PID | 32 bytes | Packet ID, used for delivery confirmation
| DTYP | 1 byte | Type of data to retrieve:
| KEY | 32 bytes | DHT key to look up
@ -134,7 +164,7 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
| | | Find Close Peers Request, or a Relay List Request
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'N'
| VER | 1 byte | Format version (only ver. 1 is supported)
| VER | 1 byte | Protocol version
| PID | 32 bytes | Packet Id of the request packet
| STA | 1 byte | Status code:
| | | 0 = OK
@ -147,30 +177,46 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
| DLEN | 2 bytes | Length of DATA field; can be 0 if no payload
| DATA | byte[] | A Payload Packet
---------------------+-------+------------+----------------------------------------------------
Delete Request | | | Request to delete all copies of a packet by DHT key
Email Packet | | | Request to delete an Email Packet by DHT key
Delete Request | | |
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'D'
| VER | 1 byte | Format version (only ver. 1 is supported)
| KEY | 32 bytes | DHT key of the packet that is to be deleted
| DEL | 32 bytes | Deletion key, encrypts to DEL value in the email pkt
| VER | 1 byte | Protocol version
| KEY | 32 bytes | DHT key of the Email Packet to delete
| DEL | 32 bytes | Deletion key, must match DEL value in the email pkt
---------------------+-------+------------+----------------------------------------------------
Index Packet | | | Request to remove one or more entries (Email
Delete Request | | | Packet key / del key pairs) from an Index Packet
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'X'
| VER | 1 byte | Protocol version
| DH | 32 bytes | The Email Destination hash of the Index Packet
| N | 1 byte | Number of entries in the packet
| DHT1 | 32 bytes | First DHT key to remove
| DEL1 | 32 bytes | Deletion key for DHT1
| DHT2 | 32 bytes | Second DHT key to remove
| DEL2 | 32 bytes | Deletion key for DHT2
| ... | ... | ...
| DHTn | 32 bytes | n-th DHT key to remove
| DELn | 32 bytes | Deletion key for DHTn
---------------------+-------+------------+-------------------------------------------------
Ping | | | Check to see if a host is still up.
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'P'
| VER | 1 byte | Format version (only ver. 1 is supported)
| VER | 1 byte | Protocol version
| PID | 32 bytes | Packet ID
---------------------+-------+------------+-------------------------------------------------
Pong | | | Let the the pinger know we're still there.
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'O'
| VER | 1 byte | Format version (only ver. 1 is supported)
| TYPE | 1 byte | Value = 'O' (the letter O)
| VER | 1 byte | Protocol version
| PID | 32 bytes | Packet ID of the corresponding Ping packet
| HKS | 1 byte | Minimum HashCash strength this node will accept
---------------------+-------+------------+----------------------------------------------------
Relay List Request | | | "Send me your list of relay peers"
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'A'
| VER | 1 byte | Format version (only ver. 1 is supported)
| VER | 1 byte | Protocol version
| PID | 32 bytes | Packet ID
---------------------+-------+------------+----------------------------------------------------
@ -181,7 +227,7 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
Retrieve Request | | | DHT Request for a value for a key
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'Q'
| VER | 1 byte | Format version (only ver. 1 is supported)
| VER | 1 byte | Protocol version
| PID | 32 bytes | Packet ID, used for delivery confirmation
| DTYP | 1 byte | Type of data to retrieve:
| | | 'I' = Index Packet
@ -192,7 +238,7 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
Store Request | | | DHT Store Request
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'S'
| VER | 1 byte | Format version (only ver. 1 is supported)
| VER | 1 byte | Protocol version
| PID | 32 bytes | Packet ID, used for delivery confirmation
| HLEN | 2 bytes | HashCash length
| HK | byte[] | HashCash token (HLEN bytes)
@ -203,7 +249,7 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
Find Close Peers | | | Request for k peers close to a key
| PFX | 4 bytes | Packet prefix, must be 0x6D 0x30 0x52 0xE9
| TYPE | 1 byte | Value = 'F'
| VER | 1 byte | Format version (only ver. 1 is supported)
| VER | 1 byte | Protocol version
| PID | 32 bytes | Packet ID, used for delivery confirmation
| KEY | 32 bytes | DHT key
---------------------+-------+------------+----------------------------------------------------
@ -217,6 +263,7 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
---------------------+-------+------------+----------------------------------------------------
Email Packet | | | An email or email fragment, 1 recipient.
(encrypted) | TYPE | 1 byte | Value = 'E'
| VER | 1 byte | Protocol version
| KEY | 32 bytes | The DHT key of the packet (generated randomly)
| DELP | 32 bytes | Deletion key in plaintext, zeroed out for retrieving
| LEN | 2 bytes | Length of the encrypted part of the packet
@ -224,8 +271,7 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
---------------------+-------+------------+----------------------------------------------------
Email Packet | | | Storage format for the Incomplete Email Folder.
(unencrypted) | TYPE | 1 byte | Value = 'U'
| DELP | 32 bytes | Deletion key in plaintext, zeroed out for retrieving
| DELV | 32 bytes | Deletion key for verification
| VER | 1 byte | Protocol version
| MSID | 32 bytes | Message ID in binary format
| FRID | 2 bytes | Fragment Index of this packet (0..NFR-1)
| NFR | 2 bytes | Number of fragments in the email
@ -234,15 +280,20 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
---------------------+-------+------------+----------------------------------------------------
Index Packet | | | Contains the DHT keys of one or more Email Packets.
| TYPE | 1 byte | Value = 'I'
| VER | 1 byte | Protocol version
| DH | 32 bytes | SHA-256 hash of the recipient's email destination
| NP | 1 byte | Number of keys in the packet
| KEY1 | 32 bytes | DHT key of the first Email Packet
| KEY1 | 32 bytes | DHT key of the second Email Packet
| DHT1 | 32 bytes | DHT key of the first Email Packet
| DEL1 | 32 bytes | Deletion key of the first Email Packet
| DHT2 | 32 bytes | DHT key of the second Email Packet
| DEL2 | 32 bytes | Deletion key of the second Email Packet
| ... | ... | ...
| KEYn | 32 bytes | DHT key of the n-th Email Packet
| DHTn | 32 bytes | DHT key of the n-th Email Packet
| DELn | 32 bytes | Deletion key of the n-th Email Packet
---------------------+-------+------------+----------------------------------------------------
Relay Packet | | | The payload of a Relay Request
| TYPE | 1 byte | Value = 'R'
| VER | 1 byte | Protocol version
| TMIN | 4 bytes | Earliest send time
| TMAX | 4 bytes | Latest sent time (no guarantee!)
| XK | 32 bytes | Key to XOR the payload with
@ -255,6 +306,7 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
Peer List | | | Response to a Find Close Peers Request or
| | | a Relay List Request
| TYPE | 1 byte | Value = 'L'
| VER | 1 byte | Protocol version
| NUMP | 2 bytes | Number of peers in the list
| P1 | 384 bytes | Destination key
| P2 | 384 bytes | Destination key
@ -315,15 +367,38 @@ TODO mention the problem of highly unbalanced trees and the sibling list solutio
4.7. Deleting an Email Packet
Every time a recipient receives an email packet (from one or more storage nodes), it asks the storage
node(s) to delete the packet.
[TODO: This doesn't work for relayed email packets because the recipient doesn't know the storage nodes.
Maybe include the storage node in an email packet? Or relay the delete request and have the
relay endpoint do a findClosestNodes + delete? Or just delete the index packet entry and let
replication take care of deleting the email packets?]
It also sends a delete request to the nodes storing the index packet, asking them to delete the DHT key
of the email packet. Each index packet node then removes the DHT key from the stored index packet, and
adds the DHT key to a list of deleted keys it maintains for each index packet (DHT key + deletion key,
actually). The purpose of this is so a node that is about to replicate an email packet can find out if
it missed an earlier delete request for that packet, in which case the node "replicates" the delete
request rather than the packet itself. This helps reduce storage space by removing old Email Packets
from nodes that weren't online at the time the delete request was sent initially.
1) Recipient knows the deletion key after it decrypts the email packet (the packet also contains a
"plaintext deletion key" field, which is all zeros after it leaves a storage node).
2) Recipient sends a Delete Request to all storage nodes close to the DHT key of the Email Packet
2) Recipient sends a Delete Request to all nodes that responded to the query for the Email Destination,
asking them to mark the Email Packet's DHT key "deleted".
***** TODO deletion key for index packets? use signed del requests instead? *****
3) Recipient sends a Delete Request to all peers that responded to the query for the Email Packet
4.8. Looking up an email address in the directory
4.8. Replication of Email Packets
4.9. Announcing a new email address to the directory
4.9. Replication of Index Packets
4.10. Updating or deleting an email address from the directory
4.10. Looking up an email address in the directory
4.11. Announcing a new email address to the directory
4.12. Updating or deleting an email address from the directory
5. Algorithms Used By Nodes Locally
@ -431,9 +506,58 @@ secure as manually adding keypairs to the local address book, although it is mor
Format: <base64 key><tab><public name>[<tab><description>][<tab><email address>]
10. To Do
10. Glossary of Terms
* fix: new emails don't automatically show up when clicking "Check Mail", have to reload inbox
Email Destination
As the name implies, an Email Destination is an identifier by which somebody can be reached via I2P-Bote.
An Email Destination is a 512-character Base64 string containing a public encryption key and a signature verification key. Example:
uQtdwFHqbWHGyxZN8wChjWbCcgWrKuoBRNoziEpE8XDt8koHdJiskYXeUyq7JmpG
In8WKXY5LNue~62IXeZ-ppUYDdqi5V~9BZrcbpvgb5tjuu3ZRtHq9Vn6T9hOO1fa
FYZbK-FqHRiKm~lewFjSmfbBf1e6Fb~FLwQqUBTMtKYrRdO1d3xVIm2XXK83k1Da
-nufGASLaHJfsEkwMMDngg8uqRQmoj0THJb6vRfXzRw4qR5a0nj6dodeBfl2NgL9
HfOLInwrD67haJqjFJ8r~vVyOxRDJYFE8~f9b7k3N0YeyUK4RJSoiPXtTBLQ2RFQ
gOaKg4CuKHE0KCigBRU-Fhhc4weUzyU-g~rbTc2SWPlfvZ6n0voSvhvkZI9V52X3
SptDXk3fAEcwnC7lZzza6RNHurSMDMyOTmppAVz6BD8PB4o4RuWq7MQcnF9znElp
HX3Q10QdV3omVZJDNPxo-Wf~CpEd88C9ga4pS~QGIHSWtMPLFazeGeSHCnPzIRYD
Email Address
Email Addresses in I2P-Bote are shortcuts for Email Destinations.
Email Address <--> Email Destination mappings are stored in two places: the local address book and
the distributed address directory.
Email Identity
An Email Identity is an Email Destination plus a name you want to be known as when you use that identity.
Technically speaking, an Email Identity consists of four things:
* An Email Destination (i.e. two public keys)
* The two private keys for the Email Destination
* A public name which is shown to other people in emails
* A description which is not shown to anybody but you.
It helps you remember which Email Identity you use for which purpose.
An email identity is not required for sending emails (although only "Anonymous" can be selected for the
"sender" field).
I2P destination
The address of a I2P-Bote node on the I2P network. There is no need to know it unless you
are having problems connecting to other I2P-Bote nodes.
I2P destinations and Email Destinations look similar, but are completely independent of each other.
11. To Do
* some synchronized ArrayLists should be replaced with externally synchronized ArrayLists
* should bootstrapping continue until the sibling bucket is full?
* identities: "publish to distributed address book" button, asks for an email address
* <jsp:useBean> instead of zero-param JSP functions?
* Bug? checking emails after deleting an email causes an error. something about not being able to delete an email.
* wenn ich eine mail von dest xyz erhalte, und diese in meinem addressbuch ist, sollte als absender nicht der vom Sender gewählte username sein (da kann er ja behaupten, was er will), sondern der beim empfänger loval im Addbuch gespeicherte pet name.
Ist der user local nicht bekannt, dann kann der public name verwendet werden, allerdings sollte ersichtlich sein dass es nur der public name ist, z.B. durch andere Farbe oder ein Feld mit nem Sternchen für authenticated wenn er in local addy book steht (eine spalte wie datum oder so, ganz schmal mit nur nem sternchen drin, ähnlich sollten auch ungelesene mails markiert werden - ist aber nicht high priority, ich sag's nur für deine todo list, falls du d'accord bist.)
* make sure del key is zeroed out when responding to a retrieve request for an EncryptedEmailPacket
* don't send delete requests to self, just delete the file locally
* make blue background solid, lighter color so chinese chars are readable
* button to delete all packets with an invalid protocol version
* auto-refresh network.jsp
* show remaining time for the 3-min delay at startup
* Packet ID field isn't really a packet id, rename to Conversation ID or maybe Session ID
* Replication per the Kademlia spec
* Have an option to disable automatic lookups in the distributed address directory for your own address
@ -452,7 +576,6 @@ secure as manually adding keypairs to the local address book, although it is mor
* Turnkey outproxy feature?
* All nodes keep track of I2PBote peers through Peer List Requests and by pinging known peers
periodically.
* optionally, packets are padded to a constant size
* select different set of hops for each packet
* instead of sending dummy messages for cover traffic, test gateways by routing packet to self
* keep track of uptimes + reliability of paths (or gateways), have a "minimum
@ -461,14 +584,12 @@ secure as manually adding keypairs to the local address book, although it is mor
* Use more storage nodes if email destination is known to be heavily frequented (like Kademlia does for popular hashes)
* mention that min/max delay = earliest/latest send time (explain that there is no guarantee for the send time being between min and max, it can be after max)
* Consider replacing ElGamal with 256-bit ECC encryption (=86 base64 chars) which comes standard in Java 7
* If an email fits in one packet, don't bother with an index packet
* Emails in the Outbox: Show addt'l "progress" progress column which is clickable and brings up detailed info.
* On first run, offer to create a new email identity, and optionally assign an email address to it.
* Folder structure on disk should reflect folders in app. All email folders visible to the user have the same root directory: [app dir]/folders.
* ask user for confirmation before deleting an email identity
* have a view of the incomplete folder, so users can look at incomplete emails
* use a key store for email identities so they are not locally readable?
* log payload type for responses/retrieve requests/store requests
* unit test candidates:
* KBucket
* BucketManager
@ -478,18 +599,14 @@ secure as manually adding keypairs to the local address book, although it is mor
* OutboxProcessor
* generate javadoc in ant build
* are non-daemon threads preventing app from shutting down in eclipse?
* avoid using ConcurrentSkipListSet whenever possible for performance reasons
* prioritize packets in I2PSendQueue? Should PeerListRequests get higher priority than relay packets etc.?
* shorten thread names to 12 chars
* use I2P time, not System.currentTimeMillis()
* make some public methods package private
* AAAAA.... in local_dest.key file doesn't look right, fix
* Don't use Kademlia for getRandomNodes(), implement a RelayPeerManager instead
* comment code
* cache message IDs for received messages so we don't receive them more than once from different relays
* don't use byte arrays for content at all, use InputStream instead so we don't have to load large attachments into memory
* ByteArrayOutputStreams don't need to be closed
* remove setFileName/getFileName from Email, move debug code that uses it into Outbox
* use I2PSocketManager.ping() and rename PING/PONG packets to REQ/RESP?
* use DataHelper to write an int to an OutputStream
* use SimpleTimer/SimpleScheduler?

57
history.txt Normal file
View File

@ -0,0 +1,57 @@
I2P-Bote Version History
------------------------
0.2.2 (Released on Mar 22, 2010)
* German translation
* Automatic mail checking is now user-configurable
* Save peers.txt on exit
* Several minor changes
* Several bug fixes
0.2.1 (Released on Mar 05, 2010)
* I2P-Bote can now be run as an I2P plugin (I2P 0.7.12 or newer required)
* Option to set a default Email Identity
* Address book
* Umlaute and other non-ASCII characters are now supported
* Increase k-bucket size to 20
* Several bug fixes
0.2 (Released on Feb 01, 2010)
* Sending and retrieving now much faster
* Email packets are now deleted from storage nodes after retrieval
* Reply button
* Drop dead peers
* Automatically check for new email
* Inbox shows new emails in bold, number of new emails is shown
* "To" addresses are shown in the inbox and when displaying an email
* Network protocol changed to version 2, peers that use the old protocol are ignored
* "Network" page now shows peer info
* Several bug fixes and minor enhancements
0.1.5 (Released on Dec 19, 2009)
* Fixes a number of bugs in the Kademlia code
* Removed unimplemented HTML links
* Network-related UI elements are now disabled until tunnel is ready
* Emails can now be deleted from a folder
* "No Identity Defined" page didn't come up, fixed
0.1.4
* Do the 3-minute wait in the background
* Added a "Connect now" button to skip the wait
* Network Status panel now gives more useful information
0.1.3
* After startup, wait 3 minutes before connecting
* Change the paths in the build file from /opt/... to ../i2p/...
* Fix: "Check Mail" button doesn't show new emails, user has to reload inbox
* Fix: high cpu usage when sending or receiving
* Set tunnel names to "I2P-Bote"
0.1.2
Mostly a bug fix release. It adds two .jar files that were missing.
0.1.1
Fix: Clicking on an email in the inbox causes an error.
0.1 (Released on Dec 06, 2009)
First version. Lets a user create email identities and send/receive plain-text emails

18
jsp2po.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
# Extracts strings from <ib:message> tags and prints them
# to stdout in .po format (the .po header is not printed).
# Limitations:
# * Only <ib:message key="....."/> and <ib:message key='.....'/>
# are supported, <ib:message>.....</ib:message> is not.
# * The key="....." parameter must be on one line.
#
# How it works:
# * The while loop greps all ib:message lines, extracts the
# "key" parameter, and prints to stdout
# * The first awk removes duplicate message keys
# * the second awk makes a .po entry for each message key
#
find "$@" -name '*.jsp' -o -name '*.tag' | while read i; do
echo "#################### $i ####################";
grep "<ib:message key=\"" "$i" | sed "s#.*\<ib:message key=\""## | sed "s#\".*##"; grep "<ib:message key='" "$i" | sed "s#.*\<ib:message key='"## | sed "s#'.*##" | sort
done | awk ' !x[$0]++' | awk '{ if (index($0, "###")==0) { print ("msgid \""$0"\""); print ("msgstr \"\""); print(""); } else { print $0; } }';

400
locale/messages_de.po Normal file
View File

@ -0,0 +1,400 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-03-15 06:49+0000\n"
"PO-Revision-Date: \n"
"Last-Translator: HungryHobo <HungryHobo@mail.i2p>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:44
msgid "Active Since"
msgstr "Aktiv seit"
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:44
msgid "BktPfx"
msgstr "K-Buck."
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:44
msgid "Distance"
msgstr "Entfernung"
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:44
msgid "I2P Destination"
msgstr "I2P-Adresse"
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:44
msgid "Locked?"
msgstr "Gesperrt?"
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:44
#: src/i2p/bote/web/PeerInfoTag.java:82
msgid "Peer"
msgstr "Gegenstelle"
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:63
msgid "No"
msgstr "Nein"
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:63
msgid "Yes"
msgstr "Ja"
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:80
msgid "(None)"
msgstr "-"
#: src/i2p/bote/network/kademlia/KademliaPeerStats.java:85
msgid "(S)"
msgstr "(S)"
#: src/i2p/bote/web/PeerInfoTag.java:53
msgid "Kademlia Peers:"
msgstr "Kademlia-Gegenstellen:"
#: src/i2p/bote/web/PeerInfoTag.java:78
msgid "Banned Peers:"
msgstr "Verbannte Gegenstellen:"
#: src/i2p/bote/web/PeerInfoTag.java:83
msgid "Destination Hash"
msgstr "I2P-Adresse (SHA256-Summe)"
#: src/i2p/bote/web/PeerInfoTag.java:84
msgid "Ban Reason"
msgstr "Verbannungsgrund"
#: src/i2p/bote/web/SendEmailTag.java:60
msgid "The email has been sent."
msgstr "Die Mail wurde gesendet."
#: src/i2p/bote/web/SendEmailTag.java:63
msgid "Error sending email:"
msgstr "Fehler beim Senden der Mail:"
msgid "New Contact"
msgstr "Neuer Eintrag"
# ################### editContact.jsp ####################
msgid "Add"
msgstr "Hinzufügen"
msgid "Edit Contact"
msgstr "Bearbeiten"
msgid "Email Destination"
msgstr "Postfachkennung"
msgid "(required field, must be 512 characters)"
msgstr "(Pflichtfeld; die Länge muss 512 Zeichen betragen)"
msgid "Name:"
msgstr "Name:"
msgid "(required field)"
msgstr "(Pflichtfeld)"
msgid "Cancel"
msgstr "Abbrechen"
# ################### editIdentity.jsp ####################
msgid "New Email Identity"
msgstr "Neue Absenderidentität"
msgid "Create"
msgstr "Anlegen"
msgid "Edit Email Identity"
msgstr "Absenderidentität bearbeiten"
msgid "Save"
msgstr "Speichern"
msgid "Public Name:"
msgstr "Öffentlicher Name:"
msgid "(required field, shown to recipients)"
msgstr "(Pflichtfeld, für Empfänger sichtbar)"
msgid "Description:"
msgstr "Beschreibung:"
msgid "(optional, kept private)"
msgstr "(Optional, nicht für andere sichtbar)"
msgid "Email Address:"
msgstr "Mailadresse:"
msgid "(optional)"
msgstr "(Optional)"
msgid "Email Destination:"
msgstr "Postfachkennung:"
msgid "Default Identity:"
msgstr "Beim Senden voreingestellt:"
# ################### noIdentities.jsp ####################
# ################### saveContact.jsp ####################
msgid "Please fill in the Destination field."
msgstr "Bitte füllen Sie das Empfängerfeld aus."
msgid "Please fill in the Name field."
msgstr "Bitte füllen Sie das Namensfeld aus."
msgid "The contact has been saved."
msgstr "Der Adressbucheintrag wurde gespeichert."
# ################### newEmail.jsp ####################
msgid "New Email"
msgstr "Neue Mail"
# ################### identities.jsp ####################
msgid "Email Identities"
msgstr "Absenderidentitäten"
msgid "No email identities are defined."
msgstr "Es sind noch keine Absenderidentitäten angelegt worden."
msgid "Def."
msgstr "Voreinst."
msgid "Public Name"
msgstr "Öffentlicher Name"
msgid "Description"
msgstr "Beschreibung"
msgid "Email Address"
msgstr "Mailadresse"
msgid "Key"
msgstr "Absenderidentität"
msgid "New Identity"
msgstr "Neue Identität"
msgid "Delete this identity"
msgstr "Diese Identität löschen"
msgid "New"
msgstr "Neu"
msgid "From:"
msgstr "Von:"
msgid "To:"
msgstr "An:"
msgid "CC:"
msgstr "Kopie:"
msgid "BCC:"
msgstr "Blindkopie:"
msgid "Reply To:"
msgstr "Antwort an:"
msgid "Addr. Book..."
msgstr "Adressbuch..."
msgid "Subject:"
msgstr "Betreff:"
msgid "Message:"
msgstr "Nachricht:"
msgid "Send"
msgstr "Senden"
# ################### about.jsp ####################
msgid "About I2P-Bote"
msgstr "über I2P-Bote"
msgid "I2P-Bote Version {0}"
msgstr "I2P-Bote, Version {0}"
msgid "Author:"
msgstr "Autor:"
msgid "Contributors:"
msgstr "Unterstützer:"
# ################### buttonFrame.jsp ####################
msgid "Check Mail"
msgstr "Mails abrufen"
# ################### connect.jsp ####################
# ################### deleteContact.jsp ####################
msgid "The contact has been deleted from the address book."
msgstr "Der Adressbucheintrag wurde gelöscht."
msgid "Error"
msgstr "Fehler"
# ################### showEmail.jsp ####################
msgid "Add to Address Book"
msgstr "Zum Adressbuch hinzufügen"
msgid "Reply"
msgstr "Antworten"
msgid "Re:"
msgstr "AW:"
msgid "Delete"
msgstr "Löschen"
# ################### addressBook.jsp ####################
msgid "Address Book"
msgstr "Adressbuch"
msgid "Private Address Book"
msgstr "Privates Addressbuch"
msgid "Select One or More Entries"
msgstr "Wählen Sie einen oder mehrere Einträge aus"
msgid "Name"
msgstr "Name"
msgid "Add Recipients"
msgstr "Empfänger hinzufügen"
msgid "Delete this contact"
msgstr "Diesen Eintrag löschen"
# ################### footer.jsp ####################
# ################### getStatus.jsp ####################
# ################### header.jsp ####################
msgid "- I2P-Bote"
msgstr "- I2P-Bote"
msgid "I2P-Bote"
msgstr "I2P-Bote"
msgid "Secure Distributed Email"
msgstr "Sichere, dezentrale Mailkommunikation"
msgid "Folders"
msgstr "Ordner"
msgid "Inbox"
msgstr "Posteingang"
msgid "Outbox"
msgstr "Postausgang"
msgid "Sent"
msgstr "Gesendet"
msgid "Drafts"
msgstr "Entwürfe"
msgid "Trash"
msgstr "Papierkorb"
msgid "Manage Folders"
msgstr "Ordner verwalten"
# ################### addressBook.jsp ####################
msgid "Addresses"
msgstr "Adressen"
msgid "Identities"
msgstr "Identitäten"
msgid "Configuration"
msgstr "Konfiguration"
msgid "Settings"
msgstr "Einstellungen"
msgid "Network Status"
msgstr "Netzwerkstatus"
msgid "Help"
msgstr "Hilfe"
msgid "User Guide"
msgstr "Benutzerleitfaden"
msgid "About"
msgstr "über I2P-Bote"
# ################### deleteIdentity.jsp ####################
msgid "Error:"
msgstr "Fehler:"
# ################### deleteEmail.jsp ####################
msgid "Error: Couldn't delete email."
msgstr "Fehler: Die Mail konnte nicht gel�scht werden."
# ################### saveIdentity.jsp ####################
msgid "Please fill in the Public Name field."
msgstr "Bitte füllen Sie das Feld „Öffentlicher Name“ aus."
msgid "The email identity has been saved."
msgstr "Die Absenderidentität wurde gespeichert."
# ################### folder.jsp ####################
msgid "From"
msgstr "Von"
msgid "To"
msgstr "An"
msgid "Subject"
msgstr "Betreff"
msgid "Date"
msgstr "Datum"
msgid "Anonymous"
msgstr "Anonym"
msgid "Unknown"
msgstr "Unbekannt"
msgid "(No subject)"
msgstr "(Kein Betreff)"
msgid "Delete this email"
msgstr "Diese Mail löschen"
# ################### index.jsp ####################
# ################### network.jsp ####################
msgid "Network"
msgstr "Netzwerk"
msgid "Network information is not available because I2P-Bote hasn't started connecting to the network yet."
msgstr "Es sind keine Netzwerkinformationen verfügbar, weil sich I2P-Bote noch nicht mit dem Netzwerk verbunden hat."
msgid "Local destination:"
msgstr "Eigene I2P-Kennung:"
# ################### statusFrame.jsp ####################
msgid "Not Started"
msgstr "Nicht gestartet"
msgid "Waiting 3 Minutes..."
msgstr "Warte 3 Minuten..."
msgid "Connect Now"
msgstr "Jetzt verbinden"
msgid "Connecting..."
msgstr "Verbinde..."
msgid "Connected"
msgstr "Verbunden"
msgid "Unknown Status"
msgstr "Unbek. Status"
#~ msgid "K-Peers:"
#~ msgstr "Gegenstellen:"

12
plugin/plugin.config Normal file
View File

@ -0,0 +1,12 @@
name=i2pbote
signer=HungryHobo@mail.i2p
consoleLinkName=I2P-Bote
consoleLinkName_de=I2P-Bote
consoleLinkURL=/i2pbote/index.jsp
websiteURL=http://i2pbote.i2p/
updateURL=http://i2pbote.i2p/i2pbote-update.xpi2p
description=Decentralized email
description_de=Dezentrale Mailanwendung
license=GPL V3
min-java-version=1.6
author=HungryHobo@mail.i2p

1
plugin/webapps.config Normal file
View File

@ -0,0 +1 @@
webapps.i2pbote.classpath=$I2P/lib/jstl.jar,$I2P/lib/standard.jar,$PLUGIN/lib/mailapi.jar

View File

@ -29,13 +29,14 @@ import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.Log;
public class Configuration extends Properties {
public class Configuration {
private static final long serialVersionUID = -6318245413106186095L;
private static final String I2P_BOTE_SUBDIR = "i2pbote"; // relative to the I2P app dir
private static final String CONFIG_FILE_NAME = "i2pbote.config";
private static final String I2P_BOTE_SUBDIR = "i2pbote"; // relative to the I2P app dir
private static final String CONFIG_FILE_NAME = "i2pbote.config";
private static final String DEST_KEY_FILE_NAME = "local_dest.key";
private static final String PEER_FILE_NAME = "peers.txt";
private static final String IDENTITIES_FILE_NAME = "identities.txt";
private static final String ADDRESS_BOOK_FILE_NAME = "addressBook.txt";
private static final String OUTBOX_DIR = "outbox"; // relative to I2P_BOTE_SUBDIR
private static final String OUTBOX_SUBDIR_LOCAL = "local"; // relative to OUTBOX_DIR
private static final String OUTBOX_SUBDIR_RELAY = "relay"; // relative to OUTBOX_DIR
@ -43,89 +44,101 @@ public class Configuration extends Properties {
private static final String EMAIL_DHT_SUBDIR = "dht_email_pkt"; // relative to I2P_BOTE_SUBDIR
private static final String INDEX_PACKET_DHT_SUBDIR = "dht_index_pkt"; // relative to I2P_BOTE_SUBDIR
private static final String INBOX_SUBDIR = "inbox"; // relative to I2P_BOTE_SUBDIR
// Parameter names in the config file
private static final String PARAMETER_REDUNDANCY = "redundancy";
private static final String PARAMETER_STORAGE_SPACE_INBOX = "storageSpaceInbox";
private static final String PARAMETER_STORAGE_SPACE_RELAY = "storageSpaceRelay";
private static final String PARAMETER_STORAGE_TIME = "storageTime";
private static final String PARAMETER_MAX_FRAGMENT_SIZE = "maxFragmentSize";
private static final String PARAMETER_HASHCASH_STRENGTH = "hashCashStrength";
private static final String PARAMETER_SMTP_PORT = "smtpPort";
// Parameter names in the config file
private static final String PARAMETER_REDUNDANCY = "redundancy";
private static final String PARAMETER_STORAGE_SPACE_INBOX = "storageSpaceInbox";
private static final String PARAMETER_STORAGE_SPACE_RELAY = "storageSpaceRelay";
private static final String PARAMETER_STORAGE_TIME = "storageTime";
private static final String PARAMETER_MAX_FRAGMENT_SIZE = "maxFragmentSize";
private static final String PARAMETER_HASHCASH_STRENGTH = "hashCashStrength";
private static final String PARAMETER_SMTP_PORT = "smtpPort";
private static final String PARAMETER_POP3_PORT = "pop3Port";
private static final String PARAMETER_MAX_CONCURRENT_IDENTITIES_CHECK_MAIL = "maxConcurIdCheckMail";
private static final String PARAMETER_AUTO_MAIL_CHECK = "autoMailCheckEnabled";
private static final String PARAMETER_MAIL_CHECK_INTERVAL = "mailCheckInterval";
// Defaults for each parameter
private static final int DEFAULT_REDUNDANCY = 2;
private static final int DEFAULT_STORAGE_SPACE_INBOX = 1024 * 1024 * 1024;
private static final int DEFAULT_STORAGE_SPACE_RELAY = 100 * 1024 * 1024;
private static final int DEFAULT_STORAGE_TIME = 31; // in days
private static final int DEFAULT_MAX_FRAGMENT_SIZE = 10 * 1024 * 1024; // the maximum size one email fragment can be, in bytes
private static final int DEFAULT_HASHCASH_STRENGTH = 10;
// Defaults for each parameter
private static final int DEFAULT_REDUNDANCY = 2;
private static final int DEFAULT_STORAGE_SPACE_INBOX = 1024 * 1024 * 1024;
private static final int DEFAULT_STORAGE_SPACE_RELAY = 100 * 1024 * 1024;
private static final int DEFAULT_STORAGE_TIME = 31; // in days
private static final int DEFAULT_MAX_FRAGMENT_SIZE = 10 * 1024 * 1024; // the maximum size one email fragment can be, in bytes
private static final int DEFAULT_HASHCASH_STRENGTH = 10;
private static final int DEFAULT_SMTP_PORT = 7661;
private static final int DEFAULT_POP3_PORT = 7662;
private static final int DEFAULT_MAX_CONCURRENT_IDENTITIES_CHECK_MAIL = 10;
private Log log = new Log(Configuration.class);
private File i2pBoteDir;
/**
* Reads configuration settings from the <code>I2P_BOTE_SUBDIR</code> subdirectory under
* the I2P application directory. The I2P application directory can be changed via the
* <code>i2p.dir.app</code> system property.
*
* Logging is done through the I2P logger. I2P reads the log configuration from the
* <code>logger.config</code> file whose location is determined by the
* <code>i2p.dir.config</code> system property.
* @return
*/
public Configuration() {
// get the I2PBote directory and make sure it exists
i2pBoteDir = getI2PBoteDirectory();
if (!i2pBoteDir.exists())
i2pBoteDir.mkdirs();
// read the configuration file
File configFile = new File(i2pBoteDir, CONFIG_FILE_NAME);
private static final boolean DEFAULT_AUTO_MAIL_CHECK = true;
private static final int DEFAULT_MAIL_CHECK_INTERVAL = 30; // in minutes
private Log log = new Log(Configuration.class);
private Properties properties;
private File i2pBoteDir;
private File configFile;
/**
* Reads configuration settings from the <code>I2P_BOTE_SUBDIR</code> subdirectory under
* the I2P application directory. The I2P application directory can be changed via the
* <code>i2p.dir.app</code> system property.
*
* Logging is done through the I2P logger. I2P reads the log configuration from the
* <code>logger.config</code> file whose location is determined by the
* <code>i2p.dir.config</code> system property.
* @return
*/
public Configuration() {
properties = new Properties();
// get the I2PBote directory and make sure it exists
i2pBoteDir = getI2PBoteDirectory();
if (!i2pBoteDir.exists() && !i2pBoteDir.mkdirs())
log.error("Cannot create directory: <" + i2pBoteDir.getAbsolutePath() + ">");
// read the configuration file
configFile = new File(i2pBoteDir, CONFIG_FILE_NAME);
boolean configurationLoaded = false;
if (configFile.exists()) {
log.debug("Loading config file <" + configFile.getAbsolutePath() + ">");
log.debug("Loading config file <" + configFile.getAbsolutePath() + ">");
try {
DataHelper.loadProps(this, configFile);
DataHelper.loadProps(properties, configFile);
configurationLoaded = true;
} catch (IOException e) {
log.error("Error loading configuration file <" + configFile.getAbsolutePath() + ">", e);
log.error("Error loading configuration file <" + configFile.getAbsolutePath() + ">", e);
}
}
if (!configurationLoaded)
log.info("Can't read configuration file <" + configFile.getAbsolutePath() + ">, using default settings.");
}
}
public File getDestinationKeyFile() {
return new File(i2pBoteDir, DEST_KEY_FILE_NAME);
}
public File getPeerFile() {
return new File(i2pBoteDir, PEER_FILE_NAME);
}
public File getDestinationKeyFile() {
return new File(i2pBoteDir, DEST_KEY_FILE_NAME);
}
public File getPeerFile() {
return new File(i2pBoteDir, PEER_FILE_NAME);
}
public File getIdentitiesFile() {
return new File(i2pBoteDir, IDENTITIES_FILE_NAME);
}
public File getLocalOutboxDir() {
return new File(getOutboxBaseDir(), OUTBOX_SUBDIR_LOCAL);
}
public File getAddressBookFile() {
return new File(i2pBoteDir, ADDRESS_BOOK_FILE_NAME);
}
public File getLocalOutboxDir() {
return new File(getOutboxBaseDir(), OUTBOX_SUBDIR_LOCAL);
}
public File getRelayOutboxDir() {
return new File(getOutboxBaseDir(), OUTBOX_SUBDIR_RELAY);
}
private File getOutboxBaseDir() {
return new File(i2pBoteDir, OUTBOX_DIR);
}
private File getOutboxBaseDir() {
return new File(i2pBoteDir, OUTBOX_DIR);
}
public File getInboxDir() {
return new File(i2pBoteDir, INBOX_SUBDIR);
}
@ -149,75 +162,111 @@ public class Configuration extends Properties {
return new File(i2pAppDir, I2P_BOTE_SUBDIR);
}
/**
* Save the configuration
* @param configFile
* @throws IOException
*/
public void saveToFile(File configFile) throws IOException {
log.debug("Saving config file <" + configFile.getAbsolutePath() + ">");
DataHelper.storeProps(this, configFile);
}
/**
* Save the configuration
* @param configFile
*/
public void save() {
log.debug("Saving config file <" + configFile.getAbsolutePath() + ">");
try {
DataHelper.storeProps(properties, configFile);
} catch (IOException e) {
log.error("Cannot save configuration to file <" + configFile.getAbsolutePath() + ">", e);
}
}
/**
* Returns the number of relays to use for sending and receiving email. Zero is a legal value.
* @return
*/
public int getRedundancy() {
return getIntParameter(PARAMETER_REDUNDANCY, DEFAULT_REDUNDANCY);
}
/**
* Returns the number of relays to use for sending and receiving email. Zero is a legal value.
* @return
*/
public int getRedundancy() {
return getIntParameter(PARAMETER_REDUNDANCY, DEFAULT_REDUNDANCY);
}
/**
* Returns the maximum size (in bytes) the inbox can take up.
* @return
*/
public int getStorageSpaceInbox() {
return getIntParameter(PARAMETER_STORAGE_SPACE_INBOX, DEFAULT_STORAGE_SPACE_INBOX);
}
/**
* Returns the maximum size (in bytes) all messages stored for relaying can take up.
* @return
*/
public int getStorageSpaceRelay() {
return getIntParameter(PARAMETER_STORAGE_SPACE_RELAY, DEFAULT_STORAGE_SPACE_RELAY);
}
/**
* Returns the time (in milliseconds) after which an email is deleted from the outbox if it cannot be sent / relayed.
* @return
*/
public long getStorageTime() {
return 24L * 3600 * 1000 * getIntParameter(PARAMETER_STORAGE_TIME, DEFAULT_STORAGE_TIME);
}
/**
* Returns the maximum size (in bytes) the inbox can take up.
* @return
*/
public int getStorageSpaceInbox() {
return getIntParameter(PARAMETER_STORAGE_SPACE_INBOX, DEFAULT_STORAGE_SPACE_INBOX);
}
/**
* Returns the maximum size (in bytes) all messages stored for relaying can take up.
* @return
*/
public int getStorageSpaceRelay() {
return getIntParameter(PARAMETER_STORAGE_SPACE_RELAY, DEFAULT_STORAGE_SPACE_RELAY);
}
/**
* Returns the time (in milliseconds) after which an email is deleted from the outbox if it cannot be sent / relayed.
* @return
*/
public long getStorageTime() {
return 24L * 3600 * 1000 * getIntParameter(PARAMETER_STORAGE_TIME, DEFAULT_STORAGE_TIME);
}
public int getMaxFragmentSize() {
return getIntParameter(PARAMETER_MAX_FRAGMENT_SIZE, DEFAULT_MAX_FRAGMENT_SIZE);
}
public int getHashCashStrength() {
return getIntParameter(PARAMETER_HASHCASH_STRENGTH, DEFAULT_HASHCASH_STRENGTH);
}
/**
* Returns the maximum number of email identities to retrieve new emails for at a time.
* @return
*/
public int getMaxConcurIdCheckMail() {
public int getMaxFragmentSize() {
return getIntParameter(PARAMETER_MAX_FRAGMENT_SIZE, DEFAULT_MAX_FRAGMENT_SIZE);
}
public int getHashCashStrength() {
return getIntParameter(PARAMETER_HASHCASH_STRENGTH, DEFAULT_HASHCASH_STRENGTH);
}
/**
* Returns the maximum number of email identities to retrieve new emails for at a time.
* @return
*/
public int getMaxConcurIdCheckMail() {
return getIntParameter(PARAMETER_MAX_CONCURRENT_IDENTITIES_CHECK_MAIL, DEFAULT_MAX_CONCURRENT_IDENTITIES_CHECK_MAIL);
}
private int getIntParameter(String parameterName, int defaultValue) {
String stringValue = getProperty(parameterName);
if (stringValue == null)
return defaultValue;
else
try {
return new Integer(getProperty(parameterName));
}
catch (NumberFormatException e) {
log.warn("Can't convert value <" + stringValue + "> for parameter <" + parameterName + "> to int, using default.");
return defaultValue;
}
}
}
}
public void setAutoMailCheckEnabled(boolean enabled) {
properties.setProperty(PARAMETER_AUTO_MAIL_CHECK, new Boolean(enabled).toString());
}
public boolean isAutoMailCheckEnabled() {
return getBooleanParameter(PARAMETER_AUTO_MAIL_CHECK, DEFAULT_AUTO_MAIL_CHECK);
}
public void setMailCheckInterval(int minutes) {
properties.setProperty(PARAMETER_MAIL_CHECK_INTERVAL, new Integer(minutes).toString());
}
/**
* Returns the number of minutes the application should wait before automatically
* checking for mail.
* @return
*/
public int getMailCheckInterval() {
return getIntParameter(PARAMETER_MAIL_CHECK_INTERVAL, DEFAULT_MAIL_CHECK_INTERVAL);
}
private boolean getBooleanParameter(String parameterName, boolean defaultValue) {
String stringValue = properties.getProperty(parameterName);
if ("true".equalsIgnoreCase(stringValue) || "yes".equalsIgnoreCase(stringValue) || "on".equalsIgnoreCase(stringValue) || "1".equals(stringValue))
return true;
else if ("false".equalsIgnoreCase(stringValue) || "no".equalsIgnoreCase(stringValue) || "off".equalsIgnoreCase(stringValue) || "0".equals(stringValue))
return false;
else {
log.warn("<" + stringValue + "> is not a legal value for the boolean parameter <" + parameterName + ">");
return defaultValue;
}
}
private int getIntParameter(String parameterName, int defaultValue) {
String stringValue = properties.getProperty(parameterName);
if (stringValue == null)
return defaultValue;
else
try {
return new Integer(properties.getProperty(parameterName));
}
catch (NumberFormatException e) {
log.warn("Can't convert value <" + stringValue + "> for parameter <" + parameterName + "> to int, using default.");
return defaultValue;
}
}
}

View File

@ -21,27 +21,33 @@
package i2p.bote;
import i2p.bote.addressbook.AddressBook;
import i2p.bote.email.Email;
import i2p.bote.email.EmailDestination;
import i2p.bote.email.EmailIdentity;
import i2p.bote.email.Identities;
import i2p.bote.folder.DhtPacketFolder;
import i2p.bote.folder.EmailFolder;
import i2p.bote.folder.EmailPacketFolder;
import i2p.bote.folder.IncompleteEmailFolder;
import i2p.bote.folder.IndexPacketFolder;
import i2p.bote.folder.Outbox;
import i2p.bote.folder.PacketFolder;
import i2p.bote.network.BanList;
import i2p.bote.network.BannedPeer;
import i2p.bote.network.CheckEmailTask;
import i2p.bote.network.DHT;
import i2p.bote.network.DhtPeerStats;
import i2p.bote.network.I2PPacketDispatcher;
import i2p.bote.network.I2PSendQueue;
import i2p.bote.network.NetworkStatus;
import i2p.bote.network.PeerManager;
import i2p.bote.network.kademlia.KademliaDHT;
import i2p.bote.packet.DataPacket;
import i2p.bote.packet.EncryptedEmailPacket;
import i2p.bote.packet.IndexPacket;
import i2p.bote.packet.RelayPacket;
import i2p.bote.packet.UnencryptedEmailPacket;
import i2p.bote.service.AutoMailCheckTask;
import i2p.bote.service.I2PBoteThread;
import i2p.bote.service.OutboxProcessor;
import i2p.bote.service.POP3Service;
import i2p.bote.service.RelayPacketSender;
@ -58,6 +64,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@ -74,124 +81,140 @@ import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.util.Log;
import net.i2p.util.Translate;
/**
* This is the core class of the application. Is is implemented as a singleton.
* This is the core class of the application. It is implemented as a singleton.
*/
public class I2PBote {
private static final String VERSION = "0.1.2";
private static I2PBote instance;
public static final int PROTOCOL_VERSION = 2;
private static final String APP_VERSION = "0.2.2";
private static final int STARTUP_DELAY = 3; // the number of minutes to wait before connecting to I2P (this gives the router time to get ready)
private static volatile I2PBote instance;
private Log log = new Log(I2PBote.class);
private I2PAppContext appContext;
private I2PClient i2pClient;
private I2PSession i2pSession;
private Configuration configuration;
private Identities identities;
private I2PSendQueue sendQueue;
private Outbox outbox; // stores outgoing emails for all local users
private I2PClient i2pClient;
private I2PSession i2pSession;
private Configuration configuration;
private Identities identities;
private AddressBook addressBook;
private I2PSendQueue sendQueue;
private Outbox outbox; // stores outgoing emails for all local users
private EmailFolder inbox; // stores incoming emails for all local users
private PacketFolder<RelayPacket> relayPacketFolder; // stores email packets we're forwarding for other machines
private IncompleteEmailFolder incompleteEmailFolder; // stores email packets addressed to a local user
private DhtPacketFolder<? extends DataPacket> emailDhtStorageFolder; // stores email packets and index packets for other peers
private PacketFolder<RelayPacket> relayPacketFolder; // stores email packets we're forwarding for other machines
private IncompleteEmailFolder incompleteEmailFolder; // stores email packets addressed to a local user
private EmailPacketFolder emailDhtStorageFolder; // stores email packets for other peers
private IndexPacketFolder indexPacketDhtStorageFolder; // stores index packets
//TODO private PacketFolder<> addressDhtStorageFolder; // stores email address-destination mappings
private SMTPService smtpService;
private POP3Service pop3Service;
private OutboxProcessor outboxProcessor; // reads emails stored in the outbox and sends them
private RelayPacketSender relayPacketSender; // reads packets stored in the relayPacketFolder and sends them
private DHT dht;
private PeerManager peerManager;
private SMTPService smtpService;
private POP3Service pop3Service;
private OutboxProcessor outboxProcessor; // reads emails stored in the outbox and sends them
private AutoMailCheckTask autoMailCheckTask;
private RelayPacketSender relayPacketSender; // reads packets stored in the relayPacketFolder and sends them
private DHT dht;
private PeerManager peerManager;
private ThreadFactory mailCheckThreadFactory;
private ExecutorService mailCheckExecutor;
private Collection<Future<Boolean>> mailCheckResults;
private Collection<Future<Boolean>> pendingMailCheckTasks;
private long lastMailCheckTime;
private ConnectTask connectTask;
private I2PBote() {
Thread.currentThread().setName("I2PBoteMain");
initializeLogging();
private I2PBote() {
Thread.currentThread().setName("I2PBoteMain");
appContext = new I2PAppContext();
i2pClient = I2PClientFactory.createClient();
configuration = new Configuration();
i2pClient = I2PClientFactory.createClient();
configuration = new Configuration();
mailCheckThreadFactory = Util.createThreadFactory("ChkMailTask", CheckEmailTask.THREAD_STACK_SIZE);
mailCheckExecutor = Executors.newFixedThreadPool(configuration.getMaxConcurIdCheckMail(), mailCheckThreadFactory);
identities = new Identities(configuration.getIdentitiesFile());
initializeSession();
initializeFolderAccess();
initializeServices();
startAllServices();
}
identities = new Identities(configuration.getIdentitiesFile());
addressBook = new AddressBook(configuration.getAddressBookFile());
initializeFolderAccess();
private void initializeLogging() {
// The rest of the initialization happens in ConnectTask because it needs an I2PSession.
// It is done in the background so we don't block the webapp thread.
connectTask = new ConnectTask();
connectTask.start();
}
/**
* Initializes objects for accessing emails and packet files on the filesystem.
*/
private void initializeFolderAccess() {
inbox = new EmailFolder(configuration.getInboxDir());
outbox = new Outbox(configuration.getLocalOutboxDir());
relayPacketFolder = new PacketFolder<RelayPacket>(configuration.getRelayOutboxDir());
incompleteEmailFolder = new IncompleteEmailFolder(configuration.getIncompleteDir(), inbox);
emailDhtStorageFolder = new DhtPacketFolder<EncryptedEmailPacket>(configuration.getEmailDhtStorageDir());
indexPacketDhtStorageFolder = new IndexPacketFolder(configuration.getIndexPacketDhtStorageDir());
}
/**
* Sets up a {@link I2PSession}, using the I2P destination stored on disk or creating a new I2P
* destination if no key file exists.
*/
private void initializeSession() {
Properties sessionProperties = new Properties();
sessionProperties.setProperty("i2cp.gzip", String.valueOf(false)); // most of the data we send is encrypted and therefore uncompressible
/**
* Initializes objects for accessing emails and packet files on the filesystem.
*/
private void initializeFolderAccess() {
inbox = new EmailFolder(configuration.getInboxDir());
outbox = new Outbox(configuration.getLocalOutboxDir());
relayPacketFolder = new PacketFolder<RelayPacket>(configuration.getRelayOutboxDir());
incompleteEmailFolder = new IncompleteEmailFolder(configuration.getIncompleteDir(), inbox);
emailDhtStorageFolder = new EmailPacketFolder(configuration.getEmailDhtStorageDir());
indexPacketDhtStorageFolder = new IndexPacketFolder(configuration.getIndexPacketDhtStorageDir());
}
/**
* Sets up a {@link I2PSession}, using the I2P destination stored on disk or creating a new I2P
* destination if no key file exists.
*/
private void initializeSession() {
Properties sessionProperties = new Properties();
sessionProperties.setProperty("inbound.nickname", "I2P-Bote");
sessionProperties.setProperty("outbound.nickname", "I2P-Bote");
sessionProperties.setProperty("i2cp.gzip", String.valueOf(false)); // most of the data we send is encrypted and therefore uncompressible
// read the local destination key from the key file if it exists
File destinationKeyFile = configuration.getDestinationKeyFile();
FileReader fileReader = null;
try {
FileReader fileReader = new FileReader(destinationKeyFile);
fileReader = new FileReader(destinationKeyFile);
char[] destKeyBuffer = new char[(int)destinationKeyFile.length()];
fileReader.read(destKeyBuffer);
byte[] localDestinationKey = Base64.decode(new String(destKeyBuffer));
ByteArrayInputStream inputStream = new ByteArrayInputStream(localDestinationKey);
i2pSession = i2pClient.createSession(inputStream, sessionProperties);
i2pSession.connect();
i2pSession = i2pClient.createSession(inputStream, sessionProperties);
i2pSession.connect();
}
catch (IOException e) {
log.debug("Destination key file doesn't exist or isn't readable: " + e);
log.debug("Destination key file doesn't exist or isn't readable." + e);
} catch (I2PSessionException e) {
log.warn("Error creating I2PSession", e);
}
log.warn("Error creating I2PSession.", e);
}
finally {
if (fileReader != null)
try {
fileReader.close();
}
catch (IOException e) {
log.debug("Error closing file: <" + destinationKeyFile.getAbsolutePath() + ">" + e);
}
}
// if the local destination key can't be read or is invalid, create a new one
// if the local destination key can't be read or is invalid, create a new one
if (i2pSession == null) {
log.debug("Creating new local destination key");
try {
ByteArrayOutputStream arrayStream = new ByteArrayOutputStream();
i2pClient.createDestination(arrayStream);
byte[] localDestinationArray = arrayStream.toByteArray();
i2pSession = i2pClient.createSession(new ByteArrayInputStream(localDestinationArray), sessionProperties);
i2pSession.connect();
saveLocalDestinationKeys(destinationKeyFile, localDestinationArray);
} catch (I2PException e) {
log.error("Error creating local destination key or I2PSession.", e);
} catch (IOException e) {
log.error("Error writing local destination key to file.", e);
}
log.debug("Creating new local destination key");
try {
ByteArrayOutputStream arrayStream = new ByteArrayOutputStream();
i2pClient.createDestination(arrayStream);
byte[] localDestinationArray = arrayStream.toByteArray();
i2pSession = i2pClient.createSession(new ByteArrayInputStream(localDestinationArray), sessionProperties);
i2pSession.connect();
saveLocalDestinationKeys(destinationKeyFile, localDestinationArray);
} catch (I2PException e) {
log.error("Error creating local destination key or I2PSession.", e);
} catch (IOException e) {
log.error("Error writing local destination key to file.", e);
}
}
Destination localDestination = i2pSession.getMyDestination();
log.debug("Local destination key = " + localDestination.toBase64());
log.debug("Local destination hash = " + localDestination.calculateHash().toBase64());
}
/**
* Initializes daemon threads, doesn't start them yet.
*/
}
/**
* Initializes daemon threads, doesn't start them yet.
*/
private void initializeServices() {
I2PPacketDispatcher dispatcher = new I2PPacketDispatcher();
i2pSession.addSessionListener(dispatcher, I2PSession.PROTO_ANY, I2PSession.PORT_ANY);
@ -207,9 +230,14 @@ public class I2PBote {
dht.setStorageHandler(IndexPacket.class, indexPacketDhtStorageFolder);
//TODO dht.setStorageHandler(AddressPacket.class, );
dispatcher.addPacketListener(emailDhtStorageFolder);
dispatcher.addPacketListener(indexPacketDhtStorageFolder);
peerManager = new PeerManager();
outboxProcessor = new OutboxProcessor(dht, outbox, configuration, peerManager, appContext);
autoMailCheckTask = new AutoMailCheckTask(configuration.getMailCheckInterval());
}
/**
@ -219,82 +247,100 @@ public class I2PBote {
* @throws DataFormatException
* @throws IOException
*/
private void saveLocalDestinationKeys(File keyFile, byte[] localDestinationArray) throws DataFormatException, IOException {
if (keyFile.exists()) {
File oldKeyFile = new File(keyFile.getPath() + "_backup");
keyFile.renameTo(oldKeyFile);
}
else
keyFile.createNewFile();
private void saveLocalDestinationKeys(File keyFile, byte[] localDestinationArray) throws DataFormatException, IOException {
if (keyFile.exists()) {
File oldKeyFile = new File(keyFile.getPath() + "_backup");
if (!keyFile.renameTo(oldKeyFile))
log.error("Cannot rename destination key file <" + keyFile.getAbsolutePath() + "> to <" + oldKeyFile.getAbsolutePath() + ">");
}
else
if (!keyFile.createNewFile())
log.error("Cannot create destination key file: <" + keyFile.getAbsolutePath() + ">");
FileWriter fileWriter = new FileWriter(keyFile);
fileWriter.write(Base64.encode(localDestinationArray));
fileWriter.close();
}
public static void startUp() {
getInstance();
}
public static void shutDown() {
if (instance != null)
instance.stopAllServices();
}
}
public static void startUp() {
getInstance();
}
public static void shutDown() {
if (instance != null)
instance.stopAllServices();
}
public static I2PBote getInstance() {
if (instance == null)
instance = new I2PBote();
return instance;
}
public String getVersion() {
return VERSION;
}
public Identities getIdentities() {
return identities;
}
public String getLocalDestination() {
return i2pSession.getMyDestination().toBase64();
}
public void sendEmail(Email email) throws Exception {
public static I2PBote getInstance() {
if (instance == null)
instance = new I2PBote();
return instance;
}
public Configuration getConfiguration() {
return configuration;
}
public static String getAppVersion() {
return APP_VERSION;
}
public String getLanguage() {
return Translate.getLanguage(appContext);
}
public Identities getIdentities() {
return identities;
}
public AddressBook getAddressBook() {
return addressBook;
}
public Destination getLocalDestination() {
return i2pSession.getMyDestination();
}
public void sendEmail(Email email) throws Exception {
/*XXX*/
email.updateHeaders();
String recipient = email.getAllRecipients().iterator().next();
if (recipient.indexOf('@')>=0)
recipient = recipient.substring(0, recipient.indexOf('@'));
String recipient = email.getAllRecipients()[0].toString();
EmailDestination emailDestination = new EmailDestination(recipient);
Collection<UnencryptedEmailPacket> emailPackets = email.createEmailPackets(recipient);
Collection<EncryptedEmailPacket> encryptedPackets = EncryptedEmailPacket.encrypt(emailPackets, emailDestination, appContext);
for (EncryptedEmailPacket packet: encryptedPackets)
dht.store(packet);
dht.store(new IndexPacket(encryptedPackets, emailDestination));
// TODO uncomment for next milestone, move code above into OutboxProcessor
/* outbox.add(email);
outboxProcessor.checkForEmail();*/
}
public synchronized void checkForMail() {
if (!isCheckingForMail())
mailCheckResults = Collections.synchronizedCollection(new ArrayList<Future<Boolean>>());
for (EmailIdentity identity: getIdentities()) {
Callable<Boolean> checkMailTask = new CheckEmailTask(identity, dht, peerManager, incompleteEmailFolder, appContext);
Future<Boolean> result = mailCheckExecutor.submit(checkMailTask);
mailCheckResults.add(result);
}
// TODO uncomment for next milestone, move code above into OutboxProcessor
/* outbox.add(email);
outboxProcessor.checkForEmail();*/
}
public synchronized void checkForMail() {
if (!isCheckingForMail()) {
log.debug("Checking mail for " + identities.size() + " Email Identities...");
lastMailCheckTime = System.currentTimeMillis();
pendingMailCheckTasks = Collections.synchronizedCollection(new ArrayList<Future<Boolean>>());
mailCheckExecutor = Executors.newFixedThreadPool(configuration.getMaxConcurIdCheckMail(), mailCheckThreadFactory);
for (EmailIdentity identity: identities) {
Callable<Boolean> checkMailTask = new CheckEmailTask(identity, dht, peerManager, sendQueue, incompleteEmailFolder, appContext);
Future<Boolean> task = mailCheckExecutor.submit(checkMailTask);
pendingMailCheckTasks.add(task);
}
mailCheckExecutor.shutdown(); // finish all tasks, then shut down
}
else
log.debug("Not checking for mail because the last mail check hasn't finished.");
}
public synchronized long getLastMailCheckTime() {
return lastMailCheckTime;
}
public synchronized boolean isCheckingForMail() {
if (mailCheckResults == null)
if (mailCheckExecutor == null)
return false;
for (Future<Boolean> result: mailCheckResults)
if (!result.isDone())
return true;
return false;
return !mailCheckExecutor.isTerminated();
}
/**
@ -305,15 +351,15 @@ dht.store(new IndexPacket(encryptedPackets, emailDestination));
* @return
*/
public synchronized boolean newMailReceived() {
if (mailCheckResults == null)
if (pendingMailCheckTasks == null)
return false;
if (isCheckingForMail())
return false;
try {
for (Future<Boolean> result: mailCheckResults)
for (Future<Boolean> result: pendingMailCheckTasks)
if (result.get(1, TimeUnit.MILLISECONDS)) {
mailCheckResults = null;
pendingMailCheckTasks = null;
return true;
}
}
@ -321,7 +367,7 @@ dht.store(new IndexPacket(encryptedPackets, emailDestination));
log.error("Error while checking whether new mail has arrived.", e);
}
mailCheckResults = null;
pendingMailCheckTasks = null;
return false;
}
@ -330,29 +376,154 @@ dht.store(new IndexPacket(encryptedPackets, emailDestination));
}
public int getNumDhtPeers() {
return dht.getNumPeers();
if (dht == null)
return 0;
else
return dht.getNumPeers();
}
public int getNumRelayPeers() {
return peerManager.getNumPeers();
public DhtPeerStats getDhtStats() {
if (dht == null)
return null;
else
return dht.getPeerStats();
}
public Collection<BannedPeer> getBannedPeers() {
return BanList.getInstance().getAll();
}
private void startAllServices() {
dht.start();
outboxProcessor.start();
relayPacketSender.start();
smtpService.start();
pop3Service.start();
sendQueue.start();
}
// outboxProcessor.start();
// relayPacketSender.start();
// smtpService.start();
// pop3Service.start();
sendQueue.start();
autoMailCheckTask.start();
}
private void stopAllServices() {
dht.shutDown();
outboxProcessor.shutDown();
relayPacketSender.requestShutdown();
smtpService.shutDown();
pop3Service.shutDown();
sendQueue.requestShutdown();
mailCheckExecutor.shutdownNow();
}
private void stopAllServices() {
if (connectTask != null)
connectTask.requestShutdown();
if (dht != null) dht.requestShutdown();
if (outboxProcessor != null) outboxProcessor.requestShutdown();
if (relayPacketSender != null) relayPacketSender.requestShutdown();
if (smtpService != null) smtpService.requestShutdown();
if (pop3Service != null) pop3Service.requestShutdown();
if (sendQueue != null) sendQueue.requestShutdown();
if (mailCheckExecutor != null) mailCheckExecutor.shutdown();
if (pendingMailCheckTasks != null)
for (Future<Boolean> mailCheckTask: pendingMailCheckTasks)
mailCheckTask.cancel(false);
if (autoMailCheckTask != null) autoMailCheckTask.requestShutdown();
long deadline = System.currentTimeMillis() + 1000 * 60; // the time at which any background threads that are still running are killed
if (dht != null)
try {
dht.awaitShutdown(deadline - System.currentTimeMillis());
}
catch(InterruptedException e) {
log.error("Interrupted while waiting for DHT shutdown.", e);
}
join(outboxProcessor, deadline);
join(relayPacketSender, deadline);
join(smtpService, deadline);
join(pop3Service, deadline);
join(sendQueue, deadline);
if (mailCheckExecutor != null) mailCheckExecutor.shutdownNow();
join(autoMailCheckTask, deadline);
long currentTime = System.currentTimeMillis();
if (mailCheckExecutor!=null && currentTime<deadline)
try {
mailCheckExecutor.awaitTermination(deadline-currentTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.error("Interrupted while waiting for mailCheckExecutor to exit", e);
}
try {
if (i2pSession != null)
i2pSession.destroySession();
} catch (I2PSessionException e) {
log.error("Can't destroy I2P session.", e);
}
}
private void join(Thread thread, long until) {
if (thread == null)
return;
long timeout = System.currentTimeMillis() - until;
if (timeout > 0)
try {
thread.join(timeout);
} catch (InterruptedException e) {
log.error("Interrupted while waiting for thread <" + thread.getName() + "> to exit", e);
}
}
/**
* Connects to the network, skipping the connect delay.
* If the delay time has already passed, calling this method has no effect.
*/
public void connectNow() {
connectTask.startSignal.countDown();
}
public NetworkStatus getNetworkStatus() {
if (!connectTask.isDone())
return connectTask.getNetworkStatus();
else if (dht != null)
return dht.isConnected()?NetworkStatus.CONNECTED:NetworkStatus.CONNECTING;
else
return NetworkStatus.ERROR;
}
/**
* Waits <code>STARTUP_DELAY</code> milliseconds or until <code>startSignal</code>
* is triggered from outside this class, then sets up an I2P session and everything
* that depends on it.
*/
private class ConnectTask extends I2PBoteThread {
volatile NetworkStatus status = NetworkStatus.NOT_STARTED;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(1);
protected ConnectTask() {
super("ConnectTask");
setDaemon(true);
}
public NetworkStatus getNetworkStatus() {
return status;
}
public boolean isDone() {
try {
return doneSignal.await(0, TimeUnit.SECONDS);
} catch (InterruptedException e) {
return false;
}
}
@Override
public void requestShutdown() {
super.requestShutdown();
startSignal.countDown();
}
@Override
public void run() {
status = NetworkStatus.DELAY;
try {
startSignal.await(STARTUP_DELAY, TimeUnit.MINUTES);
status = NetworkStatus.CONNECTING;
initializeSession();
initializeServices();
startAllServices();
doneSignal.countDown();
} catch (Exception e) {
status = NetworkStatus.ERROR;
log.error("Can't initialize the application.", e);
}
}
}
}

View File

@ -29,11 +29,13 @@ import java.nio.ByteBuffer;
import java.util.Arrays;
import net.i2p.data.Base64;
import net.i2p.util.Log;
import net.i2p.util.RandomSource;
public class UniqueId implements Comparable<UniqueId> {
public static final byte LENGTH = 32;
private Log log = new Log(UniqueId.class);
protected byte[] bytes;
/**
@ -46,7 +48,7 @@ public class UniqueId implements Comparable<UniqueId> {
}
/**
* Create a packet id from a 32 bytes of an array, starting at <code>offset</code>.
* Create a packet id from 32 bytes of an array, starting at <code>offset</code>.
* @param bytes
*/
public UniqueId(byte[] bytes, int offset) {
@ -70,7 +72,15 @@ public class UniqueId implements Comparable<UniqueId> {
*/
public UniqueId(InputStream inputStream) throws IOException {
bytes = new byte[LENGTH];
inputStream.read(bytes);
if (inputStream.read(bytes) < 0)
log.error("Cannot read " + LENGTH + " bytes: Unexpected end of stream.");
}
/**
* @param base64 A 44-character base64-encoded string
*/
public UniqueId(String base64) {
bytes = Base64.decode(base64);
}
public byte[] toByteArray() {
@ -87,7 +97,7 @@ public class UniqueId implements Comparable<UniqueId> {
@Override
public int compareTo(UniqueId otherPacketId) {
return new BigInteger(bytes).compareTo(new BigInteger(otherPacketId.bytes));
return new BigInteger(1, bytes).compareTo(new BigInteger(1, otherPacketId.bytes));
}
@Override
@ -108,11 +118,4 @@ public class UniqueId implements Comparable<UniqueId> {
public int hashCode() {
return Arrays.hashCode(bytes);
}
@Override
public UniqueId clone() {
UniqueId newUniqueId = new UniqueId();
newUniqueId.bytes = bytes.clone();
return newUniqueId;
}
}

View File

@ -26,14 +26,19 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ThreadFactory;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PSession;
import net.i2p.data.DataFormatException;
import net.i2p.util.Translate;
public class Util {
private static final int BUFFER_SIZE = 32 * 1024;
private static final String BUNDLE_NAME = "i2p.bote.locale.Messages";
private Util() { }
public static void copyBytes(InputStream inputStream, OutputStream outputStream) throws IOException {
@ -78,8 +83,45 @@ public class Util {
return new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
return new Thread(null, runnable, threadName, stackSize);
return new Thread(Thread.currentThread().getThreadGroup(), runnable, threadName, stackSize);
}
};
}
}
/**
* Creates a thread-safe <code>Iterable</code> from a thread-unsafe one.
* Modifications to the old <code>Iterable</code> will not affect the
* new one.
* @param <E>
* @param iterable
* @return
*/
public static <E> Iterable<E> synchronizedCopy(Iterable<E> iterable) {
synchronized(iterable) {
Collection<E> collection = new ArrayList<E>();
for (E element: iterable)
collection.add(element);
return collection;
}
}
/**
* Returns the <code>i</code>-th element of a <code>Collection</code>'s <code>Iterator</code>.
* @param <E>
* @param collection
* @param i
* @return
*/
public static <E> E get(Collection<E> collection, int i) {
for (E element: collection) {
if (i == 0)
return element;
i--;
}
return null;
}
public static String _(String messageKey) {
return Translate.getString(messageKey, I2PAppContext.getGlobalContext(), BUNDLE_NAME);
}
}

View File

@ -0,0 +1,161 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.addressbook;
import i2p.bote.email.EmailDestination;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import net.i2p.data.DataFormatException;
import net.i2p.util.Log;
/**
* Implements the private address book. Holds a set of <code>Addresses</code>.
*/
public class AddressBook implements Iterable<Contact> {
private Log log = new Log(AddressBook.class);
private File addressFile;
private List<Contact> contacts;
/**
* Reads an <code>AddressBook</code> from a text file. Each contact is defined
* by one line that contains an Email Destination and a name, separated by a
* tab character.
* @param addressFile
*/
public AddressBook(File addressFile) {
this.addressFile = addressFile;
contacts = Collections.synchronizedList(new ArrayList<Contact>());
if (!addressFile.exists()) {
log.debug("Address file does not exist: <" + addressFile.getAbsolutePath() + ">");
return;
}
log.debug("Reading address book from <" + addressFile.getAbsolutePath() + ">");
BufferedReader input = null;
try {
input = new BufferedReader(new FileReader(addressFile));
while (true) {
String line = input.readLine();
if (line == null) // EOF
break;
String[] fields = line.split("\\t", 2);
try {
EmailDestination destination = new EmailDestination(fields[0]);
String name = null;
if (fields.length > 1)
name = fields[1];
contacts.add(new Contact(destination, name));
}
catch (DataFormatException e) {
log.error("Not a valid Email Destination: <" + fields[0] + ">");
}
}
} catch (IOException e) {
log.error("Can't read address book.", e);
}
finally {
if (input != null)
try {
input.close();
}
catch (IOException e) {
log.error("Error closing input stream.", e);
}
}
}
public void save() throws IOException {
BufferedWriter writer = new BufferedWriter(new FileWriter(addressFile));
try {
for (Contact contact: contacts) {
writer.write(contact.toBase64());
writer.write("\t");
writer.write(contact.getName());
writer.newLine();
}
}
catch (IOException e) {
log.error("Can't save address book to file <" + addressFile.getAbsolutePath() + ">.", e);
throw e;
}
finally {
writer.close();
}
}
public void add(Contact contact) {
contacts.add(contact);
}
public void remove(String destination) {
Contact contact = get(destination);
if (contact != null)
contacts.remove(contact);
}
public Contact get(int i) {
return contacts.get(i);
}
/**
* Looks up an {@link Contact} by its Base64 key. If none is found,
* <code>null</code> is returned.
* @param destination
* @return
*/
public Contact get(String destination) {
if (destination==null || destination.isEmpty())
return null;
for (Contact contact: contacts)
if (destination.equals(contact.toBase64()))
return contact;
return null;
}
public Collection<Contact> getAll() {
return contacts;
}
public int size() {
return contacts.size();
}
@Override
public Iterator<Contact> iterator() {
return contacts.iterator();
}
}

View File

@ -1,6 +1,5 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
@ -20,36 +19,39 @@
* along with I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.email;
package i2p.bote.addressbook;
import i2p.bote.UniqueId;
import net.i2p.data.Base64;
import i2p.bote.email.EmailDestination;
/**
* Contains methods to convert a {@link UniqueId} from and to an
* RFC5322-compatible representation.
* Represents an address book entry.
*/
public class MessageId extends UniqueId {
public class Contact {
private EmailDestination destination;
private String name;
public Contact(EmailDestination destination, String name) {
this.destination = destination;
this.name = name;
}
public void setDestination(EmailDestination destination) {
this.destination = destination;
}
public EmailDestination getDestination() {
return destination;
}
public MessageId() {
super();
public String toBase64() {
return destination.toBase64();
}
public MessageId(String emailMessageId) {
// remove "@..." if present
int ampersandIndex = emailMessageId.indexOf('@');
if (ampersandIndex >= 0)
emailMessageId = emailMessageId.substring(0, ampersandIndex);
bytes = Base64.decode(emailMessageId);
public void setName(String name) {
this.name = name;
}
public String getEmailMessageId() {
return toBase64() + "@i2p";
}
@Override
public String toString() {
return getEmailMessageId();
public String getName() {
return name;
}
}

View File

@ -22,270 +22,205 @@
package i2p.bote.email;
import i2p.bote.UniqueId;
import i2p.bote.Util;
import i2p.bote.folder.FolderElement;
import i2p.bote.packet.UnencryptedEmailPacket;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.Properties;
import java.util.TimeZone;
import net.i2p.util.ConcurrentHashSet;
import javax.mail.Header;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.InternetHeaders;
import javax.mail.internet.MimeMessage;
import net.i2p.util.Log;
import com.nettgryppa.security.HashCash;
// TODO move one package up
public class Email implements FolderElement {
public class Email extends MimeMessage {
private static final int MAX_BYTES_PER_PACKET = 30 * 1024;
private static final char[] NEW_LINE = new char[] {13, 10}; // separates header section from mail body; same for all platforms per RFC 5322
private static final Set<String> HEADER_WHITELIST = createHeaderWhitelist();
private static final String[] HEADER_WHITELIST = new String[] {
"From", "Sender", "To", "CC", "BCC", "Reply-To", "Subject", "Date", "MIME-Version", "Content-Type",
"Content-Transfer-Encoding", "In-Reply-To", "X-HashCash", "X-Priority"
};
private static Log log = new Log(Email.class);
private File file;
private List<Header> headers;
private byte[] content; // save memory by using bytes rather than chars
private MessageId messageId;
private UniqueId messageId;
private boolean isNew = true;
public Email() {
headers = Collections.synchronizedList(new ArrayList<Header>());
content = new byte[0];
messageId = new MessageId();
super(Session.getDefaultInstance(new Properties()));
messageId = new UniqueId();
}
public Email(File file) throws FileNotFoundException, MessagingException {
this(new FileInputStream(file));
}
/**
* Creates an Email object from an InputStream containing a MIME email.
*
* @param inputStream
* @throws IOException
* @throws MessagingException
*/
public Email(InputStream inputStream) throws IOException {
this(Util.readInputStream(inputStream));
private Email(InputStream inputStream) throws MessagingException {
super(Session.getDefaultInstance(new Properties()), inputStream);
messageId = new UniqueId();
}
/**
* Creates an Email object from a byte array containing a MIME email.
*
* @param bytes
* @throws MessagingException
*/
public Email(byte[] bytes) {
BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
headers = Collections.synchronizedList(new ArrayList<Header>());
ByteArrayOutputStream contentStream = new ByteArrayOutputStream();
boolean allHeadersRead = false;
try {
// read mail headers
while (true) {
String line = reader.readLine();
if (line == null) // EOF
break;
if ("".equals(line)) { // empty line separates header from mail body
allHeadersRead = true;
continue;
}
if (!allHeadersRead) {
String[] splitString = line.split(":\\s*", 2);
if (splitString.length > 1) {
String name = splitString[0];
String value = splitString[1];
if (HEADER_WHITELIST.contains(name))
headers.add(new Header(name, value));
}
else
allHeadersRead = true;
}
if (allHeadersRead)
contentStream.write(line.getBytes());
}
}
catch (IOException e) {
log.error("Can't read from ByteArrayInputStream.", e);
}
content = contentStream.toByteArray();
String messageIdString = getHeader("Message-Id");
if (messageIdString != null)
messageId = new MessageId(messageIdString);
public Email(byte[] bytes) throws MessagingException {
super(Session.getDefaultInstance(new Properties()), new ByteArrayInputStream(bytes));
messageId = new UniqueId();
}
private static Set<String> createHeaderWhitelist() {
String[] headerArray = new String[] {
"From", "Sender", "To", "CC", "BCC", "Reply-To", "Subject", "Date", "MIME-Version", "Content-Type",
"Content-Transfer-Encoding", "Message-Id", "In-Reply-To", "X-HashCash"
};
ConcurrentHashSet<String> headerSet = new ConcurrentHashSet<String>();
headerSet.addAll(Arrays.asList(headerArray));
return headerSet;
}
public void setHashCash(HashCash hashCash) {
public void setHashCash(HashCash hashCash) throws MessagingException {
setHeader("X-HashCash", hashCash.toString());
}
public void setHeader(String name, String value) {
for (Header header: headers)
if (name.equals(header.name))
headers.remove(header);
addHeader(name, value);
}
public void addHeader(String name, String value) {
if (HEADER_WHITELIST.contains(name))
headers.add(new Header(name, value));
else
log.debug("Ignoring non-whitelisted header: " + name);
}
public void setSender(String sender) {
setHeader("Sender", sender);
}
/**
* Returns the value of the RFC 5322 "From" header field. If the "From" header
* field is absent, the value of the "Sender" field is returned. If both
* fields are absent, <code>null</code> is returned.
* @return
*/
public String getSender() {
String sender = getHeader("From");
if (sender != null)
return sender;
sender = getHeader("Sender");
return sender;
}
public void setSubject(String subject) {
setHeader("Subject", subject);
}
public String getSubject() {
return getHeader("Subject");
}
public Collection<String> getAllRecipients() {
List<String> recipients = new ArrayList<String>();
for (Header header: headers)
if (isRecipient(header.name))
recipients.add(header.value);
return recipients;
}
private boolean isRecipient(String headerName) {
return RecipientType.TO.equalsString(headerName) || RecipientType.CC.equalsString(headerName) || RecipientType.BCC.equalsString(headerName);
}
public void addRecipient(RecipientType type, String address) {
addHeader(type.toString(), address);
}
/**
* Initializes some basic header fields.
* Removes all headers that are not on the whitelist, and initializes some
* basic header fields.
* Called by <code>saveChanges()</code>, see JavaMail JavaDoc.
*/
@Override
public void updateHeaders() {
setHeader("Content-Type", "text/plain");
setHeader("Content-Transfer-Encoding", "7bit");
setHeader("Message-Id", messageId.getEmailMessageId());
// Set the "Date" field, using english for the locale.
Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT+0"));
DateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss +0000", Locale.ENGLISH); // always use UTC for outgoing mail
setHeader("Date", formatter.format(calendar.getTime()));
}
/**
* Returns the value of the "Date" header field.
* @return
*/
public String getDateString() {
return getHeader("Date");
}
/**
* Parses the value of the "Date" header field into a {@link Date}.
* If the field cannot be parsed, <code>null</code> is returned.
* @return
*/
public Date getDate() {
try {
return parseDate(getDateString());
}
catch (ParseException e) {
return null;
super.updateHeaders();
scrubHeaders();
// Set the "Date" field, using english for the locale.
Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT+0"));
DateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss +0000", Locale.ENGLISH); // always use UTC for outgoing mail
setHeader("Date", formatter.format(calendar.getTime()));
} catch (MessagingException e) {
log.error("Cannot set mail headers.", e);
}
}
/**
* Creates a copy of the <code>Email</code> with all "BCC" headers removed, except the one for
* <code>recipient<code>.
* @String recipient
* @return
* @throws MessagingException
* @throws IOException
*/
private Email removeBCCs(String recipient) throws MessagingException, IOException {
// make a copy of the email
Email newEmail = new Email();
newEmail.setContent(getContent(), getDataHandler().getContentType());
// set new headers
newEmail.headers = new InternetHeaders();
@SuppressWarnings("unchecked")
List<Header> headers = Collections.list(getAllHeaders());
for (Header header: headers)
if (!"BCC".equals(header.getName()) || !recipient.equals(header.getValue()))
newEmail.addHeader(header.getName(), header.getValue());
return newEmail;
}
/**
* Example for a valid date string: Sat, 19 Aug 2006 20:05:41 +0100
* @param dateString
* @return
* @throws ParseException
* Removes all mail headers except the ones in <code>HEADER_WHITELIST</code>.
* @throws MessagingException
*/
private Date parseDate(String dateString) throws ParseException {
// remove day of week if present
String[] tokens = dateString.split(",\\s+", 2);
if (tokens.length > 1)
dateString = tokens[1];
DateFormat parser = new SimpleDateFormat("dd MMM yyyy kk:mm:ss Z", Locale.ENGLISH);
return parser.parse(dateString);
}
public void setContent(String content) {
this.content = content.getBytes();
}
public void setContent(byte[] content) {
this.content = content;
}
public byte[] getContent() {
return content;
}
public String getBodyText() {
return new String(content);
}
public MessageId getMessageID() {
return messageId;
private void scrubHeaders() throws MessagingException {
@SuppressWarnings("unchecked")
List<Header> nonMatchingHeaders = Collections.list(getNonMatchingHeaders(HEADER_WHITELIST));
for (Header header: nonMatchingHeaders) {
log.debug("Removing all instances of non-whitelisted header <" + header.getName() + ">");
removeHeader(header.getName());
}
}
/**
*
* @param messageIdString Must be a 44-character Base64-encoded string.
*/
public void setMessageID(String messageIdString) {
this.messageId = new UniqueId(messageIdString);
}
public void setMessageID(UniqueId messageId) {
this.messageId = messageId;
}
@Override
public String getMessageID() {
return messageId.toBase64();
}
public void setNew(boolean isNew) {
this.isNew = isNew;
}
/**
* Returns <code>true</code> if the email is unread (incoming mail), or
* if it has not been sent yet (outgoing mail).
* @return
*/
public boolean isNew() {
return isNew;
}
public String getText() {
try {
return getContent().toString();
} catch (Exception e) {
String errorMsg = "Error reading email content.";
log.error(errorMsg, e);
return errorMsg;
}
}
/**
* Converts the email into one or more email packets.
* If an error occurs, an empty <code>Collection</code> is returned.
*
* @param bccToKeep All BCC fields in the header section of the email are removed, except this field. If this parameter is <code>null</code>, all BCC fields are written.
* @return
* @throws IOException
*/
public Collection<UnencryptedEmailPacket> createEmailPackets(String bccToKeep) {
ArrayList<UnencryptedEmailPacket> packets = new ArrayList<UnencryptedEmailPacket>();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
writeTo(outputStream, bccToKeep);
try {
saveChanges();
if (bccToKeep == null)
writeTo(outputStream);
else
removeBCCs(bccToKeep).writeTo(outputStream);
} catch (IOException e) {
log.error("Can't write to ByteArrayOutputStream.", e);
return packets;
} catch (MessagingException e) {
log.error("Can't remove BCC headers.", e);
return packets;
}
byte[] emailArray = outputStream.toByteArray();
// calculate fragment count
@ -301,9 +236,8 @@ public class Email implements FolderElement {
// make a new array with the right length
byte[] block = new byte[blockSize];
System.arraycopy(emailArray, blockStart, block, 0, blockSize);
UniqueId deletionKeyPlain = new UniqueId();
UniqueId deletionKeyEncrypted = deletionKeyPlain.clone(); // encryption happens in the constructor call below
UnencryptedEmailPacket packet = new UnencryptedEmailPacket(deletionKeyPlain, deletionKeyEncrypted, messageId, fragmentIndex, numFragments, block);
UniqueId deletionKey = new UniqueId();
UnencryptedEmailPacket packet = new UnencryptedEmailPacket(messageId, fragmentIndex, numFragments, block, deletionKey);
packets.add(packet);
fragmentIndex++;
blockStart += blockSize;
@ -312,97 +246,4 @@ public class Email implements FolderElement {
return packets;
}
/**
* Removes all "BCC" headers except the one for <code>recipient<code>.
* The mail body is not copied (which means the new email shares its body with the original).
* @String recipient
* @return
*/
public Email removeBCCs(String recipient) {
List<Header> newHeaders = Collections.synchronizedList(new ArrayList<Header>());
for (Header header: headers)
if (!"BCC".equals(header.name) || !recipient.equals(header.value))
newHeaders.add(header);
Email newEmail = new Email();
newEmail.headers = newHeaders;
newEmail.content = content;
newEmail.messageId = messageId;
return newEmail;
}
/**
* Returns the value of the first header field for a header field name,
* or <code>null</code> if no field by that name exists.
* @param name
* @return
*/
private String getHeader(String name) {
for (Header header: headers)
if (name.equals(header.name))
return header.value;
return null;
}
// FolderElement implementation
@Override
public File getFile() {
return file;
}
// FolderElement implementation
@Override
public void setFile(File file) {
this.file = file;
}
/**
* Writes the email as an RFC 5322 stream.
* @see <a href="http://tools.ietf.org/html/rfc5322">http://tools.ietf.org/html/rfc5322</a>
*/
@Override
public void writeTo(OutputStream outputStream) throws IOException {
writeTo(outputStream, null);
}
/**
* Writes the email as an RFC 5322 stream.
* @see <a href="http://tools.ietf.org/html/rfc5322">http://tools.ietf.org/html/rfc5322</a>
* @param outputStream
* @param bccToKeep All BCC fields in the header section of the email are removed, except this field. If this parameter is <code>null</code>, all BCC fields are written.
* @throws IOException
*/
public void writeTo(OutputStream outputStream, String bccToKeep) {
PrintWriter writer = new PrintWriter(outputStream);
writeHeaders(writer, bccToKeep);
writer.print(NEW_LINE);
writer.print(new String(content));
writer.print(NEW_LINE);
writer.close();
}
private void writeHeaders(PrintWriter writer, String bccToKeep) {
for (Header header: headers)
if (bccToKeep==null || !"BCC".equals(header.name) || bccToKeep.equals(header.value))
writer.println(header.toString());
}
private class Header {
String name;
String value;
Header(String name, String value) {
this.name = name;
this.value = value;
}
@Override
public String toString() {
return name + ": " + value;
}
}
}

View File

@ -79,42 +79,50 @@ public class EmailDestination {
publicSigningKey = new SigningPublicKey(signingKeyArray);
}
public EmailDestination(String base64Data) {
try {
base64Data += "AAAA"; // add a null certificate
Destination i2pDestination = new Destination(base64Data);
publicEncryptionKey = i2pDestination.getPublicKey();
publicSigningKey = i2pDestination.getSigningPublicKey();
} catch (DataFormatException e) {
log.error("Can't generate EmailDestination.", e);
/**
* @param address A string containing a valid base64-encoded Email Destination
* @throws DataFormatException If <code>address</code> doesn't contain a valid Email Destination
*/
public EmailDestination(String address) throws DataFormatException {
String base64Data = extractBase64Dest(address);
if (base64Data == null) {
String msg = "No Email Destination found in string: <" + address + ">";
log.debug(msg);
throw new DataFormatException(msg);
}
base64Data += "AAAA"; // add a null certificate
Destination i2pDestination = new Destination(base64Data);
publicEncryptionKey = i2pDestination.getPublicKey();
publicSigningKey = i2pDestination.getSigningPublicKey();
}
/* public EmailDestination(byte[] data) {
try {
byte[] destinationPlusCert = addNullCertificate(data);
ByteArrayInputStream byteStream = new ByteArrayInputStream(destinationPlusCert);
I2PSession i2pSession = I2PClientFactory.createClient().createSession(byteStream, null);
initKeys(i2pSession);
}
catch (I2PSessionException e) {
log.error("Can't generate EmailDestination.", e);
}
}*/
/* private byte[] addNullCertificate(byte[] data) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
outputStream.write(data);
outputStream.write(new Certificate().toByteArray());
// Add an extra zero byte so I2PSessionImpl.readDestination doesn't fall on its face. I believe this is a bug.
outputStream.write(0);
}
catch (IOException e) {
log.error("Can't write to ByteArrayOutputStream.", e);
}
return outputStream.toByteArray();
}*/
/**
* Looks for a Base64-encoded Email Destination in a string. Returns
* the 512-byte Base64 string, or <code>null</code> if nothing is found.
* Even if the return value is non-<code>null</code>, it is not
* guaranteed to be a valid Email Destination.
* @param address
* @return
*/
private String extractBase64Dest(String address) {
if (address==null || address.length()<512)
return null;
if (address.length() == 512)
return address;
// Check if the string contains 512 chars in angle brackets
int bracketIndex = address.indexOf('<');
if (bracketIndex>=0 && address.length()>bracketIndex+512)
return address.substring(bracketIndex+1, bracketIndex+1+512);
// Check if the string is of the form EmailDest@foo
if (address.indexOf('@') == 512)
return address.substring(0, 513);
return null;
}
protected void initKeys(I2PSession i2pSession) {
publicEncryptionKey = i2pSession.getMyDestination().getPublicKey();

View File

@ -41,6 +41,7 @@ public class EmailIdentity extends EmailDestination {
private String publicName;
private String description; // optional
private String emailAddress; // optional
private boolean isDefault;
/**
* Creates a random <code>EmailIdentity</code>.
@ -53,19 +54,15 @@ public class EmailIdentity extends EmailDestination {
* Creates a <code>EmailIdentity</code> from a Base64-encoded string. The format is the same as
* for Base64-encoded local I2P destinations, except there is no null certificate.
* @param key
* @throws I2PSessionException
*/
public EmailIdentity(String key) {
try {
key = key.substring(0, 512) + "AAAA" + key.substring(512); // insert a null certificate for I2PClient.createSession()
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.decode(key));
I2PClient i2pClient = I2PClientFactory.createClient();
I2PSession i2pSession = i2pClient.createSession(inputStream, null);
initKeys(i2pSession);
}
catch (I2PSessionException e) {
log.error("Can't generate EmailIdentity.", e);
}
public EmailIdentity(String key) throws I2PSessionException {
key = key.substring(0, 512) + "AAAA" + key.substring(512); // insert a null certificate for I2PClient.createSession()
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.decode(key));
I2PClient i2pClient = I2PClientFactory.createClient();
I2PSession i2pSession = i2pClient.createSession(inputStream, null);
initKeys(i2pSession);
}
public PrivateKey getPrivateEncryptionKey() {
@ -100,6 +97,14 @@ public class EmailIdentity extends EmailDestination {
return emailAddress;
}
public void setDefault(boolean isDefault) {
this.isDefault = isDefault;
}
public boolean isDefault() {
return isDefault;
}
protected void initKeys(I2PSession i2pSession) {
super.initKeys(i2pSession);
privateEncryptionKey = i2pSession.getDecryptionKey();
@ -132,13 +137,4 @@ public class EmailIdentity extends EmailDestination {
public String toString() {
return getKey() + " address=<" + getEmailAddress() + "> identity name=<" + getDescription() + "> visible name=<" + getPublicName() + ">";
}
/* @Override
public boolean equals(Object anotherObject) {
if (anotherObject instanceof EmailIdentity) {
EmailIdentity otherIdentity = (EmailIdentity)anotherObject;
return Arrays.equals(getKeysAsArray(), otherIdentity.getKeysAsArray());
}
return false;
}*/
}

View File

@ -35,7 +35,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.regex.PatternSyntaxException;
import net.i2p.data.Hash;
import net.i2p.client.I2PSessionException;
import net.i2p.util.Log;
/**
@ -46,9 +46,22 @@ public class Identities implements Iterable<EmailIdentity> {
private File identitiesFile;
private List<EmailIdentity> identities;
/**
* Constructs <code>Identities</code> from a text file. Each identity is defined
* by one line that contains two to four tab-separated fields:
* Email Destination key, Public Name, Description, and Email Address.
* The first two are mandatory, the last two are optional.
*
* Additionally, the file can set a default Email Destination by including a
* line that starts with "Default ", followed by an Email Destination key.
* The destination key must match one of the Email Destinations defined in
* the file.
* @param identitiesFile
*/
public Identities(File identitiesFile) {
this.identitiesFile = identitiesFile;
identities = Collections.synchronizedList(new ArrayList<EmailIdentity>());
String defaultIdentityString = null;
if (!identitiesFile.exists()) {
log.debug("Identities file does not exist: <" + identitiesFile.getAbsolutePath() + ">");
@ -65,10 +78,21 @@ public class Identities implements Iterable<EmailIdentity> {
if (line == null) // EOF
break;
EmailIdentity identity = parse(line);
if (identity != null)
identities.add(identity);
if (line.toLowerCase().startsWith("default"))
defaultIdentityString = line.substring("default ".length());
else {
EmailIdentity identity = parse(line);
if (identity != null)
identities.add(identity);
}
}
// set the default identity; if none defined, make the first one the default
EmailIdentity defaultIdentity = get(defaultIdentityString);
if (defaultIdentity != null)
defaultIdentity.setDefault(true);
else if (!identities.isEmpty())
identities.get(0).setDefault(true);
} catch (IOException e) {
log.error("Can't read identities file.", e);
}
@ -84,12 +108,12 @@ public class Identities implements Iterable<EmailIdentity> {
}
private EmailIdentity parse(String emailIdentityString) {
String[] fields = emailIdentityString.split("\\t", 4);
if (fields.length < 2) {
log.debug("Unparseable email identity: <" + emailIdentityString + ">");
return null;
}
try {
String[] fields = emailIdentityString.split("\\t", 4);
if (fields.length < 2) {
log.debug("Unparseable email identity: <" + emailIdentityString + ">");
return null;
}
EmailIdentity identity = new EmailIdentity(fields[0]);
if (fields.length > 1)
identity.setPublicName(fields[1]);
@ -102,6 +126,9 @@ public class Identities implements Iterable<EmailIdentity> {
catch (PatternSyntaxException e) {
log.debug("Unparseable email identity: <" + emailIdentityString + ">");
return null;
} catch (I2PSessionException e) {
log.debug("Invalid email identity: <" + fields[0] + ">");
return null;
}
}
@ -127,26 +154,64 @@ public class Identities implements Iterable<EmailIdentity> {
public void save() throws IOException {
String newLine = System.getProperty("line.separator");
Writer writer = new BufferedWriter(new FileWriter(identitiesFile));
try {
Writer writer = new BufferedWriter(new FileWriter(identitiesFile));
EmailIdentity defaultIdentity = getDefault();
if (defaultIdentity != null)
writer.write("Default " + defaultIdentity.getKey() + newLine);
for (EmailIdentity identity: identities)
writer.write(toFileFormat(identity) + newLine);
writer.close();
}
catch (IOException e) {
log.error("Can't save email identities to file <" + identitiesFile.getAbsolutePath() + ">.", e);
throw e;
}
finally {
writer.close();
}
}
public void add(EmailIdentity identity) {
if (identities.isEmpty())
identity.setDefault(true);
identities.add(identity);
}
public void remove(String key) {
EmailIdentity identity = get(key);
if (identity != null)
if (identity != null) {
identities.remove(identity);
// if we deleted the default identity, set a new default
if (identity.isDefault() && !identities.isEmpty())
identities.get(0).setDefault(true);
}
}
/**
* Sets the default identity. Assumes this <code>Identities</code> already
* contains <code>defaultIdentity</code>.
* @return
*/
public void setDefault(EmailIdentity defaultIdentity) {
// clear the old default
for (EmailIdentity identity: identities)
identity.setDefault(false);
defaultIdentity.setDefault(true);
}
/**
* Returns the default identity, or <code>null</code> if no default is set.
* @return
*/
public EmailIdentity getDefault() {
for (EmailIdentity identity: identities)
if (identity.isDefault())
return identity;
return null;
}
public EmailIdentity get(int i) {
@ -173,19 +238,10 @@ public class Identities implements Iterable<EmailIdentity> {
return identities;
}
public EmailIdentity[] getArray() {
return identities.toArray(new EmailIdentity[0]);
}
public int size() {
return identities.size();
}
public boolean contains(Hash emailDestination) {
// TODO
return true;
}
@Override
public Iterator<EmailIdentity> iterator() {
return identities.iterator();

View File

@ -1,20 +0,0 @@
package i2p.bote.email;
public enum RecipientType {
TO("To"), CC("CC"), BCC("BCC");
private String value;
private RecipientType(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
public boolean equalsString(String string) {
return value.equalsIgnoreCase(string);
}
}

View File

@ -22,6 +22,7 @@
package i2p.bote.folder;
import i2p.bote.network.DhtStorageHandler;
import i2p.bote.packet.MalformedDataPacketException;
import i2p.bote.packet.dht.DhtStorablePacket;
import java.io.File;
@ -47,12 +48,26 @@ public class DhtPacketFolder<T extends DhtStorablePacket> extends PacketFolder<T
add(packetToStore, getFilename(packetToStore));
}
private String getFilename(DhtStorablePacket packet) {
protected String getFilename(DhtStorablePacket packet) {
return packet.getDhtKey().toBase64() + PACKET_FILE_EXTENSION;
}
@Override
public DhtStorablePacket retrieve(Hash dhtKey) {
File packetFile = findPacketFile(dhtKey);
if (packetFile != null)
try {
return DhtStorablePacket.createPacket(packetFile);
}
catch (MalformedDataPacketException e) {
log.error("Cannot create packet from file: <" + packetFile.getAbsolutePath() + ">", e);
return null;
}
else
return null;
}
protected File findPacketFile(Hash dhtKey) {
final String base64Key = dhtKey.toBase64();
File[] files = storageDir.listFiles(new FilenameFilter() {
@ -66,7 +81,7 @@ public class DhtPacketFolder<T extends DhtStorablePacket> extends PacketFolder<T
log.warn("More than one packet files found for DHT key " + dhtKey);
if (files.length > 0) {
File file = files[0];
return DhtStorablePacket.createPacket(file);
return file;
}
return null;
}
@ -74,4 +89,14 @@ public class DhtPacketFolder<T extends DhtStorablePacket> extends PacketFolder<T
protected boolean filenameMatches(String filename, String base64DhtKey) {
return filename.startsWith(base64DhtKey);
}
public void delete(Hash dhtKey) {
File packetFile = findPacketFile(dhtKey);
if (packetFile != null) {
if (!packetFile.delete())
log.warn("File cannot be deleted: <" + packetFile.getAbsolutePath() + ">");
}
else
log.debug("No file found for DHT key: " + dhtKey);
}
}

View File

@ -21,20 +21,22 @@
package i2p.bote.folder;
import i2p.bote.UniqueId;
import i2p.bote.email.Email;
import i2p.bote.email.MessageId;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import javax.mail.MessagingException;
import net.i2p.util.Log;
/**
* Stores emails in a directory on the file system. Each email is stored in one file.
* The filename is the message Id plus an extension.
* Filenames are in the format <code><N, O>_<message ID>.mail</code>, where
* N is for new (unread or unsent) and O is for old (read or sent).
*/
public class EmailFolder extends Folder<Email> {
protected static final String EMAIL_FILE_EXTENSION = ".mail";
@ -44,29 +46,49 @@ public class EmailFolder extends Folder<Email> {
public EmailFolder(File storageDir) {
super(storageDir, EMAIL_FILE_EXTENSION);
}
// store an email file
public void add(Email email) throws IOException {
/**
* Stores an email in the folder. If an email with the same
* message ID exists already, nothing happens.
* @param email
* @throws IOException
* @throws MessagingException
*/
public void add(Email email) throws IOException, MessagingException {
// check if an email exists already with that message id
if (getEmailFile(email.getMessageID()) != null) {
log.debug("Not storing email because there is an existing one with the same message ID: <" + email.getMessageID()+ ">");
return;
}
// write out the email file
File emailFile = getEmailFile(email);
log.info("Storing email in outbox: '" + emailFile.getAbsolutePath() + "'");
OutputStream emailOutputStream = new FileOutputStream(emailFile);
email.writeTo(emailOutputStream);
emailOutputStream.close();
log.info("Mail folder <" + storageDir + ">: storing email file: <" + emailFile.getAbsolutePath() + ">");
OutputStream emailOutputStream = null;
try {
emailOutputStream = new FileOutputStream(emailFile);
email.writeTo(emailOutputStream);
}
finally {
if (emailOutputStream != null)
try {
emailOutputStream.close();
}
catch (IOException e) {
log.error("Can't close file: <" + emailFile + ">", e);
}
}
}
/**
* Finds an <code>Email</code> by message id. If the <code>Email</code> is
* not found, <code>null</code> is returned.
* The <code>messageId</code> parameter must be a 44-character base64-encoded
* message id. If the message id contains an ampersand, the ampersand and
* everything after it is ignored.
* {@link UniqueId}.
* @param messageId
* @return
*/
public Email getEmail(String messageIdString) {
MessageId messageId = new MessageId(messageIdString);
public Email getEmail(String messageId) {
File file = getEmailFile(messageId);
try {
return createFolderElement(file);
@ -78,11 +100,88 @@ public class EmailFolder extends Folder<Email> {
}
private File getEmailFile(Email email) {
return getEmailFile(email.getMessageID());
return getEmailFile(email.getMessageID(), email.isNew());
}
private File getEmailFile(MessageId messageId) {
return new File(storageDir, messageId.toBase64() + EMAIL_FILE_EXTENSION);
/**
* Returns a file in the file system for a given message ID, or <code>null</code> if
* none exists.
* @param messageId
* @return
*/
private File getEmailFile(String messageId) {
// try new email
File newEmailFile = getEmailFile(messageId, true);
if (newEmailFile.exists())
return newEmailFile;
// try old email
File oldEmailFile = getEmailFile(messageId, false);
if (oldEmailFile.exists())
return oldEmailFile;
return null;
}
private File getEmailFile(String messageId, boolean newIndicator) {
return new File(storageDir, (newIndicator?'N':'O') + "_" + messageId + EMAIL_FILE_EXTENSION);
}
/**
* @see Folder.getNumElements()
* @return
*/
public int getNumNewEmails() {
int numNew = 0;
for (File file: getFilenames())
if (isNew(file))
numNew++;
return numNew;
}
private boolean isNew(File file) {
switch (file.getName().charAt(0)) {
case 'N':
return true;
case 'O':
return false;
default:
throw new IllegalArgumentException("Illegal email filename, doesn't start with N or O: <" + file.getAbsolutePath() + ">");
}
}
/**
* Flags an email "new" (if <code>isNew</code> is <code>true</code>) or
* "old" (if <code>isNew</code> is <code>false</code>).
* @param messageId
* @param isNew
*/
public void setNew(String messageId, boolean isNew) {
File file = getEmailFile(messageId);
if (file != null) {
char newIndicator = isNew?'N':'O'; // the new start character
String newFilename = newIndicator + file.getName().substring(1);
File newFile = new File(file.getParentFile(), newFilename);
boolean success = file.renameTo(newFile);
if (!success)
log.error("Cannot rename <" + file.getAbsolutePath() + "> to <" + newFile.getAbsolutePath() + ">");
}
else
log.error("No email found for message Id: <" + messageId + ">");
}
/**
* Deletes an email with a given message ID.
* @param messageId
* @return <code>true</code> if the email was deleted, <code>false</code> otherwise
*/
public boolean delete(String messageId) {
File emailFile = getEmailFile(messageId);
if (emailFile != null)
return emailFile.delete();
else
return false;
}
public void delete(Email email) {
@ -92,7 +191,12 @@ public class EmailFolder extends Folder<Email> {
@Override
protected Email createFolderElement(File file) throws Exception {
FileInputStream inputStream = new FileInputStream(file);
return new Email(inputStream);
Email email = new Email(file);
email.setNew(isNew(file));
String messageIdString = file.getName().substring(2, 46);
email.setMessageID(messageIdString);
return email;
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.folder;
import i2p.bote.UniqueId;
import i2p.bote.network.PacketListener;
import i2p.bote.packet.CommunicationPacket;
import i2p.bote.packet.EmailPacketDeleteRequest;
import i2p.bote.packet.EncryptedEmailPacket;
import i2p.bote.packet.dht.DhtStorablePacket;
import java.io.File;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.Log;
/**
* A subclass of {@link DhtPacketFolder} that stores email packets and deletes them
* upon {@link EmailPacketDeleteRequest}s.
*/
public class EmailPacketFolder extends DhtPacketFolder<EncryptedEmailPacket> implements PacketListener {
private Log log = new Log(EmailPacketFolder.class);
public EmailPacketFolder(File storageDir) {
super(storageDir);
}
@Override
public void packetReceived(CommunicationPacket packet, Destination sender, long receiveTime) {
if (packet instanceof EmailPacketDeleteRequest) {
EmailPacketDeleteRequest delRequest = (EmailPacketDeleteRequest)packet;
Hash dhtKey = delRequest.getDhtKey();
DhtStorablePacket storedPacket = retrieve(dhtKey);
if (storedPacket instanceof EncryptedEmailPacket) {
UniqueId storedDeletionKey = ((EncryptedEmailPacket)storedPacket).getPlaintextDeletionKey();
if (storedDeletionKey.equals(delRequest.getDeletionKey()))
delete(dhtKey);
else
log.debug("Deletion key in EmailPacketDeleteRequest does not match. Should be: <" + storedDeletionKey + ">, is <" + delRequest.getDeletionKey() +">");
}
else
log.debug("EncryptedEmailPacket expected for DHT key <" + dhtKey + ">, found " + storedPacket.getClass().getSimpleName());
}
}
}

View File

@ -35,7 +35,7 @@ import net.i2p.util.Log;
*
* @param <T> The type of objects the folder can store.
*/
public abstract class Folder<T extends FolderElement> implements Iterable<T> {
public abstract class Folder<T> implements Iterable<T> {
private Log log = new Log(Folder.class);
protected File storageDir;
protected String fileExtension;
@ -52,6 +52,35 @@ public abstract class Folder<T extends FolderElement> implements Iterable<T> {
return storageDir;
}
public int getNumElements() {
return getFilenames().length;
}
/**
* Returns the names of all files in the folder.
* @return
*/
public File[] getFilenames() {
File[] files = storageDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.toUpperCase().endsWith(fileExtension.toUpperCase());
}
});
if (files == null)
log.error("Cannot list files in directory <" + storageDir + ">");
else
// sort files by date, newest first
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
return (int)Math.signum(f2.lastModified() - f1.lastModified());
}
});
return files;
}
public Collection<T> getElements() {
Collection<T> elements = new ArrayList<T>();
Iterator<T> iterator = iterator();
@ -63,24 +92,8 @@ public abstract class Folder<T extends FolderElement> implements Iterable<T> {
// An {@link Iterator} implementation that loads one file into memory at a time.
@Override
public final Iterator<T> iterator() {
final File[] files = storageDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.toUpperCase().endsWith(fileExtension.toUpperCase());
}
});
if (files == null)
log.error("Cannot list files in directory <" + storageDir + ">");
else
log.debug(files.length + " files with the extension '" + fileExtension + "' found in '" + storageDir + "'.");
// sort files by date, newest first
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
return (int)Math.signum(f1.lastModified() - f2.lastModified());
}
});
final File[] files = getFilenames();
log.debug(files.length + " files with the extension '" + fileExtension + "' found in '" + storageDir + "'.");
return new Iterator<T>() {
int nextIndex = 0;
@ -97,7 +110,6 @@ public abstract class Folder<T extends FolderElement> implements Iterable<T> {
log.info("Reading file: '" + filePath + "'");
try {
T nextElement = createFolderElement(file);
nextElement.setFile(file);
nextIndex++;
return nextElement;
}

View File

@ -25,6 +25,7 @@ import i2p.bote.UniqueId;
import i2p.bote.email.Email;
import i2p.bote.packet.DataPacket;
import i2p.bote.packet.I2PBotePacket;
import i2p.bote.packet.MalformedDataPacketException;
import i2p.bote.packet.UnencryptedEmailPacket;
import java.io.ByteArrayOutputStream;
@ -121,7 +122,7 @@ public class IncompleteEmailFolder extends PacketFolder<UnencryptedEmailPacket>
Arrays.sort(packets, new Comparator<UnencryptedEmailPacket>() {
@Override
public int compare(UnencryptedEmailPacket packet1, UnencryptedEmailPacket packet2) {
return new Integer(packet1.getFragmentIndex()).compareTo(packet2.getFragmentIndex());
return Integer.valueOf(packet1.getFragmentIndex()).compareTo(packet2.getFragmentIndex());
}
});
@ -130,6 +131,7 @@ public class IncompleteEmailFolder extends PacketFolder<UnencryptedEmailPacket>
for (UnencryptedEmailPacket packet: packets)
outputStream.write(packet.getContent());
Email email = new Email(outputStream.toByteArray());
email.setMessageID(packets[0].getMessageId()); // all packets in the array have the same message ID
inbox.add(email);
// delete packets
@ -146,11 +148,15 @@ public class IncompleteEmailFolder extends PacketFolder<UnencryptedEmailPacket>
private Collection<UnencryptedEmailPacket> getEmailPackets(File[] files) {
Collection<UnencryptedEmailPacket> packets = new ArrayList<UnencryptedEmailPacket>();
for (File file: files) {
I2PBotePacket packet = DataPacket.createPacket(file);
if (packet instanceof UnencryptedEmailPacket)
packets.add((UnencryptedEmailPacket)packet);
else
log.error("Non-Email Packet found in the IncompleteEmailFolder, file: <" + file.getAbsolutePath() + ">");
try {
I2PBotePacket packet = DataPacket.createPacket(file);
if (packet instanceof UnencryptedEmailPacket)
packets.add((UnencryptedEmailPacket)packet);
else
log.error("Non-Email Packet found in the IncompleteEmailFolder, file: <" + file.getAbsolutePath() + ">");
} catch (MalformedDataPacketException e) {
log.error("Cannot create packet from file: <" + file.getAbsolutePath() + ">", e);
}
}
return packets;
}

View File

@ -21,20 +21,34 @@
package i2p.bote.folder;
import i2p.bote.packet.I2PBotePacket;
import i2p.bote.UniqueId;
import i2p.bote.network.PacketListener;
import i2p.bote.packet.CommunicationPacket;
import i2p.bote.packet.IndexPacket;
import i2p.bote.packet.IndexPacketDeleteRequest;
import i2p.bote.packet.MalformedDataPacketException;
import i2p.bote.packet.dht.DhtStorablePacket;
import java.io.File;
import java.util.Collection;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.Log;
/**
* This class differs from {@link DhtPacketFolder} in that it doesn't overwrite an existing
* packet when a new packet is stored under the same key, but merges the packets.
* This class uses Email Destination hashes for DHT keys.
* It differs from {@link DhtPacketFolder} in two ways:
* * It doesn't overwrite an existing packet when a new packet is stored under the same key,
* but merges the packets.
* * It retains DHT keys of deleted packets in a file named <code>DEL_<dht_key>.pkt</code>
* for later reference. These files use the same format as Index Packet files.
*
*/
public class IndexPacketFolder extends DhtPacketFolder<IndexPacket> {
private final Log log = new Log(I2PBotePacket.class);
public class IndexPacketFolder extends DhtPacketFolder<IndexPacket> implements PacketListener {
private static final String DEL_FILE_PREFIX = "DEL_";
private final Log log = new Log(IndexPacketFolder.class);
public IndexPacketFolder(File storageDir) {
super(storageDir);
@ -58,4 +72,85 @@ public class IndexPacketFolder extends DhtPacketFolder<IndexPacket> {
super.store(packetToStore);
}
@Override
public void delete(Hash dhtKey) {
throw new UnsupportedOperationException("Index packets are never deleted. Use remove(Hash, Hash) to remove an entry from an index packet file.");
}
/**
* Deletes an entry from an {@link IndexPacket} and saves the packet to disk.
* @param indexPacket
* @param emailPacketKey The entry to delete
*/
public void remove(IndexPacket indexPacket, Hash emailPacketKey) {
log.debug("Removing DHT key " + emailPacketKey + " from Index Packet for Email Dest " + indexPacket.getDhtKey());
UniqueId deletionKey = indexPacket.getDeletionKey(emailPacketKey);
if (deletionKey == null)
log.debug("DHT key " + emailPacketKey + " not found in index packet " + indexPacket);
else {
indexPacket.remove(emailPacketKey);
addToDeletedPackets(indexPacket, emailPacketKey, deletionKey);
}
super.store(indexPacket); // don't merge, but overwrite the file with the key removed
}
/**
* Adds a DHT key of an Email Packet to the list of deleted packets.
* If the key is already on the list, nothing happens.
* @param indexPacket
* @param dhtKey
* @param deletionKey
*/
private void addToDeletedPackets(IndexPacket indexPacket, Hash dhtKey, UniqueId deletionKey) {
String delFileName = DEL_FILE_PREFIX + getFilename(indexPacket);
File delFile = new File(storageDir, delFileName);
// read delete list from file or create a new one if file doesn't exist
DhtStorablePacket delListPacket;
if (!delFile.exists())
delListPacket = new IndexPacket(indexPacket);
else {
try {
delListPacket = DhtStorablePacket.createPacket(delFile);
} catch (MalformedDataPacketException e) {
log.error("Cannot read Delete List Packet, creating a new one: <" + delFile.getAbsolutePath() + ">", e);
delListPacket = new IndexPacket(indexPacket);
}
if (!(delListPacket instanceof IndexPacket)) {
log.error("Not an Index Packet file: <" + delFile + ">");
return;
}
}
((IndexPacket)delListPacket).put(dhtKey, deletionKey);
add(delListPacket, delFileName);
}
@Override
public void packetReceived(CommunicationPacket packet, Destination sender, long receiveTime) {
if (packet instanceof IndexPacketDeleteRequest) {
IndexPacketDeleteRequest delRequest = (IndexPacketDeleteRequest)packet;
log.debug("IndexPacketDeleteRequest received. #entries=" + delRequest.getNumEntries());
Hash dhtKey = delRequest.getEmailDestHash();
DhtStorablePacket storedPacket = retrieve(dhtKey);
if (storedPacket instanceof IndexPacket) {
IndexPacket indexPacket = (IndexPacket)storedPacket;
Collection<Hash> keysToDelete = delRequest.getDhtKeys();
for (Hash keyToDelete: keysToDelete) {
UniqueId deletionKeyFromRequest = delRequest.getDeletionKey(keyToDelete);
UniqueId storedDeletionKey = indexPacket.getDeletionKey(keyToDelete);
if (storedDeletionKey == null)
log.debug("Deletion key " + deletionKeyFromRequest + " from IndexPacketDeleteRequest not found in index packet for destination " + dhtKey);
else if (storedDeletionKey.equals(deletionKeyFromRequest))
remove(indexPacket, keyToDelete);
else
log.debug("Deletion key in IndexPacketDeleteRequest does not match. Should be: <" + storedDeletionKey + ">, is <" + deletionKeyFromRequest +">");
}
}
else
log.debug("IndexPacket expected for DHT key <" + dhtKey + ">, found " + storedPacket.getClass().getSimpleName());
}
}
}

View File

@ -27,6 +27,8 @@ import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import javax.mail.MessagingException;
import net.i2p.util.Log;
/**
@ -38,7 +40,7 @@ import net.i2p.util.Log;
*
* Status files contain a status for each recipient address.
*/
public class Outbox extends EmailFolder implements Iterable<Email> {
public class Outbox extends EmailFolder {
private static final String STATUS_FILE_EXTENSION = ".status";
// private static final String PARAM_QUEUE_DATE = "X-QueueDate";
private static final Log log = new Log(Outbox.class);
@ -49,7 +51,7 @@ public class Outbox extends EmailFolder implements Iterable<Email> {
// store one email file + one status file.
@Override
public void add(Email email) throws IOException {
public void add(Email email) throws IOException, MessagingException {
// write out the email file
super.add(email);

View File

@ -44,16 +44,17 @@ public class PacketFolder<PacketType extends DataPacket> extends Folder<PacketTy
super(storageDir, PACKET_FILE_EXTENSION);
}
// TODO rename to write because existing files are overwritten
public <T extends PacketType> void add(T packetToStore) {
String filename = new UniqueId().toBase64() + PACKET_FILE_EXTENSION;
add(packetToStore, filename);
}
/**
*
* @param packetToStore
* @param filename A filename relative to this folder's storage directory.
*/
// TODO rename to write because existing files are overwritten
protected void add(DataPacket packetToStore, String filename) {
FileOutputStream outputStream = null;
try {
@ -74,9 +75,9 @@ public class PacketFolder<PacketType extends DataPacket> extends Folder<PacketTy
}
}
public void delete(UniqueId packetId) {
/* public void delete(UniqueId packetId) {
// TODO
}
}*/
@Override
@SuppressWarnings("unchecked")

View File

@ -0,0 +1,68 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.network;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.data.Destination;
public class BanList {
private static BanList instance;
private Map<Destination, String> bannedPeers;
public synchronized static BanList getInstance() {
if (instance == null)
instance = new BanList();
return instance;
}
private BanList() {
bannedPeers = new ConcurrentHashMap<Destination, String>();
}
public void ban(Destination destination, String reason) {
bannedPeers.put(destination, reason);
}
public void unban(Destination destination) {
bannedPeers.remove(destination);
}
public boolean isBanned(Destination destination) {
return bannedPeers.containsKey(destination);
}
public String getBanReason(Destination destination) {
return bannedPeers.get(destination);
}
public Collection<BannedPeer> getAll() {
Collection<BannedPeer> peerCollection = new ArrayList<BannedPeer>();
for (Entry<Destination, String> entry: bannedPeers.entrySet())
peerCollection.add(new BannedPeer(entry.getKey(), entry.getValue()));
return peerCollection;
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.network;
import net.i2p.data.Destination;
public class BannedPeer {
private Destination destination;
private String banReason;
public BannedPeer(Destination destination, String banReason) {
this.destination = destination;
this.banReason = banReason;
}
public Destination getDestination() {
return destination;
}
public String getBanReason() {
return banReason;
}
}

View File

@ -25,13 +25,16 @@ import i2p.bote.UniqueId;
import i2p.bote.Util;
import i2p.bote.email.EmailIdentity;
import i2p.bote.folder.IncompleteEmailFolder;
import i2p.bote.packet.EmailPacketDeleteRequest;
import i2p.bote.packet.EncryptedEmailPacket;
import i2p.bote.packet.IndexPacket;
import i2p.bote.packet.IndexPacketDeleteRequest;
import i2p.bote.packet.UnencryptedEmailPacket;
import i2p.bote.packet.dht.DhtStorablePacket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@ -41,6 +44,7 @@ import java.util.concurrent.ThreadFactory;
import net.i2p.I2PAppContext;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.Log;
@ -50,7 +54,7 @@ import net.i2p.util.Log;
* on the network.
*/
public class CheckEmailTask implements Callable<Boolean> {
public static final int THREAD_STACK_SIZE = 64 * 1024; // TODO find a safe low value (default in 64-bit Java 1.6 = 1MByte)
public static final int THREAD_STACK_SIZE = 256 * 1024; // TODO find a safe low value (64k is too low, default in 64-bit Java 1.6 = 1MByte)
private static final int MAX_THREADS = 50;
private static final ThreadFactory EMAIL_PACKET_TASK_THREAD_FACTORY = Util.createThreadFactory("EmailPktTask", THREAD_STACK_SIZE);
@ -58,17 +62,18 @@ public class CheckEmailTask implements Callable<Boolean> {
private EmailIdentity identity;
private DHT dht;
private PeerManager peerManager;
private I2PSendQueue sendQueue;
private IncompleteEmailFolder incompleteEmailFolder;
private I2PAppContext appContext;
private ExecutorService executor;
public CheckEmailTask(EmailIdentity identity, DHT dht, PeerManager peerManager, IncompleteEmailFolder incompleteEmailFolder, I2PAppContext appContext) {
// TODO move appContext into EncryptedEmailPacket so there is one less parameter here
public CheckEmailTask(EmailIdentity identity, DHT dht, PeerManager peerManager, I2PSendQueue sendQueue, IncompleteEmailFolder incompleteEmailFolder, I2PAppContext appContext) {
this.identity = identity;
this.dht = dht;
this.peerManager = peerManager;
this.sendQueue = sendQueue;
this.incompleteEmailFolder = incompleteEmailFolder;
this.appContext = appContext;
executor = Executors.newFixedThreadPool(MAX_THREADS, EMAIL_PACKET_TASK_THREAD_FACTORY);
}
/**
@ -77,13 +82,21 @@ public class CheckEmailTask implements Callable<Boolean> {
*/
@Override
public Boolean call() {
Collection<Hash> emailPacketKeys = findEmailPacketKeys();
// Use findAll rather than findOne because some peers might have an incomplete set of
// Email Packet keys, and because we want to send IndexPacketDeleteRequests to all of them.
DhtResults indexPacketResults = dht.findAll(identity.getHash(), IndexPacket.class);
if (indexPacketResults.isEmpty())
return false;
Collection<Hash> emailPacketKeys = findEmailPacketKeys(indexPacketResults.getPackets());
Collection<Future<Boolean>> results = new ArrayList<Future<Boolean>>();
for (Hash dhtKey: emailPacketKeys) {
Future<Boolean> result = executor.submit(new EmailPacketTask(dhtKey));
ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS, EMAIL_PACKET_TASK_THREAD_FACTORY);
for (Hash emailPacketKey: emailPacketKeys) {
Future<Boolean> result = executor.submit(new EmailPacketTask(emailPacketKey, identity.getHash(), indexPacketResults.getPeers()));
results.add(result);
}
executor.shutdown(); // finish all tasks, then shut down
boolean newEmail = false;
for (Future<Boolean> result: results)
@ -100,15 +113,15 @@ public class CheckEmailTask implements Callable<Boolean> {
/**
* Queries the DHT for new index packets and returns the DHT keys contained in them.
* @return A <code>Collection</code> containing zero or more elements
* @param indexPacketResults TODO comment
* @return A <code>Collection</code> containing zero or more DHT keys
*/
private Collection<Hash> findEmailPacketKeys() {
private Collection<Hash> findEmailPacketKeys(Collection<DhtStorablePacket> indexPacketResults) {
log.debug("Querying the DHT for index packets with key " + identity.getHash());
Collection<DhtStorablePacket> packets = dht.findAll(identity.getHash(), IndexPacket.class);
// build an Collection of index packets
// build a Collection of index packets
Collection<IndexPacket> indexPackets = new ArrayList<IndexPacket>();
for (DhtStorablePacket packet: packets)
for (DhtStorablePacket packet: indexPacketResults)
if (packet instanceof IndexPacket)
indexPackets.add((IndexPacket)packet);
else
@ -124,14 +137,21 @@ public class CheckEmailTask implements Callable<Boolean> {
* and deletes the packet from the DHT.
*/
private class EmailPacketTask implements Callable<Boolean> {
private Hash dhtKey;
private Hash emailPacketKey;
private Hash emailIdentityHash;
private Set<Destination> indexPacketPeers;
/**
*
* @param dhtKey The DHT key of the email packet to retrieve
* @param emailPacketKey The DHT key of the email packet to retrieve
* @param emailIdentityHash The DHT key of the packet recipient's email identity
* @param indexPacketPeers All peers that are known to be storing an index packet
* for the email destination the email packet is being sent to.
*/
public EmailPacketTask(Hash dhtKey) {
this.dhtKey = dhtKey;
public EmailPacketTask(Hash emailPacketKey, Hash emailIdentityHash, Set<Destination> indexPacketPeers) {
this.emailPacketKey = emailPacketKey;
this.emailIdentityHash = emailIdentityHash;
this.indexPacketPeers = indexPacketPeers;
}
/**
@ -141,32 +161,58 @@ public class CheckEmailTask implements Callable<Boolean> {
@Override
public Boolean call() {
boolean emailCompleted = false;
DhtStorablePacket packet = dht.findOne(dhtKey, EncryptedEmailPacket.class);
if (packet instanceof EncryptedEmailPacket) {
EncryptedEmailPacket emailPacket = (EncryptedEmailPacket)packet;
try {
UnencryptedEmailPacket decryptedPacket = emailPacket.decrypt(identity, appContext);
emailCompleted = incompleteEmailFolder.addEmailPacket(decryptedPacket);
sendDeleteRequest(dhtKey, decryptedPacket.getVerificationDeletionKey());
}
catch (DataFormatException e) {
log.error("Can't decrypt email packet: " + emailPacket, e);
// TODO propagate error message to UI
// Use findAll rather than findOne because after we receive an email packet, we want
// to send delete requests to as many of the storage nodes as possible.
DhtResults results = dht.findAll(emailPacketKey, EncryptedEmailPacket.class);
EncryptedEmailPacket validPacket = null; // stays null until a valid packet is found in the loop below
IndexPacketDeleteRequest indexDelRequest = new IndexPacketDeleteRequest(emailIdentityHash);
for (Destination peer: results.getPeers()) {
DhtStorablePacket packet = results.getPacket(peer);
if (packet instanceof EncryptedEmailPacket) {
EncryptedEmailPacket emailPacket = (EncryptedEmailPacket)packet;
try {
UnencryptedEmailPacket decryptedPacket = emailPacket.decrypt(identity, appContext);
if (validPacket == null) {
emailCompleted = incompleteEmailFolder.addEmailPacket(decryptedPacket);
validPacket = emailPacket;
}
UniqueId delKey = decryptedPacket.getVerificationDeletionKey();
sendDeleteRequest(emailPacketKey, delKey, peer);
indexDelRequest.put(emailPacketKey, delKey);
}
catch (DataFormatException e) {
log.error("Can't decrypt email packet: " + emailPacket, e);
// TODO propagate error message to UI
}
}
else
if (packet != null)
log.error("DHT returned packet of class " + packet.getClass().getSimpleName() + ", expected EmailPacket.");
}
else
if (packet != null)
log.error("DHT returned packet of class " + packet.getClass().getSimpleName() + ", expected EmailPacket.");
if (indexDelRequest.getNumEntries() > 0)
sendDeleteRequest(indexDelRequest, indexPacketPeers);
return emailCompleted;
}
/**
* Sends a delete request to the DHT.
* Sends an Email Packet Delete Request to a peer.
* @param dhtKey The DHT key of the email packet that is to be deleted
* @param deletionKey The deletion key for the email packet
* @param peer
*/
private void sendDeleteRequest(Hash dhtKey, UniqueId deletionKey) {
// TODO
private void sendDeleteRequest(Hash dhtKey, UniqueId deletionKey, Destination peer) {
EmailPacketDeleteRequest packet = new EmailPacketDeleteRequest(dhtKey, deletionKey);
log.debug("Sending an EmailPacketDeleteRequest for DHT key " + dhtKey + " to " + peer.calculateHash());
sendQueue.send(packet, peer);
}
private void sendDeleteRequest(IndexPacketDeleteRequest indexDelRequest, Set<Destination> peers) {
log.debug("Sending an IndexPacketDeleteRequest to " + peers.size() + " peers: " + indexDelRequest);
for (Destination peer: peers)
sendQueue.send(indexDelRequest, peer);
}
}
}

View File

@ -22,18 +22,15 @@
package i2p.bote.network;
import i2p.bote.packet.dht.DhtStorablePacket;
import java.util.Collection;
import net.i2p.data.Hash;
public interface DHT {
void store(DhtStorablePacket packet) throws Exception;
void store(DhtStorablePacket packet) throws DhtException;
DhtStorablePacket findOne(Hash key, Class<? extends DhtStorablePacket> dataType);
DhtResults findOne(Hash key, Class<? extends DhtStorablePacket> dataType);
Collection<DhtStorablePacket> findAll(Hash key, Class<? extends DhtStorablePacket> dataType);
DhtResults findAll(Hash key, Class<? extends DhtStorablePacket> dataType);
/**
* Registers a <code>DhtStorageHandler</code> that handles incoming storage requests of a certain
@ -43,13 +40,29 @@ public interface DHT {
*/
void setStorageHandler(Class<? extends DhtStorablePacket> packetType, DhtStorageHandler storageHandler);
/**
* Returns <code>true</code> if a connection to the DHT has been established.
* @return
*/
boolean isConnected();
/**
* Returns the current number of known active peers.
* @return
*/
int getNumPeers();
DhtPeerStats getPeerStats();
void start();
void shutDown();
void requestShutdown();
/**
* Waits <code>timeout</code> milliseconds for the DHT engine to exit
* after {@link requestShutdown} has been called.
* @param timeout
* @throw InterruptedException
*/
void awaitShutdown(long timeout) throws InterruptedException;
}

View File

@ -19,12 +19,16 @@
* along with I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.network.kademlia;
package i2p.bote.network;
public class KademliaException extends Exception {
public class DhtException extends Exception {
private static final long serialVersionUID = -1286128818859245017L;
public KademliaException(String message) {
public DhtException(String message) {
super(message);
}
public DhtException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -19,21 +19,25 @@
* along with I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.folder;
package i2p.bote.network;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
public interface FolderElement {
/**
* Holds information on currently known peers in table form,
* for displaying it on the UI.
*/
public interface DhtPeerStats {
void setFile(File file);
/**
* Returns the {@link File} from which this <code>FolderElement</code> was read, or <code>null</code>.
* Returns the header row for the table
* @return
*/
File getFile();
void writeTo(OutputStream outputStream) throws IOException;
public List<String> getHeader();
/**
* Returns the table data, one row per peer.
* @return
*/
public List<List<String>> getData();
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.network;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.data.Destination;
import i2p.bote.packet.dht.DhtStorablePacket;
public class DhtResults implements Iterable<DhtStorablePacket> {
private Map<Destination, DhtStorablePacket> map;
public DhtResults() {
map = new ConcurrentHashMap<Destination, DhtStorablePacket>();
}
public void put(Destination peer, DhtStorablePacket packet) {
map.put(peer, packet);
}
public int getNumResults() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public Collection<DhtStorablePacket> getPackets() {
return map.values();
}
public Set<Destination> getPeers() {
return map.keySet();
}
public DhtStorablePacket getPacket(Destination peer) {
return map.get(peer);
}
@Override
public Iterator<DhtStorablePacket> iterator() {
return getPackets().iterator();
}
// Returns all packets except for the local result.
/* public Collection<DhtStorablePacket> getRemoteResults() {
ArrayList<DhtStorablePacket> packets = new ArrayList<DhtStorablePacket>();
for (Destination peer: map.keySet())
if (!peer.calculateHash().equals(localDestHash))
packets.add(map.get(peer));
return packets;
}*/
}

View File

@ -23,6 +23,8 @@ package i2p.bote.network;
import i2p.bote.packet.CommunicationPacket;
import i2p.bote.packet.I2PBotePacket;
import i2p.bote.packet.MalformedCommunicationPacket;
import i2p.bote.packet.MalformedCommunicationPacketException;
import java.util.ArrayList;
import java.util.Collections;
@ -85,21 +87,24 @@ public class I2PPacketDispatcher implements I2PSessionListener {
byte[] payload = datagramDissector.extractPayload();
Destination sender = datagramDissector.getSender();
CommunicationPacket packet = CommunicationPacket.createPacket(payload);
if (packet == null)
log.debug("Ignoring unparseable packet.");
else {
CommunicationPacket packet;
try {
packet = CommunicationPacket.createPacket(payload);
logPacket(packet, sender);
firePacketReceivedEvent(packet, sender);
} catch (MalformedCommunicationPacketException e) {
log.debug("Ignoring unparseable packet.", e);
firePacketReceivedEvent(new MalformedCommunicationPacket(), sender);
}
}
catch (DataFormatException e) {
log.error("Invalid datagram received.", e);
e.printStackTrace();
}
catch (I2PInvalidDatagramException e) {
log.error("Datagram failed verification.", e);
e.printStackTrace();
}
catch (Exception e) {
log.error("Error processing datagram.", e);
}
}

View File

@ -24,6 +24,7 @@ package i2p.bote.network;
import i2p.bote.UniqueId;
import i2p.bote.packet.CommunicationPacket;
import i2p.bote.packet.DataPacket;
import i2p.bote.packet.EmptyResponse;
import i2p.bote.packet.RelayPacket;
import i2p.bote.packet.RelayRequest;
import i2p.bote.packet.ResponsePacket;
@ -31,13 +32,10 @@ import i2p.bote.packet.StatusCode;
import i2p.bote.service.I2PBoteThread;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -63,11 +61,8 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
private Log log = new Log(I2PSendQueue.class);
private I2PSession i2pSession;
private PacketQueue packetQueue;
private Map<UniqueId, ScheduledPacket> outstandingRequests; // sent packets that haven't been responded to
private Set<PacketBatch> runningBatches;
private int maxBandwidth;
private Collection<SendQueuePacketListener> sendQueueListeners;
private Map<SendQueuePacketListener, Long> timeoutValues;
/**
* @param i2pSession
@ -79,46 +74,17 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
this.i2pSession = i2pSession;
i2pReceiver.addPacketListener(this);
packetQueue = new PacketQueue();
outstandingRequests = new ConcurrentHashMap<UniqueId, ScheduledPacket>();
runningBatches = new ConcurrentHashSet<PacketBatch>();
sendQueueListeners = Collections.synchronizedCollection(new ArrayList<SendQueuePacketListener>());
timeoutValues = new HashMap<SendQueuePacketListener, Long>();
}
/**
* Queues a packet behind the last undelayed packet.
* @param packet
* @param destination
* @return
*/
public void send(CommunicationPacket packet, Destination destination) {
send(packet, destination, 0);
}
/**
* Queues a packet behind the last undelayed packet and waits for up to
* <code>timeoutSeconds</code> for a response.
* @param packet
* @param destination
* @param timeoutMilliSeconds
* @return The response packet, or <code>null</code> if a timeout occurred.
* @throws InterruptedException
*/
public CommunicationPacket sendAndWait(CommunicationPacket packet, Destination destination, long timeoutMilliSeconds) throws InterruptedException {
ScheduledPacket scheduledPacket = new ScheduledPacket(packet, destination, 0);
long scheduleTime = System.currentTimeMillis();
packetQueue.add(scheduledPacket);
scheduledPacket.awaitSending();
outstandingRequests.put(packet.getPacketId(), scheduledPacket);
scheduledPacket.awaitResponse(timeoutMilliSeconds, TimeUnit.MILLISECONDS);
if (log.shouldLog(Log.DEBUG)) {
long responseTime = scheduledPacket.getResponseTime();
String responseInfo = responseTime<0?"timed out":"took " + (responseTime-scheduledPacket.getSentTime()) + "ms.";
log.debug("Packet with id " + packet.getPacketId() + " was queued for " + (scheduledPacket.getSentTime()-scheduleTime) + " ms, response " + responseInfo);
}
return scheduledPacket.getResponse();
public CountDownLatch send(CommunicationPacket packet, Destination destination) {
return send(packet, destination, 0);
}
/**
@ -126,9 +92,12 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
* @param packet
* @param destination
* @param earliestSendTime
* @return
*/
public void send(CommunicationPacket packet, Destination destination, long earliestSendTime) {
packetQueue.add(new ScheduledPacket(packet, destination, earliestSendTime));
public CountDownLatch send(CommunicationPacket packet, Destination destination, long earliestSendTime) {
ScheduledPacket scheduledPacket = new ScheduledPacket(packet, destination, earliestSendTime);
packetQueue.add(scheduledPacket);
return scheduledPacket.getSentLatch();
}
public void sendRelayRequest(RelayPacket relayPacket, HashCash hashCash, long earliestSendTime) {
@ -177,10 +146,6 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
runningBatches.remove(batch);
}
public int getQueueLength() {
return packetQueue.size();
}
/**
* Set the maximum outgoing bandwidth in kbits/s
* @param maxBandwidth
@ -197,57 +162,22 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
return maxBandwidth;
}
public void addSendQueueListener(SendQueuePacketListener listener) {
addSendQueueListener(listener, null);
}
/**
* Add a {@link SendQueuePacketListener} that is automatically removed after
* <code>timeout</code> milliseconds.
* @param listener
* @param timeout
*/
public void addSendQueueListener(SendQueuePacketListener listener, Long timeout) {
sendQueueListeners.add(listener);
timeoutValues.put(listener, timeout);
}
public void removeSendQueueListener(SendQueuePacketListener listener) {
sendQueueListeners.remove(listener);
timeoutValues.remove(listener);
}
private void firePacketListeners(CommunicationPacket packet) {
for (SendQueuePacketListener listener: sendQueueListeners)
listener.packetSent(packet);
}
// Implementation of PacketListener
@Override
public void packetReceived(CommunicationPacket packet, Destination sender, long receiveTime) {
if (packet instanceof ResponsePacket) {
log.debug("Response Packet received: Packet Id = " + packet.getPacketId() + " Sender = " + sender);
log.debug("Response Packet received: Packet Id = " + packet.getPacketId() + " Sender = " + sender.calculateHash());
UniqueId packetId = packet.getPacketId();
for (PacketBatch batch: runningBatches)
if (batch.contains(packetId))
batch.addResponse(((ResponsePacket)packet).getPayload());
// Remove the original request if it is on the list, and notify objects waiting on a response
ScheduledPacket requestPacket = outstandingRequests.remove(packetId);
if (requestPacket != null) {
requestPacket.setResponse(packet);
requestPacket.decrementResponseLatch();
}
// If there is a packet file for the original packet, it can be deleted now.
// Use the cached filename if the packet was in the cache; otherwise, find the file by packet Id
/* ScheduledPacket cachedPacket = recentlySentCache.remove(packetId);
if (cachedPacket != null)
fileManager.removePacketFile(cachedPacket);
else
fileManager.removePacketFile(packetId);*/
for (PacketBatch batch: runningBatches)
if (batch.contains(packetId)) {
DataPacket payload = ((ResponsePacket)packet).getPayload();
if (payload != null)
batch.addResponse(sender, payload);
else
batch.addResponse(sender, new EmptyResponse());
}
}
}
@ -256,6 +186,9 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
I2PDatagramMaker datagramMaker = new I2PDatagramMaker(i2pSession);
while (true) {
if (shutdownRequested() && packetQueue.isEmpty())
break;
ScheduledPacket scheduledPacket;
try {
scheduledPacket = packetQueue.take();
@ -264,9 +197,11 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
log.warn("Interrupted while waiting for new packets.", e);
break;
}
if (shutdownRequested() && scheduledPacket==null) // send remaining packets before shutting down
break;
CommunicationPacket i2pBotePacket = scheduledPacket.data;
// wait long enough so rate <= maxBandwidth;
// wait long enough so rate <= maxBandwidth, and don't send before earliestSendTime
if (maxBandwidth > 0) {
int packetSizeBits = i2pBotePacket.getSize() * 8;
int maxBWBitsPerSecond = maxBandwidth * 1024;
@ -283,21 +218,19 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
PacketBatch batch = scheduledPacket.batch;
boolean isBatchPacket = batch != null;
log.debug("Sending " + (isBatchPacket?"":"non-") + "batch packet: [" + i2pBotePacket + "] to peer: " + scheduledPacket.destination.toBase64());
log.debug("Sending " + (isBatchPacket?"":"non-") + "batch packet: [" + i2pBotePacket + "] to peer: " + scheduledPacket.destination.calculateHash());
byte[] replyableDatagram = datagramMaker.makeI2PDatagram(i2pBotePacket.toByteArray());
try {
i2pSession.sendMessage(scheduledPacket.destination, replyableDatagram);
// set sentTime; update queue+cache, update countdown latch, fire packet listeners
scheduledPacket.data.setSentTime(System.currentTimeMillis());
packetQueue.remove(scheduledPacket);
// set sentTime, update queue and sentLatch, fire packet listeners
scheduledPacket.data.setSentTime(System.currentTimeMillis());
if (isBatchPacket)
batch.decrementSentLatch();
scheduledPacket.decrementSentLatch();
firePacketListeners(scheduledPacket.data);
log.debug("Packet sent. Send queue length is now " + packetQueue.size());
log.debug("Packet of type " + scheduledPacket.data.getClass().getSimpleName() + " sent. Send queue length is now " + packetQueue.size());
if (isBatchPacket)
log.debug(" Batch has " + batch.getPacketCount() + " packets total, " + batch.getUnsentPacketCount() + " waiting to be sent.");
}
@ -312,49 +245,71 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
}
}
}
log.info("I2PSendQueue exiting.");
}
private class PacketQueue extends LinkedList<ScheduledPacket> {
public void add(ScheduledPacket packet, long earliestSendTime) {
int index = getInsertionIndex(packet, earliestSendTime);
add(index, packet);
/**
* Keeps packets sorted by <code>earliestSendTime</code>.
*/
private class PacketQueue {
private List<ScheduledPacket> queue;
private Comparator<ScheduledPacket> comparator;
public PacketQueue() {
queue = new ArrayList<ScheduledPacket>();
comparator = new Comparator<ScheduledPacket>() {
@Override
public int compare(ScheduledPacket packet1, ScheduledPacket packet2) {
return new Long(packet1.earliestSendTime).compareTo(packet2.earliestSendTime);
}
};
}
public synchronized void add(ScheduledPacket packet) {
if (packet.earliestSendTime > 0) {
int index = getInsertionIndex(packet);
queue.add(index, packet);
}
else
queue.add(packet);
}
private int getInsertionIndex(ScheduledPacket packet, long earliestSendTime) {
if (isEmpty())
return 0;
// do a linear search (binary search isn't a good fit for LinkedList)
// TODO using foreach would be more efficient
for (int i=0; i<size(); i++)
if (get(i).earliestSendTime <= earliestSendTime)
return i;
return size();
private int getInsertionIndex(ScheduledPacket packet) {
int index = Collections.binarySearch(queue, packet, comparator);
if (index < 0)
return -(index+1);
else {
log.warn("Packet exists in queue already: " + packet);
return index;
}
}
/**
* Not synchronized because there should only be one {@link I2PSendQueue}.
* @throws InterruptedException
* @return
*/
public ScheduledPacket take() throws InterruptedException {
while (isEmpty())
while (queue.isEmpty() && !shutdownRequested())
TimeUnit.SECONDS.sleep(1);
return pollLast();
synchronized(this) {
if (queue.isEmpty())
return null;
else
return queue.remove(queue.size()-1);
}
}
public synchronized int size() {
return queue.size();
}
public synchronized boolean isEmpty() {
return queue.isEmpty();
}
}
private class ScheduledPacket implements Comparable<ScheduledPacket> {
private static class ScheduledPacket {
CommunicationPacket data;
Destination destination;
long earliestSendTime;
PacketBatch batch; // the batch this packet belongs to, or null if not part of a batch
long sentTime;
long responseTime;
CountDownLatch sentSignal;
CountDownLatch responseSignal;
CommunicationPacket response;
public ScheduledPacket(CommunicationPacket packet, Destination destination, long earliestSendTime) {
this(packet, destination, earliestSendTime, null);
@ -366,57 +321,14 @@ public class I2PSendQueue extends I2PBoteThread implements PacketListener {
this.earliestSendTime = earliestSendTime;
this.batch = batch;
this.sentSignal = new CountDownLatch(1);
this.responseSignal = new CountDownLatch(1);
this.responseTime = -1;
}
@Override
public int compareTo(ScheduledPacket anotherPacket) {
return new Long(earliestSendTime).compareTo(anotherPacket.earliestSendTime);
}
public void decrementSentLatch() {
sentTime = System.currentTimeMillis();
sentSignal.countDown();
}
public void awaitSending() throws InterruptedException {
sentSignal.await();
}
public long getSentTime() {
return sentTime;
}
/**
* Returns the time at which a response packet was received, or -1 if no response.
* @return
*/
public long getResponseTime() {
return responseTime;
}
public void decrementResponseLatch() {
responseTime = System.currentTimeMillis();
responseSignal.countDown();
}
public void awaitResponse(long timeout, TimeUnit unit) throws InterruptedException {
responseSignal.await(timeout, unit);
}
public void setResponse(CommunicationPacket response) {
this.response = response;
}
/**
* If this packet was sent via <code>sendAndWait</code>, and a response packet has been
* received (i.e., a packet with the same packet id), this method returns the response packet.
* Otherwise, <code>null</code> is returned.
* @return
*/
public CommunicationPacket getResponse() {
return response;
public CountDownLatch getSentLatch() {
return sentSignal;
}
}
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.network;
// TODO comment
public enum NetworkStatus {
NOT_STARTED, DELAY, CONNECTING, CONNECTED, ERROR;
}

View File

@ -27,13 +27,11 @@ import i2p.bote.packet.DataPacket;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import net.i2p.data.Destination;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.Log;
/**
@ -44,17 +42,18 @@ import net.i2p.util.Log;
public class PacketBatch implements Iterable<PacketBatchItem> {
private final Log log = new Log(PacketBatch.class);
private Map<UniqueId, PacketBatchItem> outgoingPackets;
private Set<DataPacket> incomingPackets;
private Map<Destination, DataPacket> incomingPackets;
private CountDownLatch sentSignal; // this field is initialized by I2PSendQueue when the batch is submitted for sending
private CountDownLatch firstReplyReceivedSignal;
public PacketBatch() {
outgoingPackets = new ConcurrentHashMap<UniqueId, PacketBatchItem>();
incomingPackets = new ConcurrentHashSet<DataPacket>();
incomingPackets = new ConcurrentHashMap<Destination, DataPacket>();
sentSignal = new CountDownLatch(0);
firstReplyReceivedSignal = new CountDownLatch(1);
}
// TODO throw an exception if this method is called after the batch has been submitted to the queue
public synchronized void putPacket(CommunicationPacket packet, Destination destination) {
outgoingPackets.put(packet.getPacketId(), new PacketBatchItem(packet, destination));
sentSignal = new CountDownLatch((int)sentSignal.getCount() + 1);
@ -101,12 +100,12 @@ public class PacketBatch implements Iterable<PacketBatchItem> {
outgoingPackets.get(packetId).confirmDelivery();
}*/
void addResponse(DataPacket packet) {
incomingPackets.add(packet);
void addResponse(Destination peer, DataPacket packet) {
incomingPackets.put(peer, packet);
firstReplyReceivedSignal.countDown();
}
public Set<DataPacket> getResponses() {
public Map<Destination, DataPacket> getResponses() {
return incomingPackets;
}
@ -134,4 +133,16 @@ public class PacketBatch implements Iterable<PacketBatchItem> {
public void awaitFirstReply(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
firstReplyReceivedSignal.await(timeout, timeoutUnit);
}
public void awaitAllResponses(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
long startTime = System.currentTimeMillis();
long timeoutMillis = timeoutUnit.toMillis(timeout);
long endTime = startTime + timeoutMillis;
log.debug("Waiting for responses to batch packets. Start time=" + startTime + ", end time=" + endTime);
while (System.currentTimeMillis()<=endTime && incomingPackets.size()<outgoingPackets.size())
TimeUnit.SECONDS.sleep(1);
log.debug("Finished waiting. Time now: " + System.currentTimeMillis() + ", #incoming=" + incomingPackets.size() + ", #outgoing=" + outgoingPackets.size());
}
}

View File

@ -0,0 +1,115 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.network.kademlia;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
/**
* This is the parent class for k-buckets and s-buckets.
*
* <strong>Iterators returned by this class are not thread safe. This
* includes iterating over bucket entries via a <code>foreach</code> loop.
* </strong>
*/
abstract class AbstractBucket implements Iterable<KademliaPeer> {
static final BigInteger MIN_HASH_VALUE = BigInteger.ZERO; // system-wide minimum hash value
static final BigInteger MAX_HASH_VALUE = BigInteger.ONE.shiftLeft(Hash.HASH_LENGTH*8).subtract(BigInteger.ONE); // system-wide maximum hash value
protected List<KademliaPeer> peers; // peers are sorted most recently seen to least recently seen
protected int capacity;
public AbstractBucket(int capacity) {
peers = Collections.synchronizedList(new ArrayList<KademliaPeer>());
this.capacity = capacity;
}
/**
* Removes a peer from the bucket. If the peer doesn't exist in the bucket, nothing happens.
* @param node
*/
void remove(Destination destination) {
peers.remove(destination);
}
Collection<KademliaPeer> getPeers() {
return peers;
}
/**
* Looks up a <code>KademliaPeer</code> by I2P destination. If the bucket
* doesn't contain the peer, <code>null</code> is returned.
* @param destination
* @return
*/
protected KademliaPeer getPeer(Destination destination) {
int index = getPeerIndex(destination);
if (index >= 0)
return peers.get(index);
else
return null;
}
/**
* Looks up the index of a <code>Destination</code> in the bucket.
* if nothing is found, -1 is returned.
* @param destination
* @return
*/
protected int getPeerIndex(Destination destination) {
// An alternative to indexOf, which does a linear search, would be to maintain a Map<Destination, KademliaPeer>.
return peers.indexOf(destination);
}
/**
* Returns <code>true</code> if a peer exists in the bucket.
* @param destination
* @return
*/
boolean contains(Destination destination) {
return getPeer(destination) != null;
}
boolean isFull() {
return size() >= capacity;
}
boolean isEmpty() {
return peers.isEmpty();
}
int size() {
return peers.size();
}
@Override
public Iterator<KademliaPeer> iterator() {
return peers.iterator();
}
}

View File

@ -21,324 +21,328 @@
package i2p.bote.network.kademlia;
import i2p.bote.network.I2PSendQueue;
import i2p.bote.I2PBote;
import i2p.bote.network.BanList;
import i2p.bote.network.DhtPeerStats;
import i2p.bote.network.PacketListener;
import i2p.bote.packet.CommunicationPacket;
import i2p.bote.service.I2PBoteThread;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.Log;
import net.i2p.util.RandomSource;
// TODO if a sibling times out, refill the sibling table
public class BucketManager extends I2PBoteThread implements PacketListener {
private static final int INTERVAL = 5 * 60 * 1000;
private static final int PING_TIMEOUT = 20 * 1000;
/**
* The k-bucket tree isn't actually implemented as a tree, but a {@link List}.
*/
class BucketManager implements PacketListener, Iterable<KBucket> {
private Log log = new Log(BucketManager.class);
private I2PSendQueue sendQueue;
private List<KBucket> kBuckets;
private KBucket siblingBucket; // TODO [ordered furthest away to closest in terms of hash distance to local node]
private SBucket sBucket; // The sibling bucket
private Hash localDestinationHash;
public BucketManager(I2PSendQueue sendQueue, Collection<KademliaPeer> initialPeers, Hash localDestinationHash) {
super("BucketMgr");
this.sendQueue = sendQueue;
public BucketManager(Hash localDestinationHash) {
this.localDestinationHash = localDestinationHash;
kBuckets = Collections.synchronizedList(new ArrayList<KBucket>());
kBuckets.add(new KBucket(KBucket.MIN_HASH_VALUE, KBucket.MAX_HASH_VALUE, 0, true)); // this is the root bucket, so depth=0
siblingBucket = new KBucket(KBucket.MIN_HASH_VALUE, KBucket.MAX_HASH_VALUE, 0, false);
addAll(initialPeers);
kBuckets.add(new KBucket(AbstractBucket.MIN_HASH_VALUE, AbstractBucket.MAX_HASH_VALUE, 0)); // this is the root bucket, so depth=0
sBucket = new SBucket(localDestinationHash);
}
public void addAll(Collection<KademliaPeer> nodes) {
for (KademliaPeer node: nodes)
/**
* Calls <code>addOrUpdate(KademliaPeer)</code> for one or more peers.
* @param peers
*/
public void addAll(Collection<KademliaPeer> peers) {
for (KademliaPeer node: peers)
addOrUpdate(node);
}
/**
* Add a <code>{@link KademliaPeer}</code> to the sibling list or a bucket.
* @param peer
* Adds a <code>{@link KademliaPeer}</code> to the s-bucket or a k-bucket,
* depending on its distance to the local node and how full the buckets are.
* @param destination
*/
public void addOrUpdate(KademliaPeer peer) {
Hash peerHash = peer.getDestinationHash();
log.debug("Adding/updating peer: Hash = " + peerHash);
peer.resetStaleCounter();
synchronized(this) {
if (!siblingBucket.isFull() || siblingBucket.contains(peer)) {
siblingBucket.addOrUpdate(peer);
getBucket(peerHash).remove(peer);
}
else if (isCloserSibling(peer)) {
KademliaPeer ejectedPeer = siblingBucket.getMostDistantPeer(localDestinationHash);
addToBucket(ejectedPeer);
siblingBucket.remove(ejectedPeer);
siblingBucket.addOrUpdate(peer);
getBucket(peerHash).remove(peer);
}
else
addToBucket(peer);
Hash destHash = peer.getDestinationHash();
if (localDestinationHash.equals(destHash)) {
log.debug("Not adding local destination to bucket.");
return;
}
logBucketStats();
/* KademliaPeer ejectedPeer = addSibling(peer);
// if the peer was added as a sibling, it may need to be removed from a bucket
if (ejectedPeer != peer)
getBucket(peerHash).remove(peer);
// if the peer didn't get added as a sibling, try a bucket
log.debug("Adding/updating peer: Hash = " + destHash);
KademliaPeer removedOrNotAdded = sBucket.addOrUpdate(peer);
if (removedOrNotAdded == null)
getKBucket(destHash).remove(peer); // if the peer was in a k-bucket, remove it because it is now in the s-bucket
else
addToBucket(peer);
// if adding the peer to the list of siblings replaced another sibling, add the old sibling to a bucket
if (ejectedPeer != null)
addToBucket(ejectedPeer);*/
/*TODO synchronized(siblings) {
if (siblings.isFull()) {
KBucket bucket = getBucket(nodeHash);
KademliaPeer mostDistantSibling = getMostDistantSibling();
if (getDistance(node.getDestinationHash()) < getDistance(mostDistantSibling)) {
bucket.addOrUpdate(mostDistantSibling);
siblings.remove(mostDistantSibling);
siblings.add(node);
}
else
bucket.addOrUpdate(node);
}
else {
siblings.add(node);
}*/
addToKBucket(removedOrNotAdded); // if a peer was removed from the s-bucket or didn't qualify as a sibling, add it to a k-bucket
logBucketStats();
}
/**
* Adds a peer to the appropriate k-bucket, splitting the bucket if necessary, or updates the
* peer if it exists in the bucket.
* @param peer
*/
private void addToKBucket(KademliaPeer peer) {
int bucketIndex = getBucketIndex(peer.calculateHash());
KBucket bucket = kBuckets.get(bucketIndex);
if (bucket.shouldSplit(peer)) {
KBucket newBucket = bucket.split();
kBuckets.add(bucketIndex+1, newBucket); // the new bucket is one higher than the old bucket
// if all peers ended up in one bucket (overfilling it and leaving the other bucket empty), split again
while (newBucket.isEmpty() || bucket.isEmpty())
if (newBucket.isEmpty()) {
newBucket = bucket.split();
kBuckets.add(bucketIndex+1, newBucket);
}
else { // if bucket.isEmpty()
bucketIndex++;
bucket = newBucket;
newBucket = newBucket.split();
kBuckets.add(bucketIndex+1, newBucket);
}
bucket = getKBucket(peer.calculateHash());
}
bucket.addOrUpdate(peer);
}
/**
* Notifies the <code>BucketManager</code> that a peer didn't respond to
* a request.
* @param destination
*/
public synchronized void noResponse(Destination destination) {
KademliaPeer peer = getPeer(destination);
if (peer != null)
peer.noResponse();
else
log.debug("Peer not found in buckets: " + destination.calculateHash());
}
private void logBucketStats() {
int numBuckets = kBuckets.size();
int numPeers = getAllPeers().size();
int numSiblings = sBucket.size();
log.debug("#buckets=" + numBuckets + "+sibBkt, #peers=" + numPeers);
log.debug("total #peers=" + numPeers + ", #siblings=" + numSiblings + ", #buckets=" + numBuckets + " (not counting the sibling bucket)");
}
/**
* Add a <code>{@link KademliaPeer}</code> to the appropriate bucket.
* @param peer
*/
private void addToBucket(KademliaPeer peer) {
Hash nodeHash = peer.getDestinationHash();
KBucket bucket = getBucket(nodeHash);
KBucket newBucket = bucket.addOrSplit(peer);
if (newBucket != null)
kBuckets.add(newBucket);
}
/**
* Return <code>true</code> if a given peer is closer to the local node than at
* least one sibling. In other words, test if <code>peer</code> should replace
* an existing sibling.
* @param peer
* @return
*/
private boolean isCloserSibling(KademliaPeer peer) {
BigInteger peerDistance = KademliaUtil.getDistance(peer, localDestinationHash);
for (KademliaPeer sibling: siblingBucket) {
BigInteger siblingDistance = KademliaUtil.getDistance(sibling.getDestinationHash(), localDestinationHash);
if (peerDistance.compareTo(siblingDistance) < 0)
return true;
}
return false;
}
/**
* Add a peer to the sibling list if the list is not full, or if there is a node that can be
* replaced.
*
* If <code>peer</code> replaced an existing sibling, that sibling is returned.
* If <code>peer</code> could not be added to the list, <code>peer</code> is returned.
* If the list was not full, <code>null</code> is returned.
* @param peer
* @return
*/
/* private KademliaPeer addSibling(KademliaPeer peer) {
// no need to handle a replacement cache because the sibling bucket has none.
KademliaPeer mostDistantSibling = siblingBucket.getMostDistantPeer(localDestinationHash);
if (!siblingBucket.isFull()) {
siblingBucket.add(peer);
return null;
}
else if (new PeerDistanceComparator(localDestinationHash).compare(peer, mostDistantSibling) < 0) {
siblingBucket.remove(mostDistantSibling);
siblingBucket.add(peer);
return mostDistantSibling;
public void remove(Destination peer) {
AbstractBucket bucket = getBucket(peer);
if (bucket != null) {
bucket.remove(peer);
if (bucket instanceof SBucket)
refillSiblings();
}
else
return peer;
}*/
public void remove(KademliaPeer peer) {
Hash nodeHash = peer.getDestinationHash();
getBucket(nodeHash).remove(peer);
log.debug("Can't remove peer because no bucket contains it: " + peer.calculateHash().toBase64());
}
/**
* Do a binary search for the index of the bucket whose key range contains a given {@link Hash}.
* Moves peers from the k-buckets to the s-bucket until the s-bucket is full
* or all k-buckets are empty.
*/
private void refillSiblings() {
// Sort all k-peers by distance to the local destination
List<KademliaPeer> kPeers = new ArrayList<KademliaPeer>();
for (KBucket kBucket: kBuckets)
kPeers.addAll(kBucket.getPeers());
Collections.sort(kPeers, new PeerDistanceComparator(localDestinationHash));
while (!sBucket.isFull() && !kPeers.isEmpty()) {
// move the closest k-peer to the s-bucket
KademliaPeer peerToMove = kPeers.remove(0);
int bucketIndex = getBucketIndex(peerToMove.getDestinationHash());
kBuckets.get(bucketIndex).remove(peerToMove);
sBucket.addOrUpdate(peerToMove);
}
}
/**
* Finds the index of the k-bucket whose key range contains a given {@link Hash}.
* This method does a binary search "by hand" because <code>Collections.binarySearch()<code>
* cannot be used to search for a <code>Hash</code> in a <code>List&lt;KBucket&gt;</code>.
* @param key
* @return
*/
private int getBucketIndex(Hash key) {
// initially, the search interval is 0..n-1
int lowEnd = 0;
int highEnd = kBuckets.size();
if (kBuckets.size() == 1)
return 0;
BigInteger keyValue = new BigInteger(key.getData());
while (lowEnd < highEnd) {
int centerIndex = (highEnd + lowEnd) / 2;
// initially, the search interval is 0..n-1
int lowIndex = 0;
int highIndex = kBuckets.size() - 1;
BigInteger keyValue = new BigInteger(1, key.getData());
while (lowIndex < highIndex) {
int centerIndex = (highIndex + lowIndex) / 2;
if (keyValue.compareTo(kBuckets.get(centerIndex).getStartId()) < 0)
highEnd = centerIndex;
else if (keyValue.compareTo(kBuckets.get(centerIndex).getEndId()) > 0)
lowEnd = centerIndex;
highIndex = centerIndex - 1;
else if (keyValue.compareTo(kBuckets.get(centerIndex).getEndId()) >= 0)
lowIndex = centerIndex + 1;
else
return centerIndex;
}
log.error("This should never happen! No k-bucket found for hash: " + key);
return -1;
return lowIndex;
}
/**
* Do a binary search for the bucket whose key range contains a given {@link Hash}.
* Does a binary search for the k-bucket whose key range contains a given
* {@link Hash}.
* The bucket may or may not contain a peer with that hash.
* @param key
* @return
*/
private KBucket getBucket(Hash key) {
public KBucket getKBucket(Hash key) {
return kBuckets.get(getBucketIndex(key));
}
/**
* Return the <code>count</code> peers that are closest to a given key.
* Less than <code>count</code> peers may be returned if there aren't
* enough peers in the k-buckets.
* @param key
* @param count
* Looks up a <code>KademliaPeer</code> by I2P destination. If no bucket
* (k or s-bucket) contains the peer, <code>null</code> is returned.
* @param destination
* @return
*/
public Collection<KademliaPeer> getClosestPeers(Hash key, int count) {
Collection<KademliaPeer> closestPeers = new ConcurrentHashSet<KademliaPeer>();
// TODO don't put all peers in one huge list, only use two buckets at a time
KademliaPeer[] allPeers = getAllPeersSortedByDistance(key);
for (int i=0; i<count && i<allPeers.length; i++)
closestPeers.add(allPeers[i]);
return closestPeers;
}
private KademliaPeer[] getAllPeersSortedByDistance(Hash key) {
List<KademliaPeer> allPeers = getAllPeers();
KademliaPeer[] peerArray = getAllPeers().toArray(new KademliaPeer[allPeers.size()]);
Arrays.sort(peerArray, new PeerDistanceComparator(key));
return peerArray;
private KademliaPeer getPeer(Destination destination) {
AbstractBucket bucket = getBucket(destination);
if (bucket != null)
return bucket.getPeer(destination);
else
return null;
}
private List<KademliaPeer> getAllPeers() {
/**
* Returns the (s or k) bucket that contains a given {@link Destination}.
* The s-bucket is checked first, then the k-buckets.
* If no bucket contains the peer, <code>null</code> is returned.
* @param destination
* @return
*/
private AbstractBucket getBucket(Destination destination) {
if (sBucket.contains(destination))
return sBucket;
else {
KBucket kBucket = getKBucket(destination.calculateHash());
if (kBucket.contains(destination))
return kBucket;
else
return null;
}
}
/**
* Returns the <code>count</code> peers that are closest to a given key,
* and which are not locked.
* Less than <code>count</code> peers may be returned if there aren't
* enough peers in the k-buckets and the s-bucket.
* @param key
* @param count
* @return Up to <code>count</code> peers, sorted by distance to <code>key</code>.
*/
public List<Destination> getClosestPeers(Hash key, int count) {
// TODO don't put all peers in one huge list, only use two k-buckets and the s-bucket at a time
List<Destination> peers = getAllUnlockedPeers();
Collections.sort(peers, new PeerDistanceComparator(key));
if (peers.size() < count)
return peers;
else
return peers.subList(0, count);
}
private synchronized List<Destination> getAllUnlockedPeers() {
List<Destination> allPeers = new ArrayList<Destination>();
for (KBucket bucket: kBuckets)
for (KademliaPeer peer: bucket.getPeers())
if (!peer.isLocked())
allPeers.add(peer);
for (KademliaPeer peer: sBucket.getPeers())
if (!peer.isLocked())
allPeers.add(peer);
return allPeers;
}
public synchronized List<KademliaPeer> getAllPeers() {
List<KademliaPeer> allPeers = new ArrayList<KademliaPeer>();
for (KBucket bucket: kBuckets)
allPeers.addAll(bucket.getNodes());
allPeers.addAll(siblingBucket.getNodes());
allPeers.addAll(bucket.getPeers());
allPeers.addAll(sBucket.getPeers());
return allPeers;
}
/**
* Return all siblings of the local node (siblings are an S/Kademlia feature).
* Return the total number of known Kademlia peers (locked + unlocked peers).
* @return
*/
public List<KademliaPeer> getSiblings() {
List<KademliaPeer> siblingDestinations = new ArrayList<KademliaPeer>();
for (KademliaPeer sibling: siblingBucket)
siblingDestinations.add(sibling);
return siblingDestinations;
}
/**
* Return the total number of known Kademlia peers.
* @return
*/
public int getPeerCount() {
int getPeerCount() {
int count = 0;
for (KBucket bucket: kBuckets)
count += bucket.size();
count += siblingBucket.size();
count += sBucket.size();
return count;
}
/**
* "refresh all k-buckets further away than the closest neighbor. This refresh is just a lookup of a random key that is within that k-bucket range."
* Return the total number of Kademlia peers that are not locked.
* @return
*/
public void refreshAll() {
for (KBucket bucket: kBuckets)
refresh(bucket);
}
private void refresh(KBucket bucket) {
byte[] randomHash = new byte[Hash.HASH_LENGTH];
for (int i=0; i<Hash.HASH_LENGTH; i++)
randomHash[i] = (byte)RandomSource.getInstance().nextInt(256);
Hash key = new Hash(randomHash);
getClosestPeers(key, KademliaConstants.K);
}
/* private void updatePeerList() {
for (Destination peer: peers) {
if (ping(peer))
requestPeerList(peer);
}
}*/
@Override
public void run() {
while (!shutdownRequested()) {
try {
// TODO replicate();
// TODO updatePeerList(); refresh every bucket to which we haven't performed a node lookup in the past hour. Refreshing means picking a random ID in the bucket's range and performing a node search for that ID.
sleep(INTERVAL);
}
catch (InterruptedException e) {
log.debug("Thread '" + getName() + "' + interrupted", e);
}
}
int getUnlockedPeerCount() {
return getAllUnlockedPeers().size();
}
/**
* @see KademliaDHT.getPeerStats()
*/
DhtPeerStats getPeerStats() {
return new KademliaPeerStats(sBucket, kBuckets, localDestinationHash);
}
// PacketListener implementation
@Override
public void packetReceived(CommunicationPacket packet, Destination sender, long receiveTime) {
boolean banned = false;
KademliaPeer peer = getPeer(sender);
if (peer != null) {
if (packet.getProtocolVersion() != I2PBote.PROTOCOL_VERSION) {
BanList.getInstance().ban(peer, "Wrong protocol version: " + packet.getProtocolVersion());
remove(peer);
banned = true;
}
else
BanList.getInstance().unban(peer);
}
// any type of incoming packet updates the peer's record in the bucket/sibling list, or adds the peer to the bucket/sibling list
addOrUpdate(new KademliaPeer(sender, receiveTime));
}
if (!banned)
addOrUpdate(new KademliaPeer(sender));
}
private class PeerDistanceComparator implements Comparator<KademliaPeer> {
private Hash reference;
PeerDistanceComparator(Hash reference) {
this.reference = reference;
}
@Override
public int compare(KademliaPeer peer1, KademliaPeer peer2) {
BigInteger distance1 = KademliaUtil.getDistance(peer1.getDestinationHash(), reference);
BigInteger distance2 = KademliaUtil.getDistance(peer2.getDestinationHash(), reference);
return distance1.compareTo(distance2);
}
SBucket getSBucket() {
return sBucket;
}
/**
* Iterates over the k-buckets. Does not include the sibling bucket.
* This method is not thread safe.
* @return
*/
@Override
public Iterator<KBucket> iterator() {
return kBuckets.iterator();
}
}

View File

@ -21,15 +21,19 @@
package i2p.bote.network.kademlia;
import static i2p.bote.network.kademlia.KademliaConstants.K;
import i2p.bote.UniqueId;
import i2p.bote.Util;
import i2p.bote.network.I2PPacketDispatcher;
import i2p.bote.network.I2PSendQueue;
import i2p.bote.network.PacketListener;
import i2p.bote.packet.CommunicationPacket;
import i2p.bote.packet.DataPacket;
import i2p.bote.packet.MalformedCommunicationPacket;
import i2p.bote.packet.PeerList;
import i2p.bote.packet.ResponsePacket;
import i2p.bote.packet.dht.FindClosePeersPacket;
import i2p.bote.service.I2PBoteThread;
import java.math.BigInteger;
import java.util.ArrayList;
@ -37,13 +41,14 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.Log;
public class ClosestNodesLookupTask implements Runnable {
@ -54,21 +59,29 @@ public class ClosestNodesLookupTask implements Runnable {
private Hash key;
private I2PPacketDispatcher i2pReceiver;
private BucketManager bucketManager;
private Random randomNumberGenerator;
private I2PSendQueue sendQueue;
private Set<Destination> responseReceived;
private Set<KademliaPeer> notQueriedYet;
private Set<FindClosePeersPacket> pendingRequests;
private Comparator<Destination> peerComparator;
private SortedSet<Destination> responses; // sorted by distance to the key to look up
private SortedSet<Destination> notQueriedYet; // sorted by distance to the key to look up
private Map<Destination, FindClosePeersPacket> pendingRequests;
private long startTime;
/**
* @param key The DHT key to look up
* @param sendQueue For sending I2P packets
* @param i2pReceiver For receiving I2P packets
* @param bucketManager For looking up peers, and updating them
*/
public ClosestNodesLookupTask(Hash key, I2PSendQueue sendQueue, I2PPacketDispatcher i2pReceiver, BucketManager bucketManager) {
this.key = key;
this.sendQueue = sendQueue;
this.i2pReceiver = i2pReceiver;
this.bucketManager = bucketManager;
randomNumberGenerator = new Random(getTime());
responseReceived = new TreeSet<Destination>(new HashDistanceComparator(key)); // nodes that have responded to a query; sorted by distance to the key
notQueriedYet = new ConcurrentHashSet<KademliaPeer>(); // peers we haven't contacted yet
pendingRequests = new ConcurrentHashSet<FindClosePeersPacket>(); // outstanding queries
peerComparator = new HashDistanceComparator(key);
responses = Collections.synchronizedSortedSet(new TreeSet<Destination>(peerComparator)); // nodes that have responded to a query
notQueriedYet = Collections.synchronizedSortedSet(new TreeSet<Destination>(peerComparator)); // peers we haven't contacted yet
pendingRequests = new ConcurrentHashMap<Destination, FindClosePeersPacket>(); // outstanding queries
}
@Override
@ -78,45 +91,74 @@ public class ClosestNodesLookupTask implements Runnable {
PacketListener packetListener = new IncomingPacketHandler();
i2pReceiver.addPacketListener(packetListener);
// prepare a list of close nodes (might need more than alpha if they don't all respond)
notQueriedYet.addAll(bucketManager.getClosestPeers(key, KademliaConstants.S));
// get a list of all peers (we don't how many we really need because some may not respond)
notQueriedYet.addAll(bucketManager.getAllPeers());
long startTime = getTime();
while (true) {
startTime = getTime();
do {
// send new requests if less than alpha are pending
while (pendingRequests.size()<KademliaConstants.ALPHA && !notQueriedYet.isEmpty()) {
KademliaPeer peer = selectRandom(notQueriedYet);
Destination peer = notQueriedYet.first(); // query the closest unqueried peer
notQueriedYet.remove(peer);
FindClosePeersPacket packet = new FindClosePeersPacket(key);
pendingRequests.add(packet);
try {
CommunicationPacket response = sendQueue.sendAndWait(packet, peer.getDestination(), REQUEST_TIMEOUT);
if (response == null) // timeout occurred
peer.incrementStaleCounter();
else
peer.resetStaleCounter();
}
catch (InterruptedException e) {
log.warn("Interrupted while waiting on a lookup request.", e);
}
pendingRequests.put(peer, packet);
sendQueue.send(packet, peer);
}
// handle timeouts
for (Map.Entry<Destination, FindClosePeersPacket> request: pendingRequests.entrySet())
if (hasTimedOut(request.getValue(), REQUEST_TIMEOUT)) {
Destination peer = request.getKey();
bucketManager.noResponse(peer);
pendingRequests.remove(peer);
}
if (responseReceived.size() >= KademliaConstants.S)
break;
if (hasTimedOut(startTime, CLOSEST_NODES_LOOKUP_TIMEOUT)) {
log.error("Lookup for closest nodes timed out.");
break;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
log.warn("Interrupted while doing a closest nodes lookup.", e);
}
}
log.debug(responseReceived.size() + " nodes found.");
} while (!isDone());
log.debug(responses.size() + " nodes found (may include local node).");
for (Destination node: responses)
log.debug(" Node: " + node.calculateHash());
i2pReceiver.removePacketListener(packetListener);
}
private KademliaPeer selectRandom(Collection<KademliaPeer> collection) {
KademliaPeer[] array = new KademliaPeer[collection.size()];
int index = randomNumberGenerator.nextInt(array.length);
return collection.toArray(array)[index];
private boolean isDone() {
// if there are no more requests to send, and no more responses to wait for, we're finished
if (pendingRequests.isEmpty() && notQueriedYet.isEmpty())
return true;
// if we have received responses from the k closest peers, we're also finished
Destination kthClosestResult = null;
synchronized(responses) {
if (responses.size() >= K)
kthClosestResult = Util.get(responses, K-1);
}
if (kthClosestResult != null) {
Destination closestUnqueriedPeer = null;
synchronized(notQueriedYet) {
if (notQueriedYet.isEmpty())
return true;
closestUnqueriedPeer = notQueriedYet.first();
}
if (peerComparator.compare(kthClosestResult, closestUnqueriedPeer) <= 0)
return true;
}
if (hasTimedOut(startTime, CLOSEST_NODES_LOOKUP_TIMEOUT)) {
log.error("Lookup for closest nodes timed out.");
return true;
}
// If a shutdown has been initiated, exit immediately
Thread currentThread = Thread.currentThread();
if (currentThread instanceof I2PBoteThread && ((I2PBoteThread)currentThread).shutdownRequested())
return true;
return false;
}
private long getTime() {
@ -127,38 +169,28 @@ public class ClosestNodesLookupTask implements Runnable {
return getTime() > startTime + timeout;
}
private boolean hasTimedOut(CommunicationPacket request, long timeout) {
long sentTime = request.getSentTime();
return sentTime>0 && hasTimedOut(sentTime, timeout);
}
/**
* Return up to <code>s</code> peers.
* Returns up to <code>k</code> peers, sorted by distance from the key.
* If no peers were found, an empty <code>List</code> is returned.
* @return
*/
public List<Destination> getResults() {
List<Destination> resultsList = new ArrayList<Destination>();
for (Destination destination: responseReceived)
for (Destination destination: responses) {
resultsList.add(destination);
Collections.sort(resultsList, new HashDistanceComparator(key));
// trim the list to the k closest nodes
if (resultsList.size() > KademliaConstants.S)
resultsList = resultsList.subList(0, KademliaConstants.S);
if (resultsList.size() >= K)
break;
}
return resultsList;
}
/**
* Return <code>true</code> if a set of peers contains a given peer.
* @param peerSet
* @param peerToFind
* @return
*/
private boolean contains(Set<KademliaPeer> peerSet, KademliaPeer peerToFind) {
Hash peerHash = peerToFind.getDestinationHash();
for (KademliaPeer peer: peerSet)
if (peer.getDestinationHash().equals(peerHash))
return true;
return false;
}
// compares two Destinations in terms of closeness to <code>reference</code>
private class HashDistanceComparator implements Comparator<Destination> {
private static class HashDistanceComparator implements Comparator<Destination> {
private Hash reference;
public HashDistanceComparator(Hash reference) {
@ -177,29 +209,45 @@ public class ClosestNodesLookupTask implements Runnable {
public void packetReceived(CommunicationPacket packet, Destination sender, long receiveTime) {
if (packet instanceof ResponsePacket) {
ResponsePacket responsePacket = (ResponsePacket)packet;
DataPacket payload = responsePacket.getPayload();
if (payload instanceof PeerList)
addPeers(responsePacket, (PeerList)payload, sender, receiveTime);
FindClosePeersPacket request = getPacketById(pendingRequests.values(), responsePacket.getPacketId()); // find the request the node list is in response to
// if the packet is in response to a pending request, update the three Sets
if (request != null) {
responses.add(sender);
DataPacket payload = responsePacket.getPayload();
if (payload instanceof PeerList)
updatePeers((PeerList)payload, sender, receiveTime);
pendingRequests.remove(sender);
}
else
log.debug("No Find Close Nodes packet found for Response Packet: " + responsePacket);
}
else if (packet instanceof MalformedCommunicationPacket)
pendingRequests.remove(sender); // since it is not generally possible to tell if an invalid comm packet is in response to a certain request, always remove invalid packets from the pending list
}
private void addPeers(ResponsePacket responsePacket, PeerList peerListPacket, Destination sender, long receiveTime) {
log.debug("Peer List Packet received: #peers=" + peerListPacket.getPeers().size() + ", sender="+ sender);
/**
* Updates the <code>notQueriedYet</code> set with the peers from a <code>peerListPacket</code>,
* and adds the <code>sender</code> to <code>responses</code>.
* @param peerListPacket
* @param sender
* @param receiveTime
*/
private void updatePeers(PeerList peerListPacket, Destination sender, long receiveTime) {
log.debug("Peer List Packet received: #peers=" + peerListPacket.getPeers().size() + ", sender="+ sender.calculateHash());
for (Destination peer: peerListPacket.getPeers())
log.debug(" Peer: " + peer.calculateHash());
// TODO make responseReceived and pendingRequests a parameter in the constructor?
responses.add(sender);
Collection<Destination> peersReceived = peerListPacket.getPeers();
// if the packet is in response to a pending request, update the three Sets
FindClosePeersPacket request = getPacketById(pendingRequests, responsePacket.getPacketId()); // find the request the node list is in response to
if (request != null) {
// TODO make responseReceived and pendingRequests a parameter in the constructor?
responseReceived.add(sender);
Collection<KademliaPeer> peersReceived = peerListPacket.getPeers();
for (KademliaPeer peer: peersReceived)
if (contains(notQueriedYet, peer))
notQueriedYet.add(peer);
pendingRequests.remove(request);
// TODO synchronize access to shortList and pendingRequests
}
else
log.debug("No Find Close Nodes packet found for Peer List: " + peerListPacket);
// add all peers from the PeerList, excluding those that we have already queried
// TODO don't add local dest
for (Destination peer: peersReceived)
if (!pendingRequests.containsKey(peer) && !responses.contains(peer))
notQueriedYet.add(peer); // this won't create duplicates because notQueriedYet is a Set
}
/**
@ -216,4 +264,4 @@ public class ClosestNodesLookupTask implements Runnable {
return null;
}
};
}
}

View File

@ -23,139 +23,151 @@ package i2p.bote.network.kademlia;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.Log;
/**
* Peers are sorted by the time of the most recent communication: Index 0 = least recent,
* index n-1 = most recent.
* An {@link AbstractBucket} that:
* * can be split in two,
* * has a start and end Kademlia ID,
* * knows its depth in the bucket tree, and
* * maintains a replacement cache.
*
* Peers are kept in an <code>ArrayList</code>. Active peers are added
* at index 0. When a peer is updated, it is moved to index 0.
* When the bucket needs to make room for a new peer, the peer at the
* highest index (the least recently seen one) is dropped.
* This effectively sorts peers by the time of most recent communication.
*
* The replacement cache is handled the same way.
*
* TODO use peers from the replacement cache when the bucket is not full
*/
class KBucket implements Iterable<KademliaPeer> {
static final BigInteger MIN_HASH_VALUE = BigInteger.ONE.negate().shiftLeft(Hash.HASH_LENGTH*8); // system-wide minimum hash value
static final BigInteger MAX_HASH_VALUE = BigInteger.ONE.shiftLeft(Hash.HASH_LENGTH*8).subtract(BigInteger.ONE); // system-wide maximum hash value
private static final int CAPACITY = KademliaConstants.K; // The maximum number of peers the bucket can hold
class KBucket extends AbstractBucket {
private static final int REPLACEMENT_CACHE_MAX_SIZE = KademliaConstants.K;
private Log log = new Log(KBucket.class);
private BigInteger startId;
private BigInteger endId;
private List<KademliaPeer> peers; // the list is always kept sorted by "last seen" time
private Set<KademliaPeer> replacementCache;
private int depth;
private boolean replacementCacheEnabled;
private List<KademliaPeer> replacementCache; // Basically a FIFO. Peers are sorted most recently seen to least recently seen
private volatile int depth;
private volatile long lastLookupTime;
KBucket(BigInteger startId, BigInteger endId, int depth, boolean replacementCacheEnabled) {
// capacity - The maximum number of peers the bucket can hold
KBucket(BigInteger startId, BigInteger endId, int depth) {
super(KademliaConstants.K);
this.startId = startId;
this.endId = endId;
Comparator<KademliaPeer> peerComparator = createLastReceptionComparator();
peers = new ArrayList<KademliaPeer>();
replacementCache = new ConcurrentSkipListSet<KademliaPeer>(peerComparator);
replacementCache = Collections.synchronizedList(new ArrayList<KademliaPeer>());
this.depth = depth;
this.replacementCacheEnabled = replacementCacheEnabled;
}
BigInteger getStartId() {
synchronized BigInteger getStartId() {
return startId;
}
BigInteger getEndId() {
synchronized BigInteger getEndId() {
return endId;
}
List<KademliaPeer> getNodes() {
// TODO only return peers that are not locked
return peers;
}
void add(KademliaPeer node) {
if (isFull())
log.error("Error: adding a node to a full k-bucket. Bucket needs to be split first. Size=" + size() + ", capacity=" + CAPACITY);
peers.add(node);
}
/**
* Adds a node to the bucket, splitting the bucket if necessary. If the bucket is split,
* the newly created bucket is returned. Otherwise, <code>null</code> is returned.
*
* If the bucket is full but cannot be split, the new node is added to the replacement
* cache and <code>null</code> is returned.
* @param peer
* @return
* @param lastLookupTime
* @see getLastLookupTime
*/
synchronized KBucket addOrSplit(KademliaPeer peer) {
if (!rangeContains(peer))
log.error("Attempt to add a node whose hash is outside the bucket's range! Bucket start=" + startId + " Bucket end=" + endId + " peer hash=" + new BigInteger(peer.getDestinationHash().getData()));
if (isFull() && !contains(peer)) {
if (canSplit(peer)) {
KBucket newBucket = split(peer.getDestinationHash());
if (rangeContains(peer))
add(peer);
else if (newBucket.rangeContains(peer))
newBucket.add(peer);
else
log.error("After splitting a bucket, node is outside of both buckets' ranges.");
return newBucket;
}
else {
replacementCache.add(peer);
return null;
}
}
else {
addOrUpdate(peer);
return null;
}
}
/**
* Returns <code>true</code> if the bucket should be split in order to make room for a new peer.
* @return
*/
private boolean canSplit(KademliaPeer peer) {
return depth%KademliaConstants.B!=0 || rangeContains(peer);
}
/**
* Updates a known peer, or adds the peer if it isn't known.
* TODO If the bucket is full, the peer is added to the bucket's replacement cache.
* @param destination
* @return <code>true</code> if the peer was added (or replacement-cached),
* <code>false</code> if it was updated.
*/
boolean addOrUpdate(KademliaPeer peer) {
// TODO log an error if peer outside bucket's range
// TODO handle stale peers
// TODO manage replacement cache
KademliaPeer existingPeer = getPeer(peer.getDestination());
if (existingPeer == null) {
add(peer);
return true;
}
else {
existingPeer.setLastReception(peer.getLastReception());
// TODO move to end of list if lastReception is highest value, which it should be most of the time
return false;
}
}
/**
* Returns <code>true</code> if a peer exists in the bucket.
* @param peer
* @return
*/
boolean contains(KademliaPeer peer) {
return getPeer(peer.getDestination()) != null;
public void setLastLookupTime(long lastLookupTime) {
this.lastLookupTime = lastLookupTime;
}
/**
* Returns the time at which a closest nodes lookup for a key in this
* bucket's range was last performed.
* @return
*/
public long getLastLookupTime() {
return lastLookupTime;
}
/**
* Returns <code>true</code> if the bucket needs to, AND can be split
* so a given <code>Destination</code> can be added.
*/
boolean shouldSplit(Destination destination) {
return isFull() && !contains(destination) && canSplit(destination);
}
/**
* Returns <code>true</code> if the bucket can be split in order to make room for a new peer.
* @return
*/
private boolean canSplit(Destination destination) {
return depth%KademliaConstants.B!=0 || rangeContains(destination);
}
/**
* Updates a known peer, or adds the peer if it isn't known. If the bucket
* is full and the replacement cache is not empty, the oldest peer is removed
* before adding the new peer.
* If the bucket is full and the replacement cache is empty, the peer is
* added to the replacement cache.
* @param peer
*/
void addOrUpdate(KademliaPeer peer) {
// TODO log an error if peer outside bucket's range
int index = getPeerIndex(peer);
if (index >= 0) {
KademliaPeer existingPeer = peers.remove(index);
existingPeer.responseReceived();
add(existingPeer);
}
else {
if (!isFull())
add(peer);
else
addOrUpdateReplacement(peer);
}
}
/**
* Adds a peer to the tail of the bucket if it is locked, or to the
* head of the bucket if it isn't locked.
* The bucket cannot be full when calling this method.
* @param peer
*/
private void add(KademliaPeer peer) {
if (isFull())
log.error("Error: adding a node to a full k-bucket. Bucket needs to be split first. Size=" + size() + ", capacity=" + capacity);
if (peer.isLocked())
peers.add(peer);
else
peers.add(0, peer);
}
void noResponse(KademliaPeer peer) {
if (!replacementCache.isEmpty())
if (peers.remove(peer))
peers.add(replacementCache.remove(0));
}
/**
* Adds a peer to the head of the replacement cache, or makes
* it the head if it exists in the replacement cache.
* @param peer
*/
private void addOrUpdateReplacement(KademliaPeer peer) {
if (replacementCache.contains(peer))
replacementCache.remove(peer);
replacementCache.add(0, peer);
while (replacementCache.size() > REPLACEMENT_CACHE_MAX_SIZE)
replacementCache.remove(REPLACEMENT_CACHE_MAX_SIZE-1);
}
/**
* Returns <code>true</code> if the bucket's Id range contains the hash of a given
* peer, regardless if the bucket contains the peer; <code>false</code> if the hash
@ -163,104 +175,40 @@ class KBucket implements Iterable<KademliaPeer> {
* @param peer
* @return
*/
private boolean rangeContains(KademliaPeer peer) {
BigInteger peerHash = new BigInteger(peer.getDestinationHash().getData());
return (startId.compareTo(peerHash)<=0 || endId.compareTo(peerHash)>0);
private boolean rangeContains(Destination peer) {
BigInteger peerHash = new BigInteger(1, peer.calculateHash().getData());
return (startId.compareTo(peerHash)<=0 && endId.compareTo(peerHash)>=0);
}
/**
* Returns a peer with a given I2P destination from the bucket, or <code>null</code> if the
* peer isn't in the bucket.
* @param destination
* @return
*/
KademliaPeer getPeer(Destination destination) {
for (KademliaPeer peer: peers)
if (peer.getDestination().equals(destination))
return peer;
return null;
}
KademliaPeer getClosestPeer(Hash key) {
KademliaPeer closestPeer = null;
BigInteger minDistance = MAX_HASH_VALUE;
for (KademliaPeer peer: peers) {
BigInteger distance = KademliaUtil.getDistance(key, peer.getDestination().calculateHash());
if (distance.compareTo(minDistance) < 0) {
closestPeer = peer;
minDistance = distance;
}
}
return closestPeer;
}
/* KademliaPeer getMostDistantPeer(KademliaPeer node) {
return getMostDistantPeer(node.getDestinationHash());
}*/
KademliaPeer getMostDistantPeer(Hash key) {
KademliaPeer mostDistantPeer = null;
BigInteger maxDistance = BigInteger.ZERO;
for (KademliaPeer peer: peers) {
BigInteger distance = KademliaUtil.getDistance(key, peer.getDestination().calculateHash());
if (distance.compareTo(maxDistance) > 0) {
mostDistantPeer = peer;
maxDistance = distance;
}
}
return mostDistantPeer;
}
@Override
public Iterator<KademliaPeer> iterator() {
return peers.iterator();
}
void remove(Destination destination) {
peers.remove(getPeer(destination));
}
/**
* Removes a peer from the bucket. If the peer doesn't exist in the bucket, nothing happens.
* @param node
*/
void remove(KademliaPeer node) {
peers.remove(node);
}
int size() {
return peers.size();
}
boolean isFull() {
return size() >= CAPACITY;
}
/**
* Moves half the nodes into a new bucket.
* Splits the bucket in two equal halves (in terms of ID ranges) and moves peers
* to the new bucket if necessary.
* The existing bucket retains the lower IDs; the new bucket will contain the
* higher IDs.
* In other words, the bucket is split into two sub-branches in the Kademlia
* tree, with the old bucket representing the left branch and the new bucket
* representing the right branch.
* @return The new bucket
* @see split(BigInteger)
*/
KBucket split() {
depth++;
KBucket newBucket = new KBucket(startId, endId, depth, replacementCacheEnabled);
for (int i=0; i<peers.size()/2; i++) {
KademliaPeer peer = peers.get(i);
newBucket.add(peer);
remove(peer);
}
return newBucket;
BigInteger pivot = startId.add(endId).divide(BigInteger.valueOf(2));
return split(pivot);
}
KBucket split(Hash hash) {
return split(new BigInteger(hash.toBase64()));
}
KBucket split(BigInteger pivot) {
/**
* Splits the bucket in two by keeping all peers with a DHT key less than
* <code>pivot</code> in the existing bucket, and moving the rest into a new bucket.
* @param pivot
* @return The new bucket (which contains the higher IDs)
*/
private KBucket split(BigInteger pivot) {
depth++;
KBucket newBucket = new KBucket(startId, pivot.subtract(BigInteger.ONE), depth, replacementCacheEnabled);
startId = pivot;
for (KademliaPeer peer: peers) {
BigInteger nodeId = new BigInteger(peer.getDestination().calculateHash().getData());
KBucket newBucket = new KBucket(pivot, endId, depth);
endId = pivot;
for (int i=peers.size()-1; i>=0; i--) {
KademliaPeer peer = peers.get(i);
BigInteger nodeId = new BigInteger(1, peer.getDestinationHash().getData());
if (nodeId.compareTo(pivot) >= 0) {
newBucket.add(peer);
remove(peer);
@ -269,12 +217,22 @@ class KBucket implements Iterable<KademliaPeer> {
return newBucket;
}
private Comparator<KademliaPeer> createLastReceptionComparator() {
return new Comparator<KademliaPeer>() {
@Override
public int compare(KademliaPeer peer1, KademliaPeer peer2) {
return Long.valueOf(peer1.getLastReception()).compareTo(peer2.getLastReception());
}
};
/**
* Returns the common prefix shared by the binary representation of all peers in the bucket.
* @return
*/
String getBucketPrefix() {
if (depth == 0)
return "(None)";
String binary = startId.toString(2);
while (binary.length() < Hash.HASH_LENGTH*8)
binary = "0" + binary;
return binary.substring(0, depth);
}
@Override
public String toString() {
return "K-Bucket (depth=" + depth + ", prefix=" + getBucketPrefix() + ", lastLookup=" + new Date(lastLookupTime) + ", start=" + startId + ", end=" + endId + ")";
}
}

View File

@ -22,13 +22,14 @@
package i2p.bote.network.kademlia;
public class KademliaConstants {
public static final int K = 2; // Number of redundant storage nodes.
public static final int S = 3; // The size of the sibling list for S/Kademlia.
public static final int K = 20; // Number of redundant storage nodes.
public static final int S = 100; // The size of the sibling list for S/Kademlia.
// public static final int B = 5; // This is the value from the original Kademlia paper.
public static final int B = 1;
public static final int ALPHA = 3; // According to the literature, this is the optimum choice for alpha.
public static final int REFRESH_TIMEOUT = 3600;
public static final int BUCKET_REFRESH_INTERVAL = 3600;
public static final int REPLICATE_INTERVAL = 3600; // TODO would it be better for REPLICATE_INTERVAL to be slightly longer than REFRESH_TIMEOUT?
public static final int STALE_THRESHOLD = 3; // Maximum number of times a peer can time out in a row before it is dropped
private KademliaConstants() { }
}

View File

@ -21,14 +21,19 @@
package i2p.bote.network.kademlia;
import i2p.bote.Util;
import i2p.bote.network.DHT;
import i2p.bote.network.DhtException;
import i2p.bote.network.DhtPeerStats;
import i2p.bote.network.DhtResults;
import i2p.bote.network.DhtStorageHandler;
import i2p.bote.network.I2PPacketDispatcher;
import i2p.bote.network.I2PSendQueue;
import i2p.bote.network.PacketBatch;
import i2p.bote.network.PacketListener;
import i2p.bote.network.kademlia.SBucket.BucketSection;
import i2p.bote.packet.CommunicationPacket;
import i2p.bote.packet.I2PBotePacket;
import i2p.bote.packet.DataPacket;
import i2p.bote.packet.PeerList;
import i2p.bote.packet.ResponsePacket;
import i2p.bote.packet.StatusCode;
@ -36,19 +41,26 @@ import i2p.bote.packet.dht.DhtStorablePacket;
import i2p.bote.packet.dht.FindClosePeersPacket;
import i2p.bote.packet.dht.RetrieveRequest;
import i2p.bote.packet.dht.StoreRequest;
import i2p.bote.service.I2PBoteThread;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@ -57,12 +69,14 @@ import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.Log;
import net.i2p.util.RandomSource;
import com.nettgryppa.security.HashCash;
/**
* The main class of the Kademlia implementation. All the high-level Kademlia logic
* is in here.
* In addition to standard Kademlia, the sibling list feature of S-Kademlia is implemented.
*
* Resources used:
* [1] http://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf
@ -73,61 +87,69 @@ import com.nettgryppa.security.HashCash;
* [6] OverSim (http://www.oversim.org/), which includes a S/Kademlia implementation
*
*/
public class KademliaDHT implements DHT, PacketListener {
public class KademliaDHT extends I2PBoteThread implements DHT, PacketListener {
private static final int RESPONSE_TIMEOUT = 60; // Max. number of seconds to wait for replies to retrieve requests
private static final URL BUILT_IN_PEER_FILE = KademliaDHT.class.getResource("built-in-peers.txt");
private Log log = new Log(KademliaDHT.class);
private Destination localDestination;
private Hash localDestinationHash;
private I2PSendQueue sendQueue;
private I2PPacketDispatcher i2pReceiver;
private File peerFile;
private Collection<KademliaPeer> initialPeers;
private Set<KademliaPeer> initialPeers;
private BucketManager bucketManager;
private Map<Class<? extends DhtStorablePacket>, DhtStorageHandler> storageHandlers;
private volatile boolean connected; // false until bootstrapping is done
public KademliaDHT(Destination localDestination, I2PSendQueue sendQueue, I2PPacketDispatcher i2pReceiver, File peerFile) {
super("Kademlia");
this.localDestination = localDestination;
localDestinationHash = localDestination.calculateHash();
this.sendQueue = sendQueue;
this.i2pReceiver = i2pReceiver;
this.peerFile = peerFile;
initialPeers = Collections.synchronizedList(new ArrayList<KademliaPeer>());
initialPeers = new ConcurrentHashSet<KademliaPeer>();
// Read the built-in peer file
readPeers(BUILT_IN_PEER_FILE);
// Read the updateable peer file
readPeers(peerFile);
// Read the updateable peer file if it exists
if (peerFile.exists())
readPeers(peerFile);
else
log.info("Peer file doesn't exist, using built-in peers only (File not found: <" + peerFile.getAbsolutePath() + ">)");
bucketManager = new BucketManager(sendQueue, initialPeers, localDestination.calculateHash());
bucketManager = new BucketManager(localDestinationHash);
storageHandlers = new ConcurrentHashMap<Class<? extends DhtStorablePacket>, DhtStorageHandler>();
}
/**
* Returns the S nodes closest to a given key by querying peers.
* This method blocks. It returns after <code>CLOSEST_NODES_LOOKUP_TIMEOUT+1</code> seconds at
* the longest.
*
* The number of pending requests never exceeds ALPHA. According to [4], this is the most efficient.
* Queries the DHT for the <code>k</code> peers closest to a given key.
* The results are sorted by distance from the key.
*
* If there are less than <code>s</code> results after the kademlia lookup finishes, nodes from
* the sibling list are used.
* This method blocks. It returns after <code>ClosestNodesLookupTask.CLOSEST_NODES_LOOKUP_TIMEOUT+1</code>
* seconds at the longest.
*
* The number of pending requests never exceeds <code>ALPHA</code>. According to [4], this is the most efficient.
* @see ClosestNodesLookupTask
*/
private Collection<Destination> getClosestNodes(Hash key) {
private List<Destination> getClosestNodes(Hash key) {
bucketManager.getKBucket(key).setLastLookupTime(System.currentTimeMillis());
bucketManager.getSBucket().setLastLookupTime(key, System.currentTimeMillis());
ClosestNodesLookupTask lookupTask = new ClosestNodesLookupTask(key, sendQueue, i2pReceiver, bucketManager);
lookupTask.run();
return lookupTask.getResults();
}
@Override
public DhtStorablePacket findOne(Hash key, Class<? extends DhtStorablePacket> dataType) {
Collection<DhtStorablePacket> results = find(key, dataType, false);
if (results.isEmpty())
return null;
else
return results.iterator().next();
public DhtResults findOne(Hash key, Class<? extends DhtStorablePacket> dataType) {
return find(key, dataType, false);
}
@Override
public Collection<DhtStorablePacket> findAll(Hash key, Class<? extends DhtStorablePacket> dataType) {
public DhtResults findAll(Hash key, Class<? extends DhtStorablePacket> dataType) {
return find(key, dataType, true);
}
@ -135,27 +157,34 @@ public class KademliaDHT implements DHT, PacketListener {
public void setStorageHandler(Class<? extends DhtStorablePacket> packetType, DhtStorageHandler storageHandler) {
storageHandlers.put(packetType, storageHandler);
}
@Override
public boolean isConnected() {
return connected;
}
@Override
public int getNumPeers() {
return bucketManager.getPeerCount();
}
private Collection<DhtStorablePacket> find(Hash key, Class<? extends DhtStorablePacket> dataType, boolean exhaustive) {
@Override
public DhtPeerStats getPeerStats() {
return bucketManager.getPeerStats();
}
private DhtResults find(Hash key, Class<? extends DhtStorablePacket> dataType, boolean exhaustive) {
final Collection<Destination> closeNodes = getClosestNodes(key);
log.debug("Querying " + closeNodes.size() + " nodes for data type " + dataType + ", Kademlia key " + key);
log.debug("Querying localhost + " + closeNodes.size() + " peers for data type " + dataType.getSimpleName() + ", Kademlia key " + key);
final Collection<I2PBotePacket> receivedPackets = new ConcurrentHashSet<I2PBotePacket>(); // avoid adding duplicate packets
/* PacketListener packetListener = new PacketListener() {
@Override
public void packetReceived(CommunicationPacket packet, Destination sender, long receiveTime) {
// add packet to list of received packets if the packet is in response to a RetrieveRequest
if (packet instanceof RetrieveRequest && closeNodes.contains(sender))
receivedPackets.add(packet);
}
};
i2pReceiver.addPacketListener(packetListener);*/
DhtStorablePacket localResult = findLocally(key, dataType);
// if a local packet exists and one result is requested, return the local packet
if (!exhaustive && localResult!=null) {
log.debug("Locally stored packet found for hash " + key + " and data type " + dataType);
DhtResults results = new DhtResults();
results.put(localDestination, localResult);
return results;
}
// Send the retrieve requests
PacketBatch batch = new PacketBatch();
@ -172,9 +201,9 @@ public class KademliaDHT implements DHT, PacketListener {
// wait for replies
try {
if (exhaustive)
TimeUnit.SECONDS.sleep(60); // TODO make a static field
batch.awaitAllResponses(RESPONSE_TIMEOUT, TimeUnit.SECONDS);
else
batch.awaitFirstReply(60, TimeUnit.SECONDS); // TODO make a static field
batch.awaitFirstReply(RESPONSE_TIMEOUT, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
log.warn("Interrupted while waiting for responses to Retrieve Requests.", e);
@ -182,15 +211,8 @@ public class KademliaDHT implements DHT, PacketListener {
log.debug(batch.getResponses().size() + " response packets received for hash " + key + " and data type " + dataType);
sendQueue.remove(batch);
// i2pReceiver.removePacketListener(packetListener);
ConcurrentHashSet<DhtStorablePacket> storablePackets = getStorablePackets(batch);
DhtStorablePacket localResult = findLocally(key, dataType);
if (localResult != null) {
log.debug("Locally stored packet found for hash " + key + " and data type " + dataType);
storablePackets.add(localResult);
}
return storablePackets;
return getDhtResults(batch, localResult);
}
private DhtStorablePacket findLocally(Hash key, Class<? extends DhtStorablePacket> dataType) {
@ -202,30 +224,51 @@ public class KademliaDHT implements DHT, PacketListener {
}
/**
* Returns all <code>DhtStorablePacket</code> packets that have been received as a response to a send batch.
* Returns all <code>DhtStorablePacket</code> packets that have been received as a response to a send batch,
* plus <code>localResult</code> if it is non-<code>null</code>.
* @param batch
* @param localResult
* @return
*/
private ConcurrentHashSet<DhtStorablePacket> getStorablePackets(PacketBatch batch) {
ConcurrentHashSet<DhtStorablePacket> storablePackets = new ConcurrentHashSet<DhtStorablePacket>();
for (I2PBotePacket packet: batch.getResponses())
private DhtResults getDhtResults(PacketBatch batch, DhtStorablePacket localResult) {
DhtResults results = new DhtResults();
for (Entry<Destination, DataPacket> result: batch.getResponses().entrySet()) {
DataPacket packet = result.getValue();
if (packet instanceof DhtStorablePacket)
storablePackets.add((DhtStorablePacket)packet);
return storablePackets;
results.put(result.getKey(), (DhtStorablePacket)packet);
}
if (localResult != null)
results.put(localDestination, localResult);
return results;
}
@Override
public void store(DhtStorablePacket packet) throws NoSuchAlgorithmException {
public void store(DhtStorablePacket packet) throws DhtException {
Hash key = packet.getDhtKey();
Collection<Destination> closeNodes = getClosestNodes(key);
List<Destination> closeNodes = getClosestNodes(key);
if (closeNodes.isEmpty())
throw new DhtException("Cannot store packet because no storage nodes found.");
// store on local node if appropriate
if (closeNodes.size()<KademliaConstants.K || isCloser(localDestination, closeNodes.get(0), key))
closeNodes.add(localDestination);
log.debug("Storing a " + packet.getClass().getSimpleName() + " with key " + key + " on " + closeNodes.size() + " nodes");
HashCash hashCash = HashCash.mintCash("", 1); // TODO
StoreRequest storeRequest = new StoreRequest(hashCash, packet);
HashCash hashCash;
try {
hashCash = HashCash.mintCash("", 1); // TODO
} catch (NoSuchAlgorithmException e) {
throw new DhtException("Cannot mint HashCash.", e);
}
PacketBatch batch = new PacketBatch();
for (Destination node: closeNodes)
for (Destination node: closeNodes) {
StoreRequest storeRequest = new StoreRequest(hashCash, packet); // use a separate packet id for each request
batch.putPacket(storeRequest, node);
}
sendQueue.send(batch);
try {
@ -239,72 +282,207 @@ public class KademliaDHT implements DHT, PacketListener {
sendQueue.remove(batch);
}
@Override
public void start() {
i2pReceiver.addPacketListener(this);
bucketManager.start();
bootstrap();
}
@Override
public void shutDown() {
i2pReceiver.removePacketListener(this);
bucketManager.requestShutdown();
writePeersToFile(peerFile);
/**
* Returns <code>true</code> if <code>dest1</code> is closer to <code>key</code> than <code>dest2</code>.
* @param key
* @param destination
* @param peers
* @return
*/
private boolean isCloser(Destination dest1, Destination dest2, Hash key) {
return new PeerDistanceComparator(key).compare(dest1, dest2) < 0;
}
/**
* Connects to the Kademlia network; blocks until done.
*/
private void bootstrap() {
new BootstrapTask().start();
new BootstrapTask(i2pReceiver).run();
}
private class BootstrapTask extends Thread {
public BootstrapTask() {
setDaemon(true);
private class BootstrapTask implements Runnable, PacketListener {
I2PPacketDispatcher i2pReceiver;
public BootstrapTask(I2PPacketDispatcher i2pReceiver) {
this.i2pReceiver = i2pReceiver;
}
@Override
public void run() {
log.debug("Bootstrap start");
while (true) {
i2pReceiver.addPacketListener(this);
outerLoop:
while (!shutdownRequested()) {
for (KademliaPeer bootstrapNode: initialPeers) {
bootstrapNode.setLastReception(-1);
bootstrapNode.setActiveSince(System.currentTimeMillis()); // Set the "active since" time to the current time before every bootstrap attempt
bucketManager.addOrUpdate(bootstrapNode);
Collection<Destination> closestNodes = getClosestNodes(localDestinationHash);
// if last reception time is not set, the node didn't respond, so remove it
if (bootstrapNode.getLastReception() <= 0)
bucketManager.remove(bootstrapNode);
if (closestNodes.isEmpty()) {
log.debug("No response from bootstrap node " + bootstrapNode);
log.debug("No response from bootstrap node " + bootstrapNode.calculateHash());
bucketManager.remove(bootstrapNode);
}
else {
bucketManager.refreshAll();
log.debug("Response from bootstrap node received, refreshing all buckets. Bootstrap node = " + bootstrapNode.calculateHash());
refreshAll();
log.info("Bootstrapping finished. Number of peers = " + bucketManager.getPeerCount());
return;
for (Destination peer: bucketManager.getAllPeers())
log.debug(" Peer: " + peer.calculateHash());
break outerLoop;
}
}
log.warn("Can't bootstrap off any known peer, will retry shortly.");
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
log.error("Interrupted while pausing after unsuccessful bootstrap attempt.", e);
}
awaitShutdownRequest(1, TimeUnit.MINUTES);
}
i2pReceiver.removePacketListener(this);
}
/**
* When a previously unknown peer contacts us, this method adds it to <code>initialPeers</code>
* so it can be used as a bootstrap node.
* @param packet
* @param sender
* @param receiveTime
*/
@Override
public void packetReceived(CommunicationPacket packet, Destination sender, long receiveTime) {
initialPeers.add(new KademliaPeer(sender, receiveTime));
}
}
/**
* Writes all peers to a file in descending order of "last seen" time.
* @param peerFile
* "refresh all k-buckets further away than the closest neighbor. This refresh is just a
* lookup of a random key that is within that k-bucket range."
*/
private void writePeersToFile(File peerFile) {
// TODO
public void refreshAll() {
for (KBucket bucket: Util.synchronizedCopy(bucketManager))
refresh(bucket);
}
/**
* Refreshes all buckets whose <code>lastLookupTime</code> is too old.
*/
private void refreshOldBuckets() {
long now = System.currentTimeMillis();
// refresh k-buckets
for (KBucket bucket: Util.synchronizedCopy(bucketManager))
if (now > bucket.getLastLookupTime() + KademliaConstants.BUCKET_REFRESH_INTERVAL*1000) {
log.debug("Refreshing bucket: " + bucket);
refresh(bucket);
}
// Refresh the s-bucket by doing a lookup for a random key in each section of the bucket.
// For example, if k=20 and s=100, there would be a lookup for a random key between
// the 0th and the 20th sibling (i=0), another one for a random key between the 20th
// and the 40th sibling (i=1), etc., and finally a lookup for a random key between the
// 80th and the 100th sibling (i=4).
SBucket sBucket = bucketManager.getSBucket();
for (BucketSection section: sBucket.getSections())
if (now > section.getLastLookupTime() + KademliaConstants.BUCKET_REFRESH_INTERVAL*1000)
refresh(sBucket, section.getStart(), section.getEnd());
}
private void refresh(KBucket bucket) {
Hash key = createRandomHash(bucket.getStartId(), bucket.getEndId());
getClosestNodes(key);
}
private void refresh(AbstractBucket bucket, BigInteger min, BigInteger max) {
Hash key = createRandomHash(min, max);
getClosestNodes(key);
}
/**
* Returns a random value <code>r</code> such that <code>min &lt;= r &lt; max</code>.
* @param min
* @param max
* @return
*/
private Hash createRandomHash(BigInteger min, BigInteger max) {
BigInteger hashValue;
if (min.compareTo(max) >= 0)
hashValue = min;
else {
hashValue = new BigInteger(Hash.HASH_LENGTH*8, RandomSource.getInstance()); // a random number between 0 and 2^256-1
hashValue = min.add(hashValue.mod(max.subtract(min))); // a random number equal to or greater than min, and less than max
}
byte[] hashArray = hashValue.toByteArray();
if (hashArray.length>Hash.HASH_LENGTH+1 || (hashArray.length==Hash.HASH_LENGTH+1 && hashArray[0]!=0)) // it's okay for the array length to be Hash.HASH_LENGTH if the zeroth byte only contains the sign bit
log.error("Hash value too big to fit in " + Hash.HASH_LENGTH + " bytes: " + hashValue);
byte[] hashArrayPadded = new byte[Hash.HASH_LENGTH];
if (hashArray.length == Hash.HASH_LENGTH + 1)
System.arraycopy(hashArray, 1, hashArrayPadded, 0, Hash.HASH_LENGTH);
else
System.arraycopy(hashArray, 0, hashArrayPadded, Hash.HASH_LENGTH-hashArray.length, hashArray.length);
return new Hash(hashArrayPadded);
}
/**
* Writes all peers to a file, sorted in descending order of uptime.
* @param file
*/
private void writePeersSorted(File file) {
List<KademliaPeer> peers = bucketManager.getAllPeers();
if (peers.isEmpty())
return;
sortByUptime(peers);
log.info("Writing peers to file: <" + file.getAbsolutePath() + ">");
writePeers(peers, file);
}
private void writePeers(List<KademliaPeer> peers, File file) {
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(file));
writer.write("# Each line is one Base64-encoded I2P destination.");
writer.newLine();
writer.write("# Do not edit while I2P-Bote is running as it will be overwritten.");
writer.newLine();
for (KademliaPeer peer: peers) {
writer.write(peer.toBase64());
writer.newLine();
}
}
catch (IOException e) {
log.error("Can't write peers to file <" + file.getAbsolutePath() + ">", e);
}
finally {
if (writer != null)
try {
writer.close();
}
catch (IOException e) {
log.error("Can't close BufferedWriter for file <" + file.getAbsolutePath() + ">", e);
}
}
}
/**
* Sorts a list of peers in descending order of "active since" time.
* Locked peers are placed after the last unlocked peer.
* @param peers
*/
private void sortByUptime(List<KademliaPeer> peers) {
Collections.sort(peers, new Comparator<KademliaPeer>() {
@Override
public int compare(KademliaPeer peer1, KademliaPeer peer2) {
if (peer1.isLocked() || peer2.isLocked()) {
int n1 = peer1.isLocked() ? 0 : 1;
int n2 = peer2.isLocked() ? 0 : 1;
return n2 - n1;
}
else
return Long.valueOf(peer2.getActiveSince()).compareTo(peer1.getActiveSince());
}
});
}
private void readPeers(URL url) {
log.info("Reading peers from URL: '" + url + "'");
log.info("Reading peers from URL: <" + url + ">");
InputStream stream = null;
try {
stream = url.openStream();
@ -322,11 +500,11 @@ public class KademliaDHT implements DHT, PacketListener {
}
}
private void readPeers(File peerFile) {
log.info("Reading peers from file: '" + peerFile.getAbsolutePath() + "'");
private void readPeers(File file) {
log.info("Reading peers from file: <" + file.getAbsolutePath() + ">");
InputStream stream = null;
try {
stream = new FileInputStream(peerFile);
stream = new FileInputStream(file);
readPeers(stream);
} catch (IOException e) {
log.error("Error reading peers from file.", e);
@ -360,7 +538,7 @@ public class KademliaDHT implements DHT, PacketListener {
KademliaPeer peer = new KademliaPeer(destination, 0);
// don't add the local destination as a peer
if (!peer.getDestinationHash().equals(localDestinationHash))
if (!peer.getDestination().equals(localDestination))
initialPeers.add(peer);
}
catch (DataFormatException e) {
@ -372,7 +550,7 @@ public class KademliaDHT implements DHT, PacketListener {
private void sendPeerList(FindClosePeersPacket packet, Destination destination) {
// TODO don't include the requesting peer
Collection<KademliaPeer> closestPeers = bucketManager.getClosestPeers(packet.getKey(), KademliaConstants.K);
Collection<Destination> closestPeers = bucketManager.getClosestPeers(packet.getKey(), KademliaConstants.K);
PeerList peerList = new PeerList(closestPeers);
sendQueue.sendResponse(peerList, destination, packet.getPacketId());
}
@ -398,13 +576,12 @@ public class KademliaDHT implements DHT, PacketListener {
if (storageHandler != null) {
DhtStorablePacket storedPacket = storageHandler.retrieve(retrieveRequest.getKey());
// if requested packet found, send it to the requester
if (storedPacket != null) {
log.debug("Packet found for retrieve request: [" + retrieveRequest + "], replying to sender: [" + sender + "]");
ResponsePacket response = new ResponsePacket(storedPacket, StatusCode.OK, retrieveRequest.getPacketId());
sendQueue.send(response, sender);
}
ResponsePacket response = new ResponsePacket(storedPacket, StatusCode.OK, retrieveRequest.getPacketId());
if (storedPacket != null)
log.debug("Packet found for retrieve request: [" + retrieveRequest + "], replying to sender: [" + sender.calculateHash() + "]");
else
log.debug("No matching packet found for retrieve request: [" + retrieveRequest + "]");
sendQueue.send(response, sender);
}
else
log.warn("No storage handler found for type " + packet.getClass().getSimpleName() + ".");
@ -413,4 +590,28 @@ public class KademliaDHT implements DHT, PacketListener {
// bucketManager is not registered as a PacketListener, so notify it here
bucketManager.packetReceived(packet, sender, receiveTime);
}
@Override
public void run() {
i2pReceiver.addPacketListener(this);
bootstrap();
connected = true;
while (!shutdownRequested()) {
if (bucketManager.getUnlockedPeerCount() == 0) {
log.debug("All peers are gone. Re-bootstrapping.");
bootstrap();
}
refreshOldBuckets();
// TODO replicate();
awaitShutdownRequest(1, TimeUnit.MINUTES);
}
writePeersSorted(peerFile);
i2pReceiver.removePacketListener(this);
}
@Override
public void awaitShutdown(long timeout) throws InterruptedException {
join(timeout);
}
}

View File

@ -21,33 +21,35 @@
package i2p.bote.network.kademlia;
import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.Log;
public class KademliaPeer {
private static final int STALE_THRESHOLD = 5;
public class KademliaPeer extends Destination {
private Log log = new Log(KademliaPeer.class);
private Destination destination;
private Hash destinationHash;
private long lastPingSent;
private long lastReception;
private long activeSince;
private AtomicInteger consecutiveTimeouts;
private volatile int consecutiveTimeouts;
private long lockedUntil;
public KademliaPeer(Destination destination, long lastReception) {
// initialize the Destination part of the KademliaPeer
setCertificate(destination.getCertificate());
setSigningPublicKey(destination.getSigningPublicKey());
setPublicKey(destination.getPublicKey());
// initialize KademliaPeer-specific fields
this.destination = destination;
destinationHash = destination.calculateHash();
if (destinationHash == null)
log.error("calculateHash() returned null!");
this.lastReception = lastReception;
lastPingSent = 0;
activeSince = lastReception;
consecutiveTimeouts = new AtomicInteger(0);
}
public KademliaPeer(Destination destination) {
this(destination, System.currentTimeMillis());
}
public Destination getDestination() {
@ -57,41 +59,44 @@ public class KademliaPeer {
public Hash getDestinationHash() {
return destinationHash;
}
public boolean isStale() {
return consecutiveTimeouts.get() >= STALE_THRESHOLD;
/**
* @param activeSince Milliseconds since Jan 1, 1970
* @return
*/
void setActiveSince(long activeSince) {
this.activeSince = activeSince;
}
public void incrementStaleCounter() {
consecutiveTimeouts.incrementAndGet();
}
public void resetStaleCounter() {
consecutiveTimeouts.set(0);
}
public long getLastPingSent() {
return lastPingSent;
}
public void setLastReception(long time) {
lastReception = time;
}
public long getLastReception() {
return lastReception;
}
public long getActiveSince() {
return activeSince;
}
public int getConsecTimeouts() {
return consecutiveTimeouts;
}
/* public BigInteger getDistance(KademliaPeer anotherPeer) {
return KademliaUtil.getDistance(getDestinationHash(), anotherPeer.getDestinationHash());
}*/
@Override
public String toString() {
return destination.toString();
long getLockedUntil() {
return lockedUntil;
}
boolean isLocked() {
return lockedUntil > System.currentTimeMillis();
}
/**
* Locks the peer for 2 minutes after the first timeout, 4 minutes after
* two consecutive timeouts, 8 minutes after 3 consecutive timeouts, etc.,
* up to 2^10 minutes (about 17h) for 10 or more consecutive timeouts.
*/
synchronized void noResponse() {
consecutiveTimeouts++;
int lockDuration = 1 << Math.min(consecutiveTimeouts, 10); // in minutes
lockedUntil = System.currentTimeMillis() + 60*1000*lockDuration;
}
void responseReceived() {
consecutiveTimeouts = 0;
lockedUntil = 0;
}
}

View File

@ -0,0 +1,97 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.network.kademlia;
import static i2p.bote.Util._;
import i2p.bote.network.DhtPeerStats;
import java.math.BigInteger;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.i2p.data.Hash;
/**
* Stores information on Kademlia peers.
* @see DhtPeerStats
*/
public class KademliaPeerStats implements DhtPeerStats {
private List<String> header;
private List<List<String>> data;
KademliaPeerStats(SBucket sBucket, List<KBucket> kBuckets, Hash localDestinationHash) {
String[] headerArray = new String[] {_("Peer"), _("I2P Destination"), _("BktPfx"), _("Distance"), _("Locked?"), _("Active Since")};
header = Arrays.asList(headerArray);
data = new ArrayList<List<String>>();
addPeerData(sBucket, localDestinationHash);
for (KBucket kBucket: kBuckets)
addPeerData(kBucket, localDestinationHash);
}
private void addPeerData(AbstractBucket bucket, Hash localDestinationHash) {
DateFormat formatter = DateFormat.getDateTimeInstance();
for (KademliaPeer peer: bucket) {
List<String> row = new ArrayList<String>();
row.add(String.valueOf(data.size() + 1));
row.add(peer.calculateHash().toBase64());
row.add(getBucketPrefix(bucket));
BigInteger distance = KademliaUtil.getDistance(localDestinationHash, peer.calculateHash());
row.add(distance.shiftRight((Hash.HASH_LENGTH-2)*8).toString()); // show the 2 most significant bytes
row.add(String.valueOf(peer.isLocked() ? _("Yes")+"("+(peer.getConsecTimeouts())+")" : _("No")));
String activeSince = formatter.format(peer.getActiveSince());
row.add(String.valueOf(activeSince));
data.add(row);
}
}
/**
* Returns the common prefix shared by the binary representation of all peers in the bucket.
* @param bucket
* @return
*/
private String getBucketPrefix(AbstractBucket bucket) {
if (bucket instanceof KBucket) {
KBucket kBucket = (KBucket)bucket;
String prefix = kBucket.getBucketPrefix();
if (prefix.isEmpty())
return _("(None)");
else
return prefix;
}
else
return _("(S)");
}
@Override
public List<String> getHeader() {
return header;
}
@Override
public List<List<String>> getData() {
return data;
}
}

View File

@ -28,13 +28,17 @@ import net.i2p.data.Hash;
public class KademliaUtil {
public static BigInteger getDistance(KademliaPeer node, Hash key) {
return getDistance(node.getDestinationHash(), key);
}
/**
* Calculates the Kademlia distance (XOR distance) between two hashes.
* If the hashes are equal, the distance is zero; otherwise, it is greater
* than zero.
* @param key1
* @param key2
* @return
*/
public static BigInteger getDistance(Hash key1, Hash key2) {
// This shouldn't be a performance bottleneck, so save some mem by not using Hash.cachedXor
byte[] xoredData = DataHelper.xor(key1.getData(), key2.getData());
return new BigInteger(xoredData);
return new BigInteger(1, xoredData);
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.network.kademlia;
import java.math.BigInteger;
import java.util.Comparator;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
public class PeerDistanceComparator implements Comparator<Destination> {
private Hash reference;
PeerDistanceComparator(Hash reference) {
this.reference = reference;
}
@Override
public int compare(Destination peer1, Destination peer2) {
BigInteger distance1 = KademliaUtil.getDistance(peer1.calculateHash(), reference);
BigInteger distance2 = KademliaUtil.getDistance(peer2.calculateHash(), reference);
return distance1.compareTo(distance2);
}
}

View File

@ -0,0 +1,168 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.network.kademlia;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import net.i2p.data.Hash;
import net.i2p.util.Log;
/**
* An {@link AbstractBucket} that implements an S/Kademlia sibling list.
* Peers are kept in an <code>ArrayList</code> sorted by XOR distance
* from the local destination. The closest peer is at index 0, the
* most distant peer is at index <code>n-1</code>.
*/
public class SBucket extends AbstractBucket {
private Log log = new Log(SBucket.class);
private PeerDistanceComparator distanceComparator;
private BucketSection[] sections; // used for refreshing the s-bucket
public SBucket(Hash localDestinationHash) {
super(KademliaConstants.S);
distanceComparator = new PeerDistanceComparator(localDestinationHash);
int numSections = (KademliaConstants.S - 1) / KademliaConstants.K + 1;
if (numSections < 1)
numSections = 1;
sections = new BucketSection[numSections];
for (int i=0; i<numSections; i++)
sections[i] = new BucketSection(BigInteger.ZERO, BigInteger.ZERO, 0);
}
/**
* Adds/updates a peer if there is room in the bucket, or if the peer is not
* further away from the local destination than the furthest sibling in the
* bucket.
* @param peer
* @return The peer that was removed to make room for the new peer, or
* <code>null</code> if no peer was removed from the bucket. If
* <code>peer</code> didn't exist in the bucket but was not added
* because it was too far away from the local destination,
* <code>peer</code> itself is returned.
*/
KademliaPeer addOrUpdate(KademliaPeer peer) {
synchronized(peers) {
int index = Collections.binarySearch(peers, peer, distanceComparator);
if (index >= 0) { // destination is already in the bucket, so update it
peers.get(index).responseReceived();
return null;
}
else {
int insertionPoint = -(index+1);
if (isFull()) {
// insertionPoint can only be equal to or greater than size() at this point, see Collections.binarySearch javadoc
if (insertionPoint > size())
log.error("insertionPoint > size(), this shouldn't happen.");
if (insertionPoint < size()) { // if destination is closer than an existing sibling, replace the furthest away sibling and return the removed sibling
peers.add(insertionPoint, new KademliaPeer(peer));
KademliaPeer removedPeer = peers.remove(size() - 1);
return removedPeer;
}
else // insertionPoint==size(), this means the new peer is further away than all other siblings
return peer;
}
else {
peers.add(insertionPoint, peer);
return null;
}
}
}
}
BucketSection[] getSections() {
if (peers.isEmpty())
return sections;
BigInteger[] siblingIDs = new BigInteger[peers.size()];
int siblingIndex = 0;
for (KademliaPeer peer: peers) {
siblingIDs[siblingIndex] = new BigInteger(1, peer.getDestinationHash().getData());
siblingIndex++;
}
Arrays.sort(siblingIDs);
for (int i=0; i<sections.length; i++) {
sections[i].start = siblingIDs[peers.size()*i/sections.length];
if (i == sections.length-1)
sections[i].end = siblingIDs[peers.size()-1].add(BigInteger.ONE);
else
sections[i].end = siblingIDs[peers.size()*(i+1)/sections.length];
}
return sections;
}
private BucketSection getSection(Hash key) {
for (BucketSection section: sections)
if (section.contains(key))
return section;
return null;
}
void setLastLookupTime(Hash key, long lastLookupTime) {
BucketSection section = getSection(key);
if (section != null)
section.lastLookupTime = lastLookupTime;
}
/**
* Stores the start and end key, and the time of the last lookup, for one section of the s-bucket.
* There are ceil(s/k) sections in the bucket.
*/
class BucketSection {
private BigInteger start;
private BigInteger end;
private long lastLookupTime;
public BucketSection(BigInteger start, BigInteger end, long lastLookupTime) {
this.start = start;
this.end = end;
this.lastLookupTime = lastLookupTime;
}
long getLastLookupTime() {
return lastLookupTime;
}
public BigInteger getStart() {
return start;
}
public void setStart(BigInteger start) {
this.start = start;
}
public BigInteger getEnd() {
return end;
}
public boolean contains(Hash key) {
BigInteger keyValue = new BigInteger(1, key.getData());
return (start.compareTo(keyValue)<=0 && keyValue.compareTo(end)<0);
}
}
}

View File

@ -1,3 +1,4 @@
# Each line in this file should be a 516-byte I2P destination key
# in Base64 format. Lines beginning with # are ignored.
AHZwTj~5kmXod-2jFj1RamSFMBijtEnp9x3FWrF19uJQQga~rH4OaAtqdrHBeKfSBwugWEQMI8~shhBpk3H~fgQEd24RntmItdFzCsdeKz~0WnYqvhZo-ZryXk6J08wTPX9hX8BWuJSTgQyJo7WvDEqxx2AxQUIDyWVPld7VdfrpNEFWTQPNo0YxQWqSlnhSZr~hIVEV6hRsRgAP89Il8tnRa0f2YqZI7Ac2p8btVTkXMExVfLqZ2Y6J71K53oJ1mXZCISGUVAJhRWhdl~QKT8x-V5~5iSLoGG0514E4KOGc1ViKhl4YvhfCVXezm-oVDT8e9QTgy1pYjk9U88ng7hg~vjlCx8g2F3zTbZMaMZch4K50j7cRHFxvWA47EiEw~AtlEWhzTyQY2HMfQE-Hj7CJISdoaPk3ehvpkSDPdLWDSeQ~7rSYujWBAH~KI1DViRElS-1oJ3rPLqNTA2IwywbbaCs0LnAX9NrTnwIx4X6~kr7Re8DLVw5Gn-vjsrYjAAAA
CYnzmb1axrzpDXEX9haV7qBT-jGxFmLmkV913DgPqgJ1lVcSfpIrTe4ETMsRRjd2JMvGJxBYznENH8oTlitVptNAe8HjXYxjPqCRkHeh3QCk9nZALTWxjGy90DUHytIytPwRYqIZlLneiePhVFgV-HXOne~Bsv-mT5zrfDIDNhkJlmyXq5vLcvTkqp-eDEMr6iC4njCNKjzHDxsTvwwecYSmzFDY2f0SMZhrmL3qKSofMVPwpEVwkBX7LF50TSOfPvIEVgX7uPKI~avIOrShksSdJ0ksDQkw5Q66tOp-yrgxh--FnQYnblfLMBhI5UyWuifV3HqGqctjjHfW2mEOVpIoarjgbdmN9knThRBlgC8gs7AffoYuKs~GbIsE4PpnJI2rS3skBfPoNcmBtUF2bfQZ6gb3zk8NH7alXWMciF3wXyLka2FBuQKQQz4fUuraVzQu0COncv0hpmV5CIO7HWTs8pdem6~r0NydoRF3wrGj8xDeySTVJfmDfXCKhi0GAAAA

View File

@ -21,6 +21,7 @@
package i2p.bote.packet;
import i2p.bote.I2PBote;
import i2p.bote.UniqueId;
import java.io.IOException;
@ -31,7 +32,6 @@ import java.util.concurrent.TimeUnit;
import net.i2p.util.Log;
public abstract class CommunicationPacket extends I2PBotePacket {
private static final byte PACKET_VERSION = 1;
protected static final int HEADER_LENGTH = 6 + UniqueId.LENGTH; // length of the common packet header in the byte array representation; this is where subclasses start reading
private static final byte[] PACKET_PREFIX = new byte[] {(byte)0x6D, (byte)0x30, (byte)0x52, (byte)0xE9};
private static Log static_log = new Log(CommunicationPacket.class);
@ -46,16 +46,20 @@ public abstract class CommunicationPacket extends I2PBotePacket {
}
protected CommunicationPacket(UniqueId packetId) {
super(I2PBote.PROTOCOL_VERSION);
this.packetId = packetId;
sentSignal = new CountDownLatch(1);
sentTime = -1;
}
/**
* Creates a packet and initializes the header fields shared by all Communication Packets: packet type and packet id.
* Creates a packet and initializes the header fields shared by all Communication Packets:
* packet type, protocol version, and packet id.
* Subclasses should start reading at byte <code>HEADER_LENGTH</code> after calling this constructor.
* @param data
*/
protected CommunicationPacket(byte[] data) {
super(data[5]); // byte 5 is the protocol version in a communication packet
verifyHeader(data);
checkPacketType(data[4]);
packetId = new UniqueId(data, 6);
@ -67,8 +71,9 @@ public abstract class CommunicationPacket extends I2PBotePacket {
* @param data
* @param log
* @return
* @throws MalformedCommunicationPacketException
*/
public static CommunicationPacket createPacket(byte[] data) {
public static CommunicationPacket createPacket(byte[] data) throws MalformedCommunicationPacketException {
char packetTypeCode = (char)data[4]; // byte 4 of a communication packet is the packet type code
Class<? extends I2PBotePacket> packetType = decodePacketTypeCode(packetTypeCode);
if (packetType==null || !CommunicationPacket.class.isAssignableFrom(packetType)) {
@ -81,18 +86,21 @@ public abstract class CommunicationPacket extends I2PBotePacket {
return commPacketType.getConstructor(byte[].class).newInstance(data);
}
catch (Exception e) {
static_log.warn("Can't instantiate packet for type code <" + packetTypeCode + ">", e);
return null;
if (e instanceof MalformedCommunicationPacketException)
throw (MalformedCommunicationPacketException)e;
else
throw new MalformedCommunicationPacketException("Can't instantiate packet for type code <" + packetTypeCode + ">", e);
}
}
// check the packet prefix and version number of a packet
/**
* Checks that the packet has the correct packet prefix.
* @param packet
*/
private void verifyHeader(byte[] packet) {
for (int i=0; i<PACKET_PREFIX.length; i++)
if (packet[i] != PACKET_PREFIX[i])
log.error("Packet prefix invalid at byte " + i + ". Expected = " + PACKET_PREFIX[i] + ", actual = " + packet[i]);
if (packet[5] != 1)
log.error("Unsupported packet version: " + packet[5]);
}
public void setPacketId(UniqueId packetId) {
@ -121,14 +129,14 @@ public abstract class CommunicationPacket extends I2PBotePacket {
}
/**
* Writes the Prefix, Version, Type, and Packet Id fields of a I2PBote packet to
* Writes the Prefix, Version, Type, and Packet Id fields of a Communication Packet to
* an {@link OutputStream}.
* @param outputStream
*/
protected void writeHeader(OutputStream outputStream) throws IOException {
outputStream.write(PACKET_PREFIX);
outputStream.write((byte)getPacketTypeCode());
outputStream.write(PACKET_VERSION);
outputStream.write(getProtocolVersion());
outputStream.write(packetId.toByteArray());
}

View File

@ -21,8 +21,8 @@
package i2p.bote.packet;
import i2p.bote.I2PBote;
import i2p.bote.Util;
import i2p.bote.folder.FolderElement;
import java.io.File;
import java.io.FileInputStream;
@ -35,23 +35,53 @@ import net.i2p.util.Log;
/**
* The superclass of all "payload" packet types.
*/
public abstract class DataPacket extends I2PBotePacket implements FolderElement {
public abstract class DataPacket extends I2PBotePacket {
protected static final int HEADER_LENGTH = 2; // length of the common packet header in the byte array representation; this is where subclasses start reading
private static Log log = new Log(DataPacket.class);
private File file;
public DataPacket() {
}
/**
* Creates a <code>DataPacket</code> from raw datagram data. The only thing that is initialized
* is the protocol version. The packet type code is verified.
* Subclasses should start reading at byte <code>HEADER_LENGTH</code> after calling this constructor.
* @param data
*/
public DataPacket(byte[] data) {
super(data[1]); // byte 1 is the protocol version in a data packet
if (data[0] != getPacketTypeCode())
log.error("Wrong type code for " + getClass().getSimpleName() + ". Expected <" + getPacketTypeCode() + ">, got <" + (char)data[0] + ">");
}
/**
* Writes the packet to an <code>OutputStream</code> in binary representation.
*/
@Override
public void writeTo(OutputStream outputStream) throws IOException {
outputStream.write(toByteArray());
}
public static DataPacket createPacket(File file) {
/**
* Writes the Type and Protocol Version fields of a Data Packet to
* an {@link OutputStream}.
* @param outputStream
*/
protected void writeHeader(OutputStream outputStream) throws IOException {
outputStream.write((byte)getPacketTypeCode());
outputStream.write(getProtocolVersion());
}
/**
* Creates a {@link DataPacket} object from a file, using the same format as the
* {@link createPacket(byte[])} method.
* @param file
* @return
* @throws MalformedDataPacketException
*/
public static DataPacket createPacket(File file) throws MalformedDataPacketException {
if (file==null || !file.exists())
return null;
InputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
@ -59,8 +89,7 @@ public abstract class DataPacket extends I2PBotePacket implements FolderElement
return packet;
}
catch (IOException e) {
log.error("Can't read packet file: " + file.getAbsolutePath(), e);
return null;
throw new MalformedDataPacketException("Can't read packet file: " + file.getAbsolutePath(), e);
}
finally {
try {
@ -77,8 +106,9 @@ public abstract class DataPacket extends I2PBotePacket implements FolderElement
* If there is an error, <code>null</code> is returned.
* @param data
* @return
* @throws MalformedDataPacketException
*/
public static DataPacket createPacket(byte[] data) {
public static DataPacket createPacket(byte[] data) throws MalformedDataPacketException {
char packetTypeCode = (char)data[0]; // first byte of a data packet is the packet type code
Class<? extends I2PBotePacket> packetType = decodePacketTypeCode(packetTypeCode);
if (packetType==null || !DataPacket.class.isAssignableFrom(packetType)) {
@ -87,23 +117,19 @@ public abstract class DataPacket extends I2PBotePacket implements FolderElement
}
Class<? extends DataPacket> dataPacketType = packetType.asSubclass(DataPacket.class);
DataPacket packet = null;
try {
return dataPacketType.getConstructor(byte[].class).newInstance(data);
packet = dataPacketType.getConstructor(byte[].class).newInstance(data);
}
catch (Exception e) {
log.warn("Can't instantiate packet for type code <" + packetTypeCode + ">", e);
throw new MalformedDataPacketException("Can't instantiate packet for type code <" + packetTypeCode + ">", e);
}
if (packet.getProtocolVersion() != I2PBote.PROTOCOL_VERSION) {
log.warn("Ignoring " + packetType.getSimpleName() + " packet with protocol version " + packet.getProtocolVersion());
return null;
}
}
// FolderElement implementation
@Override
public File getFile() {
return file;
}
@Override
public void setFile(File file) {
this.file = file;
return packet;
}
}

View File

@ -0,0 +1,78 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.packet;
import i2p.bote.UniqueId;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import net.i2p.data.Hash;
import net.i2p.util.Log;
@TypeCode('D')
public class EmailPacketDeleteRequest extends CommunicationPacket {
private Log log = new Log(EmailPacketDeleteRequest.class);
private Hash dhtKey;
private UniqueId deletionKey;
public EmailPacketDeleteRequest(Hash dhtKey, UniqueId deletionKey) {
this.dhtKey = dhtKey;
this.deletionKey = deletionKey;
}
public EmailPacketDeleteRequest(byte[] data) {
super(data);
ByteBuffer buffer = ByteBuffer.wrap(data, HEADER_LENGTH, data.length-HEADER_LENGTH);
dhtKey = readHash(buffer);
deletionKey = new UniqueId(buffer);
if (buffer.hasRemaining())
log.debug("Email Packet Delete Request has " + buffer.remaining() + " extra bytes.");
}
public Hash getDhtKey() {
return dhtKey;
}
public UniqueId getDeletionKey() {
return deletionKey;
}
@Override
public byte[] toByteArray() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
writeHeader(outputStream);
outputStream.write(dhtKey.toByteArray());
outputStream.write(deletionKey.toByteArray());
}
catch (IOException e) {
log.error("Can't write to ByteArrayOutputStream.", e);
}
return outputStream.toByteArray();
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.packet;
public class EmptyResponse extends DataPacket {
@Override
public byte[] toByteArray() {
return new byte[0];
}
}

View File

@ -42,6 +42,17 @@ import net.i2p.data.SessionKey;
import net.i2p.util.Log;
import net.i2p.util.RandomSource;
/**
* An <code>EncryptedEmailPacket</code> basically contains an encrypted <code>UnencryptedEmailPacket</code>
* and an unencrypted copy of the deletion key. The unencrypted deletion key (<code>deletionKeyPlain</code>)
* is used for validation of delete requests (the sender of a delete request has to decrypt the deletion key
* using the decryption key for the Email Destination because it doesn't have the unencrypted copy).
* deletion key).
* An alternative to this scheme would have been to only use plain-text deletion keys in email packets and
* have the recipient sign deletion requests, but this would require a storage node to know the recipient's
* public signing key.
*
*/
@TypeCode('E')
public class EncryptedEmailPacket extends DhtStorablePacket {
private static final int PADDED_SIZE = PrivateKey.KEYSIZE_BYTES; // TODO is this a good choice?
@ -51,24 +62,6 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
private UniqueId deletionKeyPlain;
private byte[] encryptedData; // the encrypted fields of an I2PBote email packet: Encrypted Deletion Key, Message ID, Fragment Index, Number of Fragments, and Content.
/**
* Creates an <code>EncryptedEmailPacket</code> from raw datagram data.
* To read the encrypted parts of the packet, <code>decrypt</code> must be called first.
* @param data
*/
public EncryptedEmailPacket(byte[] data) {
ByteBuffer buffer = ByteBuffer.wrap(data);
if (buffer.get() != getPacketTypeCode())
log.error("Wrong type code for EncryptedEmailPacket. Expected <" + getPacketTypeCode() + ">, got <" + (char)data[0] + ">");
dhtKey = readHash(buffer);
deletionKeyPlain = new UniqueId(buffer);
int encryptedLength = buffer.getShort(); // length of the encrypted part of the packet
encryptedData = new byte[encryptedLength];
buffer.get(encryptedData);
}
/**
* Creates an <code>EncryptedEmailPacket</code> from an <code>UnencryptedEmailPacket</code>.
* The public key of <code>emailDestination</code> is used for encryption.
@ -78,13 +71,13 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
*/
public EncryptedEmailPacket(UnencryptedEmailPacket unencryptedPacket, EmailDestination emailDestination, I2PAppContext appContext) {
dhtKey = generateRandomHash();
deletionKeyPlain = unencryptedPacket.getPlaintextDeletionKey();
deletionKeyPlain = unencryptedPacket.getVerificationDeletionKey();
// put all the encrypted fields into an array
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
DataOutputStream dataStream = new DataOutputStream(byteStream);
try {
unencryptedPacket.getVerificationDeletionKey().writeTo(dataStream);
deletionKeyPlain.writeTo(dataStream);
unencryptedPacket.getMessageId().writeTo(dataStream);
dataStream.writeShort(unencryptedPacket.getFragmentIndex());
dataStream.writeShort(unencryptedPacket.getNumFragments());
@ -100,14 +93,32 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
}
}
private Hash generateRandomHash() {
RandomSource randomSource = RandomSource.getInstance();
/**
* Creates an <code>EncryptedEmailPacket</code> from raw datagram data.
* To read the encrypted parts of the packet, <code>decrypt</code> must be called first.
* @param data
*/
public EncryptedEmailPacket(byte[] data) {
super(data);
byte[] bytes = new byte[Hash.HASH_LENGTH];
for (int i=0; i<bytes.length; i++)
bytes[i] = (byte)randomSource.nextInt(256);
return new Hash(bytes);
ByteBuffer buffer = ByteBuffer.wrap(data, HEADER_LENGTH, data.length-HEADER_LENGTH);
dhtKey = readHash(buffer);
deletionKeyPlain = new UniqueId(buffer);
int encryptedLength = buffer.getShort(); // length of the encrypted part of the packet
encryptedData = new byte[encryptedLength];
buffer.get(encryptedData);
}
private Hash generateRandomHash() {
RandomSource randomSource = RandomSource.getInstance();
byte[] bytes = new byte[Hash.HASH_LENGTH];
for (int i=0; i<bytes.length; i++)
bytes[i] = (byte)randomSource.nextInt(256);
return new Hash(bytes);
}
/**
@ -116,12 +127,12 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
* @param appContext
* @throws DataFormatException
*/
public UnencryptedEmailPacket decrypt(EmailIdentity identity, I2PAppContext appContext) throws DataFormatException {
byte[] decryptedData = decrypt(encryptedData, identity, appContext);
ByteBuffer buffer = ByteBuffer.wrap(decryptedData);
UniqueId deletionKeyVerify = new UniqueId(buffer);
UniqueId messageId = new UniqueId(buffer);
public UnencryptedEmailPacket decrypt(EmailIdentity identity, I2PAppContext appContext) throws DataFormatException {
byte[] decryptedData = decrypt(encryptedData, identity, appContext);
ByteBuffer buffer = ByteBuffer.wrap(decryptedData);
UniqueId deletionKeyVerify = new UniqueId(buffer);
UniqueId messageId = new UniqueId(buffer);
int fragmentIndex = buffer.getShort();
int numFragments = buffer.getShort();
@ -129,8 +140,8 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
byte[] content = new byte[contentLength];
buffer.get(content);
return new UnencryptedEmailPacket(deletionKeyPlain, deletionKeyVerify, messageId, fragmentIndex, numFragments, content);
}
return new UnencryptedEmailPacket(messageId, fragmentIndex, numFragments, content, deletionKeyVerify);
}
/**
* Decrypts data with an email identity's private key.
@ -139,10 +150,10 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
* @param appContext
* @return The decrypted data
*/
private byte[] decrypt(byte[] data, EmailIdentity identity, I2PAppContext appContext) throws DataFormatException {
private byte[] decrypt(byte[] data, EmailIdentity identity, I2PAppContext appContext) throws DataFormatException {
PrivateKey privateKey = identity.getPrivateEncryptionKey();
return appContext.elGamalAESEngine().decrypt(data, privateKey, appContext.sessionKeyManager());
}
}
/**
* Encrypts data with an email destination's public key.
@ -151,14 +162,14 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
* @param appContext
* @return The encrypted data
*/
// TODO should this method be in this class?
// TODO should this method be in this class?
public byte[] encrypt(byte[] data, EmailDestination emailDestination, I2PAppContext appContext) {
PublicKey publicKey = emailDestination.getPublicEncryptionKey();
SessionKey sessionKey = appContext.sessionKeyManager().createSession(publicKey);
return appContext.elGamalAESEngine().encrypt(data, publicKey, sessionKey, PADDED_SIZE);
}
public static Collection<EncryptedEmailPacket> encrypt(Collection<UnencryptedEmailPacket> packets, EmailDestination destination, I2PAppContext appContext) throws DataFormatException {
public static Collection<EncryptedEmailPacket> encrypt(Collection<UnencryptedEmailPacket> packets, EmailDestination destination, I2PAppContext appContext) {
Collection<EncryptedEmailPacket> encryptedPackets = new ArrayList<EncryptedEmailPacket>();
for (UnencryptedEmailPacket unencryptedPacket: packets)
encryptedPackets.add(new EncryptedEmailPacket(unencryptedPacket, destination, appContext));
@ -169,7 +180,7 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
public Hash getDhtKey() {
return dhtKey;
}
/**
* Returns the value of the "plain-text deletion key" field.
* Storage nodes set this to all zero bytes when the packet is retrieved.
@ -179,13 +190,13 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
return deletionKeyPlain;
}
@Override
public byte[] toByteArray() {
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
DataOutputStream dataStream = new DataOutputStream(byteArrayStream);
@Override
public byte[] toByteArray() {
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
DataOutputStream dataStream = new DataOutputStream(byteArrayStream);
try {
dataStream.write((byte)getPacketTypeCode());
writeHeader(dataStream);
dataStream.write(dhtKey.toByteArray());
dataStream.write(deletionKeyPlain.toByteArray());
dataStream.writeShort(encryptedData.length);
@ -194,14 +205,14 @@ public class EncryptedEmailPacket extends DhtStorablePacket {
catch (IOException e) {
log.error("Can't write to ByteArrayOutputStream.", e);
}
return byteArrayStream.toByteArray();
}
return byteArrayStream.toByteArray();
}
// Returns the number of bytes in the packet.
// TODO just return content.length+CONST so we don't call toByteArray every time
public int getSize() {
return toByteArray().length;
}
// Returns the number of bytes in the packet.
// TODO just return content.length+CONST so we don't call toByteArray every time
public int getSize() {
return toByteArray().length;
}
@Override
public String toString() {

View File

@ -21,11 +21,13 @@
package i2p.bote.packet;
import java.nio.ByteBuffer;
import i2p.bote.I2PBote;
import i2p.bote.packet.dht.FindClosePeersPacket;
import i2p.bote.packet.dht.RetrieveRequest;
import i2p.bote.packet.dht.StoreRequest;
import java.nio.ByteBuffer;
import net.i2p.data.Hash;
import net.i2p.util.Log;
@ -35,15 +37,30 @@ public abstract class I2PBotePacket {
@SuppressWarnings("unchecked")
private static Class<? extends I2PBotePacket>[] ALL_PACKET_TYPES = new Class[] {
RelayPacket.class, ResponsePacket.class, RetrieveRequest.class, StoreRequest.class, FindClosePeersPacket.class,
PeerList.class, EncryptedEmailPacket.class, UnencryptedEmailPacket.class, IndexPacket.class
PeerList.class, EncryptedEmailPacket.class, UnencryptedEmailPacket.class, IndexPacket.class,
EmailPacketDeleteRequest.class, IndexPacketDeleteRequest.class
};
private int protocolVersion;
/**
* Creates a new <code>I2PBotePacket</code> with the current protocol version.
*/
protected I2PBotePacket() {
protocolVersion = I2PBote.PROTOCOL_VERSION;
}
protected I2PBotePacket(int protocolVersion) {
this.protocolVersion = protocolVersion;
}
public abstract byte[] toByteArray();
/**
* Returns the size of the packet in bytes.
* @return
*/
// TODO rename to getPacketSize
public int getSize() {
return toByteArray().length;
}
@ -77,9 +94,13 @@ public abstract class I2PBotePacket {
checkPacketType((char)packetTypeCode);
}
/* protected void checkPacketVersion(byte version, byte minVersion, byte maxVersion) {
// TODO
}*/
/**
* Returns the version of the I2P-Bote network protocol used by this packet.
* @return
*/
public int getProtocolVersion() {
return protocolVersion;
}
/**
* Creates a {@link Hash} from bytes read from a {@link ByteBuffer}.

View File

@ -21,65 +21,61 @@
package i2p.bote.packet;
import i2p.bote.UniqueId;
import i2p.bote.email.EmailDestination;
import i2p.bote.packet.dht.DhtStorablePacket;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.data.DataFormatException;
import net.i2p.data.Hash;
import net.i2p.util.Log;
/**
* Stores DHT keys of Email Packets and their deletion keys.
*
* This class is not thread-safe.
*/
@TypeCode('I')
public class IndexPacket extends DhtStorablePacket {
private Log log = new Log(IndexPacket.class);
private Collection<Hash> dhtKeys; // DHT keys of email packets
private Map<Hash, UniqueId> entries;
private Hash destinationHash; // The DHT key of this packet
public IndexPacket(byte[] data) {
ByteBuffer dataBuffer = ByteBuffer.wrap(data);
if (dataBuffer.get() != getPacketTypeCode())
log.error("Wrong type code for IndexPacket. Expected <" + getPacketTypeCode() + ">, got <" + (char)data[0] + ">");
destinationHash = readHash(dataBuffer);
int numKeys = dataBuffer.get();
dhtKeys = new ArrayList<Hash>();
for (int i=0; i<numKeys; i++) {
Hash dhtKey = readHash(dataBuffer);
dhtKeys.add(dhtKey);
}
// TODO catch BufferUnderflowException; warn if extra bytes in the array
}
/**
*
* @param emailPackets One or more email packets
* @param emailDestination Determines the DHT key of this Index Packet
*/
public IndexPacket(Collection<EncryptedEmailPacket> emailPackets, EmailDestination emailDestination) {
dhtKeys = new ArrayList<Hash>();
entries = new ConcurrentHashMap<Hash, UniqueId>();
for (EncryptedEmailPacket emailPacket: emailPackets)
dhtKeys.add(emailPacket.getDhtKey());
entries.put(emailPacket.getDhtKey(), emailPacket.getPlaintextDeletionKey());
destinationHash = emailDestination.getHash();
}
/**
* Merges the DHT keys of multiple index packets into one big index packet.
* The DHT key of this packet is not initialized.
* The DHT key of this packet is set to the DHT key of the first input packet.
* @param indexPackets
* @throws IllegalArgumentException If an empty <code>Collection</code> or <code>null</code> was passed in
*/
public IndexPacket(Collection<IndexPacket> indexPackets) {
dhtKeys = new HashSet<Hash>();
if (indexPackets==null || indexPackets.isEmpty())
throw new IllegalArgumentException("This method must be invoked with at least one index packet.");
destinationHash = indexPackets.iterator().next().getDhtKey();
entries = new ConcurrentHashMap<Hash, UniqueId>();
for (IndexPacket packet: indexPackets)
dhtKeys.addAll(packet.getDhtKeys());
entries.putAll(packet.getEntries());
}
/**
@ -90,16 +86,39 @@ public class IndexPacket extends DhtStorablePacket {
this(Arrays.asList(indexPackets));
}
public IndexPacket(byte[] data) {
super(data);
ByteBuffer buffer = ByteBuffer.wrap(data, HEADER_LENGTH, data.length-HEADER_LENGTH);
destinationHash = readHash(buffer);
int numKeys = buffer.get();
entries = new ConcurrentHashMap<Hash, UniqueId>();
for (int i=0; i<numKeys; i++) {
Hash dhtKey = readHash(buffer);
UniqueId delKey = new UniqueId(buffer);
entries.put(dhtKey, delKey);
}
// TODO catch BufferUnderflowException; warn if extra bytes in the array
}
/**
* The keys are not guaranteed to be in any particular order in the array.
*/
@Override
public byte[] toByteArray() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write((byte)getPacketTypeCode());
try {
writeHeader(outputStream);
destinationHash.writeBytes(outputStream);
outputStream.write((byte)dhtKeys.size());
for (Hash dhtKey: dhtKeys)
dhtKey.writeBytes(outputStream);
// TODO in the unit test, verify that toByteArray().length = Hash.NUM_BYTES + 1 + dhtKeys.size()*Hash.NUM_BYTES
outputStream.write((byte)entries.size());
for (Entry<Hash, UniqueId> entry: entries.entrySet()) {
entry.getKey().writeBytes(outputStream);
entry.getValue().writeTo(outputStream);
}
} catch (DataFormatException e) {
log.error("Invalid format for email destination.", e);
} catch (IOException e) {
@ -109,15 +128,63 @@ public class IndexPacket extends DhtStorablePacket {
}
/**
* Returns the DHT keys of the {@link EncryptedEmailPacket}s referenced by this {@link IndexPacket}.
* Returns the deletion key for a given DHT key of an Email Packet, or <code>null</code> if
* the <code>IndexPacket</code> doesn't contain the DHT key.
* @param dhtKey
* @return
*/
public Collection<Hash> getDhtKeys() {
return dhtKeys;
public UniqueId getDeletionKey(Hash dhtKey) {
return entries.get(dhtKey);
}
/**
* Returns the DHT key of this packet.
* Returns all DHT keys in this <code>IndexPacket</code>.
* The keys are not guaranteed to be in any particular order.
* @return
*/
public Collection<Hash> getDhtKeys() {
return entries.keySet();
}
/**
* Adds an entry to the <code>IndexPacket</code>. If the DHT key exists in the packet already,
* nothing happens.
* @param dhtKey
* @param deletionKey
*/
public void put(Hash dhtKey, UniqueId deletionKey) {
entries.put(dhtKey, deletionKey);
}
/**
* Removes an entry from the <code>IndexPacket</code>.
* @param dhtKey
*/
public void remove(Hash dhtKey) {
entries.remove(dhtKey);
}
/**
* Tests if the <code>IndexPacket</code> contains a given DHT key.
* @param dhtKey
* @return <code>true</code> if the packet containes the DHT key, <code>false</code> otherwise.
*/
public boolean contains(Hash dhtKey) {
return entries.containsKey(dhtKey);
}
/**
* Returns the DHT key / deletion key pairs for the {@link EncryptedEmailPacket}s referenced
* by this <code>IndexPacket</code>.
* @return
*/
public Map<Hash, UniqueId> getEntries() {
return entries;
}
/**
* Returns the DHT key of this packet. The DHT key of an <code>IndexPacket</code> is the
* hash of the Email Destination whose entries this packet stores.
*/
@Override
public Hash getDhtKey() {

View File

@ -0,0 +1,119 @@
/**
* Copyright (C) 2009 HungryHobo@mail.i2p
*
* The GPG fingerprint for HungryHobo@mail.i2p is:
* 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
*
* This file is part of I2P-Bote.
* I2P-Bote 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 3 of the License, or
* (at your option) any later version.
*
* I2P-Bote 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 I2P-Bote. If not, see <http://www.gnu.org/licenses/>.
*/
package i2p.bote.packet;
import i2p.bote.UniqueId;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import net.i2p.data.DataFormatException;
import net.i2p.data.Hash;
import net.i2p.util.Log;
/**
* This class is not thread-safe.
*/
@TypeCode('X')
public class IndexPacketDeleteRequest extends CommunicationPacket {
private Log log = new Log(IndexPacketDeleteRequest.class);
private Hash emailDestHash;
private Map<Hash, UniqueId> entries;
public IndexPacketDeleteRequest(Hash emailDestHash) {
this.emailDestHash = emailDestHash;
entries = new HashMap<Hash, UniqueId>();
}
public IndexPacketDeleteRequest(byte[] data) {
super(data);
ByteBuffer buffer = ByteBuffer.wrap(data, HEADER_LENGTH, data.length-HEADER_LENGTH);
emailDestHash = readHash(buffer);
entries = new HashMap<Hash, UniqueId>();
int numEntries = buffer.getShort();
for (int i=0; i<numEntries; i++) {
Hash dhtKey = readHash(buffer);
UniqueId deletionKey = new UniqueId(buffer);
entries.put(dhtKey, deletionKey);
}
if (buffer.hasRemaining())
log.debug("Index Packet Delete Request has " + buffer.remaining() + " extra bytes.");
}
public void put(Hash dhtKey, UniqueId deletionKey) {
entries.put(dhtKey, deletionKey);
}
/* public Map<Hash, UniqueId> getAll() {
return entries;
}*/
public Hash getEmailDestHash() {
return emailDestHash;
}
public Set<Hash> getDhtKeys() {
return entries.keySet();
}
public UniqueId getDeletionKey(Hash dhtKey) {
return entries.get(dhtKey);
}
public void remove(Hash dhtKey) {
entries.remove(dhtKey);
}
public int getNumEntries() {
return entries.size();
}
@Override
public byte[] toByteArray() {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
DataOutputStream dataStream = new DataOutputStream(byteStream);
try {
writeHeader(dataStream);
emailDestHash.writeBytes(dataStream);
dataStream.writeShort(entries.size());
for (Entry<Hash, UniqueId> entry: entries.entrySet()) {
dataStream.write(entry.getKey().toByteArray());
dataStream.write(entry.getValue().toByteArray());
}
} catch (DataFormatException e) {
log.error("Invalid format for email destination.", e);
} catch (IOException e) {
log.error("Can't write to ByteArrayOutputStream.", e);
}
return byteStream.toByteArray();
}
}

View File

@ -0,0 +1,15 @@
package i2p.bote.packet;
/**
* Represents an invalid packet that a peer sent.
*/
public class MalformedCommunicationPacket extends CommunicationPacket {
public MalformedCommunicationPacket() {
}
@Override
public byte[] toByteArray() {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,13 @@
package i2p.bote.packet;
public class MalformedCommunicationPacketException extends Exception {
private static final long serialVersionUID = 7020328715467075260L;
public MalformedCommunicationPacketException(String message) {
super(message);
}
public MalformedCommunicationPacketException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,9 @@
package i2p.bote.packet;
public class MalformedDataPacket extends DataPacket {
@Override
public byte[] toByteArray() {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,9 @@
package i2p.bote.packet;
public class MalformedDataPacketException extends Exception {
private static final long serialVersionUID = -600763395717614292L;
public MalformedDataPacketException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -21,8 +21,6 @@
package i2p.bote.packet;
import i2p.bote.network.kademlia.KademliaPeer;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@ -38,28 +36,26 @@ import net.i2p.util.Log;
public class PeerList extends DataPacket {
private Log log = new Log(PeerList.class);
// TODO should be a Collection<Destination> because this class will also be used for relay peer lists
private Collection<KademliaPeer> peers;
private Collection<Destination> peers;
public PeerList(Collection<KademliaPeer> peers) {
public PeerList(Collection<Destination> peers) {
this.peers = peers;
}
public PeerList(byte[] data) throws DataFormatException {
ByteBuffer buffer = ByteBuffer.wrap(data);
if (buffer.get() != getPacketTypeCode())
log.error("Wrong type code for PeerList. Expected <" + getPacketTypeCode() + ">, got <" + (char)data[0] + ">");
super(data);
ByteBuffer buffer = ByteBuffer.wrap(data, HEADER_LENGTH, data.length-HEADER_LENGTH);
int numPeers = buffer.getShort();
peers = new ArrayList<KademliaPeer>();
peers = new ArrayList<Destination>();
for (int i=0; i<numPeers; i++) {
Destination destination = new Destination();
byte[] peerData = new byte[388];
// read 384 bytes, leave the last 3 bytes zero
buffer.get(peerData, 0, 384);
destination.readBytes(peerData, 0);
KademliaPeer peer = new KademliaPeer(destination, 0);
Destination peer = new Destination();
peer.readBytes(peerData, 0);
peers.add(peer);
}
@ -67,7 +63,7 @@ public class PeerList extends DataPacket {
log.debug("Peer List has " + buffer.remaining() + " extra bytes.");
}
public Collection<KademliaPeer> getPeers() {
public Collection<Destination> getPeers() {
return peers;
}
@ -77,10 +73,11 @@ public class PeerList extends DataPacket {
DataOutputStream dataStream = new DataOutputStream(arrayOutputStream);
try {
dataStream.write((byte)getPacketTypeCode());
writeHeader(dataStream);
dataStream.writeShort(peers.size());
for (KademliaPeer peer: peers)
dataStream.write(peer.getDestination().toByteArray());
for (Destination peer: peers)
// write the first 384 bytes (the two public keys)
dataStream.write(peer.toByteArray(), 0, 384);
}
catch (IOException e) {
log.error("Can't write to ByteArrayOutputStream.", e);

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