diff --git a/apps/i2psnark/COPYING b/apps/i2psnark/COPYING new file mode 100644 index 0000000000000000000000000000000000000000..d60c31a97a544b53039088d14fe9114583c0efc3 --- /dev/null +++ b/apps/i2psnark/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/apps/i2psnark/TODO b/apps/i2psnark/TODO new file mode 100644 index 0000000000000000000000000000000000000000..6f89c0f50749b1a8d3d6ab80fd419d2613448c69 --- /dev/null +++ b/apps/i2psnark/TODO @@ -0,0 +1,24 @@ +- I2PSnark: + - add multitorrent support by checking the metainfo hash in the + PeerAcceptor and feeding it off to the appropriate coordinator + - add a web interface + +- BEncode + - Byte array length indicator can overflow. + - Support really big BigNums (only 256 chars allowed now) + - Better BEValue toString(). Uses stupid heuristic now for debugging. + - Implemented bencoding. + - Remove application level hack to calculate sha1 hash for metainfo + (But can it be done as efficiently?) + +- Storage + - Check file name filter. + +- TrackerClient + - Support undocumented &numwant= request. + +- PeerCoordinator + - Disconnect from other seeds as soon as you are a seed yourself. + +- Text UI + - Make it completely silent. diff --git a/apps/i2psnark/authors.snark b/apps/i2psnark/authors.snark new file mode 100644 index 0000000000000000000000000000000000000000..00aecc25bad57c000240bb7d795c56801b722041 --- /dev/null +++ b/apps/i2psnark/authors.snark @@ -0,0 +1 @@ +Mark Wielaard <mark@klomp.org> diff --git a/apps/i2psnark/changelog.snark b/apps/i2psnark/changelog.snark new file mode 100644 index 0000000000000000000000000000000000000000..75304f17c59890cbc2891c6a33923fd14ba9d11a --- /dev/null +++ b/apps/i2psnark/changelog.snark @@ -0,0 +1,487 @@ +2003-06-27 14:24 Mark Wielaard <mark@klomp.org> + + * README: Update version number and explain new features. + +2003-06-27 13:51 Mark Wielaard <mark@klomp.org> + + * Makefile, org/klomp/snark/GnomeInfoWindow.java, + org/klomp/snark/GnomePeerList.java, + org/klomp/snark/PeerCoordinator.java, + org/klomp/snark/SnarkGnome.java: Add GnomeInfoWindow. + +2003-06-27 00:37 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Snark.java: Implement 'info' and 'list' commands. + +2003-06-27 00:05 Mark Wielaard <mark@klomp.org> + + * Makefile, org/klomp/snark/GnomePeerList.java, + org/klomp/snark/SnarkGnome.java: Add GnomePeerList to show state of + connected peers. + +2003-06-27 00:04 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: Peer.java, PeerID.java: Make Comparable. + +2003-06-23 23:32 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerMonitorTask.java: Correctly update + lastDownloaded and lastUploaded. + +2003-06-23 23:20 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Snark.java: When checking storage use the + MetaInfo from the storage. + +2003-06-23 21:47 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Storage.java: Fill piece hashes, not info hashes. + +2003-06-23 21:42 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/MetaInfo.java: New package private + getPieceHashes() method. + +2003-06-22 19:49 Mark Wielaard <mark@klomp.org> + + * README, TODO, org/klomp/snark/Snark.java: Add new command line + switch --no-commands. Don't read interactive commands or show + usage info. + +2003-06-22 19:26 Mark Wielaard <mark@klomp.org> + + * Makefile, org/klomp/snark/PeerCheckerTask.java, + org/klomp/snark/PeerMonitorTask.java, org/klomp/snark/Snark.java: + Split peer statistic reporting from PeerCheckerTask into + PeerMonitorTask. Use new task in Snark text ui. + +2003-06-22 18:32 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Snark.java: Only print peer id when debug level + is INFO or higher. + +2003-06-22 18:00 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/ShutdownListener.java: Add new ShutdownListener + interface. + +2003-06-22 17:18 Mark Wielaard <mark@klomp.org> + + * TODO: Text UI item to not read from stdin. + +2003-06-22 17:18 Mark Wielaard <mark@klomp.org> + + * snark-gnome.sh: kaffe java-gnome support (but crashes hard at the + moment). + +2003-06-22 14:04 Mark Wielaard <mark@klomp.org> + + * Makefile, org/klomp/snark/CoordinatorListener.java, + org/klomp/snark/PeerCoordinator.java, + org/klomp/snark/ProgressListener.java, org/klomp/snark/Snark.java, + org/klomp/snark/SnarkGnome.java, + org/klomp/snark/SnarkShutdown.java, org/klomp/snark/Storage.java, + org/klomp/snark/StorageListener.java: Split ProgressListener into + Storage, Coordinator and Shutdown listener. + +2003-06-20 19:06 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: PeerCoordinator.java, Snark.java, + SnarkGnome.java, Storage.java: Progress listeners for both Storage + and PeerCoordinator. + +2003-06-20 14:50 Mark Wielaard <mark@klomp.org> + + * Makefile, org/klomp/snark/PeerCoordinator.java, + org/klomp/snark/ProgressListener.java, + org/klomp/snark/SnarkGnome.java: Add ProgressListener. + +2003-06-20 13:22 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/SnarkGnome.java: Add Pieces collected field. + +2003-06-20 12:26 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: PeerCoordinator.java, PeerListener.java, + PeerState.java: Add PeerListener.downloaded() which gets called on + chunk updates. Keep PeerCoordinator.downloaded up to date using + this remove adjusting in gotPiece() except when we receive a bad + piece. + +2003-06-16 00:27 Mark Wielaard <mark@klomp.org> + + * Makefile, snark-gnome.sh, org/klomp/snark/Snark.java, + org/klomp/snark/SnarkGnome.java: Start of a Gnome GUI. + +2003-06-05 13:19 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerCoordinator.java: Don't remove a BAD piece + from the wantedPieces list. Revert to synchronizing on + wantedPieces for all relevant sections. + +2003-06-03 21:09 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Snark.java: Only call readLine() when !quit. + Always print exception when fatal() is called. + +2003-06-01 23:12 Mark Wielaard <mark@klomp.org> + + * README: Set release version to 0.4. + +2003-06-01 22:59 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerConnectionIn.java: Handle negative length + prefixes (terminates connection). + +2003-06-01 21:34 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: Snark.java, SnarkShutdown.java: Implement + correct shutdown and read commands from stdin. + +2003-06-01 21:34 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/TrackerInfo.java: Check that interval and peers + list actually exist. + +2003-06-01 21:33 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Storage.java: Implement close(). + +2003-06-01 21:05 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Fix debug logging. + +2003-06-01 20:55 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerCoordinator.java: Implement halt(). + +2003-06-01 20:55 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/ConnectionAcceptor.java: Rename stop() to halt(). + +2003-06-01 17:35 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Drop lock on this when calling + addRequest() from havePiece(). + +2003-06-01 14:46 Mark Wielaard <mark@klomp.org> + + * README, org/klomp/snark/ConnectionAcceptor.java, + org/klomp/snark/HttpAcceptor.java, org/klomp/snark/Peer.java, + org/klomp/snark/PeerCheckerTask.java, + org/klomp/snark/PeerConnectionIn.java, + org/klomp/snark/PeerConnectionOut.java, + org/klomp/snark/PeerCoordinator.java, + org/klomp/snark/PeerState.java, org/klomp/snark/Snark.java, + org/klomp/snark/SnarkShutdown.java, org/klomp/snark/Storage.java, + org/klomp/snark/Tracker.java, org/klomp/snark/TrackerClient.java: + Add debug/log level. + +2003-05-31 23:04 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: PeerCheckerTask.java, PeerCoordinator.java: Use + just one lock (peers) for all synchronization (even for + wantedPieces). Let PeerChecker handle real disconnect and keep + count of uploaders. + +2003-05-31 22:29 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: Peer.java, PeerConnectionIn.java: Set state to + null on first disconnect() call. So always check whether it might + already be null. Helps disconnect check. + +2003-05-31 22:27 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerConnectionOut.java: Don't explicitly close + the DataOutputStream (if another thread is using it libgcj seems to + not like it very much). + +2003-05-30 21:33 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerConnectionOut.java: Cancel + (un)interested/(un)choke when (inverse) is still in send queue. + Remove pieces from send queue when choke message is actaully send. + +2003-05-30 19:32 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Make sure listener.wantPiece(int) + is never called while lock on this is held. + +2003-05-30 19:00 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerConnectionOut.java: Indentation cleanup. + +2003-05-30 17:50 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Storage.java: Only synchronize on bitfield as + long as necessary. + +2003-05-30 17:43 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Tracker.java: Identing cleanup. + +2003-05-30 16:32 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Better error message. + +2003-05-30 15:11 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Make sure not to hold the lock on + this when calling the listener to prevent deadlocks. Implement + handling and sending of cancel messages. + +2003-05-30 14:50 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerCoordinator.java: First check if we still + want a piece before trying to add it to the Storage. + +2003-05-30 14:49 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerConnectionOut.java: Implement + sendCancel(Request). Add cancelRequest(int, int, int). + +2003-05-30 14:46 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Request.java: Add hashCode() and equals(Object) + methods. + +2003-05-30 14:45 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Peer.java: Fix wheter -> whether javadoc + comments. Mark state null immediatly after calling + listener.disconnected(). Call PeerState.havePiece() not + PeerConnectionOut.sendHave() directly. + +2003-05-25 19:23 Mark Wielaard <mark@klomp.org> + + * TODO: Add PeerCoordinator TODO for connecting to seeds. + +2003-05-23 12:12 Mark Wielaard <mark@klomp.org> + + * Makefile: Create class files with jikes again. + +2003-05-18 22:01 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: PeerCheckerTask.java, PeerCoordinator.java: + Prefer to (optimistically) unchoke first those peers that unchoked + us. And make sure to not unchoke a peer that we just choked. + +2003-05-18 21:48 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Peer.java: Fix isChoked() to not always return + true. + +2003-05-18 14:46 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: Peer.java, PeerCheckerTask.java, + PeerCoordinator.java, PeerState.java: Remove separate Peer + downloading/uploading states. Keep choke and interest always up to + date. Uploading is now just when we are not choking the peer. + Downloading is now defined as being unchoked and interesting. + CHECK_PERIOD is now 20 seconds. MAX_CONNECTIONS is now 24. + MAX_DOWNLOADERS doesn't exists anymore. We download whenever we can + from peers. + +2003-05-18 13:57 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerConnectionOut.java: Remove piece messages + from queue when we are choking. (They will have to be rerequested + when we unchoke the peer again.) + +2003-05-15 00:08 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Ignore missed chunk requests, + don't requeue them. + +2003-05-15 00:06 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Request.java: Add sanity check + +2003-05-10 15:47 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Snark.java: Add extra '(' to usage message. + +2003-05-10 15:22 Mark Wielaard <mark@klomp.org> + + * README: Set version to 0.3 (The Bakers Tale). + +2003-05-10 15:17 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Mention received piece in warning + message. + +2003-05-10 03:20 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: PeerConnectionIn.java, PeerState.java, + Request.java: Remove currentRequest and handle all piece messages + from the lastRequested list. + +2003-05-09 20:02 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Fix nothing requested warning + message. + +2003-05-09 19:59 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerConnectionOut.java: Piece messages are big. + So if there are other (control) messages make sure they are send + first. Also remove request messages from the queue if we are + currently being choked to prevent them from being send even if we + get unchoked a little later. (Since we will resent them anyway in + that case.) + +2003-05-09 18:33 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: Peer.java, PeerCheckerTask.java, + PeerCoordinator.java, PeerID.java: New definition of PeerID.equals + (port + address + id) and new method PeerID.sameID (only id). These + are used to really see if we already have a connection to a certain + peer (active setup vs passive setup). + +2003-05-08 03:05 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Use Snark.debug() not + System.out.println(). + +2003-05-06 20:29 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: s/noting/nothing/ + +2003-05-06 20:28 Mark Wielaard <mark@klomp.org> + + * Makefile: s/lagacy/legacy/ + +2003-05-05 23:17 Mark Wielaard <mark@klomp.org> + + * README: Set version to 0.2, explain new functionality and add + examples. + +2003-05-05 22:42 Mark Wielaard <mark@klomp.org> + + * .cvsignore, Makefile, org/klomp/snark/StaticSnark.java: Enable + -static binary creation. + +2003-05-05 22:42 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Tracker.java: Disable --ip support. + +2003-05-05 21:02 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: HttpAcceptor.java, PeerCheckerTask.java, + PeerCoordinator.java, TrackerClient.java: Use Snark.debug() not + System.out.println(). + +2003-05-05 21:01 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerConnectionIn.java: Be prepared to handle the + case where currentRequest is null. + +2003-05-05 21:00 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Snark.java: Improve argument parsing errors. + +2003-05-05 21:00 Mark Wielaard <mark@klomp.org> + + * Makefile: Use gcj -C again for creating the class files. + +2003-05-05 09:24 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Just clear outstandingRequests, + never make it null. + +2003-05-05 02:55 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/TrackerClient.java: Always retry both first + started event and every other event as long the TrackerClient is + not stopped. + +2003-05-05 02:54 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Snark.java: Remove double assignment port. + +2003-05-05 02:54 Mark Wielaard <mark@klomp.org> + + * TODO: Add Tracker TODO item. + +2003-05-04 23:38 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: ConnectionAcceptor.java, MetaInfo.java, + Snark.java, Storage.java, Tracker.java: Add info hash calcultation + to MetaInfo. Add torrent creation to Storage. Add ip parameter + handling to Tracker. Make ConnectionAcceptor handle + null/non-existing HttpAcceptors. Add debug output, --ip handling + and all the above to Snark. + +2003-05-04 23:36 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/TrackerClient.java: Handle all failing requests + the same (print a warning). + +2003-05-03 15:46 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: Peer.java, PeerID.java, TrackerInfo.java: Split + Peer and PeerID a little more. + +2003-05-03 15:44 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/MetaInfo.java: Add reannounce() and + getTorrentData(). + +2003-05-03 15:38 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/: PeerCheckerTask.java, PeerCoordinator.java: + More concise verbose/debug output. Always use addUpDownloader() to + set peers upload or download state to true. + +2003-05-03 13:38 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/TrackerClient.java: Compile fixes. + +2003-05-03 13:32 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/TrackerClient.java: Only generate fatal() call on + first Tracker access. Otherwise just print a warning error message. + +2003-05-03 03:10 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerState.java: Better handle resending + outstanding pieces and try to recover better from unrequested + pieces. + +2003-05-02 21:33 Mark Wielaard <mark@klomp.org> + + * Makefile, org/klomp/snark/HttpAcceptor.java, + org/klomp/snark/MetaInfo.java, org/klomp/snark/PeerID.java, + org/klomp/snark/Snark.java, org/klomp/snark/Tracker.java, + org/klomp/snark/TrackerClient.java, + org/klomp/snark/bencode/BEncoder.java: Add Tracker, PeerID and + BEncoder. + +2003-05-01 20:17 Mark Wielaard <mark@klomp.org> + + * Makefile, org/klomp/snark/ConnectionAcceptor.java, + org/klomp/snark/HttpAcceptor.java, org/klomp/snark/Peer.java, + org/klomp/snark/PeerAcceptor.java, org/klomp/snark/Snark.java: Add + ConnectionAcceptor that handles both PeerAcceptor and HttpAcceptor. + +2003-05-01 18:39 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/PeerCoordinator.java: connected() synchronize on + peers. + +2003-04-28 02:56 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/SnarkShutdown.java: Wait some time before + returning... + +2003-04-28 02:56 Mark Wielaard <mark@klomp.org> + + * TODO: More items. + +2003-04-28 02:56 Mark Wielaard <mark@klomp.org> + + * org/klomp/snark/Snark.java: Calculate real random ID. + +2003-04-27 Mark Wielaard <mark@klomp.org> + + * snark: Initial (0.1) version. diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml new file mode 100644 index 0000000000000000000000000000000000000000..94e97bbc05cbcc1c47f182575d78083c7f1b33ff --- /dev/null +++ b/apps/i2psnark/java/build.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project basedir="." default="all" name="i2psnark"> + <target name="all" depends="clean, build" /> + <target name="build" depends="builddep, jar" /> + <target name="builddep"> + <ant dir="../../ministreaming/java/" target="build" /> + <!-- ministreaming will build core --> + </target> + <target name="compile"> + <mkdir dir="./build" /> + <mkdir dir="./build/obj" /> + <javac + srcdir="./src" + debug="true" deprecation="on" source="1.3" target="1.3" + destdir="./build/obj" + classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" /> + </target> + <target name="jar" depends="builddep, compile"> + <jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class"> + <manifest> + <attribute name="Main-Class" value="org.klomp.snark.Snark" /> + <attribute name="Class-Path" value="i2p.jar jbigi.jar mstreaming.jar streaming.jar" /> + </manifest> + </jar> + </target> + <target name="clean"> + <delete dir="./build" /> + </target> + <target name="cleandep" depends="clean"> + <ant dir="../../ministreaming/java/" target="distclean" /> + </target> + <target name="distclean" depends="clean"> + <ant dir="../../ministreaming/java/" target="distclean" /> + </target> +</project> diff --git a/apps/i2psnark/java/src/org/klomp/snark/BitField.java b/apps/i2psnark/java/src/org/klomp/snark/BitField.java new file mode 100644 index 0000000000000000000000000000000000000000..2e3f8d1a76669845cd700148841e065c1edf3432 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/BitField.java @@ -0,0 +1,131 @@ +/* BitField - Container of a byte array representing set and unset bits. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.util.Iterator; +import java.util.Set; +import java.util.HashSet; + +/** + * Container of a byte array representing set and unset bits. + */ +public class BitField +{ + + private final byte[] bitfield; + private final int size; + + /** + * Creates a new BitField that represents <code>size</code> unset bits. + */ + public BitField(int size) + { + this.size = size; + int arraysize = ((size-1)/8)+1; + bitfield = new byte[arraysize]; + } + + /** + * Creates a new BitField that represents <code>size</code> bits + * as set by the given byte array. This will make a copy of the array. + * Extra bytes will be ignored. + * + * @exception ArrayOutOfBoundsException if give byte array is not large + * enough. + */ + public BitField(byte[] bitfield, int size) + { + this.size = size; + int arraysize = ((size-1)/8)+1; + this.bitfield = new byte[arraysize]; + + // XXX - More correct would be to check that unused bits are + // cleared or clear them explicitly ourselves. + System.arraycopy(bitfield, 0, this.bitfield, 0, arraysize); + } + + /** + * This returns the actual byte array used. Changes to this array + * effect this BitField. Note that some bits at the end of the byte + * array are supposed to be always unset if they represent bits + * bigger then the size of the bitfield. + */ + public byte[] getFieldBytes() + { + return bitfield; + } + + /** + * Return the size of the BitField. The returned value is one bigger + * then the last valid bit number (since bit numbers are counted + * from zero). + */ + public int size() + { + return size; + } + + /** + * Sets the given bit to true. + * + * @exception IndexOutOfBoundsException if bit is smaller then zero + * bigger then size (inclusive). + */ + public void set(int bit) + { + if (bit < 0 || bit >= size) + throw new IndexOutOfBoundsException(Integer.toString(bit)); + int index = bit/8; + int mask = 128 >> (bit % 8); + bitfield[index] |= mask; + } + + /** + * Return true if the bit is set or false if it is not. + * + * @exception IndexOutOfBoundsException if bit is smaller then zero + * bigger then size (inclusive). + */ + public boolean get(int bit) + { + if (bit < 0 || bit >= size) + throw new IndexOutOfBoundsException(Integer.toString(bit)); + + int index = bit/8; + int mask = 128 >> (bit % 8); + return (bitfield[index] & mask) != 0; + } + + public String toString() + { + // Not very efficient + StringBuffer sb = new StringBuffer("BitField["); + for (int i = 0; i < size; i++) + if (get(i)) + { + sb.append(' '); + sb.append(i); + } + sb.append(" ]"); + + return sb.toString(); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..c5b21d5c34e829970e21208dd574fd797ec151e9 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java @@ -0,0 +1,143 @@ +/* ConnectionAcceptor - Accepts connections and routes them to sub-acceptors. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.*; +import java.net.*; + +import net.i2p.I2PException; +import net.i2p.client.streaming.I2PServerSocket; +import net.i2p.client.streaming.I2PSocket; + +/** + * Accepts connections on a TCP port and routes them to sub-acceptors. + */ +public class ConnectionAcceptor implements Runnable +{ + private final I2PServerSocket serverSocket; + private final PeerAcceptor peeracceptor; + private Thread thread; + + private boolean stop; + + public ConnectionAcceptor(I2PServerSocket serverSocket, + PeerAcceptor peeracceptor) + { + this.serverSocket = serverSocket; + this.peeracceptor = peeracceptor; + + stop = false; + thread = new Thread(this); + thread.start(); + } + + public void halt() + { + stop = true; + + I2PServerSocket ss = serverSocket; + if (ss != null) + try + { + ss.close(); + } + catch(I2PException ioe) { } + + Thread t = thread; + if (t != null) + t.interrupt(); + } + + public int getPort() + { + return 6881; // serverSocket.getLocalPort(); + } + + public void run() + { + while(!stop) + { + try + { + final I2PSocket socket = serverSocket.accept(); + Thread t = new Thread("Connection-" + socket) + { + public void run() + { + try + { + InputStream in = socket.getInputStream(); + OutputStream out = socket.getOutputStream(); + BufferedInputStream bis = new BufferedInputStream(in); + BufferedOutputStream bos = new BufferedOutputStream(out); + + // See what kind of connection it is. + /* + if (httpacceptor != null) + { + byte[] scratch = new byte[4]; + bis.mark(4); + int len = bis.read(scratch); + if (len != 4) + throw new IOException("Need at least 4 bytes"); + bis.reset(); + if (scratch[0] == 19 && scratch[1] == 'B' + && scratch[2] == 'i' && scratch[3] == 't') + peeracceptor.connection(socket, bis, bos); + else if (scratch[0] == 'G' && scratch[1] == 'E' + && scratch[2] == 'T' && scratch[3] == ' ') + httpacceptor.connection(socket, bis, bos); + } + else + */ + peeracceptor.connection(socket, bis, bos); + } + catch (IOException ioe) + { + try + { + socket.close(); + } + catch (IOException ignored) { } + } + } + }; + t.start(); + } + catch (I2PException ioe) + { + Snark.debug("Error while accepting: " + ioe, Snark.ERROR); + stop = true; + } + catch (IOException ioe) + { + Snark.debug("Error while accepting: " + ioe, Snark.ERROR); + stop = true; + } + } + + try + { + serverSocket.close(); + } + catch (I2PException ignored) { } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java new file mode 100644 index 0000000000000000000000000000000000000000..fc2b9b73efb39fecbf4a87e40f96d528efee3b17 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java @@ -0,0 +1,33 @@ +/* CoordinatorListener.java - Callback when a peer changes state + + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +/** + * Callback used when some peer changes state. + */ +public interface CoordinatorListener +{ + /** + * Called when the PeerCoordinator notices a change in the state of a peer. + */ + void peerChange(PeerCoordinator coordinator, Peer peer); +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..deda083cf5825f5e750cc3c283b1a5ea56ae1cc2 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -0,0 +1,165 @@ +package org.klomp.snark; + +import net.i2p.I2PAppContext; +import net.i2p.I2PException; +import net.i2p.util.EepGet; +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.client.streaming.I2PServerSocket; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.client.streaming.I2PSocketManagerFactory; +import net.i2p.util.Log; + +import java.io.*; +import java.util.Properties; + +/** + * I2P specific helpers for I2PSnark + */ +public class I2PSnarkUtil { + private I2PAppContext _context; + private Log _log; + private static I2PSnarkUtil _instance = new I2PSnarkUtil(); + public static I2PSnarkUtil instance() { return _instance; } + + private boolean _shouldProxy; + private String _proxyHost; + private int _proxyPort; + private String _i2cpHost; + private int _i2cpPort; + private Properties _opts; + private I2PSocketManager _manager; + + private I2PSnarkUtil() { + _context = I2PAppContext.getGlobalContext(); + _log = _context.logManager().getLog(Snark.class); + setProxy("127.0.0.1", 4444); + setI2CPConfig("127.0.0.1", 7654, null); + } + + /** + * Specify what HTTP proxy tracker requests should go through (specify a null + * host for no proxying) + * + */ + public void setProxy(String host, int port) { + if ( (host != null) && (port > 0) ) { + _shouldProxy = true; + _proxyHost = host; + _proxyPort = port; + } else { + _shouldProxy = false; + _proxyHost = null; + _proxyPort = -1; + } + } + + public void setI2CPConfig(String i2cpHost, int i2cpPort, Properties opts) { + _i2cpHost = i2cpHost; + _i2cpPort = i2cpPort; + if (opts != null) + _opts = opts; + } + + /** + * Connect to the router, if we aren't already + */ + boolean connect() { + if (_manager == null) { + _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, _opts); + } + return (_manager != null); + } + + /** connect to the given destination */ + I2PSocket connect(PeerID peer) throws IOException { + try { + return _manager.connect(peer.getAddress()); + } catch (I2PException ie) { + throw new IOException("Unable to reach the peer " + peer + ": " + ie.getMessage()); + } + } + + /** + * fetch the given URL, returning the file it is stored in, or null on error + */ + File get(String url) { + File out = null; + try { + out = File.createTempFile("i2psnark", "url"); + } catch (IOException ioe) { + ioe.printStackTrace(); + return null; + } + EepGet get = new EepGet(_context, _shouldProxy, _proxyHost, _proxyPort, 1, out.getAbsolutePath(), url); + if (get.fetch()) { + return out; + } else { + out.delete(); + return null; + } + } + + I2PServerSocket getServerSocket() { + return _manager.getServerSocket(); + } + + String getOurIPString() { + return _manager.getSession().getMyDestination().toBase64(); + } + Destination getDestination(String ip) { + if (ip == null) return null; + if (ip.endsWith(".i2p")) { + Destination dest = _context.namingService().lookup(ip); + if (dest != null) { + return dest; + } else { + try { + return new Destination(ip.substring(0, ip.length()-4)); // sans .i2p + } catch (DataFormatException dfe) { + return null; + } + } + } else { + try { + return new Destination(ip); + } catch (DataFormatException dfe) { + return null; + } + } + } + + /** + * Given http://blah.i2p/foo/announce turn it into http://i2p/blah/foo/announce + */ + String rewriteAnnounce(String origAnnounce) { + int destStart = "http://".length(); + int destEnd = origAnnounce.indexOf(".i2p"); + int pathStart = origAnnounce.indexOf('/', destEnd); + return "http://i2p/" + origAnnounce.substring(destStart, destEnd) + origAnnounce.substring(pathStart); + } + + /** hook between snark's logger and an i2p log */ + void debug(String msg, int snarkDebugLevel, Throwable t) { + switch (snarkDebugLevel) { + case 0: + case 1: + _log.error(msg, t); + break; + case 2: + _log.warn(msg, t); + break; + case 3: + case 4: + _log.info(msg, t); + break; + case 5: + case 6: + default: + _log.debug(msg, t); + break; + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/Message.java b/apps/i2psnark/java/src/org/klomp/snark/Message.java new file mode 100644 index 0000000000000000000000000000000000000000..1e95fda18e07cf58179a75e604eb582c03ad9b43 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/Message.java @@ -0,0 +1,137 @@ +/* Message - A protocol message which can be send through a DataOutputStream. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.DataOutputStream; +import java.io.IOException; + +// Used to queue outgoing connections +// sendMessage() should be used to translate them to wire format. +class Message +{ + final static byte KEEP_ALIVE = -1; + final static byte CHOKE = 0; + final static byte UNCHOKE = 1; + final static byte INTERESTED = 2; + final static byte UNINTERESTED = 3; + final static byte HAVE = 4; + final static byte BITFIELD = 5; + final static byte REQUEST = 6; + final static byte PIECE = 7; + final static byte CANCEL = 8; + + // Not all fields are used for every message. + // KEEP_ALIVE doesn't have a real wire representation + byte type; + + // Used for HAVE, REQUEST, PIECE and CANCEL messages. + int piece; + + // Used for REQUEST, PIECE and CANCEL messages. + int begin; + int length; + + // Used for PIECE and BITFIELD messages + byte[] data; + int off; + int len; + + /** Utility method for sending a message through a DataStream. */ + void sendMessage(DataOutputStream dos) throws IOException + { + // KEEP_ALIVE is special. + if (type == KEEP_ALIVE) + { + dos.writeInt(0); + return; + } + + // Calculate the total length in bytes + + // Type is one byte. + int datalen = 1; + + // piece is 4 bytes. + if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL) + datalen += 4; + + // begin/offset is 4 bytes + if (type == REQUEST || type == PIECE || type == CANCEL) + datalen += 4; + + // length is 4 bytes + if (type == REQUEST || type == CANCEL) + datalen += 4; + + // add length of data for piece or bitfield array. + if (type == BITFIELD || type == PIECE) + datalen += len; + + // Send length + dos.writeInt(datalen); + dos.writeByte(type & 0xFF); + + // Send additional info (piece number) + if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL) + dos.writeInt(piece); + + // Send additional info (begin/offset) + if (type == REQUEST || type == PIECE || type == CANCEL) + dos.writeInt(begin); + + // Send additional info (length); for PIECE this is implicit. + if (type == REQUEST || type == CANCEL) + dos.writeInt(length); + + // Send actual data + if (type == BITFIELD || type == PIECE) + dos.write(data, off, len); + } + + public String toString() + { + switch (type) + { + case KEEP_ALIVE: + return "KEEP_ALIVE"; + case CHOKE: + return "CHOKE"; + case UNCHOKE: + return "UNCHOKE"; + case INTERESTED: + return "INTERESTED"; + case UNINTERESTED: + return "UNINTERESTED"; + case HAVE: + return "HAVE(" + piece + ")"; + case BITFIELD: + return "BITFIELD"; + case REQUEST: + return "REQUEST(" + piece + "," + begin + "," + length + ")"; + case PIECE: + return "PIECE(" + piece + "," + begin + "," + length + ")"; + case CANCEL: + return "CANCEL(" + piece + "," + begin + "," + length + ")"; + default: + return "<UNKNOWN>"; + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..e63b7803d4f408bf36b747ed694063f03d49ade8 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java @@ -0,0 +1,382 @@ +/* MetaInfo - Holds all information gotten from a torrent file. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.IOException; +import java.io.InputStream; +import java.io.File; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; + +import org.klomp.snark.bencode.*; + +public class MetaInfo +{ + private final String announce; + private final byte[] info_hash; + private final String name; + private final List files; + private final List lengths; + private final int piece_length; + private final byte[] piece_hashes; + private final long length; + + private byte[] torrentdata; + + MetaInfo(String announce, String name, List files, List lengths, + int piece_length, byte[] piece_hashes, long length) + { + this.announce = announce; + this.name = name; + this.files = files; + this.lengths = lengths; + this.piece_length = piece_length; + this.piece_hashes = piece_hashes; + this.length = length; + + this.info_hash = calculateInfoHash(); + } + + /** + * Creates a new MetaInfo from the given InputStream. The + * InputStream must start with a correctly bencoded dictonary + * describing the torrent. + */ + public MetaInfo(InputStream in) throws IOException + { + this(new BDecoder(in)); + } + + /** + * Creates a new MetaInfo from the given BDecoder. The BDecoder + * must have a complete dictionary describing the torrent. + */ + public MetaInfo(BDecoder be) throws IOException + { + // Note that evaluation order matters here... + this(be.bdecodeMap().getMap()); + } + + /** + * Creates a new MetaInfo from a Map of BEValues and the SHA1 over + * the original bencoded info dictonary (this is a hack, we could + * reconstruct the bencoded stream and recalculate the hash). Will + * throw a InvalidBEncodingException if the given map does not + * contain a valid announce string or info dictonary. + */ + public MetaInfo(Map m) throws InvalidBEncodingException + { + BEValue val = (BEValue)m.get("announce"); + if (val == null) + throw new InvalidBEncodingException("Missing announce string"); + this.announce = val.getString(); + + val = (BEValue)m.get("info"); + if (val == null) + throw new InvalidBEncodingException("Missing info map"); + Map info = val.getMap(); + + val = (BEValue)info.get("name"); + if (val == null) + throw new InvalidBEncodingException("Missing name string"); + name = val.getString(); + + val = (BEValue)info.get("piece length"); + if (val == null) + throw new InvalidBEncodingException("Missing piece length number"); + piece_length = val.getInt(); + + val = (BEValue)info.get("pieces"); + if (val == null) + throw new InvalidBEncodingException("Missing piece bytes"); + piece_hashes = val.getBytes(); + + val = (BEValue)info.get("length"); + if (val != null) + { + // Single file case. + length = val.getLong(); + files = null; + lengths = null; + } + else + { + // Multi file case. + val = (BEValue)info.get("files"); + if (val == null) + throw new InvalidBEncodingException + ("Missing length number and/or files list"); + + List list = val.getList(); + int size = list.size(); + if (size == 0) + throw new InvalidBEncodingException("zero size files list"); + + files = new ArrayList(size); + lengths = new ArrayList(size); + long l = 0; + for (int i = 0; i < list.size(); i++) + { + Map desc = ((BEValue)list.get(i)).getMap(); + val = (BEValue)desc.get("length"); + if (val == null) + throw new InvalidBEncodingException("Missing length number"); + long len = val.getLong(); + lengths.add(new Long(len)); + l += len; + + val = (BEValue)desc.get("path"); + if (val == null) + throw new InvalidBEncodingException("Missing path list"); + List path_list = val.getList(); + int path_length = path_list.size(); + if (path_length == 0) + throw new InvalidBEncodingException("zero size file path list"); + + List file = new ArrayList(path_length); + Iterator it = path_list.iterator(); + while (it.hasNext()) + file.add(((BEValue)it.next()).getString()); + + files.add(file); + } + length = l; + } + + info_hash = calculateInfoHash(); + } + + /** + * Returns the string representing the URL of the tracker for this torrent. + */ + public String getAnnounce() + { + return announce; + } + + /** + * Returns the original 20 byte SHA1 hash over the bencoded info map. + */ + public byte[] getInfoHash() + { + // XXX - Should we return a clone, just to be sure? + return info_hash; + } + + /** + * Returns the piece hashes. Only used by storage so package local. + */ + byte[] getPieceHashes() + { + return piece_hashes; + } + + /** + * Returns the requested name for the file or toplevel directory. + * If it is a toplevel directory name getFiles() will return a + * non-null List of file name hierarchy name. + */ + public String getName() + { + return name; + } + + /** + * Returns a list of lists of file name hierarchies or null if it is + * a single name. It has the same size as the list returned by + * getLengths(). + */ + public List getFiles() + { + // XXX - Immutable? + return files; + } + + /** + * Returns a list of Longs indication the size of the individual + * files, or null if it is a single file. It has the same size as + * the list returned by getFiles(). + */ + public List getLengths() + { + // XXX - Immutable? + return lengths; + } + + /** + * Returns the number of pieces. + */ + public int getPieces() + { + return piece_hashes.length/20; + } + + /** + * Return the length of a piece. All pieces are of equal length + * except for the last one (<code>getPieces()-1</code>). + * + * @exception IndexOutOfBoundsException when piece is equal to or + * greater then the number of pieces in the torrent. + */ + public int getPieceLength(int piece) + { + int pieces = getPieces(); + if (piece >= 0 && piece < pieces -1) + return piece_length; + else if (piece == pieces -1) + return (int)(length - piece * piece_length); + else + throw new IndexOutOfBoundsException("no piece: " + piece); + } + + /** + * Checks that the given piece has the same SHA1 hash as the given + * byte array. Returns random results or IndexOutOfBoundsExceptions + * when the piece number is unknown. + */ + public boolean checkPiece(int piece, byte[] bs, int off, int length) + { + // Check digest + MessageDigest sha1; + try + { + sha1 = MessageDigest.getInstance("SHA"); + } + catch (NoSuchAlgorithmException nsae) + { + throw new InternalError("No SHA digest available: " + nsae); + } + + sha1.update(bs, off, length); + byte[] hash = sha1.digest(); + for (int i = 0; i < 20; i++) + if (hash[i] != piece_hashes[20 * piece + i]) + return false; + return true; + } + + /** + * Returns the total length of the torrent in bytes. + */ + public long getTotalLength() + { + return length; + } + + public String toString() + { + return "MetaInfo[info_hash='" + hexencode(info_hash) + + "', announce='" + announce + + "', name='" + name + + "', files=" + files + + ", #pieces='" + piece_hashes.length/20 + + "', piece_length='" + piece_length + + "', length='" + length + + "']"; + } + + /** + * Encode a byte array as a hex encoded string. + */ + private static String hexencode(byte[] bs) + { + StringBuffer sb = new StringBuffer(bs.length*2); + for (int i = 0; i < bs.length; i++) + { + int c = bs[i] & 0xFF; + if (c < 16) + sb.append('0'); + sb.append(Integer.toHexString(c)); + } + + return sb.toString(); + } + + /** + * Creates a copy of this MetaInfo that shares everything except the + * announce URL. + */ + public MetaInfo reannounce(String announce) + { + return new MetaInfo(announce, name, files, + lengths, piece_length, + piece_hashes, length); + } + + public byte[] getTorrentData() + { + if (torrentdata == null) + { + Map m = new HashMap(); + m.put("announce", announce); + Map info = createInfoMap(); + m.put("info", info); + torrentdata = BEncoder.bencode(m); + } + return torrentdata; + } + + private Map createInfoMap() + { + Map info = new HashMap(); + info.put("name", name); + info.put("piece length", new Integer(piece_length)); + info.put("pieces", piece_hashes); + if (files == null) + info.put("length", new Long(length)); + else + { + List l = new ArrayList(); + for (int i = 0; i < files.size(); i++) + { + Map file = new HashMap(); + file.put("path", files.get(i)); + file.put("length", lengths.get(i)); + l.add(file); + } + info.put("files", l); + } + return info; + } + + private byte[] calculateInfoHash() + { + Map info = createInfoMap(); + byte[] infoBytes = BEncoder.bencode(info); + try + { + MessageDigest digest = MessageDigest.getInstance("SHA"); + return digest.digest(infoBytes); + } + catch(NoSuchAlgorithmException nsa) + { + throw new InternalError(nsa.toString()); + } + } + + +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java new file mode 100644 index 0000000000000000000000000000000000000000..e743a9312d0344aa3f80bc1361985a46f3127b03 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java @@ -0,0 +1,388 @@ +/* Peer - All public information concerning a peer. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.*; +import java.net.*; +import java.util.Arrays; +import java.util.Map; + +import org.klomp.snark.bencode.*; + +import net.i2p.client.streaming.I2PSocket; + +public class Peer implements Comparable +{ + // Identifying property, the peer id of the other side. + private final PeerID peerID; + + private final byte[] my_id; + private final MetaInfo metainfo; + + // The data in/output streams set during the handshake and used by + // the actual connections. + private DataInputStream din; + private DataOutputStream dout; + + // Keeps state for in/out connections. Non-null when the handshake + // was successful, the connection setup and runs + PeerState state; + + private boolean deregister = true; + + /** + * Creates a disconnected peer given a PeerID, your own id and the + * relevant MetaInfo. + */ + public Peer(PeerID peerID, byte[] my_id, MetaInfo metainfo) + throws IOException + { + this.peerID = peerID; + this.my_id = my_id; + this.metainfo = metainfo; + } + + /** + * Creates a unconnected peer from the input and output stream got + * from the socket. Note that the complete handshake (which can take + * some time or block indefinitely) is done in the calling Thread to + * get the remote peer id. To completely start the connection call + * the connect() method. + * + * @exception IOException when an error occurred during the handshake. + */ + public Peer(final I2PSocket sock, BufferedInputStream bis, + BufferedOutputStream bos, byte[] my_id, MetaInfo metainfo) + throws IOException + { + this.my_id = my_id; + this.metainfo = metainfo; + + byte[] id = handshake(bis, bos); + this.peerID = new PeerID(id, sock.getPeerDestination()); + } + + /** + * Returns the id of the peer. + */ + public PeerID getPeerID() + { + return peerID; + } + + /** + * Returns the String representation of the peerID. + */ + public String toString() + { + return peerID.toString(); + } + + /** + * The hash code of a Peer is the hash code of the peerID. + */ + public int hashCode() + { + return peerID.hashCode(); + } + + /** + * Two Peers are equal when they have the same PeerID. + * All other properties are ignored. + */ + public boolean equals(Object o) + { + if (o instanceof Peer) + { + Peer p = (Peer)o; + return peerID.equals(p.peerID); + } + else + return false; + } + + /** + * Compares the PeerIDs. + */ + public int compareTo(Object o) + { + Peer p = (Peer)o; + return peerID.compareTo(p.peerID); + } + + /** + * Runs the connection to the other peer. This method does not + * return until the connection is terminated. + * + * When the connection is correctly started the connected() method + * of the given PeerListener is called. If the connection ends or + * the connection could not be setup correctly the disconnected() + * method is called. + * + * If the given BitField is non-null it is send to the peer as first + * message. + */ + public void runConnection(PeerListener listener, BitField bitfield) + { + if (state != null) + throw new IllegalStateException("Peer already started"); + + try + { + // Do we need to handshake? + if (din == null) + { + I2PSocket sock = I2PSnarkUtil.instance().connect(peerID); + BufferedInputStream bis + = new BufferedInputStream(sock.getInputStream()); + BufferedOutputStream bos + = new BufferedOutputStream(sock.getOutputStream()); + byte [] id = handshake(bis, bos); + byte [] expected_id = peerID.getID(); + if (!Arrays.equals(expected_id, id)) + throw new IOException("Unexpected peerID '" + + PeerID.idencode(id) + + "' expected '" + + PeerID.idencode(expected_id) + "'"); + } + + PeerConnectionIn in = new PeerConnectionIn(this, din); + PeerConnectionOut out = new PeerConnectionOut(this, dout); + PeerState s = new PeerState(this, listener, metainfo, in, out); + + // Send our bitmap + if (bitfield != null) + s.out.sendBitfield(bitfield); + + // We are up and running! + state = s; + listener.connected(this); + + // Use this thread for running the incomming connection. + // The outgoing connection has created its own Thread. + s.in.run(); + } + catch(IOException eofe) + { + // Ignore, probably just the other side closing the connection. + // Or refusing the connection, timing out, etc. + } + catch(Throwable t) + { + Snark.debug(this + ": " + t, Snark.ERROR); + t.printStackTrace(); + } + finally + { + if (deregister) listener.disconnected(this); + } + } + + /** + * Sets DataIn/OutputStreams, does the handshake and returns the id + * reported by the other side. + */ + private byte[] handshake(BufferedInputStream bis, BufferedOutputStream bos) + throws IOException + { + din = new DataInputStream(bis); + dout = new DataOutputStream(bos); + + // Handshake write - header + dout.write(19); + dout.write("BitTorrent protocol".getBytes("UTF-8")); + // Handshake write - zeros + byte[] zeros = new byte[8]; + dout.write(zeros); + // Handshake write - metainfo hash + byte[] shared_hash = metainfo.getInfoHash(); + dout.write(shared_hash); + // Handshake write - peer id + dout.write(my_id); + dout.flush(); + + // Handshake read - header + byte b = din.readByte(); + if (b != 19) + throw new IOException("Handshake failure, expected 19, got " + + (b & 0xff)); + + byte[] bs = new byte[19]; + din.readFully(bs); + String bittorrentProtocol = new String(bs, "UTF-8"); + if (!"BitTorrent protocol".equals(bittorrentProtocol)) + throw new IOException("Handshake failure, expected " + + "'Bittorrent protocol', got '" + + bittorrentProtocol + "'"); + + // Handshake read - zeros + din.readFully(zeros); + + // Handshake read - metainfo hash + bs = new byte[20]; + din.readFully(bs); + if (!Arrays.equals(shared_hash, bs)) + throw new IOException("Unexpected MetaInfo hash"); + + // Handshake read - peer id + din.readFully(bs); + return bs; + } + + public boolean isConnected() + { + return state != null; + } + + /** + * Disconnects this peer if it was connected. If deregister is + * true, PeerListener.disconnected() will be called when the + * connection is completely terminated. Otherwise the connection is + * silently terminated. + */ + public void disconnect(boolean deregister) + { + // Both in and out connection will call this. + this.deregister = deregister; + disconnect(); + } + + void disconnect() + { + PeerState s = state; + if (s != null) + { + state = null; + + PeerConnectionIn in = s.in; + if (in != null) + in.disconnect(); + PeerConnectionOut out = s.out; + if (out != null) + out.disconnect(); + } + } + + /** + * Tell the peer we have another piece. + */ + public void have(int piece) + { + PeerState s = state; + if (s != null) + s.havePiece(piece); + } + + /** + * Whether or not the peer is interested in pieces we have. Returns + * false if not connected. + */ + public boolean isInterested() + { + PeerState s = state; + return (s != null) && s.interested; + } + + /** + * Sets whether or not we are interested in pieces from this peer. + * Defaults to false. When interest is true and this peer unchokes + * us then we start downloading from it. Has no effect when not connected. + */ + public void setInteresting(boolean interest) + { + PeerState s = state; + if (s != null) + s.setInteresting(interest); + } + + /** + * Whether or not the peer has pieces we want from it. Returns false + * if not connected. + */ + public boolean isInteresting() + { + PeerState s = state; + return (s != null) && s.interesting; + } + + /** + * Sets whether or not we are choking the peer. Defaults to + * true. When choke is false and the peer requests some pieces we + * upload them, otherwise requests of this peer are ignored. + */ + public void setChoking(boolean choke) + { + PeerState s = state; + if (s != null) + s.setChoking(choke); + } + + /** + * Whether or not we are choking the peer. Returns true when not connected. + */ + public boolean isChoking() + { + PeerState s = state; + return (s == null) || s.choking; + } + + /** + * Whether or not the peer choked us. Returns true when not connected. + */ + public boolean isChoked() + { + PeerState s = state; + return (s == null) || s.choked; + } + + /** + * Returns the number of bytes that have been downloaded. + * Can be reset to zero with <code>resetCounters()</code>/ + */ + public long getDownloaded() + { + PeerState s = state; + return (s != null) ? s.downloaded : 0; + } + + /** + * Returns the number of bytes that have been uploaded. + * Can be reset to zero with <code>resetCounters()</code>/ + */ + public long getUploaded() + { + PeerState s = state; + return (s != null) ? s.uploaded : 0; + } + + /** + * Resets the downloaded and uploaded counters to zero. + */ + public void resetCounters() + { + PeerState s = state; + if (s != null) + { + s.downloaded = 0; + s.uploaded = 0; + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..966a6a455fc23ab46dc301c1b4dade9a17ea5200 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java @@ -0,0 +1,62 @@ +/* PeerAcceptor - Accepts incomming connections from peers. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.*; +import java.net.*; + +import net.i2p.client.streaming.I2PSocket; + +/** + * Accepts incomming connections from peers. The ConnectionAcceptor + * will call the connection() method when it detects an incomming BT + * protocol connection. The PeerAcceptor will then create a new peer + * if the PeerCoordinator wants more peers. + */ +public class PeerAcceptor +{ + private final PeerCoordinator coordinator; + + public PeerAcceptor(PeerCoordinator coordinator) + { + this.coordinator = coordinator; + } + + public void connection(I2PSocket socket, + BufferedInputStream bis, BufferedOutputStream bos) + throws IOException + { + if (coordinator.needPeers()) + { + // XXX: inside this Peer constructor's handshake is where you'd deal with the other + // side saying they want to communicate with another torrent - aka multitorrent + // support. you'd then want to grab the meta info /they/ want, look that up in + // our own list of active torrents, and put it on the right coordinator for it. + // this currently, however, throws an IOException if the metainfo doesn't match + // coodinator.getMetaInfo (Peer.java:242) + Peer peer = new Peer(socket, bis, bos, coordinator.getID(), + coordinator.getMetaInfo()); + coordinator.addPeer(peer); + } + else + socket.close(); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java new file mode 100644 index 0000000000000000000000000000000000000000..041387937def4805f64f966f4fb63b2ae3e4e1ad --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java @@ -0,0 +1,197 @@ +/* PeerCheckTasks - TimerTask that checks for good/bad up/downloaders. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.util.*; + +/** + * TimerTask that checks for good/bad up/downloader. Works together + * with the PeerCoordinator to select which Peers get (un)choked. + */ +class PeerCheckerTask extends TimerTask +{ + private final long KILOPERSECOND = 1024*(PeerCoordinator.CHECK_PERIOD/1000); + + private final PeerCoordinator coordinator; + + PeerCheckerTask(PeerCoordinator coordinator) + { + this.coordinator = coordinator; + } + + public void run() + { + synchronized(coordinator.peers) + { + // Calculate total uploading and worst downloader. + long worstdownload = Long.MAX_VALUE; + Peer worstDownloader = null; + + int peers = 0; + int uploaders = 0; + int downloaders = 0; + int interested = 0; + int interesting = 0; + int choking = 0; + int choked = 0; + + long uploaded = 0; + long downloaded = 0; + + // Keep track of peers we remove now, + // we will add them back to the end of the list. + List removed = new ArrayList(); + + Iterator it = coordinator.peers.iterator(); + while (it.hasNext()) + { + Peer peer = (Peer)it.next(); + + // Remove dying peers + if (!peer.isConnected()) + { + it.remove(); + continue; + } + + peers++; + + if (!peer.isChoking()) + uploaders++; + if (!peer.isChoked() && peer.isInteresting()) + downloaders++; + if (peer.isInterested()) + interested++; + if (peer.isInteresting()) + interesting++; + if (peer.isChoking()) + choking++; + if (peer.isChoked()) + choked++; + + // XXX - We should calculate the up/download rate a bit + // more intelligently + long upload = peer.getUploaded(); + uploaded += upload; + long download = peer.getDownloaded(); + downloaded += download; + peer.resetCounters(); + + if (Snark.debug >= Snark.DEBUG) + { + Snark.debug(peer + ":", Snark.DEBUG); + Snark.debug(" ul: " + upload/KILOPERSECOND + + " dl: " + download/KILOPERSECOND + + " i: " + peer.isInterested() + + " I: " + peer.isInteresting() + + " c: " + peer.isChoking() + + " C: " + peer.isChoked(), + Snark.DEBUG); + } + + // If we are at our max uploaders and we have lots of other + // interested peers try to make some room. + // (Note use of coordinator.uploaders) + if (coordinator.uploaders >= PeerCoordinator.MAX_UPLOADERS + && interested > PeerCoordinator.MAX_UPLOADERS + && !peer.isChoking()) + { + // Check if it still wants pieces from us. + if (!peer.isInterested()) + { + if (Snark.debug >= Snark.INFO) + Snark.debug("Choke uninterested peer: " + peer, + Snark.INFO); + peer.setChoking(true); + uploaders--; + coordinator.uploaders--; + + // Put it at the back of the list + it.remove(); + removed.add(peer); + } + else if (peer.isChoked()) + { + // If they are choking us make someone else a downloader + if (Snark.debug >= Snark.DEBUG) + Snark.debug("Choke choking peer: " + peer, Snark.DEBUG); + peer.setChoking(true); + uploaders--; + coordinator.uploaders--; + + // Put it at the back of the list + it.remove(); + removed.add(peer); + } + else if (peer.isInteresting() + && !peer.isChoked() + && download == 0) + { + // We are downloading but didn't receive anything... + if (Snark.debug >= Snark.DEBUG) + Snark.debug("Choke downloader that doesn't deliver:" + + peer, Snark.DEBUG); + peer.setChoking(true); + uploaders--; + coordinator.uploaders--; + + // Put it at the back of the list + it.remove(); + removed.add(peer); + } + else if (!peer.isChoking() && download < worstdownload) + { + // Make sure download is good if we are uploading + worstdownload = download; + worstDownloader = peer; + } + } + } + + // Resync actual uploaders value + // (can shift a bit by disconnecting peers) + coordinator.uploaders = uploaders; + + // Remove the worst downloader if needed. + if (uploaders >= PeerCoordinator.MAX_UPLOADERS + && interested > PeerCoordinator.MAX_UPLOADERS + && worstDownloader != null) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug("Choke worst downloader: " + worstDownloader, + Snark.DEBUG); + + worstDownloader.setChoking(true); + coordinator.uploaders--; + + // Put it at the back of the list + coordinator.peers.remove(worstDownloader); + removed.add(worstDownloader); + } + + // Optimistically unchoke a peer + coordinator.unchokePeer(); + + // Put peers back at the end of the list that we removed earlier. + coordinator.peers.addAll(removed); + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java new file mode 100644 index 0000000000000000000000000000000000000000..c3d5f95bef0e557af31db3e1e98e013ee0153f69 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java @@ -0,0 +1,156 @@ +/* PeerConnectionIn - Handles incomming messages and hands them to PeerState. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.*; +import java.net.*; +import java.util.*; + +class PeerConnectionIn implements Runnable +{ + private final Peer peer; + private final DataInputStream din; + + private Thread thread; + private boolean quit; + + public PeerConnectionIn(Peer peer, DataInputStream din) + { + this.peer = peer; + this.din = din; + quit = false; + } + + void disconnect() + { + if (quit == true) + return; + + quit = true; + Thread t = thread; + if (t != null) + t.interrupt(); + } + + public void run() + { + thread = Thread.currentThread(); + try + { + PeerState ps = peer.state; + while (!quit && ps != null) + { + // Common variables used for some messages. + int piece; + int begin; + int len; + + // Wait till we hear something... + // The length of a complete message in bytes. + int i = din.readInt(); + if (i < 0) + throw new IOException("Unexpected length prefix: " + i); + + if (i == 0) + { + ps.keepAliveMessage(); + continue; + } + + byte b = din.readByte(); + Message m = new Message(); + m.type = b; + switch (b) + { + case 0: + ps.chokeMessage(true); + break; + case 1: + ps.chokeMessage(false); + break; + case 2: + ps.interestedMessage(true); + break; + case 3: + ps.interestedMessage(false); + break; + case 4: + piece = din.readInt(); + ps.haveMessage(piece); + break; + case 5: + byte[] bitmap = new byte[i-1]; + din.readFully(bitmap); + ps.bitfieldMessage(bitmap); + break; + case 6: + piece = din.readInt(); + begin = din.readInt(); + len = din.readInt(); + ps.requestMessage(piece, begin, len); + break; + case 7: + piece = din.readInt(); + begin = din.readInt(); + len = i-9; + Request req = ps.getOutstandingRequest(piece, begin, len); + byte[] piece_bytes; + if (req != null) + { + piece_bytes = req.bs; + din.readFully(piece_bytes, begin, len); + ps.pieceMessage(req); + } + else + { + // XXX - Consume but throw away afterwards. + piece_bytes = new byte[len]; + din.readFully(piece_bytes); + } + break; + case 8: + piece = din.readInt(); + begin = din.readInt(); + len = din.readInt(); + ps.cancelMessage(piece, begin, len); + break; + default: + byte[] bs = new byte[i-1]; + din.readFully(bs); + ps.unknownMessage(b, bs); + } + } + } + catch (IOException ioe) + { + // Ignore, probably the other side closed connection. + } + catch (Throwable t) + { + Snark.debug(peer + ": " + t, Snark.ERROR); + t.printStackTrace(); + } + finally + { + peer.disconnect(); + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java new file mode 100644 index 0000000000000000000000000000000000000000..8d09859a74686a71ded1814caeeca289f2928457 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java @@ -0,0 +1,342 @@ +/* PeerConnectionOut - Keeps a queue of outgoing messages and delivers them. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.*; +import java.net.*; +import java.util.*; + +class PeerConnectionOut implements Runnable +{ + private final Peer peer; + private final DataOutputStream dout; + + private Thread thread; + private boolean quit; + + // Contains Messages. + private List sendQueue = new ArrayList(); + + public PeerConnectionOut(Peer peer, DataOutputStream dout) + { + this.peer = peer; + this.dout = dout; + + quit = false; + thread = new Thread(this); + thread.start(); + } + + /** + * Continuesly monitors for more outgoing messages that have to be send. + * Stops if quit is true of an IOException occurs. + */ + public void run() + { + try + { + while (!quit) + { + Message m = null; + PeerState state = null; + synchronized(sendQueue) + { + while (!quit && sendQueue.isEmpty()) + { + try + { + // Make sure everything will reach the other side. + dout.flush(); + + // Wait till more data arrives. + sendQueue.wait(); + } + catch (InterruptedException ie) + { + /* ignored */ + } + } + state = peer.state; + if (!quit && state != null) + { + // Piece messages are big. So if there are other + // (control) messages make sure they are send first. + // Also remove request messages from the queue if + // we are currently being choked to prevent them from + // being send even if we get unchoked a little later. + // (Since we will resent them anyway in that case.) + // And remove piece messages if we are choking. + Iterator it = sendQueue.iterator(); + while (m == null && it.hasNext()) + { + Message nm = (Message)it.next(); + if (nm.type == Message.PIECE) + { + if (state.choking) + it.remove(); + nm = null; + } + else if (nm.type == Message.REQUEST && state.choked) + { + it.remove(); + nm = null; + } + + if (m == null && nm != null) + { + m = nm; + it.remove(); + } + } + if (m == null && sendQueue.size() > 0) + m = (Message)sendQueue.remove(0); + } + } + if (m != null) + { + if (Snark.debug >= Snark.ALL) + Snark.debug("Send " + peer + ": " + m, Snark.ALL); + m.sendMessage(dout); + + // Remove all piece messages after sending a choke message. + if (m.type == Message.CHOKE) + removeMessage(Message.PIECE); + + // XXX - Should also register overhead... + if (m.type == Message.PIECE) + state.uploaded(m.len); + + m = null; + } + } + } + catch (IOException ioe) + { + // Ignore, probably other side closed connection. + } + catch (Throwable t) + { + Snark.debug(peer + ": " + t, Snark.ERROR); + t.printStackTrace(); + } + finally + { + quit = true; + peer.disconnect(); + } + } + + public void disconnect() + { + synchronized(sendQueue) + { + if (quit == true) + return; + + quit = true; + thread.interrupt(); + + sendQueue.clear(); + sendQueue.notify(); + } + } + + /** + * Adds a message to the sendQueue and notifies the method waiting + * on the sendQueue to change. + */ + private void addMessage(Message m) + { + synchronized(sendQueue) + { + sendQueue.add(m); + sendQueue.notify(); + } + } + + /** + * Removes a particular message type from the queue. + * + * @param type the Message type to remove. + * @returns true when a message of the given type was removed, false + * otherwise. + */ + private boolean removeMessage(int type) + { + boolean removed = false; + synchronized(sendQueue) + { + Iterator it = sendQueue.iterator(); + while (it.hasNext()) + { + Message m = (Message)it.next(); + if (m.type == type) + { + it.remove(); + removed = true; + } + } + } + return removed; + } + + void sendAlive() + { + Message m = new Message(); + m.type = Message.KEEP_ALIVE; + addMessage(m); + } + + void sendChoke(boolean choke) + { + // We cancel the (un)choke but keep PIECE messages. + // PIECE messages are purged if a choke is actually send. + synchronized(sendQueue) + { + int inverseType = choke ? Message.UNCHOKE + : Message.CHOKE; + if (!removeMessage(inverseType)) + { + Message m = new Message(); + if (choke) + m.type = Message.CHOKE; + else + m.type = Message.UNCHOKE; + addMessage(m); + } + } + } + + void sendInterest(boolean interest) + { + synchronized(sendQueue) + { + int inverseType = interest ? Message.UNINTERESTED + : Message.INTERESTED; + if (!removeMessage(inverseType)) + { + Message m = new Message(); + if (interest) + m.type = Message.INTERESTED; + else + m.type = Message.UNINTERESTED; + addMessage(m); + } + } + } + + void sendHave(int piece) + { + Message m = new Message(); + m.type = Message.HAVE; + m.piece = piece; + addMessage(m); + } + + void sendBitfield(BitField bitfield) + { + Message m = new Message(); + m.type = Message.BITFIELD; + m.data = bitfield.getFieldBytes(); + m.off = 0; + m.len = m.data.length; + addMessage(m); + } + + void sendRequests(List requests) + { + Iterator it = requests.iterator(); + while (it.hasNext()) + { + Request req = (Request)it.next(); + sendRequest(req); + } + } + + void sendRequest(Request req) + { + Message m = new Message(); + m.type = Message.REQUEST; + m.piece = req.piece; + m.begin = req.off; + m.length = req.len; + addMessage(m); + } + + void sendPiece(int piece, int begin, int length, byte[] bytes) + { + Message m = new Message(); + m.type = Message.PIECE; + m.piece = piece; + m.begin = begin; + m.length = length; + m.data = bytes; + m.off = begin; + m.len = length; + addMessage(m); + } + + void sendCancel(Request req) + { + // See if it is still in our send queue + synchronized(sendQueue) + { + Iterator it = sendQueue.iterator(); + while (it.hasNext()) + { + Message m = (Message)it.next(); + if (m.type == Message.REQUEST + && m.piece == req.piece + && m.begin == req.off + && m.length == req.len) + it.remove(); + } + } + + // Always send, just to be sure it it is really canceled. + Message m = new Message(); + m.type = Message.CANCEL; + m.piece = req.piece; + m.begin = req.off; + m.length = req.len; + addMessage(m); + } + + // Called by the PeerState when the other side doesn't want this + // request to be handled anymore. Removes any pending Piece Message + // from out send queue. + void cancelRequest(int piece, int begin, int length) + { + synchronized (sendQueue) + { + Iterator it = sendQueue.iterator(); + while (it.hasNext()) + { + Message m = (Message)it.next(); + if (m.type == Message.PIECE + && m.piece == piece + && m.begin == begin + && m.length == length) + it.remove(); + } + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java new file mode 100644 index 0000000000000000000000000000000000000000..3fb39ce96b9f43d6a5270dc8f7b344373fb1083d --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java @@ -0,0 +1,508 @@ +/* PeerCoordinator - Coordinates which peers do what (up and downloading). + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.util.*; +import java.io.IOException; + +/** + * Coordinates what peer does what. + */ +public class PeerCoordinator implements PeerListener +{ + final MetaInfo metainfo; + final Storage storage; + + // package local for access by CheckDownLoadersTask + final static long CHECK_PERIOD = 20*1000; // 20 seconds + final static int MAX_CONNECTIONS = 24; + final static int MAX_UPLOADERS = 12; // i2p: might as well balance it out + + // Approximation of the number of current uploaders. + // Resynced by PeerChecker once in a while. + int uploaders = 0; + + // final static int MAX_DOWNLOADERS = MAX_CONNECTIONS; + // int downloaders = 0; + + private long uploaded; + private long downloaded; + + // synchronize on this when changing peers or downloaders + final List peers = new ArrayList(); + + /** Timer to handle all periodical tasks. */ + private final Timer timer = new Timer(true); + + private final byte[] id; + + // Some random wanted pieces + private final List wantedPieces; + + private boolean halted = false; + + private final CoordinatorListener listener; + + public PeerCoordinator(byte[] id, MetaInfo metainfo, Storage storage, + CoordinatorListener listener) + { + this.id = id; + this.metainfo = metainfo; + this.storage = storage; + this.listener = listener; + + // Make a random list of piece numbers + wantedPieces = new ArrayList(); + BitField bitfield = storage.getBitField(); + for(int i = 0; i < metainfo.getPieces(); i++) + if (!bitfield.get(i)) + wantedPieces.add(new Integer(i)); + Collections.shuffle(wantedPieces); + + // Install a timer to check the uploaders. + timer.schedule(new PeerCheckerTask(this), CHECK_PERIOD, CHECK_PERIOD); + } + + public byte[] getID() + { + return id; + } + + public boolean completed() + { + return storage.complete(); + } + + + public int getPeers() + { + synchronized(peers) + { + return peers.size(); + } + } + + /** + * Returns how many bytes are still needed to get the complete file. + */ + public long getLeft() + { + // XXX - Only an approximation. + return storage.needed() * metainfo.getPieceLength(0); + } + + /** + * Returns the total number of uploaded bytes of all peers. + */ + public long getUploaded() + { + return uploaded; + } + + /** + * Returns the total number of downloaded bytes of all peers. + */ + public long getDownloaded() + { + return downloaded; + } + + public MetaInfo getMetaInfo() + { + return metainfo; + } + + public boolean needPeers() + { + synchronized(peers) + { + return !halted && peers.size() < MAX_CONNECTIONS; + } + } + + public void halt() + { + halted = true; + synchronized(peers) + { + // Stop peer checker task. + timer.cancel(); + + // Stop peers. + Iterator it = peers.iterator(); + while(it.hasNext()) + { + Peer peer = (Peer)it.next(); + peer.disconnect(); + it.remove(); + } + } + } + + public void connected(Peer peer) + { + if (halted) + { + peer.disconnect(false); + return; + } + + synchronized(peers) + { + if (peerIDInList(peer.getPeerID(), peers)) + { + if (Snark.debug >= Snark.INFO) + Snark.debug("Already connected to: " + peer, Snark.INFO); + peer.disconnect(false); // Don't deregister this connection/peer. + } + else + { + if (Snark.debug >= Snark.INFO) + Snark.debug("New connection to peer: " + peer, Snark.INFO); + + // Add it to the beginning of the list. + // And try to optimistically make it a uploader. + peers.add(0, peer); + unchokePeer(); + + if (listener != null) + listener.peerChange(this, peer); + } + } + } + + private static boolean peerIDInList(PeerID pid, List peers) + { + Iterator it = peers.iterator(); + while (it.hasNext()) + if (pid.sameID(((Peer)it.next()).getPeerID())) + return true; + return false; + } + + public void addPeer(final Peer peer) + { + if (halted) + { + peer.disconnect(false); + return; + } + + boolean need_more; + synchronized(peers) + { + need_more = !peer.isConnected() && peers.size() < MAX_CONNECTIONS; + } + + if (need_more) + { + // Run the peer with us as listener and the current bitfield. + final PeerListener listener = this; + final BitField bitfield = storage.getBitField(); + Runnable r = new Runnable() + { + public void run() + { + peer.runConnection(listener, bitfield); + } + }; + String threadName = peer.toString(); + new Thread(r, threadName).start(); + } + else + if (Snark.debug >= Snark.INFO) + if (peer.isConnected()) + Snark.debug("Add peer already connected: " + peer, Snark.INFO); + else + Snark.debug("MAX_CONNECTIONS = " + MAX_CONNECTIONS + + " not accepting extra peer: " + peer, Snark.INFO); + } + + + // (Optimistically) unchoke. Should be called with peers synchronized + void unchokePeer() + { + // linked list will contain all interested peers that we choke. + // At the start are the peers that have us unchoked at the end the + // other peer that are interested, but are choking us. + List interested = new LinkedList(); + Iterator it = peers.iterator(); + while (it.hasNext()) + { + Peer peer = (Peer)it.next(); + boolean remove = false; + if (uploaders < MAX_UPLOADERS + && peer.isChoking() + && peer.isInterested()) + { + if (!peer.isChoked()) + interested.add(0, peer); + else + interested.add(peer); + } + } + + while (uploaders < MAX_UPLOADERS && interested.size() > 0) + { + Peer peer = (Peer)interested.remove(0); + if (Snark.debug >= Snark.INFO) + Snark.debug("Unchoke: " + peer, Snark.INFO); + peer.setChoking(false); + uploaders++; + // Put peer back at the end of the list. + peers.remove(peer); + peers.add(peer); + } + } + + public byte[] getBitMap() + { + return storage.getBitField().getFieldBytes(); + } + + /** + * Returns true if we don't have the given piece yet. + */ + public boolean gotHave(Peer peer, int piece) + { + if (listener != null) + listener.peerChange(this, peer); + + synchronized(wantedPieces) + { + return wantedPieces.contains(new Integer(piece)); + } + } + + /** + * Returns true if the given bitfield contains at least one piece we + * are interested in. + */ + public boolean gotBitField(Peer peer, BitField bitfield) + { + if (listener != null) + listener.peerChange(this, peer); + + synchronized(wantedPieces) + { + Iterator it = wantedPieces.iterator(); + while (it.hasNext()) + { + int i = ((Integer)it.next()).intValue(); + if (bitfield.get(i)) + return true; + } + } + return false; + } + + /** + * Returns one of pieces in the given BitField that is still wanted or + * -1 if none of the given pieces are wanted. + */ + public int wantPiece(Peer peer, BitField havePieces) + { + if (halted) + return -1; + + synchronized(wantedPieces) + { + Integer piece = null; + Iterator it = wantedPieces.iterator(); + while (piece == null && it.hasNext()) + { + Integer i = (Integer)it.next(); + if (havePieces.get(i.intValue())) + { + it.remove(); + piece = i; + } + } + + if (piece == null) + return -1; + + // We add it back at the back of the list. It will be removed + // if gotPiece is called later. This means that the last + // couple of pieces might very well be asked from multiple + // peers but that is OK. + wantedPieces.add(piece); + + return piece.intValue(); + } + } + + /** + * Returns a byte array containing the requested piece or null of + * the piece is unknown. + */ + public byte[] gotRequest(Peer peer, int piece) + { + if (halted) + return null; + + try + { + return storage.getPiece(piece); + } + catch (IOException ioe) + { + Snark.fatal("Error reading storage", ioe); + return null; // Never reached. + } + } + + /** + * Called when a peer has uploaded some bytes of a piece. + */ + public void uploaded(Peer peer, int size) + { + uploaded += size; + + if (listener != null) + listener.peerChange(this, peer); + } + + /** + * Called when a peer has downloaded some bytes of a piece. + */ + public void downloaded(Peer peer, int size) + { + downloaded += size; + + if (listener != null) + listener.peerChange(this, peer); + } + + /** + * Returns false if the piece is no good (according to the hash). + * In that case the peer that supplied the piece should probably be + * blacklisted. + */ + public boolean gotPiece(Peer peer, int piece, byte[] bs) + { + if (halted) + return true; // We don't actually care anymore. + + synchronized(wantedPieces) + { + Integer p = new Integer(piece); + if (!wantedPieces.contains(p)) + { + if (Snark.debug >= Snark.INFO) + Snark.debug(peer + " piece " + piece + " no longer needed", + Snark.INFO); + + // No need to announce have piece to peers. + // Assume we got a good piece, we don't really care anymore. + return true; + } + + try + { + if (storage.putPiece(piece, bs)) + { + if (Snark.debug >= Snark.INFO) + Snark.debug("Recv p" + piece + " " + peer, Snark.INFO); + } + else + { + // Oops. We didn't actually download this then... :( + downloaded -= metainfo.getPieceLength(piece); + if (Snark.debug >= Snark.NOTICE) + Snark.debug("Got BAD piece " + piece + " from " + peer, + Snark.NOTICE); + return false; // No need to announce BAD piece to peers. + } + } + catch (IOException ioe) + { + Snark.fatal("Error writing storage", ioe); + } + wantedPieces.remove(p); + } + + // Announce to the world we have it! + synchronized(peers) + { + Iterator it = peers.iterator(); + while (it.hasNext()) + { + Peer p = (Peer)it.next(); + if (p.isConnected()) + p.have(piece); + } + } + + return true; + } + + public void gotChoke(Peer peer, boolean choke) + { + if (Snark.debug >= Snark.INFO) + Snark.debug("Got choke(" + choke + "): " + peer, Snark.INFO); + + if (listener != null) + listener.peerChange(this, peer); + } + + public void gotInterest(Peer peer, boolean interest) + { + if (interest) + { + synchronized(peers) + { + if (uploaders < MAX_UPLOADERS) + { + if(peer.isChoking()) + { + uploaders++; + peer.setChoking(false); + if (Snark.debug >= Snark.INFO) + Snark.debug("Unchoke: " + peer, Snark.INFO); + } + } + } + } + + if (listener != null) + listener.peerChange(this, peer); + } + + public void disconnected(Peer peer) + { + if (Snark.debug >= Snark.INFO) + Snark.debug("Disconnected " + peer, Snark.INFO); + + synchronized(peers) + { + // Make sure it is no longer in our lists + if (peers.remove(peer)) + { + // Unchoke some random other peer + unchokePeer(); + } + } + + if (listener != null) + listener.peerChange(this, peer); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerID.java b/apps/i2psnark/java/src/org/klomp/snark/PeerID.java new file mode 100644 index 0000000000000000000000000000000000000000..2788e9e5bef3f8c0fb5a61def0109dfcde3d8d54 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerID.java @@ -0,0 +1,208 @@ +/* PeerID - All public information concerning a peer. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; + +import org.klomp.snark.bencode.*; + +import net.i2p.data.Base64; +import net.i2p.data.Destination; +import net.i2p.data.DataFormatException; + +public class PeerID implements Comparable +{ + private final byte[] id; + private final Destination address; + private final int port; + + private final int hash; + + public PeerID(byte[] id, Destination address) + { + this.id = id; + this.address = address; + this.port = 6881; + + hash = calculateHash(); + } + + /** + * Creates a PeerID from a BDecoder. + */ + public PeerID(BDecoder be) + throws IOException + { + this(be.bdecodeMap().getMap()); + } + + /** + * Creates a PeerID from a Map containing BEncoded peer id, ip and + * port. + */ + public PeerID(Map m) + throws InvalidBEncodingException, UnknownHostException + { + BEValue bevalue = (BEValue)m.get("peer id"); + if (bevalue == null) + throw new InvalidBEncodingException("peer id missing"); + id = bevalue.getBytes(); + + bevalue = (BEValue)m.get("ip"); + if (bevalue == null) + throw new InvalidBEncodingException("ip missing"); + address = I2PSnarkUtil.instance().getDestination(bevalue.getString()); + if (address == null) + throw new InvalidBEncodingException("Invalid destination [" + bevalue.getString() + "]"); + + port = 6881; + + hash = calculateHash(); + } + + public byte[] getID() + { + return id; + } + + public Destination getAddress() + { + return address; + } + + public int getPort() + { + return port; + } + + private int calculateHash() + { + int b = 0; + for (int i = 0; i < id.length; i++) + b ^= id[i]; + return (b ^ address.hashCode()) ^ port; + } + + /** + * The hash code of a PeerID is the exclusive or of all id bytes. + */ + public int hashCode() + { + return hash; + } + + /** + * Returns true if and only if this peerID and the given peerID have + * the same 20 bytes as ID. + */ + public boolean sameID(PeerID pid) + { + boolean equal = true; + for (int i = 0; equal && i < id.length; i++) + equal = id[i] == pid.id[i]; + return equal; + } + + /** + * Two PeerIDs are equal when they have the same id, address and port. + */ + public boolean equals(Object o) + { + if (o instanceof PeerID) + { + PeerID pid = (PeerID)o; + + return port == pid.port + && address.equals(pid.address) + && sameID(pid); + } + else + return false; + } + + /** + * Compares port, address and id. + */ + public int compareTo(Object o) + { + PeerID pid = (PeerID)o; + + int result = port - pid.port; + if (result != 0) + return result; + + result = address.hashCode() - pid.address.hashCode(); + if (result != 0) + return result; + + for (int i = 0; i < id.length; i++) + { + result = id[i] - pid.id[i]; + if (result != 0) + return result; + } + + return 0; + } + + /** + * Returns the String "id@address" where id is the base64 encoded id. + */ + public String toString() + { + int nonZero = 0; + for (int i = 0; i < id.length; i++) { + if (id[i] != 0) { + nonZero = i; + break; + } + } + return Base64.encode(id, nonZero, id.length-nonZero).substring(0,4) + "@" + address.calculateHash().toBase64().substring(0,6); + } + + /** + * Encode an id as a hex encoded string and remove leading zeros. + */ + public static String idencode(byte[] bs) + { + boolean leading_zeros = true; + + StringBuffer sb = new StringBuffer(bs.length*2); + for (int i = 0; i < bs.length; i++) + { + int c = bs[i] & 0xFF; + if (leading_zeros && c == 0) + continue; + else + leading_zeros = false; + + if (c < 16) + sb.append('0'); + sb.append(Integer.toHexString(c)); + } + + return sb.toString(); + } + +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java new file mode 100644 index 0000000000000000000000000000000000000000..959b42fa1492a230fa6fc531614360e18a92784d --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java @@ -0,0 +1,145 @@ +/* PeerListener - Interface for listening to peer events. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +/** + * Listener for Peer events. + */ +public interface PeerListener +{ + /** + * Called when the connection to the peer has started and the + * handshake was successfull. + * + * @param peer the Peer that just got connected. + */ + void connected(Peer peer); + + /** + * Called when the connection to the peer was terminated or the + * connection handshake failed. + * + * @param peer the Peer that just got disconnected. + */ + void disconnected(Peer peer); + + /** + * Called when a choke message is received. + * + * @param peer the Peer that got the message. + * @param choke true when the peer got a choke message, false when + * the peer got an unchoke message. + */ + void gotChoke(Peer peer, boolean choke); + + /** + * Called when an interested message is received. + * + * @param peer the Peer that got the message. + * @param interest true when the peer got a interested message, false when + * the peer got an uninterested message. + */ + void gotInterest(Peer peer, boolean interest); + + /** + * Called when a have piece message is received. If the method + * returns true and the peer has not yet received a interested + * message or we indicated earlier to be not interested then an + * interested message will be send. + * + * @param peer the Peer that got the message. + * @param piece the piece number that the per just got. + * + * @return true when it is a piece that we want, false if the piece is + * already known. + */ + boolean gotHave(Peer peer, int piece); + + /** + * Called when a bitmap message is received. If this method returns + * true a interested message will be send back to the peer. + * + * @param peer the Peer that got the message. + * @param bitfield a BitField containing the pieces that the other + * side has. + * + * @return true when the BitField contains pieces we want, false if + * the piece is already known. + */ + boolean gotBitField(Peer peer, BitField bitfield); + + /** + * Called when a piece is received from the peer. The piece must be + * requested by Peer.request() first. If this method returns false + * that means the Peer provided a corrupted piece and the connection + * will be closed. + * + * @param peer the Peer that got the piece. + * @param piece the piece number received. + * @param bs the byte array containing the piece. + * + * @return true when the bytes represent the piece, false otherwise. + */ + boolean gotPiece(Peer peer, int piece, byte[] bs); + + /** + * Called when the peer wants (part of) a piece from us. Only called + * when the peer is not choked by us (<code>peer.choke(false)</code> + * was called). + * + * @param peer the Peer that wants the piece. + * @param piece the piece number requested. + * + * @return a byte array containing the piece or null when the piece + * is not available (which is a protocol error). + */ + byte[] gotRequest(Peer peer, int piece); + + /** + * Called when a (partial) piece has been downloaded from the peer. + * + * @param peer the Peer from which size bytes where downloaded. + * @param size the number of bytes that where downloaded. + */ + void downloaded(Peer peer, int size); + + /** + * Called when a (partial) piece has been uploaded to the peer. + * + * @param peer the Peer to which size bytes where uploaded. + * @param size the number of bytes that where uploaded. + */ + void uploaded(Peer peer, int size); + + /** + * Called when we are downloading from the peer and need to ask for + * a new piece. Might be called multiple times before + * <code>gotPiece()</code> is called. + * + * @param peer the Peer that will be asked to provide the piece. + * @param bitfield a BitField containing the pieces that the other + * side has. + * + * @return one of the pieces from the bitfield that we want or -1 if + * we are no longer interested in the peer. + */ + int wantPiece(Peer peer, BitField bitfield); +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerMonitorTask.java b/apps/i2psnark/java/src/org/klomp/snark/PeerMonitorTask.java new file mode 100644 index 0000000000000000000000000000000000000000..b409f58c8e5d3edcc9817ee145a0b8cd8bed76ec --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerMonitorTask.java @@ -0,0 +1,128 @@ +/* PeerMonitorTasks - TimerTask that monitors the peers and total up/down speed + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.util.*; + +/** + * TimerTask that monitors the peers and total up/download speeds. + * Works together with the main Snark class to report periodical statistics. + */ +class PeerMonitorTask extends TimerTask +{ + final static long MONITOR_PERIOD = 10 * 1000; // Ten seconds. + private final long KILOPERSECOND = 1024 * (MONITOR_PERIOD / 1000); + + private final PeerCoordinator coordinator; + + private long lastDownloaded = 0; + private long lastUploaded = 0; + + PeerMonitorTask(PeerCoordinator coordinator) + { + this.coordinator = coordinator; + } + + public void run() + { + // Get some statistics + int peers = 0; + int uploaders = 0; + int downloaders = 0; + int interested = 0; + int interesting = 0; + int choking = 0; + int choked = 0; + + synchronized(coordinator.peers) + { + Iterator it = coordinator.peers.iterator(); + while (it.hasNext()) + { + Peer peer = (Peer)it.next(); + + // Don't list dying peers + if (!peer.isConnected()) + continue; + + peers++; + + if (!peer.isChoking()) + uploaders++; + if (!peer.isChoked() && peer.isInteresting()) + downloaders++; + if (peer.isInterested()) + interested++; + if (peer.isInteresting()) + interesting++; + if (peer.isChoking()) + choking++; + if (peer.isChoked()) + choked++; + } + } + + // Print some statistics + long downloaded = coordinator.getDownloaded(); + String totalDown; + if (downloaded >= 10 * 1024 * 1024) + totalDown = (downloaded / (1024 * 1024)) + "MB"; + else + totalDown = (downloaded / 1024 )+ "KB"; + long uploaded = coordinator.getUploaded(); + String totalUp; + if (uploaded >= 10 * 1024 * 1024) + totalUp = (uploaded / (1024 * 1024)) + "MB"; + else + totalUp = (uploaded / 1024) + "KB"; + + int needP = coordinator.storage.needed(); + long needMB + = needP * coordinator.metainfo.getPieceLength(0) / (1024 * 1024); + int totalP = coordinator.metainfo.getPieces(); + long totalMB = coordinator.metainfo.getTotalLength() / (1024 * 1024); + + System.out.println(); + System.out.println("Down: " + + (downloaded - lastDownloaded) / KILOPERSECOND + + "KB/s" + + " (" + totalDown + ")" + + " Up: " + + (uploaded - lastUploaded) / KILOPERSECOND + + "KB/s" + + " (" + totalUp + ")" + + " Need " + needP + + " (" + needMB + "MB)" + + " of " + totalP + + " (" + totalMB + "MB)" + + " pieces"); + System.out.println(peers + ": Download #" + downloaders + + " Upload #" + uploaders + + " Interested #" + interested + + " Interesting #" + interesting + + " Choking #" + choking + + " Choked #" + choked); + System.out.println(); + + lastDownloaded = downloaded; + lastUploaded = uploaded; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java new file mode 100644 index 0000000000000000000000000000000000000000..75b64e443401ac582107513005fa92707d71161e --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java @@ -0,0 +1,539 @@ +/* PeerState - Keeps track of the Peer state through connection callbacks. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.HashSet; + +class PeerState +{ + final Peer peer; + final PeerListener listener; + final MetaInfo metainfo; + + // Interesting and choking describes whether we are interested in or + // are choking the other side. + boolean interesting = false; + boolean choking = true; + + // Interested and choked describes whether the other side is + // interested in us or choked us. + boolean interested = false; + boolean choked = true; + + // Package local for use by Peer. + long downloaded; + long uploaded; + + BitField bitfield; + + // Package local for use by Peer. + final PeerConnectionIn in; + final PeerConnectionOut out; + + // Outstanding request + private final List outstandingRequests = new ArrayList(); + private Request lastRequest = null; + + // If we have te resend outstanding requests (true after we got choked). + private boolean resend = false; + + private final static int MAX_PIPELINE = 5; + private final static int PARTSIZE = 64*1024; // default was 16K, i2p-bt uses 64KB + + PeerState(Peer peer, PeerListener listener, MetaInfo metainfo, + PeerConnectionIn in, PeerConnectionOut out) + { + this.peer = peer; + this.listener = listener; + this.metainfo = metainfo; + + this.in = in; + this.out = out; + } + + // NOTE Methods that inspect or change the state synchronize (on this). + + void keepAliveMessage() + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " rcv alive", Snark.DEBUG); + /* XXX - ignored */ + } + + void chokeMessage(boolean choke) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " rcv " + (choke ? "" : "un") + "choked", + Snark.DEBUG); + + choked = choke; + if (choked) + resend = true; + + listener.gotChoke(peer, choke); + + if (!choked && interesting) + request(); + } + + void interestedMessage(boolean interest) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " rcv " + (interest ? "" : "un") + + "interested", Snark.DEBUG); + interested = interest; + listener.gotInterest(peer, interest); + } + + void haveMessage(int piece) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " rcv have(" + piece + ")", Snark.DEBUG); + // Sanity check + if (piece < 0 || piece >= metainfo.getPieces()) + { + // XXX disconnect? + if (Snark.debug >= Snark.INFO) + Snark.debug("Got strange 'have: " + piece + "' message from " + peer, + + Snark.INFO); + return; + } + + synchronized(this) + { + // Can happen if the other side never send a bitfield message. + if (bitfield == null) + bitfield = new BitField(metainfo.getPieces()); + + bitfield.set(piece); + } + + if (listener.gotHave(peer, piece)) + setInteresting(true); + } + + void bitfieldMessage(byte[] bitmap) + { + synchronized(this) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " rcv bitfield", Snark.DEBUG); + if (bitfield != null) + { + // XXX - Be liberal in what you except? + if (Snark.debug >= Snark.INFO) + Snark.debug("Got unexpected bitfield message from " + peer, + Snark.INFO); + return; + } + + // XXX - Check for weird bitfield and disconnect? + bitfield = new BitField(bitmap, metainfo.getPieces()); + } + setInteresting(listener.gotBitField(peer, bitfield)); + } + + void requestMessage(int piece, int begin, int length) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " rcv request(" + + piece + ", " + begin + ", " + length + ") ", + Snark.DEBUG); + if (choking) + { + if (Snark.debug >= Snark.INFO) + Snark.debug("Request received, but choking " + peer, Snark.INFO); + return; + } + + // Sanity check + if (piece < 0 + || piece >= metainfo.getPieces() + || begin < 0 + || begin > metainfo.getPieceLength(piece) + || length <= 0 + || length > 4*PARTSIZE) + { + // XXX - Protocol error -> disconnect? + if (Snark.debug >= Snark.INFO) + Snark.debug("Got strange 'request: " + piece + + ", " + begin + + ", " + length + + "' message from " + peer, + Snark.INFO); + return; + } + + byte[] pieceBytes = listener.gotRequest(peer, piece); + if (pieceBytes == null) + { + // XXX - Protocol error-> diconnect? + if (Snark.debug >= Snark.INFO) + Snark.debug("Got request for unknown piece: " + piece, Snark.INFO); + return; + } + + // More sanity checks + if (begin >= pieceBytes.length || begin + length > pieceBytes.length) + { + // XXX - Protocol error-> disconnect? + if (Snark.debug >= Snark.INFO) + Snark.debug("Got out of range 'request: " + piece + + ", " + begin + + ", " + length + + "' message from " + peer, + Snark.INFO); + return; + } + + if (Snark.debug >= Snark.DEBUG) + Snark.debug("Sending (" + piece + ", " + begin + ", " + + length + ")" + " to " + peer, Snark.DEBUG); + out.sendPiece(piece, begin, length, pieceBytes); + + // Tell about last subpiece delivery. + if (begin + length == pieceBytes.length) + if (Snark.debug >= Snark.DEBUG) + Snark.debug("Send p" + piece + " " + peer, + Snark.DEBUG); + } + + /** + * Called when some bytes have left the outgoing connection. + * XXX - Should indicate whether it was a real piece or overhead. + */ + void uploaded(int size) + { + uploaded += size; + listener.uploaded(peer, size); + } + + /** + * Called when a partial piece request has been handled by + * PeerConnectionIn. + */ + void pieceMessage(Request req) + { + int size = req.len; + downloaded += size; + listener.downloaded(peer, size); + + // Last chunk needed for this piece? + if (getFirstOutstandingRequest(req.piece) == -1) + { + if (listener.gotPiece(peer, req.piece, req.bs)) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug("Got " + req.piece + ": " + peer, Snark.DEBUG); + } + else + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug("Got BAD " + req.piece + " from " + peer, + Snark.DEBUG); + // XXX ARGH What now !?! + downloaded = 0; + } + } + } + + synchronized private int getFirstOutstandingRequest(int piece) + { + for (int i = 0; i < outstandingRequests.size(); i++) + if (((Request)outstandingRequests.get(i)).piece == piece) + return i; + return -1; + } + + /** + * Called when a piece message is being processed by the incoming + * connection. Returns null when there was no such request. It also + * requeues/sends requests when it thinks that they must have been + * lost. + */ + Request getOutstandingRequest(int piece, int begin, int length) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug("getChunk(" + + piece + "," + begin + "," + length + ") " + + peer, Snark.DEBUG); + + int r = getFirstOutstandingRequest(piece); + + // Unrequested piece number? + if (r == -1) + { + if (Snark.debug >= Snark.INFO) + Snark.debug("Unrequested 'piece: " + piece + ", " + + begin + ", " + length + "' received from " + + peer, + Snark.INFO); + downloaded = 0; // XXX - punishment? + return null; + } + + // Lookup the correct piece chunk request from the list. + Request req; + synchronized(this) + { + req = (Request)outstandingRequests.get(r); + while (req.piece == piece && req.off != begin + && r < outstandingRequests.size() - 1) + { + r++; + req = (Request)outstandingRequests.get(r); + } + + // Something wrong? + if (req.piece != piece || req.off != begin || req.len != length) + { + if (Snark.debug >= Snark.INFO) + Snark.debug("Unrequested or unneeded 'piece: " + + piece + ", " + + begin + ", " + + length + "' received from " + + peer, + Snark.INFO); + downloaded = 0; // XXX - punishment? + return null; + } + + // Report missing requests. + if (r != 0) + { + if (Snark.debug >= Snark.INFO) + System.err.print("Some requests dropped, got " + req + + ", wanted:"); + for (int i = 0; i < r; i++) + { + Request dropReq = (Request)outstandingRequests.remove(0); + outstandingRequests.add(dropReq); + // We used to rerequest the missing chunks but that mostly + // just confuses the other side. So now we just keep + // waiting for them. They will be rerequested when we get + // choked/unchoked again. + /* + if (!choked) + out.sendRequest(dropReq); + */ + if (Snark.debug >= Snark.INFO) + System.err.print(" " + dropReq); + } + if (Snark.debug >= Snark.INFO) + System.err.println(" " + peer); + } + outstandingRequests.remove(0); + } + + // Request more if necessary to keep the pipeline filled. + addRequest(); + + return req; + + } + + void cancelMessage(int piece, int begin, int length) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug("Got cancel message (" + + piece + ", " + begin + ", " + length + ")", + Snark.DEBUG); + out.cancelRequest(piece, begin, length); + } + + void unknownMessage(int type, byte[] bs) + { + if (Snark.debug >= Snark.WARNING) + Snark.debug("Warning: Ignoring unknown message type: " + type + + " length: " + bs.length, Snark.WARNING); + } + + void havePiece(int piece) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug("Tell " + peer + " havePiece(" + piece + ")", Snark.DEBUG); + + synchronized(this) + { + // Tell the other side that we are no longer interested in any of + // the outstanding requests for this piece. + if (lastRequest != null && lastRequest.piece == piece) + lastRequest = null; + + Iterator it = outstandingRequests.iterator(); + while (it.hasNext()) + { + Request req = (Request)it.next(); + if (req.piece == piece) + { + it.remove(); + // Send cancel even when we are choked to make sure that it is + // really never ever send. + out.sendCancel(req); + } + } + } + + // Tell the other side that we really have this piece. + out.sendHave(piece); + + // Request something else if necessary. + addRequest(); + + synchronized(this) + { + // Is the peer still interesting? + if (lastRequest == null) + setInteresting(false); + } + } + + // Starts or resumes requesting pieces. + private void request() + { + // Are there outstanding requests that have to be resend? + if (resend) + { + out.sendRequests(outstandingRequests); + resend = false; + } + + // Add/Send some more requests if necessary. + addRequest(); + } + + /** + * Adds a new request to the outstanding requests list. + */ + private void addRequest() + { + boolean more_pieces = true; + while (more_pieces) + { + synchronized(this) + { + more_pieces = outstandingRequests.size() < MAX_PIPELINE; + } + + // We want something and we don't have outstanding requests? + if (more_pieces && lastRequest == null) + more_pieces = requestNextPiece(); + else if (more_pieces) // We want something + { + int pieceLength; + boolean isLastChunk; + synchronized(this) + { + pieceLength = metainfo.getPieceLength(lastRequest.piece); + isLastChunk = lastRequest.off + lastRequest.len == pieceLength; + } + + // Last part of a piece? + if (isLastChunk) + more_pieces = requestNextPiece(); + else + { + synchronized(this) + { + int nextPiece = lastRequest.piece; + int nextBegin = lastRequest.off + PARTSIZE; + byte[] bs = lastRequest.bs; + int maxLength = pieceLength - nextBegin; + int nextLength = maxLength > PARTSIZE ? PARTSIZE + : maxLength; + Request req + = new Request(nextPiece, bs, nextBegin, nextLength); + outstandingRequests.add(req); + if (!choked) + out.sendRequest(req); + lastRequest = req; + } + } + } + } + + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " requests " + outstandingRequests, Snark.DEBUG); + } + + // Starts requesting first chunk of next piece. Returns true if + // something has been added to the requests, false otherwise. + private boolean requestNextPiece() + { + // Check that we already know what the other side has. + if (bitfield != null) + { + int nextPiece = listener.wantPiece(peer, bitfield); + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " want piece " + nextPiece, Snark.DEBUG); + synchronized(this) + { + if (nextPiece != -1 + && (lastRequest == null || lastRequest.piece != nextPiece)) + { + int piece_length = metainfo.getPieceLength(nextPiece); + byte[] bs = new byte[piece_length]; + + int length = Math.min(piece_length, PARTSIZE); + Request req = new Request(nextPiece, bs, 0, length); + outstandingRequests.add(req); + if (!choked) + out.sendRequest(req); + lastRequest = req; + return true; + } + } + } + + return false; + } + + synchronized void setInteresting(boolean interest) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " setInteresting(" + interest + ")", Snark.DEBUG); + + if (interest != interesting) + { + interesting = interest; + out.sendInterest(interest); + + if (interesting && !choked) + request(); + } + } + + synchronized void setChoking(boolean choke) + { + if (Snark.debug >= Snark.DEBUG) + Snark.debug(peer + " setChoking(" + choke + ")", Snark.DEBUG); + + if (choking != choke) + { + choking = choke; + out.sendChoke(choke); + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/Request.java b/apps/i2psnark/java/src/org/klomp/snark/Request.java new file mode 100644 index 0000000000000000000000000000000000000000..2a07a4289315e93b4d2464c2be3b84d623b101d4 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/Request.java @@ -0,0 +1,73 @@ +/* Request - Holds all information needed for a (partial) piece request. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +/** + * Holds all information needed for a partial piece request. + */ +class Request +{ + final int piece; + final byte[] bs; + final int off; + final int len; + + /** + * Creates a new Request. + * + * @param piece Piece number requested. + * @param bs byte array where response should be stored. + * @param off the offset in the array. + * @param len the number of bytes requested. + */ + Request(int piece, byte[] bs, int off, int len) + { + this.piece = piece; + this.bs = bs; + this.off = off; + this.len = len; + + // Sanity check + if (piece < 0 || off < 0 || len <= 0 || off + len > bs.length) + throw new IndexOutOfBoundsException("Illegal Request " + toString()); + } + + public int hashCode() + { + return piece ^ off ^ len; + } + + public boolean equals(Object o) + { + if (o instanceof Request) + { + Request req = (Request)o; + return req.piece == piece && req.off == off && req.len == len; + } + + return false; + } + + public String toString() + { + return "(" + piece + "," + off + "," + len + ")"; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/ShutdownListener.java b/apps/i2psnark/java/src/org/klomp/snark/ShutdownListener.java new file mode 100644 index 0000000000000000000000000000000000000000..b64f75753849cf79c66d65755c4e74c1b80c2c1b --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/ShutdownListener.java @@ -0,0 +1,34 @@ +/* ShutdownListener - Callback for end of shutdown sequence + + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +/** + * Callback for end of shutdown sequence. + */ +interface ShutdownListener +{ + /** + * Called when the SnarkShutdown hook has finished shutting down all + * subcomponents. + */ + void shutdown(); +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java new file mode 100644 index 0000000000000000000000000000000000000000..b09a0d4eb44281ead1544b39bf9e1cd8a930d584 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -0,0 +1,598 @@ +/* Snark - Main snark program startup class. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.*; +import java.net.*; +import java.util.*; + +import org.klomp.snark.bencode.*; + +import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PServerSocket; + +/** + * Main Snark program startup class. + * + * @author Mark Wielaard (mark@klomp.org) + */ +public class Snark + implements StorageListener, CoordinatorListener, ShutdownListener +{ + private final static int MIN_PORT = 6881; + private final static int MAX_PORT = 6889; + + // Error messages (non-fatal) + public final static int ERROR = 1; + + // Warning messages + public final static int WARNING = 2; + + // Notices (peer level) + public final static int NOTICE = 3; + + // Info messages (protocol policy level) + public final static int INFO = 4; + + // Debug info (protocol level) + public final static int DEBUG = 5; + + // Very low level stuff (network level) + public final static int ALL = 6; + + /** + * What level of debug info to show. + */ + public static int debug = NOTICE; + + // Whether or not to ask the user for commands while sharing + private static boolean command_interpreter = true; + + private static final String newline = System.getProperty("line.separator"); + + private static final String copyright = + "The Hunting of the Snark Project - Copyright (C) 2003 Mark J. Wielaard" + + newline + newline + + "Snark comes with ABSOLUTELY NO WARRANTY. This is free software, and" + + newline + + "you are welcome to redistribute it under certain conditions; read the" + + newline + + "COPYING file for details." + newline + newline + + "This is the I2P port, allowing anonymous bittorrent (http://www.i2p.net/)" + newline + + "It will not work with normal torrents, so don't even try ;)"; + + private static final String usage = + "Press return for help. Type \"quit\" and return to stop."; + private static final String help = + "Commands: 'info', 'list', 'quit'."; + + // String indicating main activity + static String activity = "Not started"; + + public static void main(String[] args) + { + System.out.println(copyright); + System.out.println(); + + // Parse debug, share/ip and torrent file options. + Snark snark = parseArguments(args); + + SnarkShutdown snarkhook + = new SnarkShutdown(snark.storage, + snark.coordinator, + snark.acceptor, + snark.trackerclient, + snark); + Runtime.getRuntime().addShutdownHook(snarkhook); + + Timer timer = new Timer(true); + TimerTask monitor = new PeerMonitorTask(snark.coordinator); + timer.schedule(monitor, + PeerMonitorTask.MONITOR_PERIOD, + PeerMonitorTask.MONITOR_PERIOD); + + // Start command interpreter + if (Snark.command_interpreter) + { + boolean quit = false; + + System.out.println(); + System.out.println(usage); + System.out.println(); + + try + { + BufferedReader br = new BufferedReader + (new InputStreamReader(System.in)); + String line = br.readLine(); + while(!quit && line != null) + { + line = line.toLowerCase(); + if ("quit".equals(line)) + quit = true; + else if ("list".equals(line)) + { + synchronized(coordinator.peers) + { + System.out.println(coordinator.peers.size() + + " peers -" + + " (i)nterested," + + " (I)nteresting," + + " (c)hoking," + + " (C)hoked:"); + Iterator it = coordinator.peers.iterator(); + while (it.hasNext()) + { + Peer peer = (Peer)it.next(); + System.out.println(peer); + System.out.println("\ti: " + peer.isInterested() + + " I: " + peer.isInteresting() + + " c: " + peer.isChoking() + + " C: " + peer.isChoked()); + } + } + } + else if ("info".equals(line)) + { + System.out.println("Name: " + meta.getName()); + System.out.println("Torrent: " + torrent); + System.out.println("Tracker: " + meta.getAnnounce()); + List files = meta.getFiles(); + System.out.println("Files: " + + ((files == null) ? 1 : files.size())); + System.out.println("Pieces: " + meta.getPieces()); + System.out.println("Piece size: " + + meta.getPieceLength(0) / 1024 + + " KB"); + System.out.println("Total size: " + + meta.getTotalLength() / (1024 * 1024) + + " MB"); + } + else if ("".equals(line) || "help".equals(line)) + { + System.out.println(usage); + System.out.println(help); + } + else + { + System.out.println("Unknown command: " + line); + System.out.println(usage); + } + + if (!quit) + { + System.out.println(); + line = br.readLine(); + } + } + } + catch(IOException ioe) + { + debug("ERROR while reading stdin: " + ioe, ERROR); + } + + // Explicit shutdown. + Runtime.getRuntime().removeShutdownHook(snarkhook); + snarkhook.start(); + } + } + + static String torrent; + static MetaInfo meta; + static Storage storage; + static PeerCoordinator coordinator; + static ConnectionAcceptor acceptor; + static TrackerClient trackerclient; + + private Snark(String torrent, String ip, int user_port, + StorageListener slistener, CoordinatorListener clistener) + { + if (slistener == null) + slistener = this; + + if (clistener == null) + clistener = this; + + this.torrent = torrent; + + activity = "Network setup"; + + // "Taking Three as the subject to reason about-- + // A convenient number to state-- + // We add Seven, and Ten, and then multiply out + // By One Thousand diminished by Eight. + // + // "The result we proceed to divide, as you see, + // By Nine Hundred and Ninety Two: + // Then subtract Seventeen, and the answer must be + // Exactly and perfectly true. + + // Create a new ID and fill it with something random. First nine + // zeros bytes, then three bytes filled with snark and then + // sixteen random bytes. + byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17; + byte[] id = new byte[20]; + Random random = new Random(); + int i; + for (i = 0; i < 9; i++) + id[i] = 0; + id[i++] = snark; + id[i++] = snark; + id[i++] = snark; + while (i < 20) + id[i++] = (byte)random.nextInt(256); + + Snark.debug("My peer id: " + PeerID.idencode(id), Snark.INFO); + + int port; + IOException lastException = null; + boolean ok = I2PSnarkUtil.instance().connect(); + if (!ok) fatal("Unable to connect to I2P"); + I2PServerSocket serversocket = I2PSnarkUtil.instance().getServerSocket(); + if (serversocket == null) + fatal("Unable to listen for I2P connections"); + else + debug("Listening on I2P destination " + serversocket.getManager().getSession().getMyDestination().toBase64(), NOTICE); + + // Figure out what the torrent argument represents. + meta = null; + File f = null; + try + { + InputStream in = null; + f = new File(torrent); + if (f.exists()) + in = new FileInputStream(f); + else + { + activity = "Getting torrent"; + File torrentFile = I2PSnarkUtil.instance().get(torrent); + if (torrentFile == null) { + fatal("Unable to fetch " + torrent); + if (false) return; // never reached - fatal(..) throws + } else { + torrentFile.deleteOnExit(); + in = new FileInputStream(torrentFile); + } + } + meta = new MetaInfo(new BDecoder(in)); + } + catch(IOException ioe) + { + // OK, so it wasn't a torrent metainfo file. + if (f != null && f.exists()) + if (ip == null) + fatal("'" + torrent + "' exists," + + " but is not a valid torrent metainfo file." + + System.getProperty("line.separator"), ioe); + else + fatal("I2PSnark does not support creating and tracking a torrent at the moment"); + /* + { + // Try to create a new metainfo file + Snark.debug + ("Trying to create metainfo torrent for '" + torrent + "'", + NOTICE); + try + { + activity = "Creating torrent"; + storage = new Storage + (f, "http://" + ip + ":" + port + "/announce", slistener); + storage.create(); + meta = storage.getMetaInfo(); + } + catch (IOException ioe2) + { + fatal("Could not create torrent for '" + torrent + "'", ioe2); + } + } + */ + else + fatal("Cannot open '" + torrent + "'", ioe); + } + + debug(meta.toString(), INFO); + + // When the metainfo torrent was created from an existing file/dir + // it already exists. + if (storage == null) + { + try + { + activity = "Checking storage"; + storage = new Storage(meta, slistener); + storage.check(); + } + catch (IOException ioe) + { + fatal("Could not create storage", ioe); + } + } + + activity = "Collecting pieces"; + coordinator = new PeerCoordinator(id, meta, storage, clistener); + PeerAcceptor peeracceptor = new PeerAcceptor(coordinator); + ConnectionAcceptor acceptor = new ConnectionAcceptor(serversocket, + peeracceptor); + + trackerclient = new TrackerClient(meta, coordinator); + trackerclient.start(); + + } + + static Snark parseArguments(String[] args) + { + return parseArguments(args, null, null); + } + + /** + * Sets debug, ip and torrent variables then creates a Snark + * instance. Calls usage(), which terminates the program, if + * non-valid argument list. The given listeners will be + * passed to all components that take one. + */ + static Snark parseArguments(String[] args, + StorageListener slistener, + CoordinatorListener clistener) + { + int user_port = -1; + String ip = null; + String torrent = null; + + int i = 0; + while (i < args.length) + { + if (args[i].equals("--debug")) + { + debug = INFO; + i++; + + // Try if there is an level argument. + if (i < args.length) + { + try + { + int level = Integer.parseInt(args[i]); + if (level >= 0) + { + debug = level; + i++; + } + } + catch (NumberFormatException nfe) { } + } + } + else if (args[i].equals("--port")) + { + if (args.length - 1 < i + 1) + usage("--port needs port number to listen on"); + try + { + user_port = Integer.parseInt(args[i + 1]); + } + catch (NumberFormatException nfe) + { + usage("--port argument must be a number (" + nfe + ")"); + } + i += 2; + } + else if (args[i].equals("--no-commands")) + { + command_interpreter = false; + i++; + } + else if (args[i].equals("--eepproxy")) + { + String proxyHost = args[i+1]; + String proxyPort = args[i+2]; + I2PSnarkUtil.instance().setProxy(proxyHost, Integer.parseInt(proxyPort)); + i += 3; + } + else if (args[i].equals("--i2cp")) + { + String i2cpHost = args[i+1]; + String i2cpPort = args[i+2]; + Properties opts = null; + if (i+3 < args.length) { + if (!args[i+3].startsWith("--")) { + opts = new Properties(); + StringTokenizer tok = new StringTokenizer(args[i+3], " \t"); + while (tok.hasMoreTokens()) { + String str = tok.nextToken(); + int split = str.indexOf('='); + if (split > 0) { + opts.setProperty(str.substring(0, split), str.substring(split+1)); + } + } + } + } + I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, Integer.parseInt(i2cpPort), opts); + i += 3 + (opts != null ? 1 : 0); + } + else + { + torrent = args[i]; + i++; + break; + } + } + + if (torrent == null || i != args.length) + if (torrent != null && torrent.startsWith("-")) + usage("Unknow option '" + torrent + "'."); + else + usage("Need exactly one <url>, <file> or <dir>."); + + return new Snark(torrent, ip, user_port, slistener, clistener); + } + + private static void usage(String s) + { + System.out.println("snark: " + s); + usage(); + } + + private static void usage() + { + System.out.println + ("Usage: snark [--debug [level]] [--no-commands] [--port <port>]"); + System.out.println + (" [--eepproxy hostname portnum]"); + System.out.println + (" [--i2cp routerHost routerPort ['name=val name=val name=val']]"); + System.out.println + (" (<url>|<file>)"); + System.out.println + (" --debug\tShows some extra info and stacktraces"); + System.out.println + (" level\tHow much debug details to show"); + System.out.println + (" \t(defaults to " + + NOTICE + ", with --debug to " + + INFO + ", highest level is " + + ALL + ")."); + System.out.println + (" --no-commands\tDon't read interactive commands or show usage info."); + System.out.println + (" --port\tThe port to listen on for incomming connections"); + System.out.println + (" \t(if not given defaults to first free port between " + + MIN_PORT + "-" + MAX_PORT + ")."); + System.out.println + (" --share\tStart torrent tracker on <ip> address or <host> name."); + System.out.println + (" --eepproxy\thttp proxy to use (default of 127.0.0.1 port 4444)"); + System.out.println + (" --i2cp\tlocation of your I2P router (default of 127.0.0.1 port 7654)"); + System.out.println + (" \toptional settings may be included, such as"); + System.out.println + (" \tinbound.length=2 outbound.length=2 inbound.lengthVariance=-1 "); + System.out.println + (" <url> \tURL pointing to .torrent metainfo file to download/share."); + System.out.println + (" <file> \tEither a local .torrent metainfo file to download"); + System.out.println + (" \tor (with --share) a file to share."); + System.exit(-1); + } + + /** + * Aborts program abnormally. + */ + public static void fatal(String s) + { + fatal(s, null); + } + + /** + * Aborts program abnormally. + */ + public static void fatal(String s, Throwable t) + { + I2PSnarkUtil.instance().debug(s, ERROR, t); + //System.err.println("snark: " + s + ((t == null) ? "" : (": " + t))); + //if (debug >= INFO && t != null) + // t.printStackTrace(); + throw new RuntimeException("die bart die"); + } + + /** + * Show debug info if debug is true. + */ + public static void debug(String s, int level) + { + I2PSnarkUtil.instance().debug(s, level, null); + //if (debug >= level) + // System.out.println(s); + } + + public void peerChange(PeerCoordinator coordinator, Peer peer) + { + // System.out.println(peer.toString()); + } + + boolean allocating = false; + public void storageCreateFile(Storage storage, String name, long length) + { + if (allocating) + System.out.println(); // Done with last file. + + System.out.print("Creating file '" + name + + "' of length " + length + ": "); + allocating = true; + } + + // How much storage space has been allocated + private long allocated = 0; + + public void storageAllocated(Storage storage, long length) + { + allocating = true; + System.out.print("."); + allocated += length; + if (allocated == meta.getTotalLength()) + System.out.println(); // We have all the disk space we need. + } + + boolean allChecked = false; + boolean checking = false; + boolean prechecking = true; + public void storageChecked(Storage storage, int num, boolean checked) + { + allocating = false; + if (!allChecked && !checking) + { + // Use the MetaInfo from the storage since our own might not + // yet be setup correctly. + MetaInfo meta = storage.getMetaInfo(); + if (meta != null) + System.out.print("Checking existing " + + meta.getPieces() + + " pieces: "); + checking = true; + } + if (checking) + if (checked) + System.out.print("+"); + else + System.out.print("-"); + else + Snark.debug("Got " + (checked ? "" : "BAD ") + "piece: " + num, + Snark.INFO); + } + + public void storageAllChecked(Storage storage) + { + if (checking) + System.out.println(); + + allChecked = true; + checking = false; + } + + public void shutdown() + { + // Should not be necessary since all non-deamon threads should + // have died. But in reality this does not always happen. + System.exit(0); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java new file mode 100644 index 0000000000000000000000000000000000000000..2b58628bb7f6999b961fc2762d20d72c712e6c93 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java @@ -0,0 +1,89 @@ +/* TrackerShutdown - Makes sure everything ends correctly when shutting down. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.IOException; + +/** + * Makes sure everything ends correctly when shutting down. + */ +public class SnarkShutdown extends Thread +{ + private final Storage storage; + private final PeerCoordinator coordinator; + private final ConnectionAcceptor acceptor; + private final TrackerClient trackerclient; + + private final ShutdownListener listener; + + public SnarkShutdown(Storage storage, + PeerCoordinator coordinator, + ConnectionAcceptor acceptor, + TrackerClient trackerclient, + ShutdownListener listener) + { + this.storage = storage; + this.coordinator = coordinator; + this.acceptor = acceptor; + this.trackerclient = trackerclient; + this.listener = listener; + } + + public void run() + { + Snark.debug("Shutting down...", Snark.NOTICE); + + Snark.debug("Halting ConnectionAcceptor...", Snark.INFO); + if (acceptor != null) + acceptor.halt(); + + Snark.debug("Halting TrackerClient...", Snark.INFO); + if (trackerclient != null) + trackerclient.halt(); + + Snark.debug("Halting PeerCoordinator...", Snark.INFO); + if (coordinator != null) + coordinator.halt(); + + Snark.debug("Closing Storage...", Snark.INFO); + if (storage != null) + { + try + { + storage.close(); + } + catch(IOException ioe) + { + Snark.fatal("Couldn't properly close storage", ioe); + } + } + + // XXX - Should actually wait till done... + try + { + Snark.debug("Waiting 5 seconds...", Snark.INFO); + Thread.sleep(5*1000); + } + catch (InterruptedException ie) { /* ignored */ } + + listener.shutdown(); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java b/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java new file mode 100644 index 0000000000000000000000000000000000000000..22ef1c2a456b1f4b8907e254303497b3c0a28e39 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java @@ -0,0 +1,47 @@ +/* StaticSnark - Main snark startup class for staticly linking with gcj. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.security.Provider; +import java.security.Security; + +import org.klomp.snark.bencode.*; + +/** + * Main snark startup class for staticly linking with gcj. + * It references somee necessary classes that are normally loaded through + * reflection. + * + * @author Mark Wielaard (mark@klomp.org) + */ +public class StaticSnark +{ + public static void main(String[] args) + { + // The GNU security provider is needed for SHA-1 MessageDigest checking. + // So make sure it is available as a security provider. + //Provider gnu = new gnu.java.security.provider.Gnu(); + //Security.addProvider(gnu); + + // And finally call the normal starting point. + Snark.main(args); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java new file mode 100644 index 0000000000000000000000000000000000000000..287abf216bbb5e1a27b7873cbc7d051439dfcdff --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java @@ -0,0 +1,525 @@ +/* Storage - Class used to store and retrieve pieces. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.*; +import java.util.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Maintains pieces on disk. Can be used to store and retrieve pieces. + */ +public class Storage +{ + private MetaInfo metainfo; + private long[] lengths; + private RandomAccessFile[] rafs; + private String[] names; + + private final StorageListener listener; + + private final BitField bitfield; + private int needed; + + // XXX - Not always set correctly + int piece_size; + int pieces; + + /** The default piece size. */ + private static int MIN_PIECE_SIZE = 256*1024; + /** The maximum number of pieces in a torrent. */ + private static long MAX_PIECES = 100*1024/20; + + /** + * Creates a new storage based on the supplied MetaInfo. This will + * try to create and/or check all needed files in the MetaInfo. + * + * @exception IOException when creating and/or checking files fails. + */ + public Storage(MetaInfo metainfo, StorageListener listener) + throws IOException + { + this.metainfo = metainfo; + this.listener = listener; + needed = metainfo.getPieces(); + bitfield = new BitField(needed); + } + + /** + * Creates a storage from the existing file or directory together + * with an appropriate MetaInfo file as can be announced on the + * given announce String location. + */ + public Storage(File baseFile, String announce, StorageListener listener) + throws IOException + { + this.listener = listener; + + // Create names, rafs and lengths arrays. + getFiles(baseFile); + + long total = 0; + ArrayList lengthsList = new ArrayList(); + for (int i = 0; i < lengths.length; i++) + { + long length = lengths[i]; + total += length; + lengthsList.add(new Long(length)); + } + + piece_size = MIN_PIECE_SIZE; + pieces = (int) ((total - 1)/piece_size) + 1; + while (pieces > MAX_PIECES) + { + piece_size = piece_size*2; + pieces = (int) ((total - 1)/piece_size) +1; + } + + // Note that piece_hashes and the bitfield will be filled after + // the MetaInfo is created. + byte[] piece_hashes = new byte[20*pieces]; + bitfield = new BitField(pieces); + needed = 0; + + List files = new ArrayList(); + for (int i = 0; i < names.length; i++) + { + List file = new ArrayList(); + StringTokenizer st = new StringTokenizer(names[i], File.separator); + while (st.hasMoreTokens()) + { + String part = st.nextToken(); + file.add(part); + } + files.add(file); + } + + String name = baseFile.getName(); + if (files.size() == 1) + { + files = null; + lengthsList = null; + } + + // Note that the piece_hashes are not correctly setup yet. + metainfo = new MetaInfo(announce, baseFile.getName(), files, + lengthsList, piece_size, piece_hashes, total); + + } + + // Creates piece hases for a new storage. + public void create() throws IOException + { + // Calculate piece_hashes + MessageDigest digest = null; + try + { + digest = MessageDigest.getInstance("SHA"); + } + catch(NoSuchAlgorithmException nsa) + { + throw new InternalError(nsa.toString()); + } + + byte[] piece_hashes = metainfo.getPieceHashes(); + + byte[] piece = new byte[piece_size]; + for (int i = 0; i < pieces; i++) + { + int length = getUncheckedPiece(i, piece, 0); + digest.update(piece, 0, length); + byte[] hash = digest.digest(); + for (int j = 0; j < 20; j++) + piece_hashes[20 * i + j] = hash[j]; + + bitfield.set(i); + + if (listener != null) + listener.storageChecked(this, i, true); + } + + if (listener != null) + listener.storageAllChecked(this); + + // Reannounce to force recalculating the info_hash. + metainfo = metainfo.reannounce(metainfo.getAnnounce()); + } + + private void getFiles(File base) throws IOException + { + ArrayList files = new ArrayList(); + addFiles(files, base); + + int size = files.size(); + names = new String[size]; + lengths = new long[size]; + rafs = new RandomAccessFile[size]; + + int i = 0; + Iterator it = files.iterator(); + while (it.hasNext()) + { + File f = (File)it.next(); + names[i] = f.getPath(); + lengths[i] = f.length(); + rafs[i] = new RandomAccessFile(f, "r"); + i++; + } + } + + private static void addFiles(List l, File f) + { + if (!f.isDirectory()) + l.add(f); + else + { + File[] files = f.listFiles(); + if (files == null) + { + Snark.debug("WARNING: Skipping '" + f + + "' not a normal file.", Snark.WARNING); + return; + } + for (int i = 0; i < files.length; i++) + addFiles(l, files[i]); + } + } + + /** + * Returns the MetaInfo associated with this Storage. + */ + public MetaInfo getMetaInfo() + { + return metainfo; + } + + /** + * How many pieces are still missing from this storage. + */ + public int needed() + { + return needed; + } + + /** + * Whether or not this storage contains all pieces if the MetaInfo. + */ + public boolean complete() + { + return needed == 0; + } + + /** + * The BitField that tells which pieces this storage contains. + * Do not change this since this is the current state of the storage. + */ + public BitField getBitField() + { + return bitfield; + } + + /** + * Creates (and/or checks) all files from the metainfo file list. + */ + public void check() throws IOException + { + File base = new File(filterName(metainfo.getName())); + + List files = metainfo.getFiles(); + if (files == null) + { + // Create base as file. + Snark.debug("Creating/Checking file: " + base, Snark.NOTICE); + if (!base.createNewFile() && !base.exists()) + throw new IOException("Could not create file " + base); + + lengths = new long[1]; + rafs = new RandomAccessFile[1]; + names = new String[1]; + lengths[0] = metainfo.getTotalLength(); + rafs[0] = new RandomAccessFile(base, "rw"); + names[0] = base.getName(); + } + else + { + // Create base as dir. + Snark.debug("Creating/Checking directory: " + base, Snark.NOTICE); + if (!base.mkdir() && !base.isDirectory()) + throw new IOException("Could not create directory " + base); + + List ls = metainfo.getLengths(); + int size = files.size(); + long total = 0; + lengths = new long[size]; + rafs = new RandomAccessFile[size]; + names = new String[size]; + for (int i = 0; i < size; i++) + { + File f = createFileFromNames(base, (List)files.get(i)); + lengths[i] = ((Long)ls.get(i)).longValue(); + total += lengths[i]; + rafs[i] = new RandomAccessFile(f, "rw"); + names[i] = f.getName(); + } + + // Sanity check for metainfo file. + long metalength = metainfo.getTotalLength(); + if (total != metalength) + throw new IOException("File lengths do not add up " + + total + " != " + metalength); + } + checkCreateFiles(); + } + + /** + * Removes 'suspicious' characters from the give file name. + */ + private String filterName(String name) + { + // XXX - Is this enough? + return name.replace(File.separatorChar, '_'); + } + + private File createFileFromNames(File base, List names) throws IOException + { + File f = null; + Iterator it = names.iterator(); + while (it.hasNext()) + { + String name = filterName((String)it.next()); + if (it.hasNext()) + { + // Another dir in the hierarchy. + f = new File(base, name); + if (!f.mkdir() && !f.isDirectory()) + throw new IOException("Could not create directory " + f); + base = f; + } + else + { + // The final element (file) in the hierarchy. + f = new File(base, name); + if (!f.createNewFile() && !f.exists()) + throw new IOException("Could not create file " + f); + } + } + return f; + } + + private void checkCreateFiles() throws IOException + { + // Whether we are resuming or not, + // if any of the files already exists we assume we are resuming. + boolean resume = false; + + // Make sure all files are available and of correct length + for (int i = 0; i < rafs.length; i++) + { + long length = rafs[i].length(); + if(length == lengths[i]) + { + if (listener != null) + listener.storageAllocated(this, length); + resume = true; // XXX Could dynamicly check + } + else if (length == 0) + allocateFile(i); + else + throw new IOException("File '" + names[i] + + "' exists, but has wrong length"); + } + + // Check which pieces match and which don't + if (resume) + { + pieces = metainfo.getPieces(); + byte[] piece = new byte[metainfo.getPieceLength(0)]; + for (int i = 0; i < pieces; i++) + { + int length = getUncheckedPiece(i, piece, 0); + boolean correctHash = metainfo.checkPiece(i, piece, 0, length); + if (correctHash) + { + bitfield.set(i); + needed--; + } + + if (listener != null) + listener.storageChecked(this, i, correctHash); + } + } + + if (listener != null) + listener.storageAllChecked(this); + } + + private void allocateFile(int nr) throws IOException + { + // XXX - Is this the best way to make sure we have enough space for + // the whole file? + listener.storageCreateFile(this, names[nr], lengths[nr]); + final int ZEROBLOCKSIZE = metainfo.getPieceLength(0); + byte[] zeros = new byte[ZEROBLOCKSIZE]; + int i; + for (i = 0; i < lengths[nr]/ZEROBLOCKSIZE; i++) + { + rafs[nr].write(zeros); + if (listener != null) + listener.storageAllocated(this, ZEROBLOCKSIZE); + } + int size = (int)(lengths[nr] - i*ZEROBLOCKSIZE); + rafs[nr].write(zeros, 0, size); + if (listener != null) + listener.storageAllocated(this, size); + } + + + /** + * Closes the Storage and makes sure that all RandomAccessFiles are + * closed. The Storage is unusable after this. + */ + public void close() throws IOException + { + for (int i = 0; i < rafs.length; i++) + { + synchronized(rafs[i]) + { + rafs[i].close(); + } + } + } + + /** + * Returns a byte array containing the requested piece or null if + * the storage doesn't contain the piece yet. + */ + public byte[] getPiece(int piece) throws IOException + { + if (!bitfield.get(piece)) + return null; + + byte[] bs = new byte[metainfo.getPieceLength(piece)]; + getUncheckedPiece(piece, bs, 0); + return bs; + } + + /** + * Put the piece in the Storage if it is correct. + * + * @return true if the piece was correct (sha metainfo hash + * matches), otherwise false. + * @exception IOException when some storage related error occurs. + */ + public boolean putPiece(int piece, byte[] bs) throws IOException + { + // First check if the piece is correct. + // If we were paranoia we could copy the array first. + int length = bs.length; + boolean correctHash = metainfo.checkPiece(piece, bs, 0, length); + if (listener != null) + listener.storageChecked(this, piece, correctHash); + if (!correctHash) + return false; + + boolean complete; + synchronized(bitfield) + { + if (bitfield.get(piece)) + return true; // No need to store twice. + else + { + bitfield.set(piece); + needed--; + complete = needed == 0; + } + } + + long start = piece * metainfo.getPieceLength(0); + int i = 0; + long raflen = lengths[i]; + while (start > raflen) + { + i++; + start -= raflen; + raflen = lengths[i]; + } + + int written = 0; + int off = 0; + while (written < length) + { + int need = length - written; + int len = (start + need < raflen) ? need : (int)(raflen - start); + synchronized(rafs[i]) + { + rafs[i].seek(start); + rafs[i].write(bs, off + written, len); + } + written += len; + if (need - len > 0) + { + i++; + raflen = lengths[i]; + start = 0; + } + } + + return true; + } + + private int getUncheckedPiece(int piece, byte[] bs, int off) + throws IOException + { + // XXX - copy/paste code from putPiece(). + long start = piece * metainfo.getPieceLength(0); + int length = metainfo.getPieceLength(piece); + int i = 0; + long raflen = lengths[i]; + while (start > raflen) + { + i++; + start -= raflen; + raflen = lengths[i]; + } + + int read = 0; + while (read < length) + { + int need = length - read; + int len = (start + need < raflen) ? need : (int)(raflen - start); + synchronized(rafs[i]) + { + rafs[i].seek(start); + rafs[i].readFully(bs, off + read, len); + } + read += len; + if (need - len > 0) + { + i++; + raflen = lengths[i]; + start = 0; + } + } + + return length; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java b/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java new file mode 100644 index 0000000000000000000000000000000000000000..1cb5c119396983f6123e192b4ffddfee2e9817a0 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java @@ -0,0 +1,52 @@ +/* StorageListener.java - Interface used as callback when storage changes. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +/** + * Callback used when Storage changes. + */ +public interface StorageListener +{ + /** + * Called when the storage creates a new file of a given length. + */ + void storageCreateFile(Storage storage, String name, long length); + + /** + * Called to indicate that length bytes have been allocated. + */ + void storageAllocated(Storage storage, long length); + + /** + * Called when storage is being checked and the num piece of that + * total pieces has been checked. When the piece hash matches the + * expected piece hash checked will be true, otherwise it will be + * false. + */ + void storageChecked(Storage storage, int num, boolean checked); + + /** + * Called when all pieces in the storage have been checked. Does not + * mean that the storage is complete, just that the state of the + * storage is known. + */ + void storageAllChecked(Storage storage); +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java new file mode 100644 index 0000000000000000000000000000000000000000..49939311e1f1d12b7626e8d6fc6b80f956e69528 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -0,0 +1,254 @@ +/* TrackerClient - Class that informs a tracker and gets new peers. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.*; +import java.net.*; +import java.util.*; + +import org.klomp.snark.bencode.*; + +/** + * Informs metainfo tracker of events and gets new peers for peer + * coordinator. + * + * @author Mark Wielaard (mark@klomp.org) + */ +public class TrackerClient extends Thread +{ + private static final String NO_EVENT = ""; + private static final String STARTED_EVENT = "started"; + private static final String COMPLETED_EVENT = "completed"; + private static final String STOPPED_EVENT = "stopped"; + + private final static int SLEEP = 5; // 5 minutes. + + private final MetaInfo meta; + private final PeerCoordinator coordinator; + private final int port; + + private boolean stop; + + private long interval; + private long lastRequestTime; + + public TrackerClient(MetaInfo meta, PeerCoordinator coordinator) + { + // Set unique name. + super("TrackerClient-" + urlencode(coordinator.getID())); + this.meta = meta; + this.coordinator = coordinator; + + this.port = 6881; //(port == -1) ? 9 : port; + + stop = false; + } + + /** + * Interrupts this Thread to stop it. + */ + public void halt() + { + stop = true; + this.interrupt(); + } + + public void run() + { + // XXX - Support other IPs + String announce = I2PSnarkUtil.instance().rewriteAnnounce(meta.getAnnounce()); + String infoHash = urlencode(meta.getInfoHash()); + String peerID = urlencode(coordinator.getID()); + + long uploaded = coordinator.getUploaded(); + long downloaded = coordinator.getDownloaded(); + long left = coordinator.getLeft(); + + boolean completed = (left == 0); + + try + { + boolean started = false; + while (!started) + { + try + { + // Send start. + TrackerInfo info = doRequest(announce, infoHash, peerID, + uploaded, downloaded, left, + STARTED_EVENT); + Iterator it = info.getPeers().iterator(); + while (it.hasNext()) + coordinator.addPeer((Peer)it.next()); + started = true; + } + catch (IOException ioe) + { + // Probably not fatal (if it doesn't last to long...) + Snark.debug + ("WARNING: Could not contact tracker at '" + + announce + "': " + ioe, Snark.WARNING); + } + + if (!started && !stop) + { + Snark.debug(" Retrying in one minute...", Snark.DEBUG); + try + { + // Sleep one minutes... + Thread.sleep(60*1000); + } + catch(InterruptedException interrupt) + { + // ignore + } + } + } + + while(!stop) + { + try + { + // Sleep some minutes... + Thread.sleep(SLEEP*60*1000); + } + catch(InterruptedException interrupt) + { + // ignore + } + + if (stop) + break; + + uploaded = coordinator.getUploaded(); + downloaded = coordinator.getDownloaded(); + left = coordinator.getLeft(); + + // First time we got a complete download? + String event; + if (!completed && left == 0) + { + completed = true; + event = COMPLETED_EVENT; + } + else + event = NO_EVENT; + + // Only do a request when necessary. + if (event == COMPLETED_EVENT + || coordinator.needPeers() + || System.currentTimeMillis() > lastRequestTime + interval) + { + try + { + TrackerInfo info = doRequest(announce, infoHash, peerID, + uploaded, downloaded, left, + event); + + Iterator it = info.getPeers().iterator(); + while (it.hasNext()) + coordinator.addPeer((Peer)it.next()); + } + catch (IOException ioe) + { + // Probably not fatal (if it doesn't last to long...) + Snark.debug + ("WARNING: Could not contact tracker at '" + + announce + "': " + ioe, Snark.WARNING); + } + } + } + } + catch (Throwable t) + { + Snark.debug("TrackerClient: " + t, Snark.ERROR); + t.printStackTrace(); + } + finally + { + try + { + TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded, + downloaded, left, STOPPED_EVENT); + } + catch(IOException ioe) { /* ignored */ } + } + + } + + private TrackerInfo doRequest(String announce, String infoHash, + String peerID, long uploaded, + long downloaded, long left, String event) + throws IOException + { + String s = announce + + "?info_hash=" + infoHash + + "&peer_id=" + peerID + + "&port=" + port + + "&ip=" + I2PSnarkUtil.instance().getOurIPString() + + "&uploaded=" + uploaded + + "&downloaded=" + downloaded + + "&left=" + left + + ((event != NO_EVENT) ? ("&event=" + event) : ""); + if (Snark.debug >= Snark.INFO) + Snark.debug("Sending TrackerClient request: " + s, Snark.INFO); + + File fetched = I2PSnarkUtil.instance().get(s); + if (fetched == null) { + throw new IOException("Error fetching " + s); + } + + fetched.deleteOnExit(); + InputStream in = new FileInputStream(fetched); + + TrackerInfo info = new TrackerInfo(in, coordinator.getID(), + coordinator.getMetaInfo()); + if (Snark.debug >= Snark.INFO) + Snark.debug("TrackerClient response: " + info, Snark.INFO); + lastRequestTime = System.currentTimeMillis(); + + String failure = info.getFailureReason(); + if (failure != null) + throw new IOException(failure); + + interval = info.getInterval() * 1000; + return info; + } + + /** + * Very lazy byte[] to URL encoder. Just encodes everything, even + * "normal" chars. + */ + static String urlencode(byte[] bs) + { + StringBuffer sb = new StringBuffer(bs.length*3); + for (int i = 0; i < bs.length; i++) + { + int c = bs[i] & 0xFF; + sb.append('%'); + if (c < 16) + sb.append('0'); + sb.append(Integer.toHexString(c)); + } + + return sb.toString(); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..6d464f87b016a8a934783a8bc52b43854421ab66 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java @@ -0,0 +1,128 @@ +/* TrackerInfo - Holds information returned by a tracker, mainly the peer list. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.HashSet; + +import org.klomp.snark.bencode.*; + +public class TrackerInfo +{ + private final String failure_reason; + private final int interval; + private final Set peers; + + public TrackerInfo(InputStream in, byte[] my_id, MetaInfo metainfo) + throws IOException + { + this(new BDecoder(in), my_id, metainfo); + } + + public TrackerInfo(BDecoder be, byte[] my_id, MetaInfo metainfo) + throws IOException + { + this(be.bdecodeMap().getMap(), my_id, metainfo); + } + + public TrackerInfo(Map m, byte[] my_id, MetaInfo metainfo) + throws IOException + { + BEValue reason = (BEValue)m.get("failure reason"); + if (reason != null) + { + failure_reason = reason.getString(); + interval = -1; + peers = null; + } + else + { + failure_reason = null; + BEValue beInterval = (BEValue)m.get("interval"); + if (beInterval == null) + throw new InvalidBEncodingException("No interval given"); + else + interval = beInterval.getInt(); + BEValue bePeers = (BEValue)m.get("peers"); + if (bePeers == null) + throw new InvalidBEncodingException("No peer list"); + else + peers = getPeers(bePeers.getList(), my_id, metainfo); + } + } + + public static Set getPeers(InputStream in, byte[] my_id, MetaInfo metainfo) + throws IOException + { + return getPeers(new BDecoder(in), my_id, metainfo); + } + + public static Set getPeers(BDecoder be, byte[] my_id, MetaInfo metainfo) + throws IOException + { + return getPeers(be.bdecodeList().getList(), my_id, metainfo); + } + + public static Set getPeers(List l, byte[] my_id, MetaInfo metainfo) + throws IOException + { + Set peers = new HashSet(l.size()); + + Iterator it = l.iterator(); + while (it.hasNext()) + { + PeerID peerID = new PeerID(((BEValue)it.next()).getMap()); + peers.add(new Peer(peerID, my_id, metainfo)); + } + + return peers; + } + + public Set getPeers() + { + return peers; + } + + public String getFailureReason() + { + return failure_reason; + } + + public int getInterval() + { + return interval; + } + + public String toString() + { + if (failure_reason != null) + return "TrackerInfo[FAILED: " + failure_reason + "]"; + else + return "TrackerInfo[interval=" + interval + + ", peers=" + peers + "]"; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BDecoder.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BDecoder.java new file mode 100644 index 0000000000000000000000000000000000000000..87b3bf4c547862de8009b70c1835ebb2c2202390 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BDecoder.java @@ -0,0 +1,355 @@ +/* BDecoder - Converts an InputStream to BEValues. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark.bencode; + +import java.io.IOException; +import java.io.InputStream; +import java.io.EOFException; +import java.io.UnsupportedEncodingException; + +import java.math.BigInteger; + +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Decodes a bencoded stream to <code>BEValue</code>s. + * + * A bencoded byte stream can represent byte arrays, numbers, lists and + * maps (dictionaries). + * + * It currently contains a hack to indicate a name of a dictionary of + * which a SHA-1 digest hash should be calculated (the hash over the + * original bencoded bytes). + * + * @author Mark Wielaard (mark@klomp.org). + */ +public class BDecoder +{ + // The InputStream to BDecode. + private final InputStream in; + + // The last indicator read. + // Zero if unknown. + // '0'..'9' indicates a byte[]. + // 'i' indicates an Number. + // 'l' indicates a List. + // 'd' indicates a Map. + // 'e' indicates end of Number, List or Map (only used internally). + // -1 indicates end of stream. + // Call getNextIndicator to get the current value (will never return zero). + private int indicator = 0; + + // Used for ugly hack to get SHA hash over the metainfo info map + private String special_map = "info"; + private boolean in_special_map = false; + private final MessageDigest sha_digest; + + // Ugly hack. Return the SHA has over bytes that make up the special map. + public byte[] get_special_map_digest() + { + byte[] result = sha_digest.digest(); + return result; + } + + // Ugly hack. Name defaults to "info". + public void set_special_map_name(String name) + { + special_map = name; + } + + /** + * Initalizes a new BDecoder. Nothing is read from the given + * <code>InputStream</code> yet. + */ + public BDecoder(InputStream in) + { + this.in = in; + // XXX - Used for ugly hack. + try + { + sha_digest = MessageDigest.getInstance("SHA"); + } + catch(NoSuchAlgorithmException nsa) + { + throw new InternalError(nsa.toString()); + } + } + + /** + * Creates a new BDecoder and immediatly decodes the first value it + * sees. + * + * @return The first BEValue on the stream or null when the stream + * has ended. + * + * @exception InvalidBEncoding when the stream doesn't start with a + * bencoded value or the stream isn't a bencoded stream at all. + * @exception IOException when somthing bad happens with the stream + * to read from. + */ + public static BEValue bdecode(InputStream in) throws IOException + { + return new BDecoder(in).bdecode(); + } + + /** + * Returns what the next bencoded object will be on the stream or -1 + * when the end of stream has been reached. Can return something + * unexpected (not '0' .. '9', 'i', 'l' or 'd') when the stream + * isn't bencoded. + * + * This might or might not read one extra byte from the stream. + */ + public int getNextIndicator() throws IOException + { + if (indicator == 0) + { + indicator = in.read(); + // XXX - Used for ugly hack + if (in_special_map) sha_digest.update((byte)indicator); + } + return indicator; + } + + /** + * Gets the next indicator and returns either null when the stream + * has ended or bdecodes the rest of the stream and returns the + * appropriate BEValue encoded object. + */ + public BEValue bdecode() throws IOException + { + indicator = getNextIndicator(); + if (indicator == -1) + return null; + + if (indicator >= '0' && indicator <= '9') + return bdecodeBytes(); + else if (indicator == 'i') + return bdecodeNumber(); + else if (indicator == 'l') + return bdecodeList(); + else if (indicator == 'd') + return bdecodeMap(); + else + throw new InvalidBEncodingException + ("Unknown indicator '" + indicator + "'"); + } + + /** + * Returns the next bencoded value on the stream and makes sure it + * is a byte array. If it is not a bencoded byte array it will throw + * InvalidBEncodingException. + */ + public BEValue bdecodeBytes() throws IOException + { + int c = getNextIndicator(); + int num = c - '0'; + if (num < 0 || num > 9) + throw new InvalidBEncodingException("Number expected, not '" + + (char)c + "'"); + indicator = 0; + + c = read(); + int i = c - '0'; + while (i >= 0 && i <= 9) + { + // XXX - This can overflow! + num = num*10 + i; + c = read(); + i = c - '0'; + } + + if (c != ':') + throw new InvalidBEncodingException("Colon expected, not '" + + (char)c + "'"); + + return new BEValue(read(num)); + } + + /** + * Returns the next bencoded value on the stream and makes sure it + * is a number. If it is not a number it will throw + * InvalidBEncodingException. + */ + public BEValue bdecodeNumber() throws IOException + { + int c = getNextIndicator(); + if (c != 'i') + throw new InvalidBEncodingException("Expected 'i', not '" + + (char)c + "'"); + indicator = 0; + + c = read(); + if (c == '0') + { + c = read(); + if (c == 'e') + return new BEValue(BigInteger.ZERO); + else + throw new InvalidBEncodingException("'e' expected after zero," + + " not '" + (char)c + "'"); + } + + // XXX - We don't support more the 255 char big integers + char[] chars = new char[256]; + int off = 0; + + if (c == '-') + { + c = read(); + if (c == '0') + throw new InvalidBEncodingException("Negative zero not allowed"); + chars[off] = (char)c; + off++; + } + + if (c < '1' || c > '9') + throw new InvalidBEncodingException("Invalid Integer start '" + + (char)c + "'"); + chars[off] = (char)c; + off++; + + c = read(); + int i = c - '0'; + while(i >= 0 && i <= 9) + { + chars[off] = (char)c; + off++; + c = read(); + i = c - '0'; + } + + if (c != 'e') + throw new InvalidBEncodingException("Integer should end with 'e'"); + + String s = new String(chars, 0, off); + return new BEValue(new BigInteger(s)); + } + + /** + * Returns the next bencoded value on the stream and makes sure it + * is a list. If it is not a list it will throw + * InvalidBEncodingException. + */ + public BEValue bdecodeList() throws IOException + { + int c = getNextIndicator(); + if (c != 'l') + throw new InvalidBEncodingException("Expected 'l', not '" + + (char)c + "'"); + indicator = 0; + + List result = new ArrayList(); + c = getNextIndicator(); + while (c != 'e') + { + result.add(bdecode()); + c = getNextIndicator(); + } + indicator = 0; + + return new BEValue(result); + } + + /** + * Returns the next bencoded value on the stream and makes sure it + * is a map (dictonary). If it is not a map it will throw + * InvalidBEncodingException. + */ + public BEValue bdecodeMap() throws IOException + { + int c = getNextIndicator(); + if (c != 'd') + throw new InvalidBEncodingException("Expected 'd', not '" + + (char)c + "'"); + indicator = 0; + + Map result = new HashMap(); + c = getNextIndicator(); + while (c != 'e') + { + // Dictonary keys are always strings. + String key = bdecode().getString(); + + // XXX ugly hack + boolean special = special_map.equals(key); + if (special) + in_special_map = true; + + BEValue value = bdecode(); + result.put(key, value); + + // XXX ugly hack continued + if (special) + in_special_map = false; + + c = getNextIndicator(); + } + indicator = 0; + + return new BEValue(result); + } + + /** + * Returns the next byte read from the InputStream (as int). + * Throws EOFException if InputStream.read() returned -1. + */ + private int read() throws IOException + { + int c = in.read(); + if (c == -1) + throw new EOFException(); + if (in_special_map) sha_digest.update((byte)c); + return c; + } + + /** + * Returns a byte[] containing length valid bytes starting at offset + * zero. Throws EOFException if InputStream.read() returned -1 + * before all requested bytes could be read. Note that the byte[] + * returned might be bigger then requested but will only contain + * length valid bytes. The returned byte[] will be reused when this + * method is called again. + */ + private byte[] read(int length) throws IOException + { + byte[] result = new byte[length]; + + int read = 0; + while (read < length) + { + int i = in.read(result, read, length - read); + if (i == -1) + throw new EOFException(); + read += i; + } + + if (in_special_map) sha_digest.update(result, 0, length); + return result; + } + +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java new file mode 100644 index 0000000000000000000000000000000000000000..cad99ed70d82df03cc99f1466afb9b17b1f8accd --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java @@ -0,0 +1,190 @@ +/* BEValue - Holds different types that a bencoded byte array can represent. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark.bencode; + +import java.io.UnsupportedEncodingException; + +import java.util.List; +import java.util.Map; + +/** + * Holds different types that a bencoded byte array can represent. + * You need to call the correct get method to get the correct java + * type object. If the BEValue wasn't actually of the requested type + * you will get a InvalidBEncodingException. + * + * @author Mark Wielaard (mark@klomp.org) + */ +public class BEValue +{ + // This is either a byte[], Number, List or Map. + private final Object value; + + public BEValue(byte[] value) + { + this.value = value; + } + + public BEValue(Number value) + { + this.value = value; + } + + public BEValue(List value) + { + this.value = value; + } + + public BEValue(Map value) + { + this.value = value; + } + + /** + * Returns this BEValue as a String. This operation only succeeds + * when the BEValue is a byte[], otherwise it will throw a + * InvalidBEncodingException. The byte[] will be interpreted as + * UTF-8 encoded characters. + */ + public String getString() throws InvalidBEncodingException + { + try + { + return new String(getBytes(), "UTF-8"); + } + catch (ClassCastException cce) + { + throw new InvalidBEncodingException(cce.toString()); + } + catch (UnsupportedEncodingException uee) + { + throw new InternalError(uee.toString()); + } + } + + /** + * Returns this BEValue as a byte[]. This operation only succeeds + * when the BEValue is actually a byte[], otherwise it will throw a + * InvalidBEncodingException. + */ + public byte[] getBytes() throws InvalidBEncodingException + { + try + { + return (byte[])value; + } + catch (ClassCastException cce) + { + throw new InvalidBEncodingException(cce.toString()); + } + } + + /** + * Returns this BEValue as a Number. This operation only succeeds + * when the BEValue is actually a Number, otherwise it will throw a + * InvalidBEncodingException. + */ + public Number getNumber() throws InvalidBEncodingException + { + try + { + return (Number)value; + } + catch (ClassCastException cce) + { + throw new InvalidBEncodingException(cce.toString()); + } + } + + /** + * Returns this BEValue as int. This operation only succeeds when + * the BEValue is actually a Number, otherwise it will throw a + * InvalidBEncodingException. The returned int is the result of + * <code>Number.intValue()</code>. + */ + public int getInt() throws InvalidBEncodingException + { + return getNumber().intValue(); + } + + /** + * Returns this BEValue as long. This operation only succeeds when + * the BEValue is actually a Number, otherwise it will throw a + * InvalidBEncodingException. The returned long is the result of + * <code>Number.longValue()</code>. + */ + public long getLong() throws InvalidBEncodingException + { + return getNumber().longValue(); + } + + /** + * Returns this BEValue as a List of BEValues. This operation only + * succeeds when the BEValue is actually a List, otherwise it will + * throw a InvalidBEncodingException. + */ + public List getList() throws InvalidBEncodingException + { + try + { + return (List)value; + } + catch (ClassCastException cce) + { + throw new InvalidBEncodingException(cce.toString()); + } + } + + /** + * Returns this BEValue as a Map of BEValue keys and BEValue + * values. This operation only succeeds when the BEValue is actually + * a Map, otherwise it will throw a InvalidBEncodingException. + */ + public Map getMap() throws InvalidBEncodingException + { + try + { + return (Map)value; + } + catch (ClassCastException cce) + { + throw new InvalidBEncodingException(cce.toString()); + } + } + + public String toString() + { + String valueString; + if (value instanceof byte[]) + { + byte[] bs = (byte[])value; + // XXX - Stupid heuristic... + if (bs.length <= 12) + valueString = new String(bs); + else + valueString = "bytes:" + bs.length; + } + else + valueString = value.toString(); + + return "BEValue[" + valueString + "]"; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java new file mode 100644 index 0000000000000000000000000000000000000000..e17a36d92ff82b3257d5d3d26e7a29976d165596 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java @@ -0,0 +1,191 @@ +/* BDecoder - Converts an InputStream to BEValues. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark.bencode; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class BEncoder +{ + + public static byte[] bencode(Object o) throws IllegalArgumentException + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bencode(o, baos); + return baos.toByteArray(); + } + catch (IOException ioe) + { + throw new InternalError(ioe.toString()); + } + } + + public static void bencode(Object o, OutputStream out) + throws IOException, IllegalArgumentException + { + if (o instanceof String) + bencode((String)o, out); + else if (o instanceof byte[]) + bencode((byte[])o, out); + else if (o instanceof Number) + bencode((Number)o, out); + else if (o instanceof List) + bencode((List)o, out); + else if (o instanceof Map) + bencode((Map)o, out); + else + throw new IllegalArgumentException("Cannot bencode: " + o.getClass()); + } + + public static byte[] bencode(String s) + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bencode(s, baos); + return baos.toByteArray(); + } + catch (IOException ioe) + { + throw new InternalError(ioe.toString()); + } + } + + public static void bencode(String s, OutputStream out) throws IOException + { + byte[] bs = s.getBytes("UTF-8"); + bencode(bs, out); + } + + public static byte[] bencode(Number n) + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bencode(n, baos); + return baos.toByteArray(); + } + catch (IOException ioe) + { + throw new InternalError(ioe.toString()); + } + } + + public static void bencode(Number n, OutputStream out) throws IOException + { + out.write('i'); + String s = n.toString(); + out.write(s.getBytes("UTF-8")); + out.write('e'); + } + + public static byte[] bencode(List l) + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bencode(l, baos); + return baos.toByteArray(); + } + catch (IOException ioe) + { + throw new InternalError(ioe.toString()); + } + } + + public static void bencode(List l, OutputStream out) throws IOException + { + out.write('l'); + Iterator it = l.iterator(); + while (it.hasNext()) + bencode(it.next(), out); + out.write('e'); + } + + public static byte[] bencode(byte[] bs) + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bencode(bs, baos); + return baos.toByteArray(); + } + catch (IOException ioe) + { + throw new InternalError(ioe.toString()); + } + } + + public static void bencode(byte[] bs, OutputStream out) throws IOException + { + String l = Integer.toString(bs.length); + out.write(l.getBytes("UTF-8")); + out.write(':'); + out.write(bs); + } + + public static byte[] bencode(Map m) + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bencode(m, baos); + return baos.toByteArray(); + } + catch (IOException ioe) + { + throw new InternalError(ioe.toString()); + } + } + + public static void bencode(Map m, OutputStream out) throws IOException + { + out.write('d'); + + // Keys must be sorted. XXX - But is this the correct order? + Set s = m.keySet(); + List l = new ArrayList(s); + Collections.sort(l); + + Iterator it = l.iterator(); + while(it.hasNext()) + { + // Keys must be Strings. + String key = (String)it.next(); + Object value = m.get(key); + bencode(key, out); + bencode(value, out); + } + + out.write('e'); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/InvalidBEncodingException.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/InvalidBEncodingException.java new file mode 100644 index 0000000000000000000000000000000000000000..1c4552944ce24026cfd6d1c2a7ae164a54feacd6 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/InvalidBEncodingException.java @@ -0,0 +1,36 @@ +/* InvalidBEncodingException - Thrown when a bencoded stream is corrupted. + Copyright (C) 2003 Mark J. Wielaard + + This file is part of Snark. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +package org.klomp.snark.bencode; + +import java.io.IOException; + +/** + * Exception thrown when a bencoded stream is corrupted. + * + * @author Mark Wielaard (mark@klomp.org) + */ +public class InvalidBEncodingException extends IOException +{ + public InvalidBEncodingException(String message) + { + super(message); + } +} diff --git a/apps/i2psnark/readme.txt b/apps/i2psnark/readme.txt new file mode 100644 index 0000000000000000000000000000000000000000..3970e3cba14d7a58ed94d2a16f0a70e55cf5a131 --- /dev/null +++ b/apps/i2psnark/readme.txt @@ -0,0 +1,11 @@ +This is an I2P port of snark [http://klomp.org/snark], a GPL'ed bittorrent client + +The build in tracker has been removed for simplicity. + +Example usage: + java -jar lib/i2psnark.jar myFile.torrent + +or, a more verbose setting: + java -jar lib/i2psnark.jar --eepproxy 127.0.0.1 4444 \ + --i2cp 127.0.0.1 7654 "inbound.length=2 outbound.length=2" \ + --debug 6 myFile.torrent diff --git a/apps/i2psnark/readme.txt.snark b/apps/i2psnark/readme.txt.snark new file mode 100644 index 0000000000000000000000000000000000000000..0567e01d469c5a94d6de4387b637aee05496aa73 --- /dev/null +++ b/apps/i2psnark/readme.txt.snark @@ -0,0 +1,141 @@ +The Hunting of the Snark Project - BitTorrent Application Suite +0.5 - The Beaver's Lesson (27 June 2003) + + "It's a Snark!" was the sound that first came to their ears, + And seemed almost too good to be true. + Then followed a torrent of laughter and cheers: + Then the ominous words "It's a Boo-" + + -- from The Hunting Of The Snark by Lewis Carroll + +Snark is a client for downloading and sharing files distributed with +the BitTorrent protocol. It is mainly used for exploring the BitTorrent +protocol and experimenting with the the GNU Compiler for Java (gcj). +But it can also be used as a regular BitTorrent Client. + +Snark can also act as a torrent creator, micro http server for delivering +metainfo.torrent files and has an integrated Tracker for making sharing of +files as easy as possible. + +When you give the option --share Snark will automatically +create a .torrent file, start a very simple webserver to distribute +the metainfo.torrent file and a local tracker that other BitTorrent +clients can connect to. + +Distribution +------------ + + Copyright (C) 2003 Mark J. Wielaard + + Snark is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Requirements/Installation +------------------------- + +The GNU Compiler for java (gcj) version 3.3 or later. +(Earlier versions have a faulty SHA message digest implementation.) +On Debian GNU/Linux based distributions just install the gcj-3.3 package. +Edit the GCJ variable in the Makefile if your gcj binary is not gcj-3.3. + +Typing 'make' will create the native snark binary and a snark.jar file +for use with traditional java byte code interpreters. + +It is possible to compile the sources with other java compilers +like jikes or kjc to produce the snark.jar file. Edit the JAVAC and +JAVAC_FLAGS variables on top of the Makefile for this. And type +'make snark.jar' to create a jar file that can be used by traditional +java bytecode interpreters like kaffe: 'kaffe -jar snark.jar'. +You will need at least version 1.1 of kaffe for all functionality to work +correctly ('--share' does not work with older versions). + +When trying out the experimental Gnome frontend you also need the java-gnome +bindings. On Debian GNU/Linux systems install the package libgnome0-java. +You can try it out by typing 'make snark-gnome' and then run 'snark-gnome.sh' +like you would with the normal command line client. + +Running +------- + +To use the program start it with: + +snark [--debug [level]] [--no-commands] [--port <port>] + [--share (<ip>|<host>)] (<url>|<file>|<dir>) + --debug Shows some extra info and stacktraces. + level How much debug details to show + (defaults to 3, with --debug to 4, highest level is 6). + --no-commands Don't read interactive commands or show usage info. + --port The port to listen on for incomming connections + (if not given defaults to first free port between 6881-6889). + --share Start torrent tracker on <ip> address or <host> name. + <url> URL pointing to .torrent metainfo file to download/share. + <file> Either a local .torrent metainfo file to download + or (with --share) a file to share. + <dir> A directory with files to share (needs --share). + +Since this is an early beta release there are probably still some bugs +in the program. To help find them run the program with the --debug +option which shows more information on what it going on. You can also give +the level of debug output you want. Zero will give (almost) no output at all. +Everything above debug level 4 is probably to much (only really useful to +see what goes on on the protocol/network level). + +Examples + +- To simple start downloading/sharing a file. + Either download the .torrent file to disk and start snark with: + ./snark somefile.torrent + + Or give it the complete URL: + ./snark http://somehost.example.com/cd-images/bbc-lnx.iso.torrent + +- To start seeding/sharing a local file: + ./snark --share my-host.example.com some-file + + Snark will respond with: + Listening on port: 6881 + Trying to create metainfo torrent for 'some-file' + Creating torrent piece hashes: ++++++++++ + Torrent available on http://my-host.example.com:6881/metainfo.torrent + + You can now point other people to the above URL so they can share + the file with their own BitTorrent client. + +Commands + +While the program is running in text mode you can currently give the +following commands: 'info', 'list' and 'quit'. + +Interactive commands are disabled when the '--no-commands' flag is given. +This is sometimes desireable for running snark in the background. + +More information +---------------- + +- The Evolution of Cooperation - Robert Axelrod + ISBN 0-465-02121-2 + +- The BitTorrent protocol description: + <http://bitconjurer.org/BitTorrent/protocol.html> + +- The GNU Compiler for Java (gcj): + <http://gcc.gnu.org/java/> + +- java-gnome bindings : <http://java-gnome.sourceforge.net/> + +- The Hunting of the Snark - Lewis Carroll + +Comments welcome + + - Mark Wielaard <mark@klomp.org>