diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..1e5c3f7 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,7 @@ +2001-11-14 Chris Smith (0.3-b1) + + * Renamed package and main class from GalleryUp to GalleryRemote. + +2001-11-12 Chris Smith (0.2) + + * v0.2 - The first release of Gallery Remote. \ No newline at end of file diff --git a/HTTPClient/0COPYRIGHT b/HTTPClient/0COPYRIGHT new file mode 100644 index 0000000..8f195fe --- /dev/null +++ b/HTTPClient/0COPYRIGHT @@ -0,0 +1,28 @@ +/* + * Copyright (c) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ diff --git a/HTTPClient/0LICENSE b/HTTPClient/0LICENSE new file mode 100644 index 0000000..223ede7 --- /dev/null +++ b/HTTPClient/0LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 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. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, 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 library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete 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 distribute a copy of this License along with the +Library. + + 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 Library or any portion +of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +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 Library, 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 Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you 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. + + If distribution of 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 satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be 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. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library 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. + + 9. 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 Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +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 with +this License. + + 11. 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 Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library 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 Library. + +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. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library 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. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +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 Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +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 + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. 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 LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), 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 Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. 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. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; 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. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/HTTPClient/0README b/HTTPClient/0README new file mode 100644 index 0000000..8293dbc --- /dev/null +++ b/HTTPClient/0README @@ -0,0 +1,76 @@ + +This is Version 0.3-3 of the HTTPClient package. The latest version should +always be available at http://www.innovation.ch/java/HTTPClient/ . +Copyright (C) 1996-2001 Ronald Tschalär + +The HTTPClient is fairly full-featured http client library. It implements +most of the relevant parts of HTTP/1.1, and automatically handles things +like redirections, authorization, and cookies. The functionality can be +easily extended through the use of modules. + + +Installation: +------------- + +Unpacking the .tar.Z or .zip should've created a subdirectory +'HTTPClient'. Put this directory somewhere in your classpath. If you +haven't done the following already I recommend setting up a main +directory that contains a subdirectory for every java package you +install, and then add this main directory to your classpath - that way +for new packages all you have to do is unpack them into a subdirectory +under that main directory and away you go. Now add a 'import +HTTPClient.*;' statement to each of your files that use any part of the +package and you're set to go. + +Alternatively you can put everything into a zip file and add that file +to your CLASSPATH. If your JDK/development-environment supports +compressed zip files and you downloaded the HTTPClient.zip file then +you can just add that file to your CLASSPATH. If you downloaded the +tar file, or if your JDK does not support compressed zip files, then +unpack the HTTPClient and create an uncompressed zip file to put in +the CLASSPATH with something like the following: + + zip -r0 HTTPClient.zip HTTPClient -i '*.class' + + +Directory Structure: +-------------------- + + HTTPClient -- the source and compiled class files + | + +-- http --- the http handler (for use with URLConnection) + | + +-- shttp -- the shttp handler (for use with URLConnection) + | + +-- https -- the https handler (for use with URLConnection) + | + +-- alt ---- alternative versions of the core classes + | | + | +-- HotJava ---- the HotJava specific replacement classes + | + +-- doc -- all the documentation + | + +-- api ----- all the javadoc generated api docs + | + +-- images -- images for the documentation + + +Use: +---- + +See the documentation in the doc subdirectory. The beginning is at +HTTPClient/doc/index.html . + + +Comments: +--------- + +Mail suggestions, comments, bugs, enhancement-requests to: + +ronald@innovation.ch + + + Have fun, + + Ronald + diff --git a/HTTPClient/AuthSchemeNotImplException.java b/HTTPClient/AuthSchemeNotImplException.java new file mode 100644 index 0000000..cc3ad7a --- /dev/null +++ b/HTTPClient/AuthSchemeNotImplException.java @@ -0,0 +1,66 @@ +/* + * @(#)AuthSchemeNotImplException.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + + +/** + * Signals that the handling of a authorization scheme is not implemented. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public class AuthSchemeNotImplException extends ModuleException +{ + + /** + * Constructs an AuthSchemeNotImplException with no detail message. + * A detail message is a String that describes this particular exception. + */ + public AuthSchemeNotImplException() + { + super(); + } + + + /** + * Constructs an AuthSchemeNotImplException class with the specified + * detail message. A detail message is a String that describes this + * particular exception. + * + * @param msg the String containing a detail message + */ + public AuthSchemeNotImplException(String msg) + { + super(msg); + } +} diff --git a/HTTPClient/AuthorizationHandler.java b/HTTPClient/AuthorizationHandler.java new file mode 100644 index 0000000..e17a0f8 --- /dev/null +++ b/HTTPClient/AuthorizationHandler.java @@ -0,0 +1,151 @@ +/* + * @(#)AuthorizationHandler.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; + + +/** + * This is the interface that an Authorization handler must implement. You + * can implement your own auth handler to add support for auth schemes other + * than the ones handled by the default handler, to use a different UI for + * soliciting usernames and passwords, or for using an altogether different + * way of getting the necessary auth info. + * + * @see AuthorizationInfo#setAuthHandler(HTTPClient.AuthorizationHandler) + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public interface AuthorizationHandler +{ + /** + * This method is called whenever a 401 or 407 response is received and + * no candidate info is found in the list of known auth info. Usually + * this method will query the user for the necessary info. + * + *

If the returned info is not null it will be added to the list of + * known info. If the info is valid for more than one (host, port, realm, + * scheme) tuple then this method must add the corresponding auth infos + * itself. + * + *

This method must check req.allow_ui and only attempt + * user interaction if it's true. + * + * @param challenge the parsed challenge from the server; the host, + * port, scheme, realm and params are set to the + * values given by the server in the challenge. + * @param req the request which provoked this response. + * @param resp the full response. + * @return the authorization info to use when retrying the request, + * or null if the request is not to be retried. The necessary + * info includes the host, port, scheme and realm as given in + * the challenge parameter, plus either the basic + * cookie or any necessary params. + * @exception AuthSchemeNotImplException if the authorization scheme + * in the challenge cannot be handled. + * @exception IOException if an exception occurs while processing the + * challenge + */ + AuthorizationInfo getAuthorization(AuthorizationInfo challenge, + RoRequest req, RoResponse resp) + throws AuthSchemeNotImplException, IOException; + + + /** + * This method is called whenever auth info is chosen from the list of + * known info in the AuthorizationInfo class to be sent with a request. + * This happens when either auth info is being preemptively sent or if + * a 401 response is retrieved and a matching info is found in the list + * of known info. The intent of this method is to allow the handler to + * fix up the info being sent based on the actual request (e.g. in digest + * authentication the digest-uri, nonce and response-digest usually need + * to be recalculated). + * + * @param info the authorization info retrieved from the list of + * known info. + * @param req the request this info is targeted for. + * @param challenge the authorization challenge received from the server + * if this is in response to a 401, or null if we are + * preemptively sending the info. + * @param resp the full 401 response received, or null if we are + * preemptively sending the info. + * @return the authorization info to be sent with the request, or null + * if none is to be sent. + * @exception AuthSchemeNotImplException if the authorization scheme + * in the info cannot be handled. + * @exception IOException if an exception occurs while fixing up the + * info + */ + AuthorizationInfo fixupAuthInfo(AuthorizationInfo info, RoRequest req, + AuthorizationInfo challenge, RoResponse resp) + throws AuthSchemeNotImplException, IOException; + + + /** + * Sometimes even non-401 responses will contain headers pertaining to + * authorization (such as the "Authentication-Info" header). Therefore + * this method is invoked for each response received, even if it is not + * a 401 or 407 response. In case of a 401 or 407 response the methods + * fixupAuthInfo() and getAuthorization() are + * invoked after this method. + * + * @param resp the full Response + * @param req the Request which provoked this response + * @param prev the previous auth info sent, or null if none was sent + * @param prxy the previous proxy auth info sent, or null if none was sent + * @exception IOException if an exception occurs during the reading of + * the headers. + */ + void handleAuthHeaders(Response resp, RoRequest req, + AuthorizationInfo prev, AuthorizationInfo prxy) + throws IOException; + + + /** + * This method is similar to handleAuthHeaders except that + * it is called if any headers in the trailer were sent. This also + * implies that it is invoked after any fixupAuthInfo() or + * getAuthorization() invocation. + * + * @param resp the full Response + * @param req the Request which provoked this response + * @param prev the previous auth info sent, or null if none was sent + * @param prxy the previous proxy auth info sent, or null if none was sent + * @exception IOException if an exception occurs during the reading of + * the trailers. + * @see #handleAuthHeaders(HTTPClient.Response, HTTPClient.RoRequest, HTTPClient.AuthorizationInfo, HTTPClient.AuthorizationInfo) + */ + void handleAuthTrailers(Response resp, RoRequest req, + AuthorizationInfo prev, AuthorizationInfo prxy) + throws IOException; +} diff --git a/HTTPClient/AuthorizationInfo.java b/HTTPClient/AuthorizationInfo.java new file mode 100644 index 0000000..2c06c71 --- /dev/null +++ b/HTTPClient/AuthorizationInfo.java @@ -0,0 +1,1222 @@ +/* + * @(#)AuthorizationInfo.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.net.ProtocolException; +import java.util.Vector; +import java.util.Hashtable; +import java.util.Enumeration; + + +/** + * Holds the information for an authorization response. + * + *

There are 7 fields which make up this class: host, port, scheme, + * realm, cookie, params, and extra_info. The host and port select which + * server the info will be sent to. The realm is server specified string + * which groups various URLs under a given server together and which is + * used to select the correct info when a server issues an auth challenge; + * for schemes which don't use a realm (such as "NTLM", "PEM", and + * "Kerberos") the realm must be the empty string (""). The scheme is the + * authorization scheme used (such as "Basic" or "Digest"). + * + *

There are basically two formats used for the Authorization header, + * the one used by the "Basic" scheme and derivatives, and the one used by + * the "Digest" scheme and derivatives. The first form contains just the + * the scheme and a "cookie": + * + *

    Authorization: Basic aGVsbG86d29ybGQ=
+ * + * The second form contains the scheme followed by a number of parameters + * in the form of name=value pairs: + * + *
    Authorization: Digest username="hello", realm="test", nonce="42", ...
+ * + * The two fields "cookie" and "params" correspond to these two forms. + * toString() is used by the AuthorizationModule + * when generating the Authorization header and will format the info + * accordingly. Note that "cookie" and "params" are mutually exclusive: if + * the cookie field is non-null then toString() will generate the first + * form; otherwise it will generate the second form. + * + *

In some schemes "extra" information needs to be kept which doesn't + * appear directly in the Authorization header. An example of this are the + * A1 and A2 strings in the Digest scheme. Since all elements in the params + * field will appear in the Authorization header this field can't be used + * for storing such info. This is what the extra_info field is for. It is + * an arbitrary object which can be manipulated by the corresponding + * setExtraInfo() and getExtraInfo() methods, but which will not be printed + * by toString(). + * + *

The addXXXAuthorization(), removeXXXAuthorization(), and + * getAuthorization() methods manipulate and query an internal list of + * AuthorizationInfo instances. There can be only one instance per host, + * port, scheme, and realm combination (see equals()). + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.1 + */ +public class AuthorizationInfo implements Cloneable +{ + // class fields + + /** Holds the list of lists of authorization info structures */ + private static Hashtable CntxtList = new Hashtable(); + + /** A pointer to the handler to be called when we need authorization info */ + private static AuthorizationHandler + AuthHandler = new DefaultAuthHandler(); + + static + { + CntxtList.put(HTTPConnection.getDefaultContext(), new Hashtable()); + } + + + // the instance oriented stuff + + /** the host (lowercase) */ + private String host; + + /** the port */ + private int port; + + /** the scheme. (e.g. "Basic") + * Note: don't lowercase because some buggy servers use a case-sensitive + * match */ + private String scheme; + + /** the realm */ + private String realm; + + /** the string used for the "Basic", "NTLM", and other authorization + * schemes which don't use parameters */ + private String cookie; + + /** any parameters */ + private NVPair[] auth_params = new NVPair[0]; + + /** additional info which won't be displayed in the toString() */ + private Object extra_info = null; + + /** a list of paths where this realm has been known to be required */ + private String[] paths = new String[0]; + + + // Constructors + + /** + * Creates an new info structure for the specified host and port. + * + * @param host the host + * @param port the port + */ + AuthorizationInfo(String host, int port) + { + this.host = host.trim().toLowerCase(); + this.port = port; + } + + + /** + * Creates a new info structure for the specified host and port with the + * specified scheme, realm, params. The cookie is set to null. + * + * @param host the host + * @param port the port + * @param scheme the scheme + * @param realm the realm + * @param params the parameters as an array of name/value pairs, or null + * @param info arbitrary extra info, or null + */ + public AuthorizationInfo(String host, int port, String scheme, + String realm, NVPair params[], Object info) + { + this.scheme = scheme.trim(); + this.host = host.trim().toLowerCase(); + this.port = port; + this.realm = realm; + this.cookie = null; + + if (params != null) + auth_params = Util.resizeArray(params, params.length); + + this.extra_info = info; + } + + + /** + * Creates a new info structure for the specified host and port with the + * specified scheme, realm and cookie. The params is set to a zero-length + * array, and the extra_info is set to null. + * + * @param host the host + * @param port the port + * @param scheme the scheme + * @param realm the realm + * @param cookie for the "Basic" scheme this is the base64-encoded + * username/password; for the "NTLM" scheme this is the + * base64-encoded username/password message. + */ + public AuthorizationInfo(String host, int port, String scheme, + String realm, String cookie) + { + this.scheme = scheme.trim(); + this.host = host.trim().toLowerCase(); + this.port = port; + this.realm = realm; + if (cookie != null) + this.cookie = cookie.trim(); + else + this.cookie = null; + } + + + /** + * Creates a new copy of the given AuthorizationInfo. + * + * @param templ the info to copy + */ + AuthorizationInfo(AuthorizationInfo templ) + { + this.scheme = templ.scheme; + this.host = templ.host; + this.port = templ.port; + this.realm = templ.realm; + this.cookie = templ.cookie; + + this.auth_params = + Util.resizeArray(templ.auth_params, templ.auth_params.length); + + this.extra_info = templ.extra_info; + } + + + // Class Methods + + /** + * Set's the authorization handler. This handler is called whenever + * the server requests authorization and no entry for the requested + * scheme and realm can be found in the list. The handler must implement + * the AuthorizationHandler interface. + * + *

If no handler is set then a {@link DefaultAuthHandler default + * handler} is used. This handler currently only handles the "Basic" and + * "Digest" schemes and brings up a popup which prompts for the username + * and password. + * + *

The default handler can be disabled by setting the auth handler to + * null. + * + * @param handler the new authorization handler + * @return the old authorization handler + * @see AuthorizationHandler + */ + public static AuthorizationHandler + setAuthHandler(AuthorizationHandler handler) + { + AuthorizationHandler tmp = AuthHandler; + AuthHandler = handler; + + return tmp; + } + + + /** + * Get's the current authorization handler. + * + * @return the current authorization handler, or null if none is set. + * @see AuthorizationHandler + */ + public static AuthorizationHandler getAuthHandler() + { + return AuthHandler; + } + + + /** + * Searches for the authorization info using the given host, port, + * scheme and realm. The context is the default context. + * + * @param host the host + * @param port the port + * @param scheme the scheme + * @param realm the realm + * @return a pointer to the authorization data or null if not found + */ + public static AuthorizationInfo getAuthorization( + String host, int port, + String scheme, String realm) + { + return getAuthorization(host, port, scheme, realm, + HTTPConnection.getDefaultContext()); + } + + + /** + * Searches for the authorization info in the given context using the + * given host, port, scheme and realm. + * + * @param host the host + * @param port the port + * @param scheme the scheme + * @param realm the realm + * @param context the context this info is associated with + * @return a pointer to the authorization data or null if not found + */ + public static synchronized AuthorizationInfo getAuthorization( + String host, int port, + String scheme, String realm, + Object context) + { + Hashtable AuthList = Util.getList(CntxtList, context); + + AuthorizationInfo auth_info = + new AuthorizationInfo(host, port, scheme, realm, (NVPair[]) null, + null); + + return (AuthorizationInfo) AuthList.get(auth_info); + } + + + /** + * Queries the AuthHandler for authorization info. It also adds this + * info to the list. + * + * @param auth_info any info needed by the AuthHandler; at a minimum the + * host, scheme and realm should be set. + * @param req the request which initiated this query + * @param resp the full response + * @return a structure containing the requested info, or null if either + * no AuthHandler is set or the user canceled the request. + * @exception AuthSchemeNotImplException if this is thrown by + * the AuthHandler. + */ + static AuthorizationInfo queryAuthHandler(AuthorizationInfo auth_info, + RoRequest req, RoResponse resp) + throws AuthSchemeNotImplException, IOException + { + if (AuthHandler == null) + return null; + + AuthorizationInfo new_info = + AuthHandler.getAuthorization(auth_info, req, resp); + if (new_info != null) + { + if (req != null) + addAuthorization((AuthorizationInfo) new_info.clone(), + req.getConnection().getContext()); + else + addAuthorization((AuthorizationInfo) new_info.clone(), + HTTPConnection.getDefaultContext()); + } + + return new_info; + } + + + /** + * Searches for the authorization info using the host, port, scheme and + * realm from the given info struct. If not found it queries the + * AuthHandler (if set). + * + * @param auth_info the AuthorizationInfo + * @param req the request which initiated this query + * @param resp the full response + * @param query_auth_h if true, query the auth-handler if no info found. + * @return a pointer to the authorization data or null if not found + * @exception AuthSchemeNotImplException If thrown by the AuthHandler. + */ + static synchronized AuthorizationInfo getAuthorization( + AuthorizationInfo auth_info, RoRequest req, + RoResponse resp, boolean query_auth_h) + throws AuthSchemeNotImplException, IOException + { + Hashtable AuthList; + if (req != null) + AuthList = Util.getList(CntxtList, req.getConnection().getContext()); + else + AuthList = Util.getList(CntxtList, HTTPConnection.getDefaultContext()); + + AuthorizationInfo new_info = + (AuthorizationInfo) AuthList.get(auth_info); + + if (new_info == null && query_auth_h) + new_info = queryAuthHandler(auth_info, req, resp); + + return new_info; + } + + + /** + * Searches for the authorization info given a host, port, scheme and + * realm. Queries the AuthHandler if not found in list. + * + * @param host the host + * @param port the port + * @param scheme the scheme + * @param realm the realm + * @param req the request which initiated this query + * @param resp the full response + * @param query_auth_h if true, query the auth-handler if no info found. + * @return a pointer to the authorization data or null if not found + * @exception AuthSchemeNotImplException If thrown by the AuthHandler. + */ + static AuthorizationInfo getAuthorization(String host, int port, + String scheme, String realm, + RoRequest req, RoResponse resp, + boolean query_auth_h) + throws AuthSchemeNotImplException, IOException + { + return getAuthorization(new AuthorizationInfo(host, port, scheme, + realm, (NVPair[]) null, null), + req, resp, query_auth_h); + } + + + /** + * Adds an authorization entry to the list using the default context. + * If an entry for the specified scheme and realm already exists then + * its cookie and params are replaced with the new data. + * + * @param auth_info the AuthorizationInfo to add + */ + public static void addAuthorization(AuthorizationInfo auth_info) + { + addAuthorization(auth_info, HTTPConnection.getDefaultContext()); + } + + + /** + * Adds an authorization entry to the list. If an entry for the + * specified scheme and realm already exists then its cookie and + * params are replaced with the new data. + * + * @param auth_info the AuthorizationInfo to add + * @param context the context to associate this info with + */ + public static void addAuthorization(AuthorizationInfo auth_info, + Object context) + { + Hashtable AuthList = Util.getList(CntxtList, context); + + // merge path list + AuthorizationInfo old_info = + (AuthorizationInfo) AuthList.get(auth_info); + if (old_info != null) + { + int ol = old_info.paths.length, + al = auth_info.paths.length; + + if (al == 0) + auth_info.paths = old_info.paths; + else + { + auth_info.paths = Util.resizeArray(auth_info.paths, al+ol); + System.arraycopy(old_info.paths, 0, auth_info.paths, al, ol); + } + } + + AuthList.put(auth_info, auth_info); + } + + + /** + * Adds an authorization entry to the list using the default context. + * If an entry for the specified scheme and realm already exists then + * its cookie and params are replaced with the new data. + * + * @param host the host + * @param port the port + * @param scheme the scheme + * @param realm the realm + * @param cookie the cookie + * @param params an array of name/value pairs of parameters + * @param info arbitrary extra auth info + */ + public static void addAuthorization(String host, int port, String scheme, + String realm, String cookie, + NVPair params[], Object info) + { + addAuthorization(host, port, scheme, realm, cookie, params, info, + HTTPConnection.getDefaultContext()); + } + + + /** + * Adds an authorization entry to the list. If an entry for the + * specified scheme and realm already exists then its cookie and + * params are replaced with the new data. + * + * @param host the host + * @param port the port + * @param scheme the scheme + * @param realm the realm + * @param cookie the cookie + * @param params an array of name/value pairs of parameters + * @param info arbitrary extra auth info + * @param context the context to associate this info with + */ + public static void addAuthorization(String host, int port, String scheme, + String realm, String cookie, + NVPair params[], Object info, + Object context) + { + AuthorizationInfo auth = + new AuthorizationInfo(host, port, scheme, realm, cookie); + if (params != null && params.length > 0) + auth.auth_params = Util.resizeArray(params, params.length); + auth.extra_info = info; + + addAuthorization(auth, context); + } + + + /** + * Adds an authorization entry for the "Basic" authorization scheme to + * the list using the default context. If an entry already exists for + * the "Basic" scheme and the specified realm then it is overwritten. + * + * @param host the host + * @param port the port + * @param realm the realm + * @param user the username + * @param passwd the password + */ + public static void addBasicAuthorization(String host, int port, + String realm, String user, + String passwd) + { + addAuthorization(host, port, "Basic", realm, + Codecs.base64Encode(user + ":" + passwd), + (NVPair[]) null, null); + } + + + /** + * Adds an authorization entry for the "Basic" authorization scheme to + * the list. If an entry already exists for the "Basic" scheme and the + * specified realm then it is overwritten. + * + * @param host the host + * @param port the port + * @param realm the realm + * @param user the username + * @param passwd the password + * @param context the context to associate this info with + */ + public static void addBasicAuthorization(String host, int port, + String realm, String user, + String passwd, Object context) + { + addAuthorization(host, port, "Basic", realm, + Codecs.base64Encode(user + ":" + passwd), + (NVPair[]) null, null, context); + } + + + /** + * Adds an authorization entry for the "Digest" authorization scheme to + * the list using the default context. If an entry already exists for the + * "Digest" scheme and the specified realm then it is overwritten. + * + * @param host the host + * @param port the port + * @param realm the realm + * @param user the username + * @param passwd the password + */ + public static void addDigestAuthorization(String host, int port, + String realm, String user, + String passwd) + { + addDigestAuthorization(host, port, realm, user, passwd, + HTTPConnection.getDefaultContext()); + } + + + /** + * Adds an authorization entry for the "Digest" authorization scheme to + * the list. If an entry already exists for the "Digest" scheme and the + * specified realm then it is overwritten. + * + * @param host the host + * @param port the port + * @param realm the realm + * @param user the username + * @param passwd the password + * @param context the context to associate this info with + */ + public static void addDigestAuthorization(String host, int port, + String realm, String user, + String passwd, Object context) + { + AuthorizationInfo prev = + getAuthorization(host, port, "Digest", realm, context); + NVPair[] params; + + if (prev == null) + { + params = new NVPair[4]; + params[0] = new NVPair("username", user); + params[1] = new NVPair("uri", ""); + params[2] = new NVPair("nonce", ""); + params[3] = new NVPair("response", ""); + } + else + { + params = prev.getParams(); + for (int idx=0; idx for " + + "quoted-string starting at position " + beg + + " not found"); + param_value = + Util.dequoteString(challenge.substring(beg, end)); + end++; + } + } + else // this is not strictly allowed + param_value = null; + + if (param_name.equalsIgnoreCase("realm")) + curr.realm = param_value; + else + params.addElement(new NVPair(param_name, param_value)); + + first = false; + } + + pos_ref[0] = beg; + pos_ref[1] = end; + return params; + } + + + // Instance Methods + + /** + * Get the host. + * + * @return a string containing the host name. + */ + public final String getHost() + { + return host; + } + + + /** + * Get the port. + * + * @return an int containing the port number. + */ + public final int getPort() + { + return port; + } + + + /** + * Get the scheme. + * + * @return a string containing the scheme. + */ + public final String getScheme() + { + return scheme; + } + + + /** + * Get the realm. + * + * @return a string containing the realm. + */ + public final String getRealm() + { + return realm; + } + + + /** + * Get the cookie + * + * @return the cookie String + * @since V0.3-1 + */ + public final String getCookie() + { + return cookie; + } + + + /** + * Set the cookie + * + * @param cookie the new cookie + * @since V0.3-1 + */ + public final void setCookie(String cookie) + { + this.cookie = cookie; + } + + + /** + * Get the authentication parameters. + * + * @return an array of name/value pairs. + */ + public final NVPair[] getParams() + { + return Util.resizeArray(auth_params, auth_params.length); + } + + + /** + * Set the authentication parameters. + * + * @param an array of name/value pairs. + */ + public final void setParams(NVPair[] params) + { + if (params != null) + auth_params = Util.resizeArray(params, params.length); + else + auth_params = new NVPair[0]; + } + + + /** + * Get the extra info. + * + * @return the extra_info object + */ + public final Object getExtraInfo() + { + return extra_info; + } + + + /** + * Set the extra info. + * + * @param info the extra info + */ + public final void setExtraInfo(Object info) + { + extra_info = info; + } + + + /** + * Constructs a string containing the authorization info. The format + * is that of the http Authorization header. + * + * @return a String containing all info. + */ + public String toString() + { + StringBuffer field = new StringBuffer(100); + + field.append(scheme); + field.append(" "); + + if (cookie != null) + { + field.append(cookie); + } + else + { + if (realm.length() > 0) + { + field.append("realm=\""); + field.append(Util.quoteString(realm, "\\\"")); + field.append('"'); + } + + for (int idx=0; idx= 0) + { + System.arraycopy(hdrs, rem_idx+1, hdrs, rem_idx, hdrs.length-rem_idx-1); + hdrs = Util.resizeArray(hdrs, hdrs.length-1); + req.setHeaders(hdrs); + } + + + // Preemptively send authorization info + + rem_idx = -1; + Auth: if (!auth_from_4xx) + { + // first remove any Auth header that still may be around + + for (int idx=0; idx= 0) + { + System.arraycopy(hdrs, rem_idx+1, hdrs, rem_idx, hdrs.length-rem_idx-1); + hdrs = Util.resizeArray(hdrs, hdrs.length-1); + req.setHeaders(hdrs); + } + + return REQ_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase1Handler(Response resp, RoRequest req) + throws IOException + { + /* If auth info successful update path list. Note: if we + * preemptively sent auth info we don't actually know if + * it was necessary. Therefore we don't update the path + * list in this case; this prevents it from being + * contaminated. If the info was necessary, then the next + * time we access this resource we will again guess the + * same info and send it. + */ + if (resp.getStatusCode() != 401 && resp.getStatusCode() != 407) + { + if (auth_sent != null && auth_from_4xx) + { + try + { + AuthorizationInfo.getAuthorization(auth_sent, req, resp, + false).addPath(req.getRequestURI()); + } + catch (AuthSchemeNotImplException asnie) + { /* shouldn't happen */ } + } + + // reset guard if not an auth challenge + num_tries = 0; + } + + auth_from_4xx = false; + prxy_from_4xx = false; + + if (resp.getHeader("WWW-Authenticate") == null) + { + auth_lst_idx = 0; + auth_scm_idx = 0; + } + + if (resp.getHeader("Proxy-Authenticate") == null) + { + prxy_lst_idx = 0; + prxy_scm_idx = 0; + } + } + + + /** + * Invoked by the HTTPClient. + */ + public int responsePhase2Handler(Response resp, Request req) + throws IOException, AuthSchemeNotImplException + { + // Let the AuthHandler handle any Authentication headers. + + AuthorizationHandler h = AuthorizationInfo.getAuthHandler(); + if (h != null) + h.handleAuthHeaders(resp, req, auth_sent, prxy_sent); + + + // handle 401 and 407 response codes + + int sts = resp.getStatusCode(); + switch(sts) + { + case 401: // Unauthorized + case 407: // Proxy Authentication Required + + // guard against infinite retries due to bugs + + num_tries++; + if (num_tries > 10) + throw new ProtocolException("Bug in authorization handling: server refused the given info 10 times"); + + + // defer handling if a stream was used + + if (req.getStream() != null) + { + if (!HTTPConnection.deferStreamed) + { + Log.write(Log.AUTH, "AuthM: status " + sts + + " not handled - request has " + + "an output stream"); + return RSP_CONTINUE; + } + + saved_req = (Request) req.clone(); + saved_resp = (Response) resp.clone(); + deferred_auth_list.put(req.getStream(), this); + + req.getStream().reset(); + resp.setRetryRequest(true); + + Log.write(Log.AUTH, "AuthM: Handling of status " + + sts + " deferred because an " + + "output stream was used"); + + return RSP_CONTINUE; + } + + + // handle the challenge + + Log.write(Log.AUTH, "AuthM: Handling status: " + sts + " " + + resp.getReasonLine()); + + handle_auth_challenge(req, resp); + + + // check for valid challenge + + if (auth_sent != null || prxy_sent != null) + { + try { resp.getInputStream().close(); } + catch (IOException ioe) { } + + if (auth_sent != null) + Log.write(Log.AUTH, "AuthM: Resending request " + + "with Authorization '" + + auth_sent + "'"); + else + Log.write(Log.AUTH, "AuthM: Resending request " + + "with Proxy-Authorization '" + + prxy_sent + "'"); + + return RSP_REQUEST; + } + + + if (req.getStream() != null) + Log.write(Log.AUTH, "AuthM: status " + sts + " not " + + "handled - request has an output " + + "stream"); + else + Log.write(Log.AUTH, "AuthM: No Auth Info found - " + + "status " + sts + " not handled"); + + return RSP_CONTINUE; + + default: + + return RSP_CONTINUE; + } + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase3Handler(Response resp, RoRequest req) + { + } + + + /** + * Invoked by the HTTPClient. + */ + public void trailerHandler(Response resp, RoRequest req) throws IOException + { + // Let the AuthHandler handle any Authentication headers. + + AuthorizationHandler h = AuthorizationInfo.getAuthHandler(); + if (h != null) + h.handleAuthTrailers(resp, req, auth_sent, prxy_sent); + } + + + /** + * + */ + private void handle_auth_challenge(Request req, Response resp) + throws AuthSchemeNotImplException, IOException + { + // handle WWW-Authenticate + + int[] idx_arr = { auth_lst_idx, // hack to pass by ref + auth_scm_idx}; + auth_sent = setAuthHeaders(resp.getHeader("WWW-Authenticate"), + req, resp, "Authorization", idx_arr, + auth_sent); + if (auth_sent != null) + { + auth_from_4xx = true; + auth_lst_idx = idx_arr[0]; + auth_scm_idx = idx_arr[1]; + } + else + { + auth_lst_idx = 0; + auth_scm_idx = 0; + } + + + // handle Proxy-Authenticate + + idx_arr[0] = prxy_lst_idx; // hack to pass by ref + idx_arr[1] = prxy_scm_idx; + prxy_sent = setAuthHeaders(resp.getHeader("Proxy-Authenticate"), + req, resp, "Proxy-Authorization", + idx_arr, prxy_sent); + if (prxy_sent != null) + { + prxy_from_4xx = true; + prxy_lst_idx = idx_arr[0]; + prxy_scm_idx = idx_arr[1]; + } + else + { + prxy_lst_idx = 0; + prxy_scm_idx = 0; + } + + if (prxy_sent != null) + { + HTTPConnection con = req.getConnection(); + Util.getList(proxy_cntxt_list, con.getContext()) + .put(con.getProxyHost()+":"+con.getProxyPort(), + prxy_sent); + } + + // check for headers + + if (auth_sent == null && prxy_sent == null && + resp.getHeader("WWW-Authenticate") == null && + resp.getHeader("Proxy-Authenticate") == null) + { + if (resp.getStatusCode() == 401) + throw new ProtocolException("Missing WWW-Authenticate header"); + else + throw new ProtocolException("Missing Proxy-Authenticate header"); + } + } + + + /** + * Handles authentication requests and sets the authorization headers. + * It tries to retrieve the neccessary parameters from AuthorizationInfo, + * and failing that calls the AuthHandler. Handles multiple authentication + * headers. + * + * @param auth_str the authentication header field returned by the server. + * @param req the Request used + * @param resp the full Response received + * @param header the header name to use in the new headers array. + * @param idx_arr an array of indicies holding the state of where we + * are when handling multiple authorization headers. + * @param prev the previous auth info sent, or null if none + * @return the new credentials, or null if none found + * @exception ProtocolException if auth_str is null. + * @exception AuthSchemeNotImplException if thrown by the AuthHandler. + * @exception IOException if thrown by the AuthHandler. + */ + private AuthorizationInfo setAuthHeaders(String auth_str, Request req, + RoResponse resp, String header, + int[] idx_arr, + AuthorizationInfo prev) + throws ProtocolException, AuthSchemeNotImplException, IOException + { + if (auth_str == null) return null; + + // get the list of challenges the server sent + AuthorizationInfo[] challenges = + AuthorizationInfo.parseAuthString(auth_str, req, resp); + + if (Log.isEnabled(Log.AUTH)) + { + Log.write(Log.AUTH, "AuthM: parsed " + challenges.length + + " challenges:"); + for (int idx=0; idx= challenges.length) + idx_arr[1] = 0; + + try + { + credentials = AuthorizationInfo.queryAuthHandler( + challenges[idx_arr[1]], req, resp); + break; + } + catch (AuthSchemeNotImplException asnie) + { + if (idx == challenges.length-1) + throw asnie; + } + finally + { idx_arr[1]++; } + } + } + + // if we still don't have any credentials then give up + if (credentials == null) + return null; + + // find auth info + int auth_idx; + NVPair[] hdrs = req.getHeaders(); + for (auth_idx=0; auth_idxCancel button). + */ + NVPair getUsernamePassword(AuthorizationInfo challenge, boolean forProxy); +} diff --git a/HTTPClient/BufferedInputStream.java b/HTTPClient/BufferedInputStream.java new file mode 100644 index 0000000..708b78f --- /dev/null +++ b/HTTPClient/BufferedInputStream.java @@ -0,0 +1,230 @@ +/* + * @(#)BufferedInputStream.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.InputStream; +import java.io.FilterInputStream; +import java.io.IOException; + +/** + * This class is similar to java.io.BufferedInputStream, except that it fixes + * certain bugs and provides support for finding multipart boundaries. + * + *

Note: none of the methods here are synchronized because we assume the + * caller is already taking care of that. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class BufferedInputStream extends FilterInputStream +{ + /** our read buffer */ + private byte[] buffer = new byte[2000]; + /** the next byte in the buffer at which to read */ + private int pos = 0; + /** the end of the valid data in the buffer */ + private int end = 0; + /** the current mark position, or -1 if none */ + private int mark_pos = -1; + /** + * the large read threashhold: reads larger than this aren't buffered if + * both the current buffer is empty and no mark has been set. This is just + * an attempt to balance copying vs. multiple reads. + */ + private int lr_thrshld = 1500; + + + /** + * Create a new BufferedInputStream around the given input stream. + * + * @param stream the underlying input stream to use + */ + BufferedInputStream(InputStream stream) + { + super(stream); + } + + /** + * Read a single byte. + * + * @return the read byte, or -1 if the end of the stream has been reached + * @exception IOException if thrown by the underlying stream + */ + public int read() throws IOException + { + if (pos >= end) + fillBuff(); + + return (end > pos) ? (buffer[pos++] & 0xFF) : -1; + } + + /** + * Read a buffer full. + * + * @param buf the buffer to read into + * @param off the offset within buf at which to start writing + * @param len the number of bytes to read + * @return the number of bytes read + * @exception IOException if thrown by the underlying stream + */ + public int read(byte[] buf, int off, int len) throws IOException + { + if (len <= 0) + return 0; + + // optimize for large reads + if (pos >= end && len >= lr_thrshld && mark_pos < 0) + return in.read(buf, off, len); + + if (pos >= end) + fillBuff(); + + if (pos >= end) + return -1; + + int left = end - pos; + if (len > left) + len = left; + System.arraycopy(buffer, pos, buf, off, len); + pos += len; + + return len; + } + + /** + * Skip the given number of bytes in the stream. + * + * @param n the number of bytes to skip + * @return the actual number of bytes skipped + * @exception IOException if thrown by the underlying stream + */ + public long skip(long n) throws IOException + { + if (n <= 0) + return 0; + + int left = end - pos; + if (n <= left) + { + pos += n; + return n; + } + else + { + pos = end; + return left + in.skip(n - left); + } + } + + /** + * Fill buffer by reading from the underlying stream. This assumes the + * current buffer is empty, i.e. pos == end. + */ + private final void fillBuff() throws IOException + { + if (mark_pos > 0) // keep the marked stuff around if possible + { + // only copy if we don't have any space left + if (end >= buffer.length) + { + System.arraycopy(buffer, mark_pos, buffer, 0, end - mark_pos); + pos = end - mark_pos; + } + } + else if (mark_pos == 0 && end < buffer.length) + ; // pos == end, so we just fill what's left + else + pos = 0; // try to fill complete buffer + + // make sure our state is consistent even if read() throws InterruptedIOException + end = pos; + + int got = in.read(buffer, pos, buffer.length - pos); + if (got > 0) + end = pos + got; + } + + /** + * @return the number of bytes available for reading without blocking + * @exception IOException if the buffer is empty and the underlying stream has been + * closed + */ + public int available() throws IOException + { + int avail = end - pos; + if (avail == 0) + return in.available(); + + try + { avail += in.available(); } + catch (IOException ignored) + { /* ignore this because we have something available */ } + return avail; + } + + /** + * Mark the current read position so that we can start searching for the end boundary. + */ + void markForSearch() + { + mark_pos = pos; + } + + /** + * Figures out how many bytes past the end of the multipart we read. If we + * found the end, it then resets the read pos to just past the end of the + * boundary and unsets the mark; if not found, is sets the mark_pos back + * enough from the current position so we can always be sure to find the + * boundary. + * + * @param search the search string (end boundary) + * @param search_cmp the compiled info of the search string + * @return how many bytes past the end of the boundary we went; -1 if we + * haven't gone passed it yet. + */ + int pastEnd(byte[] search, int[] search_cmp) + { + int idx = Util.findStr(search, search_cmp, buffer, mark_pos, pos); + if (idx == -1) + mark_pos = (pos > search.length) ? pos - search.length : 0; + else + { + int eos = idx + search.length; + idx = pos - eos; + pos = eos; + mark_pos = -1; + } + + return idx; + } +} diff --git a/HTTPClient/CIHashtable.java b/HTTPClient/CIHashtable.java new file mode 100644 index 0000000..5bc82e5 --- /dev/null +++ b/HTTPClient/CIHashtable.java @@ -0,0 +1,276 @@ +/* + * @(#)CIHashtable.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.util.Hashtable; +import java.util.Enumeration; + +/** + * This class implements a Hashtable with case-insensitive Strings as keys. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class CIHashtable extends Hashtable +{ + // Constructors + + /** + * Create a new CIHashtable with the specified initial capacity and the + * specified load factor. + * + * @param intialCapacity the initial number of buckets + * @param loadFactor a number between 0.0 and 1.0 + * @see java.util.Hashtable(int, float) + */ + public CIHashtable(int initialCapacity, float loadFactor) + { + super(initialCapacity, loadFactor); + } + + + /** + * Create a new CIHashtable with the specified initial capacity. + * + * @param intialCapacity the initial number of buckets + * @see java.util.Hashtable(int) + */ + public CIHashtable(int initialCapacity) + { + super(initialCapacity); + } + + + /** + * Create a new CIHashtable with a default initial capacity. + * + * @see java.util.Hashtable() + */ + public CIHashtable() + { + super(); + } + + + // Methods + + /** + * Retrieves the object associated with the specified key. The key lookup + * is case-insensitive. + * + * @param key the key + * @return the object associated with the key, or null if none found. + * @see java.util.Hashtable.get(Object) + */ + public Object get(String key) + { + return super.get(new CIString(key)); + } + + + /** + * Stores the specified object with the specified key. + * + * @param key the key + * @param value the object to be associated with the key + * @return the object previously associated with the key, or null if + * there was none. + * @see java.util.Hashtable.put(Object, Object) + */ + public Object put(String key, Object value) + { + return super.put(new CIString(key), value); + } + + + /** + * Looks whether any object is associated with the specified key. The + * key lookup is case insensitive. + * + * @param key the key + * @return true is there is an object associated with key, false otherwise + * @see java.util.Hashtable.containsKey(Object) + */ + public boolean containsKey(String key) + { + return super.containsKey(new CIString(key)); + } + + + /** + * Removes the object associated with this key from the Hashtable. The + * key lookup is case insensitive. + * + * @param key the key + * @return the object associated with this key, or null if there was none. + * @see java.util.Hashtable.remove(Object) + */ + public Object remove(String key) + { + return super.remove(new CIString(key)); + } + + + /** + * Returns an enumeration of all the keys in the Hashtable. + * + * @return the requested Enumerator + * @see java.util.Hashtable.keys(Object) + */ + public Enumeration keys() + { + return new CIHashtableEnumeration(super.keys()); + } +} + + +/** + * A simple enumerator which delegates everything to the real enumerator. + * If a CIString element is returned, then the string it represents is + * returned instead. + */ +final class CIHashtableEnumeration implements Enumeration +{ + Enumeration HTEnum; + + public CIHashtableEnumeration(Enumeration enum) + { + HTEnum = enum; + } + + public boolean hasMoreElements() + { + return HTEnum.hasMoreElements(); + } + + public Object nextElement() + { + Object tmp = HTEnum.nextElement(); + if (tmp instanceof CIString) + return ((CIString) tmp).getString(); + + return tmp; + } +} + + +/** + * This class' raison d'etre is that I want to use a Hashtable using + * Strings as keys and I want the lookup be case insensitive, but I + * also want to be able retrieve the keys with original case (otherwise + * I could just use toLowerCase() in the get() and put()). Since the + * class String is final we create a new class that holds the string + * and overrides the methods hashCode() and equals(). + */ +final class CIString +{ + /** the string */ + private String string; + + /** the hash code */ + private int hash; + + + /** the constructor */ + public CIString(String string) + { + this.string = string; + this.hash = calcHashCode(string); + } + + /** return the original string */ + public final String getString() + { + return string; + } + + /** the hash code was precomputed */ + public int hashCode() + { + return hash; + } + + + /** + * We smash case before calculation so that the hash code is + * "case insensitive". This is based on code snarfed from + * java.lang.String.hashCode(). + */ + private static final int calcHashCode(String str) + { + int hash = 0; + char llc[] = lc; + int len = str.length(); + + for (int idx= 0; idx 0) // it's data + { + if (len > chunk_len) len = (int) chunk_len; + int rcvd = in.read(buf, off, len); + if (rcvd == -1) + throw new EOFException("Premature EOF encountered"); + + chunk_len -= rcvd; + if (chunk_len == 0) // got the whole chunk + { + in.read(); // CR + in.read(); // LF + chunk_len = -1; + } + + return rcvd; + } + else // the footers (trailers) + { + // discard + Request dummy = + new Request(null, null, null, null, null, null, false); + new Response(dummy, null).readTrailers(in); + + eof = true; + return -1; + } + } + + + public synchronized long skip(long num) throws IOException + { + byte[] tmp = new byte[(int) num]; + int got = read(tmp, 0, (int) num); + + if (got > 0) + return (long) got; + else + return 0L; + } + + + public synchronized int available() throws IOException + { + if (eof) return 0; + + if (chunk_len != -1) + return (int) chunk_len + in.available(); + else + return in.available(); + } +} diff --git a/HTTPClient/Codecs.java b/HTTPClient/Codecs.java new file mode 100644 index 0000000..18476e1 --- /dev/null +++ b/HTTPClient/Codecs.java @@ -0,0 +1,1566 @@ +/* + * @(#)Codecs.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.util.BitSet; +import java.util.Vector; +import java.util.StringTokenizer; +import java.io.IOException; +import java.io.EOFException; +import java.io.InputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.URLConnection; + + +/** + * This class collects various encoders and decoders. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public class Codecs +{ + private static BitSet BoundChar; + private static BitSet EBCDICUnsafeChar; + private static byte[] Base64EncMap, Base64DecMap; + private static char[] UUEncMap; + private static byte[] UUDecMap; + + + private final static String ContDisp = "\r\nContent-Disposition: form-data; name=\""; + private final static String FileName = "\"; filename=\""; + private final static String ContType = "\r\nContent-Type: "; + private final static String Boundary = "\r\n----------ieoau._._+2_8_GoodLuck8.3-dskdfJwSJKl234324jfLdsjfdAuaoei-----"; + + + // Class Initializer + + static + { + // rfc-2046 & rfc-2045: (bcharsnospace & token) + // used for multipart codings + BoundChar = new BitSet(256); + for (int ch='0'; ch <= '9'; ch++) BoundChar.set(ch); + for (int ch='A'; ch <= 'Z'; ch++) BoundChar.set(ch); + for (int ch='a'; ch <= 'z'; ch++) BoundChar.set(ch); + BoundChar.set('+'); + BoundChar.set('_'); + BoundChar.set('-'); + BoundChar.set('.'); + + // EBCDIC unsafe characters to be quoted in quoted-printable + // See first NOTE in section 6.7 of rfc-2045 + EBCDICUnsafeChar = new BitSet(256); + EBCDICUnsafeChar.set('!'); + EBCDICUnsafeChar.set('"'); + EBCDICUnsafeChar.set('#'); + EBCDICUnsafeChar.set('$'); + EBCDICUnsafeChar.set('@'); + EBCDICUnsafeChar.set('['); + EBCDICUnsafeChar.set('\\'); + EBCDICUnsafeChar.set(']'); + EBCDICUnsafeChar.set('^'); + EBCDICUnsafeChar.set('`'); + EBCDICUnsafeChar.set('{'); + EBCDICUnsafeChar.set('|'); + EBCDICUnsafeChar.set('}'); + EBCDICUnsafeChar.set('~'); + + // rfc-2045: Base64 Alphabet + byte[] map = { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', + (byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', + (byte)'M', (byte)'N', (byte)'O', (byte)'P', (byte)'Q', (byte)'R', + (byte)'S', (byte)'T', (byte)'U', (byte)'V', (byte)'W', (byte)'X', + (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', + (byte)'g', (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', + (byte)'m', (byte)'n', (byte)'o', (byte)'p', (byte)'q', (byte)'r', + (byte)'s', (byte)'t', (byte)'u', (byte)'v', (byte)'w', (byte)'x', + (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' }; + Base64EncMap = map; + Base64DecMap = new byte[128]; + for (int idx=0; idxstr + */ + public final static String base64Encode(String str) + { + if (str == null) return null; + + try + { return new String(base64Encode(str.getBytes("8859_1")), "8859_1"); } + catch (UnsupportedEncodingException uee) + { throw new Error(uee.toString()); } + } + + + /** + * This method encodes the given byte[] using the base64-encoding + * specified in RFC-2045 (Section 6.8). + * + * @param data the data + * @return the base64-encoded data + */ + public final static byte[] base64Encode(byte[] data) + { + if (data == null) return null; + + int sidx, didx; + byte dest[] = new byte[((data.length+2)/3)*4]; + + + // 3-byte to 4-byte conversion + 0-63 to ascii printable conversion + for (sidx=0, didx=0; sidx < data.length-2; sidx += 3) + { + dest[didx++] = Base64EncMap[(data[sidx] >>> 2) & 077]; + dest[didx++] = Base64EncMap[(data[sidx+1] >>> 4) & 017 | + (data[sidx] << 4) & 077]; + dest[didx++] = Base64EncMap[(data[sidx+2] >>> 6) & 003 | + (data[sidx+1] << 2) & 077]; + dest[didx++] = Base64EncMap[data[sidx+2] & 077]; + } + if (sidx < data.length) + { + dest[didx++] = Base64EncMap[(data[sidx] >>> 2) & 077]; + if (sidx < data.length-1) + { + dest[didx++] = Base64EncMap[(data[sidx+1] >>> 4) & 017 | + (data[sidx] << 4) & 077]; + dest[didx++] = Base64EncMap[(data[sidx+1] << 2) & 077]; + } + else + dest[didx++] = Base64EncMap[(data[sidx] << 4) & 077]; + } + + // add padding + for ( ; didx < dest.length; didx++) + dest[didx] = (byte) '='; + + return dest; + } + + + /** + * This method decodes the given string using the base64-encoding + * specified in RFC-2045 (Section 6.8). + * + * @param str the base64-encoded string. + * @return the decoded str. + */ + public final static String base64Decode(String str) + { + if (str == null) return null; + + try + { return new String(base64Decode(str.getBytes("8859_1")), "8859_1"); } + catch (UnsupportedEncodingException uee) + { throw new Error(uee.toString()); } + } + + + /** + * This method decodes the given byte[] using the base64-encoding + * specified in RFC-2045 (Section 6.8). + * + * @param data the base64-encoded data. + * @return the decoded data. + */ + public final static byte[] base64Decode(byte[] data) + { + if (data == null) return null; + + int tail = data.length; + while (data[tail-1] == '=') tail--; + + byte dest[] = new byte[tail - data.length/4]; + + + // ascii printable to 0-63 conversion + for (int idx = 0; idx >> 4) & 003) ); + dest[didx+1] = (byte) ( ((data[sidx+1] << 4) & 255) | + ((data[sidx+2] >>> 2) & 017) ); + dest[didx+2] = (byte) ( ((data[sidx+2] << 6) & 255) | + (data[sidx+3] & 077) ); + } + if (didx < dest.length) + dest[didx] = (byte) ( ((data[sidx] << 2) & 255) | + ((data[sidx+1] >>> 4) & 003) ); + if (++didx < dest.length) + dest[didx] = (byte) ( ((data[sidx+1] << 4) & 255) | + ((data[sidx+2] >>> 2) & 017) ); + + return dest; + } + + + /** + * This method encodes the given byte[] using the unix uuencode + * encding. The output is split into lines starting with the encoded + * number of encoded octets in the line and ending with a newline. + * No line is longer than 45 octets (60 characters), not including + * length and newline. + * + *

Note: just the raw data is encoded; no 'begin' and 'end' + * lines are added as is done by the unix uuencode utility. + * + * @param data the data + * @return the uuencoded data + */ + public final static char[] uuencode(byte[] data) + { + if (data == null) return null; + if (data.length == 0) return new char[0]; + + int line_len = 45; // line length, in octets + + int sidx, didx; + char nl[] = System.getProperty("line.separator", "\n").toCharArray(), + dest[] = new char[(data.length+2)/3*4 + + ((data.length+line_len-1)/line_len)*(nl.length+1)]; + + // split into lines, adding line-length and line terminator + for (sidx=0, didx=0; sidx+line_len < data.length; ) + { + // line length + dest[didx++] = UUEncMap[line_len]; + + // 3-byte to 4-byte conversion + 0-63 to ascii printable conversion + for (int end = sidx+line_len; sidx < end; sidx += 3) + { + dest[didx++] = UUEncMap[(data[sidx] >>> 2) & 077]; + dest[didx++] = UUEncMap[(data[sidx+1] >>> 4) & 017 | + (data[sidx] << 4) & 077]; + dest[didx++] = UUEncMap[(data[sidx+2] >>> 6) & 003 | + (data[sidx+1] << 2) & 077]; + dest[didx++] = UUEncMap[data[sidx+2] & 077]; + } + + // line terminator + for (int idx=0; idx>> 2) & 077]; + dest[didx++] = UUEncMap[(data[sidx+1] >>> 4) & 017 | + (data[sidx] << 4) & 077]; + dest[didx++] = UUEncMap[(data[sidx+2] >>> 6) & 003 | + (data[sidx+1] << 2) & 077]; + dest[didx++] = UUEncMap[data[sidx+2] & 077]; + } + + if (sidx < data.length-1) + { + dest[didx++] = UUEncMap[(data[sidx] >>> 2) & 077]; + dest[didx++] = UUEncMap[(data[sidx+1] >>> 4) & 017 | + (data[sidx] << 4) & 077]; + dest[didx++] = UUEncMap[(data[sidx+1] << 2) & 077]; + dest[didx++] = UUEncMap[0]; + } + else if (sidx < data.length) + { + dest[didx++] = UUEncMap[(data[sidx] >>> 2) & 077]; + dest[didx++] = UUEncMap[(data[sidx] << 4) & 077]; + dest[didx++] = UUEncMap[0]; + dest[didx++] = UUEncMap[0]; + } + + // line terminator + for (int idx=0; idxrdr throws an IOException + */ + private final static byte[] uudecode(BufferedReader rdr) + throws ParseException, IOException + { + String line, file_name; + int file_mode; + + + // search for beginning + + while ((line = rdr.readLine()) != null && !line.startsWith("begin ")) + ; + if (line == null) + throw new ParseException("'begin' line not found"); + + + // parse 'begin' line + + StringTokenizer tok = new StringTokenizer(line); + tok.nextToken(); // throw away 'begin' + try // extract mode + { file_mode = Integer.parseInt(tok.nextToken(), 8); } + catch (Exception e) + { throw new ParseException("Invalid mode on line: " + line); } + try // extract name + { file_name = tok.nextToken(); } + catch (java.util.NoSuchElementException e) + { throw new ParseException("No file name found on line: " + line); } + + + // read and parse body + + byte[] body = new byte[1000]; + int off = 0; + + while ((line = rdr.readLine()) != null && !line.equals("end")) + { + byte[] tmp = uudecode(line.toCharArray()); + if (off + tmp.length > body.length) + body = Util.resizeArray(body, off+1000); + System.arraycopy(tmp, 0, body, off, tmp.length); + off += tmp.length; + } + + if (line == null) + throw new ParseException("'end' line not found"); + + + return Util.resizeArray(body, off); + } + + + /** + * This method decodes the given uuencoded char[]. + * + *

Note: just the actual data is decoded; any 'begin' and + * 'end' lines such as those generated by the unix uuencode + * utility must not be included. + * + * @param data the uuencode-encoded data. + * @return the decoded data. + */ + public final static byte[] uudecode(char[] data) + { + if (data == null) return null; + + int sidx, didx; + byte dest[] = new byte[data.length/4*3]; + + + for (sidx=0, didx=0; sidx < data.length; ) + { + // get line length (in number of encoded octets) + int len = UUDecMap[data[sidx++]]; + + // ascii printable to 0-63 and 4-byte to 3-byte conversion + int end = didx+len; + for (; didx < end-2; sidx += 4) + { + byte A = UUDecMap[data[sidx]], + B = UUDecMap[data[sidx+1]], + C = UUDecMap[data[sidx+2]], + D = UUDecMap[data[sidx+3]]; + dest[didx++] = (byte) ( ((A << 2) & 255) | ((B >>> 4) & 003) ); + dest[didx++] = (byte) ( ((B << 4) & 255) | ((C >>> 2) & 017) ); + dest[didx++] = (byte) ( ((C << 6) & 255) | (D & 077) ); + } + + if (didx < end) + { + byte A = UUDecMap[data[sidx]], + B = UUDecMap[data[sidx+1]]; + dest[didx++] = (byte) ( ((A << 2) & 255) | ((B >>> 4) & 003) ); + } + if (didx < end) + { + byte B = UUDecMap[data[sidx+1]], + C = UUDecMap[data[sidx+2]]; + dest[didx++] = (byte) ( ((B << 4) & 255) | ((C >>> 2) & 017) ); + } + + // skip padding + while (sidx < data.length && + data[sidx] != '\n' && data[sidx] != '\r') + sidx++; + + // skip end of line + while (sidx < data.length && + (data[sidx] == '\n' || data[sidx] == '\r')) + sidx++; + } + + return Util.resizeArray(dest, didx); + } + + + /** + * This method does a quoted-printable encoding of the given string + * according to RFC-2045 (Section 6.7). Note: this assumes + * 8-bit characters. + * + * @param str the string + * @return the quoted-printable encoded string + */ + public final static String quotedPrintableEncode(String str) + { + if (str == null) return null; + + char map[] = + {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}, + nl[] = System.getProperty("line.separator", "\n").toCharArray(), + res[] = new char[(int) (str.length()*1.5)], + src[] = str.toCharArray(); + char ch; + int cnt = 0, + didx = 1, + last = 0, + slen = str.length(); + + + for (int sidx=0; sidx < slen; sidx++) + { + ch = src[sidx]; + + if (ch == nl[0] && match(src, sidx, nl)) // Rule #4 + { + if (res[didx-1] == ' ') // Rule #3 + { + res[didx-1] = '='; + res[didx++] = '2'; + res[didx++] = '0'; + } + else if (res[didx-1] == '\t') // Rule #3 + { + res[didx-1] = '='; + res[didx++] = '0'; + res[didx++] = '9'; + } + + res[didx++] = '\r'; + res[didx++] = '\n'; + sidx += nl.length - 1; + cnt = didx; + } + else if (ch > 126 || (ch < 32 && ch != '\t') || ch == '=' || + EBCDICUnsafeChar.get((int) ch)) + { // Rule #1, #2 + res[didx++] = '='; + res[didx++] = map[(ch & 0xf0) >>> 4]; + res[didx++] = map[ch & 0x0f]; + } + else // Rule #1 + { + res[didx++] = ch; + } + + if (didx > cnt+70) // Rule #5 + { + res[didx++] = '='; + res[didx++] = '\r'; + res[didx++] = '\n'; + cnt = didx; + } + + if (didx > res.length-5) + res = Util.resizeArray(res, res.length+500); + } + + return String.valueOf(res, 1, didx-1); + } + + private final static boolean match(char[] str, int start, char[] arr) + { + if (str.length < start + arr.length) return false; + + for (int idx=1; idx < arr.length; idx++) + if (str[start+idx] != arr[idx]) return false; + return true; + } + + + /** + * This method does a quoted-printable decoding of the given string + * according to RFC-2045 (Section 6.7). Note: this method + * expects the whole message in one chunk, not line by line. + * + * @param str the message + * @return the decoded message + * @exception ParseException If a '=' is not followed by a valid + * 2-digit hex number or '\r\n'. + */ + public final static String quotedPrintableDecode(String str) + throws ParseException + { + if (str == null) return null; + + char res[] = new char[(int) (str.length()*1.1)], + src[] = str.toCharArray(), + nl[] = System.getProperty("line.separator", "\n").toCharArray(); + int last = 0, + didx = 0, + slen = str.length(); + + + for (int sidx=0; sidx res.length-nl.length-2) + res = Util.resizeArray(res, res.length+500); + } + + return new String(res, 0, didx); + } + + + /** + * This method urlencodes the given string. This method is here for + * symmetry reasons and just calls java.net.URLEncoder.encode(). + * + * @param str the string + * @return the url-encoded string + */ + public final static String URLEncode(String str) + { + if (str == null) return null; + + return java.net.URLEncoder.encode(str); + } + + + /** + * This method decodes the given urlencoded string. + * + * @param str the url-encoded string + * @return the decoded string + * @exception ParseException If a '%' is not followed by a valid + * 2-digit hex number. + */ + public final static String URLDecode(String str) throws ParseException + { + if (str == null) return null; + + char[] res = new char[str.length()]; + int didx = 0; + + for (int sidx=0; sidxcont_type parameter, which must be of the + * form 'multipart/form-data; boundary=...'. Any encoded files are created + * in the directory specified by dir using the encoded filename. + * + *

Note: Does not handle nested encodings (yet). + * + *

Examples: If you're receiving a multipart/form-data encoded response + * from a server you could use something like: + *

+     *     NVPair[] opts = Codecs.mpFormDataDecode(resp.getData(),
+     *                                  resp.getHeader("Content-type"), ".");
+     * 
+ * If you're using this in a Servlet to decode the body of a request from + * a client you could use something like: + *
+     *     byte[] body = new byte[req.getContentLength()];
+     *     new DataInputStream(req.getInputStream()).readFully(body);
+     *     NVPair[] opts = Codecs.mpFormDataDecode(body, req.getContentType(),
+     *                                             ".");
+     * 
+ * (where 'req' is the HttpServletRequest). + * + *

Assuming the data received looked something like: + *

+     * -----------------------------114975832116442893661388290519
+     * Content-Disposition: form-data; name="option"
+     *                                                          
+     * doit
+     * -----------------------------114975832116442893661388290519
+     * Content-Disposition: form-data; name="comment"; filename="comment.txt"
+     *                                                          
+     * Gnus and Gnats are not Gnomes.
+     * -----------------------------114975832116442893661388290519--
+     * 
+ * you would get one file called comment.txt in the current + * directory, and opts would contain two elements: {"option", "doit"} + * and {"comment", "comment.txt"} + * + * @param data the form-data to decode. + * @param cont_type the content type header (must contain the + * boundary string). + * @param dir the directory to create the files in. + * @param mangler the filename mangler, or null if no mangling is + * to be done. This is invoked just before each + * file is created and written, thereby allowing + * you to control the names of the files. + * @return an array of name/value pairs, one for each part; + * the name is the 'name' attribute given in the + * Content-Disposition header; the value is either + * the name of the file if a filename attribute was + * found, or the contents of the part. + * @exception IOException If any file operation fails. + * @exception ParseException If an error during parsing occurs. + */ + public final static NVPair[] mpFormDataDecode(byte[] data, String cont_type, + String dir, + FilenameMangler mangler) + throws IOException, ParseException + { + // Find and extract boundary string + + String bndstr = Util.getParameter("boundary", cont_type); + if (bndstr == null) + throw new ParseException("'boundary' parameter not found in Content-type: " + cont_type); + + byte[] srtbndry = ( "--" + bndstr + "\r\n").getBytes("8859_1"), + boundary = ("\r\n--" + bndstr + "\r\n").getBytes("8859_1"), + endbndry = ("\r\n--" + bndstr + "--" ).getBytes("8859_1"); + + + // setup search routines + + int[] bs = Util.compile_search(srtbndry), + bc = Util.compile_search(boundary), + be = Util.compile_search(endbndry); + + + // let's start parsing the actual data + + int start = Util.findStr(srtbndry, bs, data, 0, data.length); + if (start == -1) // didn't even find the start + throw new ParseException("Starting boundary not found: " + + new String(srtbndry, "8859_1")); + start += srtbndry.length; + + NVPair[] res = new NVPair[10]; + boolean done = false; + int idx; + + for (idx=0; !done; idx++) + { + // find end of this part + + int end = Util.findStr(boundary, bc, data, start, data.length); + if (end == -1) // must be the last part + { + end = Util.findStr(endbndry, be, data, start, data.length); + if (end == -1) + throw new ParseException("Ending boundary not found: " + + new String(endbndry, "8859_1")); + done = true; + } + + // parse header(s) + + String hdr, name=null, value, filename=null, cont_disp = null; + + while (true) + { + int next = findEOL(data, start) + 2; + if (next-2 <= start) break; // empty line -> end of headers + hdr = new String(data, start, next-2-start, "8859_1"); + start = next; + + // handle line continuation + byte ch; + while (next < data.length-1 && + ((ch = data[next]) == ' ' || ch == '\t')) + { + next = findEOL(data, start) + 2; + hdr += new String(data, start, next-2-start, "8859_1"); + start = next; + } + + if (!hdr.regionMatches(true, 0, "Content-Disposition", 0, 19)) + continue; + Vector pcd = + Util.parseHeader(hdr.substring(hdr.indexOf(':')+1)); + HttpHeaderElement elem = Util.getElement(pcd, "form-data"); + + if (elem == null) + throw new ParseException("Expected 'Content-Disposition: form-data' in line: "+hdr); + + NVPair[] params = elem.getParams(); + name = filename = null; + for (int pidx=0; pidx end) + throw new ParseException("End of header not found at offset "+end); + + if (cont_disp == null) + throw new ParseException("Missing 'Content-Disposition' header at offset "+start); + + // handle data for this part + + if (filename != null) // It's a file + { + if (mangler != null) + filename = mangler.mangleFilename(filename, name); + if (filename != null && filename.length() > 0) + { + File file = new File(dir, filename); + FileOutputStream out = new FileOutputStream(file); + + out.write(data, start, end-start); + out.close(); + } + + value = filename; + } + else // It's simple data + { + value = new String(data, start, end-start, "8859_1"); + } + + if (idx >= res.length) + res = Util.resizeArray(res, idx+10); + res[idx] = new NVPair(name, value); + + start = end + boundary.length; + } + + return Util.resizeArray(res, idx); + } + + + /** + * Searches for the next CRLF in an array. + * + * @param arr the byte array to search. + * @param off the offset at which to start the search. + * @return the position of the CR or (arr.length-2) if not found + */ + private final static int findEOL(byte[] arr, int off) + { + while (off < arr.length-1 && + !(arr[off++] == '\r' && arr[off] == '\n')); + return off-1; + } + + /** + * This method encodes name/value pairs and files into a byte array + * using the multipart/form-data encoding. + * + * @param opts the simple form-data to encode (may be null); + * for each NVPair the name refers to the 'name' + * attribute to be used in the header of the part, + * and the value is contents of the part. + * @param files the files to encode (may be null); for each + * NVPair the name refers to the 'name' attribute + * to be used in the header of the part, and the + * value is the actual filename (the file will be + * read and it's contents put in the body of that + * part). + * @param ct_hdr this returns a new NVPair in the 0'th element + * which contains name = "Content-Type", + * value = "multipart/form-data; boundary=..." + * (the reason this parameter is an array is + * because a) that's the only way to simulate + * pass-by-reference and b) you need an array for + * the headers parameter to the Post() or Put() + * anyway). + * @return an encoded byte array containing all the opts + * and files. + * @exception IOException If any file operation fails. + * @see #mpFormDataEncode(HTTPClient.NVPair[], HTTPClient.NVPair[], HTTPClient.NVPair[], HTTPClient.FilenameMangler) + */ + public final static byte[] mpFormDataEncode(NVPair[] opts, NVPair[] files, + NVPair[] ct_hdr) + throws IOException + { + return mpFormDataEncode(opts, files, ct_hdr, null); + } + + + private static NVPair[] dummy = new NVPair[0]; + + /** + * This method encodes name/value pairs and files into a byte array + * using the multipart/form-data encoding. The boundary is returned + * as part of ct_hdr. + *
Example: + *
+     *     NVPair[] opts = { new NVPair("option", "doit") };
+     *     NVPair[] file = { new NVPair("comment", "comment.txt") };
+     *     NVPair[] hdrs = new NVPair[1];
+     *     byte[]   data = Codecs.mpFormDataEncode(opts, file, hdrs);
+     *     con.Post("/cgi-bin/handle-it", data, hdrs);
+     * 
+ * data will look something like the following: + *
+     * -----------------------------114975832116442893661388290519
+     * Content-Disposition: form-data; name="option"
+     *                                                          
+     * doit
+     * -----------------------------114975832116442893661388290519
+     * Content-Disposition: form-data; name="comment"; filename="comment.txt"
+     * Content-Type: text/plain
+     *                                                          
+     * Gnus and Gnats are not Gnomes.
+     * -----------------------------114975832116442893661388290519--
+     * 
+ * where the "Gnus and Gnats ..." is the contents of the file + * comment.txt in the current directory. + * + *

If no elements are found in the parameters then a zero-length + * byte[] is returned and the content-type is set to + * application/octet-string (because a multipart must + * always have at least one part. + * + *

For files an attempt is made to discover the content-type, and if + * found a Content-Type header will be added to that part. The content type + * is retrieved using java.net.URLConnection.guessContentTypeFromName() - + * see java.net.URLConnection.setFileNameMap() for how to modify that map. + * Note that under JDK 1.1 by default the map seems to be empty. If you + * experience troubles getting the server to accept the data then make + * sure the fileNameMap is returning a content-type for each file (this + * may mean you'll have to set your own). + * + * @param opts the simple form-data to encode (may be null); + * for each NVPair the name refers to the 'name' + * attribute to be used in the header of the part, + * and the value is contents of the part. + * null elements in the array are ingored. + * @param files the files to encode (may be null); for each + * NVPair the name refers to the 'name' attribute + * to be used in the header of the part, and the + * value is the actual filename (the file will be + * read and it's contents put in the body of + * that part). null elements in the array + * are ingored. + * @param ct_hdr this returns a new NVPair in the 0'th element + * which contains name = "Content-Type", + * value = "multipart/form-data; boundary=..." + * (the reason this parameter is an array is + * because a) that's the only way to simulate + * pass-by-reference and b) you need an array for + * the headers parameter to the Post() or Put() + * anyway). The exception to this is that if no + * opts or files are given the type is set to + * "application/octet-stream" instead. + * @param mangler the filename mangler, or null if no mangling is + * to be done. This allows you to change the name + * used in the filename attribute of the + * Content-Disposition header. Note: the mangler + * will be invoked twice for each filename. + * @return an encoded byte array containing all the opts + * and files. + * @exception IOException If any file operation fails. + */ + public final static byte[] mpFormDataEncode(NVPair[] opts, NVPair[] files, + NVPair[] ct_hdr, + FilenameMangler mangler) + throws IOException + { + byte[] boundary = Boundary.getBytes("8859_1"), + cont_disp = ContDisp.getBytes("8859_1"), + cont_type = ContType.getBytes("8859_1"), + filename = FileName.getBytes("8859_1"); + int len = 0, + hdr_len = boundary.length + cont_disp.length+1 + 2 + 2; + // \r\n -- bnd \r\n C-D: ..; n=".." \r\n \r\n + + if (opts == null) opts = dummy; + if (files == null) files = dummy; + + + // Calculate the length of the data + + for (int idx=0; idx>8 & 0xff)) new_c += 0x00000100; + while (!BoundChar.get(new_c>>16 & 0xff)) new_c += 0x00010000; + while (!BoundChar.get(new_c>>24 & 0xff)) new_c += 0x01000000; + boundary[40] = (byte) (new_c & 0xff); + boundary[42] = (byte) (new_c>>8 & 0xff); + boundary[44] = (byte) (new_c>>16 & 0xff); + boundary[46] = (byte) (new_c>>24 & 0xff); + + int off = 2; + int[] bnd_cmp = Util.compile_search(boundary); + + for (int idx=0; idx= boundary.length && + Util.findStr(boundary, bnd_cmp, res, start, pos) != -1) + continue NewBound; + } + + for (int idx=0; idx 0) + { + int got = fin.read(res, pos, nlen); + nlen -= got; + pos += got; + } + fin.close(); + + if ((pos-start) >= boundary.length && + Util.findStr(boundary, bnd_cmp, res, start, pos) != -1) + continue NewBound; + } + + break NewBound; + } + + System.arraycopy(boundary, 0, res, pos, boundary.length); + pos += boundary.length; + res[pos++] = (byte) '-'; + res[pos++] = (byte) '-'; + res[pos++] = (byte) '\r'; + res[pos++] = (byte) '\n'; + + if (pos != len) + throw new Error("Calculated "+len+" bytes but wrote "+pos+" bytes!"); + + /* the boundary parameter should be quoted (rfc-2046, section 5.1.1) + * but too many script authors are not capable of reading specs... + * So, I give up and don't quote it. + */ + ct_hdr[0] = new NVPair("Content-Type", + "multipart/form-data; boundary=" + + new String(boundary, 4, boundary.length-4, "8859_1")); + + return res; + } + + private static class CT extends URLConnection + { + protected static final String getContentType(String fname) + { + return guessContentTypeFromName(fname); + } + + private CT() { super(null); } + public void connect() { } + } + + + /** + * Turns an array of name/value pairs into the string + * "name1=value1&name2=value2&name3=value3". The names and values are + * first urlencoded. This is the form in which form-data is passed to + * a cgi script. + * + * @param pairs the array of name/value pairs + * @return a string containg the encoded name/value pairs + */ + public final static String nv2query(NVPair pairs[]) + { + if (pairs == null) + return null; + + + int idx; + StringBuffer qbuf = new StringBuffer(); + + for (idx = 0; idx < pairs.length; idx++) + { + if (pairs[idx] != null) + qbuf.append(URLEncode(pairs[idx].getName()) + "=" + + URLEncode(pairs[idx].getValue()) + "&"); + } + + if (qbuf.length() > 0) + qbuf.setLength(qbuf.length()-1); // remove trailing '&' + + return qbuf.toString(); + } + + + /** + * Turns a string of the form "name1=value1&name2=value2&name3=value3" + * into an array of name/value pairs. The names and values are + * urldecoded. The query string is in the form in which form-data is + * received in a cgi script. + * + * @param query the query string containing the encoded name/value pairs + * @return an array of NVPairs + * @exception ParseException If the '=' is missing in any field, or if + * the urldecoding of the name or value fails + */ + public final static NVPair[] query2nv(String query) throws ParseException + { + if (query == null) return null; + + int idx = -1, + cnt = 1; + while ((idx = query.indexOf('&', idx+1)) != -1) cnt ++; + NVPair[] pairs = new NVPair[cnt]; + + for (idx=0, cnt=0; cnt= end) + throw new ParseException("'=' missing in " + + query.substring(idx, end)); + + pairs[cnt] = + new NVPair(URLDecode(query.substring(idx,eq)), + URLDecode(query.substring(eq+1,end))); + + idx = end + 1; + } + + return pairs; + } + + + /** + * Encodes data used the chunked encoding. last signales if + * this is the last chunk, in which case the appropriate footer is + * generated. + * + * @param data the data to be encoded; may be null. + * @param ftrs optional headers to include in the footer (ignored if + * not last); may be null. + * @param last whether this is the last chunk. + * @return an array of bytes containing the chunk + */ + public final static byte[] chunkedEncode(byte[] data, NVPair[] ftrs, + boolean last) + { + return + chunkedEncode(data, 0, data == null ? 0 : data.length, ftrs, last); + } + + + /** + * Encodes data used the chunked encoding. last signales if + * this is the last chunk, in which case the appropriate footer is + * generated. + * + * @param data the data to be encoded; may be null. + * @param off an offset into the data + * @param len the number of bytes to take from data + * @param ftrs optional headers to include in the footer (ignored if + * not last); may be null. + * @param last whether this is the last chunk. + * @return an array of bytes containing the chunk + */ + public final static byte[] chunkedEncode(byte[] data, int off, int len, + NVPair[] ftrs, boolean last) + { + if (data == null) + { + data = new byte[0]; + len = 0; + } + if (last && ftrs == null) ftrs = new NVPair[0]; + + // get length of data as hex-string + String hex_len = Integer.toString(len, 16); + + + // calculate length of chunk + + int res_len = 0; + if (len > 0) // len CRLF data CRLF + res_len += hex_len.length() + 2 + len + 2; + + if (last) + { + res_len += 1 + 2; // 0 CRLF + for (int idx=0; idx 0) + { + int hlen = hex_len.length(); + try + { System.arraycopy(hex_len.getBytes("8859_1"), 0, res, r_off, hlen); } + catch (UnsupportedEncodingException uee) + { throw new Error(uee.toString()); } + r_off += hlen; + res[r_off++] = (byte) '\r'; + res[r_off++] = (byte) '\n'; + + System.arraycopy(data, off, res, r_off, len); + r_off += len; + res[r_off++] = (byte) '\r'; + res[r_off++] = (byte) '\n'; + } + + if (last) + { + res[r_off++] = (byte) '0'; + res[r_off++] = (byte) '\r'; + res[r_off++] = (byte) '\n'; + + for (int idx=0; idx Integer.MAX_VALUE) // Huston, what the hell are you sending? + throw new ParseException("Can't deal with chunk lengths greater " + + "Integer.MAX_VALUE: " + clen + " > " + + Integer.MAX_VALUE); + + if (clen > 0) // it's a chunk + { + byte[] res = new byte[(int) clen]; + + int off = 0, len = 0; + while (len != -1 && off < res.length) + { + len = input.read(res, off, res.length-off); + off += len; + } + + if (len == -1) + throw new ParseException("Premature EOF while reading chunk;" + + "Expected: "+res.length+" Bytes, " + + "Received: "+(off+1)+" Bytes"); + + input.read(); // CR + input.read(); // LF + + return res; + } + else // it's the end + { + NVPair[] res = new NVPair[0]; + + BufferedReader reader = new BufferedReader(new InputStreamReader(input, "8859_1")); + String line; + + // read and parse footer + while ((line = reader.readLine()) != null && line.length() > 0) + { + int colon = line.indexOf(':'); + if (colon == -1) + throw new ParseException("Error in Footer format: no "+ + "':' found in '" + line + "'"); + res = Util.resizeArray(res, res.length+1); + res[res.length-1] = new NVPair(line.substring(0, colon).trim(), + line.substring(colon+1).trim()); + } + + return res; + } + + } + + + /** + * Gets the length of the chunk. + * + * @param input the stream from which to read the next chunk. + * @return the length of chunk to follow (w/o trailing CR LF). + * @exception ParseException If any exception during parsing occured. + * @exception IOException If any exception during reading occured. + */ + final static long getChunkLength(InputStream input) + throws ParseException, IOException + { + byte[] hex_len = new byte[16]; // if they send more than 8EB chunks... + int off = 0, + ch; + + + // read chunk length + + while ((ch = input.read()) > 0 && (ch == ' ' || ch == '\t')) ; + if (ch < 0) + throw new EOFException("Premature EOF while reading chunk length"); + hex_len[off++] = (byte) ch; + while ((ch = input.read()) > 0 && ch != '\r' && ch != '\n' && + ch != ' ' && ch != '\t' && ch != ';' && + off < hex_len.length) + hex_len[off++] = (byte) ch; + + while ((ch == ' ' || ch == '\t') && (ch = input.read()) > 0) ; + if (ch == ';') // chunk-ext (ignore it) + while ((ch = input.read()) > 0 && ch != '\r' && ch != '\n') ; + + if (ch < 0) + throw new EOFException("Premature EOF while reading chunk length"); + if (ch != '\n' && (ch != '\r' || input.read() != '\n')) + throw new ParseException("Didn't find valid chunk length: " + + new String(hex_len, 0, off, "8859_1")); + + // parse chunk length + + try + { return Long.parseLong(new String(hex_len, 0, off, "8859_1").trim(), + 16); } + catch (NumberFormatException nfe) + { throw new ParseException("Didn't find valid chunk length: " + + new String(hex_len, 0, off, "8859_1") ); } + } + +} diff --git a/HTTPClient/ContentEncodingModule.java b/HTTPClient/ContentEncodingModule.java new file mode 100644 index 0000000..00dec67 --- /dev/null +++ b/HTTPClient/ContentEncodingModule.java @@ -0,0 +1,219 @@ +/* + * @(#)ContentEncodingModule.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.util.Vector; +import java.util.zip.InflaterInputStream; +import java.util.zip.GZIPInputStream; + + +/** + * This module handles the Content-Encoding response header. It currently + * handles the "gzip", "deflate", "compress" and "identity" tokens. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class ContentEncodingModule implements HTTPClientModule +{ + // Methods + + /** + * Invoked by the HTTPClient. + */ + public int requestHandler(Request req, Response[] resp) + throws ModuleException + { + // parse Accept-Encoding header + + int idx; + NVPair[] hdrs = req.getHeaders(); + for (idx=0; idx 0.) + return REQ_CONTINUE; + } + catch (NumberFormatException nfe) + { + throw new ModuleException("Invalid q value for \"*\" in " + + "Accept-Encoding header: " + nfe.getMessage()); + } + } + + + // Add gzip, deflate and compress tokens to the Accept-Encoding header + + if (!pae.contains(new HttpHeaderElement("deflate"))) + pae.addElement(new HttpHeaderElement("deflate")); + if (!pae.contains(new HttpHeaderElement("gzip"))) + pae.addElement(new HttpHeaderElement("gzip")); + if (!pae.contains(new HttpHeaderElement("x-gzip"))) + pae.addElement(new HttpHeaderElement("x-gzip")); + if (!pae.contains(new HttpHeaderElement("compress"))) + pae.addElement(new HttpHeaderElement("compress")); + if (!pae.contains(new HttpHeaderElement("x-compress"))) + pae.addElement(new HttpHeaderElement("x-compress")); + + hdrs[idx] = new NVPair("Accept-Encoding", Util.assembleHeader(pae)); + + return REQ_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase1Handler(Response resp, RoRequest req) + { + } + + + /** + * Invoked by the HTTPClient. + */ + public int responsePhase2Handler(Response resp, Request req) + { + return RSP_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase3Handler(Response resp, RoRequest req) + throws IOException, ModuleException + { + String ce = resp.getHeader("Content-Encoding"); + if (ce == null || req.getMethod().equals("HEAD") || + resp.getStatusCode() == 206) + return; + + Vector pce; + try + { pce = Util.parseHeader(ce); } + catch (ParseException pe) + { throw new ModuleException(pe.toString()); } + + if (pce.size() == 0) + return; + + String encoding = ((HttpHeaderElement) pce.firstElement()).getName(); + if (encoding.equalsIgnoreCase("gzip") || + encoding.equalsIgnoreCase("x-gzip")) + { + Log.write(Log.MODS, "CEM: pushing gzip-input-stream"); + + resp.inp_stream = new GZIPInputStream(resp.inp_stream); + pce.removeElementAt(pce.size()-1); + resp.deleteHeader("Content-length"); + } + else if (encoding.equalsIgnoreCase("deflate")) + { + Log.write(Log.MODS, "CEM: pushing inflater-input-stream"); + + resp.inp_stream = new InflaterInputStream(resp.inp_stream); + pce.removeElementAt(pce.size()-1); + resp.deleteHeader("Content-length"); + } + else if (encoding.equalsIgnoreCase("compress") || + encoding.equalsIgnoreCase("x-compress")) + { + Log.write(Log.MODS, "CEM: pushing uncompress-input-stream"); + + resp.inp_stream = new UncompressInputStream(resp.inp_stream); + pce.removeElementAt(pce.size()-1); + resp.deleteHeader("Content-length"); + } + else if (encoding.equalsIgnoreCase("identity")) + { + Log.write(Log.MODS, "CEM: ignoring 'identity' token"); + pce.removeElementAt(pce.size()-1); + } + else + { + Log.write(Log.MODS, "CEM: Unknown content encoding '" + + encoding + "'"); + } + + if (pce.size() > 0) + resp.setHeader("Content-Encoding", Util.assembleHeader(pce)); + else + resp.deleteHeader("Content-Encoding"); + } + + + /** + * Invoked by the HTTPClient. + */ + public void trailerHandler(Response resp, RoRequest req) + { + } +} diff --git a/HTTPClient/ContentMD5Module.java b/HTTPClient/ContentMD5Module.java new file mode 100644 index 0000000..5aae29a --- /dev/null +++ b/HTTPClient/ContentMD5Module.java @@ -0,0 +1,185 @@ +/* + * @(#)ContentMD5Module.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; + + +/** + * This module handles the Content-MD5 response header. If this header was + * sent with a response and the entity isn't encoded using an unknown + * transport encoding then an MD5InputStream is wrapped around the response + * input stream. The MD5InputStream keeps a running digest and checks this + * against the expected digest from the Content-MD5 header the stream is + * closed. An IOException is thrown at that point if the digests don't match. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class ContentMD5Module implements HTTPClientModule +{ + // Constructors + + ContentMD5Module() + { + } + + + // Methods + + /** + * Invoked by the HTTPClient. + */ + public int requestHandler(Request req, Response[] resp) + { + return REQ_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase1Handler(Response resp, RoRequest req) + { + } + + + /** + * Invoked by the HTTPClient. + */ + public int responsePhase2Handler(Response resp, Request req) + { + return RSP_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase3Handler(Response resp, RoRequest req) + throws IOException, ModuleException + { + if (req.getMethod().equals("HEAD")) + return; + + String md5_digest = resp.getHeader("Content-MD5"); + String trailer = resp.getHeader("Trailer"); + boolean md5_tok = false; + try + { + if (trailer != null) + md5_tok = Util.hasToken(trailer, "Content-MD5"); + } + catch (ParseException pe) + { throw new ModuleException(pe.toString()); } + + if ((md5_digest == null && !md5_tok) || + resp.getHeader("Transfer-Encoding") != null) + return; + + if (md5_digest != null) + Log.write(Log.MODS, "CMD5M: Received digest: " + md5_digest + + " - pushing md5-check-stream"); + else + Log.write(Log.MODS, "CMD5M: Expecting digest in trailer " + + " - pushing md5-check-stream"); + + resp.inp_stream = new MD5InputStream(resp.inp_stream, + new VerifyMD5(resp)); + } + + + /** + * Invoked by the HTTPClient. + */ + public void trailerHandler(Response resp, RoRequest req) + { + } +} + + +class VerifyMD5 implements HashVerifier +{ + RoResponse resp; + + + public VerifyMD5(RoResponse resp) + { + this.resp = resp; + } + + + public void verifyHash(byte[] hash, long len) throws IOException + { + String hdr; + try + { + if ((hdr = resp.getHeader("Content-MD5")) == null) + hdr = resp.getTrailer("Content-MD5"); + } + catch (IOException ioe) + { return; } // shouldn't happen + + if (hdr == null) return; + + byte[] ContMD5 = Codecs.base64Decode(hdr.trim().getBytes("8859_1")); + + for (int idx=0; idx>> 4) & 15, 16)); + str.append(Character.forDigit(buf[idx] & 15, 16)); + str.append(':'); + } + str.setLength(str.length()-1); + + return str.toString(); + } +} diff --git a/HTTPClient/Cookie.java b/HTTPClient/Cookie.java new file mode 100644 index 0000000..055f0ad --- /dev/null +++ b/HTTPClient/Cookie.java @@ -0,0 +1,574 @@ +/* + * @(#)Cookie.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.Serializable; +import java.net.ProtocolException; +import java.util.Date; + + +/** + * This class represents an http cookie as specified in Netscape's + * cookie spec; however, because not even Netscape follows their own spec, + * and because very few folks out there actually read specs but instead just + * look whether Netscape accepts their stuff, the Set-Cookie header field + * parser actually tries to follow what Netscape has implemented, instead of + * what the spec says. Additionally, the parser it will also recognize the + * Max-Age parameter from rfc-2109, as that uses the + * same header field (Set-Cookie). + * + *

Some notes about how Netscape (4.7) parses: + *

    + *
  • Quoting: only quotes around the expires value are recognized as such; + * quotes around any other value are treated as part of the value. + *
  • White space: white space around names and values is ignored + *
  • Default path: if no path parameter is given, the path defaults to the + * path in the request-uri up to, but not including, the last '/'. Note + * that this is entirely different from what the spec says. + *
  • Commas and other delimiters: Netscape just parses until the next ';'. + * This means will allow commas etc inside values. + *
+ * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +public class Cookie implements Serializable +{ + /** Make this compatible with V0.3-2 */ + private static final long serialVersionUID = 8599975325569296615L; + + protected String name; + protected String value; + protected Date expires; + protected String domain; + protected String path; + protected boolean secure; + + + /** + * Create a cookie. + * + * @param name the cookie name + * @param value the cookie value + * @param domain the host this cookie will be sent to + * @param path the path prefix for which this cookie will be sent + * @param epxires the Date this cookie expires, null if at end of + * session + * @param secure if true this cookie will only be over secure connections + * @exception NullPointerException if name, value, + * domain, or path + * is null + * @since V0.3-1 + */ + public Cookie(String name, String value, String domain, String path, + Date expires, boolean secure) + { + if (name == null) throw new NullPointerException("missing name"); + if (value == null) throw new NullPointerException("missing value"); + if (domain == null) throw new NullPointerException("missing domain"); + if (path == null) throw new NullPointerException("missing path"); + + this.name = name; + this.value = value; + this.domain = domain.toLowerCase(); + this.path = path; + this.expires = expires; + this.secure = secure; + + if (this.domain.indexOf('.') == -1) this.domain += ".local"; + } + + + /** + * Use parse() to create cookies. + * + * @see #parse(java.lang.String, HTTPClient.RoRequest) + */ + protected Cookie(RoRequest req) + { + name = null; + value = null; + expires = null; + domain = req.getConnection().getHost(); + if (domain.indexOf('.') == -1) domain += ".local"; + path = Util.getPath(req.getRequestURI()); + /* This does not follow netscape's spec at all, but it's the way + * netscape seems to do it, and because people rely on that we + * therefore also have to do it... + */ + int slash = path.lastIndexOf('/'); + if (slash >= 0) + path = path.substring(0, slash); + secure = false; + } + + + /** + * Parses the Set-Cookie header into an array of Cookies. + * + * @param set_cookie the Set-Cookie header received from the server + * @param req the request used + * @return an array of Cookies as parsed from the Set-Cookie header + * @exception ProtocolException if an error occurs during parsing + */ + protected static Cookie[] parse(String set_cookie, RoRequest req) + throws ProtocolException + { + int beg = 0, + end = 0, + start = 0; + char[] buf = set_cookie.toCharArray(); + int len = buf.length; + + Cookie cookie_arr[] = new Cookie[0], curr; + + + cookies: while (true) // get all cookies + { + beg = Util.skipSpace(buf, beg); + if (beg >= len) break; // no more left + if (buf[beg] == ',') // empty header + { + beg++; + continue; + } + + curr = new Cookie(req); + start = beg; + + // get cookie name and value first + + end = set_cookie.indexOf('=', beg); + if (end == -1) + throw new ProtocolException("Bad Set-Cookie header: " + + set_cookie + "\nNo '=' found " + + "for token starting at " + + "position " + beg); + curr.name = set_cookie.substring(beg, end).trim(); + + beg = Util.skipSpace(buf, end+1); + int comma = set_cookie.indexOf(',', beg); + int semic = set_cookie.indexOf(';', beg); + if (comma == -1 && semic == -1) end = len; + else if (comma == -1) end = semic; + else if (semic == -1) end = comma; + else + { + if (comma > semic) + end = semic; + else + { + // try to handle broken servers which put commas + // into cookie values + int eq = set_cookie.indexOf('=', comma); + if (eq > 0 && eq < semic) + end = set_cookie.lastIndexOf(',', eq); + else + end = semic; + } + } + curr.value = set_cookie.substring(beg, end).trim(); + + beg = end; + + // now parse attributes + + boolean legal = true; + parts: while (true) // parse all parts + { + if (beg >= len || buf[beg] == ',') break; + + // skip empty fields + if (buf[beg] == ';') + { + beg = Util.skipSpace(buf, beg+1); + continue; + } + + // first check for secure, as this is the only one w/o a '=' + if ((beg+6 <= len) && + set_cookie.regionMatches(true, beg, "secure", 0, 6)) + { + curr.secure = true; + beg += 6; + + beg = Util.skipSpace(buf, beg); + if (beg < len && buf[beg] == ';') // consume ";" + beg = Util.skipSpace(buf, beg+1); + else if (beg < len && buf[beg] != ',') + throw new ProtocolException("Bad Set-Cookie header: " + + set_cookie + "\nExpected " + + "';' or ',' at position " + + beg); + + continue; + } + + // alright, must now be of the form x=y + end = set_cookie.indexOf('=', beg); + if (end == -1) + throw new ProtocolException("Bad Set-Cookie header: " + + set_cookie + "\nNo '=' found " + + "for token starting at " + + "position " + beg); + + String name = set_cookie.substring(beg, end).trim(); + beg = Util.skipSpace(buf, end+1); + + if (name.equalsIgnoreCase("expires")) + { + /* Netscape ignores quotes around the date, and some twits + * actually send that... + */ + if (set_cookie.charAt(beg) == '\"') + beg = Util.skipSpace(buf, beg+1); + + /* cut off the weekday if it is there. This is a little + * tricky because the comma is also used between cookies + * themselves. To make sure we don't inadvertantly + * mistake a date for a weekday we only skip letters. + */ + int pos = beg; + while (pos < len && + (buf[pos] >= 'a' && buf[pos] <= 'z' || + buf[pos] >= 'A' && buf[pos] <= 'Z')) + pos++; + pos = Util.skipSpace(buf, pos); + if (pos < len && buf[pos] == ',' && pos > beg) + beg = pos+1; + } + + comma = set_cookie.indexOf(',', beg); + semic = set_cookie.indexOf(';', beg); + if (comma == -1 && semic == -1) end = len; + else if (comma == -1) end = semic; + else if (semic == -1) end = comma; + else end = Math.min(comma, semic); + + String value = set_cookie.substring(beg, end).trim(); + legal &= setAttribute(curr, name, value, set_cookie); + + beg = end; + if (beg < len && buf[beg] == ';') // consume ";" + beg = Util.skipSpace(buf, beg+1); + } + + if (legal) + { + cookie_arr = Util.resizeArray(cookie_arr, cookie_arr.length+1); + cookie_arr[cookie_arr.length-1] = curr; + } else + Log.write(Log.COOKI, "Cooki: Ignoring cookie: " + curr); + } + + return cookie_arr; + } + + /** + * Set the given attribute, if valid. + * + * @param cookie the cookie on which to set the value + * @param name the name of the attribute + * @param value the value of the attribute + * @param set_cookie the complete Set-Cookie header + * @return true if the attribute is legal; false otherwise + */ + private static boolean setAttribute(Cookie cookie, String name, + String value, String set_cookie) + throws ProtocolException + { + if (name.equalsIgnoreCase("expires")) + { + if (value.charAt(value.length()-1) == '\"') + value = value.substring(0, value.length()-1).trim(); + try + // This is too strict... + // { cookie.expires = Util.parseHttpDate(value); } + { cookie.expires = new Date(value); } + catch (IllegalArgumentException iae) + { + /* More broken servers to deal with... Ignore expires + * if it's invalid + throw new ProtocolException("Bad Set-Cookie header: " + + set_cookie + "\nInvalid date found at " + + "position " + beg); + */ + Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie + + "\n Invalid date `" + value + "'"); + } + } + else if (name.equals("max-age")) // from rfc-2109 + { + if (cookie.expires != null) return true; + if (value.charAt(0) == '\"' && value.charAt(value.length()-1) == '\"') + value = value.substring(1, value.length()-1).trim(); + int age; + try + { age = Integer.parseInt(value); } + catch (NumberFormatException nfe) + { + throw new ProtocolException("Bad Set-Cookie header: " + + set_cookie + "\nMax-Age '" + value + + "' not a number"); + } + cookie.expires = new Date(System.currentTimeMillis() + age*1000L); + } + else if (name.equalsIgnoreCase("domain")) + { + // you get everything these days... + if (value.length() == 0) + { + Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie + + "\n domain is empty - ignoring domain"); + return true; + } + + // domains are case insensitive. + value = value.toLowerCase(); + + // add leading dot, if missing + if (value.length() != 0 && value.charAt(0) != '.' && + !value.equals(cookie.domain)) + value = '.' + value; + + // must be the same domain as in the url + if (!cookie.domain.endsWith(value)) + { + Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie + + "\n Current domain " + cookie.domain + + " does not match given parsed " + value); + return false; + } + + + /* Netscape's original 2-/3-dot rule really doesn't work because + * many countries use a shallow hierarchy (similar to the special + * TLDs defined in the spec). While the rules in rfc-2965 aren't + * perfect either, they are better. OTOH, some sites use a domain + * so that the host name minus the domain name contains a dot (e.g. + * host x.x.yahoo.com and domain .yahoo.com). So, for the seven + * special TLDs we use the 2-dot rule, and for all others we use + * the rules in the state-man draft instead. + */ + + // domain must be either .local or must contain at least + // two dots + if (!value.equals(".local") && value.indexOf('.', 1) == -1) + { + Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie + + "\n Domain attribute " + value + + "isn't .local and doesn't have at " + + "least 2 dots"); + return false; + } + + // If TLD not special then host minus domain may not + // contain any dots + String top = null; + if (value.length() > 3 ) + top = value.substring(value.length()-4); + if (top == null || !( + top.equalsIgnoreCase(".com") || + top.equalsIgnoreCase(".edu") || + top.equalsIgnoreCase(".net") || + top.equalsIgnoreCase(".org") || + top.equalsIgnoreCase(".gov") || + top.equalsIgnoreCase(".mil") || + top.equalsIgnoreCase(".int"))) + { + int dl = cookie.domain.length(), vl = value.length(); + if (dl > vl && + cookie.domain.substring(0, dl-vl).indexOf('.') != -1) + { + Log.write(Log.COOKI, "Cooki: Bad Set-Cookie header: " + set_cookie + + "\n Domain attribute " + value + + "is more than one level below " + + "current domain " + cookie.domain); + return false; + } + } + + cookie.domain = value; + } + else if (name.equalsIgnoreCase("path")) + cookie.path = value; + else + ; // unknown attribute - ignore + + return true; + } + + + /** + * Return the name of this cookie. + */ + public String getName() + { + return name; + } + + + /** + * Return the value of this cookie. + */ + public String getValue() + { + return value; + } + + + /** + * @return the expiry date of this cookie, or null if none set. + */ + public Date expires() + { + return expires; + } + + + /** + * @return true if the cookie should be discarded at the end of the + * session; false otherwise + */ + public boolean discard() + { + return (expires == null); + } + + + /** + * Return the domain this cookie is valid in. + */ + public String getDomain() + { + return domain; + } + + + /** + * Return the path this cookie is associated with. + */ + public String getPath() + { + return path; + } + + + /** + * Return whether this cookie should only be sent over secure connections. + */ + public boolean isSecure() + { + return secure; + } + + + /** + * @return true if this cookie has expired + */ + public boolean hasExpired() + { + return (expires != null && expires.getTime() <= System.currentTimeMillis()); + } + + + /** + * @param req the request to be sent + * @return true if this cookie should be sent with the request + */ + protected boolean sendWith(RoRequest req) + { + HTTPConnection con = req.getConnection(); + String eff_host = con.getHost(); + if (eff_host.indexOf('.') == -1) eff_host += ".local"; + + return ((domain.charAt(0) == '.' && eff_host.endsWith(domain) || + domain.charAt(0) != '.' && eff_host.equals(domain)) && + Util.getPath(req.getRequestURI()).startsWith(path) && + (!secure || con.getProtocol().equals("https") || + con.getProtocol().equals("shttp"))); + } + + + /** + * Hash up name, path and domain into new hash. + */ + public int hashCode() + { + return (name.hashCode() + path.hashCode() + domain.hashCode()); + } + + + /** + * Two cookies match if the name, path and domain match. + */ + public boolean equals(Object obj) + { + if ((obj != null) && (obj instanceof Cookie)) + { + Cookie other = (Cookie) obj; + return (this.name.equals(other.name) && + this.path.equals(other.path) && + this.domain.equals(other.domain)); + } + return false; + } + + + /** + * @return a string suitable for sending in a Cookie header. + */ + protected String toExternalForm() + { + return name + "=" + value; + } + + + /** + * Create a string containing all the cookie fields. The format is that + * used in the Set-Cookie header. + */ + public String toString() + { + StringBuffer res = new StringBuffer(name.length() + value.length() + 30); + res.append(name).append('=').append(value); + if (expires != null) res.append("; expires=").append(expires); + if (path != null) res.append("; path=").append(path); + if (domain != null) res.append("; domain=").append(domain); + if (secure) res.append("; secure"); + return res.toString(); + } +} diff --git a/HTTPClient/Cookie2.java b/HTTPClient/Cookie2.java new file mode 100644 index 0000000..25e4e07 --- /dev/null +++ b/HTTPClient/Cookie2.java @@ -0,0 +1,575 @@ +/* + * @(#)Cookie2.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.UnsupportedEncodingException; +import java.net.ProtocolException; +import java.util.Date; +import java.util.Vector; +import java.util.StringTokenizer; + + +/** + * This class represents an http cookie as specified in the HTTP State Management Mechanism spec + * (also known as a version 1 cookie). + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +public class Cookie2 extends Cookie +{ + /** Make this compatible with V0.3-2 */ + private static final long serialVersionUID = 2208203902820875917L; + + protected int version; + protected boolean discard; + protected String comment; + protected URI comment_url; + protected int[] port_list; + protected String port_list_str; + + protected boolean path_set; + protected boolean port_set; + protected boolean domain_set; + + + /** + * Create a cookie. + * + * @param name the cookie name + * @param value the cookie value + * @param domain the host this cookie will be sent to + * @param port_list an array of allowed server ports for this cookie, + * or null if the the cookie may be sent to any port + * @param path the path prefix for which this cookie will be sent + * @param epxires the Date this cookie expires, or null if never + * @param discard if true then the cookie will be discarded at the + * end of the session regardless of expiry + * @param secure if true this cookie will only be over secure connections + * @param comment the comment associated with this cookie, or null if none + * @param comment_url the comment URL associated with this cookie, or null + * if none + * @exception NullPointerException if name, value, + * domain, or path + * is null + */ + public Cookie2(String name, String value, String domain, int[] port_list, + String path, Date expires, boolean discard, boolean secure, + String comment, URI comment_url) + { + super(name, value, domain, path, expires, secure); + + this.discard = discard; + this.port_list = port_list; + this.comment = comment; + this.comment_url = comment_url; + + path_set = true; + domain_set = true; + + if (port_list != null && port_list.length > 0) + { + StringBuffer tmp = new StringBuffer(); + tmp.append(port_list[0]); + for (int idx=1; idxparse() to create cookies. + * + * @see #parse(java.lang.String, HTTPClient.RoRequest) + */ + protected Cookie2(RoRequest req) + { + super(req); + + path = Util.getPath(req.getRequestURI()); + int slash = path.lastIndexOf('/'); + if (slash != -1) path = path.substring(0, slash+1); + if (domain.indexOf('.') == -1) domain += ".local"; + + version = -1; + discard = false; + comment = null; + comment_url = null; + port_list = null; + port_list_str = null; + + path_set = false; + port_set = false; + domain_set = false; + } + + + /** + * Parses the Set-Cookie2 header into an array of Cookies. + * + * @param set_cookie the Set-Cookie2 header received from the server + * @param req the request used + * @return an array of Cookies as parsed from the Set-Cookie2 header + * @exception ProtocolException if an error occurs during parsing + */ + protected static Cookie[] parse(String set_cookie, RoRequest req) + throws ProtocolException + { + Vector cookies; + try + { cookies = Util.parseHeader(set_cookie); } + catch (ParseException pe) + { throw new ProtocolException(pe.getMessage()); } + + Cookie cookie_arr[] = new Cookie[cookies.size()]; + int cidx=0; + for (int idx=0; idxSet-Cookie + * and Set-Cookie2 response headers and sets the Cookie + * and Cookie2 headers as neccessary. + * + *

The accepting and sending of cookies is controlled by a + * CookiePolicyHandler. This allows you to fine tune your privacy + * preferences. A cookie is only added to the cookie list if the handler + * allows it, and a cookie from the cookie list is only sent if the handler + * allows it. + * + *

This module expects to be the only one handling cookies. Specifically, it + * will remove any Cookie and Cookie2 header fields found + * in the request, and it will remove the Set-Cookie and + * Set-Cookie2 header fields in the response (after processing them). + * In order to add cookies to a request or to prevent cookies from being sent, + * you can use the {@link #addCookie(HTTPClient.Cookie) addCookie} and {@link + * #removeCookie(HTTPClient.Cookie) removeCookie} methods to manipulate the + * module's list of cookies. + * + *

A cookie jar can be used to store cookies between sessions. This file is + * read when this class is loaded and is written when the application exits; + * only cookies from the default context are saved. The name of the file is + * controlled by the system property HTTPClient.cookies.jar and + * defaults to a system dependent name. The reading and saving of cookies is + * enabled by setting the system property HTTPClient.cookies.save + * to true. + * + * @see Netscape's cookie spec + * @see HTTP State Management Mechanism spec + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +public class CookieModule implements HTTPClientModule +{ + /** the list of known cookies */ + private static Hashtable cookie_cntxt_list = new Hashtable(); + + /** the file to use for persistent cookie storage */ + private static File cookie_jar = null; + + /** an object, whose finalizer will save the cookies to the jar */ + private static Object cookieSaver = null; + + /** the cookie policy handler */ + private static CookiePolicyHandler cookie_handler = + new DefaultCookiePolicyHandler(); + + + + // read in cookies from disk at startup + + static + { + boolean persist; + try + { persist = Boolean.getBoolean("HTTPClient.cookies.save"); } + catch (Exception e) + { persist = false; } + + if (persist) + { + loadCookies(); + + // the nearest thing to atexit() I know of... + + cookieSaver = new Object() + { + public void finalize() { saveCookies(); } + }; + try + { System.runFinalizersOnExit(true); } + catch (Throwable t) + { } + } + } + + + private static void loadCookies() + { + // The isFile() etc need to be protected by the catch as signed + // applets may be allowed to read properties but not do IO + try + { + cookie_jar = new File(getCookieJarName()); + if (cookie_jar.isFile() && cookie_jar.canRead()) + { + ObjectInputStream ois = + new ObjectInputStream(new FileInputStream(cookie_jar)); + cookie_cntxt_list.put(HTTPConnection.getDefaultContext(), + (Hashtable) ois.readObject()); + ois.close(); + } + } + catch (Throwable t) + { cookie_jar = null; } + } + + + private static void saveCookies() + { + if (cookie_jar != null && (!cookie_jar.exists() || + cookie_jar.isFile() && cookie_jar.canWrite())) + { + Hashtable cookie_list = new Hashtable(); + Enumeration enum = Util.getList(cookie_cntxt_list, + HTTPConnection.getDefaultContext()) + .elements(); + + // discard cookies which are not to be kept across sessions + + while (enum.hasMoreElements()) + { + Cookie cookie = (Cookie) enum.nextElement(); + if (!cookie.discard()) + cookie_list.put(cookie, cookie); + } + + + // save any remaining cookies in jar + + if (cookie_list.size() > 0) + { + try + { + ObjectOutputStream oos = + new ObjectOutputStream(new FileOutputStream(cookie_jar)); + oos.writeObject(cookie_list); + oos.close(); + } + catch (Throwable t) + { } + } + } + } + + + private static String getCookieJarName() + { + String file = null; + + try + { file = System.getProperty("HTTPClient.cookies.jar"); } + catch (Exception e) + { } + + if (file == null) + { + // default to something reasonable + + String os = System.getProperty("os.name"); + if (os.equalsIgnoreCase("Windows 95") || + os.equalsIgnoreCase("16-bit Windows") || + os.equalsIgnoreCase("Windows")) + { + file = System.getProperty("java.home") + + File.separator + ".httpclient_cookies"; + } + else if (os.equalsIgnoreCase("Windows NT")) + { + file = System.getProperty("user.home") + + File.separator + ".httpclient_cookies"; + } + else if (os.equalsIgnoreCase("OS/2")) + { + file = System.getProperty("user.home") + + File.separator + ".httpclient_cookies"; + } + else if (os.equalsIgnoreCase("Mac OS") || + os.equalsIgnoreCase("MacOS")) + { + file = "System Folder" + File.separator + + "Preferences" + File.separator + + "HTTPClientCookies"; + } + else // it's probably U*IX or VMS + { + file = System.getProperty("user.home") + + File.separator + ".httpclient_cookies"; + } + } + + return file; + } + + + // Constructors + + CookieModule() + { + } + + + // Methods + + /** + * Invoked by the HTTPClient. + */ + public int requestHandler(Request req, Response[] resp) + { + // First remove any Cookie headers we might have set for a previous + // request + + NVPair[] hdrs = req.getHeaders(); + int length = hdrs.length; + for (int idx=0; idx 0) + { + length -= idx-beg; + System.arraycopy(hdrs, idx, hdrs, beg, length-beg); + } + } + if (length < hdrs.length) + { + hdrs = Util.resizeArray(hdrs, length); + req.setHeaders(hdrs); + } + + + // Now set any new cookie headers + + Hashtable cookie_list = + Util.getList(cookie_cntxt_list, req.getConnection().getContext()); + if (cookie_list.size() == 0) + return REQ_CONTINUE; // no need to create a lot of objects + + Vector names = new Vector(); + Vector lens = new Vector(); + int version = 0; + + synchronized (cookie_list) + { + Enumeration list = cookie_list.elements(); + Vector remove_list = null; + + while (list.hasMoreElements()) + { + Cookie cookie = (Cookie) list.nextElement(); + + if (cookie.hasExpired()) + { + Log.write(Log.COOKI, "CookM: cookie has expired and is " + + "being removed: " + cookie); + if (remove_list == null) remove_list = new Vector(); + remove_list.addElement(cookie); + continue; + } + + if (cookie.sendWith(req) && (cookie_handler == null || + cookie_handler.sendCookie(cookie, req))) + { + int len = cookie.getPath().length(); + int idx; + + // insert in correct position + for (idx=0; idx 0) + value.append("$Version=\"" + version + "\"; "); + + value.append((String) names.elementAt(0)); + for (int idx=1; idxCookie.equals()) + * already exists in the list then it is replaced with the new cookie. + * + * @param cookie the Cookie to add + * @since V0.3-1 + */ + public static void addCookie(Cookie cookie) + { + Hashtable cookie_list = + Util.getList(cookie_cntxt_list, HTTPConnection.getDefaultContext()); + cookie_list.put(cookie, cookie); + } + + + /** + * Add the specified cookie to the list of cookies for the specified + * context. If a compatible cookie (as defined by + * Cookie.equals()) already exists in the list then it is + * replaced with the new cookie. + * + * @param cookie the cookie to add + * @param context the context Object. + * @since V0.3-1 + */ + public static void addCookie(Cookie cookie, Object context) + { + Hashtable cookie_list = Util.getList(cookie_cntxt_list, context); + cookie_list.put(cookie, cookie); + } + + + /** + * Remove the specified cookie from the list of cookies in the default + * context. If the cookie is not found in the list then this method does + * nothing. + * + * @param cookie the Cookie to remove + * @since V0.3-1 + */ + public static void removeCookie(Cookie cookie) + { + Hashtable cookie_list = + Util.getList(cookie_cntxt_list, HTTPConnection.getDefaultContext()); + cookie_list.remove(cookie); + } + + + /** + * Remove the specified cookie from the list of cookies for the specified + * context. If the cookie is not found in the list then this method does + * nothing. + * + * @param cookie the cookie to remove + * @param context the context Object + * @since V0.3-1 + */ + public static void removeCookie(Cookie cookie, Object context) + { + Hashtable cookie_list = Util.getList(cookie_cntxt_list, context); + cookie_list.remove(cookie); + } + + + /** + * Sets a new cookie policy handler. This handler will be called for each + * cookie that a server wishes to set and for each cookie that this + * module wishes to send with a request. In either case the handler may + * allow or reject the operation. If you wish to blindly accept and send + * all cookies then just disable the handler with + * CookieModule.setCookiePolicyHandler(null);. + * + *

At initialization time a default handler is installed. This + * handler allows all cookies to be sent. For any cookie that a server + * wishes to be set two lists are consulted. If the server matches any + * host or domain in the reject list then the cookie is rejected; if + * the server matches any host or domain in the accept list then the + * cookie is accepted (in that order). If no host or domain match is + * found in either of these two lists and user interaction is allowed + * then a dialog box is poped up to ask the user whether to accept or + * reject the cookie; if user interaction is not allowed the cookie is + * accepted. + * + *

The accept and reject lists in the default handler are initialized + * at startup from the two properties + * HTTPClient.cookies.hosts.accept and + * HTTPClient.cookies.hosts.reject. These properties must + * contain a "|" separated list of host and domain names. All names + * beginning with a "." are treated as domain names, all others as host + * names. An empty string will match all hosts. The two lists are + * further expanded if the user chooses one of the "Accept All from + * Domain" or "Reject All from Domain" buttons in the dialog box. + * + *

Note: the default handler does not implement the rules concerning + * unverifiable transactions (section 3.3.6, RFC-2965). The reason + * for this is simple: the default handler knows nothing about the + * application using this client, and it therefore does not have enough + * information to determine when a request is verifiable and when not. You + * are therefore encouraged to provide your own handler which implements + * section 3.3.6 (use the CookiePolicyHandler.sendCookie + * method for this). + * + * @param handler the new policy handler + * @return the previous policy handler + */ + public static synchronized CookiePolicyHandler + setCookiePolicyHandler(CookiePolicyHandler handler) + { + CookiePolicyHandler old = cookie_handler; + cookie_handler = handler; + return old; + } +} + + +/** + * A simple cookie policy handler. + */ +class DefaultCookiePolicyHandler implements CookiePolicyHandler +{ + /** a list of all hosts and domains from which to silently accept cookies */ + private String[] accept_domains = new String[0]; + + /** a list of all hosts and domains from which to silently reject cookies */ + private String[] reject_domains = new String[0]; + + /** the query popup */ + private BasicCookieBox popup = null; + + + DefaultCookiePolicyHandler() + { + // have all cookies been accepted or rejected? + String list; + + try + { list = System.getProperty("HTTPClient.cookies.hosts.accept"); } + catch (Exception e) + { list = null; } + String[] domains = Util.splitProperty(list); + for (int idx=0; idx 0) + domain += ".local"; + + for (int idx=0; idx 0) + domain += ".local"; + + for (int idx=0; idxBy default, when a username and password is required, this handler throws + * up a message box requesting the desired info. However, applications can + * {@link #setAuthorizationPrompter(HTTPClient.AuthorizationPrompter) set their + * own authorization prompter} if desired. + * + *

Note: all methods except for + * setAuthorizationPrompter are meant to be invoked by the + * AuthorizationModule only, i.e. should not be invoked by the application + * (those methods are only public because implementing the + * AuthorizationHandler interface requires them to be). + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.2 + */ +public class DefaultAuthHandler implements AuthorizationHandler, GlobalConstants +{ + private static final byte[] NUL = new byte[0]; + + private static final int DI_A1 = 0; + private static final int DI_A1S = 1; + private static final int DI_QOP = 2; + + private static byte[] digest_secret = null; + + private static AuthorizationPrompter prompter = null; + private static boolean prompterSet = false; + + + /** + * For Digest authentication we need to set the uri, response and + * opaque parameters. For "Basic" and "SOCKS5" nothing is done. + */ + public AuthorizationInfo fixupAuthInfo(AuthorizationInfo info, + RoRequest req, + AuthorizationInfo challenge, + RoResponse resp) + throws AuthSchemeNotImplException + { + // nothing to do for Basic and SOCKS5 schemes + + if (info.getScheme().equalsIgnoreCase("Basic") || + info.getScheme().equalsIgnoreCase("SOCKS5")) + return info; + else if (!info.getScheme().equalsIgnoreCase("Digest")) + throw new AuthSchemeNotImplException(info.getScheme()); + + if (Log.isEnabled(Log.AUTH)) + Log.write(Log.AUTH, "Auth: fixing up Authorization for host " + + info.getHost()+":"+info.getPort() + + "; scheme: " + info.getScheme() + + "; realm: " + info.getRealm()); + + return digest_fixup(info, req, challenge, resp); + } + + + /** + * returns the requested authorization, or null if none was given. + * + * @param challenge the parsed challenge from the server. + * @param req the request which solicited this response + * @param resp the full response received + * @return a structure containing the necessary authorization info, + * or null + * @exception AuthSchemeNotImplException if the authentication scheme + * in the challenge cannot be handled. + */ + public AuthorizationInfo getAuthorization(AuthorizationInfo challenge, + RoRequest req, RoResponse resp) + throws AuthSchemeNotImplException, IOException + { + AuthorizationInfo cred; + + + if (Log.isEnabled(Log.AUTH)) + Log.write(Log.AUTH, "Auth: Requesting Authorization for host " + + challenge.getHost()+":"+challenge.getPort() + + "; scheme: " + challenge.getScheme() + + "; realm: " + challenge.getRealm()); + + + // we only handle Basic, Digest and SOCKS5 authentication + + if (!challenge.getScheme().equalsIgnoreCase("Basic") && + !challenge.getScheme().equalsIgnoreCase("Digest") && + !challenge.getScheme().equalsIgnoreCase("SOCKS5")) + throw new AuthSchemeNotImplException(challenge.getScheme()); + + + // For digest authentication, check if stale is set + + if (challenge.getScheme().equalsIgnoreCase("Digest")) + { + cred = digest_check_stale(challenge, req, resp); + if (cred != null) + return cred; + } + + + // Ask the user for username/password + + NVPair answer; + synchronized (getClass()) + { + if (!req.allowUI() || prompterSet && prompter == null) + return null; + + if (prompter == null) + setDefaultPrompter(); + + answer = prompter.getUsernamePassword(challenge, + resp.getStatusCode() == 407); + } + + if (answer == null) + return null; + + + // Now process the username/password + + if (challenge.getScheme().equalsIgnoreCase("basic")) + { + cred = new AuthorizationInfo(challenge.getHost(), + challenge.getPort(), + challenge.getScheme(), + challenge.getRealm(), + Codecs.base64Encode( + answer.getName() + ":" + + answer.getValue())); + } + else if (challenge.getScheme().equalsIgnoreCase("Digest")) + { + cred = digest_gen_auth_info(challenge.getHost(), + challenge.getPort(), + challenge.getRealm(), answer.getName(), + answer.getValue(), + req.getConnection().getContext()); + cred = digest_fixup(cred, req, challenge, null); + } + else // SOCKS5 + { + NVPair[] upwd = { answer }; + cred = new AuthorizationInfo(challenge.getHost(), + challenge.getPort(), + challenge.getScheme(), + challenge.getRealm(), + upwd, null); + } + + + // try to get rid of any unencoded passwords in memory + + answer = null; + System.gc(); + + + // Done + + Log.write(Log.AUTH, "Auth: Got Authorization"); + + return cred; + } + + + /** + * We handle the "Authentication-Info" and "Proxy-Authentication-Info" + * headers here. + */ + public void handleAuthHeaders(Response resp, RoRequest req, + AuthorizationInfo prev, + AuthorizationInfo prxy) + throws IOException + { + String auth_info = resp.getHeader("Authentication-Info"); + String prxy_info = resp.getHeader("Proxy-Authentication-Info"); + + if (auth_info == null && prev != null && + hasParam(prev.getParams(), "qop", "auth-int")) + auth_info = ""; + + if (prxy_info == null && prxy != null && + hasParam(prxy.getParams(), "qop", "auth-int")) + prxy_info = ""; + + try + { + handleAuthInfo(auth_info, "Authentication-Info", prev, resp, req, + true); + handleAuthInfo(prxy_info, "Proxy-Authentication-Info", prxy, resp, + req, true); + } + catch (ParseException pe) + { throw new IOException(pe.toString()); } + } + + + /** + * We handle the "Authentication-Info" and "Proxy-Authentication-Info" + * trailers here. + */ + public void handleAuthTrailers(Response resp, RoRequest req, + AuthorizationInfo prev, + AuthorizationInfo prxy) + throws IOException + { + String auth_info = resp.getTrailer("Authentication-Info"); + String prxy_info = resp.getTrailer("Proxy-Authentication-Info"); + + try + { + handleAuthInfo(auth_info, "Authentication-Info", prev, resp, req, + false); + handleAuthInfo(prxy_info, "Proxy-Authentication-Info", prxy, resp, + req, false); + } + catch (ParseException pe) + { throw new IOException(pe.toString()); } + } + + + private static void handleAuthInfo(String auth_info, String hdr_name, + AuthorizationInfo prev, Response resp, + RoRequest req, boolean in_headers) + throws ParseException, IOException + { + if (auth_info == null) return; + + Vector pai = Util.parseHeader(auth_info); + HttpHeaderElement elem; + + if (handle_nextnonce(prev, req, + elem = Util.getElement(pai, "nextnonce"))) + pai.removeElement(elem); + if (handle_discard(prev, req, elem = Util.getElement(pai, "discard"))) + pai.removeElement(elem); + + if (in_headers) + { + HttpHeaderElement qop = null; + + if (pai != null && + (qop = Util.getElement(pai, "qop")) != null && + qop.getValue() != null) + { + handle_rspauth(prev, resp, req, pai, hdr_name); + } + else if (prev != null && + (Util.hasToken(resp.getHeader("Trailer"), hdr_name) && + hasParam(prev.getParams(), "qop", null) || + hasParam(prev.getParams(), "qop", "auth-int"))) + { + handle_rspauth(prev, resp, req, null, hdr_name); + } + + else if ((pai != null && qop == null && + pai.contains(new HttpHeaderElement("digest"))) || + (Util.hasToken(resp.getHeader("Trailer"), hdr_name) && + prev != null && + !hasParam(prev.getParams(), "qop", null))) + { + handle_digest(prev, resp, req, hdr_name); + } + } + + if (pai.size() > 0) + resp.setHeader(hdr_name, Util.assembleHeader(pai)); + else + resp.deleteHeader(hdr_name); + } + + + private static final boolean hasParam(NVPair[] params, String name, + String val) + { + for (int idx=0; idx> 8) & 0xFF); + time[2] = (byte) ((l_time >> 16) & 0xFF); + time[3] = (byte) ((l_time >> 24) & 0xFF); + time[4] = (byte) ((l_time >> 32) & 0xFF); + time[5] = (byte) ((l_time >> 40) & 0xFF); + time[6] = (byte) ((l_time >> 48) & 0xFF); + time[7] = (byte) ((l_time >> 56) & 0xFF); + + params[cnonce] = + new NVPair("cnonce", MD5.hexDigest(digest_secret, time)); + } + + + // select qop option + + if (ch_qop != -1) + { + if (qop == -1) + { + params = Util.resizeArray(params, params.length+1); + qop = params.length-1; + } + extra[DI_QOP] = ch_params[ch_qop].getValue(); + + + // select qop option + + String[] qops = splitList(extra[DI_QOP], ","); + String p = null; + for (int idx=0; idx= HTTP_1_1)) + { + p = "auth-int"; + break; + } + if (qops[idx].equalsIgnoreCase("auth")) + p = "auth"; + } + if (p == null) + { + for (int idx=0; idxnum bytes of random data. + * + * @param num the number of bytes to generate + * @return a byte array of random data + */ + private static byte[] gen_random_bytes(int num) + { + // first try /dev/random + try + { + FileInputStream rnd = new FileInputStream("/dev/random"); + DataInputStream din = new DataInputStream(rnd); + byte[] data = new byte[num]; + din.readFully(data); + try { din.close(); } catch (IOException ioe) { } + return data; + } + catch (Throwable t) + { } + + /* This is probably a much better generator, but it can be awfully + * slow (~ 6 secs / byte on my old LX) + */ + //return new java.security.SecureRandom().getSeed(num); + + /* this is faster, but needs to be done better... */ + byte[] data = new byte[num]; + try + { + long fm = Runtime.getRuntime().freeMemory(); + data[0] = (byte) (fm & 0xFF); + data[1] = (byte) ((fm >> 8) & 0xFF); + + int h = data.hashCode(); + data[2] = (byte) (h & 0xFF); + data[3] = (byte) ((h >> 8) & 0xFF); + data[4] = (byte) ((h >> 16) & 0xFF); + data[5] = (byte) ((h >> 24) & 0xFF); + + long time = System.currentTimeMillis(); + data[6] = (byte) (time & 0xFF); + data[7] = (byte) ((time >> 8) & 0xFF); + } + catch (ArrayIndexOutOfBoundsException aioobe) + { } + + return data; + } + + + /** + * Return the value of the first NVPair whose name matches the key + * using a case-insensitive search. + * + * @param list an array of NVPair's + * @param key the key to search for + * @return the value of the NVPair with that key, or null if not + * found. + */ + private final static String getValue(NVPair[] list, String key) + { + int len = list.length; + + for (int idx=0; idx> 4) & 15, 16)); + str.append(Character.forDigit(buf[idx] & 15, 16)); + str.append(':'); + } + str.setLength(str.length()-1); + + return str.toString(); + } + + + static final byte[] unHex(String hex) + { + byte[] digest = new byte[hex.length()/2]; + + for (int idx=0; idx= 0 || os.indexOf("SunOS") >= 0 || + os.indexOf("Solaris") >= 0 || os.indexOf("BSD") >= 0 || + os.indexOf("AIX") >= 0 || os.indexOf("HP-UX") >= 0 || + os.indexOf("IRIX") >= 0 || os.indexOf("OSF") >= 0 || + os.indexOf("A/UX") >= 0 || os.indexOf("VMS") >= 0); + } +} diff --git a/HTTPClient/DefaultModule.java b/HTTPClient/DefaultModule.java new file mode 100644 index 0000000..6d78419 --- /dev/null +++ b/HTTPClient/DefaultModule.java @@ -0,0 +1,153 @@ +/* + * @(#)DefaultModule.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.net.ProtocolException; + + +/** + * This is the default module which gets called after all other modules + * have done their stuff. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class DefaultModule implements HTTPClientModule +{ + /** number of times the request will be retried */ + private int req_timeout_retries; + + + // Constructors + + /** + * Three retries upon receipt of a 408. + */ + DefaultModule() + { + req_timeout_retries = 3; + } + + + // Methods + + /** + * Invoked by the HTTPClient. + */ + public int requestHandler(Request req, Response[] resp) + { + return REQ_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase1Handler(Response resp, RoRequest req) + { + } + + + /** + * Invoked by the HTTPClient. + */ + public int responsePhase2Handler(Response resp, Request req) + throws IOException + { + /* handle various response status codes until satisfied */ + + int sts = resp.getStatusCode(); + switch(sts) + { + case 408: // Request Timeout + + if (req_timeout_retries-- == 0 || req.getStream() != null) + { + Log.write(Log.MODS, "DefM: Status " + sts + " " + + resp.getReasonLine() + " not handled - " + + "maximum number of retries exceeded"); + + return RSP_CONTINUE; + } + else + { + Log.write(Log.MODS, "DefM: Handling " + sts + " " + + resp.getReasonLine() + " - " + + "resending request"); + + return RSP_REQUEST; + } + + case 411: // Length Required + if (req.getStream() != null && + req.getStream().getLength() == -1) + return RSP_CONTINUE; + + try { resp.getInputStream().close(); } + catch (IOException ioe) { } + if (req.getData() != null) + throw new ProtocolException("Received status code 411 even"+ + " though Content-Length was sent"); + + Log.write(Log.MODS, "DefM: Handling " + sts + " " + + resp.getReasonLine() + " - resending " + + "request with 'Content-length: 0'"); + + req.setData(new byte[0]); // will send Content-Length: 0 + return RSP_REQUEST; + + case 505: // HTTP Version not supported + return RSP_CONTINUE; + + default: + return RSP_CONTINUE; + } + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase3Handler(Response resp, RoRequest req) + { + } + + + /** + * Invoked by the HTTPClient. + */ + public void trailerHandler(Response resp, RoRequest req) + { + } +} diff --git a/HTTPClient/FilenameMangler.java b/HTTPClient/FilenameMangler.java new file mode 100644 index 0000000..16700fd --- /dev/null +++ b/HTTPClient/FilenameMangler.java @@ -0,0 +1,73 @@ +/* + * @(#)FilenameMangler.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + + +/** + * {@link Codecs#mpFormDataDecode(byte[], java.lang.String, java.lang.String, + * HTTPClient.FilenameMangler) Codecs.mpFormDataDecode} and {@link + * Codecs#mpFormDataEncode(HTTPClient.NVPair[], HTTPClient.NVPair[], + * HTTPClient.NVPair[], HTTPClient.FilenameMangler) Codecs.mpFormDataEncode} + * may be handed an instance of a class which implements this interface in + * order to control names of the decoded files or the names sent in the encoded + * data. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3-1 + */ +public interface FilenameMangler +{ + /** + * This is invoked by {@link Codecs#mpFormDataDecode(byte[], + * java.lang.String, java.lang.String, HTTPClient.FilenameMangler) + * Codecs.mpFormDataDecode} for each file found in the data, just before + * the file is created and written. If null is returned then the file is + * not created or written. This allows you to control which files are + * written and the names of the resulting files. + * + *

For {@link Codecs#mpFormDataEncode(HTTPClient.NVPair[], + * HTTPClient.NVPair[], HTTPClient.NVPair[], HTTPClient.FilenameMangler) + * Codecs.mpFormDataEncode} this is also invoked on each filename, allowing + * you to control the actual name used in the filename attribute + * of the Content-Disposition header. This does not change the name of the + * file actually read. If null is returned then the file is ignored. + * + * @param filename the original filename in the Content-Disposition header + * @param fieldname the name of the this field, i.e. the value of the + * name attribute in Content-Disposition + * header + * @return the new file name, or null if the file is to be ignored. + */ + public String mangleFilename(String filename, String fieldname); +} diff --git a/HTTPClient/GlobalConstants.java b/HTTPClient/GlobalConstants.java new file mode 100644 index 0000000..874d343 --- /dev/null +++ b/HTTPClient/GlobalConstants.java @@ -0,0 +1,63 @@ +/* + * @(#)GlobalConstants.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + + +/** + * This interface defines various global constants. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +interface GlobalConstants +{ + /** possible http protocols we (might) handle */ + int HTTP = 0; // plain http + int HTTPS = 1; // http on top of SSL + int SHTTP = 2; // secure http + int HTTP_NG = 3; // http next-generation + + /** some known http versions */ + int HTTP_1_0 = (1 << 16) + 0; + int HTTP_1_1 = (1 << 16) + 1; + + /** Content delimiters */ + int CD_NONE = 0; // raw read from the stream + int CD_HDRS = 1; // reading headers/trailers + int CD_0 = 2; // no body + int CD_CLOSE = 3; // by closing connection + int CD_CONTLEN = 4; // via the Content-Length header + int CD_CHUNKED = 5; // via chunked transfer encoding + int CD_MP_BR = 6; // via multipart/byteranges +} diff --git a/HTTPClient/HTTPClientModule.java b/HTTPClient/HTTPClientModule.java new file mode 100644 index 0000000..ec45f12 --- /dev/null +++ b/HTTPClient/HTTPClientModule.java @@ -0,0 +1,199 @@ +/* + * @(#)HTTPClientModule.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; + +/** + * This is the interface that a module must implement. There are two parts + * during a request: the construction of the request, and the handling of + * the response. A request may cycle through these parts multiple times + * when a module generates additional subrequests (such as a redirection + * status handling module might do). + * + *

In the first step the request handler is invoked; here the headers, + * the request-uri, etc. can be modified, or a complete response can be + * generated. Then, if no response was generated, the request is sent over + * the wire. In the second step the response handlers are invoked. These + * may modify the response or, in phase 2, may generate a new request; the + * returned status from the phase 2 handler specifies how the processing of + * the request or response should further proceed. + * + *

The response handling is split into three phases. In the first phase + * the response handling cannot be modified; this is so that all modules + * get a chance to see the returned response. Modules will typically make + * notes of responses and do certain header processing here (for example the + * cookie module does it's work in this phase). In the second phase modules + * may generate new subrequests or otherwise control the further handling of + * the response. This is typically used for response status handling (such + * as for redirections and authentication). Finally, if no new subrequest + * was generated, the phase 3 response handlers are invoked so that modules + * can perform any necessary cleanups and final processing (no additional + * subrequests can be made anymore). It is recommended that any response + * processing which needn't be done if the request is not returned to the + * user is deferred until this phase. For example, the Content-MD5, + * Content-Encoding and Transfer-Encoding modules do their work in this + * phase as the response body is usually discarded if a new subrequest is + * generated. + * + *

When the user invokes any request method (such as Get(...)) + * a list of of modules to be used is built. Then, for each module in the + * list, an instance is created using the Class.newInstance() + * method. This means that each module must have a constructor which takes + * no arguments. This instance is then used to handle the request, its + * response, and any additional subrequests and their responses. In this way + * a module can easily keep state between related subrequests. For example, a + * redirection module might want to keep track of the number of redirections + * made to detect redirect loops; it could do this by defining an instance + * variable and incrementing it each time the request handler is invoked. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +public interface HTTPClientModule extends HTTPClientModuleConstants +{ + /** + * This is invoked before the request is sent. A module will typically + * use this to make a note of headers, to modify headers and/or data, + * or even generate and return a response (e.g. for a cache module). + * If a response is generated the module must return the appropriate + * return code (REQ_RESPONSE or REQ_RETURN). + * + *

Return codes for phase 1 (defined in HTTPClientModuleConstants.java) + *

+ *
REQ_CONTINUE continue processing + *
REQ_RESTART restart processing with first module + *
REQ_SHORTCIRC stop processing and send + *
REQ_RESPONSE go to phase 2 + *
REQ_RETURN return response immediately (no processing) + *
REQ_NEWCON_RST use a new HTTPConnection, restart processing + *
REQ_NEWCON_SND use a new HTTPConnection, send immediately + *
+ * + * @param request the request - may be modified as needed + * @param response the response if the status is REQ_RESPONSE or REQ_RETURN + * @return status code REQ_XXX specifying further action + * @exception IOException if an IOException occurs on the socket + * @exception ModuleException if an exception occurs during the handling + * of the request + */ + public int requestHandler(Request request, Response[] response) + throws IOException, ModuleException; + + + /** + * The phase 1 response handler. This will be invoked for every response. + * Modules will typically make notes of the response and do any header + * processing which must always be performed. + * + * @param response the response - may be modified + * @param request the original request + * @exception IOException if an IOException occurs on the socket + * @exception ModuleException if an exception occurs during the handling + * of the response + */ + public void responsePhase1Handler(Response response, RoRequest request) + throws IOException, ModuleException; + + /** + * The phase 2 response handler. A module may modify the response or + * generate a new request (e.g. for redirection). This handler will + * only be invoked for a given module if all previous modules returned + * RSP_CONTINUE. If the request is modified the handler must + * return an appropriate return code (RSP_REQUEST, + * RSP_SEND, RSP_NEWCON_REQ or + * RSP_NEWCON_SND). If any other code is return the request + * must not be modified. + * + *

Return codes for phase 2 (defined in HTTPClientModuleConstants.java) + *

+ *
RSP_CONTINUE continue processing + *
RSP_RESTART restart processing with first module (phase 1) + *
RSP_SHORTCIRC stop processing and return + *
RSP_REQUEST go to phase 1 + *
RSP_SEND send request immediately (no processing) + *
RSP_NEWCON_REQ go to phase 1 using a new HTTPConnection + *
RSP_NEWCON_SND send request using a new HTTPConnection + *
+ * + * @param response the response - may be modified + * @param request the request; if the status is RSP_REQUEST then this + * must contain the new request; however do not modify + * this if you don't return a RSP_REQUEST status. + * @return status code RSP_XXX specifying further action + * @exception IOException if an IOException occurs on the socket + * @exception ModuleException if an exception occurs during the handling + * of the response + */ + public int responsePhase2Handler(Response response, Request request) + throws IOException, ModuleException; + + + /** + * The phase 3 response handler. This will only be invoked if no new + * subrequest was generated in phase 2. Modules should defer any repsonse + * handling which need only be done if the response is returned to the + * user to this phase. + * + * @param response the response - may be modified + * @param request the original request + * @exception IOException if an IOException occurs on the socket + * @exception ModuleException if an exception occurs during the handling + * of the response + */ + public void responsePhase3Handler(Response response, RoRequest request) + throws IOException, ModuleException; + + + /** + * The chunked transfer-encoding (and in future maybe others) can contain + * trailer fields at the end of the body. Since the + * responsePhaseXHandler()'s are invoked before the body is + * read and therefore do not have access to the trailers (unless they + * force the complete body to be read) this method will be invoked when + * the trailers have been read and parsed (sort of a post-response + * handling). + * + *

Note: This method must not modify any part of the + * response other than the trailers. + * + * @param response the response + * @param request the request + * @exception IOException if an IOException occurs on the socket + * @exception ModuleException if an exception occurs during the handling + * of the trailers + */ + public void trailerHandler(Response response, RoRequest request) + throws IOException, ModuleException; +} diff --git a/HTTPClient/HTTPClientModuleConstants.java b/HTTPClient/HTTPClientModuleConstants.java new file mode 100644 index 0000000..c50987d --- /dev/null +++ b/HTTPClient/HTTPClientModuleConstants.java @@ -0,0 +1,93 @@ +/* + * @(#)HTTPClientModuleConstants.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + + +/** + * This interface defines the return codes that the handlers in modules + * may return. + * + * @see HTTPClientModule + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +public interface HTTPClientModuleConstants +{ + // valid return codes for request handlers + + /** continue processing the request */ + int REQ_CONTINUE = 0; + + /** restart request processing with first module */ + int REQ_RESTART = 1; + + /** stop processing and send the request */ + int REQ_SHORTCIRC = 2; + + /** response generated; go to phase 2 */ + int REQ_RESPONSE = 3; + + /** response generated; return response immediately (no processing) */ + int REQ_RETURN = 4; + + /** using a new HTTPConnection, restart request processing */ + int REQ_NEWCON_RST = 5; + + /** using a new HTTPConnection, send request immediately */ + int REQ_NEWCON_SND = 6; + + + // valid return codes for the phase 2 response handlers + + /** continue processing response */ + int RSP_CONTINUE = 10; + + /** restart response processing with first module */ + int RSP_RESTART = 11; + + /** stop processing and return response */ + int RSP_SHORTCIRC = 12; + + /** new request generated; go to phase 1 */ + int RSP_REQUEST = 13; + + /** new request generated; send request immediately (no processing) */ + int RSP_SEND = 14; + + /** go to phase 1 using a new HTTPConnection */ + int RSP_NEWCON_REQ = 15; + + /** send request using a new HTTPConnection */ + int RSP_NEWCON_SND = 16; +} diff --git a/HTTPClient/HTTPConnection.java b/HTTPClient/HTTPConnection.java new file mode 100644 index 0000000..c78739b --- /dev/null +++ b/HTTPClient/HTTPConnection.java @@ -0,0 +1,3781 @@ +/* + * @(#)HTTPConnection.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.OutputStream; +import java.io.DataOutputStream; +import java.io.FilterOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.URL; +import java.net.Socket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.net.NoRouteToHostException; +import java.util.Vector; +import java.applet.Applet; + + +/** + * This class implements http protocol requests; it contains most of HTTP/1.1 + * and ought to be unconditionally compliant. + * Redirections are automatically handled, and authorizations requests are + * recognized and dealt with via an authorization handler. + * Only full HTTP/1.0 and HTTP/1.1 requests are generated. HTTP/1.1, HTTP/1.0 + * and HTTP/0.9 responses are recognized. + * + *

Using the HTTPClient should be quite simple. First add the import + * statement 'import HTTPClient.*;' to your file(s). Request + * can then be sent using one of the methods Head(), + * Get(), Post(), etc in HTTPConnection. + * These methods all return an instance of HTTPResponse which + * has methods for accessing the response headers (getHeader(), + * getHeaderAsInt(), etc), various response info + * (getStatusCode(), getReasonLine(), etc) and the + * reponse data (getData(), getText(), and + * getInputStream()). Following are some examples. + * + *

If this is in an applet you can retrieve files from your server + * as follows: + * + *

+ *     try
+ *     {
+ *         HTTPConnection con = new HTTPConnection(this);
+ *         HTTPResponse   rsp = con.Get("/my_file");
+ *         if (rsp.getStatusCode() >= 300)
+ *         {
+ *             System.err.println("Received Error: "+rsp.getReasonLine());
+ *             System.err.println(rsp.getText());
+ *         }
+ *         else
+ *             data = rsp.getData();
+ *
+ *         rsp = con.Get("/another_file");
+ *         if (rsp.getStatusCode() >= 300)
+ *         {
+ *             System.err.println("Received Error: "+rsp.getReasonLine());
+ *             System.err.println(rsp.getText());
+ *         }
+ *         else
+ *             other_data = rsp.getData();
+ *     }
+ *     catch (IOException ioe)
+ *     {
+ *         System.err.println(ioe.toString());
+ *     }
+ *     catch (ModuleException me)
+ *     {
+ *         System.err.println("Error handling request: " + me.getMessage());
+ *     }
+ * 
+ * + * This will get the files "/my_file" and "/another_file" and put their + * contents into byte[]'s accessible via getData(). Note that + * you need to only create a new HTTPConnection when sending a + * request to a new server (different host or port); although you may create + * a new HTTPConnection for every request to the same server this + * not recommended, as various information about the server + * is cached after the first request (to optimize subsequent requests) and + * persistent connections are used whenever possible. + * + *

To POST form data you would use something like this (assuming you + * have two fields called name and e-mail, whose + * contents are stored in the variables name and email): + * + *

+ *     try
+ *     {
+ *         NVPair form_data[] = new NVPair[2];
+ *         form_data[0] = new NVPair("name", name);
+ *         form_data[1] = new NVPair("e-mail", email);
+ *
+ *         HTTPConnection con = new HTTPConnection(this);
+ *         HTTPResponse   rsp = con.Post("/cgi-bin/my_script", form_data);
+ *         if (rsp.getStatusCode() >= 300)
+ *         {
+ *             System.err.println("Received Error: "+rsp.getReasonLine());
+ *             System.err.println(rsp.getText());
+ *         }
+ *         else
+ *             stream = rsp.getInputStream();
+ *     }
+ *     catch (IOException ioe)
+ *     {
+ *         System.err.println(ioe.toString());
+ *     }
+ *     catch (ModuleException me)
+ *     {
+ *         System.err.println("Error handling request: " + me.getMessage());
+ *     }
+ * 
+ * + * Here the response data is read at leasure via an InputStream + * instead of all at once into a byte[]. + * + *

As another example, if you have a URL you're trying to send a request + * to you would do something like the following: + * + *

+ *     try
+ *     {
+ *         URL url = new URL("http://www.mydomain.us/test/my_file");
+ *         HTTPConnection con = new HTTPConnection(url);
+ *         HTTPResponse   rsp = con.Put(url.getFile(), "Hello World");
+ *         if (rsp.getStatusCode() >= 300)
+ *         {
+ *             System.err.println("Received Error: "+rsp.getReasonLine());
+ *             System.err.println(rsp.getText());
+ *         }
+ *         else
+ *             text = rsp.getText();
+ *     }
+ *     catch (IOException ioe)
+ *     {
+ *         System.err.println(ioe.toString());
+ *     }
+ *     catch (ModuleException me)
+ *     {
+ *         System.err.println("Error handling request: " + me.getMessage());
+ *     }
+ * 
+ * + *

There are a whole number of methods for each request type; however the + * general forms are ([...] means that the enclosed is optional): + *

    + *
  • Head ( file [, form-data [, headers ] ] ) + *
  • Head ( file [, query [, headers ] ] ) + *
  • Get ( file [, form-data [, headers ] ] ) + *
  • Get ( file [, query [, headers ] ] ) + *
  • Post ( file [, form-data [, headers ] ] ) + *
  • Post ( file [, data [, headers ] ] ) + *
  • Post ( file [, stream [, headers ] ] ) + *
  • Put ( file , data [, headers ] ) + *
  • Put ( file , stream [, headers ] ) + *
  • Delete ( file [, headers ] ) + *
  • Options ( file [, headers [, data] ] ) + *
  • Options ( file [, headers [, stream] ] ) + *
  • Trace ( file [, headers ] ) + *
+ * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public class HTTPConnection implements GlobalConstants, HTTPClientModuleConstants +{ + /** The current version of this package. */ + public final static String version = "RPT-HTTPClient/0.3-3"; + + /** The default context */ + private final static Object dflt_context = new Object(); + + /** The current context */ + private Object Context = null; + + /** The protocol used on this connection */ + private int Protocol; + + /** The server's protocol version; M.m stored as (M<<16 | m) */ + int ServerProtocolVersion; + + /** Have we gotten the server's protocol version yet? */ + boolean ServProtVersKnown; + + /** The protocol version we send in a request; this is always HTTP/1.1 + unless we're talking to a broken server in which case it's HTTP/1.0 */ + private String RequestProtocolVersion; + + /** The remote host this connection is associated with */ + private String Host; + + /** The remote port this connection is attached to */ + private int Port; + + /** The local address this connection is associated with */ + private InetAddress LocalAddr; + + /** The local port this connection is attached to */ + private int LocalPort; + + /** The current proxy host to use (if any) */ + private String Proxy_Host = null; + + /** The current proxy port */ + private int Proxy_Port; + + /** The default proxy host to use (if any) */ + private static String Default_Proxy_Host = null; + + /** The default proxy port */ + private static int Default_Proxy_Port; + + /** The list of hosts for which no proxy is to be used */ + private static CIHashtable non_proxy_host_list = new CIHashtable(); + private static Vector non_proxy_dom_list = new Vector(); + private static Vector non_proxy_addr_list = new Vector(); + private static Vector non_proxy_mask_list = new Vector(); + + /** The socks server to use */ + private SocksClient Socks_client = null; + + /** The default socks server to use */ + private static SocksClient Default_Socks_client = null; + + /** the current stream demultiplexor */ + private StreamDemultiplexor input_demux = null; + + /** a list of active stream demultiplexors */ + LinkedList DemuxList = new LinkedList(); + + /** a list of active requests */ + private LinkedList RequestList = new LinkedList(); + + /** does the server support keep-alive's? */ + private boolean doesKeepAlive = false; + + /** have we been able to determine the above yet? */ + private boolean keepAliveUnknown = true; + + /** the maximum number of requests over a HTTP/1.0 keep-alive connection */ + private int keepAliveReqMax = -1; + + /** the number of requests over a HTTP/1.0 keep-alive connection left */ + private int keepAliveReqLeft; + + /** hack to force buffering of data instead of using chunked T-E */ + private static boolean no_chunked = false; + + /** hack to force HTTP/1.0 requests */ + private static boolean force_1_0 = false; + + /** hack to be able to disable pipelining */ + private static boolean neverPipeline = false; + + /** hack to be able to disable keep-alives */ + private static boolean noKeepAlives = false; + + /** hack to work around M$ bug */ + private static boolean haveMSLargeWritesBug = false; + + /** hack to only enable defered handling of streamed requests when + * configured to do so. */ + static boolean deferStreamed = false; + + /** the default timeout to use for new connections */ + private static int DefaultTimeout = 0; + + /** the timeout to use for reading responses */ + private int Timeout; + + /** The list of default http headers */ + private NVPair[] DefaultHeaders = new NVPair[0]; + + /** The default list of modules (as a Vector of Class objects) */ + private static Vector DefaultModuleList; + + /** The list of modules (as a Vector of Class objects) */ + private Vector ModuleList; + + /** controls whether modules are allowed to interact with user */ + private static boolean defaultAllowUI = true; + + /** controls whether modules are allowed to interact with user */ + private boolean allowUI; + + + static + { + /* + * Let's try and see if we can figure out whether any proxies are + * being used. + */ + + try // JDK 1.1 naming + { + String host = System.getProperty("http.proxyHost"); + if (host == null) + throw new Exception(); // try JDK 1.0.x naming + int port = Integer.getInteger("http.proxyPort", -1).intValue(); + + Log.write(Log.CONN, "Conn: using proxy " + host + ":" + port); + setProxyServer(host, port); + } + catch (Exception e) + { + try // JDK 1.0.x naming + { + if (Boolean.getBoolean("proxySet")) + { + String host = System.getProperty("proxyHost"); + int port = Integer.getInteger("proxyPort", -1).intValue(); + Log.write(Log.CONN, "Conn: using proxy " + host + ":" + port); + setProxyServer(host, port); + } + } + catch (Exception ee) + { Default_Proxy_Host = null; } + } + + + /* + * now check for the non-proxy list + */ + try + { + String hosts = System.getProperty("HTTPClient.nonProxyHosts"); + if (hosts == null) + hosts = System.getProperty("http.nonProxyHosts"); + + String[] list = Util.splitProperty(hosts); + dontProxyFor(list); + } + catch (Exception e) + { } + + + /* + * we can't turn the JDK SOCKS handling off, so we don't use the + * properties 'socksProxyHost' and 'socksProxyPort'. Instead we + * define 'HTTPClient.socksHost', 'HTTPClient.socksPort' and + * 'HTTPClient.socksVersion'. + */ + try + { + String host = System.getProperty("HTTPClient.socksHost"); + if (host != null && host.length() > 0) + { + int port = Integer.getInteger("HTTPClient.socksPort", -1).intValue(); + int version = Integer.getInteger("HTTPClient.socksVersion", -1).intValue(); + Log.write(Log.CONN, "Conn: using SOCKS " + host + ":" + port); + if (version == -1) + setSocksServer(host, port); + else + setSocksServer(host, port, version); + } + } + catch (Exception e) + { Default_Socks_client = null; } + + + // Set up module list + + String modules = "HTTPClient.RetryModule|" + + "HTTPClient.CookieModule|" + + "HTTPClient.RedirectionModule|" + + "HTTPClient.AuthorizationModule|" + + "HTTPClient.DefaultModule|" + + "HTTPClient.TransferEncodingModule|" + + "HTTPClient.ContentMD5Module|" + + "HTTPClient.ContentEncodingModule"; + + boolean in_applet = false; + try + { modules = System.getProperty("HTTPClient.Modules", modules); } + catch (SecurityException se) + { in_applet = true; } + + DefaultModuleList = new Vector(); + String[] list = Util.splitProperty(modules); + for (int idx=0; idx= 0 && + System.getProperty("java.version").startsWith("1.1")) + haveMSLargeWritesBug = true; + if (haveMSLargeWritesBug) + Log.write(Log.CONN, "Conn: splitting large writes into 20K chunks (M$ bug)"); + } + catch (Exception e) + { } + + /* + * Deferring the handling of responses to requests which used an output + * stream is new in V0.3-3. Because it can cause memory leaks for apps + * which aren't expecting this, we only enable this feature if + * explicitly requested to do so. + */ + try + { + deferStreamed = Boolean.getBoolean("HTTPClient.deferStreamed"); + if (deferStreamed) + Log.write(Log.CONN, "Conn: enabling defered handling of " + + "responses to streamed requests"); + } + catch (Exception e) + { } + } + + + // Constructors + + /** + * Constructs a connection to the host from where the applet was loaded. + * Note that current security policies only let applets connect home. + * + * @param applet the current applet + */ + public HTTPConnection(Applet applet) throws ProtocolNotSuppException + { + this(applet.getCodeBase().getProtocol(), + applet.getCodeBase().getHost(), + applet.getCodeBase().getPort()); + } + + /** + * Constructs a connection to the specified host on port 80 + * + * @param host the host + */ + public HTTPConnection(String host) + { + Setup(HTTP, host, 80, null, -1); + } + + /** + * Constructs a connection to the specified host on the specified port + * + * @param host the host + * @param port the port + */ + public HTTPConnection(String host, int port) + { + Setup(HTTP, host, port, null, -1); + } + + /** + * Constructs a connection to the specified host on the specified port, + * using the specified protocol (currently only "http" is supported). + * + * @param prot the protocol + * @param host the host + * @param port the port, or -1 for the default port + * @exception ProtocolNotSuppException if the protocol is not HTTP + */ + public HTTPConnection(String prot, String host, int port) + throws ProtocolNotSuppException + { + this(prot, host, port, null, -1); + } + + /** + * Constructs a connection to the specified host on the specified port, + * using the specified protocol (currently only "http" is supported), + * local address, and local port. + * + * @param prot the protocol + * @param host the host + * @param port the port, or -1 for the default port + * @param localAddr the local address to bind to + * @param lcoalPort the local port to bind to + * @exception ProtocolNotSuppException if the protocol is not HTTP + */ + public HTTPConnection(String prot, String host, int port, + InetAddress localAddr, int localPort) + throws ProtocolNotSuppException + { + prot = prot.trim().toLowerCase(); + + //if (!prot.equals("http") && !prot.equals("https")) + if (!prot.equals("http")) + throw new ProtocolNotSuppException("Unsupported protocol '" + prot + "'"); + + if (prot.equals("http")) + Setup(HTTP, host, port, localAddr, localPort); + else if (prot.equals("https")) + Setup(HTTPS, host, port, localAddr, localPort); + else if (prot.equals("shttp")) + Setup(SHTTP, host, port, localAddr, localPort); + else if (prot.equals("http-ng")) + Setup(HTTP_NG, host, port, localAddr, localPort); + } + + /** + * Constructs a connection to the host (port) as given in the url. + * + * @param url the url + * @exception ProtocolNotSuppException if the protocol is not HTTP + */ + public HTTPConnection(URL url) throws ProtocolNotSuppException + { + this(url.getProtocol(), url.getHost(), url.getPort()); + } + + /** + * Constructs a connection to the host (port) as given in the uri. + * + * @param uri the uri + * @exception ProtocolNotSuppException if the protocol is not HTTP + */ + public HTTPConnection(URI uri) throws ProtocolNotSuppException + { + this(uri.getScheme(), uri.getHost(), uri.getPort()); + } + + /** + * Sets the class variables. Must not be public. + * + * @param prot the protocol + * @param host the host + * @param port the port + * @param localAddr the local address to bind to; if null, it's ignored + * @param localPort the local port to bind to + */ + private void Setup(int prot, String host, int port, InetAddress localAddr, + int localPort) + { + Protocol = prot; + Host = host.trim().toLowerCase(); + Port = port; + LocalAddr = localAddr; + LocalPort = localPort; + + if (Port == -1) + Port = URI.defaultPort(getProtocol()); + + if (Default_Proxy_Host != null && !matchNonProxy(Host)) + setCurrentProxy(Default_Proxy_Host, Default_Proxy_Port); + else + setCurrentProxy(null, 0); + + Socks_client = Default_Socks_client; + Timeout = DefaultTimeout; + ModuleList = (Vector) DefaultModuleList.clone(); + allowUI = defaultAllowUI; + if (noKeepAlives) + setDefaultHeaders(new NVPair[] { new NVPair("Connection", "close") }); + } + + + /** + * Determines if the given host matches any entry in the non-proxy list. + * + * @param host the host to match - must be trim()'d and lowercase + * @return true if a match is found, false otherwise + * @see #dontProxyFor(java.lang.String) + */ + private boolean matchNonProxy(String host) + { + // Check host name list + + if (non_proxy_host_list.get(host) != null) + return true; + + + // Check domain name list + + for (int idx=0; idx 0) + File += "?" + query; + + return setupRequest("HEAD", File, headers, null, null); + } + + /** + * Sends the HEAD request. This request is just like the corresponding + * GET except that it only returns the headers and no data. + * + * @see #Get(java.lang.String, java.lang.String) + * @param file the absolute path of the file + * @param query the query string; it will be urlencoded + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Head(String file, String query) + throws IOException, ModuleException + { + return Head(file, query, null); + } + + + /** + * Sends the HEAD request. This request is just like the corresponding + * GET except that it only returns the headers and no data. + * + * @see #Get(java.lang.String, java.lang.String, HTTPClient.NVPair[]) + * @param file the absolute path of the file + * @param query the query string; it will be urlencoded + * @param headers additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Head(String file, String query, NVPair[] headers) + throws IOException, ModuleException + { + String File = stripRef(file); + if (query != null && query.length() > 0) + File += "?" + Codecs.URLEncode(query); + + return setupRequest("HEAD", File, headers, null, null); + } + + + /** + * GETs the file. + * + * @param file the absolute path of the file + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Get(String file) throws IOException, ModuleException + { + return Get(file, (String) null, null); + } + + /** + * GETs the file with a query consisting of the specified form-data. + * The data is urlencoded, turned into a string of the form + * "name1=value1&name2=value2" and then sent as a query string. + * + * @param file the absolute path of the file + * @param form_data an array of Name/Value pairs + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Get(String file, NVPair form_data[]) + throws IOException, ModuleException + { + return Get(file, form_data, null); + } + + /** + * GETs the file with a query consisting of the specified form-data. + * The data is urlencoded, turned into a string of the form + * "name1=value1&name2=value2" and then sent as a query string. + * + * @param file the absolute path of the file + * @param form_data an array of Name/Value pairs + * @param headers additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Get(String file, NVPair[] form_data, NVPair[] headers) + throws IOException, ModuleException + { + String File = stripRef(file), + query = Codecs.nv2query(form_data); + if (query != null && query.length() > 0) + File += "?" + query; + + return setupRequest("GET", File, headers, null, null); + } + + /** + * GETs the file using the specified query string. The query string + * is first urlencoded. + * + * @param file the absolute path of the file + * @param query the query + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Get(String file, String query) + throws IOException, ModuleException + { + return Get(file, query, null); + } + + /** + * GETs the file using the specified query string. The query string + * is first urlencoded. + * + * @param file the absolute path of the file + * @param query the query string + * @param headers additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Get(String file, String query, NVPair[] headers) + throws IOException, ModuleException + { + String File = stripRef(file); + if (query != null && query.length() > 0) + File += "?" + Codecs.URLEncode(query); + + return setupRequest("GET", File, headers, null, null); + } + + + /** + * POSTs to the specified file. No data is sent. + * + * @param file the absolute path of the file + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Post(String file) throws IOException, ModuleException + { + return Post(file, (byte []) null, null); + } + + /** + * POSTs form-data to the specified file. The data is first urlencoded + * and then turned into a string of the form "name1=value1&name2=value2". + * A Content-type header with the value + * application/x-www-form-urlencoded is added. + * + * @param file the absolute path of the file + * @param form_data an array of Name/Value pairs + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Post(String file, NVPair form_data[]) + throws IOException, ModuleException + { + NVPair[] headers = + { new NVPair("Content-type", "application/x-www-form-urlencoded") }; + + return Post(file, Codecs.nv2query(form_data), headers); + } + + /** + * POST's form-data to the specified file using the specified headers. + * The data is first urlencoded and then turned into a string of the + * form "name1=value1&name2=value2". If no Content-type header + * is given then one is added with a value of + * application/x-www-form-urlencoded. + * + * @param file the absolute path of the file + * @param form_data an array of Name/Value pairs + * @param headers additional headers + * @return a HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Post(String file, NVPair form_data[], NVPair headers[]) + throws IOException, ModuleException + { + int idx; + for (idx=0; idx 0) + tmp = data.getBytes(); + + return Post(file, tmp, headers); + } + + /** + * POSTs the raw data to the specified file. + * The request is sent using the content-type "application/octet-stream" + * + * @param file the absolute path of the file + * @param data the data + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Post(String file, byte data[]) + throws IOException, ModuleException + { + return Post(file, data, null); + } + + /** + * POSTs the raw data to the specified file using the specified headers. + * + * @param file the absolute path of the file + * @param data the data + * @param headers additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Post(String file, byte data[], NVPair[] headers) + throws IOException, ModuleException + { + if (data == null) data = new byte[0]; // POST must always have a CL + return setupRequest("POST", stripRef(file), headers, data, null); + } + + + /** + * POSTs the data written to the output stream to the specified file. + * The request is sent using the content-type "application/octet-stream" + * + * @param file the absolute path of the file + * @param stream the output stream on which the data is written + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Post(String file, HttpOutputStream stream) + throws IOException, ModuleException + { + return Post(file, stream, null); + } + + /** + * POSTs the data written to the output stream to the specified file + * using the specified headers. + * + * @param file the absolute path of the file + * @param stream the output stream on which the data is written + * @param headers additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Post(String file, HttpOutputStream stream, + NVPair[] headers) + throws IOException, ModuleException + { + return setupRequest("POST", stripRef(file), headers, null, stream); + } + + + /** + * PUTs the data into the specified file. The data is converted to an + * array of bytes using the default character converter. + * The request ist sent using the content-type "application/octet-stream". + * + * @param file the absolute path of the file + * @param data the data + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + * @see java.lang.String#getBytes() + */ + public HTTPResponse Put(String file, String data) + throws IOException, ModuleException + { + return Put(file, data, null); + } + + /** + * PUTs the data into the specified file using the additional headers + * for the request. + * + * @param file the absolute path of the file + * @param data the data + * @param headers additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + * @see java.lang.String#getBytes() + */ + public HTTPResponse Put(String file, String data, NVPair[] headers) + throws IOException, ModuleException + { + byte tmp[] = null; + + if (data != null && data.length() > 0) + tmp = data.getBytes(); + + return Put(file, tmp, headers); + } + + /** + * PUTs the raw data into the specified file. + * The request is sent using the content-type "application/octet-stream". + * + * @param file the absolute path of the file + * @param data the data + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Put(String file, byte data[]) + throws IOException, ModuleException + { + return Put(file, data, null); + } + + /** + * PUTs the raw data into the specified file using the additional + * headers. + * + * @param file the absolute path of the file + * @param data the data + * @param headers any additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Put(String file, byte data[], NVPair[] headers) + throws IOException, ModuleException + { + if (data == null) data = new byte[0]; // PUT must always have a CL + return setupRequest("PUT", stripRef(file), headers, data, null); + } + + /** + * PUTs the data written to the output stream into the specified file. + * The request is sent using the content-type "application/octet-stream". + * + * @param file the absolute path of the file + * @param stream the output stream on which the data is written + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Put(String file, HttpOutputStream stream) + throws IOException, ModuleException + { + return Put(file, stream, null); + } + + /** + * PUTs the data written to the output stream into the specified file + * using the additional headers. + * + * @param file the absolute path of the file + * @param stream the output stream on which the data is written + * @param headers any additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Put(String file, HttpOutputStream stream, + NVPair[] headers) + throws IOException, ModuleException + { + return setupRequest("PUT", stripRef(file), headers, null, stream); + } + + + /** + * Request OPTIONS from the server. If file is "*" then + * the request applies to the server as a whole; otherwise it applies + * only to that resource. + * + * @param file the absolute path of the resource, or "*" + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Options(String file) + throws IOException, ModuleException + { + return Options(file, null, (byte[]) null); + } + + + /** + * Request OPTIONS from the server. If file is "*" then + * the request applies to the server as a whole; otherwise it applies + * only to that resource. + * + * @param file the absolute path of the resource, or "*" + * @param headers the headers containing optional info. + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Options(String file, NVPair[] headers) + throws IOException, ModuleException + { + return Options(file, headers, (byte[]) null); + } + + + /** + * Request OPTIONS from the server. If file is "*" then + * the request applies to the server as a whole; otherwise it applies + * only to that resource. + * + * @param file the absolute path of the resource, or "*" + * @param headers the headers containing optional info. + * @param data any data to be sent in the optional body + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Options(String file, NVPair[] headers, byte[] data) + throws IOException, ModuleException + { + return setupRequest("OPTIONS", stripRef(file), headers, data, null); + } + + + /** + * Request OPTIONS from the server. If file is "*" then + * the request applies to the server as a whole; otherwise it applies + * only to that resource. + * + * @param file the absolute path of the resource, or "*" + * @param headers the headers containing optional info. + * @param stream an output stream for sending the optional body + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Options(String file, NVPair[] headers, + HttpOutputStream stream) + throws IOException, ModuleException + { + return setupRequest("OPTIONS", stripRef(file), headers, null, stream); + } + + + /** + * Requests that file be DELETEd from the server. + * + * @param file the absolute path of the resource + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Delete(String file) + throws IOException, ModuleException + { + return Delete(file, null); + } + + + /** + * Requests that file be DELETEd from the server. + * + * @param file the absolute path of the resource + * @param headers additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Delete(String file, NVPair[] headers) + throws IOException, ModuleException + { + return setupRequest("DELETE", stripRef(file), headers, null, null); + } + + + /** + * Requests a TRACE. Headers of particular interest here are "Via" + * and "Max-Forwards". + * + * @param file the absolute path of the resource + * @param headers additional headers + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Trace(String file, NVPair[] headers) + throws IOException, ModuleException + { + return setupRequest("TRACE", stripRef(file), headers, null, null); + } + + + /** + * Requests a TRACE. + * + * @param file the absolute path of the resource + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse Trace(String file) + throws IOException, ModuleException + { + return Trace(file, null); + } + + + /** + * This is here to allow an arbitrary, non-standard request to be sent. + * I'm assuming you know what you are doing... + * + * @param method the extension method + * @param file the absolute path of the resource, or null + * @param data optional data, or null + * @param headers optional headers, or null + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse ExtensionMethod(String method, String file, + byte[] data, NVPair[] headers) + throws IOException, ModuleException + { + return setupRequest(method.trim(), stripRef(file), headers, data, null); + } + + + /** + * This is here to allow an arbitrary, non-standard request to be sent. + * I'm assuming you know what you are doing... + * + * @param method the extension method + * @param file the absolute path of the resource, or null + * @param stream optional output stream, or null + * @param headers optional headers, or null + * @return an HTTPResponse structure containing the response + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + public HTTPResponse ExtensionMethod(String method, String file, + HttpOutputStream os, NVPair[] headers) + throws IOException, ModuleException + { + return setupRequest(method.trim(), stripRef(file), headers, null, os); + } + + + /** + * Aborts all the requests currently in progress on this connection and + * closes all associated sockets. You usually do not need to + * invoke this - it only meant for when you need to abruptly stop things, + * such as for example the stop button in a browser. + * + *

Note: there is a small window where a request method such as + * Get() may have been invoked but the request has not + * been built and added to the list. Any request in this window will + * not be aborted. + * + * @since V0.2-3 + */ + public void stop() + { + for (Request req = (Request) RequestList.enumerate(); req != null; + req = (Request) RequestList.next()) + req.aborted = true; + + for (StreamDemultiplexor demux = + (StreamDemultiplexor) DemuxList.enumerate(); + demux != null; demux = (StreamDemultiplexor) DemuxList.next()) + demux.abort(); + } + + + /** + * Sets the default http headers to be sent with each request. The + * actual headers sent are determined as follows: for each header + * specified in multiple places a value given as part of the request + * takes priority over any default values set by this method, which + * in turn takes priority over any built-in default values. A different + * way of looking at it is that we start off with a list of all headers + * specified with the request, then add any default headers set by this + * method which aren't already in our list, and finally add any built-in + * headers which aren't yet in the list. There is one exception to this + * rule: the "Content-length" header is always ignored; and when posting + * form-data any default "Content-type" is ignored in favor of the built-in + * "application/x-www-form-urlencoded" (however it will be overriden by any + * content-type header specified as part of the request). + * + *

Typical headers you might want to set here are "Accept" and its + * "Accept-*" relatives, "Connection", "From", "User-Agent", etc. + * + * @param headers an array of header-name/value pairs (do not give the + * separating ':'). + */ + public void setDefaultHeaders(NVPair[] headers) + { + int length = (headers == null ? 0 : headers.length); + NVPair[] def_hdrs = new NVPair[length]; + + // weed out undesired headers + int sidx, didx; + for (sidx=0, didx=0; sidxThe default is false. + * + * @deprecated This is not really needed anymore; in V0.2 request were + * synchronous and therefore to do pipelining you needed + * to disable the processing of responses. + * @see #removeModule(java.lang.Class) + * + * @param raw if true removes all modules (except for the retry module) + */ + public void setRawMode(boolean raw) + { + // Don't remove the retry module + String[] modules = { "HTTPClient.CookieModule", + "HTTPClient.RedirectionModule", + "HTTPClient.AuthorizationModule", + "HTTPClient.DefaultModule", + "HTTPClient.TransferEncodingModule", + "HTTPClient.ContentMD5Module", + "HTTPClient.ContentEncodingModule"}; + + for (int idx=0; idxresp.getInputStream().close() should be + * invoked. + * + *

When creating new sockets the timeout will limit the time spent + * doing the host name translation and establishing the connection with + * the server. + * + *

The timeout also influences the reading of the response headers. + * However, it does not specify a how long, for example, getStatusCode() + * may take, as might be assumed. Instead it specifies how long a read + * on the socket may take. If the response dribbles in slowly with + * packets arriving quicker than the timeout then the method will + * complete normally. I.e. the exception is only thrown if nothing + * arrives on the socket for the specified time. Furthermore, the + * timeout only influences the reading of the headers, not the reading + * of the body. + * + *

Read Timeouts are associated with responses, so that you may change + * this value before each request and it won't affect the reading of + * responses to previous requests. + * + * @param time the time in milliseconds. A time of 0 means wait + * indefinitely. + * @see #stop() + */ + public void setTimeout(int time) + { + Timeout = time; + } + + + /** + * Gets the timeout used for reading response data. + * + * @return the current timeout value + * @see #setTimeout(int) + */ + public int getTimeout() + { + return Timeout; + } + + + /** + * Controls whether modules are allowed to prompt the user or pop up + * dialogs if neccessary. + * + * @param allow if true allows modules to interact with user. + */ + public void setAllowUserInteraction(boolean allow) + { + allowUI = allow; + } + + /** + * returns whether modules are allowed to prompt or popup dialogs + * if neccessary. + * + * @return true if modules are allowed to interact with user. + */ + public boolean getAllowUserInteraction() + { + return allowUI; + } + + + /** + * Sets the default allow-user-action. + * + * @param allow if true allows modules to interact with user. + */ + public static void setDefaultAllowUserInteraction(boolean allow) + { + defaultAllowUI = allow; + } + + /** + * Gets the default allow-user-action. + * + * @return true if modules are allowed to interact with user. + */ + public static boolean getDefaultAllowUserInteraction() + { + return defaultAllowUI; + } + + + /** + * Returns the default list of modules. + * + * @return an array of classes + */ + public static Class[] getDefaultModules() + { + return getModules(DefaultModuleList); + } + + /** + * Adds a module to the default list. It must implement the + * HTTPClientModule interface. If the module is already in + * the list then this method does nothing. This method only affects + * instances of HTTPConnection created after this method has been + * invoked; it does not affect existing instances. + * + *

Example: + *

+     * HTTPConnection.addDefaultModule(Class.forName("HTTPClient.CookieModule"), 1);
+     * 
+ * adds the cookie module as the second module in the list. + * + *

The default list is created at class initialization time from the + * property HTTPClient.Modules. This must contain a "|" + * separated list of classes in the order they're to be invoked. If this + * property is not set it defaults to: + * + * "HTTPClient.RetryModule | HTTPClient.CookieModule | + * HTTPClient.RedirectionModule | HTTPClient.AuthorizationModule | + * HTTPClient.DefaultModule | HTTPClient.TransferEncodingModule | + * HTTPClient.ContentMD5Module | HTTPClient.ContentEncodingModule" + * + * @see HTTPClientModule + * @param module the module's Class object + * @param pos the position of this module in the list; if pos + * >= 0 then this is the absolute position in the list (0 is + * the first position); if pos < 0 then this is + * the position relative to the end of the list (-1 means + * the last element, -2 the second to last element, etc). + * @return true if module was successfully added; false if the + * module is already in the list. + * @exception ArrayIndexOutOfBoundsException if pos > + * list-size or if pos < -(list-size). + * @exception ClassCastException if module does not + * implement the HTTPClientModule interface. + * @exception RuntimeException if module cannot be + * instantiated. + */ + public static boolean addDefaultModule(Class module, int pos) + { + return addModule(DefaultModuleList, module, pos); + } + + + /** + * Removes a module from the default list. If the module is not in the + * list it does nothing. This method only affects instances of + * HTTPConnection created after this method has been invoked; it does not + * affect existing instances. + * + * @param module the module's Class object + * @return true if module was successfully removed; false otherwise + */ + public static boolean removeDefaultModule(Class module) + { + return removeModule(DefaultModuleList, module); + } + + + /** + * Returns the list of modules used currently. + * + * @return an array of classes + */ + public Class[] getModules() + { + return getModules(ModuleList); + } + + + /** + * Adds a module to the current list. It must implement the + * HTTPClientModule interface. If the module is already in + * the list then this method does nothing. + * + * @see HTTPClientModule + * @param module the module's Class object + * @param pos the position of this module in the list; if pos + * >= 0 then this is the absolute position in the list (0 is + * the first position); if pos < 0 then this is + * the position relative to the end of the list (-1 means + * the last element, -2 the second to last element, etc). + * @return true if module was successfully added; false if the + * module is already in the list. + * @exception ArrayIndexOutOfBoundsException if pos > + * list-size or if pos < -(list-size). + * @exception ClassCastException if module does not + * implement the HTTPClientModule interface. + * @exception RuntimeException if module cannot be + * instantiated. + */ + public boolean addModule(Class module, int pos) + { + return addModule(ModuleList, module, pos); + } + + + /** + * Removes a module from the current list. If the module is not in the + * list it does nothing. + * + * @param module the module's Class object + * @return true if module was successfully removed; false otherwise + */ + public boolean removeModule(Class module) + { + return removeModule(ModuleList, module); + } + + private static final Class[] getModules(Vector list) + { + synchronized(list) + { + Class[] modules = new Class[list.size()]; + list.copyInto(modules); + return modules; + } + } + + private static final boolean addModule(Vector list, Class module, int pos) + { + if (module == null) return false; + + // check if module implements HTTPClientModule + try + { HTTPClientModule tmp = (HTTPClientModule) module.newInstance(); } + catch (RuntimeException re) + { throw re; } + catch (Exception e) + { throw new RuntimeException(e.toString()); } + + synchronized (list) + { + // check if module already in list + if (list.contains(module)) + return false; + + // add module to list + if (pos < 0) + list.insertElementAt(module, DefaultModuleList.size()+pos+1); + else + list.insertElementAt(module, pos); + } + + Log.write(Log.CONN, "Conn: Added module " + module.getName() + + " to " + + ((list == DefaultModuleList) ? "default " : "") + + "list"); + + return true; + } + + private static final boolean removeModule(Vector list, Class module) + { + if (module == null) return false; + + boolean removed = list.removeElement(module); + if (removed) + Log.write(Log.CONN, "Conn: Removed module " + module.getName() + + " from " + + ((list == DefaultModuleList) ? "default " : "") + + "list"); + + return removed; + } + + + /** + * Sets the current context. The context is used by modules such as + * the AuthorizationModule and the CookieModule which keep lists of + * info that is normally shared between all instances of HTTPConnection. + * This is usually the desired behaviour. However, in some cases one + * would like to simulate multiple independent clients within the + * same application and hence the sharing of such info should be + * restricted. This is where the context comes in. Modules will only + * share their info between requests using the same context (i.e. they + * keep multiple lists, one for each context). + * + *

The context may be any object. Contexts are considered equal + * if equals() returns true. Examples of useful context + * objects are threads (e.g. if you are running multiple clients, one + * per thread) and sockets (e.g. if you are implementing a gateway). + * + *

When a new HTTPConnection is created it is initialized with a + * default context which is the same for all instances. This method + * must be invoked immediately after a new HTTPConnection is created + * and before any request method is invoked. Furthermore, this method + * may only be called once (i.e. the context is "sticky"). + * + * @param context the new context; must be non-null + * @exception IllegalArgumentException if context is null + * @exception IllegalStateException if the context has already been set + */ + public void setContext(Object context) + { + if (context == null) + throw new IllegalArgumentException("Context must be non-null"); + if (Context != null) + throw new IllegalStateException("Context already set"); + + Context = context; + } + + + /** + * Returns the current context. + * + * @see #setContext(java.lang.Object) + * @return the current context, or the default context if + * setContext() hasn't been invoked + */ + public Object getContext() + { + if (Context != null) + return Context; + else + return dflt_context; + } + + + /** + * Returns the default context. + * + * @see #setContext(java.lang.Object) + * @return the default context + */ + public static Object getDefaultContext() + { + return dflt_context; + } + + + /** + * Adds an authorization entry for the "digest" authorization scheme to + * the list. If an entry already exists for the "digest" scheme and the + * specified realm then it is overwritten. + * + *

This is a convenience method and just invokes the corresponding + * method in AuthorizationInfo. + * + * @param realm the realm + * @param user the username + * @param passw the password + * @see AuthorizationInfo#addDigestAuthorization(java.lang.String, int, java.lang.String, java.lang.String, java.lang.String) + */ + public void addDigestAuthorization(String realm, String user, String passwd) + { + AuthorizationInfo.addDigestAuthorization(Host, Port, realm, user, + passwd, getContext()); + } + + + /** + * Adds an authorization entry for the "basic" authorization scheme to + * the list. If an entry already exists for the "basic" scheme and the + * specified realm then it is overwritten. + * + *

This is a convenience method and just invokes the corresponding + * method in AuthorizationInfo. + * + * @param realm the realm + * @param user the username + * @param passw the password + * @see AuthorizationInfo#addBasicAuthorization(java.lang.String, int, java.lang.String, java.lang.String, java.lang.String) + */ + public void addBasicAuthorization(String realm, String user, String passwd) + { + AuthorizationInfo.addBasicAuthorization(Host, Port, realm, user, + passwd, getContext()); + } + + + /** + * Sets the default proxy server to use. The proxy will only be used + * for new HTTPConnections created after this call and will + * not affect currrent instances of HTTPConnection. A null + * or empty string host parameter disables the proxy. + * + *

In an application or using the Appletviewer an alternative to + * this method is to set the following properties (either in the + * properties file or on the command line): + * http.proxyHost and http.proxyPort. Whether + * http.proxyHost is set or not determines whether a proxy + * server is used. + * + *

If the proxy server requires authorization and you wish to set + * this authorization information in the code, then you may use any + * of the AuthorizationInfo.addXXXAuthorization() methods to + * do so. Specify the same host and port as in + * this method. If you have not given any authorization info and the + * proxy server requires authorization then you will be prompted for + * the necessary info via a popup the first time you do a request. + * + * @see #setCurrentProxy(java.lang.String, int) + * @param host the host on which the proxy server resides. + * @param port the port the proxy server is listening on. + */ + public static void setProxyServer(String host, int port) + { + if (host == null || host.trim().length() == 0) + Default_Proxy_Host = null; + else + { + Default_Proxy_Host = host.trim().toLowerCase(); + Default_Proxy_Port = port; + } + } + + + /** + * Sets the proxy used by this instance. This can be used to override + * the proxy setting inherited from the default proxy setting. A null + * or empty string host parameter disables the proxy. + * + *

Note that if you set a proxy for the connection using this + * method, and a request made over this connection is redirected + * to a different server, then the connection used for new server + * will not pick this proxy setting, but instead will use + * the default proxy settings. + * + * @see #setProxyServer(java.lang.String, int) + * @param host the host the proxy runs on + * @param port the port the proxy is listening on + */ + public synchronized void setCurrentProxy(String host, int port) + { + if (host == null || host.trim().length() == 0) + Proxy_Host = null; + else + { + Proxy_Host = host.trim().toLowerCase(); + if (port <= 0) + Proxy_Port = 80; + else + Proxy_Port = port; + } + + // the proxy might be talking a different version, so renegotiate + switch(Protocol) + { + case HTTP: + case HTTPS: + if (force_1_0) + { + ServerProtocolVersion = HTTP_1_0; + ServProtVersKnown = true; + RequestProtocolVersion = "HTTP/1.0"; + } + else + { + ServerProtocolVersion = HTTP_1_1; + ServProtVersKnown = false; + RequestProtocolVersion = "HTTP/1.1"; + } + break; + case HTTP_NG: + ServerProtocolVersion = -1; /* Unknown */ + ServProtVersKnown = false; + RequestProtocolVersion = ""; + break; + case SHTTP: + ServerProtocolVersion = -1; /* Unknown */ + ServProtVersKnown = false; + RequestProtocolVersion = "Secure-HTTP/1.3"; + break; + default: + throw new Error("HTTPClient Internal Error: invalid protocol " + + Protocol); + } + + keepAliveUnknown = true; + doesKeepAlive = false; + + input_demux = null; + early_stall = null; + late_stall = null; + prev_resp = null; + } + + + /** + * Add host to the list of hosts which should be accessed + * directly, not via any proxy set by setProxyServer(). + * + *

The host may be any of: + *

    + *
  • a complete host name (e.g. "www.disney.com") + *
  • a domain name; domain names must begin with a dot (e.g. + * ".disney.com") + *
  • an IP-address (e.g. "12.34.56.78") + *
  • an IP-subnet, specified as an IP-address and a netmask separated + * by a "/" (e.g. "34.56.78/255.255.255.192"); a 0 bit in the netmask + * means that that bit won't be used in the comparison (i.e. the + * addresses are AND'ed with the netmask before comparison). + *
+ * + *

The two properties HTTPClient.nonProxyHosts and + * http.nonProxyHosts are used when this class is loaded to + * initialize the list of non-proxy hosts. The second property is only + * read if the first one is not set; the second property is also used + * the JDK's URLConnection. These properties must contain a "|" + * separated list of entries which conform to the above rules for the + * host parameter (e.g. "11.22.33.44|.disney.com"). + * + * @param host a host name, domain name, IP-address or IP-subnet. + * @exception ParseException if the length of the netmask does not match + * the length of the IP-address + */ + public static void dontProxyFor(String host) throws ParseException + { + host = host.trim().toLowerCase(); + + // check for domain name + + if (host.charAt(0) == '.') + { + if (!non_proxy_dom_list.contains(host)) + non_proxy_dom_list.addElement(host); + return; + } + + + // check for host name + + for (int idx=0; idxhost from the list of hosts for which the proxy + * should not be used. This modifies the same list that + * dontProxyFor() uses, i.e. this is used to undo a + * dontProxyFor() setting. The syntax for host is + * specified in dontProxyFor(). + * + * @param host a host name, domain name, IP-address or IP-subnet. + * @return true if the remove was sucessful, false otherwise + * @exception ParseException if the length of the netmask does not match + * the length of the IP-address + * @see #dontProxyFor(java.lang.String) + */ + public static boolean doProxyFor(String host) throws ParseException + { + host = host.trim().toLowerCase(); + + // check for domain name + + if (host.charAt(0) == '.') + return non_proxy_dom_list.removeElement(host); + + + // check for host name + + for (int idx=0; idxThe code will try to determine the SOCKS version to use at + * connection time. This might fail for a number of reasons, however, + * in which case you must specify the version explicitly. + * + * @see #setSocksServer(java.lang.String, int, int) + * @param host the host on which the proxy server resides. The port + * used is the default port 1080. + */ + public static void setSocksServer(String host) + { + setSocksServer(host, 1080); + } + + + /** + * Sets the SOCKS server to use. The server will only be used + * for new HTTPConnections created after this call and will not affect + * currrent instances of HTTPConnection. A null or empty string host + * parameter disables SOCKS. + *

The code will try to determine the SOCKS version to use at + * connection time. This might fail for a number of reasons, however, + * in which case you must specify the version explicitly. + * + * @see #setSocksServer(java.lang.String, int, int) + * @param host the host on which the proxy server resides. + * @param port the port the proxy server is listening on. + */ + public static void setSocksServer(String host, int port) + { + if (port <= 0) + port = 1080; + + if (host == null || host.length() == 0) + Default_Socks_client = null; + else + Default_Socks_client = new SocksClient(host, port); + } + + + /** + * Sets the SOCKS server to use. The server will only be used + * for new HTTPConnections created after this call and will not affect + * currrent instances of HTTPConnection. A null or empty string host + * parameter disables SOCKS. + * + *

In an application or using the Appletviewer an alternative to + * this method is to set the following properties (either in the + * properties file or on the command line): + * HTTPClient.socksHost, HTTPClient.socksPort + * and HTTPClient.socksVersion. Whether + * HTTPClient.socksHost is set or not determines whether a + * SOCKS server is used; if HTTPClient.socksPort is not set + * it defaults to 1080; if HTTPClient.socksVersion is not + * set an attempt will be made to automatically determine the version + * used by the server. + * + *

Note: If you have also set a proxy server then a connection + * will be made to the SOCKS server, which in turn then makes a + * connection to the proxy server (possibly via other SOCKS servers), + * which in turn makes the final connection. + * + *

If the proxy server is running SOCKS version 5 and requires + * username/password authorization, and you wish to set + * this authorization information in the code, then you may use the + * AuthorizationInfo.addAuthorization() method to do so. + * Specify the same host and port as in this + * method, give the scheme "SOCKS5" and the realm + * "USER/PASS", set the cookie to null and the + * params to an array containing a single NVPair + * in turn containing the username and password. Example: + *

+     *     NVPair[] up = { new NVPair(username, password) };
+     *     AuthorizationInfo.addAuthorization(host, port, "SOCKS5", "USER/PASS",
+     *                                        null, up);
+     * 
+ * If you have not given any authorization info and the proxy server + * requires authorization then you will be prompted for the necessary + * info via a popup the first time you do a request. + * + * @param host the host on which the proxy server resides. + * @param port the port the proxy server is listening on. + * @param version the SOCKS version the server is running. Currently + * this must be '4' or '5'. + * @exception SocksException If version is not '4' or '5'. + */ + public static void setSocksServer(String host, int port, int version) + throws SocksException + { + if (port <= 0) + port = 1080; + + if (host == null || host.length() == 0) + Default_Socks_client = null; + else + Default_Socks_client = new SocksClient(host, port, version); + } + + + /** + * Removes the #... part. Returns the stripped name, or "" if either + * the file is null or is the empty string (after stripping). + * + * @param file the name to strip + * @return the stripped name + */ + private final String stripRef(String file) + { + if (file == null) return ""; + + int hash = file.indexOf('#'); + if (hash != -1) + file = file.substring(0,hash); + + return file.trim(); + } + + + // private helper methods + + /** + * Sets up the request, creating the list of headers to send and + * creating instances of the modules. This may be invoked by subclasses + * which add further methods (such as those from DAV and IPP). + * + * @param method GET, POST, etc. + * @param resource the resource + * @param headers an array of headers to be used + * @param entity the entity (or null) + * @param stream the output stream (or null) - only one of stream and + * entity may be non-null + * @return the response. + * @exception java.io.IOException when an exception is returned from + * the socket. + * @exception ModuleException if an exception is encountered in any module. + */ + protected final HTTPResponse setupRequest(String method, String resource, + NVPair[] headers, byte[] entity, + HttpOutputStream stream) + throws IOException, ModuleException + { + Request req = new Request(this, method, resource, + mergedHeaders(headers), entity, stream, + allowUI); + RequestList.addToEnd(req); + + try + { + HTTPResponse resp = new HTTPResponse(gen_mod_insts(), Timeout, req); + handleRequest(req, resp, null, true); + return resp; + } + finally + { RequestList.remove(req); } + } + + + /** + * This merges built-in default headers, user-specified default headers, + * and method-specified headers. Method-specified take precedence over + * user defaults, which take precedence over built-in defaults. + * + * The following headers are removed if found: "Content-length". + * + * @param spec the headers specified in the call to the method + * @return an array consisting of merged headers. + */ + private NVPair[] mergedHeaders(NVPair[] spec) + { + int spec_len = (spec != null ? spec.length : 0), + defs_len; + NVPair[] merged; + + synchronized (DefaultHeaders) + { + defs_len = (DefaultHeaders != null ? DefaultHeaders.length : 0); + merged = new NVPair[spec_len + defs_len]; + + // copy default headers + System.arraycopy(DefaultHeaders, 0, merged, 0, defs_len); + } + + // merge in selected headers + int sidx, didx = defs_len; + for (sidx=0; sidx= HTTP_1_1 && + !Util.hasToken(con_hdrs[0], "close") + || + ServerProtocolVersion == HTTP_1_0 && + Util.hasToken(con_hdrs[0], "keep-alive") + ) + keep_alive = true; + else + keep_alive = false; + } + catch (ParseException pe) + { throw new IOException(pe.toString()); } + + + synchronized (this) + { + // Sometimes we must stall the pipeline until the previous request + // has been answered. However, if we are going to open up a new + // connection anyway we don't really need to stall. + + if (late_stall != null) + { + if (input_demux != null || keepAliveUnknown) + { + Log.write(Log.CONN, "Conn: Stalling Request: " + + req.getMethod() + " " + req.getRequestURI()); + + try // wait till the response is received + { + late_stall.getVersion(); + if (keepAliveUnknown) + determineKeepAlive(late_stall); + } + catch (IOException ioe) + { } + } + + late_stall = null; + } + + + /* POSTs must not be pipelined because of problems if the connection + * is aborted. Since it is generally impossible to know what urls + * POST will influence it is impossible to determine if a sequence + * of requests containing a POST is idempotent. + * Also, for retried requests we don't want to pipeline either. + */ + if ((req.getMethod().equals("POST") || req.dont_pipeline) && + prev_resp != null && input_demux != null) + { + Log.write(Log.CONN, "Conn: Stalling Request: " + + req.getMethod() + " " + req.getRequestURI()); + + try // wait till the response is received + { prev_resp.getVersion(); } + catch (IOException ioe) + { } + } + + + // If the previous request used an output stream, then wait till + // all the data has been written + + if (!output_finished) + { + try + { wait(); } + catch (InterruptedException ie) + { throw new IOException(ie.toString()); } + } + + + if (req.aborted) throw new IOException("Request aborted by user"); + + int try_count = 3; + /* what a hack! This is to handle the case where the server closes + * the connection but we don't realize it until we try to send + * something. The problem is that we only get IOException, but + * we need a finer specification (i.e. whether it's an EPIPE or + * something else); I don't trust relying on the message part + * of IOException (which on SunOS/Solaris gives 'Broken pipe', + * but what on Windoze/Mac?). + */ + + while (try_count-- > 0) + { + try + { + // get a client socket + + Socket sock; + if (input_demux == null || + (sock = input_demux.getSocket()) == null) + { + sock = getSocket(con_timeout); + + if (Protocol == HTTPS) + { + if (Proxy_Host != null) + { + Socket[] sarr = { sock }; + resp = enableSSLTunneling(sarr, req, con_timeout); + if (resp != null) + { + resp.final_resp = true; + return resp; + } + sock = sarr[0]; + } + + sock.setSoTimeout(con_timeout); + //sock = new SSLSocket(sock); + } + + input_demux = new StreamDemultiplexor(Protocol, sock, this); + DemuxList.addToEnd(input_demux); + keepAliveReqLeft = keepAliveReqMax; + } + + if (req.aborted) + throw new IOException("Request aborted by user"); + + Log.write(Log.CONN, "Conn: Sending Request: ", hdr_buf); + + + // Send headers + + OutputStream sock_out = sock.getOutputStream(); + if (haveMSLargeWritesBug) + sock_out = new MSLargeWritesBugStream(sock_out); + + hdr_buf.writeTo(sock_out); + + + // Wait for "100 Continue" status if necessary + + try + { + if (ServProtVersKnown && + ServerProtocolVersion >= HTTP_1_1 && + Util.hasToken(con_hdrs[1], "100-continue")) + { + resp = new Response(req, (Proxy_Host != null && Protocol != HTTPS), input_demux); + resp.timeout = 60; + if (resp.getContinue() != 100) + break; + } + } + catch (ParseException pe) + { throw new IOException(pe.toString()); } + catch (InterruptedIOException iioe) + { } + finally + { if (resp != null) resp.timeout = 0; } + + + // POST/PUT data + + if (req.getData() != null && req.getData().length > 0) + { + if (req.delay_entity > 0) + { + // wait for something on the network; check available() + // roughly every 100 ms + + long num_units = req.delay_entity / 100; + long one_unit = req.delay_entity / num_units; + + for (int idx=0; idxhdr_buf. It takes + * special precautions for the following headers: + *
+ *
Content-typeThis is only written if the request has an entity. + * If the request has an entity and no content-type + * header was given for the request it defaults to + * "application/octet-stream" + *
Content-lengthThis header is generated if the request has an + * entity and the entity isn't being sent with the + * Transfer-Encoding "chunked". + *
User-Agent If not present it will be generated with the current + * HTTPClient version strings. Otherwise the version + * string is appended to the given User-Agent string. + *
Connection This header is only written if no proxy is used. + * If no connection header is specified and the server + * is not known to understand HTTP/1.1 or later then + * a "Connection: keep-alive" header is generated. + *
Proxy-ConnectionThis header is only written if a proxy is used. + * If no connection header is specified and the proxy + * is not known to understand HTTP/1.1 or later then + * a "Proxy-Connection: keep-alive" header is generated. + *
Keep-Alive This header is only written if the Connection or + * Proxy-Connection header contains the Keep-Alive + * token. + *
Expect If there is no entity and this header contains the + * "100-continue" token then this token is removed. + * before writing the header. + *
TE If this header does not exist, it is created; else + * if the "trailers" token is not specified this token + * is added; else the header is not touched. + *
+ * + * Furthermore, it escapes various characters in request-URI. + * + * @param req the Request + * @param hdr_buf the buffer onto which to write the headers + * @return an array of headers; the first element contains the + * the value of the Connection or Proxy-Connectin header, + * the second element the value of the Expect header. + * @exception IOException if writing on hdr_buf generates an + * an IOException, or if an error occurs during + * parsing of a header + */ + private String[] assembleHeaders(Request req, + ByteArrayOutputStream hdr_buf) + throws IOException + { + DataOutputStream dataout = new DataOutputStream(hdr_buf); + String[] con_hdrs = { "", "" }; + NVPair[] hdrs = req.getHeaders(); + + + + // remember various headers + + int ho_idx = -1, + ct_idx = -1, + ua_idx = -1, + co_idx = -1, + pc_idx = -1, + ka_idx = -1, + ex_idx = -1, + te_idx = -1, + tc_idx = -1, + ug_idx = -1; + for (int idx=0; idx= 0) ? hdrs[ho_idx].getValue().trim() : Host; + if (Port != URI.defaultPort(getProtocol())) + dataout.writeBytes("Host: " + h_hdr + ":" + Port + "\r\n"); + else // Netscape-Enterprise has some bugs... + dataout.writeBytes("Host: " + h_hdr + "\r\n"); + + + /* + * What follows is the setup for persistent connections. We default + * to doing persistent connections for both HTTP/1.0 and HTTP/1.1, + * unless we're using a proxy server and HTTP/1.0 in which case we + * must make sure we don't do persistence (because of the problem of + * 1.0 proxies blindly passing the Connection header on). + * + * Note: there is a "Proxy-Connection" header for use with proxies. + * This however is only understood by Netscape and Netapp caches. + * Furthermore, it suffers from the same problem as the Connection + * header in HTTP/1.0 except that at least two proxies must be + * involved. But I've taken the risk now and decided to send the + * Proxy-Connection header. If I get complaints I'll remove it again. + * + * In any case, with this header we can now modify the above to send + * the Proxy-Connection header whenever we wouldn't send the normal + * Connection header. + */ + + String co_hdr = null; + if (!(ServProtVersKnown && ServerProtocolVersion >= HTTP_1_1 && + co_idx == -1)) + { + if (co_idx == -1) + { // no connection header given by user + co_hdr = "Keep-Alive"; + con_hdrs[0] = "Keep-Alive"; + } + else + { + con_hdrs[0] = hdrs[co_idx].getValue().trim(); + co_hdr = con_hdrs[0]; + } + + try + { + if (ka_idx != -1 && + Util.hasToken(con_hdrs[0], "keep-alive")) + dataout.writeBytes("Keep-Alive: " + + hdrs[ka_idx].getValue().trim() + "\r\n"); + } + catch (ParseException pe) + { + throw new IOException(pe.toString()); + } + } + + if ((Proxy_Host != null && Protocol != HTTPS) && + !(ServProtVersKnown && ServerProtocolVersion >= HTTP_1_1)) + { + if (co_hdr != null) + { + dataout.writeBytes("Proxy-Connection: "); + dataout.writeBytes(co_hdr); + dataout.writeBytes("\r\n"); + co_hdr = null; + } + } + + if (co_hdr != null) + { + try + { + if (!Util.hasToken(co_hdr, "TE")) + co_hdr += ", TE"; + } + catch (ParseException pe) + { throw new IOException(pe.toString()); } + } + else + co_hdr = "TE"; + + if (ug_idx != -1) + co_hdr += ", Upgrade"; + + if (co_hdr != null) + { + dataout.writeBytes("Connection: "); + dataout.writeBytes(co_hdr); + dataout.writeBytes("\r\n"); + } + + + + // handle TE header + + if (te_idx != -1) + { + dataout.writeBytes("TE: "); + Vector pte; + try + { pte = Util.parseHeader(hdrs[te_idx].getValue()); } + catch (ParseException pe) + { throw new IOException(pe.toString()); } + + if (!pte.contains(new HttpHeaderElement("trailers"))) + dataout.writeBytes("trailers, "); + + dataout.writeBytes(hdrs[te_idx].getValue().trim() + "\r\n"); + } + else + dataout.writeBytes("TE: trailers\r\n"); + + + // User-Agent + + if (ua_idx != -1) + dataout.writeBytes("User-Agent: " + hdrs[ua_idx].getValue().trim() + " " + + version + "\r\n"); + else + dataout.writeBytes("User-Agent: " + version + "\r\n"); + + + // Write out any headers left + + for (int idx=0; idx then this will fail too. Unfortunately there + * seems to be no way to reliably detect broken HTTP/1.0 + * proxies... + */ + int sts = resp.getStatusCode(); + if ((Proxy_Host != null && Protocol != HTTPS) && + resp.getHeader("Via") == null && + sts != 407 && sts != 502 && sts != 504) + ServerProtocolVersion = HTTP_1_0; + + Log.write(Log.CONN, "Conn: Protocol Version established: " + + ProtVers2String(ServerProtocolVersion)); + + + // some (buggy) servers return an error status if they get a + // version they don't comprehend + + if (ServerProtocolVersion == HTTP_1_0 && + (resp.getStatusCode() == 400 || resp.getStatusCode() == 500)) + { + if (input_demux != null) + input_demux.markForClose(resp); + input_demux = null; + RequestProtocolVersion = "HTTP/1.0"; + return false; + } + + return true; + } + + + private void determineKeepAlive(Response resp) throws IOException + { + // try and determine if this server does keep-alives + + String con; + + try + { + if (ServerProtocolVersion >= HTTP_1_1 || + ( + ( + ((Proxy_Host == null || Protocol == HTTPS) && + (con = resp.getHeader("Connection")) != null) + || + ((Proxy_Host != null && Protocol != HTTPS) && + (con = resp.getHeader("Proxy-Connection")) != null) + ) && + Util.hasToken(con, "keep-alive") + ) + ) + { + doesKeepAlive = true; + keepAliveUnknown = false; + + Log.write(Log.CONN, "Conn: Keep-Alive enabled"); + } + else if (resp.getStatusCode() < 400) + keepAliveUnknown = false; + + + // get maximum number of requests + + if (doesKeepAlive && ServerProtocolVersion == HTTP_1_0 && + (con = resp.getHeader("Keep-Alive")) != null) + { + HttpHeaderElement max = + Util.getElement(Util.parseHeader(con), "max"); + if (max != null && max.getValue() != null) + { + keepAliveReqMax = Integer.parseInt(max.getValue()); + keepAliveReqLeft = keepAliveReqMax; + + Log.write(Log.CONN, "Conn: Max Keep-Alive requests: " + + keepAliveReqMax); + } + } + } + catch (ParseException pe) { } + catch (NumberFormatException nfe) { } + catch (ClassCastException cce) { } + } + + + synchronized void outputFinished() + { + output_finished = true; + notify(); + } + + + synchronized void closeDemux(IOException ioe, boolean was_reset) + { + if (input_demux != null) input_demux.close(ioe, was_reset); + + early_stall = null; + late_stall = null; + prev_resp = null; + } + + + final static String ProtVers2String(int prot_vers) + { + return "HTTP/" + (prot_vers >>> 16) + "." + (prot_vers & 0xFFFF); + } + + final static int String2ProtVers(String prot_vers) + { + String vers = prot_vers.substring(5); + int dot = vers.indexOf('.'); + return Integer.parseInt(vers.substring(0, dot)) << 16 | + Integer.parseInt(vers.substring(dot+1)); + } + + + /** + * Generates a string of the form protocol://host.domain:port . + * + * @return the string + */ + public String toString() + { + return getProtocol() + "://" + getHost() + + (getPort() != URI.defaultPort(getProtocol()) ? ":" + getPort() : ""); + } + + + private class EstablishConnection extends Thread + { + String actual_host; + int actual_port; + IOException exception; + Socket sock; + SocksClient Socks_client; + boolean close; + + + EstablishConnection(String host, int port, SocksClient socks) + { + super("EstablishConnection (" + host + ":" + port + ")"); + try { setDaemon(true); } + catch (SecurityException se) { } // Oh well... + + actual_host = host; + actual_port = port; + Socks_client = socks; + + exception = null; + sock = null; + close = false; + } + + + public void run() + { + try + { + if (Socks_client != null) + sock = Socks_client.getSocket(actual_host, actual_port); + else + { + // try all A records + InetAddress[] addr_list = InetAddress.getAllByName(actual_host); + for (int idx=0; idx CHUNK_SIZE) + { + out.write(b, off, CHUNK_SIZE); + off += CHUNK_SIZE; + len -= CHUNK_SIZE; + } + out.write(b, off, len); + } + } +} diff --git a/HTTPClient/HTTPResponse.java b/HTTPClient/HTTPResponse.java new file mode 100644 index 0000000..0579730 --- /dev/null +++ b/HTTPClient/HTTPResponse.java @@ -0,0 +1,960 @@ +/* + * @(#)HTTPResponse.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.net.URL; +import java.util.Date; +import java.util.Enumeration; + + +/** + * This defines the http-response class returned by the requests. It's + * basically a wrapper around the Response class which first lets all + * the modules handle the response before finally giving the info to + * the user. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since 0.3 + */ +public class HTTPResponse implements HTTPClientModuleConstants +{ + /** the list of modules */ + private HTTPClientModule[] modules; + + /** the timeout for reads */ + private int timeout; + + /** the request */ + private Request request = null; + + /** the current response */ + Response response = null; + + /** the HttpOutputStream to synchronize on */ + private HttpOutputStream out_stream = null; + + /** our input stream from the stream demux */ + private InputStream inp_stream; + + /** the status code returned. */ + private int StatusCode; + + /** the reason line associated with the status code. */ + private String ReasonLine; + + /** the HTTP version of the response. */ + private String Version; + + /** the original URI used. */ + private URI OriginalURI = null; + + /** the final URI of the document. */ + private URI EffectiveURI = null; + + /** any headers which were received and do not fit in the above list. */ + private CIHashtable Headers = null; + + /** any trailers which were received and do not fit in the above list. */ + private CIHashtable Trailers = null; + + /** the ContentLength of the data. */ + private int ContentLength = -1; + + /** the data (body) returned. */ + private byte[] Data = null; + + /** signals if we have got and parsed the headers yet? */ + private boolean initialized = false; + + /** signals if we have got the trailers yet? */ + private boolean got_trailers = false; + + /** marks this response as aborted (stop() in HTTPConnection) */ + private boolean aborted = false; + + /** should the request be retried by the application? */ + private boolean retry = false; + + /** the method used in the request */ + private String method = null; + + + // Constructors + + /** + * Creates a new HTTPResponse. + * + * @param modules the list of modules handling this response + * @param timeout the timeout to be used on stream read()'s + */ + HTTPResponse(HTTPClientModule[] modules, int timeout, Request orig) + { + this.modules = modules; + this.timeout = timeout; + try + { + int qp = orig.getRequestURI().indexOf('?'); + this.OriginalURI = new URI(orig.getConnection().getProtocol(), + null, + orig.getConnection().getHost(), + orig.getConnection().getPort(), + qp < 0 ? orig.getRequestURI() : + orig.getRequestURI().substring(0, qp), + qp < 0 ? null : + orig.getRequestURI().substring(qp+1), + null); + } + catch (ParseException pe) + { } + this.method = orig.getMethod(); + } + + + /** + * @param req the request + * @param resp the response + */ + void set(Request req, Response resp) + { + this.request = req; + this.response = resp; + resp.http_resp = this; + resp.timeout = timeout; + this.aborted = resp.final_resp; + } + + + /** + * @param req the request + * @param resp the response + */ + void set(Request req, HttpOutputStream out_stream) + { + this.request = req; + this.out_stream = out_stream; + } + + + // Methods + + /** + * Give the status code for this request. These are grouped as follows: + *
    + *
  • 1xx - Informational (new in HTTP/1.1) + *
  • 2xx - Success + *
  • 3xx - Redirection + *
  • 4xx - Client Error + *
  • 5xx - Server Error + *
+ * + * @exception IOException if any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public final int getStatusCode() throws IOException, ModuleException + { + if (!initialized) handleResponse(); + return StatusCode; + } + + /** + * Give the reason line associated with the status code. + * + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public final String getReasonLine() throws IOException, ModuleException + { + if (!initialized) handleResponse(); + return ReasonLine; + } + + /** + * Get the HTTP version used for the response. + * + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public final String getVersion() throws IOException, ModuleException + { + if (!initialized) handleResponse(); + return Version; + } + + /** + * Get the name and type of server. + * + * @deprecated This method is a remnant of V0.1; use + * getHeader("Server") instead. + * @see #getHeader(java.lang.String) + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public final String getServer() throws IOException, ModuleException + { + if (!initialized) handleResponse(); + return getHeader("Server"); + } + + + /** + * Get the original URI used in the request. + * + * @return the URI used in primary request + */ + public final URI getOriginalURI() + { + return OriginalURI; + } + + + /** + * Get the final URL of the document. This is set if the original + * request was deferred via the "moved" (301, 302, or 303) return + * status. + * + * @return the effective URL, or null if no redirection occured + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + * @deprecated use getEffectiveURI() instead + * @see #getEffectiveURI + */ + public final URL getEffectiveURL() throws IOException, ModuleException + { + if (!initialized) handleResponse(); + if (EffectiveURI != null) + return EffectiveURI.toURL(); + return null; + } + + + /** + * Get the final URI of the document. If the request was redirected + * via the "moved" (301, 302, 303, or 307) return status this returns + * the URI used in the last redirection; otherwise it returns the + * original URI. + * + * @return the effective URI + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public final URI getEffectiveURI() throws IOException, ModuleException + { + if (!initialized) handleResponse(); + if (EffectiveURI != null) + return EffectiveURI; + return OriginalURI; + } + + + /** + * Retrieves the value for a given header. + * + * @param hdr the header name. + * @return the value for the header, or null if non-existent. + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public String getHeader(String hdr) throws IOException, ModuleException + { + if (!initialized) handleResponse(); + return (String) Headers.get(hdr.trim()); + } + + /** + * Retrieves the value for a given header. The value is parsed as an + * int. + * + * @param hdr the header name. + * @return the value for the header if the header exists + * @exception NumberFormatException if the header's value is not a number + * or if the header does not exist. + * @exception IOException if any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public int getHeaderAsInt(String hdr) + throws IOException, ModuleException, NumberFormatException + { + String val = getHeader(hdr); + if (val == null) + throw new NumberFormatException("null"); + return Integer.parseInt(val); + } + + /** + * Retrieves the value for a given header. The value is parsed as a + * date; if this fails it is parsed as a long representing the number + * of seconds since 12:00 AM, Jan 1st, 1970. If this also fails an + * exception is thrown. + *
Note: When sending dates use Util.httpDate(). + * + * @param hdr the header name. + * @return the value for the header, or null if non-existent. + * @exception IllegalArgumentException if the header's value is neither a + * legal date nor a number. + * @exception IOException if any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public Date getHeaderAsDate(String hdr) + throws IOException, IllegalArgumentException, ModuleException + { + String raw_date = getHeader(hdr); + if (raw_date == null) return null; + + // asctime() format is missing an explicit GMT specifier + if (raw_date.toUpperCase().indexOf("GMT") == -1 && + raw_date.indexOf(' ') > 0) + raw_date += " GMT"; + + Date date; + + try + { date = Util.parseHttpDate(raw_date); } + catch (IllegalArgumentException iae) + { + // some servers erroneously send a number, so let's try that + long time; + try + { time = Long.parseLong(raw_date); } + catch (NumberFormatException nfe) + { throw iae; } // give up + if (time < 0) time = 0; + date = new Date(time * 1000L); + } + + return date; + } + + /** + * Returns an enumeration of all the headers available via getHeader(). + * + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public Enumeration listHeaders() throws IOException, ModuleException + { + if (!initialized) handleResponse(); + return Headers.keys(); + } + + + /** + * Retrieves the value for a given trailer. This should not be invoked + * until all response data has been read. If invoked before it will + * call getData() to force the data to be read. + * + * @param trailer the trailer name. + * @return the value for the trailer, or null if non-existent. + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + * @see #getData() + */ + public String getTrailer(String trailer) throws IOException, ModuleException + { + if (!got_trailers) getTrailers(); + return (String) Trailers.get(trailer.trim()); + } + + /** + * Retrieves the value for a given tailer. The value is parsed as an + * int. + * + * @param trailer the tailer name. + * @return the value for the trailer if the trailer exists + * @exception NumberFormatException if the trailer's value is not a number + * or if the trailer does not exist. + * @exception IOException if any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public int getTrailerAsInt(String trailer) + throws IOException, ModuleException, NumberFormatException + { + String val = getTrailer(trailer); + if (val == null) + throw new NumberFormatException("null"); + return Integer.parseInt(val); + } + + /** + * Retrieves the value for a given trailer. The value is parsed as a + * date; if this fails it is parsed as a long representing the number + * of seconds since 12:00 AM, Jan 1st, 1970. If this also fails an + * IllegalArgumentException is thrown. + *
Note: When sending dates use Util.httpDate(). + * + * @param trailer the trailer name. + * @return the value for the trailer, or null if non-existent. + * @exception IllegalArgumentException if the trailer's value is neither a + * legal date nor a number. + * @exception IOException if any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public Date getTrailerAsDate(String trailer) + throws IOException, IllegalArgumentException, ModuleException + { + String raw_date = getTrailer(trailer); + if (raw_date == null) return null; + + // asctime() format is missing an explicit GMT specifier + if (raw_date.toUpperCase().indexOf("GMT") == -1 && + raw_date.indexOf(' ') > 0) + raw_date += " GMT"; + + Date date; + + try + { date = Util.parseHttpDate(raw_date); } + catch (IllegalArgumentException iae) + { + // some servers erroneously send a number, so let's try that + long time; + try + { time = Long.parseLong(raw_date); } + catch (NumberFormatException nfe) + { throw iae; } // give up + if (time < 0) time = 0; + date = new Date(time * 1000L); + } + + return date; + } + + /** + * Returns an enumeration of all the trailers available via getTrailer(). + * + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public Enumeration listTrailers() throws IOException, ModuleException + { + if (!got_trailers) getTrailers(); + return Trailers.keys(); + } + + + /** + * Reads all the response data into a byte array. Note that this method + * won't return until all the data has been received (so for + * instance don't invoke this method if the server is doing a server + * push). If getInputStream() had been previously invoked + * then this method only returns any unread data remaining on the stream + * and then closes it. + * + *

Note to the unwary: code like + *

+     *     System.out.println("The data: " + resp.getData())
+     *
+ * will probably not do what you want - use + *
+     *     System.out.println("The data: " + resp.getText())
+     *
+ * instead. + * + * @see #getInputStream() + * @return an array containing the data (body) returned. If no data + * was returned then it's set to a zero-length array. + * @exception IOException If any io exception occured while reading + * the data + * @exception ModuleException if any module encounters an exception. + */ + public synchronized byte[] getData() throws IOException, ModuleException + { + if (!initialized) handleResponse(); + + if (Data == null) + { + try + { readResponseData(inp_stream); } + catch (InterruptedIOException ie) // don't intercept + { throw ie; } + catch (IOException ioe) + { + Log.write(Log.RESP, "HResp: (\"" + method + " " + + OriginalURI.getPathAndQuery() + "\")"); + Log.write(Log.RESP, " ", ioe); + + try { inp_stream.close(); } catch (Exception e) { } + throw ioe; + } + + inp_stream.close(); + } + + return Data; + } + + /** + * Reads all the response data into a buffer and turns it into a string + * using the appropriate character converter. Since this uses {@link + * #getData() getData()}, the caveats of that method apply here as well. + * + * @see #getData() + * @return the body as a String. If no data was returned then an empty + * string is returned. + * @exception IOException If any io exception occured while reading + * the data, or if the content is not text + * @exception ModuleException if any module encounters an exception. + * @exception ParseException if an error occured trying to parse the + * content-type header field + */ + public synchronized String getText() + throws IOException, ModuleException, ParseException + { + String ct = getHeader("Content-Type"); + if (ct == null || !ct.toLowerCase().startsWith("text/")) + throw new IOException("Content-Type `" + ct + "' is not a text type"); + + String charset = Util.getParameter("charset", ct); + if (charset == null) + charset = "ISO-8859-1"; + + return new String(getData(), charset); + } + + /** + * Gets an input stream from which the returned data can be read. Note + * that if getData() had been previously invoked it will + * actually return a ByteArrayInputStream created from that data. + * + * @see #getData() + * @return the InputStream. + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public synchronized InputStream getInputStream() + throws IOException, ModuleException + { + if (!initialized) handleResponse(); + + if (Data == null) + return inp_stream; + else + { + getData(); // ensure complete data is read + return new ByteArrayInputStream(Data); + } + } + + + /** + * Should the request be retried by the application? If the application + * used an HttpOutputStream in the request then various + * modules (such as the redirection and authorization modules) are not + * able to resend the request themselves. Instead, it becomes the + * application's responsibility. The application can check this flag, and + * if it's set, resend the exact same request. The modules such as the + * RedirectionModule or AuthorizationModule will then recognize the resend + * and fix up or redirect the request as required (i.e. they defer their + * normal action until the resend). + * + *

If the application resends the request then it must + * use the same HttpOutputStream instance. This is because the + * modules use this to recognize the retried request and to perform the + * necessary work on the request before it's sent. + * + *

Here is a skeleton example of usage: + *

+     *     OutputStream out = new HttpOutputStream(1234);
+     *     do
+     *     {
+     *         rsp = con.Post("/cgi-bin/my_cgi", out);
+     *         out.write(...);
+     *         out.close();
+     *     } while (rsp.retryRequest());
+     *
+     *     if (rsp.getStatusCode() >= 300)
+     *         ...
+     * 
+ * + *

Note that for this to ever return true, the java system property + * HTTPClient.deferStreamed must be set to true at the beginning + * of the application (before the HTTPConnection class is loaded). This + * prevents unwary applications from causing inadvertent memory leaks. If + * an application does set this, then it must resend any request + * whose response returns true here in order to prevent memory leaks (a + * switch to JDK 1.2 will allow us to use weak references and eliminate + * this problem). + * + * @return true if the request should be retried. + * @exception IOException If any exception occurs on the socket. + * @exception ModuleException if any module encounters an exception. + */ + public boolean retryRequest() throws IOException, ModuleException + { + if (!initialized) + { + try + { handleResponse(); } + catch (RetryException re) + { this.retry = response.retry; } + } + return retry; + } + + + /** + * produces a full list of headers and their values, one per line. + * + * @return a string containing the headers + */ + public String toString() + { + if (!initialized) + { + try + { handleResponse(); } + catch (Exception e) + { + if (!(e instanceof InterruptedIOException)) + { + Log.write(Log.RESP, "HResp: (\"" + method + " " + + OriginalURI.getPathAndQuery() + "\")"); + Log.write(Log.RESP, " ", e); + } + return "Failed to read headers: " + e; + } + } + + String nl = System.getProperty("line.separator", "\n"); + + StringBuffer str = new StringBuffer(Version); + str.append(' '); + str.append(StatusCode); + str.append(' '); + str.append(ReasonLine); + str.append(nl); + + if (EffectiveURI != null) + { + str.append("Effective-URI: "); + str.append(EffectiveURI); + str.append(nl); + } + + Enumeration hdr_list = Headers.keys(); + while (hdr_list.hasMoreElements()) + { + String hdr = (String) hdr_list.nextElement(); + str.append(hdr); + str.append(": "); + str.append(Headers.get(hdr)); + str.append(nl); + } + + return str.toString(); + } + + + // Helper Methods + + + HTTPClientModule[] getModules() + { + return modules; + } + + + /** + * Processes a Response. This is done by calling the response handler + * in each module. When all is done, the various fields of this instance + * are intialized from the last Response. + * + * @exception IOException if any handler throws an IOException. + * @exception ModuleException if any module encounters an exception. + * @return true if a new request was generated. This is used for internal + * subrequests only + */ + synchronized boolean handleResponse() throws IOException, ModuleException + { + if (initialized) return false; + + + /* first get the response if necessary */ + + if (out_stream != null) + { + response = out_stream.getResponse(); + response.http_resp = this; + out_stream = null; + } + + + /* go through modules and handle them */ + + doModules: while (true) + { + + Phase1: for (int idx=0; idxcase-insensitive. + * + * @param obj the object to compare with + * @return true if obj is an HttpHeaderElement with the same + * name as this element. + */ + public boolean equals(Object obj) + { + if ((obj != null) && (obj instanceof HttpHeaderElement)) + { + String other = ((HttpHeaderElement) obj).name; + return name.equalsIgnoreCase(other); + } + + return false; + } + + + /** + * @return a string containing the HttpHeaderElement formatted as it + * would appear in a header + */ + public String toString() + { + StringBuffer buf = new StringBuffer(); + appendTo(buf); + return buf.toString(); + } + + + /** + * Append this header element to the given buffer. This is basically a + * more efficient version of toString() for assembling + * multiple elements. + * + * @param buf the StringBuffer to append this header to + * @see #toString() + */ + public void appendTo(StringBuffer buf) + { + buf.append(name); + + if (value != null) + { + if (Util.needsQuoting(value)) + { + buf.append("=\""); + buf.append(Util.quoteString(value, "\\\"")); + buf.append('"'); + } + else + { + buf.append('='); + buf.append(value); + } + } + + for (int idx=0; idx + * OutputStream out = new HttpOutputStream(12345); + * rsp = con.Post("/cgi-bin/my_cgi", out); + * out.write(...); + * out.close(); + * if (rsp.getStatusCode() >= 300) + * ... + * + * + *

There are two constructors for this class, one taking a length parameter, + * and one without any parameters. If the stream is created with a length + * then the request will be sent with the corresponding Content-length header + * and anything written to the stream will be written on the socket immediately. + * This is the preferred way. If the stream is created without a length then + * one of two things will happen: if, at the time of the request, the server + * is known to understand HTTP/1.1 then each write() will send the data + * immediately using the chunked encoding. If, however, either the server + * version is unknown (because this is first request to that server) or the + * server only understands HTTP/1.0 then all data will be written to a buffer + * first, and only when the stream is closed will the request be sent. + * + *

Another reason that using the HttpOutputStream(length) + * constructor is recommended over the HttpOutputStream() one is + * that some HTTP/1.1 servers do not allow the chunked transfer encoding to + * be used when POSTing to a cgi script. This is because the way the cgi API + * is defined the cgi script expects a Content-length environment variable. + * If the data is sent using the chunked transfer encoding however, then the + * server would have to buffer all the data before invoking the cgi so that + * this variable could be set correctly. Not all servers are willing to do + * this. + * + *

If you cannot use the HttpOutputStream(length) constructor and + * are having problems sending requests (usually a 411 response) then you can + * try setting the system property HTTPClient.dontChunkRequests to + * true (this needs to be done either on the command line or + * somewhere in the code before the HTTPConnection is first accessed). This + * will prevent the client from using the chunked encoding in this case and + * will cause the HttpOutputStream to buffer all the data instead, sending it + * only when close() is invoked. + * + *

The behaviour of a request sent with an output stream may differ from + * that of a request sent with a data parameter. The reason for this is that + * the various modules cannot resend a request which used an output stream. + * Therefore such things as authorization and retrying of requests won't be + * done by the HTTPClient for such requests. But see {@link + * HTTPResponse#retryRequest() HTTPResponse.retryRequest} for a partial + * solution. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +public class HttpOutputStream extends OutputStream +{ + /** null trailers */ + private static final NVPair[] empty = new NVPair[0]; + + /** the length of the data to be sent */ + private int length; + + /** the length of the data received so far */ + private int rcvd = 0; + + /** the request this stream is associated with */ + private Request req = null; + + /** the response from sendRequest if we stalled the request */ + private Response resp = null; + + /** the socket output stream */ + private OutputStream os = null; + + /** the buffer to be used if needed */ + private ByteArrayOutputStream bos = null; + + /** the trailers to send if using chunked encoding. */ + private NVPair[] trailers = empty; + + /** the timeout to pass to SendRequest() */ + private int con_to = 0; + + /** just ignore all the data if told to do so */ + private boolean ignore = false; + + + // Constructors + + /** + * Creates an output stream of unspecified length. Note that it is + * highly recommended that this constructor be avoided + * where possible and HttpOutputStream(int) used instead. + * + * @see HttpOutputStream#HttpOutputStream(int) + */ + public HttpOutputStream() + { + length = -1; + } + + + /** + * This creates an output stream which will take length bytes + * of data. + * + * @param length the number of bytes which will be sent over this stream + */ + public HttpOutputStream(int length) + { + if (length < 0) + throw new IllegalArgumentException("Length must be greater equal 0"); + this.length = length; + } + + + // Methods + + /** + * Associates this stream with a request and the actual output stream. + * No other methods in this class may be invoked until this method has + * been invoked by the HTTPConnection. + * + * @param req the request this stream is to be associated with + * @param os the underlying output stream to write our data to, or null + * if we should write to a ByteArrayOutputStream instead. + * @param con_to connection timeout to use in sendRequest() + */ + void goAhead(Request req, OutputStream os, int con_to) + { + this.req = req; + this.os = os; + this.con_to = con_to; + + if (os == null) + bos = new ByteArrayOutputStream(); + + Log.write(Log.CONN, "OutS: Stream ready for writing"); + if (bos != null) + Log.write(Log.CONN, "OutS: Buffering all data before sending " + + "request"); + } + + + /** + * Setup this stream to dump the data to the great bit-bucket in the sky. + * This is needed for when a module handles the request directly. + * + * @param req the request this stream is to be associated with + */ + void ignoreData(Request req) + { + this.req = req; + ignore = true; + } + + + /** + * Return the response we got from sendRequest(). This waits until + * the request has actually been sent. + * + * @return the response returned by sendRequest() + */ + synchronized Response getResponse() + { + while (resp == null) + try { wait(); } catch (InterruptedException ie) { } + + return resp; + } + + + /** + * Returns the number of bytes this stream is willing to accept, or -1 + * if it is unbounded. + * + * @return the number of bytes + */ + public int getLength() + { + return length; + } + + + /** + * Gets the trailers which were set with setTrailers(). + * + * @return an array of header fields + * @see #setTrailers(HTTPClient.NVPair[]) + */ + public NVPair[] getTrailers() + { + return trailers; + } + + + /** + * Sets the trailers to be sent if the output is sent with the + * chunked transfer encoding. These must be set before the output + * stream is closed for them to be sent. + * + *

Any trailers set here should be mentioned + * in a Trailer header in the request (see section 14.40 + * of draft-ietf-http-v11-spec-rev-06.txt). + * + *

This method (and its related getTrailers())) are + * in this class and not in Request because setting + * trailers is something an application may want to do, not only + * modules. + * + * @param trailers an array of header fields + */ + public void setTrailers(NVPair[] trailers) + { + if (trailers != null) + this.trailers = trailers; + else + this.trailers = empty; + } + + + /** + * Reset this output stream, so it may be reused in a retried request. + * This method may only be invoked by modules, and must + * never be invoked by an application. + */ + public void reset() + { + rcvd = 0; + req = null; + resp = null; + os = null; + bos = null; + con_to = 0; + ignore = false; + } + + + /** + * Writes a single byte on the stream. It is subject to the same rules + * as write(byte[], int, int). + * + * @param b the byte to write + * @exception IOException if any exception is thrown by the socket + * @see #write(byte[], int, int) + */ + public void write(int b) throws IOException, IllegalAccessError + { + byte[] tmp = { (byte) b }; + write(tmp, 0, 1); + } + + + /** + * Writes an array of bytes on the stream. This method may not be used + * until this stream has been passed to one of the methods in + * HTTPConnection (i.e. until it has been associated with a request). + * + * @param buf an array containing the data to write + * @param off the offset of the data whithin the buffer + * @param len the number bytes (starting at off) to write + * @exception IOException if any exception is thrown by the socket, or + * if writing len bytes would cause more bytes to + * be written than this stream is willing to accept. + * @exception IllegalAccessError if this stream has not been associated + * with a request yet + */ + public synchronized void write(byte[] buf, int off, int len) + throws IOException, IllegalAccessError + { + if (req == null) + throw new IllegalAccessError("Stream not associated with a request"); + + if (ignore) return; + + if (length != -1 && rcvd+len > length) + { + IOException ioe = + new IOException("Tried to write too many bytes (" + (rcvd+len) + + " > " + length + ")"); + req.getConnection().closeDemux(ioe, false); + req.getConnection().outputFinished(); + throw ioe; + } + + try + { + if (bos != null) + bos.write(buf, off, len); + else if (length != -1) + os.write(buf, off, len); + else + os.write(Codecs.chunkedEncode(buf, off, len, null, false)); + } + catch (IOException ioe) + { + req.getConnection().closeDemux(ioe, true); + req.getConnection().outputFinished(); + throw ioe; + } + + rcvd += len; + } + + + /** + * Closes the stream and causes the data to be sent if it has not already + * been done so. This method must be invoked when all + * data has been written. + * + * @exception IOException if any exception is thrown by the underlying + * socket, or if too few bytes were written. + * @exception IllegalAccessError if this stream has not been associated + * with a request yet. + */ + public synchronized void close() throws IOException, IllegalAccessError + { + if (req == null) + throw new IllegalAccessError("Stream not associated with a request"); + + if (ignore) return; + + if (bos != null) + { + req.setData(bos.toByteArray()); + req.setStream(null); + + if (trailers.length > 0) + { + NVPair[] hdrs = req.getHeaders(); + + // remove any Trailer header field + + int len = hdrs.length; + for (int idx=0; idx 0) + { + Log.write(Log.CONN, "OutS: Sending trailers:"); + for (int idx=0; idxThis class can be used to replace the HttpClient in the JDK with this + * HTTPClient by defining the property + * java.protocol.handler.pkgs=HTTPClient. + * + *

One difference between Sun's HttpClient and this one is that this + * one will provide you with a real output stream if possible. This leads + * to two changes: you should set the request property "Content-Length", + * if possible, before invoking getOutputStream(); and in many cases + * getOutputStream() implies connect(). This should be transparent, though, + * apart from the fact that you can't change any headers or other settings + * anymore once you've gotten the output stream. + * So, for large data do: + *

+ *   HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ *
+ *   con.setDoOutput(true);
+ *   con.setRequestProperty("Content-Length", ...);
+ *   OutputStream out = con.getOutputStream();
+ *
+ *   out.write(...);
+ *   out.close();
+ *
+ *   if (con.getResponseCode() != 200)
+ *       ...
+ * 
+ * + *

The HTTPClient will send the request data using the chunked transfer + * encoding when no Content-Length is specified and the server is HTTP/1.1 + * compatible. Because cgi-scripts can't usually handle this, you may + * experience problems trying to POST data. For this reason, whenever + * the Content-Type is application/x-www-form-urlencoded getOutputStream() + * will buffer the data before sending it so as prevent chunking. If you + * are sending requests with a different Content-Type and are experiencing + * problems then you may want to try setting the system property + * HTTPClient.dontChunkRequests to true (this needs + * to be done either on the command line or somewhere in the code before + * the first URLConnection.openConnection() is invoked). + * + *

A second potential incompatibility is that the HTTPClient aggresively + * resuses connections, and can do so more often that Sun's client. This + * can cause problems if you send multiple requests, and the first one has + * a long response. In this case (assuming the server allows the connection + * to be used for multiple requests) the responses to second, third, etc + * request won't be received until the first response has been completely + * read. With Sun's client on the other hand you may not experience this, + * as it may not be able to keep the connection open and there may create + * multiple connections for the requests. This allows the responses to the + * second, third, etc requests to be read before the first response has + * completed. Note: whether this will happen depends on + * details of the resource being requested and the server. In many cases + * the HTTPClient and Sun's client will exhibit the same behaviour. Also, + * applications which depend on being able to read the second response + * before the first one has completed must be considered broken, because + * A) this behaviour cannot be relied upon even in Sun's current client, + * and B) Sun's implementation will exhibit the same problem if they ever + * switch to HTTP/1.1. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +public class HttpURLConnection extends java.net.HttpURLConnection +{ + /** the cache of HTTPConnections */ + protected static Hashtable connections = new Hashtable(); + + /** the current connection */ + protected HTTPConnection con; + + /** the cached url.toString() */ + private String urlString; + + /** the resource */ + private String resource; + + /** the current method */ + private String method; + + /** has the current method been set via setRequestMethod()? */ + private boolean method_set; + + /** the default request headers */ + private static NVPair[] default_headers = new NVPair[0]; + + /** the request headers */ + private NVPair[] headers; + + /** the response */ + protected HTTPResponse resp; + + /** is the redirection module activated for this instance? */ + private boolean do_redir; + + /** the RedirectionModule class */ + private static Class redir_mod; + + /** the output stream used for POST and PUT */ + private OutputStream output_stream; + + + static + { + // The default allowUserAction in java.net.URLConnection is + // false. + try + { + if (Boolean.getBoolean("HTTPClient.HttpURLConnection.AllowUI")) + setDefaultAllowUserInteraction(true); + } + catch (SecurityException se) + { } + + // get the RedirectionModule class + try + { redir_mod = Class.forName("HTTPClient.RedirectionModule"); } + catch (ClassNotFoundException cnfe) + { throw new NoClassDefFoundError(cnfe.getMessage()); } + + // Set the User-Agent if the http.agent property is set + try + { + String agent = System.getProperty("http.agent"); + if (agent != null) + setDefaultRequestProperty("User-Agent", agent); + } + catch (SecurityException se) + { } + } + + + // Constructors + + private static String non_proxy_hosts = ""; + private static String proxy_host = ""; + private static int proxy_port = -1; + + /** + * Construct a connection to the specified url. A cache of + * HTTPConnections is used to maximize the reuse of these across + * multiple HttpURLConnections. + * + *
The default method is "GET". + * + * @param url the url of the request + * @exception ProtocolNotSuppException if the protocol is not supported + */ + public HttpURLConnection(URL url) + throws ProtocolNotSuppException, IOException + { + super(url); + + // first read proxy properties and set + try + { + String hosts = System.getProperty("http.nonProxyHosts", ""); + if (!hosts.equalsIgnoreCase(non_proxy_hosts)) + { + connections.clear(); + non_proxy_hosts = hosts; + String[] list = Util.splitProperty(hosts); + for (int idx=0; idxnull, even though it the + * 0-th header has a value. + * + * @param n which header to return. + * @return the header name, or null if not that many headers. + */ + public String getHeaderFieldKey(int n) + { + if (hdr_keys == null) + fill_hdr_arrays(); + + if (n >= 0 && n < hdr_keys.length) + return hdr_keys[n]; + else + return null; + } + + + /** + * Gets header value of the n-th header. Calls connect() if not connected. + * The value of 0-th header is the Status-Line (e.g. "HTTP/1.1 200 Ok"). + * + * @param n which header to return. + * @return the header value, or null if not that many headers. + */ + public String getHeaderField(int n) + { + if (hdr_values == null) + fill_hdr_arrays(); + + if (n >= 0 && n < hdr_values.length) + return hdr_values[n]; + else + return null; + } + + + /** + * Cache the list of headers. + */ + private void fill_hdr_arrays() + { + try + { + if (!connected) connect(); + + // count number of headers + int num = 1; + Enumeration enum = resp.listHeaders(); + while (enum.hasMoreElements()) + { + num++; + enum.nextElement(); + } + + // allocate arrays + hdr_keys = new String[num]; + hdr_values = new String[num]; + + // fill arrays + enum = resp.listHeaders(); + for (int idx=1; idxThis method will not cause a connection to be initiated. + * + * @return an InputStream, or null if either the connection hasn't + * been established yet or no error occured + * @see java.net.HttpURLConnection#getErrorStream() + * @since V0.3-1 + */ + public InputStream getErrorStream() + { + try + { + if (!doInput || !connected || resp.getStatusCode() < 300 || + resp.getHeaderAsInt("Content-length") <= 0) + return null; + + return resp.getInputStream(); + } + catch (Exception e) + { return null; } + } + + + /** + * Gets an output stream which can be used send an entity with the + * request. Can be called multiple times, in which case always the + * same stream is returned. + * + *

The default request method changes to "POST" when this method is + * called. Cannot be called after connect(). + * + *

If no Content-type has been set it defaults to + * application/x-www-form-urlencoded. Furthermore, if the + * Content-type is application/x-www-form-urlencoded then all + * output will be collected in a buffer before sending it to the server; + * otherwise an HttpOutputStream is used. + * + * @return an OutputStream + * @exception ProtocolException if already connect()'ed, if output is not + * enabled or if the request method does not + * support output. + * @see java.net.URLConnection#setDoOutput(boolean) + * @see HTTPClient.HttpOutputStream + */ + public synchronized OutputStream getOutputStream() throws IOException + { + if (connected) + throw new ProtocolException("Already connected!"); + + if (!doOutput) + throw new ProtocolException("Output not enabled! (use setDoOutput(true))"); + if (!method_set) + method = "POST"; + else if (method.equals("HEAD") || method.equals("GET") || + method.equals("TRACE")) + throw new ProtocolException("Method "+method+" does not support output!"); + + if (getRequestProperty("Content-type") == null) + setRequestProperty("Content-type", "application/x-www-form-urlencoded"); + + if (output_stream == null) + { + Log.write(Log.URLC, "URLC: (" + urlString + ") creating output stream"); + + String cl = getRequestProperty("Content-Length"); + if (cl != null) + output_stream = new HttpOutputStream(Integer.parseInt(cl.trim())); + else + { + // Hack: because of restrictions when using true output streams + // and because form-data is usually quite limited in size, we + // first collect all data before sending it if this is + // form-data. + if (getRequestProperty("Content-type").equals( + "application/x-www-form-urlencoded")) + output_stream = new ByteArrayOutputStream(300); + else + output_stream = new HttpOutputStream(); + } + + if (output_stream instanceof HttpOutputStream) + connect(); + } + + return output_stream; + } + + + /** + * Gets the url for this connection. If we're connect()'d and the request + * was redirected then the url returned is that of the final request. + * + * @return the final url, or null if any exception occured. + */ + public URL getURL() + { + if (connected) + { + try + { return resp.getEffectiveURI().toURL(); } + catch (Exception e) + { return null; } + } + + return url; + } + + + /** + * Sets the If-Modified-Since header. + * + * @param time the number of milliseconds since 1970. + */ + public void setIfModifiedSince(long time) + { + super.setIfModifiedSince(time); + setRequestProperty("If-Modified-Since", Util.httpDate(new Date(time))); + } + + + /** + * Sets an arbitrary request header. + * + * @param name the name of the header. + * @param value the value for the header. + */ + public void setRequestProperty(String name, String value) + { + Log.write(Log.URLC, "URLC: (" + urlString + ") Setting request property: " + + name + " : " + value); + + int idx; + for (idx=0; idxconnect(). + * + * @param set enables automatic redirection handling if true. + */ + public void setInstanceFollowRedirects(boolean set) + { + if (connected) + throw new IllegalStateException("Already connected!"); + + do_redir = set; + } + + + /** + * @return true if automatic redirection handling for this instance is + * enabled. + */ + public boolean getInstanceFollowRedirects() + { + return do_redir; + } + + + /** + * Connects to the server (if connection not still kept alive) and + * issues the request. + */ + public synchronized void connect() throws IOException + { + if (connected) return; + + Log.write(Log.URLC, "URLC: (" + urlString + ") Connecting ..."); + + // useCaches TBD!!! + + synchronized (con) + { + con.setAllowUserInteraction(allowUserInteraction); + if (do_redir) + con.addModule(redir_mod, 2); + else + con.removeModule(redir_mod); + + try + { + if (output_stream instanceof ByteArrayOutputStream) + resp = con.ExtensionMethod(method, resource, + ((ByteArrayOutputStream) output_stream).toByteArray(), + headers); + else + resp = con.ExtensionMethod(method, resource, + (HttpOutputStream) output_stream, headers); + } + catch (ModuleException e) + { throw new IOException(e.toString()); } + } + + connected = true; + } + + + /** + * Closes all the connections to this server. + */ + public void disconnect() + { + Log.write(Log.URLC, "URLC: (" + urlString + ") Disconnecting ..."); + + con.stop(); + } + + + /** + * Shows if request are being made through an http proxy or directly. + * + * @return true if an http proxy is being used. + */ + public boolean usingProxy() + { + return (con.getProxyHost() != null); + } + + + /** + * produces a string. + * @return a string containing the HttpURLConnection + */ + public String toString() + { + return getClass().getName() + "[" + url + "]"; + } +} diff --git a/HTTPClient/IdempotentSequence.java b/HTTPClient/IdempotentSequence.java new file mode 100644 index 0000000..178cccc --- /dev/null +++ b/HTTPClient/IdempotentSequence.java @@ -0,0 +1,405 @@ +/* + * @(#)IdempotentSequence.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.util.Hashtable; +import java.util.Enumeration; + +/** + *

This class checks whether a sequence of requests is idempotent. This + * is used to determine which requests may be automatically retried. This + * class also serves as a central place to record which methods have side + * effects and which methods are idempotent. + * + *

Note: unknown methods (i.e. a method which is not HEAD, GET, POST, PUT, + * DELETE, OPTIONS, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, or + * UNLOCK) are treated conservatively, meaning they are assumed to have side + * effects and are not idempotent. + * + *

Usage: + *

+ *     IdempotentSequence seq = new IdempotentSequence();
+ *     seq.add(r1);
+ *     ...
+ *     if (seq.isIdempotent(r1)) ...
+ *     ...
+ * 
+ * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class IdempotentSequence +{ + /** method number definitions */ + private static final int UNKNOWN = 0, + HEAD = 1, + GET = 2, + POST = 3, + PUT = 4, + DELETE = 5, + OPTIONS = 6, + TRACE = 7, + + // DAV methods + PROPFIND = 8, + PROPPATCH = 9, + MKCOL = 10, + COPY = 11, + MOVE = 12, + LOCK = 13, + UNLOCK = 14; + + /** these are the history of previous requests */ + private int[] m_history; + private String[] r_history; + private int m_len, r_len; + + /** trigger analysis of threads */ + private boolean analysis_done = false; + private Hashtable threads = new Hashtable(); + + + // Constructors + + /** + * Start a new sequence of requests. + */ + public IdempotentSequence() + { + m_history = new int[10]; + r_history = new String[10]; + m_len = 0; + r_len = 0; + } + + + // Methods + + /** + * Add the request to the end of the list of requests. This is used + * to build the complete sequence of requests before determining + * whether the sequence is idempotent. + * + * @param req the next request + */ + public void add(Request req) + { + if (m_len >= m_history.length) + m_history = Util.resizeArray(m_history, m_history.length+10); + m_history[m_len++] = methodNum(req.getMethod()); + + if (r_len >= r_history.length) + r_history = Util.resizeArray(r_history, r_history.length+10); + r_history[r_len++] = req.getRequestURI(); + } + + + /** + * Is this request part of an idempotent sequence? This method must + * not be called before all requests have been added to this + * sequence; similarly, add() must not be called + * after this method was invoked. + * + *

We split up the sequence of requests into individual sub-sequences, + * or threads, with all requests in a thread having the same request-URI + * and no two threads having the same request-URI. Each thread is then + * marked as idempotent or not according to the following rules: + * + *

    + *
  1. If any method is UNKNOWN then the thread is not idempotent; + *
  2. else, if no method has side effects then the thread is idempotent; + *
  3. else, if the first method has side effects and is complete then + * the thread is idempotent; + *
  4. else, if the first method has side effects, is not complete, + * and no other method has side effects then the thread is idempotent; + *
  5. else the thread is not idempotent. + *
+ * + *

The major assumption here is that the side effects of any method + * only apply to resource specified. E.g. a "PUT /barbara.html" + * will only affect the resource "/barbara.html" and nothing else. + * This assumption is violated by POST of course; however, POSTs are + * not pipelined and will therefore never show up here. + * + * @param req the request + */ + public boolean isIdempotent(Request req) + { + if (!analysis_done) + do_analysis(); + + return ((Boolean) threads.get(req.getRequestURI())).booleanValue(); + } + + + private static final Object INDET = new Object(); + + private void do_analysis() + { + for (int idx=0; idxenumerate(). + * + * @return the next element, or null if none left + * @see #enumerate() + */ + public synchronized Object next() + { + if (next_enum == null) return null; + + Object elem = next_enum.element; + next_enum = next_enum.next; + + return elem; + } + + + public static void main(String args[]) throws Exception + { + // LinkedList Test Suite + + System.err.println("\n*** Linked List Tests ..."); + + LinkedList list = new LinkedList(); + list.addToHead("One"); + list.addToEnd("Last"); + if (!list.getFirst().equals("One")) + throw new Exception("First element wrong"); + if (!list.enumerate().equals("One")) + throw new Exception("First element wrong"); + if (!list.next().equals("Last")) + throw new Exception("Last element wrong"); + if (list.next() != null) + throw new Exception("End of list wrong"); + list.remove("One"); + if (!list.getFirst().equals("Last")) + throw new Exception("First element wrong"); + list.remove("Last"); + if (list.getFirst() != null) + throw new Exception("End of list wrong"); + + list = new LinkedList(); + list.addToEnd("Last"); + list.addToHead("One"); + if (!list.getFirst().equals("One")) + throw new Exception("First element wrong"); + if (!list.enumerate().equals("One")) + throw new Exception("First element wrong"); + if (!list.next().equals("Last")) + throw new Exception("Last element wrong"); + if (list.next() != null) + throw new Exception("End of list wrong"); + if (!list.enumerate().equals("One")) + throw new Exception("First element wrong"); + list.remove("One"); + if (!list.next().equals("Last")) + throw new Exception("Last element wrong"); + list.remove("Last"); + if (list.next() != null) + throw new Exception("End of list wrong"); + + list = new LinkedList(); + list.addToEnd("Last"); + list.addToHead("Two"); + list.addToHead("One"); + if (!list.getFirst().equals("One")) + throw new Exception("First element wrong"); + if (!list.enumerate().equals("One")) + throw new Exception("First element wrong"); + if (!list.next().equals("Two")) + throw new Exception("Second element wrong"); + if (!list.next().equals("Last")) + throw new Exception("Last element wrong"); + if (list.next() != null) + throw new Exception("End of list wrong"); + list.remove("Last"); + list.remove("Two"); + list.remove("One"); + if (list.getFirst() != null) + throw new Exception("Empty list wrong"); + + System.err.println("\n*** Tests finished successfuly"); + } +} + + +/** + * The represents a single element in the linked list. + */ +class LinkElement +{ + Object element; + LinkElement next; + + LinkElement(Object elem, LinkElement next) + { + this.element = elem; + this.next = next; + } +} diff --git a/HTTPClient/Log.java b/HTTPClient/Log.java new file mode 100644 index 0000000..35d9915 --- /dev/null +++ b/HTTPClient/Log.java @@ -0,0 +1,318 @@ +/* + * @(#)Log.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.io.Writer; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.io.OutputStreamWriter; +import java.io.ByteArrayOutputStream; +import java.util.Calendar; +import java.util.TimeZone; + +/** + * This is a simple logger for the HTTPClient. It defines a number of + * "facilities", each representing one or more logically connected classes. + * Logging can be enabled or disabled on a per facility basis. Furthermore, the + * logging output can be redirected to any desired place, such as a file or a + * buffer; by default all logging goes to System.err. + * + *

All log entries are preceded by the name of the currently executing + * thread, enclosed in {}'s, and the current time in hours, minutes, seconds, + * and milliseconds, enclosed in []'s. Example: + * {Thread-5} [20:14:03.244] Conn: Sending Request + * + *

When the class is loaded, two java system properties are read: + * HTTPClient.log.file and HTTPClient.log.mask. The first + * one, if set, causes all logging to be redirected to the file with the given + * name. The second one, if set, is used for setting which facilities are + * enabled; the value must be the bitwise OR ('|') of the values of the desired + * enabled facilities. E.g. a value of 3 would enable logging for the + * HTTPConnection and HTTPResponse, a value of 16 would enable cookie related + * logging, and a value of 8 would enable authorization related logging; a + * value of -1 would enable logging for all facilities. By default logging is + * disabled. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3-3 + */ +public class Log +{ + /** The HTTPConnection facility (1) */ + public static final int CONN = 1 << 0; + /** The HTTPResponse facility (2) */ + public static final int RESP = 1 << 1; + /** The StreamDemultiplexor facility (4) */ + public static final int DEMUX = 1 << 2; + /** The Authorization facility (8) */ + public static final int AUTH = 1 << 3; + /** The Cookies facility (16) */ + public static final int COOKI = 1 << 4; + /** The Modules facility (32) */ + public static final int MODS = 1 << 5; + /** The Socks facility (64) */ + public static final int SOCKS = 1 << 6; + /** The ULRConnection facility (128) */ + public static final int URLC = 1 << 7; + /** All the facilities - for use in setLogging (-1) */ + public static final int ALL = ~0; + + private static final String NL = System.getProperty("line.separator"); + private static final long TZ_OFF; + + private static int facMask = 0; + private static Writer logWriter = new OutputStreamWriter(System.err); + private static boolean closeWriter = false; + + + static + { + Calendar now = Calendar.getInstance(); + TZ_OFF = TimeZone.getDefault().getOffset(now.get(Calendar.ERA), + now.get(Calendar.YEAR), + now.get(Calendar.MONTH), + now.get(Calendar.DAY_OF_MONTH), + now.get(Calendar.DAY_OF_WEEK), + now.get(Calendar.MILLISECOND)); + + try + { + String file = System.getProperty("HTTPClient.log.file"); + if (file != null) + { + try + { setLogWriter(new FileWriter(file), true); } + catch (IOException ioe) + { + System.err.println("failed to open file log stream `" + + file + "': " + ioe); + } + } + } + catch (Exception e) + { } + + try + { + facMask = Integer.getInteger("HTTPClient.log.mask", 0).intValue(); + } + catch (Exception e) + { } + } + + + // Constructors + + /** + * Not meant to be instantiated + */ + private Log() + { + } + + + // Methods + + /** + * Write the given message to the current log if logging for the given facility is + * enabled. + * + * @param facility the facility which is logging the message + * @param msg the message to log + */ + public static void write(int facility, String msg) + { + if ((facMask & facility) == 0) + return; + + try + { + writePrefix(); + logWriter.write(msg); + logWriter.write(NL); + logWriter.flush(); + } + catch (IOException ioe) + { + System.err.println("Failed to write to log: " + ioe); + System.err.println("Failed log Entry was: " + msg); + } + } + + /** + * Write the stack trace of the given exception to the current log if logging for the + * given facility is enabled. + * + * @param facility the facility which is logging the message + * @param prefix the string with which to prefix the stack trace; may be null + * @param t the exception to log + */ + public static void write(int facility, String prefix, Throwable t) + { + if ((facMask & facility) == 0) + return; + + synchronized (Log.class) + { + if (!(logWriter instanceof PrintWriter)) + logWriter = new PrintWriter(logWriter); + } + + try + { + writePrefix(); + if (prefix != null) + logWriter.write(prefix); + t.printStackTrace((PrintWriter) logWriter); + logWriter.flush(); + } + catch (IOException ioe) + { + System.err.println("Failed to write to log: " + ioe); + System.err.print("Failed log Entry was: " + prefix); + t.printStackTrace(System.err); + } + } + + /** + * Write the contents of the given buffer to the current log if logging for + * the given facility is enabled. + * + * @param facility the facility which is logging the message + * @param prefix the string with which to prefix the buffer contents; + * may be null + * @param buf the buffer to dump + */ + public static void write(int facility, String prefix, ByteArrayOutputStream buf) + { + if ((facMask & facility) == 0) + return; + + try + { + writePrefix(); + if (prefix != null) + logWriter.write(prefix); + logWriter.write(NL); + logWriter.write(new String(buf.toByteArray(), "ISO_8859-1")); + logWriter.flush(); + } + catch (IOException ioe) + { + System.err.println("Failed to write to log: " + ioe); + System.err.println("Failed log Entry was: " + prefix); + System.err.println(new String(buf.toByteArray())); + } + } + + /** + * Write a log line prefix of the form + *

+     *  {thread-name} [time]
+     * 
+ */ + private static final void writePrefix() throws IOException { + logWriter.write("{" + Thread.currentThread().getName() + "} "); + + int mill = (int) ((System.currentTimeMillis() + TZ_OFF) % (24 * 3600000)); + int secs = mill / 1000; + int mins = secs / 60; + int hours = mins / 60; + logWriter.write("[" + fill2(hours) + ':' + fill2(mins - hours*60) + + ':' + fill2(secs - mins * 60) + '.' + + fill3(mill - secs * 1000) + "] "); + } + + private static final String fill2(int num) { + return ((num < 10) ? "0" : "") + num; + } + + private static final String fill3(int num) { + return ((num < 10) ? "00" : (num < 100) ? "0" : "") + num; + } + + /** + * Check whether logging for the given facility is enabled or not. + * + * @param facility the facility to check + * @return true if logging for the given facility is enable; false otherwise + */ + public static boolean isEnabled(int facility) + { + return ((facMask & facility) != 0); + } + + /** + * Enable or disable logging for the given facilities. + * + * @param facilities the facilities for which to enable or disable logging. + * This is bitwise OR ('|') of all the desired + * facilities; use {@link #ALL ALL} to affect all facilities + * @param enable if true, enable logging for the chosen facilities; if + * false, disable logging for them. + */ + public static void setLogging(int facilities, boolean enable) + { + if (enable) + facMask |= facilities; + else + facMask &= ~facilities; + } + + /** + * Set the writer to which to log. By default, things are logged to + * System.err. + * + * @param log the writer to log to; if null, nothing is changed + * @param closeWhenDone if true, close this stream when a new stream is set + * again + */ + public static void setLogWriter(Writer log, boolean closeWhenDone) + { + if (log == null) + return; + + if (closeWriter) + { + try + { logWriter.close(); } + catch (IOException ioe) + { System.err.println("Error closing log stream: " + ioe); } + } + + logWriter = log; + closeWriter = closeWhenDone; + } +} diff --git a/HTTPClient/MD5.java b/HTTPClient/MD5.java new file mode 100644 index 0000000..da6d974 --- /dev/null +++ b/HTTPClient/MD5.java @@ -0,0 +1,161 @@ +/* + * @(#)MD5.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + + +/** + * Some utility methods for digesting info using MD5. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3-3 + */ +class MD5 +{ + private static final char[] hex = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', + }; + + /** + * Turns array of bytes into string representing each byte as + * unsigned hex number. + * + * @param hash array of bytes to convert to hex-string + * @return generated hex string + */ + public static final String toHex(byte hash[]) + { + StringBuffer buf = new StringBuffer(hash.length * 2); + + for (int idx=0; idx> 4) & 0x0f]).append(hex[hash[idx] & 0x0f]); + + return buf.toString(); + } + + /** + * Digest the input. + * + * @param input the data to be digested. + * @return the md5-digested input + */ + public static final byte[] digest(byte[] input) + { + try + { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return md5.digest(input); + } + catch (NoSuchAlgorithmException nsae) + { + throw new Error(nsae.toString()); + } + } + + /** + * Digest the input. + * + * @param input1 the first part of the data to be digested. + * @param input2 the second part of the data to be digested. + * @return the md5-digested input + */ + public static final byte[] digest(byte[] input1, byte[] input2) + { + try + { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(input1); + return md5.digest(input2); + } + catch (NoSuchAlgorithmException nsae) + { + throw new Error(nsae.toString()); + } + } + + /** + * Digest the input. + * + * @param input the data to be digested. + * @return the md5-digested input as a hex string + */ + public static final String hexDigest(byte[] input) + { + return toHex(digest(input)); + } + + /** + * Digest the input. + * + * @param input1 the first part of the data to be digested. + * @param input2 the second part of the data to be digested. + * @return the md5-digested input as a hex string + */ + public static final String hexDigest(byte[] input1, byte[] input2) + { + return toHex(digest(input1, input2)); + } + + /** + * Digest the input. + * + * @param input the data to be digested. + * @return the md5-digested input as a hex string + */ + public static final byte[] digest(String input) + { + try + { return digest(input.getBytes("8859_1")); } + catch (UnsupportedEncodingException uee) + { throw new Error(uee.toString()); } + } + + /** + * Digest the input. + * + * @param input the data to be digested. + * @return the md5-digested input as a hex string + */ + public static final String hexDigest(String input) + { + try + { return toHex(digest(input.getBytes("8859_1"))); } + catch (UnsupportedEncodingException uee) + { throw new Error(uee.toString()); } + } +} diff --git a/HTTPClient/MD5InputStream.java b/HTTPClient/MD5InputStream.java new file mode 100644 index 0000000..96f3346 --- /dev/null +++ b/HTTPClient/MD5InputStream.java @@ -0,0 +1,143 @@ +/* + * @(#)MD5InputStream.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.io.InputStream; +import java.io.FilterInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + + +/** + * This class calculates a running md5 digest of the data read. When the + * stream is closed the calculated digest is passed to a HashVerifier which + * is expected to verify this digest and to throw an Exception if it fails. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class MD5InputStream extends FilterInputStream +{ + private HashVerifier verifier; + private MessageDigest md5; + private long rcvd = 0; + private boolean closed = false; + + + /** + * @param is the input stream over which the md5 hash is to be calculated + * @param verifier the HashVerifier to invoke when the stream is closed + */ + public MD5InputStream(InputStream is, HashVerifier verifier) + { + super(is); + this.verifier = verifier; + try + { md5 = MessageDigest.getInstance("MD5"); } + catch (NoSuchAlgorithmException nsae) + { throw new Error(nsae.toString()); } + } + + + public synchronized int read() throws IOException + { + int b = in.read(); + if (b != -1) + md5.update((byte) b); + else + real_close(); + + rcvd++; + return b; + } + + + public synchronized int read(byte[] buf, int off, int len) + throws IOException + { + int num = in.read(buf, off, len); + if (num > 0) + md5.update(buf, off, num); + else + real_close(); + + rcvd += num; + return num; + } + + + public synchronized long skip(long num) throws IOException + { + byte[] tmp = new byte[(int) num]; + int got = read(tmp, 0, (int) num); + + if (got > 0) + return (long) got; + else + return 0L; + } + + + /** + * Close the stream and check the digest. If the stream has not been + * fully read then the rest of the data will first be read (and discarded) + * to complete the digest calculation. + * + * @exception IOException if the close()'ing the underlying stream throws + * an IOException, or if the expected digest and + * the calculated digest don't match. + */ + public synchronized void close() throws IOException + { + while (skip(10000) > 0) ; + real_close(); + } + + + /** + * Close the stream and check the digest. + * + * @exception IOException if the close()'ing the underlying stream throws + * an IOException, or if the expected digest and + * the calculated digest don't match. + */ + private void real_close() throws IOException + { + if (closed) return; + closed = true; + + in.close(); + verifier.verifyHash(md5.digest(), rcvd); + } +} diff --git a/HTTPClient/ModuleException.java b/HTTPClient/ModuleException.java new file mode 100644 index 0000000..fb75d6a --- /dev/null +++ b/HTTPClient/ModuleException.java @@ -0,0 +1,66 @@ +/* + * @(#)ModuleException.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + + +/** + * Signals that an exception occured in a module. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +public class ModuleException extends Exception +{ + + /** + * Constructs an ModuleException with no detail message. A detail + * message is a String that describes this particular exception. + */ + public ModuleException() + { + super(); + } + + + /** + * Constructs an ModuleException class with the specified detail message. + * A detail message is a String that describes this particular exception. + * + * @param msg the String containing a detail message + */ + public ModuleException(String msg) + { + super(msg); + } +} diff --git a/HTTPClient/NVPair.java b/HTTPClient/NVPair.java new file mode 100644 index 0000000..dd2a6d3 --- /dev/null +++ b/HTTPClient/NVPair.java @@ -0,0 +1,110 @@ +/* + * @(#)NVPair.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + + +/** + * This class holds a Name/Value pair of strings. It's used for headers, + * form-data, attribute-lists, etc. This class is immutable. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public final class NVPair +{ + /** the name */ + private String name; + + /** the value */ + private String value; + + + // Constructors + + /** + * Creates a new name/value pair and initializes it to the + * specified name and value. + * + * @param name the name + * @param value the value + */ + public NVPair(String name, String value) + { + this.name = name; + this.value = value; + } + + /** + * Creates a copy of a given name/value pair. + * + * @param p the name/value pair to copy + */ + public NVPair(NVPair p) + { + this(p.name, p.value); + } + + + // Methods + + /** + * Get the name. + * + * @return the name + */ + public final String getName() + { + return name; + } + + /** + * Get the value. + * + * @return the value + */ + public final String getValue() + { + return value; + } + + + /** + * Produces a string containing the name and value of this instance. + * + * @return a string containing the class name and the name and value + */ + public String toString() + { + return getClass().getName() + "[name=" + name + ",value=" + value + "]"; + } +} diff --git a/HTTPClient/ParseException.java b/HTTPClient/ParseException.java new file mode 100644 index 0000000..dce4694 --- /dev/null +++ b/HTTPClient/ParseException.java @@ -0,0 +1,67 @@ +/* + * @(#)ParseException.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + + +/** + * Signals that something went wrong while parsing data. Usually means the + * input data was invalid. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public class ParseException extends Exception +{ + + /** + * Constructs an ParseException with no detail message. + * A detail message is a String that describes this particular exception. + */ + public ParseException() + { + super(); + } + + + /** + * Constructs an ParseException class with the specified detail message. + * A detail message is a String that describes this particular exception. + * + * @param s the String containing a detail message + */ + public ParseException(String s) + { + super(s); + } + +} diff --git a/HTTPClient/ProtocolNotSuppException.java b/HTTPClient/ProtocolNotSuppException.java new file mode 100644 index 0000000..3217b77 --- /dev/null +++ b/HTTPClient/ProtocolNotSuppException.java @@ -0,0 +1,67 @@ +/* + * @(#)ProtocolNotSuppException.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; + +/** + * Signals that the protocol is not supported. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public class ProtocolNotSuppException extends IOException +{ + + /** + * Constructs an ProtocolNotSuppException with no detail message. + * A detail message is a String that describes this particular exception. + */ + public ProtocolNotSuppException() + { + super(); + } + + + /** + * Constructs an ProtocolNotSuppException class with the specified + * detail message. A detail message is a String that describes this + * particular exception. + * @param s the String containing a detail message + */ + public ProtocolNotSuppException(String s) + { + super(s); + } + +} diff --git a/HTTPClient/RedirectionModule.java b/HTTPClient/RedirectionModule.java new file mode 100644 index 0000000..4a5f91d --- /dev/null +++ b/HTTPClient/RedirectionModule.java @@ -0,0 +1,543 @@ +/* + * @(#)RedirectionModule.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.net.ProtocolException; +import java.io.IOException; +import java.util.Hashtable; + + +/** + * This module handles the redirection status codes 301, 302, 303, 305, 306 + * and 307. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class RedirectionModule implements HTTPClientModule +{ + /** a list of permanent redirections (301) */ + private static Hashtable perm_redir_cntxt_list = new Hashtable(); + + /** a list of deferred redirections (used with Response.retryRequest()) */ + private static Hashtable deferred_redir_list = new Hashtable(); + + /** the level of redirection */ + private int level; + + /** the url used in the last redirection */ + private URI lastURI; + + /** used for deferred redirection retries */ + private boolean new_con; + + /** used for deferred redirection retries */ + private Request saved_req; + + + // Constructors + + /** + * Start with level 0. + */ + RedirectionModule() + { + level = 0; + lastURI = null; + saved_req = null; + } + + + // Methods + + /** + * Invoked by the HTTPClient. + */ + public int requestHandler(Request req, Response[] resp) + { + HTTPConnection con = req.getConnection(); + URI new_loc, + cur_loc; + + + // check for retries + + HttpOutputStream out = req.getStream(); + if (out != null && deferred_redir_list.get(out) != null) + { + copyFrom((RedirectionModule) deferred_redir_list.remove(out)); + req.copyFrom(saved_req); + + if (new_con) + return REQ_NEWCON_RST; + else + return REQ_RESTART; + } + + + // handle permanent redirections + + try + { + cur_loc = new URI(new URI(con.getProtocol(), con.getHost(), con.getPort(), null), + req.getRequestURI()); + } + catch (ParseException pe) + { + throw new Error("HTTPClient Internal Error: unexpected exception '" + + pe + "'"); + } + + + // handle permanent redirections + + Hashtable perm_redir_list = Util.getList(perm_redir_cntxt_list, + req.getConnection().getContext()); + if ((new_loc = (URI) perm_redir_list.get(cur_loc)) != null) + { + /* copy query if present in old url but not in new url. This + * isn't strictly conforming, but some scripts fail to properly + * propagate the query string to the Location header. + * + * Unfortunately it looks like we're fucked either way: some + * scripts fail if you don't propagate the query string, some + * fail if you do... God, don't you just love it when people + * can't read a spec? Anway, since we can't get it right for + * all scripts we opt to follow the spec. + String nres = new_loc.getPathAndQuery(), + oquery = Util.getQuery(req.getRequestURI()), + nquery = Util.getQuery(nres); + if (nquery == null && oquery != null) + nres += "?" + oquery; + */ + String nres = new_loc.getPathAndQuery(); + req.setRequestURI(nres); + + try + { lastURI = new URI(new_loc, nres); } + catch (ParseException pe) + { } + + Log.write(Log.MODS, "RdirM: matched request in permanent " + + "redirection list - redoing request to " + + lastURI.toExternalForm()); + + if (!con.isCompatibleWith(new_loc)) + { + try + { con = new HTTPConnection(new_loc); } + catch (Exception e) + { + throw new Error("HTTPClient Internal Error: unexpected " + + "exception '" + e + "'"); + } + + con.setContext(req.getConnection().getContext()); + req.setConnection(con); + return REQ_NEWCON_RST; + } + else + { + return REQ_RESTART; + } + } + + return REQ_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase1Handler(Response resp, RoRequest req) + throws IOException + { + int sts = resp.getStatusCode(); + if (sts < 301 || sts > 307 || sts == 304) + { + if (lastURI != null) // it's been redirected + resp.setEffectiveURI(lastURI); + } + } + + + /** + * Invoked by the HTTPClient. + */ + public int responsePhase2Handler(Response resp, Request req) + throws IOException + { + /* handle various response status codes until satisfied */ + + int sts = resp.getStatusCode(); + switch(sts) + { + case 302: // General (temporary) Redirection (handle like 303) + + /* Note we only do this munging for POST and PUT. For GET it's + * not necessary; for HEAD we probably want to do another HEAD. + * For all others (i.e. methods from WebDAV, IPP, etc) it's + * somewhat unclear - servers supporting those should really + * return a 307 or 303, but some don't (guess who...), so we + * just don't touch those. + */ + if (req.getMethod().equals("POST") || + req.getMethod().equals("PUT")) + { + Log.write(Log.MODS, "RdirM: Received status: " + sts + + " " + resp.getReasonLine() + + " - treating as 303"); + + sts = 303; + } + + case 301: // Moved Permanently + case 303: // See Other (use GET) + case 307: // Moved Temporarily (we mean it!) + + Log.write(Log.MODS, "RdirM: Handling status: " + sts + + " " + resp.getReasonLine()); + + // the spec says automatic redirection may only be done if + // the second request is a HEAD or GET. + if (!req.getMethod().equals("GET") && + !req.getMethod().equals("HEAD") && + sts != 303) + { + Log.write(Log.MODS, "RdirM: not redirected because " + + "method is neither HEAD nor GET"); + + if (sts == 301 && resp.getHeader("Location") != null) + update_perm_redir_list(req, + resLocHdr(resp.getHeader("Location"), req)); + + resp.setEffectiveURI(lastURI); + return RSP_CONTINUE; + } + + case 305: // Use Proxy + case 306: // Switch Proxy + + if (sts == 305 || sts == 306) + Log.write(Log.MODS, "RdirM: Handling status: " + sts + + " " + resp.getReasonLine()); + + // Don't accept 305 from a proxy + if (sts == 305 && req.getConnection().getProxyHost() != null) + { + Log.write(Log.MODS, "RdirM: 305 ignored because " + + "a proxy is already in use"); + + resp.setEffectiveURI(lastURI); + return RSP_CONTINUE; + } + + + /* the level is a primitive way of preventing infinite + * redirections. RFC-2068 set the max to 5, but RFC-2616 + * has loosened this. Since some sites (notably M$) need + * more levels, this is now set to the (arbitrary) value + * of 15 (god only knows why they need to do even 5 + * redirections...). + */ + if (level >= 15 || resp.getHeader("Location") == null) + { + if (level >= 15) + Log.write(Log.MODS, "RdirM: not redirected because "+ + "of too many levels of redirection"); + else + Log.write(Log.MODS, "RdirM: not redirected because "+ + "no Location header was present"); + + resp.setEffectiveURI(lastURI); + return RSP_CONTINUE; + } + level++; + + URI loc = resLocHdr(resp.getHeader("Location"), req); + + HTTPConnection mvd; + String nres; + new_con = false; + + if (sts == 305) + { + mvd = new HTTPConnection(req.getConnection().getProtocol(), + req.getConnection().getHost(), + req.getConnection().getPort()); + mvd.setCurrentProxy(loc.getHost(), loc.getPort()); + mvd.setContext(req.getConnection().getContext()); + new_con = true; + + nres = req.getRequestURI(); + + /* There was some discussion about this, and especially + * Foteos Macrides (Lynx) said a 305 should also imply + * a change to GET (for security reasons) - see the thread + * starting at + * http://www.ics.uci.edu/pub/ietf/http/hypermail/1997q4/0351.html + * However, this is not in the latest draft, but since I + * agree with Foteos we do it anyway... + */ + req.setMethod("GET"); + req.setData(null); + req.setStream(null); + } + else if (sts == 306) + { + // We'll have to wait for Josh to create a new spec here. + return RSP_CONTINUE; + } + else + { + if (req.getConnection().isCompatibleWith(loc)) + { + mvd = req.getConnection(); + nres = loc.getPathAndQuery(); + } + else + { + try + { + mvd = new HTTPConnection(loc); + nres = loc.getPathAndQuery(); + } + catch (Exception e) + { + if (req.getConnection().getProxyHost() == null || + !loc.getScheme().equalsIgnoreCase("ftp")) + return RSP_CONTINUE; + + // We're using a proxy and the protocol is ftp - + // maybe the proxy will also proxy ftp... + mvd = new HTTPConnection("http", + req.getConnection().getProxyHost(), + req.getConnection().getProxyPort()); + mvd.setCurrentProxy(null, 0); + nres = loc.toExternalForm(); + } + + mvd.setContext(req.getConnection().getContext()); + new_con = true; + } + + /* copy query if present in old url but not in new url. + * This isn't strictly conforming, but some scripts fail + * to propagate the query properly to the Location + * header. + * + * See comment on line 126. + String oquery = Util.getQuery(req.getRequestURI()), + nquery = Util.getQuery(nres); + if (nquery == null && oquery != null) + nres += "?" + oquery; + */ + + if (sts == 303) + { + // 303 means "use GET" + + if (!req.getMethod().equals("HEAD")) + req.setMethod("GET"); + req.setData(null); + req.setStream(null); + } + else + { + // If they used an output stream then they'll have + // to do the resend themselves + if (req.getStream() != null) + { + if (!HTTPConnection.deferStreamed) + { + Log.write(Log.MODS, "RdirM: status " + sts + + " not handled - request " + + "has an output stream"); + return RSP_CONTINUE; + } + + saved_req = (Request) req.clone(); + deferred_redir_list.put(req.getStream(), this); + req.getStream().reset(); + resp.setRetryRequest(true); + } + + if (sts == 301) + { + // update permanent redirection list + try + { + update_perm_redir_list(req, new URI(loc, nres)); + } + catch (ParseException pe) + { + throw new Error("HTTPClient Internal Error: " + + "unexpected exception '" + pe + + "'"); + } + } + } + + // Adjust Referer, if present + NVPair[] hdrs = req.getHeaders(); + for (int idx=0; idx 0) + { + req_uri = req_uri.trim(); + if (req_uri.charAt(0) != '/' && !req_uri.equals("*") && + !method.equals("CONNECT") && !isAbsolute(req_uri)) + req_uri = "/" + req_uri; + this.req_uri = req_uri; + } + else + this.req_uri = "/"; + } + + private static final boolean isAbsolute(String uri) + { + char ch = '\0'; + int pos = 0, len = uri.length(); + while (pos < len && (ch = uri.charAt(pos)) != ':' && + ch != '/' && ch != '?' && ch != '#') + pos++; + + return (ch == ':'); + } + + + /** + * @return the headers making up this request + */ + public NVPair[] getHeaders() + { + return headers; + } + + /** + * @param headers the headers for this request + */ + public void setHeaders(NVPair[] headers) + { + if (headers != null) + this.headers = headers; + else + this.headers = empty; + } + + + /** + * @return the body of this request + */ + public byte[] getData() + { + return data; + } + + /** + * @param data the entity for this request + */ + public void setData(byte[] data) + { + this.data = data; + } + + + /** + * @return the output stream on which the body is written + */ + public HttpOutputStream getStream() + { + return stream; + } + + /** + * @param stream an output stream on which the entity is written + */ + public void setStream(HttpOutputStream stream) + { + this.stream = stream; + } + + + /** + * @return true if the modules or handlers for this request may popup + * windows or otherwise interact with the user + */ + public boolean allowUI() + { + return allow_ui; + } + + /** + * @param allow_ui are modules and handlers allowed to popup windows or + * otherwise interact with the user? + */ + public void setAllowUI(boolean allow_ui) + { + this.allow_ui = allow_ui; + } + + + /** + * @return a clone of this request object + */ + public Object clone() + { + Request cl; + try + { cl = (Request) super.clone(); } + catch (CloneNotSupportedException cnse) + { throw new InternalError(cnse.toString()); /* shouldn't happen */ } + + cl.headers = new NVPair[headers.length]; + System.arraycopy(headers, 0, cl.headers, 0, headers.length); + + return cl; + } + + + /** + * Copy all the fields from other to this request. + * + * @param other the Request to copy from + */ + public void copyFrom(Request other) + { + this.connection = other.connection; + this.method = other.method; + this.req_uri = other.req_uri; + this.headers = other.headers; + this.data = other.data; + this.stream = other.stream; + this.allow_ui = other.allow_ui; + this.delay_entity = other.delay_entity; + this.num_retries = other.num_retries; + this.dont_pipeline = other.dont_pipeline; + this.aborted = other.aborted; + this.internal_subrequest = other.internal_subrequest; + } + + + /** + * @return a string containing the method and request-uri + */ + public String toString() + { + return getClass().getName() + ": " + method + " " + req_uri; + } +} diff --git a/HTTPClient/RespInputStream.java b/HTTPClient/RespInputStream.java new file mode 100644 index 0000000..953d5a7 --- /dev/null +++ b/HTTPClient/RespInputStream.java @@ -0,0 +1,345 @@ +/* + * @(#)RespInputStream.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.InputStream; +import java.io.IOException; +import java.io.InterruptedIOException; + +/** + * This is the InputStream that gets returned to the user. The extensions + * consist of the capability to have the data pushed into a buffer if the + * stream demux needs to. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.2 + */ +final class RespInputStream extends InputStream implements GlobalConstants +{ + /** Use old behaviour: don't set a timeout when reading the response body */ + private static boolean dontTimeoutBody = false; + + /** the stream demultiplexor */ + private StreamDemultiplexor demux = null; + + /** our response handler */ + private ResponseHandler resph; + + /** signals that the user has closed the stream and will therefore + not read any further data */ + boolean closed = false; + + /** signals that the connection may not be closed prematurely */ + private boolean dont_truncate = false; + + /** this buffer is used to buffer data that the demux has to get rid of */ + private byte[] buffer = null; + + /** signals that we were interrupted and that the buffer is not complete */ + private boolean interrupted = false; + + /** the offset at which the unread data starts in the buffer */ + private int offset = 0; + + /** the end of the data in the buffer */ + private int end = 0; + + /** the total number of bytes of entity data read from the demux so far */ + int count = 0; + + static + { + try + { + dontTimeoutBody = Boolean.getBoolean("HTTPClient.dontTimeoutRespBody"); + if (dontTimeoutBody) + Log.write(Log.DEMUX, "RspIS: disabling timeouts when " + + "reading response body"); + } + catch (Exception e) + { } + } + + + // Constructors + + RespInputStream(StreamDemultiplexor demux, ResponseHandler resph) + { + this.demux = demux; + this.resph = resph; + } + + + // public Methods + + private byte[] ch = new byte[1]; + /** + * Reads a single byte. + * + * @return the byte read, or -1 if EOF. + * @exception IOException if any exception occured on the connection. + */ + public synchronized int read() throws IOException + { + int rcvd = read(ch, 0, 1); + if (rcvd == 1) + return ch[0] & 0xff; + else + return -1; + } + + + /** + * Reads len bytes into b, starting at offset + * off. + * + * @return the number of bytes actually read, or -1 if EOF. + * @exception IOException if any exception occured on the connection. + */ + public synchronized int read(byte[] b, int off, int len) throws IOException + { + if (closed) + return -1; + + int left = end - offset; + if (buffer != null && !(left == 0 && interrupted)) + { + if (left == 0) return -1; + + len = (len > left ? left : len); + System.arraycopy(buffer, offset, b, off, len); + offset += len; + + return len; + } + else + { + if (resph.resp.cd_type != CD_HDRS) + Log.write(Log.DEMUX, "RspIS: Reading stream " + this.hashCode()); + + int rcvd; + if (dontTimeoutBody && resph.resp.cd_type != CD_HDRS) + rcvd = demux.read(b, off, len, resph, 0); + else + rcvd = demux.read(b, off, len, resph, resph.resp.timeout); + if (rcvd != -1 && resph.resp.got_headers) + count += rcvd; + + return rcvd; + } + } + + + /** + * skips num bytes. + * + * @return the number of bytes actually skipped. + * @exception IOException if any exception occured on the connection. + */ + public synchronized long skip(long num) throws IOException + { + if (closed) + return 0; + + int left = end - offset; + if (buffer != null && !(left == 0 && interrupted)) + { + num = (num > left ? left : num); + offset += num; + return num; + } + else + { + long skpd = demux.skip(num, resph); + if (resph.resp.got_headers) + count += skpd; + return skpd; + } + } + + + /** + * gets the number of bytes available for reading without blocking. + * + * @return the number of bytes available. + * @exception IOException if any exception occured on the connection. + */ + public synchronized int available() throws IOException + { + if (closed) + return 0; + + if (buffer != null && !(end-offset == 0 && interrupted)) + return end-offset; + else + return demux.available(resph); + } + + + /** + * closes the stream. + * + * @exception if any exception occured on the connection before or + * during close. + */ + public synchronized void close() throws IOException + { + if (!closed) + { + closed = true; + + if (dont_truncate && (buffer == null || interrupted)) + readAll(resph.resp.timeout); + + Log.write(Log.DEMUX, "RspIS: User closed stream " + hashCode()); + + demux.closeSocketIfAllStreamsClosed(); + + if (dont_truncate) + { + try + { resph.resp.http_resp.invokeTrailerHandlers(false); } + catch (ModuleException me) + { throw new IOException(me.toString()); } + } + } + } + + + /** + * A safety net to clean up. + */ + protected void finalize() throws Throwable + { + try + { close(); } + finally + { super.finalize(); } + } + + + // local Methods + + /** + * Reads all remainings data into buffer. This is used to force a read + * of upstream responses. + * + *

This is probably the most tricky and buggy method around. It's the + * only one that really violates the strict top-down method invocation + * from the Response through the ResponseStream to the StreamDemultiplexor. + * This means we need to be awfully careful about what is synchronized + * and what parameters are passed to whom. + * + * @param timeout the timeout to use for reading from the demux + * @exception IOException If any exception occurs while reading stream. + */ + void readAll(int timeout) throws IOException + { + Log.write(Log.DEMUX, "RspIS: Read-all on stream " + this.hashCode()); + + synchronized (resph.resp) + { + if (!resph.resp.got_headers) // force headers to be read + { + int sav_to = resph.resp.timeout; + resph.resp.timeout = timeout; + resph.resp.getStatusCode(); + resph.resp.timeout = sav_to; + } + } + + synchronized (this) + { + if (buffer != null && !interrupted) return; + + int rcvd = 0; + try + { + if (closed) // throw away + { + buffer = new byte[10000]; + do + { + count += rcvd; + rcvd = demux.read(buffer, 0, buffer.length, resph, + timeout); + } while (rcvd != -1); + buffer = null; + } + else + { + if (buffer == null) + { + buffer = new byte[10000]; + offset = 0; + end = 0; + } + + do + { + rcvd = demux.read(buffer, end, buffer.length-end, resph, + timeout); + if (rcvd < 0) break; + + count += rcvd; + end += rcvd; + buffer = Util.resizeArray(buffer, end+10000); + } while (true); + } + } + catch (InterruptedIOException iioe) + { + interrupted = true; + throw iioe; + } + catch (IOException ioe) + { + buffer = null; // force a read on demux for exception + } + + interrupted = false; + } + } + + + /** + * Sometime the full response body must be read, i.e. the connection may + * not be closed prematurely (by us). Currently this is needed when the + * chunked encoding with trailers is used in a response. + */ + synchronized void dontTruncate() + { + dont_truncate = true; + } +} diff --git a/HTTPClient/Response.java b/HTTPClient/Response.java new file mode 100644 index 0000000..908124f --- /dev/null +++ b/HTTPClient/Response.java @@ -0,0 +1,1420 @@ +/* + * @(#)Response.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + */ + +package HTTPClient; + +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.EOFException; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.ProtocolException; +import java.util.Date; +import java.util.Vector; +import java.util.Hashtable; +import java.util.StringTokenizer; +import java.util.NoSuchElementException; + + +/** + * This class represents an intermediate response. It's used internally by the + * modules. When all modules have handled the response then the HTTPResponse + * fills in its fields with the data from this class. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public final class Response implements RoResponse, GlobalConstants, Cloneable +{ + /** This contains a list of headers which may only have a single value */ + private static final Hashtable singleValueHeaders; + + /** our http connection */ + private HTTPConnection connection; + + /** our stream demux */ + private StreamDemultiplexor stream_handler; + + /** the HTTPResponse we're coupled with */ + HTTPResponse http_resp; + + /** the timeout for read operations */ + int timeout = 0; + + /** our input stream (usually from the stream demux). Push input streams + * onto this if necessary. */ + public InputStream inp_stream; + + /** our response input stream from the stream demux */ + private RespInputStream resp_inp_stream = null; + + /** the method used in the request */ + private String method; + + /** the resource in the request (for debugging purposes) */ + String resource; + + /** was a proxy used for the request? */ + private boolean used_proxy; + + /** did the request contain an entity? */ + private boolean sent_entity; + + /** the status code returned. */ + int StatusCode = 0; + + /** the reason line associated with the status code. */ + String ReasonLine; + + /** the HTTP version of the response. */ + String Version; + + /** the final URI of the document. */ + URI EffectiveURI = null; + + /** any headers which were received and do not fit in the above list. */ + CIHashtable Headers = new CIHashtable(); + + /** any trailers which were received and do not fit in the above list. */ + CIHashtable Trailers = new CIHashtable(); + + /** the message length of the response if either there is no data (in which + * case ContentLength=0) or if the message length is controlled by a + * Content-Length header. If neither of these, then it's -1 */ + int ContentLength = -1; + + /** this indicates how the length of the entity body is determined */ + int cd_type = CD_HDRS; + + /** the data (body) returned. */ + byte[] Data = null; + + /** signals if we in the process of reading the headers */ + boolean reading_headers = false; + + /** signals if we have got and parsed the headers yet */ + boolean got_headers = false; + + /** signals if we have got and parsed the trailers yet */ + boolean got_trailers = false; + + /** remembers any exception received while reading/parsing headers */ + private IOException exception = null; + + /** should this response be handled further? */ + boolean final_resp = false; + + /** should the request be retried by the application? */ + boolean retry = false; + + + static + { + /* This static initializer creates a hashtable of header names that + * should only have at most a single value in a server response. Other + * headers that may have multiple values (ie Set-Cookie) will have + * their values combined into one header, with individual values being + * separated by commas. + */ + String[] singleValueHeaderNames = { + "age", "location", "content-base", "content-length", + "content-location", "content-md5", "content-range", "content-type", + "date", "etag", "expires", "proxy-authenticate", "retry-after", + }; + + singleValueHeaders = new Hashtable(singleValueHeaderNames.length); + for (int idx=0; idxIf data is not null then that is used; else if the + * is is not null that is used; else the entity is empty. + * If the input stream is used then cont_len specifies + * the length of the data that can be read from it, or -1 if unknown. + * + * @param version the response version (such as "HTTP/1.1") + * @param status the status code + * @param reason the reason line + * @param headers the response headers + * @param data the response entity + * @param is the response entity as an InputStream + * @param cont_len the length of the data in the InputStream + */ + public Response(String version, int status, String reason, NVPair[] headers, + byte[] data, InputStream is, int cont_len) + { + this.Version = version; + this.StatusCode = status; + this.ReasonLine = reason; + if (headers != null) + for (int idx=0; idx + *

  • 1xx - Informational (new in HTTP/1.1) + *
  • 2xx - Success + *
  • 3xx - Redirection + *
  • 4xx - Client Error + *
  • 5xx - Server Error + * + * + * @exception IOException If any exception occurs on the socket. + */ + public final int getStatusCode() throws IOException + { + if (!got_headers) getHeaders(true); + return StatusCode; + } + + /** + * give the reason line associated with the status code. + * + * @exception IOException If any exception occurs on the socket. + */ + public final String getReasonLine() throws IOException + { + if (!got_headers) getHeaders(true); + return ReasonLine; + } + + /** + * get the HTTP version used for the response. + * + * @exception IOException If any exception occurs on the socket. + */ + public final String getVersion() throws IOException + { + if (!got_headers) getHeaders(true); + return Version; + } + + /** + * Wait for either a '100 Continue' or an error. + * + * @return the return status. + */ + int getContinue() throws IOException + { + getHeaders(false); + return StatusCode; + } + + /** + * get the final URI of the document. This is set if the original + * request was deferred via the "moved" (301, 302, or 303) return + * status. + * + * @return the new URI, or null if not redirected + * @exception IOException If any exception occurs on the socket. + */ + public final URI getEffectiveURI() throws IOException + { + if (!got_headers) getHeaders(true); + return EffectiveURI; + } + + /** + * set the final URI of the document. This is only for internal use. + */ + public void setEffectiveURI(URI final_uri) + { + EffectiveURI = final_uri; + } + + /** + * get the final URL of the document. This is set if the original + * request was deferred via the "moved" (301, 302, or 303) return + * status. + * + * @exception IOException If any exception occurs on the socket. + * @deprecated use getEffectiveURI() instead + * @see #getEffectiveURI + */ + public final URL getEffectiveURL() throws IOException + { + return getEffectiveURI().toURL(); + } + + /** + * set the final URL of the document. This is only for internal use. + * + * @deprecated use setEffectiveURI() instead + * @see #setEffectiveURI + */ + public void setEffectiveURL(URL final_url) + { + try + { setEffectiveURI(new URI(final_url)); } + catch (ParseException pe) + { throw new Error(pe.toString()); } // shouldn't happen + } + + /** + * retrieves the field for a given header. + * + * @param hdr the header name. + * @return the value for the header, or null if non-existent. + * @exception IOException If any exception occurs on the socket. + */ + public String getHeader(String hdr) throws IOException + { + if (!got_headers) getHeaders(true); + return (String) Headers.get(hdr.trim()); + } + + /** + * retrieves the field for a given header. The value is parsed as an + * int. + * + * @param hdr the header name. + * @return the value for the header if the header exists + * @exception NumberFormatException if the header's value is not a number + * or if the header does not exist. + * @exception IOException if any exception occurs on the socket. + */ + public int getHeaderAsInt(String hdr) + throws IOException, NumberFormatException + { + String val = getHeader(hdr); + if (val == null) + throw new NumberFormatException("null"); + return Integer.parseInt(val); + } + + /** + * retrieves the field for a given header. The value is parsed as a + * date; if this fails it is parsed as a long representing the number + * of seconds since 12:00 AM, Jan 1st, 1970. If this also fails an + * IllegalArgumentException is thrown. + * + *

    Note: When sending dates use Util.httpDate(). + * + * @param hdr the header name. + * @return the value for the header, or null if non-existent. + * @exception IOException If any exception occurs on the socket. + * @exception IllegalArgumentException If the header cannot be parsed + * as a date or time. + */ + public Date getHeaderAsDate(String hdr) + throws IOException, IllegalArgumentException + { + String raw_date = getHeader(hdr); + if (raw_date == null) return null; + + // asctime() format is missing an explicit GMT specifier + if (raw_date.toUpperCase().indexOf("GMT") == -1 && + raw_date.indexOf(' ') > 0) + raw_date += " GMT"; + + Date date; + + try + { date = Util.parseHttpDate(raw_date); } + catch (IllegalArgumentException iae) + { + long time; + try + { time = Long.parseLong(raw_date); } + catch (NumberFormatException nfe) + { throw iae; } + if (time < 0) time = 0; + date = new Date(time * 1000L); + } + + return date; + } + + + /** + * Set a header field in the list of headers. If the header already + * exists it will be overwritten; otherwise the header will be added + * to the list. This is used by some modules when they process the + * header so that higher level stuff doesn't get confused when the + * headers and data don't match. + * + * @param header The name of header field to set. + * @param value The value to set the field to. + */ + public void setHeader(String header, String value) + { + Headers.put(header.trim(), value.trim()); + } + + + /** + * Removes a header field from the list of headers. This is used by + * some modules when they process the header so that higher level stuff + * doesn't get confused when the headers and data don't match. + * + * @param header The name of header field to remove. + */ + public void deleteHeader(String header) + { + Headers.remove(header.trim()); + } + + + /** + * Retrieves the field for a given trailer. Note that this should not + * be invoked until all the response data has been read. If invoked + * before, it will force the data to be read via getData(). + * + * @param trailer the trailer name. + * @return the value for the trailer, or null if non-existent. + * @exception IOException If any exception occurs on the socket. + */ + public String getTrailer(String trailer) throws IOException + { + if (!got_trailers) getTrailers(); + return (String) Trailers.get(trailer.trim()); + } + + + /** + * Retrieves the field for a given tailer. The value is parsed as an + * int. + * + * @param trailer the tailer name. + * @return the value for the trailer if the trailer exists + * @exception NumberFormatException if the trailer's value is not a number + * or if the trailer does not exist. + * @exception IOException if any exception occurs on the socket. + */ + public int getTrailerAsInt(String trailer) + throws IOException, NumberFormatException + { + String val = getTrailer(trailer); + if (val == null) + throw new NumberFormatException("null"); + return Integer.parseInt(val); + } + + + /** + * Retrieves the field for a given trailer. The value is parsed as a + * date; if this fails it is parsed as a long representing the number + * of seconds since 12:00 AM, Jan 1st, 1970. If this also fails an + * IllegalArgumentException is thrown. + * + *

    Note: When sending dates use Util.httpDate(). + * + * @param trailer the trailer name. + * @return the value for the trailer, or null if non-existent. + * @exception IllegalArgumentException if the trailer's value is neither a + * legal date nor a number. + * @exception IOException if any exception occurs on the socket. + * @exception IllegalArgumentException If the header cannot be parsed + * as a date or time. + */ + public Date getTrailerAsDate(String trailer) + throws IOException, IllegalArgumentException + { + String raw_date = getTrailer(trailer); + if (raw_date == null) return null; + + // asctime() format is missing an explicit GMT specifier + if (raw_date.toUpperCase().indexOf("GMT") == -1 && + raw_date.indexOf(' ') > 0) + raw_date += " GMT"; + + Date date; + + try + { date = Util.parseHttpDate(raw_date); } + catch (IllegalArgumentException iae) + { + // some servers erroneously send a number, so let's try that + long time; + try + { time = Long.parseLong(raw_date); } + catch (NumberFormatException nfe) + { throw iae; } // give up + if (time < 0) time = 0; + date = new Date(time * 1000L); + } + + return date; + } + + + /** + * Set a trailer field in the list of trailers. If the trailer already + * exists it will be overwritten; otherwise the trailer will be added + * to the list. This is used by some modules when they process the + * trailer so that higher level stuff doesn't get confused when the + * trailer and data don't match. + * + * @param trailer The name of trailer field to set. + * @param value The value to set the field to. + */ + public void setTrailer(String trailer, String value) + { + Trailers.put(trailer.trim(), value.trim()); + } + + + /** + * Removes a trailer field from the list of trailers. This is used by + * some modules when they process the trailer so that higher level stuff + * doesn't get confused when the trailers and data don't match. + * + * @param trailer The name of trailer field to remove. + */ + public void deleteTrailer(String trailer) + { + Trailers.remove(trailer.trim()); + } + + + /** + * Reads all the response data into a byte array. Note that this method + * won't return until all the data has been received (so for + * instance don't invoke this method if the server is doing a server + * push). If getInputStream() had been previously called then this method + * only returns any unread data remaining on the stream and then closes + * it. + * + * @see #getInputStream() + * @return an array containing the data (body) returned. If no data + * was returned then it's set to a zero-length array. + * @exception IOException If any io exception occured while reading + * the data + */ + public synchronized byte[] getData() throws IOException + { + if (!got_headers) getHeaders(true); + + if (Data == null) + { + try + { readResponseData(inp_stream); } + catch (InterruptedIOException ie) // don't intercept + { throw ie; } + catch (IOException ioe) + { + Log.write(Log.RESP, "Resp: (" + inp_stream.hashCode() + ")", + ioe); + + try { inp_stream.close(); } catch (Exception e) { } + throw ioe; + } + + inp_stream.close(); + } + + return Data; + } + + /** + * Gets an input stream from which the returned data can be read. Note + * that if getData() had been previously called it will actually return + * a ByteArrayInputStream created from that data. + * + * @see #getData() + * @return the InputStream. + * @exception IOException If any exception occurs on the socket. + */ + public synchronized InputStream getInputStream() throws IOException + { + if (!got_headers) getHeaders(true); + + if (Data == null) + return inp_stream; + else + return new ByteArrayInputStream(Data); + } + + /** + * Some responses such as those from a HEAD or with certain status + * codes don't have an entity. This is detected by the client and + * can be queried here. Note that this won't try to do a read() on + * the input stream (it will however cause the headers to be read + * and parsed if not already done). + * + * @return true if the response has an entity, false otherwise + * @since V0.3-1 + */ + public synchronized boolean hasEntity() throws IOException + { + if (!got_headers) getHeaders(true); + + return (cd_type != CD_0); + } + + /** + * Should the request be retried by the application? This can be used + * by modules to signal to the application that it should retry the + * request. It's used when the request used an HttpOutputStream + * and the module is therefore not able to retry the request itself. + * This flag is false by default. + * + *

    If a module sets this flag then it must also reset() the + * the HttpOutputStream so it may be reused by the application. + * It should then also use this HttpOutputStream to recognize + * the retried request in the requestHandler(). + * + * @param flag indicates whether the application should retry the request. + */ + public void setRetryRequest(boolean flag) + { + retry = flag; + } + + /** + * @return true if the request should be retried. + */ + public boolean retryRequest() + { + return retry; + } + + + // Helper Methods + + /** + * Gets and parses the headers. Sets up Data if no data will be received. + * + * @param skip_cont if true skips over '100 Continue' status codes. + * @exception IOException If any exception occurs while reading the headers. + */ + private synchronized void getHeaders(boolean skip_cont) throws IOException + { + if (got_headers) return; + if (exception != null) + { + exception.fillInStackTrace(); + throw exception; + } + + reading_headers = true; + try + { + do + { + Headers.clear(); // clear any headers from 100 Continue + String headers = readResponseHeaders(inp_stream); + parseResponseHeaders(headers); + } while ((StatusCode == 100 && skip_cont) || // Continue + (StatusCode > 101 && StatusCode < 200)); // Unknown + } + catch (IOException ioe) + { + if (!(ioe instanceof InterruptedIOException)) + exception = ioe; + if (ioe instanceof ProtocolException) // thrown internally + { + cd_type = CD_CLOSE; + if (stream_handler != null) + stream_handler.markForClose(this); + } + throw ioe; + } + finally + { reading_headers = false; } + if (StatusCode == 100) return; + + + // parse the Content-Length header + + int cont_len = -1; + String cl_hdr = (String) Headers.get("Content-Length"); + if (cl_hdr != null) + { + try + { + cont_len = Integer.parseInt(cl_hdr); + if (cont_len < 0) + throw new NumberFormatException(); + } + catch (NumberFormatException nfe) + { + throw new ProtocolException("Invalid Content-length header"+ + " received: "+cl_hdr); + } + } + + + // parse the Transfer-Encoding header + + boolean te_chunked = false, te_is_identity = true, ct_mpbr = false; + Vector te_hdr = null; + try + { te_hdr = Util.parseHeader((String) Headers.get("Transfer-Encoding")); } + catch (ParseException pe) + { } + if (te_hdr != null) + { + te_chunked = ((HttpHeaderElement) te_hdr.lastElement()).getName(). + equalsIgnoreCase("chunked"); + for (int idx=0; idx 0) + setHeader("Transfer-Encoding", Util.assembleHeader(te_hdr)); + else + deleteHeader("Transfer-Encoding"); + } + else if (cont_len != -1 && te_is_identity) + cd_type = CD_CONTLEN; + else if (ct_mpbr && te_is_identity) + cd_type = CD_MP_BR; + else if (!method.equals("HEAD")) + { + cd_type = CD_CLOSE; + if (stream_handler != null) + stream_handler.markForClose(this); + + if (Version.equals("HTTP/0.9")) + { + inp_stream = + new SequenceInputStream(new ByteArrayInputStream(Data), + inp_stream); + Data = null; + } + } + + if (cd_type == CD_CONTLEN) + ContentLength = cont_len; + else + deleteHeader("Content-Length"); // Content-Length is not valid in this case + + /* We treat HEAD specially down here because the above code needs + * to know whether to remove the Content-length header or not. + */ + if (method.equals("HEAD")) + cd_type = CD_0; + + if (cd_type == CD_0) + { + ContentLength = 0; + Data = new byte[0]; + inp_stream.close(); // we will not receive any more data + } + + Log.write(Log.RESP, "Resp: Response entity delimiter: " + + (cd_type == CD_0 ? "No Entity" : + cd_type == CD_CLOSE ? "Close" : + cd_type == CD_CONTLEN ? "Content-Length" : + cd_type == CD_CHUNKED ? "Chunked" : + cd_type == CD_MP_BR ? "Multipart" : + "???" ) + " (" + inp_stream.hashCode() + ")"); + + + // remove erroneous connection tokens + + if (connection.ServerProtocolVersion >= HTTP_1_1) + deleteHeader("Proxy-Connection"); + else // HTTP/1.0 + { + if (connection.getProxyHost() != null) + deleteHeader("Connection"); + else + deleteHeader("Proxy-Connection"); + + Vector pco; + try + { pco = Util.parseHeader((String) Headers.get("Connection")); } + catch (ParseException pe) + { pco = null; } + + if (pco != null) + { + for (int idx=0; idx 0) + setHeader("Connection", Util.assembleHeader(pco)); + else + deleteHeader("Connection"); + } + + try + { pco = Util.parseHeader((String) Headers.get("Proxy-Connection")); } + catch (ParseException pe) + { pco = null; } + + if (pco != null) + { + for (int idx=0; idx 0) + setHeader("Proxy-Connection", Util.assembleHeader(pco)); + else + deleteHeader("Proxy-Connection"); + } + } + + + // this must be set before we invoke handleFirstRequest() + got_headers = true; + + // special handling if this is the first response received + if (isFirstResponse) + { + if (!connection.handleFirstRequest(req, this)) + { + // got a buggy server - need to redo the request + Response resp; + try + { resp = connection.sendRequest(req, timeout); } + catch (ModuleException me) + { throw new IOException(me.toString()); } + resp.getVersion(); + + this.StatusCode = resp.StatusCode; + this.ReasonLine = resp.ReasonLine; + this.Version = resp.Version; + this.EffectiveURI = resp.EffectiveURI; + this.ContentLength = resp.ContentLength; + this.Headers = resp.Headers; + this.inp_stream = resp.inp_stream; + this.Data = resp.Data; + + req = null; + } + } + } + + + /* these are external to readResponseHeaders() because we need to be + * able to restart after an InterruptedIOException + */ + private byte[] buf = new byte[7]; + private int buf_pos = 0; + private StringBuffer hdrs = new StringBuffer(400); + private boolean reading_lines = false; + private boolean bol = true; + private boolean got_cr = false; + + /** + * Reads the response headers received, folding continued lines. + * + *

    Some of the code is a bit convoluted because we have to be able + * restart after an InterruptedIOException. + * + * @inp the input stream from which to read the response + * @return a (newline separated) list of headers + * @exception IOException if any read on the input stream fails + */ + private String readResponseHeaders(InputStream inp) throws IOException + { + if (buf_pos == 0) + Log.write(Log.RESP, "Resp: Reading Response headers " + + inp_stream.hashCode()); + else + Log.write(Log.RESP, "Resp: Resuming reading Response headers " + + inp_stream.hashCode()); + + + // read 7 bytes to see type of response + if (!reading_lines) + { + try + { + // Skip any leading white space to accomodate buggy responses + if (buf_pos == 0) + { + int c; + do + { + if ((c = inp.read()) == -1) + throw new EOFException("Encountered premature EOF " + + "while reading Version"); + } while (Character.isWhitespace((char) c)) ; + buf[0] = (byte) c; + buf_pos = 1; + } + + // Now read first seven bytes (the version string) + while (buf_pos < buf.length) + { + int got = inp.read(buf, buf_pos, buf.length-buf_pos); + if (got == -1) + throw new EOFException("Encountered premature EOF " + + "while reading Version"); + buf_pos += got; + } + } + catch (EOFException eof) + { + Log.write(Log.RESP, "Resp: (" + inp_stream.hashCode() + ")", + eof); + + throw eof; + } + for (int idx=0; idx or . The lines are + * stored in the hdrs buffers. Continued lines are merged + * and stored as one line. + * + *

    This method is restartable after an InterruptedIOException. + * + * @param inp the input stream to read from + * @exception IOException if any IOException is thrown by the stream + */ + private void readLines(InputStream inp) throws IOException + { + /* This loop is a merge of readLine() from DataInputStream and + * the necessary header logic to merge continued lines and terminate + * after an empty line. The reason this is explicit is because of + * the need to handle InterruptedIOExceptions. + */ + loop: while (true) + { + int b = inp.read(); + switch (b) + { + case -1: + throw new EOFException("Encountered premature EOF while reading headers:\n" + hdrs); + case '\r': + got_cr = true; + break; + case '\n': + if (bol) break loop; // all headers read + hdrs.append('\n'); + bol = true; + got_cr = false; + break; + case ' ': + case '\t': + if (bol) // a continued line + { + // replace previous \n with SP + hdrs.setCharAt(hdrs.length()-1, ' '); + bol = false; + break; + } + default: + if (got_cr) + { + hdrs.append('\r'); + got_cr = false; + } + hdrs.append((char) (b & 0xFF)); + bol = false; + break; + } + } + } + + + /** + * Parses the headers received into a new Response structure. + * + * @param headers a (newline separated) list of headers + * @exception ProtocolException if any part of the headers do not + * conform + */ + private void parseResponseHeaders(String headers) throws ProtocolException + { + String sts_line = null; + StringTokenizer lines = new StringTokenizer(headers, "\r\n"), + elem; + + if (Log.isEnabled(Log.RESP)) + Log.write(Log.RESP, "Resp: Parsing Response headers from Request "+ + "\"" + method + " " + resource + "\": (" + + inp_stream.hashCode() + ")\n\n" + headers); + + + // Detect and handle HTTP/0.9 responses + + if (!headers.regionMatches(true, 0, "HTTP/", 0, 5) && + !headers.regionMatches(true, 0, "HTTP ", 0, 5)) // NCSA bug + { + Version = "HTTP/0.9"; + StatusCode = 200; + ReasonLine = "OK"; + + try + { Data = headers.getBytes("8859_1"); } + catch (UnsupportedEncodingException uee) + { throw new Error(uee.toString()); } + + return; + } + + + // get the status line + + try + { + sts_line = lines.nextToken(); + elem = new StringTokenizer(sts_line, " \t"); + + Version = elem.nextToken(); + StatusCode = Integer.valueOf(elem.nextToken()).intValue(); + + if (Version.equalsIgnoreCase("HTTP")) // NCSA bug + Version = "HTTP/1.0"; + } + catch (NoSuchElementException e) + { + throw new ProtocolException("Invalid HTTP status line received: " + + sts_line); + } + try + { ReasonLine = elem.nextToken("").trim(); } + catch (NoSuchElementException e) + { ReasonLine = ""; } + + + /* If the status code shows an error and we're sending (or have sent) + * an entity and it's length is delimited by a Content-length header, + * then we must close the the connection (if indeed it hasn't already + * been done) - RFC-2616, Section 8.2.2 . + */ + if (StatusCode >= 300 && sent_entity) + { + if (stream_handler != null) + stream_handler.markForClose(this); + } + + + // get the rest of the headers + + parseHeaderFields(lines, Headers); + + + /* make sure the connection isn't closed prematurely if we have + * trailer fields + */ + if (Headers.get("Trailer") != null && resp_inp_stream != null) + resp_inp_stream.dontTruncate(); + + // Mark the end of the connection if it's not to be kept alive + + int vers; + if (Version.equalsIgnoreCase("HTTP/0.9") || + Version.equalsIgnoreCase("HTTP/1.0")) + vers = 0; + else + vers = 1; + + try + { + String con = (String) Headers.get("Connection"), + pcon = (String) Headers.get("Proxy-Connection"); + + // parse connection header + if ((vers == 1 && con != null && Util.hasToken(con, "close")) + || + (vers == 0 && + !((!used_proxy && con != null && + Util.hasToken(con, "keep-alive")) || + (used_proxy && pcon != null && + Util.hasToken(pcon, "keep-alive"))) + ) + ) + if (stream_handler != null) + stream_handler.markForClose(this); + } + catch (ParseException pe) { } + } + + + /** + * If the trailers have not been read it calls getData() + * to first force all data and trailers to be read. Then the trailers + * parsed into the Trailers hashtable. + * + * @exception IOException if any exception occured during reading of the + * response + */ + private synchronized void getTrailers() throws IOException + { + if (got_trailers) return; + if (exception != null) + { + exception.fillInStackTrace(); + throw exception; + } + + Log.write(Log.RESP, "Resp: Reading Response trailers " + + inp_stream.hashCode()); + + try + { + if (!trailers_read) + { + if (resp_inp_stream != null) + resp_inp_stream.readAll(timeout); + } + + if (trailers_read) + { + Log.write(Log.RESP, "Resp: Parsing Response trailers from "+ + "Request \"" + method + " " + resource + + "\": (" + inp_stream.hashCode() + + ")\n\n" + hdrs); + + parseHeaderFields(new StringTokenizer(hdrs.toString(), "\r\n"), + Trailers); + } + } + finally + { + got_trailers = true; + } + } + + + /** + * Parses the given lines as header fields of the form ": " + * into the given list. + * + * @param lines the header or trailer lines, one header field per line + * @param list the Hashtable to store the parsed fields in + * @exception ProtocolException if any part of the headers do not + * conform + */ + private void parseHeaderFields(StringTokenizer lines, CIHashtable list) + throws ProtocolException + { + while (lines.hasMoreTokens()) + { + String hdr = lines.nextToken(); + int sep = hdr.indexOf(':'); + + /* Once again we have to deal with broken servers and try + * to wing it here. If no ':' is found, try using the first + * space: + */ + if (sep == -1) + sep = hdr.indexOf(' '); + if (sep == -1) + { + throw new ProtocolException("Invalid HTTP header received: " + + hdr); + } + + String hdr_name = hdr.substring(0, sep).trim(); + String hdr_value = hdr.substring(sep+1).trim(); + + // Can header have multiple values? + if (!singleValueHeaders.containsKey(hdr_name.toLowerCase())) + { + String old_value = (String) list.get(hdr_name); + if (old_value == null) + list.put(hdr_name, hdr_value); + else + list.put(hdr_name, old_value + ", " + hdr_value); + } + else + // No multiple values--just replace/put latest header value + list.put(hdr_name, hdr_value); + } + } + + + /** + * Reads the response data received. Does not return until either + * Content-Length bytes have been read or EOF is reached. + * + * @inp the input stream from which to read the data + * @exception IOException if any read on the input stream fails + */ + private void readResponseData(InputStream inp) throws IOException + { + if (ContentLength == 0) + return; + + if (Data == null) + Data = new byte[0]; + + + // read response data + + int off = Data.length; + + try + { + // check Content-length header in case CE-Module removed it + if (getHeader("Content-Length") != null) + { + int rcvd = 0; + Data = new byte[ContentLength]; + + do + { + off += rcvd; + rcvd = inp.read(Data, off, ContentLength-off); + } while (rcvd != -1 && off+rcvd < ContentLength); + + /* Don't do this! + * If we do, then getData() won't work after a getInputStream() + * because we'll never get all the expected data. Instead, let + * the underlying RespInputStream throw the EOF. + if (rcvd == -1) // premature EOF + { + throw new EOFException("Encountered premature EOF while " + + "reading headers: received " + off + + " bytes instead of the expected " + + ContentLength + " bytes"); + } + */ + } + else + { + int inc = 1000, + rcvd = 0; + + do + { + off += rcvd; + Data = Util.resizeArray(Data, off+inc); + } while ((rcvd = inp.read(Data, off, inc)) != -1); + + Data = Util.resizeArray(Data, off); + } + } + catch (IOException ioe) + { + Data = Util.resizeArray(Data, off); + throw ioe; + } + finally + { + try + { inp.close(); } + catch (IOException ioe) + { } + } + } + + + Request req = null; + boolean isFirstResponse = false; + /** + * This marks this response as belonging to the first request made + * over an HTTPConnection. The con and req + * parameters are needed in case we have to do a resend of the request - + * this is to handle buggy servers which barf upon receiving a request + * marked as HTTP/1.1 . + * + * @param con The HTTPConnection used + * @param req The Request sent + */ + void markAsFirstResponse(Request req) + { + this.req = req; + isFirstResponse = true; + } + + + /** + * @return a clone of this request object + */ + public Object clone() + { + Response cl; + try + { cl = (Response) super.clone(); } + catch (CloneNotSupportedException cnse) + { throw new InternalError(cnse.toString()); /* shouldn't happen */ } + + cl.Headers = (CIHashtable) Headers.clone(); + cl.Trailers = (CIHashtable) Trailers.clone(); + + return cl; + } +} diff --git a/HTTPClient/ResponseHandler.java b/HTTPClient/ResponseHandler.java new file mode 100644 index 0000000..6490328 --- /dev/null +++ b/HTTPClient/ResponseHandler.java @@ -0,0 +1,138 @@ +/* + * @(#)ResponseHandler.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; + +/** + * This holds various information about an active response. Used by the + * StreamDemultiplexor and RespInputStream. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.2 + */ +final class ResponseHandler +{ + /** the response stream */ + RespInputStream stream; + + /** the response class */ + Response resp; + + /** the response class */ + Request request; + + /** signals that the demux has closed the response stream, and that + therefore no more data can be read */ + boolean eof = false; + + /** this is non-null if the stream has an exception pending */ + IOException exception = null; + + + /** + * Creates a new handler. This also allocates the response input + * stream. + * + * @param resp the reponse + * @param request the request + * @param demux our stream demultiplexor. + */ + ResponseHandler(Response resp, Request request, StreamDemultiplexor demux) + { + this.resp = resp; + this.request = request; + this.stream = new RespInputStream(demux, this); + + Log.write(Log.DEMUX, "Demux: Opening stream " + this.stream.hashCode() + + " for demux (" + demux.hashCode() + ")"); + } + + + /** holds the string that marks the end of this stream; used for + multipart delimited responses. */ + private byte[] endbndry = null; + + /** holds the compilation of the above string */ + private int[] end_cmp = null; + + /** + * return the boundary string for this response. Set's up the + * InputStream buffer if neccessary. + * + * @param MasterStream the input stream from which the stream demux + * is reading. + * @return the boundary string. + */ + byte[] getEndBoundary(BufferedInputStream MasterStream) + throws IOException, ParseException + { + if (endbndry == null) + setupBoundary(MasterStream); + + return endbndry; + } + + /** + * return the compilation of the boundary string for this response. + * Set's up the InputStream buffer if neccessary. + * + * @param MasterStream the input stream from which the stream demux + * is reading. + * @return the compiled boundary string. + */ + int[] getEndCompiled(BufferedInputStream MasterStream) + throws IOException, ParseException + { + if (end_cmp == null) + setupBoundary(MasterStream); + + return end_cmp; + } + + /** + * Gets the boundary string, compiles it for searching, and initializes + * the buffered input stream. + */ + void setupBoundary(BufferedInputStream MasterStream) + throws IOException, ParseException + { + String endstr = "--" + Util.getParameter("boundary", + resp.getHeader("Content-Type")) + + "--\r\n"; + endbndry = endstr.getBytes("8859_1"); + end_cmp = Util.compile_search(endbndry); + MasterStream.markForSearch(); + } +} diff --git a/HTTPClient/RetryException.java b/HTTPClient/RetryException.java new file mode 100644 index 0000000..c8c0964 --- /dev/null +++ b/HTTPClient/RetryException.java @@ -0,0 +1,105 @@ +/* + * @(#)RetryException.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; + +/** + * Signals that an exception was thrown and caught, and the request was + * retried. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class RetryException extends IOException +{ + /** the request to retry */ + Request request = null; + + /** the response associated with the above request */ + Response response = null; + + /** the start of the liked list */ + RetryException first = null; + + /** the next exception in the list */ + RetryException next = null; + + /** the original exception which caused the connection to be closed. */ + IOException exception = null; + + /** was this exception generated because of an abnormal connection reset? */ + boolean conn_reset = true; + + /** restart processing? */ + boolean restart = false; + + + /** + * Constructs an RetryException with no detail message. + * A detail message is a String that describes this particular exception. + */ + public RetryException() + { + super(); + } + + + /** + * Constructs an RetryException class with the specified detail message. + * A detail message is a String that describes this particular exception. + * + * @param s the String containing a detail message + */ + public RetryException(String s) + { + super(s); + } + + + // Methods + + /** + * Inserts this exception into the list. + * + * @param re the retry exception after which to add this one + */ + void addToListAfter(RetryException re) + { + if (re == null) return; + + if (re.next != null) + this.next = re.next; + re.next = this; + } +} diff --git a/HTTPClient/RetryModule.java b/HTTPClient/RetryModule.java new file mode 100644 index 0000000..427c178 --- /dev/null +++ b/HTTPClient/RetryModule.java @@ -0,0 +1,285 @@ +/* + * @(#)RetryModule.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; + + +/** + * This module handles request retries when a connection closes prematurely. + * It is triggered by the RetryException thrown by the StreamDemultiplexor. + * + *

    This module is somewhat unique in that it doesn't strictly limit itself + * to the HTTPClientModule interface and its return values. That is, it + * sends request directly using the HTTPConnection.sendRequest() method. This + * is necessary because this module will not only resend its request but it + * also resend all other requests in the chain. Also, it rethrows the + * RetryException in Phase1 to restart the processing of the modules. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3 + */ +class RetryModule implements HTTPClientModule, GlobalConstants +{ + // Constructors + + /** + */ + RetryModule() + { + } + + + // Methods + + /** + * Invoked by the HTTPClient. + */ + public int requestHandler(Request req, Response[] resp) + { + return REQ_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase1Handler(Response resp, RoRequest roreq) + throws IOException, ModuleException + { + try + { + resp.getStatusCode(); + } + catch (RetryException re) + { + Log.write(Log.MODS, "RtryM: Caught RetryException"); + + boolean got_lock = false; + + try + { + synchronized (re.first) + { + got_lock = true; + + // initialize idempotent sequence checking + IdempotentSequence seq = new IdempotentSequence(); + for (RetryException e=re.first; e!=null; e=e.next) + seq.add(e.request); + + for (RetryException e=re.first; e!=null; e=e.next) + { + Log.write(Log.MODS, "RtryM: handling exception ", e); + + Request req = e.request; + HTTPConnection con = req.getConnection(); + + /* Don't retry if either the sequence is not idempotent + * (Sec 8.1.4 and 9.1.2), or we've already retried enough + * times, or the headers have been read and parsed + * already + */ + if (!seq.isIdempotent(req) || + (con.ServProtVersKnown && + con.ServerProtocolVersion >= HTTP_1_1 && + req.num_retries > 0) || + ((!con.ServProtVersKnown || + con.ServerProtocolVersion <= HTTP_1_0) && + req.num_retries > 4) || + e.response.got_headers) + { + e.first = null; + continue; + } + + + /** + * if an output stream was used (i.e. we don't have the + * data to resend) then delegate the responsibility for + * resending to the application. + */ + if (req.getStream() != null) + { + if (HTTPConnection.deferStreamed) + { + req.getStream().reset(); + e.response.setRetryRequest(true); + } + e.first = null; + continue; + } + + + /* If we have an entity then setup either the entity-delay + * or the Expect header + */ + if (req.getData() != null && e.conn_reset) + { + if (con.ServProtVersKnown && + con.ServerProtocolVersion >= HTTP_1_1) + addToken(req, "Expect", "100-continue"); + else + req.delay_entity = 5000L << req.num_retries; + } + + /* If the next request in line has an entity and we're + * talking to an HTTP/1.0 server then close the socket + * after this request. This is so that the available() + * call (to watch for an error response from the server) + * will work correctly. + */ + if (e.next != null && e.next.request.getData() != null && + (!con.ServProtVersKnown || + con.ServerProtocolVersion < HTTP_1_1) && + e.conn_reset) + { + addToken(req, "Connection", "close"); + } + + + /* If this an HTTP/1.1 server then don't pipeline retries. + * The problem is that if the server for some reason + * decides not to use persistent connections and it does + * not do a correct shutdown of the connection, then the + * response will be ReSeT. If we did pipeline then we + * would keep falling into this trap indefinitely. + * + * Note that for HTTP/1.0 servers, if they don't support + * keep-alives then the normal code will already handle + * this accordingly and won't pipe over the same + * connection. + */ + if (con.ServProtVersKnown && + con.ServerProtocolVersion >= HTTP_1_1 && + e.conn_reset) + { + req.dont_pipeline = true; + } + // The above is too risky - for moment let's be safe + // and never pipeline retried request at all. + req.dont_pipeline = true; + + + // now resend the request + + Log.write(Log.MODS, "RtryM: Retrying request '" + + req.getMethod() + " " + + req.getRequestURI() + "'"); + + if (e.conn_reset) + req.num_retries++; + e.response.http_resp.set(req, + con.sendRequest(req, e.response.timeout)); + e.exception = null; + e.first = null; + } + } + } + catch (NullPointerException npe) + { if (got_lock) throw npe; } + catch (ParseException pe) + { throw new IOException(pe.getMessage()); } + + if (re.exception != null) throw re.exception; + + re.restart = true; + throw re; + } + } + + + /** + * Invoked by the HTTPClient. + */ + public int responsePhase2Handler(Response resp, Request req) + { + // reset any stuff we might have set previously + req.delay_entity = 0; + req.dont_pipeline = false; + req.num_retries = 0; + + return RSP_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase3Handler(Response resp, RoRequest req) + { + } + + + /** + * Invoked by the HTTPClient. + */ + public void trailerHandler(Response resp, RoRequest req) + { + } + + + /** + * Add a token to the given header. If the header does not exist then + * create it with the given token. + * + * @param req the request who's headers are to be modified + * @param hdr the name of the header to add the token to (or to create) + * @param tok the token to add + * @exception ParseException if parsing the header fails + */ + private void addToken(Request req, String hdr, String tok) + throws ParseException + { + int idx; + NVPair[] hdrs = req.getHeaders(); + for (idx=0; idx + *

  • 1xx - Informational (new in HTTP/1.1) + *
  • 2xx - Success + *
  • 3xx - Redirection + *
  • 4xx - Client Error + *
  • 5xx - Server Error + * + * + * @return the status code + * @exception IOException If any exception occurs on the socket. + */ + public int getStatusCode() throws IOException; + + /** + * @return the reason line associated with the status code. + * @exception IOException If any exception occurs on the socket. + */ + public String getReasonLine() throws IOException; + + /** + * @return the HTTP version returned by the server. + * @exception IOException If any exception occurs on the socket. + */ + public String getVersion() throws IOException; + + /** + * retrieves the field for a given header. + * + * @param hdr the header name. + * @return the value for the header, or null if non-existent. + * @exception IOException If any exception occurs on the socket. + */ + public String getHeader(String hdr) throws IOException; + + /** + * retrieves the field for a given header. The value is parsed as an + * int. + * + * @param hdr the header name. + * @return the value for the header if the header exists + * @exception NumberFormatException if the header's value is not a number + * or if the header does not exist. + * @exception IOException if any exception occurs on the socket. + */ + public int getHeaderAsInt(String hdr) + throws IOException, NumberFormatException; + + /** + * retrieves the field for a given header. The value is parsed as a + * date; if this fails it is parsed as a long representing the number + * of seconds since 12:00 AM, Jan 1st, 1970. If this also fails an + * IllegalArgumentException is thrown. + * + * @param hdr the header name. + * @return the value for the header, or null if non-existent. + * @exception IOException If any exception occurs on the socket. + * @exception IllegalArgumentException If the header cannot be parsed + * as a date or time. + */ + public Date getHeaderAsDate(String hdr) + throws IOException, IllegalArgumentException; + + /** + * Retrieves the field for a given trailer. Note that this should not + * be invoked until all the response data has been read. If invoked + * before, it will force the data to be read via getData(). + * + * @param trailer the trailer name. + * @return the value for the trailer, or null if non-existent. + * @exception IOException If any exception occurs on the socket. + */ + public String getTrailer(String trailer) throws IOException; + + /** + * Retrieves the field for a given tailer. The value is parsed as an + * int. + * + * @param trailer the tailer name. + * @return the value for the trailer if the trailer exists + * @exception NumberFormatException if the trailer's value is not a number + * or if the trailer does not exist. + * @exception IOException if any exception occurs on the socket. + */ + public int getTrailerAsInt(String trailer) + throws IOException, NumberFormatException; + + + /** + * Retrieves the field for a given trailer. The value is parsed as a + * date; if this fails it is parsed as a long representing the number + * of seconds since 12:00 AM, Jan 1st, 1970. If this also fails an + * IllegalArgumentException is thrown. + *
    Note: When sending dates use Util.httpDate(). + * + * @param trailer the trailer name. + * @return the value for the trailer, or null if non-existent. + * @exception IllegalArgumentException if the trailer's value is neither a + * legal date nor a number. + * @exception IOException if any exception occurs on the socket. + * @exception IllegalArgumentException If the header cannot be parsed + * as a date or time. + */ + public Date getTrailerAsDate(String trailer) + throws IOException, IllegalArgumentException; + + /** + * Reads all the response data into a byte array. Note that this method + * won't return until all the data has been received (so for + * instance don't invoke this method if the server is doing a server + * push). If getInputStream() had been previously called then this method + * only returns any unread data remaining on the stream and then closes + * it. + * + * @see #getInputStream() + * @return an array containing the data (body) returned. If no data + * was returned then it's set to a zero-length array. + * @exception IOException If any io exception occured while reading + * the data + */ + public byte[] getData() throws IOException; + + /** + * Gets an input stream from which the returned data can be read. Note + * that if getData() had been previously called it will actually return + * a ByteArrayInputStream created from that data. + * + * @see #getData() + * @return the InputStream. + * @exception IOException If any exception occurs on the socket. + */ + public InputStream getInputStream() throws IOException; +} diff --git a/HTTPClient/SocksClient.java b/HTTPClient/SocksClient.java new file mode 100644 index 0000000..60d09bc --- /dev/null +++ b/HTTPClient/SocksClient.java @@ -0,0 +1,622 @@ +/* + * @(#)SocksClient.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.net.Socket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; + +/** + * This class implements a SOCKS Client. Supports both versions 4 and 5. + * GSSAPI however is not yet implemented. + *

    Usage is as follows: somewhere in the initialization code (and before + * the first socket creation call) create a SocksClient instance. Then replace + * each socket creation call + * + * sock = new Socket(host, port); + * + * with + * + * sock = socks_client.getSocket(host, port); + * + * (where socks_client is the above created SocksClient instance). + * That's all. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class SocksClient +{ + /** the host the socks server sits on */ + private String socks_host; + + /** the port the socks server listens on */ + private int socks_port; + + /** the version of socks that the server handles */ + private int socks_version; + + /** socks commands */ + private final static byte CONNECT = 1, + BIND = 2, + UDP_ASS = 3; + + /** socks version 5 authentication methods */ + private final static byte NO_AUTH = 0, + GSSAPI = 1, + USERPWD = 2, + NO_ACC = (byte) 0xFF; + + /** socks version 5 address types */ + private final static byte IP_V4 = 1, + DMNAME = 3, + IP_V6 = 4; + + + // Constructors + + /** + * Creates a new SOCKS Client using the specified host and port for + * the server. Will try to establish the SOCKS version used when + * establishing the first connection. + * + * @param host the host the SOCKS server is sitting on. + * @param port the port the SOCKS server is listening on. + */ + SocksClient(String host, int port) + { + this.socks_host = host; + this.socks_port = port; + this.socks_version = -1; // as yet unknown + } + + /** + * Creates a new SOCKS Client using the specified host and port for + * the server. + * + * @param host the host the SOCKS server is sitting on. + * @param port the port the SOCKS server is listening on. + * @param version the version the SOCKS server is using. + * @exception SocksException if the version is invalid (Currently allowed + * are: 4 and 5). + */ + SocksClient(String host, int port, int version) throws SocksException + { + this.socks_host = host; + this.socks_port = port; + + if (version != 4 && version != 5) + throw new SocksException("SOCKS Version not supported: "+version); + this.socks_version = version; + } + + + // Methods + + /** + * Initiates a connection to the socks server, does the startup + * protocol and returns a socket ready for talking. + * + * @param host the host you wish to connect to + * @param port the port you wish to connect to + * @return a Socket with a connection via socks to the desired host/port + * @exception IOException if any socket operation fails + */ + Socket getSocket(String host, int port) throws IOException + { + return getSocket(host, port, null, -1); + } + + /** + * Initiates a connection to the socks server, does the startup + * protocol and returns a socket ready for talking. + * + * @param host the host you wish to connect to + * @param port the port you wish to connect to + * @param localAddr the local address to bind to + * @param localPort the local port to bind to + * @return a Socket with a connection via socks to the desired host/port + * @exception IOException if any socket operation fails + */ + Socket getSocket(String host, int port, InetAddress localAddr, + int localPort) throws IOException + { + Socket sock = null; + + try + { + Log.write(Log.SOCKS, "Socks: contacting server on " + + socks_host + ":" + socks_port); + + + // create socket and streams + + sock = connect(socks_host, socks_port, localAddr, localPort); + InputStream inp = sock.getInputStream(); + OutputStream out = sock.getOutputStream(); + + + // setup connection depending on socks version + + switch (socks_version) + { + case 4: + v4ProtExchg(inp, out, host, port); + break; + case 5: + v5ProtExchg(inp, out, host, port); + break; + case -1: + // Ok, let's try and figure it out + try + { + v4ProtExchg(inp, out, host, port); + socks_version = 4; + } + catch (SocksException se) + { + Log.write(Log.SOCKS, "Socks: V4 request failed: " + + se.getMessage()); + + sock.close(); + sock = connect(socks_host, socks_port, localAddr, + localPort); + inp = sock.getInputStream(); + out = sock.getOutputStream(); + + v5ProtExchg(inp, out, host, port); + socks_version = 5; + } + break; + default: + throw new Error("SocksClient internal error: unknown " + + "version "+socks_version); + } + + Log.write(Log.SOCKS, "Socks: connection established."); + + return sock; + } + catch (IOException ioe) + { + if (sock != null) + { + try { sock.close(); } + catch (IOException ee) {} + } + + throw ioe; + } + } + + + /** + * Connect to the host/port, trying all addresses assciated with that + * host. + * + * @param host the host you wish to connect to + * @param port the port you wish to connect to + * @param localAddr the local address to bind to + * @param localPort the local port to bind to + * @return the Socket + * @exception IOException if the connection could not be established + */ + private static final Socket connect(String host, int port, + InetAddress localAddr, int localPort) + throws IOException + { + InetAddress[] addr_list = InetAddress.getAllByName(host); + for (int idx=0; idx> 8) & 0xff); // port + buffer.write(port & 0xff); + buffer.write(addr); // address + buffer.write(user); // user + if (v4A) + { + buffer.write(host.getBytes("8859_1")); // host name + buffer.write(0); // terminating 0 + } + buffer.writeTo(out); + + + // read response + + int version = inp.read(); + if (version == -1) + throw new SocksException("Connection refused by server"); + else if (version == 4) // not all socks4 servers are correct... + Log.write(Log.SOCKS, "Socks: Warning: received version 4 " + + "instead of 0"); + else if (version != 0) + throw new SocksException("Received invalid version: " + version + + "; expected: 0"); + + int sts = inp.read(); + + Log.write(Log.SOCKS, "Socks: Received response; version: " + version + + "; status: " + sts); + + switch (sts) + { + case 90: // request granted + break; + case 91: // request rejected + throw new SocksException("Connection request rejected"); + case 92: // request rejected: can't connect to identd + throw new SocksException("Connection request rejected: " + + "can't connect to identd"); + case 93: // request rejected: identd reports diff uid + throw new SocksException("Connection request rejected: " + + "identd reports different user-id " + + "from "+ + new String(user, 0, user.length-1)); + default: // unknown status + throw new SocksException("Connection request rejected: " + + "unknown error " + sts); + } + + byte[] skip = new byte[2+4]; // skip port + address + int rcvd = 0, + tot = 0; + while (tot < skip.length && + (rcvd = inp.read(skip, 0, skip.length-tot)) != -1) + tot += rcvd; + } + + + /** + * Does the protocol exchange for a version 5 SOCKS connection. + * (rfc-1928) + */ + private void v5ProtExchg(InputStream inp, OutputStream out, String host, + int port) + throws SocksException, IOException + { + int version; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(100); + + Log.write(Log.SOCKS, "Socks: Beginning V5 Protocol Exchange for host " + + host + ":" + port); + + // send version 5 verification methods + + Log.write(Log.SOCKS, "Socks: Sending authentication request; methods" + + " No-Authentication, Username/Password"); + + buffer.reset(); + buffer.write(5); // version + buffer.write(2); // number of verification methods + buffer.write(NO_AUTH); // method: no authentication + buffer.write(USERPWD); // method: username/password + //buffer.write(GSSAPI); // method: gssapi + buffer.writeTo(out); + + + // receive servers repsonse + + version = inp.read(); + if (version == -1) + throw new SocksException("Connection refused by server"); + else if (version != 5) + throw new SocksException("Received invalid version: " + version + + "; expected: 5"); + + int method = inp.read(); + + Log.write(Log.SOCKS, "Socks: Received response; version: " + version + + "; method: " + method); + + + // enter sub-negotiation for authentication + + switch(method) + { + case NO_AUTH: + break; + case GSSAPI: + negotiate_gssapi(inp, out); + break; + case USERPWD: + negotiate_userpwd(inp, out); + break; + case NO_ACC: + throw new SocksException("Server unwilling to accept any " + + "standard authentication methods"); + default: + throw new SocksException("Cannot handle authentication method " + + method); + } + + + // send version 5 request + + Log.write(Log.SOCKS, "Socks: Sending connect request"); + + buffer.reset(); + buffer.write(5); // version + buffer.write(CONNECT); // command + buffer.write(0); // reserved - must be 0 + buffer.write(DMNAME); // address type + buffer.write(host.length() & 0xff); // address length + buffer.write(host.getBytes("8859_1")); // address + buffer.write((port >> 8) & 0xff); // port + buffer.write(port & 0xff); + buffer.writeTo(out); + + + // read response + + version = inp.read(); + if (version != 5) + throw new SocksException("Received invalid version: " + version + + "; expected: 5"); + + int sts = inp.read(); + + Log.write(Log.SOCKS, "Socks: Received response; version: " + version + + "; status: " + sts); + + switch (sts) + { + case 0: // succeeded + break; + case 1: + throw new SocksException("General SOCKS server failure"); + case 2: + throw new SocksException("Connection not allowed"); + case 3: + throw new SocksException("Network unreachable"); + case 4: + throw new SocksException("Host unreachable"); + case 5: + throw new SocksException("Connection refused"); + case 6: + throw new SocksException("TTL expired"); + case 7: + throw new SocksException("Command not supported"); + case 8: + throw new SocksException("Address type not supported"); + default: + throw new SocksException("Unknown reply received from server: " + + sts); + } + + inp.read(); // Reserved + int atype = inp.read(), // address type + alen; // address length + switch(atype) + { + case IP_V6: + alen = 16; + break; + case IP_V4: + alen = 4; + break; + case DMNAME: + alen = inp.read(); + break; + default: + throw new SocksException("Invalid address type received from" + + " server: "+atype); + } + + byte[] skip = new byte[alen+2]; // skip address + port + int rcvd = 0, + tot = 0; + while (tot < skip.length && + (rcvd = inp.read(skip, 0, skip.length-tot)) != -1) + tot += rcvd; + } + + + /** + * Negotiates authentication using the gssapi protocol + * (draft-ietf-aft-gssapi-02). + * + * NOTE: this is not implemented currently. Will have to wait till + * Java provides the necessary access to the system routines. + */ + private void negotiate_gssapi(InputStream inp, OutputStream out) + throws SocksException, IOException + { + throw new + SocksException("GSSAPI authentication protocol not implemented"); + } + + + /** + * Negotiates authentication using the username/password protocol + * (rfc-1929). The username and password should previously have been + * stored using the scheme "SOCKS5" and realm "USER/PASS"; e.g. + * AuthorizationInfo.addAuthorization(socks_host, socks_port, "SOCKS5", + * "USER/PASS", null, + * { new NVPair(username, password) }); + * + */ + private void negotiate_userpwd(InputStream inp, OutputStream out) + throws SocksException, IOException + { + byte[] buffer; + + + Log.write(Log.SOCKS, "Socks: Entering authorization subnegotiation" + + "; method: Username/Password"); + + // get username/password + + AuthorizationInfo auth_info; + try + { + auth_info = + AuthorizationInfo.getAuthorization(socks_host, socks_port, + "SOCKS5", "USER/PASS", + null, null, true); + } + catch (AuthSchemeNotImplException atnie) + { auth_info = null; } + + if (auth_info == null) + throw new SocksException("No Authorization info for SOCKS found " + + "(server requested username/password)."); + + NVPair[] unpw = auth_info.getParams(); + if (unpw == null || unpw.length == 0) + throw new SocksException("No Username/Password found in " + + "authorization info for SOCKS."); + + String user_str = unpw[0].getName(); + String pass_str = unpw[0].getValue(); + + + // send them to server + + Log.write(Log.SOCKS, "Socks: Sending authorization request for user "+ + user_str); + + byte[] utmp = user_str.getBytes(); + byte[] ptmp = pass_str.getBytes(); + buffer = new byte[1+1+utmp.length+1+ptmp.length]; + buffer[0] = 1; // version 1 (subnegotiation) + buffer[1] = (byte) utmp.length; // Username length + System.arraycopy(utmp, 0, buffer, 2, utmp.length); // Username + buffer[2+buffer[1]] = (byte) ptmp.length; // Password length + System.arraycopy(ptmp, 0, buffer, 2+buffer[1]+1, ptmp.length); // Password + out.write(buffer); + + + // get reply + + int version = inp.read(); + if (version != 1) + throw new SocksException("Wrong version received in username/" + + "password subnegotiation response: " + + version + "; expected: 1"); + + int sts = inp.read(); + if (sts != 0) + throw new SocksException("Username/Password authentication " + + "failed; status: "+sts); + + Log.write(Log.SOCKS, "Socks: Received response; version: " + version + + "; status: " + sts); + } + + + /** + * produces a string. + * @return a string containing the host and port of the socks server + */ + public String toString() + { + return getClass().getName() + "[" + socks_host + ":" + socks_port + "]"; + } +} diff --git a/HTTPClient/SocksException.java b/HTTPClient/SocksException.java new file mode 100644 index 0000000..56124a1 --- /dev/null +++ b/HTTPClient/SocksException.java @@ -0,0 +1,67 @@ +/* + * @(#)SocksException.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; + +/** + * Signals that an error was received while trying to set up a connection + * with the Socks server. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public class SocksException extends IOException +{ + /** + * Constructs a SocksException with no detail message. + * A detail message is a String that describes this particular exception. + */ + public SocksException() + { + super(); + } + + + /** + * Constructs a SocksException with the specified detail message. + * A detail message is a String that describes this particular exception. + * + * @param s the String containing a detail message + */ + public SocksException(String s) + { + super(s); + } + +} diff --git a/HTTPClient/StreamDemultiplexor.java b/HTTPClient/StreamDemultiplexor.java new file mode 100644 index 0000000..ac9a3e3 --- /dev/null +++ b/HTTPClient/StreamDemultiplexor.java @@ -0,0 +1,968 @@ +/* + * @(#)StreamDemultiplexor.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.io.EOFException; +import java.io.InterruptedIOException; +import java.net.Socket; +import java.net.SocketException; + +/** + * This class handles the demultiplexing of input stream. This is needed + * for things like keep-alive in HTTP/1.0, persist in HTTP/1.1 and in HTTP-NG. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class StreamDemultiplexor implements GlobalConstants +{ + /** the protocol were handling request for */ + private int Protocol; + + /** the connection we're working for */ + private HTTPConnection Connection; + + /** the input stream to demultiplex */ + private BufferedInputStream Stream; + + /** the socket this hangs off */ + private Socket Sock = null; + + /** signals after the closing of which stream to close the socket */ + private ResponseHandler MarkedForClose; + + /** timer used to close the socket if unused for a given time */ + private SocketTimeout.TimeoutEntry Timer = null; + + /** timer thread which implements the timers */ + private static SocketTimeout TimerThread = null; + + /** cleanup object to stop timer thread when we're gc'd */ + private static Object cleanup; + + /** a Vector to hold the list of response handlers were serving */ + private LinkedList RespHandlerList; + + /** number of unread bytes in current chunk (if transf-enc == chunked) */ + private long chunk_len; + + /** the currently set timeout for the socket */ + private int cur_timeout = 0; + + + static + { + TimerThread = new SocketTimeout(60); + TimerThread.start(); + + /* This is here to clean up the timer thread should the + * StreamDemultiplexor class be gc'd. This will not usually happen, + * unless the stuff is being run in an Applet or similar environment + * where multiple classloaders are used to load the same class + * multiple times. However, even in those environments it's not clear + * that this here will do us any good, because classes aren't usually + * gc'd unless their classloader is, but the timer thread keeps a + * reference to the classloader, and hence ought to prevent the + * classloader from being gc'd. + */ + cleanup = new Object() { + private final SocketTimeout timer = StreamDemultiplexor.TimerThread; + + protected void finalize() + { + timer.kill(); + } + }; + } + + + // Constructors + + /** + * a simple contructor. + * + * @param protocol the protocol used on this stream. + * @param sock the socket which we're to demux. + * @param connection the http-connection this socket belongs to. + */ + StreamDemultiplexor(int protocol, Socket sock, HTTPConnection connection) + throws IOException + { + this.Protocol = protocol; + this.Connection = connection; + RespHandlerList = new LinkedList(); + init(sock); + } + + + /** + * Initializes the demultiplexor with a new socket. + * + * @param stream the stream to demultiplex + */ + private void init(Socket sock) throws IOException + { + Log.write(Log.DEMUX, "Demux: Initializing Stream Demultiplexor (" + + this.hashCode() + ")"); + + this.Sock = sock; + this.Stream = new BufferedInputStream(sock.getInputStream()); + MarkedForClose = null; + chunk_len = -1; + + // create a timer to close the socket after 60 seconds, but don't + // start it yet + Timer = TimerThread.setTimeout(this); + Timer.hyber(); + } + + + // Methods + + /** + * Each Response must register with us. + */ + void register(Response resp_handler, Request req) throws RetryException + { + synchronized (RespHandlerList) + { + if (Sock == null) + throw new RetryException(); + + RespHandlerList.addToEnd( + new ResponseHandler(resp_handler, req, this)); + } + } + + /** + * creates an input stream for the response. + * + * @param resp the response structure requesting the stream + * @return an InputStream + */ + RespInputStream getStream(Response resp) + { + ResponseHandler resph; + synchronized (RespHandlerList) + { + for (resph = (ResponseHandler) RespHandlerList.enumerate(); + resph != null; + resph = (ResponseHandler) RespHandlerList.next()) + { + if (resph.resp == resp) break; + } + } + + if (resph != null) + return resph.stream; + else + return null; + } + + + /** + * Restarts the timer thread that will close an unused socket after + * 60 seconds. + */ + void restartTimer() + { + if (Timer != null) Timer.reset(); + } + + + /** + * reads an array of bytes from the master stream. + */ + int read(byte[] b, int off, int len, ResponseHandler resph, int timeout) + throws IOException + { + if (resph.exception != null) + { + resph.exception.fillInStackTrace(); + throw resph.exception; + } + + if (resph.eof) + return -1; + + + // read the headers and data for all responses preceding us. + + ResponseHandler head; + while ((head = (ResponseHandler) RespHandlerList.getFirst()) != null && + head != resph) + { + try + { head.stream.readAll(timeout); } + catch (IOException ioe) + { + if (ioe instanceof InterruptedIOException) + throw ioe; + else + { + resph.exception.fillInStackTrace(); + throw resph.exception; + } + } + } + + + // Now we can read from the stream. + + synchronized (this) + { + if (resph.exception != null) + { + resph.exception.fillInStackTrace(); + throw resph.exception; + } + + if (resph.resp.cd_type != CD_HDRS) + Log.write(Log.DEMUX, "Demux: Reading for stream " + + resph.stream.hashCode()); + + if (Timer != null) Timer.hyber(); + + try + { + int rcvd = -1; + + if (timeout != cur_timeout) + { + Log.write(Log.DEMUX, "Demux: Setting timeout to " + + timeout + " ms"); + + Sock.setSoTimeout(timeout); + cur_timeout = timeout; + } + + switch (resph.resp.cd_type) + { + case CD_HDRS: + rcvd = Stream.read(b, off, len); + if (rcvd == -1) + throw new EOFException("Premature EOF encountered"); + break; + + case CD_0: + rcvd = -1; + close(resph); + break; + + case CD_CLOSE: + rcvd = Stream.read(b, off, len); + if (rcvd == -1) + close(resph); + break; + + case CD_CONTLEN: + int cl = resph.resp.ContentLength; + if (len > cl - resph.stream.count) + len = cl - resph.stream.count; + + rcvd = Stream.read(b, off, len); + if (rcvd == -1) + throw new EOFException("Premature EOF encountered"); + + if (resph.stream.count+rcvd == cl) + close(resph); + + break; + + case CD_CHUNKED: + if (chunk_len == -1) // it's a new chunk + chunk_len = Codecs.getChunkLength(Stream); + + if (chunk_len > 0) // it's data + { + if (len > chunk_len) len = (int) chunk_len; + rcvd = Stream.read(b, off, len); + if (rcvd == -1) + throw new EOFException("Premature EOF encountered"); + chunk_len -= rcvd; + if (chunk_len == 0) // got the whole chunk + { + Stream.read(); // CR + Stream.read(); // LF + chunk_len = -1; + } + } + else // the footers (trailers) + { + resph.resp.readTrailers(Stream); + rcvd = -1; + close(resph); + chunk_len = -1; + } + break; + + case CD_MP_BR: + byte[] endbndry = resph.getEndBoundary(Stream); + int[] end_cmp = resph.getEndCompiled(Stream); + + rcvd = Stream.read(b, off, len); + if (rcvd == -1) + throw new EOFException("Premature EOF encountered"); + + int ovf = Stream.pastEnd(endbndry, end_cmp); + if (ovf != -1) + { + rcvd -= ovf; + close(resph); + } + + break; + + default: + throw new Error("Internal Error in StreamDemultiplexor: " + + "Invalid cd_type " + resph.resp.cd_type); + } + + restartTimer(); + return rcvd; + + } + catch (InterruptedIOException ie) // don't intercept this one + { + restartTimer(); + throw ie; + } + catch (IOException ioe) + { + Log.write(Log.DEMUX, "Demux: ", ioe); + + close(ioe, true); + throw resph.exception; // set by retry_requests + } + catch (ParseException pe) + { + Log.write(Log.DEMUX, "Demux: ", pe); + + close(new IOException(pe.toString()), true); + throw resph.exception; // set by retry_requests + } + } + } + + /** + * skips a number of bytes in the master stream. This is done via a + * dummy read, as the socket input stream doesn't like skip()'s. + */ + synchronized long skip(long num, ResponseHandler resph) throws IOException + { + if (resph.exception != null) + { + resph.exception.fillInStackTrace(); + throw resph.exception; + } + + if (resph.eof) + return 0; + + byte[] dummy = new byte[(int) num]; + int rcvd = read(dummy, 0, (int) num, resph, 0); + if (rcvd == -1) + return 0; + else + return rcvd; + } + + /** + * Determines the number of available bytes. If resph is null, return + * available bytes on the socket stream itself (used by HTTPConnection). + */ + synchronized int available(ResponseHandler resph) throws IOException + { + if (resph != null && resph.exception != null) + { + resph.exception.fillInStackTrace(); + throw resph.exception; + } + + if (resph != null && resph.eof) + return 0; + + int avail = Stream.available(); + if (resph == null) + return avail; + + switch (resph.resp.cd_type) + { + case CD_0: + return 0; + case CD_HDRS: + // this is something of a hack; I could return 0, but then + // if you were waiting for something on a response that + // wasn't first in line (and you didn't try to read the + // other response) you'd wait forever. On the other hand, + // we might be making a false promise here... + return (avail > 0 ? 1 : 0); + case CD_CLOSE: + return avail; + case CD_CONTLEN: + int cl = resph.resp.ContentLength; + cl -= resph.stream.count; + return (avail < cl ? avail : cl); + case CD_CHUNKED: + return avail; // not perfect... + case CD_MP_BR: + return avail; // not perfect... + default: + throw new Error("Internal Error in StreamDemultiplexor: " + + "Invalid cd_type " + resph.resp.cd_type); + } + + } + + + /** + * Closes the socket and all associated streams. If exception + * is not null then all active requests are retried. + * + *

    There are five ways this method may be activated. 1) if an exception + * occurs during read or write. 2) if the stream is marked for close but + * no responses are outstanding (e.g. due to a timeout). 3) when the + * markedForClose response is closed. 4) if all response streams up until + * and including the markedForClose response have been closed. 5) if this + * demux is finalized. + * + * @param exception the IOException to be sent to the streams. + * @param was_reset if true then the exception is due to a connection + * reset; otherwise it means we generated the exception + * ourselves and this is a "normal" close. + */ + synchronized void close(IOException exception, boolean was_reset) + { + if (Sock == null) // already cleaned up + return; + + Log.write(Log.DEMUX, "Demux: Closing all streams and socket (" + + this.hashCode() + ")"); + + try + { Stream.close(); } + catch (IOException ioe) { } + try + { Sock.close(); } + catch (IOException ioe) { } + Sock = null; + + if (Timer != null) + { + Timer.kill(); + Timer = null; + } + + Connection.DemuxList.remove(this); + + + // Here comes the tricky part: redo outstanding requests! + + if (exception != null) + synchronized (RespHandlerList) + { retry_requests(exception, was_reset); } + } + + + /** + * Retries outstanding requests. Well, actually the RetryModule does + * that. Here we just throw a RetryException for each request so that + * the RetryModule can catch and handle them. + * + * @param exception the exception that led to this call. + * @param was_reset this flag is passed to the RetryException and is + * used by the RetryModule to distinguish abnormal closes + * from expected closes. + */ + private void retry_requests(IOException exception, boolean was_reset) + { + RetryException first = null, + prev = null; + ResponseHandler resph = (ResponseHandler) RespHandlerList.enumerate(); + + while (resph != null) + { + /* if the application is already reading the data then the + * response has already been handled. In this case we must + * throw the real exception. + */ + if (resph.resp.got_headers) + { + resph.exception = exception; + } + else + { + RetryException tmp = new RetryException(exception.getMessage()); + if (first == null) first = tmp; + + tmp.request = resph.request; + tmp.response = resph.resp; + tmp.exception = exception; + tmp.conn_reset = was_reset; + tmp.first = first; + tmp.addToListAfter(prev); + + prev = tmp; + resph.exception = tmp; + } + + RespHandlerList.remove(resph); + resph = (ResponseHandler) RespHandlerList.next(); + } + } + + + /** + * Closes the associated stream. If this one has been markedForClose then + * the socket is closed; else closeSocketIfAllStreamsClosed is invoked. + */ + private void close(ResponseHandler resph) + { + synchronized (RespHandlerList) + { + if (resph != (ResponseHandler) RespHandlerList.getFirst()) + return; + + Log.write(Log.DEMUX, "Demux: Closing stream " + + resph.stream.hashCode()); + + resph.eof = true; + RespHandlerList.remove(resph); + } + + if (resph == MarkedForClose) + close(new IOException("Premature end of Keep-Alive"), false); + else + closeSocketIfAllStreamsClosed(); + } + + + /** + * Close the socket if all the streams have been closed. + * + *

    When a stream reaches eof it is removed from the response handler + * list, but when somebody close()'s the response stream it is just + * marked as such. This means that all responses in the list have either + * not been read at all or only partially read, but they might have been + * close()'d meaning that nobody is interested in the data. So If all the + * response streams up till and including the one markedForClose have + * been close()'d then we can remove them from our list and close the + * socket. + * + *

    Note: if the response list is emtpy or if no response is + * markedForClose then this method does nothing. Specifically it does + * not close the socket. We only want to close the socket if we've been + * told to do so. + * + *

    Also note that there might still be responses in the list after + * the markedForClose one. These are due to us having pipelined more + * requests to the server than it's willing to serve on a single + * connection. These requests will be retried if possible. + */ + synchronized void closeSocketIfAllStreamsClosed() + { + synchronized (RespHandlerList) + { + ResponseHandler resph = (ResponseHandler) RespHandlerList.enumerate(); + + while (resph != null && resph.stream.closed) + { + if (resph == MarkedForClose) + { + // remove all response handlers first + ResponseHandler tmp; + do + { + tmp = (ResponseHandler) RespHandlerList.getFirst(); + RespHandlerList.remove(tmp); + } + while (tmp != resph); + + // close the socket + close(new IOException("Premature end of Keep-Alive"), false); + return; + } + + resph = (ResponseHandler) RespHandlerList.next(); + } + } + } + + + /** + * returns the socket associated with this demux + */ + synchronized Socket getSocket() + { + if (MarkedForClose != null) + return null; + + if (Timer != null) Timer.hyber(); + return Sock; + } + + + /** + * Mark this demux to not accept any more request and to close the + * stream after this response or all requests have been + * processed, or close immediately if no requests are registered. + * + * @param response the Response after which the connection should + * be closed. + */ + synchronized void markForClose(Response resp) + { + synchronized (RespHandlerList) + { + if (RespHandlerList.getFirst() == null) // no active request, + { // so close the socket + close(new IOException("Premature end of Keep-Alive"), false); + return; + } + + if (Timer != null) + { + Timer.kill(); + Timer = null; + } + + ResponseHandler resph, lasth = null; + for (resph = (ResponseHandler) RespHandlerList.enumerate(); + resph != null; + resph = (ResponseHandler) RespHandlerList.next()) + { + if (resph.resp == resp) // new resp precedes any others + { + MarkedForClose = resph; + + Log.write(Log.DEMUX, "Demux: stream " + + resp.inp_stream.hashCode() + + " marked for close"); + + closeSocketIfAllStreamsClosed(); + return; + } + + if (MarkedForClose == resph) + return; // already marked for closing after an earlier resp + + lasth = resph; + } + + if (lasth == null) + return; + + MarkedForClose = lasth; // resp == null, so use last resph + closeSocketIfAllStreamsClosed(); + + Log.write(Log.DEMUX, "Demux: stream " + lasth.stream.hashCode() + + " marked for close"); + } + } + + + /** + * Emergency stop. Closes the socket and notifies the responses that + * the requests are aborted. + * + * @since V0.3 + */ + void abort() + { + Log.write(Log.DEMUX, "Demux: Aborting socket (" + this.hashCode() + ")"); + + + // notify all responses of abort + + synchronized (RespHandlerList) + { + for (ResponseHandler resph = + (ResponseHandler) RespHandlerList.enumerate(); + resph != null; + resph = (ResponseHandler) RespHandlerList.next()) + { + if (resph.resp.http_resp != null) + resph.resp.http_resp.markAborted(); + if (resph.exception == null) + resph.exception = new IOException("Request aborted by user"); + } + + + /* Close the socket. + * Note: this duplicates most of close(IOException, boolean). We + * do *not* call close() because that is synchronized, but we want + * abort() to be asynch. + */ + if (Sock != null) + { + try + { + try + { Sock.setSoLinger(false, 0); } + catch (SocketException se) + { } + + try + { Stream.close(); } + catch (IOException ioe) { } + try + { Sock.close(); } + catch (IOException ioe) { } + Sock = null; + + if (Timer != null) + { + Timer.kill(); + Timer = null; + } + } + catch (NullPointerException npe) + { } + + Connection.DemuxList.remove(this); + } + } + } + + + /** + * A safety net to close the connection. + */ + protected void finalize() throws Throwable + { + close((IOException) null, false); + super.finalize(); + } + + + /** + * produces a string. + * @return a string containing the class name and protocol number + */ + public String toString() + { + String prot; + + switch (Protocol) + { + case HTTP: + prot = "HTTP"; break; + case HTTPS: + prot = "HTTPS"; break; + case SHTTP: + prot = "SHTTP"; break; + case HTTP_NG: + prot = "HTTP_NG"; break; + default: + throw new Error("HTTPClient Internal Error: invalid protocol " + + Protocol); + } + + return getClass().getName() + "[Protocol=" + prot + "]"; + } +} + + +/** + * This thread is used to reap idle connections. It is NOT used to timeout + * reads or writes on a socket. It keeps a list of timer entries and expires + * them after a given time. + */ +class SocketTimeout extends Thread +{ + private boolean alive = true; + + /** + * This class represents a timer entry. It is used to close an + * inactive socket after n seconds. Once running, the timer may be + * suspended (hyber()), restarted (reset()), or aborted (kill()). + * When the timer expires it invokes markForClose() on the + * associated stream demultipexer. + */ + class TimeoutEntry + { + boolean restart = false, + hyber = false, + alive = true; + StreamDemultiplexor demux; + TimeoutEntry next = null, + prev = null; + + TimeoutEntry(StreamDemultiplexor demux) + { + this.demux = demux; + } + + void reset() + { + hyber = false; + if (restart) return; + restart = true; + + synchronized (time_list) + { + if (!alive) return; + + // remove from current position + next.prev = prev; + prev.next = next; + + // and add to end of timeout list + next = time_list[current]; + prev = time_list[current].prev; + prev.next = this; + next.prev = this; + } + } + + void hyber() + { + if (alive) hyber = true; + } + + void kill() + { + alive = false; + restart = false; + hyber = false; + + synchronized (time_list) + { + if (prev == null) return; + next.prev = prev; + prev.next = next; + prev = null; + } + } + } + + TimeoutEntry[] time_list; // jdk 1.1.x javac bug: these must not + int current; // be private! + + + SocketTimeout(int secs) + { + super("SocketTimeout"); + + try { setDaemon(true); } + catch (SecurityException se) { } // Oh well... + setPriority(MAX_PRIORITY); + + time_list = new TimeoutEntry[secs]; + for (int idx=0; idx= time_list.length) + current = 0; + + // remove all expired timers + for (TimeoutEntry entry = time_list[current].next; + entry != time_list[current]; + entry = entry.next) + { + if (entry.alive && !entry.hyber) + { + TimeoutEntry prev = entry.prev; + entry.kill(); + /* put on death row. Note: we must not invoke + * markForClose() here because it is synch'd + * and can therefore lead to a deadlock if that + * thread is trying to do a reset() or kill() + */ + entry.next = marked; + marked = entry; + entry = prev; + } + } + } + + while (marked != null) + { + marked.demux.markForClose(null); + marked = marked.next; + } + } + } + + /** + * Stop the timer thread. + */ + public void kill() { + alive = false; + } +} diff --git a/HTTPClient/TransferEncodingModule.java b/HTTPClient/TransferEncodingModule.java new file mode 100644 index 0000000..f713f07 --- /dev/null +++ b/HTTPClient/TransferEncodingModule.java @@ -0,0 +1,216 @@ +/* + * @(#)TransferEncodingModule.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.util.Vector; +import java.util.zip.InflaterInputStream; +import java.util.zip.GZIPInputStream; + + +/** + * This module handles the TransferEncoding response header. It currently + * handles the "gzip", "deflate", "compress", "chunked" and "identity" + * tokens. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class TransferEncodingModule implements HTTPClientModule +{ + // Methods + + /** + * Invoked by the HTTPClient. + */ + public int requestHandler(Request req, Response[] resp) + throws ModuleException + { + // Parse TE header + + int idx; + NVPair[] hdrs = req.getHeaders(); + for (idx=0; idx 0.) + return REQ_CONTINUE; + } + catch (NumberFormatException nfe) + { + throw new ModuleException("Invalid q value for \"*\" in TE " + + "header: " + nfe.getMessage()); + } + } + + + // Add gzip, deflate, and compress tokens to the TE header + + if (!pte.contains(new HttpHeaderElement("deflate"))) + pte.addElement(new HttpHeaderElement("deflate")); + if (!pte.contains(new HttpHeaderElement("gzip"))) + pte.addElement(new HttpHeaderElement("gzip")); + if (!pte.contains(new HttpHeaderElement("compress"))) + pte.addElement(new HttpHeaderElement("compress")); + + hdrs[idx] = new NVPair("TE", Util.assembleHeader(pte)); + + return REQ_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase1Handler(Response resp, RoRequest req) + { + } + + + /** + * Invoked by the HTTPClient. + */ + public int responsePhase2Handler(Response resp, Request req) + { + return RSP_CONTINUE; + } + + + /** + * Invoked by the HTTPClient. + */ + public void responsePhase3Handler(Response resp, RoRequest req) + throws IOException, ModuleException + { + String te = resp.getHeader("Transfer-Encoding"); + if (te == null || req.getMethod().equals("HEAD")) + return; + + Vector pte; + try + { pte = Util.parseHeader(te); } + catch (ParseException pe) + { throw new ModuleException(pe.toString()); } + + while (pte.size() > 0) + { + String encoding = ((HttpHeaderElement) pte.lastElement()).getName(); + if (encoding.equalsIgnoreCase("gzip")) + { + Log.write(Log.MODS, "TEM: pushing gzip-input-stream"); + + resp.inp_stream = new GZIPInputStream(resp.inp_stream); + } + else if (encoding.equalsIgnoreCase("deflate")) + { + Log.write(Log.MODS, "TEM: pushing inflater-input-stream"); + + resp.inp_stream = new InflaterInputStream(resp.inp_stream); + } + else if (encoding.equalsIgnoreCase("compress")) + { + Log.write(Log.MODS, "TEM: pushing uncompress-input-stream"); + + resp.inp_stream = new UncompressInputStream(resp.inp_stream); + } + else if (encoding.equalsIgnoreCase("chunked")) + { + Log.write(Log.MODS, "TEM: pushing chunked-input-stream"); + + resp.inp_stream = new ChunkedInputStream(resp.inp_stream); + } + else if (encoding.equalsIgnoreCase("identity")) + { + Log.write(Log.MODS, "TEM: ignoring 'identity' token"); + } + else + { + Log.write(Log.MODS, "TEM: Unknown transfer encoding '" + + encoding + "'"); + + break; + } + + pte.removeElementAt(pte.size()-1); + } + + if (pte.size() > 0) + resp.setHeader("Transfer-Encoding", Util.assembleHeader(pte)); + else + resp.deleteHeader("Transfer-Encoding"); + } + + + /** + * Invoked by the HTTPClient. + */ + public void trailerHandler(Response resp, RoRequest req) + { + } +} diff --git a/HTTPClient/URI.java b/HTTPClient/URI.java new file mode 100644 index 0000000..cc6d5e0 --- /dev/null +++ b/HTTPClient/URI.java @@ -0,0 +1,1939 @@ +/* + * @(#)URI.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.net.URL; +import java.net.MalformedURLException; +import java.util.BitSet; +import java.util.Hashtable; + +/** + * This class represents a generic URI, as defined in RFC-2396. + * This is similar to java.net.URL, with the following enhancements: + *

      + *
    • it doesn't require a URLStreamhandler to exist for the scheme; this + * allows this class to be used to hold any URI, construct absolute + * URIs from relative ones, etc. + *
    • it handles escapes correctly + *
    • equals() works correctly + *
    • relative URIs are correctly constructed + *
    • it has methods for accessing various fields such as userinfo, + * fragment, params, etc. + *
    • it handles less common forms of resources such as the "*" used in + * http URLs. + *
    + * + *

    The elements are always stored in escaped form. + * + *

    While RFC-2396 distinguishes between just two forms of URI's, those that + * follow the generic syntax and those that don't, this class knows about a + * third form, named semi-generic, used by quite a few popular schemes. + * Semi-generic syntax treats the path part as opaque, i.e. has the form + * <scheme>://<authority>/<opaque> . Relative URI's of this + * type are only resolved as far as absolute paths - relative paths do not + * exist. + * + *

    Ideally, java.net.URL should subclass URI. + * + * @see rfc-2396 + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + * @since V0.3-1 + */ +public class URI +{ + /** + * If true, then the parser will resolve certain URI's in backwards + * compatible (but technically incorrect) manner. Example: + * + *

    +     * base   = http://a/b/c/d;p?q
    +     * rel    = http:g
    +     * result = http:g		(correct)
    +     * result = http://a/b/c/g	(backwards compatible)
    +     *
    + * + * See rfc-2396, section 5.2, step 3, second paragraph. + */ + public static final boolean ENABLE_BACKWARDS_COMPATIBILITY = true; + + protected static final Hashtable defaultPorts = new Hashtable(); + protected static final Hashtable usesGenericSyntax = new Hashtable(); + protected static final Hashtable usesSemiGenericSyntax = new Hashtable(); + + /* various character classes as defined in the draft */ + protected static final BitSet alphanumChar; + protected static final BitSet markChar; + protected static final BitSet reservedChar; + protected static final BitSet unreservedChar; + protected static final BitSet uricChar; + protected static final BitSet pcharChar; + protected static final BitSet userinfoChar; + protected static final BitSet schemeChar; + protected static final BitSet hostChar; + protected static final BitSet opaqueChar; + protected static final BitSet reg_nameChar; + + /* These are not directly in the spec, but used for escaping and + * unescaping parts + */ + + /** list of characters which must not be unescaped when unescaping a scheme */ + public static final BitSet resvdSchemeChar; + /** list of characters which must not be unescaped when unescaping a userinfo */ + public static final BitSet resvdUIChar; + /** list of characters which must not be unescaped when unescaping a host */ + public static final BitSet resvdHostChar; + /** list of characters which must not be unescaped when unescaping a path */ + public static final BitSet resvdPathChar; + /** list of characters which must not be unescaped when unescaping a query string */ + public static final BitSet resvdQueryChar; + /** list of characters which must not be escaped when escaping a path */ + public static final BitSet escpdPathChar; + /** list of characters which must not be escaped when escaping a query string */ + public static final BitSet escpdQueryChar; + /** list of characters which must not be escaped when escaping a fragment identifier */ + public static final BitSet escpdFragChar; + + static + { + defaultPorts.put("http", new Integer(80)); + defaultPorts.put("shttp", new Integer(80)); + defaultPorts.put("http-ng", new Integer(80)); + defaultPorts.put("coffee", new Integer(80)); + defaultPorts.put("https", new Integer(443)); + defaultPorts.put("ftp", new Integer(21)); + defaultPorts.put("telnet", new Integer(23)); + defaultPorts.put("nntp", new Integer(119)); + defaultPorts.put("news", new Integer(119)); + defaultPorts.put("snews", new Integer(563)); + defaultPorts.put("hnews", new Integer(80)); + defaultPorts.put("smtp", new Integer(25)); + defaultPorts.put("gopher", new Integer(70)); + defaultPorts.put("wais", new Integer(210)); + defaultPorts.put("whois", new Integer(43)); + defaultPorts.put("whois++", new Integer(63)); + defaultPorts.put("rwhois", new Integer(4321)); + defaultPorts.put("imap", new Integer(143)); + defaultPorts.put("pop", new Integer(110)); + defaultPorts.put("prospero", new Integer(1525)); + defaultPorts.put("irc", new Integer(194)); + defaultPorts.put("ldap", new Integer(389)); + defaultPorts.put("nfs", new Integer(2049)); + defaultPorts.put("z39.50r", new Integer(210)); + defaultPorts.put("z39.50s", new Integer(210)); + defaultPorts.put("vemmi", new Integer(575)); + defaultPorts.put("videotex", new Integer(516)); + defaultPorts.put("cmp", new Integer(829)); + + usesGenericSyntax.put("http", Boolean.TRUE); + usesGenericSyntax.put("https", Boolean.TRUE); + usesGenericSyntax.put("shttp", Boolean.TRUE); + usesGenericSyntax.put("coffee", Boolean.TRUE); + usesGenericSyntax.put("ftp", Boolean.TRUE); + usesGenericSyntax.put("file", Boolean.TRUE); + usesGenericSyntax.put("nntp", Boolean.TRUE); + usesGenericSyntax.put("news", Boolean.TRUE); + usesGenericSyntax.put("snews", Boolean.TRUE); + usesGenericSyntax.put("hnews", Boolean.TRUE); + usesGenericSyntax.put("imap", Boolean.TRUE); + usesGenericSyntax.put("wais", Boolean.TRUE); + usesGenericSyntax.put("nfs", Boolean.TRUE); + usesGenericSyntax.put("sip", Boolean.TRUE); + usesGenericSyntax.put("sips", Boolean.TRUE); + usesGenericSyntax.put("sipt", Boolean.TRUE); + usesGenericSyntax.put("sipu", Boolean.TRUE); + /* Note: schemes which definitely don't use the generic-URI syntax + * and must therefore never appear in the above list: + * "urn", "mailto", "sdp", "service", "tv", "gsm-sms", "tel", "fax", + * "modem", "eid", "cid", "mid", "data", "ldap" + */ + + usesSemiGenericSyntax.put("ldap", Boolean.TRUE); + usesSemiGenericSyntax.put("irc", Boolean.TRUE); + usesSemiGenericSyntax.put("gopher", Boolean.TRUE); + usesSemiGenericSyntax.put("videotex", Boolean.TRUE); + usesSemiGenericSyntax.put("rwhois", Boolean.TRUE); + usesSemiGenericSyntax.put("whois++", Boolean.TRUE); + usesSemiGenericSyntax.put("smtp", Boolean.TRUE); + usesSemiGenericSyntax.put("telnet", Boolean.TRUE); + usesSemiGenericSyntax.put("prospero", Boolean.TRUE); + usesSemiGenericSyntax.put("pop", Boolean.TRUE); + usesSemiGenericSyntax.put("vemmi", Boolean.TRUE); + usesSemiGenericSyntax.put("z39.50r", Boolean.TRUE); + usesSemiGenericSyntax.put("z39.50s", Boolean.TRUE); + usesSemiGenericSyntax.put("stream", Boolean.TRUE); + usesSemiGenericSyntax.put("cmp", Boolean.TRUE); + + alphanumChar = new BitSet(128); + for (int ch='0'; ch<='9'; ch++) alphanumChar.set(ch); + for (int ch='A'; ch<='Z'; ch++) alphanumChar.set(ch); + for (int ch='a'; ch<='z'; ch++) alphanumChar.set(ch); + + markChar = new BitSet(128); + markChar.set('-'); + markChar.set('_'); + markChar.set('.'); + markChar.set('!'); + markChar.set('~'); + markChar.set('*'); + markChar.set('\''); + markChar.set('('); + markChar.set(')'); + + reservedChar = new BitSet(128); + reservedChar.set(';'); + reservedChar.set('/'); + reservedChar.set('?'); + reservedChar.set(':'); + reservedChar.set('@'); + reservedChar.set('&'); + reservedChar.set('='); + reservedChar.set('+'); + reservedChar.set('$'); + reservedChar.set(','); + + unreservedChar = new BitSet(128); + unreservedChar.or(alphanumChar); + unreservedChar.or(markChar); + + uricChar = new BitSet(128); + uricChar.or(unreservedChar); + uricChar.or(reservedChar); + uricChar.set('%'); + + pcharChar = new BitSet(128); + pcharChar.or(unreservedChar); + pcharChar.set('%'); + pcharChar.set(':'); + pcharChar.set('@'); + pcharChar.set('&'); + pcharChar.set('='); + pcharChar.set('+'); + pcharChar.set('$'); + pcharChar.set(','); + + userinfoChar = new BitSet(128); + userinfoChar.or(unreservedChar); + userinfoChar.set('%'); + userinfoChar.set(';'); + userinfoChar.set(':'); + userinfoChar.set('&'); + userinfoChar.set('='); + userinfoChar.set('+'); + userinfoChar.set('$'); + userinfoChar.set(','); + + // this actually shouldn't contain uppercase letters... + schemeChar = new BitSet(128); + schemeChar.or(alphanumChar); + schemeChar.set('+'); + schemeChar.set('-'); + schemeChar.set('.'); + + opaqueChar = new BitSet(128); + opaqueChar.or(uricChar); + + hostChar = new BitSet(128); + hostChar.or(alphanumChar); + hostChar.set('-'); + hostChar.set('.'); + + reg_nameChar = new BitSet(128); + reg_nameChar.or(unreservedChar); + reg_nameChar.set('$'); + reg_nameChar.set(','); + reg_nameChar.set(';'); + reg_nameChar.set(':'); + reg_nameChar.set('@'); + reg_nameChar.set('&'); + reg_nameChar.set('='); + reg_nameChar.set('+'); + + resvdSchemeChar = new BitSet(128); + resvdSchemeChar.set(':'); + + resvdUIChar = new BitSet(128); + resvdUIChar.set('@'); + + resvdHostChar = new BitSet(128); + resvdHostChar.set(':'); + resvdHostChar.set('/'); + resvdHostChar.set('?'); + resvdHostChar.set('#'); + + resvdPathChar = new BitSet(128); + resvdPathChar.set('/'); + resvdPathChar.set(';'); + resvdPathChar.set('?'); + resvdPathChar.set('#'); + + resvdQueryChar = new BitSet(128); + resvdQueryChar.set('#'); + + escpdPathChar = new BitSet(128); + escpdPathChar.or(pcharChar); + escpdPathChar.set('%'); + escpdPathChar.set('/'); + escpdPathChar.set(';'); + + escpdQueryChar = new BitSet(128); + escpdQueryChar.or(uricChar); + escpdQueryChar.clear('#'); + + escpdFragChar = new BitSet(128); + escpdFragChar.or(uricChar); + } + + + /* our uri in pieces */ + + protected static final int OPAQUE = 0; + protected static final int SEMI_GENERIC = 1; + protected static final int GENERIC = 2; + + protected int type; + protected String scheme; + protected String opaque; + protected String userinfo; + protected String host; + protected int port = -1; + protected String path; + protected String query; + protected String fragment; + + + /* cache the java.net.URL */ + + protected URL url = null; + + + // Constructors + + /** + * Constructs a URI from the given string representation. The string + * must be an absolute URI. + * + * @param uri a String containing an absolute URI + * @exception ParseException if no scheme can be found or a specified + * port cannot be parsed as a number + */ + public URI(String uri) throws ParseException + { + this((URI) null, uri); + } + + + /** + * Constructs a URI from the given string representation, relative to + * the given base URI. + * + * @param base the base URI, relative to which rel_uri + * is to be parsed + * @param rel_uri a String containing a relative or absolute URI + * @exception ParseException if base is null and + * rel_uri is not an absolute URI, or + * if base is not null and the scheme + * is not known to use the generic syntax, or + * if a given port cannot be parsed as a number + */ + public URI(URI base, String rel_uri) throws ParseException + { + /* Parsing is done according to the following RE: + * + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + * 12 3 4 5 6 7 8 9 + * + * 2: scheme + * 4: authority + * 5: path + * 7: query + * 9: fragment + */ + + char[] uri = rel_uri.toCharArray(); + int pos = 0, idx, len = uri.length; + + + // trim() + + while (pos < len && Character.isWhitespace(uri[pos])) pos++; + while (len > 0 && Character.isWhitespace(uri[len-1])) len--; + + + // strip the special "url" or "uri" scheme + + if (pos < len-3 && uri[pos+3] == ':' && + (uri[pos+0] == 'u' || uri[pos+0] == 'U') && + (uri[pos+1] == 'r' || uri[pos+1] == 'R') && + (uri[pos+2] == 'i' || uri[pos+2] == 'I' || + uri[pos+2] == 'l' || uri[pos+2] == 'L')) + pos += 4; + + + // get scheme: (([^:/?#]+):)? + + idx = pos; + while (idx < len && uri[idx] != ':' && uri[idx] != '/' && + uri[idx] != '?' && uri[idx] != '#') + idx++; + if (idx < len && uri[idx] == ':') + { + scheme = rel_uri.substring(pos, idx).trim().toLowerCase(); + pos = idx + 1; + } + + + // check and resolve scheme + + String final_scheme = scheme; + if (scheme == null) + { + if (base == null) + throw new ParseException("No scheme found"); + final_scheme = base.scheme; + } + + + // check for generic vs. opaque + + type = usesGenericSyntax(final_scheme) ? GENERIC : + usesSemiGenericSyntax(final_scheme) ? SEMI_GENERIC : OPAQUE; + if (type == OPAQUE) + { + if (base != null && scheme == null) + throw new ParseException("Can't resolve relative URI for " + + "scheme " + final_scheme); + + opaque = escape(rel_uri.substring(pos), opaqueChar, true); + if (opaque.length() > 0 && opaque.charAt(0) == '/') + opaque = "%2F" + opaque.substring(1); + return; + } + + + // get authority: (//([^/?#]*))? + + if (pos+1 < len && uri[pos] == '/' && uri[pos+1] == '/') + { + pos += 2; + idx = pos; + while (idx < len && uri[idx] != '/' && uri[idx] != '?' && + uri[idx] != '#') + idx++; + + parse_authority(rel_uri.substring(pos, idx), final_scheme); + pos = idx; + } + + + // handle semi-generic and generic uri's + + if (type == SEMI_GENERIC) + { + path = escape(rel_uri.substring(pos), uricChar, true); + if (path.length() > 0 && path.charAt(0) != '/') + path = '/' + path; + } + else + { + // get path: ([^?#]*) + + idx = pos; + while (idx < len && uri[idx] != '?' && uri[idx] != '#') + idx++; + path = escape(rel_uri.substring(pos, idx), escpdPathChar, true); + pos = idx; + + + // get query: (\?([^#]*))? + + if (pos < len && uri[pos] == '?') + { + pos += 1; + idx = pos; + while (idx < len && uri[idx] != '#') + idx++; + this.query = escape(rel_uri.substring(pos, idx), escpdQueryChar, true); + pos = idx; + } + + + // get fragment: (#(.*))? + + if (pos < len && uri[pos] == '#') + this.fragment = escape(rel_uri.substring(pos+1, len), escpdFragChar, true); + } + + + // now resolve the parts relative to the base + + if (base != null) + { + if (scheme != null && // resolve scheme + !(scheme.equals(base.scheme) && ENABLE_BACKWARDS_COMPATIBILITY)) + return; + scheme = base.scheme; + + if (host != null) // resolve authority + return; + userinfo = base.userinfo; + host = base.host; + port = base.port; + + if (type == SEMI_GENERIC) // can't resolve relative paths + return; + + if (path.length() == 0 && query == null) // current doc + { + path = base.path; + query = base.query; + return; + } + + if (path.length() == 0 || path.charAt(0) != '/') // relative path + { + idx = (base.path != null) ? base.path.lastIndexOf('/') : -1; + if (idx < 0) + path = '/' + path; + else + path = base.path.substring(0, idx+1) + path; + + path = canonicalizePath(path); + } + } + } + + /** + * Remove all "/../" and "/./" from path, where possible. Leading "/../"'s + * are not removed. + * + * @param path the path to canonicalize + * @return the canonicalized path + */ + public static String canonicalizePath(String path) + { + int idx, len = path.length(); + if (!((idx = path.indexOf("/.")) != -1 && + (idx == len-2 || path.charAt(idx+2) == '/' || + (path.charAt(idx+2) == '.' && + (idx == len-3 || path.charAt(idx+3) == '/')) ))) + return path; + + char[] p = new char[path.length()]; // clean path + path.getChars(0, p.length, p, 0); + + int beg = 0; + for (idx=1; idx beg && p[end] != '/') end--; + if (p[end] != '/') continue; + if (idx == len-2) end++; + idx += 2; + } + else + continue; + System.arraycopy(p, idx, p, end, len-idx); + len -= idx - end; + idx = end; + } + } + + return new String(p, 0, len); + } + + /** + * Parse the authority specific part + */ + private void parse_authority(String authority, String scheme) + throws ParseException + { + /* The authority is further parsed according to: + * + * ^(([^@]*)@?)(\[[^]]*\]|[^:]*)?(:(.*))? + * 12 3 4 5 + * + * 2: userinfo + * 3: host + * 5: port + */ + + char[] uri = authority.toCharArray(); + int pos = 0, idx, len = uri.length; + + + // get userinfo: (([^@]*)@?) + + idx = pos; + while (idx < len && uri[idx] != '@') + idx++; + if (idx < len && uri[idx] == '@') + { + this.userinfo = escape(authority.substring(pos, idx), userinfoChar, true); + pos = idx + 1; + } + + + // get host: (\[[^]]*\]|[^:]*)? + + idx = pos; + if (idx < len && uri[idx] == '[') // IPv6 + { + while (idx < len && uri[idx] != ']') + idx++; + if (idx == len) + throw new ParseException("No closing ']' found for opening '['"+ + " at position " + pos + + " in authority `" + authority + "'"); + this.host = authority.substring(pos+1, idx); + idx++; + } + else + { + while (idx < len && uri[idx] != ':') + idx++; + this.host = escape(authority.substring(pos, idx), uricChar, true); + } + pos = idx; + + + // get port: (:(.*))? + + if (pos < (len-1) && uri[pos] == ':') + { + int p; + try + { + p = Integer.parseInt( + unescape(authority.substring(pos+1, len), null)); + if (p < 0) throw new NumberFormatException(); + } + catch (NumberFormatException e) + { + throw new ParseException(authority.substring(pos+1, len) + + " is an invalid port number"); + } + if (p == defaultPort(scheme)) + this.port = -1; + else + this.port = p; + } + } + + + /** + * Construct a URI from the given URL. + * + * @param url the URL + * @exception ParseException if url.toExternalForm() generates + * an invalid string representation + */ + public URI(URL url) throws ParseException + { + this((URI) null, url.toExternalForm()); + } + + + /** + * Constructs a URI from the given parts, using the default port for + * this scheme (if known). The parts must be in unescaped form. + * + * @param scheme the scheme (sometimes known as protocol) + * @param host the host + * @param path the path part + * @exception ParseException if scheme is null + */ + public URI(String scheme, String host, String path) throws ParseException + { + this(scheme, null, host, -1, path, null, null); + } + + + /** + * Constructs a URI from the given parts. The parts must be in unescaped + * form. + * + * @param scheme the scheme (sometimes known as protocol) + * @param host the host + * @param port the port + * @param path the path part + * @exception ParseException if scheme is null + */ + public URI(String scheme, String host, int port, String path) + throws ParseException + { + this(scheme, null, host, port, path, null, null); + } + + + /** + * Constructs a URI from the given parts. Any part except for the + * the scheme may be null. The parts must be in unescaped form. + * + * @param scheme the scheme (sometimes known as protocol) + * @param userinfo the userinfo + * @param host the host + * @param port the port + * @param path the path part + * @param query the query string + * @param fragment the fragment identifier + * @exception ParseException if scheme is null + */ + public URI(String scheme, String userinfo, String host, int port, + String path, String query, String fragment) + throws ParseException + { + if (scheme == null) + throw new ParseException("missing scheme"); + this.scheme = escape(scheme.trim().toLowerCase(), schemeChar, true); + if (userinfo != null) + this.userinfo = escape(userinfo.trim(), userinfoChar, true); + if (host != null) + { + host = host.trim(); + this.host = isIPV6Addr(host) ? host : escape(host, hostChar, true); + } + if (port != defaultPort(scheme)) + this.port = port; + if (path != null) + this.path = escape(path.trim(), escpdPathChar, true); // ??? + if (query != null) + this.query = escape(query.trim(), escpdQueryChar, true); + if (fragment != null) + this.fragment = escape(fragment.trim(), escpdFragChar, true); + + type = usesGenericSyntax(scheme) ? GENERIC : SEMI_GENERIC; + } + + private static final boolean isIPV6Addr(String host) + { + if (host.indexOf(':') < 0) + return false; + + for (int idx=0; idx '9') && ch != ':') + return false; + } + + return true; + } + + + /** + * Constructs an opaque URI from the given parts. + * + * @param scheme the scheme (sometimes known as protocol) + * @param opaque the opaque part + * @exception ParseException if scheme is null + */ + public URI(String scheme, String opaque) + throws ParseException + { + if (scheme == null) + throw new ParseException("missing scheme"); + this.scheme = escape(scheme.trim().toLowerCase(), schemeChar, true); + this.opaque = escape(opaque, opaqueChar, true); + + type = OPAQUE; + } + + + // Class Methods + + /** + * @return true if the scheme should be parsed according to the + * generic-URI syntax + */ + public static boolean usesGenericSyntax(String scheme) + { + return usesGenericSyntax.containsKey(scheme.trim().toLowerCase()); + } + + + /** + * @return true if the scheme should be parsed according to a + * semi-generic-URI syntax <scheme&tgt;://<hostport>/<opaque> + */ + public static boolean usesSemiGenericSyntax(String scheme) + { + return usesSemiGenericSyntax.containsKey(scheme.trim().toLowerCase()); + } + + + /** + * Return the default port used by a given protocol. + * + * @param protocol the protocol + * @return the port number, or 0 if unknown + */ + public final static int defaultPort(String protocol) + { + Integer port = (Integer) defaultPorts.get(protocol.trim().toLowerCase()); + return (port != null) ? port.intValue() : 0; + } + + + // Instance Methods + + /** + * @return the scheme (often also referred to as protocol) + */ + public String getScheme() + { + return scheme; + } + + + /** + * @return the opaque part, or null if this URI is generic + */ + public String getOpaque() + { + return opaque; + } + + + /** + * @return the host + */ + public String getHost() + { + return host; + } + + + /** + * @return the port, or -1 if it's the default port, or 0 if unknown + */ + public int getPort() + { + return port; + } + + + /** + * @return the user info + */ + public String getUserinfo() + { + return userinfo; + } + + + /** + * @return the path + */ + public String getPath() + { + return path; + } + + + /** + * @return the query string + */ + public String getQueryString() + { + return query; + } + + + /** + * @return the path and query + */ + public String getPathAndQuery() + { + if (query == null) + return path; + if (path == null) + return "?" + query; + return path + "?" + query; + } + + + /** + * @return the fragment + */ + public String getFragment() + { + return fragment; + } + + + /** + * Does the scheme specific part of this URI use the generic-URI syntax? + * + *

    In general URI are split into two categories: opaque-URI and + * generic-URI. The generic-URI syntax is the syntax most are familiar + * with from URLs such as ftp- and http-URLs, which is roughly: + *

    +     * generic-URI = scheme ":" [ "//" server ] [ "/" ] [ path_segments ] [ "?" query ]
    +     * 
    + * (see RFC-2396 for exact syntax). Only URLs using the generic-URI syntax + * can be used to create and resolve relative URIs. + * + *

    Whether a given scheme is parsed according to the generic-URI + * syntax or wether it is treated as opaque is determined by an internal + * table of URI schemes. + * + * @see rfc-2396 + */ + public boolean isGenericURI() + { + return (type == GENERIC); + } + + /** + * Does the scheme specific part of this URI use the semi-generic-URI syntax? + * + *

    Many schemes which don't follow the full generic syntax actually + * follow a reduced form where the path part is treated is opaque. This + * is used for example by ldap, smtp, pop, etc, and is roughly + *

    +     * generic-URI = scheme ":" [ "//" server ] [ "/" [ opaque_path ] ]
    +     * 
    + * I.e. parsing is identical to the generic-syntax, except that the path + * part is not further parsed. URLs using the semi-generic-URI syntax can + * be used to create and resolve relative URIs with the restriction that + * all paths are treated as absolute. + * + *

    Whether a given scheme is parsed according to the semi-generic-URI + * syntax is determined by an internal table of URI schemes. + * + * @see #isGenericURI() + */ + public boolean isSemiGenericURI() + { + return (type == SEMI_GENERIC); + } + + + /** + * Will try to create a java.net.URL object from this URI. + * + * @return the URL + * @exception MalformedURLException if no handler is available for the + * scheme + */ + public URL toURL() throws MalformedURLException + { + if (url != null) return url; + + if (opaque != null) + return (url = new URL(scheme + ":" + opaque)); + + String hostinfo; + if (userinfo != null && host != null) + hostinfo = userinfo + "@" + host; + else if (userinfo != null) + hostinfo = userinfo + "@"; + else + hostinfo = host; + + StringBuffer file = new StringBuffer(100); + assemblePath(file, true, true, false); + + url = new URL(scheme, hostinfo, port, file.toString()); + return url; + } + + + private final void assemblePath(StringBuffer buf, boolean printEmpty, + boolean incFragment, boolean unescape) + { + if ((path == null || path.length() == 0) && printEmpty) + buf.append('/'); + + if (path != null) + buf.append(unescape ? unescapeNoPE(path, resvdPathChar) : path); + + if (query != null) + { + buf.append('?'); + buf.append(unescape ? unescapeNoPE(query, resvdQueryChar) : query); + } + + if (fragment != null && incFragment) + { + buf.append('#'); + buf.append(unescape ? unescapeNoPE(fragment, null) : fragment); + } + } + + + private final String stringify(boolean unescape) + { + StringBuffer uri = new StringBuffer(100); + + if (scheme != null) + { + uri.append(unescape ? unescapeNoPE(scheme, resvdSchemeChar) : scheme); + uri.append(':'); + } + + if (opaque != null) // it's an opaque-uri + { + uri.append(unescape ? unescapeNoPE(opaque, null) : opaque); + return uri.toString(); + } + + if (userinfo != null || host != null || port != -1) + uri.append("//"); + + if (userinfo != null) + { + uri.append(unescape ? unescapeNoPE(userinfo, resvdUIChar) : userinfo); + uri.append('@'); + } + + if (host != null) + { + if (host.indexOf(':') < 0) + uri.append(unescape ? unescapeNoPE(host, resvdHostChar) : host); + else + uri.append('[').append(host).append(']'); + } + + if (port != -1) + { + uri.append(':'); + uri.append(port); + } + + assemblePath(uri, false, true, unescape); + + return uri.toString(); + } + + + /** + * @return a string representation of this URI suitable for use in + * links, headers, etc. + */ + public String toExternalForm() + { + return stringify(false); + } + + + /** + * Return the URI as string. This differs from toExternalForm() in that + * all elements are unescaped before assembly. This is not suitable + * for passing to other apps or in header fields and such, and is usually + * not what you want. + * + * @return the URI as a string + * @see #toExternalForm() + */ + public String toString() + { + return stringify(true); + } + + + /** + * @return true if other is either a URI or URL and it + * matches the current URI + */ + public boolean equals(Object other) + { + if (other instanceof URI) + { + URI o = (URI) other; + return (scheme.equals(o.scheme) && + ( + type == OPAQUE && areEqual(opaque, o.opaque) || + + type == SEMI_GENERIC && + areEqual(userinfo, o.userinfo) && + areEqualIC(host, o.host) && + port == o.port && + areEqual(path, o.path) || + + type == GENERIC && + areEqual(userinfo, o.userinfo) && + areEqualIC(host, o.host) && + port == o.port && + pathsEqual(path, o.path) && + areEqual(query, o.query) && + areEqual(fragment, o.fragment) + )); + } + + if (other instanceof URL) + { + URL o = (URL) other; + String h, f; + + if (userinfo != null) + h = userinfo + "@" + host; + else + h = host; + + f = getPathAndQuery(); + + return (scheme.equalsIgnoreCase(o.getProtocol()) && + (type == OPAQUE && opaque.equals(o.getFile()) || + + type == SEMI_GENERIC && + areEqualIC(h, o.getHost()) && + (port == o.getPort() || + o.getPort() == defaultPort(scheme)) && + areEqual(f, o.getFile()) || + + type == GENERIC && + areEqualIC(h, o.getHost()) && + (port == o.getPort() || + o.getPort() == defaultPort(scheme)) && + pathsEqual(f, o.getFile()) && + areEqual(fragment, o.getRef()) + ) + ); + } + + return false; + } + + private static final boolean areEqual(String s1, String s2) + { + return (s1 == null && s2 == null || + s1 != null && s2 != null && + (s1.equals(s2) || + unescapeNoPE(s1, null).equals(unescapeNoPE(s2, null))) + ); + } + + private static final boolean areEqualIC(String s1, String s2) + { + return (s1 == null && s2 == null || + s1 != null && s2 != null && + (s1.equalsIgnoreCase(s2) || + unescapeNoPE(s1, null).equalsIgnoreCase(unescapeNoPE(s2, null))) + ); + } + + private static final boolean pathsEqual(String p1, String p2) + { + if (p1 == null && p2 == null) + return true; + if (p1 == null || p2 == null) + return false; + if (p1.equals(p2)) + return true; + + // ok, so it wasn't that simple. Let's split into parts and compare + // unescaped. + int pos1 = 0, end1 = p1.length(), pos2 = 0, end2 = p2.length(); + while (pos1 < end1 && pos2 < end2) + { + int start1 = pos1, start2 = pos2; + + char ch; + while (pos1 < end1 && (ch = p1.charAt(pos1)) != '/' && ch != ';') + pos1++; + while (pos2 < end2 && (ch = p2.charAt(pos2)) != '/' && ch != ';') + pos2++; + + if (pos1 == end1 && pos2 < end2 || + pos2 == end2 && pos1 < end1 || + pos1 < end1 && pos2 < end2 && p1.charAt(pos1) != p2.charAt(pos2)) + return false; + + if ((!p1.regionMatches(start1, p2, start2, pos1-start1) || (pos1-start1) != (pos2-start2)) && + !unescapeNoPE(p1.substring(start1, pos1), null).equals(unescapeNoPE(p2.substring(start2, pos2), null))) + return false; + + pos1++; + pos2++; + } + + return (pos1 == end1 && pos2 == end2); + } + + private int hashCode = -1; + + /** + * The hash code is calculated over scheme, host, path, and query. + * + * @return the hash code + */ + public int hashCode() + { + if (hashCode == -1) + hashCode = (scheme != null ? unescapeNoPE(scheme, null).hashCode() : 0) + + (type == OPAQUE ? + (opaque != null ? unescapeNoPE(opaque, null).hashCode() : 0) * 7 + : (host != null ? unescapeNoPE(host, null).toLowerCase().hashCode() : 0) * 7 + + (path != null ? unescapeNoPE(path, null).hashCode() : 0) * 13 + + (query != null ? unescapeNoPE(query, null).hashCode() : 0) * 17); + + return hashCode; + } + + + /** + * Escape any character not in the given character class. Characters + * greater 255 are always escaped according to ??? . + * + * @param elem the string to escape + * @param allowed_char the BitSet of all allowed characters + * @param utf8 if true, will first UTF-8 encode unallowed characters + * @return the string with all characters not in allowed_char + * escaped + */ + public static String escape(String elem, BitSet allowed_char, boolean utf8) + { + return new String(escape(elem.toCharArray(), allowed_char, utf8)); + } + + /** + * Escape any character not in the given character class. Characters + * greater 255 are always escaped according to ??? . + * + * @param elem the array of characters to escape + * @param allowed_char the BitSet of all allowed characters + * @param utf8 if true, will first UTF-8 encode unallowed characters + * @return the elem array with all characters not in allowed_char + * escaped + */ + public static char[] escape(char[] elem, BitSet allowed_char, boolean utf8) + { + int cnt=0; + for (int idx=0; idx= 0x0080) + cnt += 3; + if (elem[idx] >= 0x00800) + cnt += 3; + if ((elem[idx] & 0xFC00) == 0xD800 && idx+1 < elem.length && + (elem[idx+1] & 0xFC00) == 0xDC00) + cnt -= 6; + } + } + } + + if (cnt == 0) return elem; + + char[] tmp = new char[elem.length + cnt]; + for (int idx=0, pos=0; idx> 6) & 0x1F)); + pos = enc(tmp, pos, 0x80 | ((c >> 0) & 0x3F)); + } + else if (!((c & 0xFC00) == 0xD800 && idx+1 < elem.length && + (elem[idx+1] & 0xFC00) == 0xDC00)) + { + pos = enc(tmp, pos, 0xE0 | ((c >> 12) & 0x0F)); + pos = enc(tmp, pos, 0x80 | ((c >> 6) & 0x3F)); + pos = enc(tmp, pos, 0x80 | ((c >> 0) & 0x3F)); + } + else + { + int ch = ((c & 0x03FF) << 10) | (elem[++idx] & 0x03FF); + ch += 0x10000; + pos = enc(tmp, pos, 0xF0 | ((ch >> 18) & 0x07)); + pos = enc(tmp, pos, 0x80 | ((ch >> 12) & 0x3F)); + pos = enc(tmp, pos, 0x80 | ((ch >> 6) & 0x3F)); + pos = enc(tmp, pos, 0x80 | ((ch >> 0) & 0x3F)); + } + } + else + pos = enc(tmp, pos, c); + } + + return tmp; + } + + private static final char[] hex = + {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; + + private static final int enc(char[] out, int pos, int c) + { + out[pos++] = '%'; + out[pos++] = hex[(c >> 4) & 0xf]; + out[pos++] = hex[c & 0xf]; + return pos; + } + + /** + * Unescape escaped characters (i.e. %xx) except reserved ones. + * + * @param str the string to unescape + * @param reserved the characters which may not be unescaped, or null + * @return the unescaped string + * @exception ParseException if the two digits following a `%' are + * not a valid hex number + */ + public static final String unescape(String str, BitSet reserved) + throws ParseException + { + if (str == null || str.indexOf('%') == -1) + return str; // an optimization + + char[] buf = str.toCharArray(); + char[] res = new char[buf.length]; + + char[] utf = new char[4]; + int utf_idx = 0, utf_len = -1; + int didx = 0; + for (int sidx=0; sidx buf.length) + throw new NumberFormatException(); + ch = Integer.parseInt(str.substring(sidx+1,sidx+3), 16); + if (ch < 0) + throw new NumberFormatException(); + sidx += 2; + } + catch (NumberFormatException e) + { + /* Hmm, people not reading specs again, so we just + * ignore it... + throw new ParseException(str.substring(sidx,sidx+3) + + " is an invalid code"); + */ + ch = buf[sidx]; + } + + // check if we're working on a utf-char + if (utf_len > 0) + { + if ((ch & 0xC0) != 0x80) // oops, we misinterpreted + { + didx = copyBuf(utf, utf_idx, ch, res, didx, reserved, false); + utf_len = -1; + } + else if (utf_idx == utf_len - 1) // end-of-char + { + if ((utf[0] & 0xE0) == 0xC0) + ch = (utf[0] & 0x1F) << 6 | + (ch & 0x3F); + else if ((utf[0] & 0xF0) == 0xE0) + ch = (utf[0] & 0x0F) << 12 | + (utf[1] & 0x3F) << 6 | + (ch & 0x3F); + else + ch = (utf[0] & 0x07) << 18 | + (utf[1] & 0x3F) << 12 | + (utf[2] & 0x3F) << 6 | + (ch & 0x3F); + if (reserved != null && reserved.get(ch)) + didx = copyBuf(utf, utf_idx, ch, res, didx, null, true); + else if (utf_len < 4) + res[didx++] = (char) ch; + else + { + ch -= 0x10000; + res[didx++] = (char) ((ch >> 10) | 0xD800); + res[didx++] = (char) ((ch & 0x03FF) | 0xDC00); + } + utf_len = -1; + } + else // continue + utf[utf_idx++] = (char) ch; + } + // check if this is the start of a utf-char + else if ((ch & 0xE0) == 0xC0 || (ch & 0xF0) == 0xE0 || + (ch & 0xF8) == 0xF0) + { + if ((ch & 0xE0) == 0xC0) + utf_len = 2; + else if ((ch & 0xF0) == 0xE0) + utf_len = 3; + else + utf_len = 4; + utf[0] = (char) ch; + utf_idx = 1; + } + // leave reserved alone + else if (reserved != null && reserved.get(ch)) + { + res[didx++] = buf[sidx]; + sidx -= 2; + } + // just use the decoded version + else + res[didx++] = (char) ch; + } + else if (utf_len > 0) // oops, we misinterpreted + { + didx = copyBuf(utf, utf_idx, buf[sidx], res, didx, reserved, false); + utf_len = -1; + } + else + res[didx++] = buf[sidx]; + } + if (utf_len > 0) // oops, we misinterpreted + didx = copyBuf(utf, utf_idx, -1, res, didx, reserved, false); + + return new String(res, 0, didx); + } + + private static final int copyBuf(char[] utf, int utf_idx, int ch, + char[] res, int didx, BitSet reserved, + boolean escapeAll) + { + if (ch >= 0) + utf[utf_idx++] = (char) ch; + + for (int idx=0; idx" + nl + + " rel-URI = <" + relURI + ">" + nl+ + " expected <" + result + ">" + nl+ + " but got <" + new URI(base, relURI) + ">"); + } + } + + private static void testEqual(String one, String two) throws Exception + { + URI u1 = new URI(one); + URI u2 = new URI(two); + + if (!u1.equals(u2)) + { + throw new Exception("Test failed: " + nl + + " <" + one + "> != <" + two + ">"); + } + if (u1.hashCode() != u2.hashCode()) + { + throw new Exception("Test failed: " + nl + + " hashCode <" + one + "> != hashCode <" + two + ">"); + } + } + + private static void testNotEqual(String one, String two) throws Exception + { + URI u1 = new URI(one); + URI u2 = new URI(two); + + if (u1.equals(u2)) + { + throw new Exception("Test failed: " + nl + + " <" + one + "> == <" + two + ">"); + } + } + + private static void testPE(URI base, String uri) throws Exception + { + boolean got_pe = false; + try + { new URI(base, uri); } + catch (ParseException pe) + { got_pe = true; } + if (!got_pe) + { + throw new Exception("Test failed: " + nl + + " <" + uri + "> should be invalid"); + } + } + + private static void testEscape(String raw, String escaped) throws Exception + { + String test = new String(escape(raw.toCharArray(), uricChar, true)); + if (!test.equals(escaped)) + throw new Exception("Test failed: " + nl + + " raw-string: " + raw + nl + + " escaped: " + test + nl + + " expected: " + escaped); + } + + private static void testUnescape(String escaped, String raw) + throws Exception + { + if (!unescape(escaped, null).equals(raw)) + throw new Exception("Test failed: " + nl + + " escaped-string: " + escaped + nl + + " unescaped: " + unescape(escaped, null) + nl + + " expected: " + raw); + } +} diff --git a/HTTPClient/UncompressInputStream.java b/HTTPClient/UncompressInputStream.java new file mode 100644 index 0000000..d6f6e8e --- /dev/null +++ b/HTTPClient/UncompressInputStream.java @@ -0,0 +1,451 @@ +/* + * @(#)UncompressInputStream.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.io.IOException; +import java.io.EOFException; +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.FilterInputStream; + + +/** + * This class decompresses an input stream containing data compressed with + * the unix "compress" utility (LZC, a LZW variant). This code is based + * heavily on the unlzw.c code in gzip-1.2.4 (written + * by Peter Jannesen) and the original compress code. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +class UncompressInputStream extends FilterInputStream +{ + /** + * @param is the input stream to decompress + * @exception IOException if the header is malformed + */ + public UncompressInputStream(InputStream is) throws IOException + { + super(is); + parse_header(); + } + + + byte[] one = new byte[1]; + public synchronized int read() throws IOException + { + int b = in.read(one, 0, 1); + if (b == 1) + return (one[0] & 0xff); + else + return -1; + } + + + // string table stuff + private static final int TBL_CLEAR = 0x100; + private static final int TBL_FIRST = TBL_CLEAR + 1; + + private int[] tab_prefix; + private byte[] tab_suffix; + private int[] zeros = new int[256]; + private byte[] stack; + + // various state + private boolean block_mode; + private int n_bits; + private int maxbits; + private int maxmaxcode; + private int maxcode; + private int bitmask; + private int oldcode; + private byte finchar; + private int stackp; + private int free_ent; + + // input buffer + private byte[] data = new byte[10000]; + private int bit_pos = 0, end = 0, got = 0; + private boolean eof = false; + private static final int EXTRA = 64; + + + public synchronized int read(byte[] buf, int off, int len) + throws IOException + { + if (eof) return -1; + int start = off; + + /* Using local copies of various variables speeds things up by as + * much as 30% ! + */ + int[] l_tab_prefix = tab_prefix; + byte[] l_tab_suffix = tab_suffix; + byte[] l_stack = stack; + int l_n_bits = n_bits; + int l_maxcode = maxcode; + int l_maxmaxcode = maxmaxcode; + int l_bitmask = bitmask; + int l_oldcode = oldcode; + byte l_finchar = finchar; + int l_stackp = stackp; + int l_free_ent = free_ent; + byte[] l_data = data; + int l_bit_pos = bit_pos; + + + // empty stack if stuff still left + + int s_size = l_stack.length - l_stackp; + if (s_size > 0) + { + int num = (s_size >= len) ? len : s_size ; + System.arraycopy(l_stack, l_stackp, buf, off, num); + off += num; + len -= num; + l_stackp += num; + } + + if (len == 0) + { + stackp = l_stackp; + return off-start; + } + + + // loop, filling local buffer until enough data has been decompressed + + main_loop: do + { + if (end < EXTRA) fill(); + + int bit_in = (got > 0) ? (end - end%l_n_bits)<<3 : + (end<<3)-(l_n_bits-1); + + while (l_bit_pos < bit_in) + { + // check for code-width expansion + + if (l_free_ent > l_maxcode) + { + int n_bytes = l_n_bits << 3; + l_bit_pos = (l_bit_pos-1) + + n_bytes - (l_bit_pos-1+n_bytes) % n_bytes; + + l_n_bits++; + l_maxcode = (l_n_bits==maxbits) ? l_maxmaxcode : + (1<>3; + int code = (((l_data[pos]&0xFF) | ((l_data[pos+1]&0xFF)<<8) | + ((l_data[pos+2]&0xFF)<<16)) + >> (l_bit_pos & 0x7)) & l_bitmask; + l_bit_pos += l_n_bits; + + + // handle first iteration + + if (l_oldcode == -1) + { + if (code >= 256) + throw new IOException("corrupt input: " + code + + " > 255"); + l_finchar = (byte) (l_oldcode = code); + buf[off++] = l_finchar; + len--; + continue; + } + + + // handle CLEAR code + + if (code == TBL_CLEAR && block_mode) + { + System.arraycopy(zeros, 0, l_tab_prefix, 0, zeros.length); + l_free_ent = TBL_FIRST - 1; + + int n_bytes = l_n_bits << 3; + l_bit_pos = (l_bit_pos-1) + + n_bytes - (l_bit_pos-1+n_bytes) % n_bytes; + l_n_bits = INIT_BITS; + l_maxcode = (1 << l_n_bits) - 1; + l_bitmask = l_maxcode; + + if (debug) System.err.println("Code tables reset"); + + l_bit_pos = resetbuf(l_bit_pos); + continue main_loop; + } + + + // setup + + int incode = code; + l_stackp = l_stack.length; + + + // Handle KwK case + + if (code >= l_free_ent) + { + if (code > l_free_ent) + throw new IOException("corrupt input: code=" + code + + ", free_ent=" + l_free_ent); + + l_stack[--l_stackp] = l_finchar; + code = l_oldcode; + } + + + // Generate output characters in reverse order + + while (code >= 256) + { + l_stack[--l_stackp] = l_tab_suffix[code]; + code = l_tab_prefix[code]; + } + l_finchar = l_tab_suffix[code]; + buf[off++] = l_finchar; + len--; + + + // And put them out in forward order + + s_size = l_stack.length - l_stackp; + int num = (s_size >= len) ? len : s_size ; + System.arraycopy(l_stack, l_stackp, buf, off, num); + off += num; + len -= num; + l_stackp += num; + + + // generate new entry in table + + if (l_free_ent < l_maxmaxcode) + { + l_tab_prefix[l_free_ent] = l_oldcode; + l_tab_suffix[l_free_ent] = l_finchar; + l_free_ent++; + } + + + // Remember previous code + + l_oldcode = incode; + + + // if output buffer full, then return + + if (len == 0) + { + n_bits = l_n_bits; + maxcode = l_maxcode; + bitmask = l_bitmask; + oldcode = l_oldcode; + finchar = l_finchar; + stackp = l_stackp; + free_ent = l_free_ent; + bit_pos = l_bit_pos; + + return off-start; + } + } + + l_bit_pos = resetbuf(l_bit_pos); + } while (got > 0); + + n_bits = l_n_bits; + maxcode = l_maxcode; + bitmask = l_bitmask; + oldcode = l_oldcode; + finchar = l_finchar; + stackp = l_stackp; + free_ent = l_free_ent; + bit_pos = l_bit_pos; + + eof = true; + return off-start; + } + + + /** + * Moves the unread data in the buffer to the beginning and resets + * the pointers. + */ + private final int resetbuf(int bit_pos) + { + int pos = bit_pos >> 3; + System.arraycopy(data, pos, data, 0, end-pos); + end -= pos; + return 0; + } + + + private final void fill() throws IOException + { + got = in.read(data, end, data.length-1-end); + if (got > 0) end += got; + } + + + public synchronized long skip(long num) throws IOException + { + byte[] tmp = new byte[(int) num]; + int got = read(tmp, 0, (int) num); + + if (got > 0) + return (long) got; + else + return 0L; + } + + + public synchronized int available() throws IOException + { + if (eof) return 0; + + return in.available(); + } + + + private static final int LZW_MAGIC = 0x1f9d; + private static final int MAX_BITS = 16; + private static final int INIT_BITS = 9; + private static final int HDR_MAXBITS = 0x1f; + private static final int HDR_EXTENDED = 0x20; + private static final int HDR_FREE = 0x40; + private static final int HDR_BLOCK_MODE = 0x80; + + private void parse_header() throws IOException + { + // read in and check magic number + + int t = in.read(); + if (t < 0) throw new EOFException("Failed to read magic number"); + int magic = (t & 0xff) << 8; + t = in.read(); + if (t < 0) throw new EOFException("Failed to read magic number"); + magic += t & 0xff; + if (magic != LZW_MAGIC) + throw new IOException("Input not in compress format (read " + + "magic number 0x" + + Integer.toHexString(magic) + ")"); + + + // read in header byte + + int header = in.read(); + if (header < 0) throw new EOFException("Failed to read header"); + + block_mode = (header & HDR_BLOCK_MODE) > 0; + maxbits = header & HDR_MAXBITS; + + if (maxbits > MAX_BITS) + throw new IOException("Stream compressed with " + maxbits + + " bits, but can only handle " + MAX_BITS + + " bits"); + + if ((header & HDR_EXTENDED) > 0) + throw new IOException("Header extension bit set"); + + if ((header & HDR_FREE) > 0) + throw new IOException("Header bit 6 set"); + + if (debug) + { + System.err.println("block mode: " + block_mode); + System.err.println("max bits: " + maxbits); + } + + + // initialize stuff + + maxmaxcode = 1 << maxbits; + n_bits = INIT_BITS; + maxcode = (1 << n_bits) - 1; + bitmask = maxcode; + oldcode = -1; + finchar = 0; + free_ent = block_mode ? TBL_FIRST : 256; + + tab_prefix = new int[1 << maxbits]; + tab_suffix = new byte[1 << maxbits]; + stack = new byte[1 << maxbits]; + stackp = stack.length; + + for (int idx=255; idx>=0; idx--) + tab_suffix[idx] = (byte) idx; + } + + + private static final boolean debug = false; + + public static void main (String args[]) throws Exception + { + if (args.length != 1) + { + System.err.println("Usage: UncompressInputStream "); + System.exit(1); + } + + InputStream in = + new UncompressInputStream(new FileInputStream(args[0])); + + byte[] buf = new byte[100000]; + int tot = 0; + long beg = System.currentTimeMillis(); + + while (true) + { + int got = in.read(buf); + if (got < 0) break; + System.out.write(buf, 0, got); + tot += got; + } + + long end = System.currentTimeMillis(); + System.err.println("Decompressed " + tot + " bytes"); + System.err.println("Time: " + (end-beg)/1000. + " seconds"); + } +} diff --git a/HTTPClient/Util.java b/HTTPClient/Util.java new file mode 100644 index 0000000..d4d8bcd --- /dev/null +++ b/HTTPClient/Util.java @@ -0,0 +1,1131 @@ +/* + * @(#)Util.java 0.3-3 06/05/2001 + * + * This file is part of the HTTPClient package + * Copyright (C) 1996-2001 Ronald Tschalär + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307, USA + * + * For questions, suggestions, bug-reports, enhancement-requests etc. + * I may be contacted at: + * + * ronald@innovation.ch + * + * The HTTPClient's home page is located at: + * + * http://www.innovation.ch/java/HTTPClient/ + * + */ + +package HTTPClient; + +import java.lang.reflect.Array; +import java.net.URL; +import java.util.Date; +import java.util.BitSet; +import java.util.Locale; +import java.util.Vector; +import java.util.Hashtable; +import java.util.SimpleTimeZone; +import java.util.StringTokenizer; +import java.text.DateFormat; +import java.text.SimpleDateFormat; + + +/** + * This class holds various utility methods. + * + * @version 0.3-3 06/05/2001 + * @author Ronald Tschalär + */ +public class Util +{ + private static final BitSet Separators = new BitSet(128); + private static final BitSet TokenChar = new BitSet(128); + private static final BitSet UnsafeChar = new BitSet(128); + private static DateFormat http_format; + private static DateFormat parse_1123; + private static DateFormat parse_850; + private static DateFormat parse_asctime; + private static final Object http_format_lock = new Object(); + private static final Object http_parse_lock = new Object(); + + static + { + // rfc-2616 tspecial + Separators.set('('); + Separators.set(')'); + Separators.set('<'); + Separators.set('>'); + Separators.set('@'); + Separators.set(','); + Separators.set(';'); + Separators.set(':'); + Separators.set('\\'); + Separators.set('"'); + Separators.set('/'); + Separators.set('['); + Separators.set(']'); + Separators.set('?'); + Separators.set('='); + Separators.set('{'); + Separators.set('}'); + Separators.set(' '); + Separators.set('\t'); + + // rfc-2616 token + for (int ch=32; ch<127; ch++) TokenChar.set(ch); + TokenChar.xor(Separators); + + // rfc-1738 unsafe characters, including CTL and SP, and excluding + // "#" and "%" + for (int ch=0; ch<32; ch++) UnsafeChar.set(ch); + UnsafeChar.set(' '); + UnsafeChar.set('<'); + UnsafeChar.set('>'); + UnsafeChar.set('"'); + UnsafeChar.set('{'); + UnsafeChar.set('}'); + UnsafeChar.set('|'); + UnsafeChar.set('\\'); + UnsafeChar.set('^'); + UnsafeChar.set('~'); + UnsafeChar.set('['); + UnsafeChar.set(']'); + UnsafeChar.set('`'); + UnsafeChar.set(127); + + // rfc-1123 date format (restricted to GMT, as per rfc-2616) + /* This initialization has been moved to httpDate() because it + * takes an awfully long time and is often not needed + * + http_format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", + Locale.US); + http_format.setTimeZone(new SimpleTimeZone(0, "GMT")); + */ + } + + + // Constructors + + /** + * This class isn't meant to be instantiated. + */ + private Util() {} + + + // Methods + + final static Object[] resizeArray(Object[] src, int new_size) + { + Class compClass = src.getClass().getComponentType(); + Object tmp[] = (Object[]) Array.newInstance(compClass, new_size); + System.arraycopy(src, 0, tmp, 0, + (src.length < new_size ? src.length : new_size)); + return tmp; + } + + final static NVPair[] resizeArray(NVPair[] src, int new_size) + { + NVPair tmp[] = new NVPair[new_size]; + System.arraycopy(src, 0, tmp, 0, + (src.length < new_size ? src.length : new_size)); + return tmp; + } + + final static AuthorizationInfo[] resizeArray(AuthorizationInfo[] src, + int new_size) + { + AuthorizationInfo tmp[] = new AuthorizationInfo[new_size]; + System.arraycopy(src, 0, tmp, 0, + (src.length < new_size ? src.length : new_size)); + return tmp; + } + + final static Cookie[] resizeArray(Cookie[] src, int new_size) + { + Cookie tmp[] = new Cookie[new_size]; + System.arraycopy(src, 0, tmp, 0, + (src.length < new_size ? src.length : new_size)); + return tmp; + } + + final static String[] resizeArray(String[] src, int new_size) + { + String tmp[] = new String[new_size]; + System.arraycopy(src, 0, tmp, 0, + (src.length < new_size ? src.length : new_size)); + return tmp; + } + + final static boolean[] resizeArray(boolean[] src, int new_size) + { + boolean tmp[] = new boolean[new_size]; + System.arraycopy(src, 0, tmp, 0, + (src.length < new_size ? src.length : new_size)); + return tmp; + } + + final static byte[] resizeArray(byte[] src, int new_size) + { + byte tmp[] = new byte[new_size]; + System.arraycopy(src, 0, tmp, 0, + (src.length < new_size ? src.length : new_size)); + return tmp; + } + + final static char[] resizeArray(char[] src, int new_size) + { + char tmp[] = new char[new_size]; + System.arraycopy(src, 0, tmp, 0, + (src.length < new_size ? src.length : new_size)); + return tmp; + } + + final static int[] resizeArray(int[] src, int new_size) + { + int tmp[] = new int[new_size]; + System.arraycopy(src, 0, tmp, 0, + (src.length < new_size ? src.length : new_size)); + return tmp; + } + + + /** + * Split a property into an array of Strings, using "|" as the + * separator. + */ + static String[] splitProperty(String prop) + { + if (prop == null) return new String[0]; + + StringTokenizer tok = new StringTokenizer(prop, "|"); + String[] list = new String[tok.countTokens()]; + for (int idx=0; idx cmp[1]) + { + cmp[4] = cmp[2]; + cmp[5] = cmp[3]; + cmp[2] = cmp[0]; + cmp[3] = cmp[1]; + cmp[0] = idx; + cmp[1] = end - idx; + } + else if ((end-idx) > cmp[3]) + { + cmp[4] = cmp[2]; + cmp[5] = cmp[3]; + cmp[2] = idx; + cmp[3] = end - idx; + } + else if ((end-idx) > cmp[3]) + { + cmp[4] = idx; + cmp[5] = end - idx; + } + } + } + + cmp[1] += cmp[0]; + cmp[3] += cmp[2]; + cmp[5] += cmp[4]; + return cmp; + } + + /** + * Search for a string. Use compile_search() to first generate the second + * argument. This uses a Knuth-Morris-Pratt like algorithm. + * + * @param search the string to search for. + * @param cmp the the array returned by compile_search. + * @param str the string in which to look for search. + * @param beg the position at which to start the search in + * str. + * @param end the position at which to end the search in str, + * noninclusive. + * @return the position in str where search was + * found, or -1 if not found. + */ + final static int findStr(byte[] search, int[] cmp, byte[] str, + int beg, int end) + { + int c1f = cmp[0], + c1l = cmp[1], + d1 = c1l - c1f, + c2f = cmp[2], + c2l = cmp[3], + d2 = c2l - c2f, + c3f = cmp[4], + c3l = cmp[5], + d3 = c3l - c3f; + + Find: while (beg+search.length <= end) + { + if (search[c1l] == str[beg+c1l]) + { + /* This is correct, but Visual J++ can't cope with it... + Comp: if (search[c1f] == str[beg+c1f]) + { + for (int idx=0; idx end) + return -1; + else + return beg; + } + + + /** + * Replace quoted characters by their unquoted version. Quoted characters + * are characters preceded by a slash. E.g. "\c" would be replaced by "c". + * This is used in parsing http headers where quoted-characters are + * allowed in quoted-strings and often used to quote the quote character + * <">. + * + * @param str the string do dequote + * @return the string do with all quoted characters replaced by their + * true value. + */ + public final static String dequoteString(String str) + { + if (str.indexOf('\\') == -1) return str; + + char[] buf = str.toCharArray(); + int pos = 0, num_deq = 0; + while (pos < buf.length) + { + if (buf[pos] == '\\' && pos+1 < buf.length) + { + System.arraycopy(buf, pos+1, buf, pos, buf.length-pos-1); + num_deq++; + } + pos++; + } + + return new String(buf, 0, buf.length-num_deq); + } + + + /** + * Replace given characters by their quoted version. Quoted characters + * are characters preceded by a slash. E.g. "c" would be replaced by "\c". + * This is used in generating http headers where certain characters need + * to be quoted, such as the quote character <">. + * + * @param str the string do quote + * @param qlist the list of characters to quote + * @return the string do with all characters replaced by their + * quoted version. + */ + public final static String quoteString(String str, String qlist) + { + char[] list = qlist.toCharArray(); + int idx; + for (idx=0; idxHttpHeaderElement. + * @exception ParseException if the syntax rules are violated. + */ + public final static Vector parseHeader(String header) throws ParseException + { + return parseHeader(header, true); + } + + + /** + * This parses the value part of a header. The result is a Vector of + * HttpHeaderElement's. The syntax the header must conform to is: + * + *

    +     * header  = [ element ] *( "," [ element ] )
    +     * element = name [ "=" [ value ] ] *( ";" [ param ] )
    +     * param   = name [ "=" [ value ] ]
    +     * 
    +     * name    = token
    +     * value   = ( token | quoted-string )
    +     * 
    +     * token         = 1*<any char except "=", ",", ";", <"> and
    +     *                       white space>
    +     * quoted-string = <"> *( text | quoted-char ) <">
    +     * text          = any char except <">
    +     * quoted-char   = "\" char
    +     * 
    + * + * Any amount of white space is allowed between any part of the header, + * element or param and is ignored. A missing value in any element or + * param will be stored as the empty string; if the "=" is also missing + * null will be stored instead. + * + * @param header the value part of the header. + * @param dequote if true all quoted strings are dequoted. + * @return a Vector containing all the elements; each entry is an + * instance of HttpHeaderElement. + * @exception ParseException if the above syntax rules are violated. + * @see HTTPClient.HttpHeaderElement + */ + public final static Vector parseHeader(String header, boolean dequote) + throws ParseException + { + if (header == null) return null; + char[] buf = header.toCharArray(); + Vector elems = new Vector(); + boolean first = true; + int beg = -1, end = 0, len = buf.length, abeg[] = new int[1]; + String elem_name, elem_value; + + + elements: while (true) + { + if (!first) // find required "," + { + beg = skipSpace(buf, end); + if (beg == len) break; + if (buf[beg] != ',') + throw new ParseException("Bad header format: '" + header + + "'\nExpected \",\" at position " + + beg); + } + first = false; + + beg = skipSpace(buf, beg+1); + if (beg == len) break elements; + if (buf[beg] == ',') // skip empty elements + { + end = beg; + continue elements; + } + + if (buf[beg] == '=' || buf[beg] == ';' || buf[beg] == '"') + throw new ParseException("Bad header format: '" + header + + "'\nEmpty element name at position " + + beg); + + end = beg+1; // extract element name + while (end < len && !Character.isWhitespace(buf[end]) && + buf[end] != '=' && buf[end] != ',' && buf[end] != ';') + end++; + elem_name = new String(buf, beg, end-beg); + + beg = skipSpace(buf, end); + if (beg < len && buf[beg] == '=') // element value + { + abeg[0] = beg+1; + elem_value = parseValue(buf, abeg, header, dequote); + end = abeg[0]; + } + else + { + elem_value = null; + end = beg; + } + + NVPair[] params = new NVPair[0]; + params: while (true) + { + String param_name, param_value; + + beg = skipSpace(buf, end); // expect ";" + if (beg == len || buf[beg] != ';') + break params; + + beg = skipSpace(buf, beg+1); + if (beg == len || buf[beg] == ',') + { + end = beg; + break params; + } + if (buf[beg] == ';') // skip empty parameters + { + end = beg; + continue params; + } + + if (buf[beg] == '=' || buf[beg] == '"') + throw new ParseException("Bad header format: '" + header + + "'\nEmpty parameter name at position "+ + beg); + + end = beg+1; // extract param name + while (end < len && !Character.isWhitespace(buf[end]) && + buf[end] != '=' && buf[end] != ',' && buf[end] != ';') + end++; + param_name = new String(buf, beg, end-beg); + + beg = skipSpace(buf, end); + if (beg < len && buf[beg] == '=') // element value + { + abeg[0] = beg+1; + param_value = parseValue(buf, abeg, header, dequote); + end = abeg[0]; + } + else + { + param_value = null; + end = beg; + } + + params = Util.resizeArray(params, params.length+1); + params[params.length-1] = new NVPair(param_name, param_value); + } + + elems.addElement( + new HttpHeaderElement(elem_name, elem_value, params)); + } + + return elems; + } + + + /** + * Parse the value part. Accepts either token or quoted string. + */ + private static String parseValue(char[] buf, int[] abeg, String header, + boolean dequote) + throws ParseException + { + int beg = abeg[0], end = beg, len = buf.length; + String value; + + + beg = skipSpace(buf, beg); + + if (beg < len && buf[beg] == '"') // it's a quoted-string + { + beg++; + end = beg; + char[] deq_buf = null; + int deq_pos = 0, lst_pos = beg; + + while (end < len && buf[end] != '"') + { + if (buf[end] == '\\') + { + if (dequote) // dequote char + { + if (deq_buf == null) + deq_buf = new char[buf.length]; + System.arraycopy(buf, lst_pos, deq_buf, deq_pos, + end-lst_pos); + deq_pos += end-lst_pos; + lst_pos = ++end; + } + else + end++; // skip quoted char + } + + end++; + } + if (end == len) + throw new ParseException("Bad header format: '" + header + + "'\nClosing <\"> for quoted-string"+ + " starting at position " + + (beg-1) + " not found"); + if (deq_buf != null) + { + System.arraycopy(buf, lst_pos, deq_buf, deq_pos, end-lst_pos); + deq_pos += end-lst_pos; + value = new String(deq_buf, 0, deq_pos); + } + else + value = new String(buf, beg, end-beg); + end++; + } + else // it's a simple token value + { + end = beg; + while (end < len && !Character.isWhitespace(buf[end]) && + buf[end] != ',' && buf[end] != ';') + end++; + + value = new String(buf, beg, end-beg); + } + + abeg[0] = end; + return value; + } + + + /** + * Determines if the given header contains a certain token. The header + * must conform to the rules outlined in parseHeader(). + * + * @see #parseHeader(java.lang.String) + * @param header the header value. + * @param token the token to find; the match is case-insensitive. + * @return true if the token is present, false otherwise. + * @exception ParseException if this is thrown parseHeader(). + */ + public final static boolean hasToken(String header, String token) + throws ParseException + { + if (header == null) + return false; + else + return parseHeader(header).contains(new HttpHeaderElement(token)); + } + + + /** + * Get the HttpHeaderElement with the name name. + * + * @param header a vector of HttpHeaderElement's, such as is returned + * from parseHeader() + * @param name the name of element to retrieve; matching is + * case-insensitive + * @return the request element, or null if none found. + * @see #parseHeader(java.lang.String) + */ + public final static HttpHeaderElement getElement(Vector header, String name) + { + int idx = header.indexOf(new HttpHeaderElement(name)); + if (idx == -1) + return null; + else + return (HttpHeaderElement) header.elementAt(idx); + } + + + /** + * retrieves the value associated with the parameter param in + * a given header string. It parses the header using + * parseHeader() and then searches the first element for the + * given parameter. This is used especially in headers like + * 'Content-type' and 'Content-Disposition'. + * + *

    quoted characters ("\x") in a quoted string are dequoted. + * + * @see #parseHeader(java.lang.String) + * @param param the parameter name + * @param hdr the header value + * @return the value for this parameter, or null if not found. + * @exception ParseException if the above syntax rules are violated. + */ + public final static String getParameter(String param, String hdr) + throws ParseException + { + NVPair[] params = ((HttpHeaderElement) parseHeader(hdr).firstElement()). + getParams(); + + for (int idx=0; idxjava.net.URL.sameFile() is broken (an explicit port 80 + * doesn't compare equal to an implicit port, and it doesn't take + * escapes into account). + * + *

    Two http urls are considered equal if they have the same protocol + * (case-insensitive match), the same host (case-insensitive), the + * same port and the same file (after decoding escaped characters). + * + * @param url1 the first url + * @param url1 the second url + * @return true if url1 and url2 compare equal + */ + public final static boolean sameHttpURL(URL url1, URL url2) + { + if (!url1.getProtocol().equalsIgnoreCase(url2.getProtocol())) + return false; + + if (!url1.getHost().equalsIgnoreCase(url2.getHost())) + return false; + + int port1 = url1.getPort(), port2 = url2.getPort(); + if (port1 == -1) port1 = URI.defaultPort(url1.getProtocol()); + if (port2 == -1) port2 = URI.defaultPort(url1.getProtocol()); + if (port1 != port2) + return false; + + try + { return URI.unescape(url1.getFile(), null).equals(URI.unescape(url2.getFile(), null)); } + catch (ParseException pe) + { return url1.getFile().equals(url2.getFile());} + } + + + /** + * Return the default port used by a given protocol. + * + * @param protocol the protocol + * @return the port number, or 0 if unknown + * @deprecated use URI.defaultPort() instead + * @see HTTPClient.URI#defaultPort(java.lang.String) + */ + public final static int defaultPort(String protocol) + { + return URI.defaultPort(protocol); + } + + + /** + * Parse the http date string. java.util.Date will do this fine, but + * is deprecated, so we use SimpleDateFormat instead. + * + * @param dstr the date string to parse + * @return the Date object + */ + final static Date parseHttpDate(String dstr) + { + synchronized (http_parse_lock) + { + if (parse_1123 == null) + setupParsers(); + } + + try + { return parse_1123.parse(dstr); } + catch (java.text.ParseException pe) + { } + try + { return parse_850.parse(dstr); } + catch (java.text.ParseException pe) + { } + try + { return parse_asctime.parse(dstr); } + catch (java.text.ParseException pe) + { throw new IllegalArgumentException(pe.toString()); } + } + + private static final void setupParsers() + { + parse_1123 = + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US); + parse_850 = + new SimpleDateFormat("EEEE, dd-MMM-yy HH:mm:ss 'GMT'", Locale.US); + parse_asctime = + new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy", Locale.US); + + parse_1123.setTimeZone(new SimpleTimeZone(0, "GMT")); + parse_850.setTimeZone(new SimpleTimeZone(0, "GMT")); + parse_asctime.setTimeZone(new SimpleTimeZone(0, "GMT")); + + parse_1123.setLenient(true); + parse_850.setLenient(true); + parse_asctime.setLenient(true); + } + + /** + * This returns a string containing the date and time in date + * formatted according to a subset of RFC-1123. The format is defined in + * the HTTP/1.0 spec (RFC-1945), section 3.3, and the HTTP/1.1 spec + * (RFC-2616), section 3.3.1. Note that Date.toGMTString() is close, but + * is missing the weekday and supresses the leading zero if the day is + * less than the 10th. Instead we use the SimpleDateFormat class. + * + *

    Some versions of JDK 1.1.x are bugged in that their GMT uses + * daylight savings time... Therefore we use our own timezone + * definitions. + * + * @param date the date and time to be converted + * @return a string containg the date and time as used in http + */ + public static final String httpDate(Date date) + { + synchronized (http_format_lock) + { + if (http_format == null) + setupFormatter(); + } + + return http_format.format(date); + } + + private static final void setupFormatter() + { + http_format = + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US); + http_format.setTimeZone(new SimpleTimeZone(0, "GMT")); + } + + + /** + * Escape unsafe characters in a path. + * + * @param path the original path + * @return the path with all unsafe characters escaped + */ + final static String escapeUnsafeChars(String path) + { + int len = path.length(); + char[] buf = new char[3*len]; + + int dst = 0; + for (int src=0; src= 128 || UnsafeChar.get(ch)) + { + buf[dst++] = '%'; + buf[dst++] = hex_map[(ch & 0xf0) >>> 4]; + buf[dst++] = hex_map[ch & 0x0f]; + } + else + buf[dst++] = ch; + } + + if (dst > len) + return new String(buf, 0, dst); + else + return path; + } + + static final char[] hex_map = + {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; + + + /** + * Extract the path from an http resource. + * + *

    The "resource" part of an HTTP URI can contain a number of parts, + * some of which are not always of interest. These methods here will + * extract the various parts, assuming the following syntanx (taken from + * RFC-2616): + * + *

    +     * resource = [ "/" ] [ path ] [ ";" params ] [ "?" query ] [ "#" fragment ]
    +     * 
    + * + * @param the resource to split + * @return the path, including any leading "/" + * @see #getParams + * @see #getQuery + * @see #getFragment + */ + public final static String getPath(String resource) + { + int p, end = resource.length(); + if ((p = resource.indexOf('#')) != -1) // find fragment + end = p; + if ((p = resource.indexOf('?')) != -1 && p < end) // find query + end = p; + if ((p = resource.indexOf(';')) != -1 && p < end) // find params + end = p; + return resource.substring(0, end); + } + + + /** + * Extract the params part from an http resource. + * + * @param the resource to split + * @return the params, or null if there are none + * @see #getPath + */ + public final static String getParams(String resource) + { + int beg, f, q; + if ((beg = resource.indexOf(';')) == -1) // find params + return null; + if ((f = resource.indexOf('#')) != -1 && f < beg) // find fragment + return null; + if ((q = resource.indexOf('?')) != -1 && q < beg) // find query + return null; + if (q == -1 && f == -1) + return resource.substring(beg+1); + if (f == -1 || (q != -1 && q < f)) + return resource.substring(beg+1, q); + else + return resource.substring(beg+1, f); + } + + + /** + * Extract the query string from an http resource. + * + * @param the resource to split + * @return the query, or null if there was none + * @see #getPath + */ + public final static String getQuery(String resource) + { + int beg, f; + if ((beg = resource.indexOf('?')) == -1) // find query + return null; + if ((f = resource.indexOf('#')) != -1 && f < beg) // find fragment + return null; // '?' is in fragment + if (f == -1) + return resource.substring(beg+1); // no fragment + else + return resource.substring(beg+1, f); // strip fragment + } + + + /** + * Extract the fragment part from an http resource. + * + * @param the resource to split + * @return the fragment, or null if there was none + * @see #getPath + */ + public final static String getFragment(String resource) + { + int beg; + if ((beg = resource.indexOf('#')) == -1) // find fragment + return null; + else + return resource.substring(beg+1); + } + + + /** + * Match pattern against name, where + * pattern may contain wildcards ('*'). + * + * @param pattern the pattern to match; may contain '*' which match + * any number (0 or more) of any character (think file + * globbing) + * @param name the name to match against the pattern + * @return true if the name matches the pattern; false otherwise + */ + public static final boolean wildcardMatch(String pattern, String name) + { + return + wildcardMatch(pattern, name, 0, 0, pattern.length(), name.length()); + } + + private static final boolean wildcardMatch(String pattern, String name, + int ppos, int npos, int plen, + int nlen) + { + // find wildcard + int star = pattern.indexOf('*', ppos); + if (star < 0) + { + return ((plen-ppos) == (nlen-npos) && + pattern.regionMatches(ppos, name, npos, plen-ppos)); + } + + // match prefix + if (!pattern.regionMatches(ppos, name, npos, star-ppos)) + return false; + + // match suffix + if (star == plen-1) + return true; + while(!wildcardMatch(pattern, name, star+1, npos, plen, nlen) && + npos < nlen) + npos++; + return (npos < nlen); + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2174049 --- /dev/null +++ b/LICENSE @@ -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. +^L + 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.) +^L +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. +^L + 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. +^L + 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. + + + Copyright (C) 19yy + + 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) 19yy 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. + + , 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. \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..4efdb1b --- /dev/null +++ b/README @@ -0,0 +1,87 @@ +Gallery Remote Readme + +CONTENTS: + 1. What is Gallery Remote? + 2. What is Gallery? + 3. Using Gallery Remote + 4. Can I run Gallery Remote on my non-Windows OS? + 5. Known problems + 6. HELP!! Where do I go for Help? + +1 WHAT IS GALLERY REMOTE? + +Gallery Remote is a Desktop file upload utility for Gallery. + + +2. WHAT IS GALLERY? + +Gallery is a web based photo album viewer/creator. For the latest +information, check out the web site: + + http://gallery.sourceforge.net/ + + +3. USING GALLERY REMOTE + +You're on your own here, but it's pretty straightforward. + + +4. CAN I RUN GALLERY REMOTE ON MY NON-WINDOWS OS? + +Yes, maybe. Gallery Remote was written in Java. If you have Java 1.3 or later +installed on your machine, you may be able to run the app with the command: + + java -cp GalleryUp.jar com.gallery.GalleryUp.GalleryUp + +assuming you are in the same directory as the GalleryUp.jar file. + + +5. KNOWN PROBLEMS + +There are many, I am sure. + + +6. HELP!! WHERE DO I GO FOR HELP? + +We're here to help you. We want you to get Gallery up and running. +But please remember that Gallery is installed on many websites and +the Gallery authors get bombarded with lots of emails every day. +Be kind to us and try to use the resources we make available before +you fire off an email to us. + +The first thing you should do is to read the Gallery Frequently +Asked Questions page, which is found here: + + http://gallery.sourceforge.net/faq.php + +If you can't find your answer there, send your help requests to the +mailing lists. + +The Gallery users mailing list is here: + + http://lists.sourceforge.net/lists/listinfo/gallery-users + +there's an archive of all the emails for the past few months here: + + http://marc.theaimsgroup.com/?l=gallery-users&r=1&w=2 + +We respond to each and every request for help. There are very few +problems that we have not been able to work through. We *will* get +you up and running. But, remember that every email we respond to robs +us of time that we could be spending improving the quality of Gallery +and adding new features. + +Speaking of quality and features, if you find a bug you can file +it in our Bug Tracker: + + http://sourceforge.net/tracker/?group_id=7130&atid=107130 + +and if you think of a new feature, you can file it in our Feature +Tracker: + + http://sourceforge.net/tracker/?atid=357130&group_id=7130&func=browse + +It's a good idea to check through these databases to see if the +bug/feature that you're referring to is already in there. + +THE END. \ No newline at end of file diff --git a/com/gallery/GalleryRemote/AboutBox.java b/com/gallery/GalleryRemote/AboutBox.java new file mode 100644 index 0000000..d33e6fa --- /dev/null +++ b/com/gallery/GalleryRemote/AboutBox.java @@ -0,0 +1,142 @@ +/* + * Gallery Remote - a File Upload Utility for Gallery + * + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2001 Bharat Mediratta + * + * 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. + */ + +package com.gallery.GalleryRemote; + +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; + +public class AboutBox extends javax.swing.JFrame { + +// IMPORTANT: Source code between BEGIN/END comment pair will be regenerated +// every time the form is saved. All manual changes will be overwritten. +// BEGIN GENERATED CODE + // member declarations + javax.swing.JLabel jLabel1 = new javax.swing.JLabel(); + javax.swing.JLabel jLabel2 = new javax.swing.JLabel(); + javax.swing.JLabel jLabelVersion = new javax.swing.JLabel(); + javax.swing.JLabel jLabel3 = new javax.swing.JLabel(); + javax.swing.JLabel jLabel4 = new javax.swing.JLabel(); + javax.swing.JLabel jLabel5 = new javax.swing.JLabel(); +// END GENERATED CODE + + public AboutBox() { + } + + + public void setVersionString(String versionString) { + jLabelVersion.setText(versionString); + } + + public void initComponents() throws Exception { +// IMPORTANT: Source code between BEGIN/END comment pair will be regenerated +// every time the form is saved. All manual changes will be overwritten. +// BEGIN GENERATED CODE + // the following code sets the frame's initial state + + jLabel1.setSize(new java.awt.Dimension(320, 50)); + jLabel1.setLocation(new java.awt.Point(30, 30)); + jLabel1.setVisible(true); + jLabel1.setVerticalAlignment(javax.swing.JLabel.TOP); + jLabel1.setText("Gallery Remote"); + jLabel1.setFont(new java.awt.Font("Dialog", 1, 36)); + + jLabel2.setSize(new java.awt.Dimension(104, 20)); + jLabel2.setLocation(new java.awt.Point(10, 80)); + jLabel2.setVisible(true); + jLabel2.setVerticalAlignment(javax.swing.JLabel.TOP); + jLabel2.setText("Version:"); + jLabel2.setHorizontalAlignment(javax.swing.JLabel.RIGHT); + + jLabelVersion.setSize(new java.awt.Dimension(130, 20)); + jLabelVersion.setLocation(new java.awt.Point(120, 80)); + jLabelVersion.setVisible(true); + jLabelVersion.setVerticalAlignment(javax.swing.JLabel.TOP); + jLabelVersion.setText("00000"); + + jLabel3.setSize(new java.awt.Dimension(160, 20)); + jLabel3.setLocation(new java.awt.Point(250, 170)); + jLabel3.setVisible(true); + jLabel3.setText("© 2001 Chris Smith"); + jLabel3.setHorizontalAlignment(javax.swing.JLabel.RIGHT); + + jLabel4.setSize(new java.awt.Dimension(250, 20)); + jLabel4.setLocation(new java.awt.Point(160, 190)); + jLabel4.setVisible(true); + jLabel4.setText("A part of the Gallery Open Source Project"); + jLabel4.setHorizontalAlignment(javax.swing.JLabel.RIGHT); + + jLabel5.setSize(new java.awt.Dimension(250, 20)); + jLabel5.setLocation(new java.awt.Point(160, 210)); + jLabel5.setVisible(true); + jLabel5.setText("http://gallery.sourceforge.net"); + jLabel5.setHorizontalAlignment(javax.swing.JLabel.RIGHT); + + setLocation(new java.awt.Point(0, 0)); + setSize(new java.awt.Dimension(428, 263)); + setBackground(java.awt.Color.white); + getContentPane().setLayout(null); + setTitle("com.gallery.GalleryRemote.AboutBox"); + getContentPane().add(jLabel1); + getContentPane().add(jLabel2); + getContentPane().add(jLabelVersion); + getContentPane().add(jLabel3); + getContentPane().add(jLabel4); + getContentPane().add(jLabel5); + + + addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent e) { + thisWindowClosing(e); + } + }); + +// END GENERATED CODE + } + + private boolean mShown = false; + + public void addNotify() { + super.addNotify(); + + if (mShown) + return; + + // resize frame to account for menubar + JMenuBar jMenuBar = getJMenuBar(); + if (jMenuBar != null) { + int jMenuBarHeight = jMenuBar.getPreferredSize().height; + Dimension dimension = getSize(); + dimension.height += jMenuBarHeight; + setSize(dimension); + } + + mShown = true; + } + + // Close the window when the close box is clicked + void thisWindowClosing(java.awt.event.WindowEvent e) { + setVisible(false); + dispose(); + } + +} diff --git a/com/gallery/GalleryRemote/AddFileDialog.java b/com/gallery/GalleryRemote/AddFileDialog.java new file mode 100644 index 0000000..6b64e6c --- /dev/null +++ b/com/gallery/GalleryRemote/AddFileDialog.java @@ -0,0 +1,61 @@ +/* + * Gallery Remote - a File Upload Utility for Gallery + * + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2001 Bharat Mediratta + * + * 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. + */ + +package com.gallery.GalleryRemote; + +import java.awt.Component; +import java.util.*; +import javax.swing.JFileChooser; +import java.io.File; + +public class AddFileDialog { + + + static File[] addFiles(Component parent) { + + JFileChooser fc = new JFileChooser(); + + fc.addChoosableFileFilter(new GalleryFileFilter()); + fc.setFileSelectionMode(JFileChooser.FILES_ONLY); + fc.setMultiSelectionEnabled(true); + + int retval = fc.showDialog(parent, "Add"); + if (retval != JFileChooser.CANCEL_OPTION) { + + File[] files = fc.getSelectedFiles(); + + //Iterator iter = Arrays.asList(files).iterator(); + //while (iter.hasNext()) { + // File file = (File) iter.next(); + // System.out.println(file.getAbsolutePath()); + //} + + return files; + + } + + + return null; + } + + + +} diff --git a/com/gallery/GalleryRemote/GalleryComm.java b/com/gallery/GalleryRemote/GalleryComm.java new file mode 100644 index 0000000..9571f4d --- /dev/null +++ b/com/gallery/GalleryRemote/GalleryComm.java @@ -0,0 +1,358 @@ +/* + * Gallery Remote - a File Upload Utility for Gallery + * + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2001 Bharat Mediratta + * + * 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. + */ + +package com.gallery.GalleryRemote; + +import HTTPClient.*; +import java.util.*; +import java.io.*; +import java.net.*; +import javax.swing.*; + +public class GalleryComm { + + private static final String PROTOCAL_VERSION = "1"; + private static final String SCRIPT_NAME = "gallery_remote.php"; + + private String mURLString = new String(); + private String mUsername = new String(); + private String mPassword = new String(); + private String mAlbum = new String(); + + private ArrayList mFileList; + private ArrayList mAlbumList; + + private String mStatus; + private int mUploadedCount; + + private boolean mLoggedIn = false; + private boolean mDone = false; + + private HTTPConnection mConnection; + + public GalleryComm() { + + //-- our policy handler accepts all cookies --- + CookieModule.setCookiePolicyHandler(new GalleryCookiePolicyHandler()); + } + + public void setURLString (String val) { + mURLString = val; + mLoggedIn = false; + } + public String getURLString () { + return mURLString; + } + public void setUsername (String val) { + mUsername = val; + mLoggedIn = false; + } + public String getUsername () { + return mUsername; + } + public void setPassword (String val) { + mPassword = val; + mLoggedIn = false; + } + public String getPassword () { + return mPassword; + } + public void setAlbum (String val) { + mAlbum = val; + } + public String getAlbum () { + return mAlbum; + } + + + //------------------------------------------------------------------------- + //-- + //------------------------------------------------------------------------- + public void uploadFiles(ArrayList fileList) { + + mFileList = fileList; + final SwingWorker worker = new SwingWorker() { + public Object construct() { + return new ActualTask("upload"); + } + }; + worker.start(); + } + + //------------------------------------------------------------------------- + //-- + //------------------------------------------------------------------------- + public void fetchAlbums() { + + final SwingWorker worker = new SwingWorker() { + public Object construct() { + return new ActualTask("fetch-albums"); + } + }; + worker.start(); + } + + //------------------------------------------------------------------------- + //-- + //------------------------------------------------------------------------- + class ActualTask { + ActualTask (String task) { + + //-- login --- + if (!mLoggedIn) { + if (!login()) { + mDone = true; + return; + } + } + + mDone = false; + + if (task.equals("upload")) { + + //-- upload each file, one at a time --- + mDone = false; + boolean allGood = true; + mUploadedCount = 0; + Iterator iter = mFileList.iterator(); + while (iter.hasNext() && allGood) { + File file = (File) iter.next(); + allGood = uploadFile(file); + mUploadedCount++; + } + status("[Upload] Complete."); + } + + else if (task.equals("fetch-albums")) { + + requestAlbumList(); + status("[Album Fetch] Complete."); + + } + + mDone = true; + + + } + } + + //------------------------------------------------------------------------- + //-- + //------------------------------------------------------------------------- + private boolean login() { + + mLoggedIn = false; + String loginMessage; + status("[Login] Logging in..."); + + try + { + URL url = new URL(mURLString); + String loginPage = url.getFile() + SCRIPT_NAME; + + NVPair form_data[] = { + new NVPair("cmd", "login"), + new NVPair("protocal_version", PROTOCAL_VERSION), + new NVPair("uname", mUsername), + new NVPair("password", mPassword) + }; + + mConnection = new HTTPConnection(url); + HTTPResponse rsp = mConnection.Post(loginPage, form_data); + + if (rsp.getStatusCode() >= 300) + { + loginMessage = "HTTP Error: "+rsp.getReasonLine(); + + } else { + String response = new String(rsp.getData()); + + if (response.equals("SUCCESS")) { + mLoggedIn = true; + loginMessage = "Success"; + } else { + loginMessage = "Login Error: " + response; + } + + } + } + catch (IOException ioe) + { + loginMessage = "Error: " + ioe.toString(); + } + + catch (ModuleException me) + { + loginMessage = "Error handling request: " + me.getMessage(); + } + + status("[Login] " + loginMessage); + return mLoggedIn; + } + + //------------------------------------------------------------------------- + //-- + //------------------------------------------------------------------------- + private boolean requestAlbumList() { + + mLoggedIn = false; + String albumMessage; + status("[Fetch Albums]"); + + try + { + URL url = new URL(mURLString); + String loginPage = url.getFile() + SCRIPT_NAME; + + NVPair form_data[] = { + new NVPair("cmd", "fetch-albums"), + new NVPair("protocal_version", PROTOCAL_VERSION), + new NVPair("uname", mUsername), + new NVPair("password", mPassword) + }; + + mConnection = new HTTPConnection(url); + HTTPResponse rsp = mConnection.Post(loginPage, form_data); + + if (rsp.getStatusCode() >= 300) + { + albumMessage = "HTTP Error: "+rsp.getReasonLine(); + + } else { + String response = new String(rsp.getData()); + + if (response.indexOf("SUCCESS") >= 0) { + albumMessage = "Success"; + + System.out.println(response); + mAlbumList = new ArrayList(); + + // build the list of hashtables here... + StringTokenizer lineT = new StringTokenizer(response, "\n"); + while (lineT.hasMoreTokens()) { + StringTokenizer colT = new StringTokenizer(lineT.nextToken(), "\t"); + Hashtable h = new Hashtable(); + if (colT.countTokens() == 2) { + h.put("name", URLDecoder.decode(colT.nextToken())); + h.put("title", URLDecoder.decode(colT.nextToken())); + mAlbumList.add(h); + } + } + + } else { + albumMessage = " Error: [" + response + "]"; + } + + } + } + catch (IOException ioe) + { + albumMessage = "Error: " + ioe.toString(); + } + + catch (ModuleException me) + { + albumMessage = "Error handling request: " + me.getMessage(); + } + catch (Exception ee) + { + albumMessage = "Error: " + ee.getMessage(); + } + + status("[Album Fetch] " + albumMessage); + return mLoggedIn; + } + + //------------------------------------------------------------------------- + //-- + //------------------------------------------------------------------------- + public boolean uploadFile(File file) { + String filename = file.getName(); + status("[Upload " + filename + "] Uploading..."); + + String uploadMessage; + + try + { + URL url = new URL(mURLString); + String savePage = url.getFile() + SCRIPT_NAME; + + NVPair[] opts = { + new NVPair("set_albumName", mAlbum), + new NVPair("cmd", "add-item"), + new NVPair("protocal_version", PROTOCAL_VERSION) + }; + NVPair[] afile = { new NVPair("userfile", file.getAbsolutePath()) }; + NVPair[] hdrs = new NVPair[1]; + byte[] data = Codecs.mpFormDataEncode(opts, afile, hdrs); + HTTPResponse rsp = mConnection.Post(savePage, data, hdrs); + if (rsp.getStatusCode() >= 300) + { + uploadMessage = "HTTP Error: "+rsp.getReasonLine(); + + } else { + String response = new String(rsp.getData()); + + if (response.equals("SUCCESS")) { + uploadMessage = "Success"; + } else { + uploadMessage = "Upload Error: " + response; + } + } + } + catch (IOException ioe) + { + uploadMessage = "Error: " + ioe.toString(); + } + + catch (ModuleException me) + { + uploadMessage = "Error handling request: " + me.getMessage(); + } + + status("[Upload " + filename + "] " + uploadMessage); + return (uploadMessage.equals("Success")); + } + + //------------------------------------------------------------------------- + //-- + //------------------------------------------------------------------------- + private void status(String message) { + mStatus = message; + System.out.println(message); + } + + public String getStatus() { + return mStatus; + } + + public int getUploadedCount() { + return mUploadedCount; + } + + public ArrayList getAlbumList() { + return mAlbumList; + } + public boolean done() { + return mDone; + } + +} \ No newline at end of file diff --git a/com/gallery/GalleryRemote/GalleryCookiePolicyHandler.java b/com/gallery/GalleryRemote/GalleryCookiePolicyHandler.java new file mode 100644 index 0000000..5a75a09 --- /dev/null +++ b/com/gallery/GalleryRemote/GalleryCookiePolicyHandler.java @@ -0,0 +1,36 @@ +/* + * Gallery Remote - a File Upload Utility for Gallery + * + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2001 Bharat Mediratta + * + * 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. + */ + +package com.gallery.GalleryRemote; + +import HTTPClient.*; + +public class GalleryCookiePolicyHandler implements CookiePolicyHandler +{ + + public boolean acceptCookie(Cookie cookie, RoRequest req, RoResponse resp) { + return true; + } + + public boolean sendCookie(Cookie cookie, RoRequest req) { + return true; + } +} diff --git a/com/gallery/GalleryRemote/GalleryFileFilter.java b/com/gallery/GalleryRemote/GalleryFileFilter.java new file mode 100644 index 0000000..757d926 --- /dev/null +++ b/com/gallery/GalleryRemote/GalleryFileFilter.java @@ -0,0 +1,66 @@ +/* + * Gallery Remote - a File Upload Utility for Gallery + * + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2001 Bharat Mediratta + * + * 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. + */ + +package com.gallery.GalleryRemote; + +import java.io.File; +import javax.swing.*; +import javax.swing.filechooser.*; + +public class GalleryFileFilter extends FileFilter { + + // Accept all directories and all gif, jpg files. + public boolean accept(File f) { + if (f.isDirectory()) { + return true; + } + + String extension = getExtension(f); + if (extension != null) { + if (extension.equals("gif") || + extension.equals("jpeg") || + extension.equals("jpg")) { + return true; + } else { + return false; + } + } + + return false; + } + + // The description of this filter + public String getDescription() { + return "Gallery Items (*.jpg, *.jpeg, *.gif)"; + } + + public String getExtension(File f) { + String ext = null; + String s = f.getName(); + int i = s.lastIndexOf('.'); + + if (i > 0 && i < s.length() - 1) { + ext = s.substring(i+1).toLowerCase(); + } + return ext; + } +} + diff --git a/com/gallery/GalleryRemote/GalleryRemote.java b/com/gallery/GalleryRemote/GalleryRemote.java new file mode 100644 index 0000000..c8165ad --- /dev/null +++ b/com/gallery/GalleryRemote/GalleryRemote.java @@ -0,0 +1,50 @@ +/* + * Gallery Remote - a File Upload Utility for Gallery + * + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2001 Bharat Mediratta + * + * 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. + */ + +package com.gallery.GalleryRemote; +import javax.swing.*; + +public class GalleryRemote { + public GalleryRemote() { + try { + // For native Look and Feel, uncomment the following code. + /// * + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Exception e) { + } + //* / + MainFrame frame = new MainFrame(); + frame.initComponents(); + frame.setVisible(true); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + // Main entry point + static public void main(String[] args) { + new GalleryRemote(); + } + +} diff --git a/com/gallery/GalleryRemote/MainFrame.java b/com/gallery/GalleryRemote/MainFrame.java new file mode 100644 index 0000000..692d5cc --- /dev/null +++ b/com/gallery/GalleryRemote/MainFrame.java @@ -0,0 +1,637 @@ +/* + * Gallery Remote - a File Upload Utility for Gallery + * + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2001 Bharat Mediratta + * + * 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. + */ + +package com.gallery.GalleryRemote; + +import java.awt.*; +import java.awt.event.*; +import java.awt.dnd.*; +import java.awt.datatransfer.*; +import javax.swing.*; +import javax.swing.border.*; +import java.util.*; +import HTTPClient.*; +import java.io.*; +import java.net.URL; + +public class MainFrame extends javax.swing.JFrame { + + public final static String APP_VERSION_STRING = "0.3"; + public final static int ONE_SECOND = 1000; + + javax.swing.JMenuBar jMenuBar = new javax.swing.JMenuBar(); + javax.swing.JMenu jMenuFile = new javax.swing.JMenu(); + javax.swing.JMenuItem jMenuFileQuit = new javax.swing.JMenuItem(); + javax.swing.JMenu jMenuHelp = new javax.swing.JMenu(); + javax.swing.JMenuItem jMenuHelpAbout = new javax.swing.JMenuItem(); + javax.swing.JPanel jPanelMain = new javax.swing.JPanel(); + javax.swing.JPanel jPanelSettings = new javax.swing.JPanel(); + javax.swing.JLabel jLabelURL = new javax.swing.JLabel(); + javax.swing.JLabel jLabelUser = new javax.swing.JLabel(); + javax.swing.JLabel jLabelPasword = new javax.swing.JLabel(); + javax.swing.JTextField jTextURL = new javax.swing.JTextField(); + javax.swing.JTextField jTextUsername = new javax.swing.JTextField(); + javax.swing.JPasswordField jPasswordField = new javax.swing.JPasswordField(); + javax.swing.JLabel jLabelAlbum = new javax.swing.JLabel(); + javax.swing.JButton jButtonFetchAlbums = new javax.swing.JButton(); + javax.swing.JComboBox jComboBoxAlbums = new javax.swing.JComboBox(); + javax.swing.JPanel jPanelScrollPane = new javax.swing.JPanel(); + javax.swing.JScrollPane jScrollPane = new javax.swing.JScrollPane(); + DroppableList jListItems = new DroppableList(this); + javax.swing.JPanel jPanelButtons = new javax.swing.JPanel(); + javax.swing.JPanel jPanelButtonsTop = new javax.swing.JPanel(); + javax.swing.JButton jButtonAddFiles = new javax.swing.JButton(); + javax.swing.JButton jButtonUpload = new javax.swing.JButton(); + javax.swing.JPanel jPanelButtonsBottom = new javax.swing.JPanel(); + javax.swing.JTextField jLabelStatus = new javax.swing.JTextField(); + javax.swing.JProgressBar jProgressBar = new javax.swing.JProgressBar(); + + private GalleryComm mGalleryComm; + private ArrayList mFileList; + private ArrayList mAlbumList; + private boolean mInProgress = false; + private Timer mTimer; + + public MainFrame() { + + mGalleryComm = new GalleryComm(); + mFileList = new ArrayList(); + mAlbumList = new ArrayList(); + + } + + public void initComponents() throws Exception { + + jMenuBar.setVisible(true); + jMenuBar.add(jMenuFile); + jMenuBar.add(jMenuHelp); + + jMenuFile.setVisible(true); + jMenuFile.setText("File"); + jMenuFile.add(jMenuFileQuit); + + jMenuFileQuit.setVisible(true); + jMenuFileQuit.setText("Quit"); + jMenuFileQuit.setActionCommand("File.Quit"); + + jMenuHelp.setVisible(true); + jMenuHelp.setText("Help"); + jMenuHelp.add(jMenuHelpAbout); + + jMenuHelpAbout.setVisible(true); + jMenuHelpAbout.setText("About"); + jMenuHelpAbout.setActionCommand("Help.About"); + + //-- the settongs panel --- + jLabelURL.setSize(new java.awt.Dimension(90, 20)); + jLabelURL.setLocation(new java.awt.Point(7, 20)); + jLabelURL.setVisible(true); + jLabelURL.setText("Gallery URL"); + jLabelURL.setHorizontalTextPosition(javax.swing.JLabel.RIGHT); + jLabelURL.setHorizontalAlignment(javax.swing.JLabel.RIGHT); + + jLabelUser.setSize(new java.awt.Dimension(90, 20)); + jLabelUser.setLocation(new java.awt.Point(9, 40)); + jLabelUser.setVisible(true); + jLabelUser.setText("Username"); + jLabelUser.setHorizontalTextPosition(javax.swing.JLabel.RIGHT); + jLabelUser.setHorizontalAlignment(javax.swing.JLabel.RIGHT); + + jLabelPasword.setSize(new java.awt.Dimension(90, 20)); + jLabelPasword.setLocation(new java.awt.Point(9, 60)); + jLabelPasword.setVisible(true); + jLabelPasword.setText("Password"); + jLabelPasword.setHorizontalTextPosition(javax.swing.JLabel.RIGHT); + jLabelPasword.setHorizontalAlignment(javax.swing.JLabel.RIGHT); + + jTextURL.setSize(new java.awt.Dimension(305, 20)); + jTextURL.setLocation(new java.awt.Point(106, 20)); + jTextURL.setVisible(true); + jTextURL.setText(""); + + jTextUsername.setSize(new java.awt.Dimension(180, 20)); + jTextUsername.setLocation(new java.awt.Point(106, 40)); + jTextUsername.setVisible(true); + jTextUsername.setText(""); + + jPasswordField.setSize(new java.awt.Dimension(180, 20)); + jPasswordField.setLocation(new java.awt.Point(106, 60)); + jPasswordField.setVisible(true); + jPasswordField.setText(""); + + jLabelAlbum.setSize(new java.awt.Dimension(103, 20)); + jLabelAlbum.setLocation(new java.awt.Point(-4, 91)); + jLabelAlbum.setVisible(true); + jLabelAlbum.setText("Upload to Album"); + jLabelAlbum.setHorizontalTextPosition(javax.swing.JLabel.RIGHT); + jLabelAlbum.setHorizontalAlignment(javax.swing.JLabel.RIGHT); + + jButtonFetchAlbums.setSize(new java.awt.Dimension(117, 25)); + jButtonFetchAlbums.setEnabled(false); + jButtonFetchAlbums.setLocation(new java.awt.Point(294, 92)); + jButtonFetchAlbums.setVisible(true); + jButtonFetchAlbums.setText("Fetch Albums"); + + jComboBoxAlbums.setSize(new java.awt.Dimension(180, 25)); + jComboBoxAlbums.setLocation(new java.awt.Point(106, 92)); + jComboBoxAlbums.setVisible(true); + + jPanelSettings.setVisible(true); + jPanelSettings.setLayout(null); + jPanelSettings.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Destination Gallery")); + jPanelSettings.setPreferredSize(new java.awt.Dimension(420, 125)); + jPanelSettings.setMinimumSize(new java.awt.Dimension(420, 125)); + jPanelSettings.setMaximumSize(new java.awt.Dimension(4000, 125)); + jPanelSettings.add(jLabelURL); + jPanelSettings.add(jLabelUser); + jPanelSettings.add(jLabelPasword); + jPanelSettings.add(jTextURL); + jPanelSettings.add(jTextUsername); + jPanelSettings.add(jPasswordField); + jPanelSettings.add(jLabelAlbum); + jPanelSettings.add(jButtonFetchAlbums); + jPanelSettings.add(jComboBoxAlbums); + + //-- the scroll pane --- + jScrollPane.setVisible(true); + jScrollPane.getViewport().add(jListItems); + jListItems.setVisible(true); + jPanelScrollPane.setLayout(new BorderLayout()); + jPanelScrollPane.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Files to Upload (Drag and Drop files into this panel)")); + jPanelScrollPane.setVisible(true); + jPanelScrollPane.add(jScrollPane); + + jListItems = new DroppableList(this); + //jListItems.setCellRenderer(new FileCellRenderer()); + + jScrollPane.getViewport().add(jListItems); + + //-- the button panel --- + jButtonAddFiles.setSize(new java.awt.Dimension(200, 25)); + jButtonAddFiles.setLocation(new java.awt.Point(0, 0)); + jButtonAddFiles.setVisible(true); + jButtonAddFiles.setText("Browse for Files to Upload..."); + + jButtonUpload.setSize(new java.awt.Dimension(200, 25)); + jButtonUpload.setLocation(new java.awt.Point(220, 0)); + jButtonUpload.setVisible(true); + jButtonUpload.setText("Upload Now!"); + + jPanelButtonsTop.setVisible(true); + jPanelButtonsTop.setLayout(null); + jPanelButtonsTop.setPreferredSize(new java.awt.Dimension(420, 30)); + jPanelButtonsTop.add(jButtonAddFiles); + jPanelButtonsTop.add(jButtonUpload); + + jLabelStatus.setVisible(true); + jLabelStatus.setPreferredSize(new java.awt.Dimension(420, 22)); + jLabelStatus.setMinimumSize(new java.awt.Dimension(4,22)); + jLabelStatus.setMaximumSize(new java.awt.Dimension(4000, 22)); + jLabelStatus.setText("Drag and Drop files to Upload into panel above or Browse for them."); + jLabelStatus.setEnabled(false); + + jProgressBar.setVisible(true); + jProgressBar.setStringPainted(true); + + jPanelButtonsBottom.setVisible(true); + jPanelButtonsBottom.setLayout(new javax.swing.BoxLayout(jPanelButtonsBottom, 1)); + jPanelButtonsBottom.setPreferredSize(new java.awt.Dimension(420, 46)); + jPanelButtonsBottom.add(jLabelStatus); + jPanelButtonsBottom.add(Box.createVerticalStrut(2)); + jPanelButtonsBottom.add(jProgressBar); + + jPanelButtons.setVisible(true); + jPanelButtons.setLayout(new javax.swing.BoxLayout(jPanelButtons, 1)); + jPanelButtons.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + jPanelButtons.add(jPanelButtonsTop); + jPanelButtons.add(jPanelButtonsBottom); + + jPanelMain.setVisible(true); + jPanelMain.setLayout(new javax.swing.BoxLayout(jPanelMain, 1)); + jPanelMain.add(jPanelSettings); + jPanelMain.add(jPanelScrollPane); + jPanelMain.add(jPanelButtons); + + setLocation(new java.awt.Point(2, 0)); + setSize(new java.awt.Dimension(435, 573)); + setJMenuBar(jMenuBar); + getContentPane().setLayout(new java.awt.BorderLayout(0, 0)); + setTitle("Gallery Remote"); + getContentPane().add(jPanelMain, "Center"); + + + jButtonFetchAlbums.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent e) { + jButtonFetchAlbumsActionPerformed(e); + } + }); + jButtonAddFiles.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent e) { + jButtonAddFilesActionPerformed(e); + } + }); + jButtonUpload.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent e) { + jButtonUploadActionPerformed(e); + } + }); + addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent e) { + thisWindowClosing(e); + } + }); + + jMenuFileQuit.addActionListener(new menuBarActionListener()); + jMenuHelpAbout.addActionListener(new menuBarActionListener()); + + updateUI(); + } + + private boolean mShown = false; + + public void addNotify() { + super.addNotify(); + + if (mShown) + return; + + // resize frame to account for menubar + JMenuBar jMenuBar = getJMenuBar(); + if (jMenuBar != null) { + int jMenuBarHeight = jMenuBar.getPreferredSize().height; + Dimension dimension = getSize(); + dimension.height += jMenuBarHeight; + setSize(dimension); + } + + mShown = true; + } + + // Close the window when the close box is clicked + void thisWindowClosing(java.awt.event.WindowEvent e) { + setVisible(false); + dispose(); + System.exit(0); + } + + //------------------------------------------------------------------------- + //-- Update the UI + //------------------------------------------------------------------------- + private void updateUI() { + + Runnable doUpdate = new Runnable() { + public void run() { + + //-- if the list is empty, disable the Upload --- + jButtonUpload.setEnabled((mFileList.size() > 0) && + !mInProgress && + (jComboBoxAlbums.getSelectedIndex() >= 0)); + jButtonAddFiles.setEnabled(!mInProgress); + jButtonFetchAlbums.setEnabled(!mInProgress); + + jProgressBar.setStringPainted(mInProgress); + + //-- build the file list --- + ArrayList filenames = new ArrayList(); + + Iterator iter = mFileList.iterator(); + while (iter.hasNext()) { + File file = (File) iter.next(); + filenames.add(file.getName() + " [" + file.getParent() + "]"); + } + + jListItems.setListData(filenames.toArray()); + + } + }; + SwingUtilities.invokeLater(doUpdate); + }; + + + //------------------------------------------------------------------------- + //-- Update the Status + //------------------------------------------------------------------------- + private void updateFileList() { + + Runnable doUpdate = new Runnable() { + public void run() { + //-- build the file list --- + ArrayList filenames = new ArrayList(); + + Iterator iter = mFileList.iterator(); + while (iter.hasNext()) { + File file = (File) iter.next(); + filenames.add(file.getName() + " [" + file.getParent() + "]"); + } + + jListItems.setListData(filenames.toArray()); + } + }; + SwingUtilities.invokeLater(doUpdate); + + }; + + //------------------------------------------------------------------------- + //-- Update the Status + //------------------------------------------------------------------------- + private void updateAlbumCombo() { + + Runnable doUpdate = new Runnable() { + public void run() { + //-- build the list --- + Iterator iter = mAlbumList.iterator(); + while (iter.hasNext()) { + Hashtable h = (Hashtable) iter.next(); + jComboBoxAlbums.addItem((String) h.get("title")); + } + + } + }; + SwingUtilities.invokeLater(doUpdate); + + }; + + //------------------------------------------------------------------------- + //-- Update the Status + //------------------------------------------------------------------------- + private void updateStatus(String message) { + + final String m = message; + Runnable doUpdate = new Runnable() { + public void run() { + jLabelStatus.setText(m); + } + }; + SwingUtilities.invokeLater(doUpdate); + + }; + + //------------------------------------------------------------------------- + //-- Update the Status + //------------------------------------------------------------------------- + private void updateProgress(int value) { + + final int val = value; + Runnable doUpdate = new Runnable() { + public void run() { + jProgressBar.setValue(val); + } + }; + SwingUtilities.invokeLater(doUpdate); + + }; + + //------------------------------------------------------------------------- + //-- Add Files Button + //------------------------------------------------------------------------- + public void jButtonAddFilesActionPerformed(java.awt.event.ActionEvent e) { + + //-- get any new files --- + File[] files = AddFileDialog.addFiles(this); + + if (files != null) { + + mFileList.addAll(Arrays.asList(files)); + Object[] items = mFileList.toArray(); + Arrays.sort(items, new Comparator() { + public int compare (Object o1, Object o2) { + String f1 = ((File) o1).getAbsolutePath(); + String f2 = ((File) o2).getAbsolutePath(); + return (f1.compareToIgnoreCase(f2)); + } + public boolean equals (Object o1, Object o2) { + String f1 = ((File) o1).getAbsolutePath(); + String f2 = ((File) o2).getAbsolutePath(); + return (f1.equals(f2)); + } + }); + + mFileList.clear(); + mFileList.addAll(Arrays.asList(items)); + + updateFileList(); + updateUI(); + + } + + + + + } + + //------------------------------------------------------------------------- + //-- Upload Action + //------------------------------------------------------------------------- + public void jButtonUploadActionPerformed(java.awt.event.ActionEvent e) { + + mGalleryComm.setURLString(jTextURL.getText()); + mGalleryComm.setUsername(jTextUsername.getText()); + mGalleryComm.setPassword(jPasswordField.getText()); + + int index = jComboBoxAlbums.getSelectedIndex(); + Hashtable h = (Hashtable)mAlbumList.get(index); + mGalleryComm.setAlbum((String) h.get("name")); + + mGalleryComm.uploadFiles(mFileList); + + mInProgress = true; + updateUI(); + + jProgressBar.setMaximum(mFileList.size()); + + mTimer = new Timer(ONE_SECOND, new ActionListener() { + public void actionPerformed(ActionEvent evt) { + updateProgress(mGalleryComm.getUploadedCount()); + updateStatus(mGalleryComm.getStatus()); + if (mGalleryComm.done()) { + mTimer.stop(); + + //-- reset the UI --- + updateProgress(jProgressBar.getMinimum()); + mFileList.clear(); + mInProgress = false; + updateUI(); + + } + } + }); + + mTimer.start(); + + } + + //------------------------------------------------------------------------- + //-- Fetch Albums Action + //------------------------------------------------------------------------- + public void jButtonFetchAlbumsActionPerformed(java.awt.event.ActionEvent e) { + + mGalleryComm.setURLString(jTextURL.getText()); + mGalleryComm.setUsername(jTextUsername.getText()); + mGalleryComm.setPassword(jPasswordField.getText()); + mGalleryComm.fetchAlbums(); + + mInProgress = true; + updateUI(); + + jProgressBar.setMaximum(mFileList.size()); + + mTimer = new Timer(ONE_SECOND, new ActionListener() { + public void actionPerformed(ActionEvent evt) { + updateStatus(mGalleryComm.getStatus()); + if (mGalleryComm.done()) { + mTimer.stop(); + + //-- reset the UI --- + mAlbumList = mGalleryComm.getAlbumList(); + mInProgress = false; + updateAlbumCombo(); + updateUI(); + } + } + }); + + mTimer.start(); + + } + + //------------------------------------------------------------------------- + //-- The menu bar action listener + //------------------------------------------------------------------------- + public class menuBarActionListener implements ActionListener + { + + public void actionPerformed(java.awt.event.ActionEvent e) { + if (e.getActionCommand().equals("File.Quit")) { + + thisWindowClosing(null); + + } else if (e.getActionCommand().equals("Help.About")) { + + try { + AboutBox ab = new AboutBox(); + ab.initComponents(); + ab.setVersionString(APP_VERSION_STRING); + ab.setVisible(true); + } + catch (Exception err) { + err.printStackTrace(); + } + } else { + } + } + + } + + //------------------------------------------------------------------------- + //-- + //------------------------------------------------------------------------- + public class FileCellRenderer implements ListCellRenderer + { + DefaultListCellRenderer listCellRenderer = + new DefaultListCellRenderer(); + public Component getListCellRendererComponent( + JList list, Object value, int index, + boolean selected, boolean hasFocus) + { + listCellRenderer.getListCellRendererComponent( + list, value, index, selected, hasFocus); + listCellRenderer.setText(getValueString(value)); + return listCellRenderer; + } + private String getValueString(Object value) + { + String returnString = "null"; + if (value != null) { + if (value instanceof Hashtable) { + Hashtable h = (Hashtable)value; + String name = (String)h.get("name"); + String parent = (String)h.get("parent"); + returnString = name + " [" + parent + "]"; + } else { + returnString = "X: " + value.toString(); + } + } + return returnString; + } + } + + //------------------------------------------------------------------------- + //-- + //------------------------------------------------------------------------- + public class DroppableList extends JList + implements DropTargetListener + { + DropTarget dropTarget = new DropTarget (this, this); + MainFrame mDaddy; + + public DroppableList(MainFrame daddy) + { + setModel(new DefaultListModel()); + mDaddy = daddy; + } + + public void dragEnter (DropTargetDragEvent dropTargetDragEvent) + { + dropTargetDragEvent.acceptDrag (DnDConstants.ACTION_COPY_OR_MOVE); + } + + public void dragExit (DropTargetEvent dropTargetEvent) {} + public void dragOver (DropTargetDragEvent dropTargetDragEvent) {} + public void dropActionChanged (DropTargetDragEvent dropTargetDragEvent){} + + public synchronized void drop (DropTargetDropEvent dropTargetDropEvent) + { + try + { + Transferable tr = dropTargetDropEvent.getTransferable(); + if (tr.isDataFlavorSupported (DataFlavor.javaFileListFlavor)) + { + dropTargetDropEvent.acceptDrop ( + DnDConstants.ACTION_COPY_OR_MOVE); + java.util.List fileList = (java.util.List) + tr.getTransferData(DataFlavor.javaFileListFlavor); + Iterator iterator = fileList.iterator(); + while (iterator.hasNext()) + { + File file = (File)iterator.next(); + mFileList.add(file); + } + dropTargetDropEvent.getDropTargetContext().dropComplete(true); + mDaddy.updateUI(); + } else { + System.err.println ("Rejected"); + dropTargetDropEvent.rejectDrop(); + } + } catch (IOException io) { + io.printStackTrace(); + dropTargetDropEvent.rejectDrop(); + } catch (UnsupportedFlavorException ufe) { + ufe.printStackTrace(); + dropTargetDropEvent.rejectDrop(); + } + } + + } + + +} diff --git a/com/gallery/GalleryRemote/SwingWorker.java b/com/gallery/GalleryRemote/SwingWorker.java new file mode 100644 index 0000000..f4535fa --- /dev/null +++ b/com/gallery/GalleryRemote/SwingWorker.java @@ -0,0 +1,153 @@ +/* + * Gallery Remote - a File Upload Utility for Gallery + * + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2001 Bharat Mediratta + * + * 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. + */ + +package com.gallery.GalleryRemote; + +import javax.swing.SwingUtilities; + +/** + * This is the 3rd version of SwingWorker (also known as + * SwingWorker 3), an abstract class that you subclass to + * perform GUI-related work in a dedicated thread. For + * instructions on using this class, see: + * + * http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html + * + * Note that the API changed slightly in the 3rd version: + * You must now invoke start() on the SwingWorker after + * creating it. + */ +public abstract class SwingWorker { + private Object value; // see getValue(), setValue() + private Thread thread; + + /** + * Class to maintain reference to current worker thread + * under separate synchronization control. + */ + private static class ThreadVar { + private Thread thread; + ThreadVar(Thread t) { thread = t; } + synchronized Thread get() { return thread; } + synchronized void clear() { thread = null; } + } + + private ThreadVar threadVar; + + /** + * Get the value produced by the worker thread, or null if it + * hasn't been constructed yet. + */ + protected synchronized Object getValue() { + return value; + } + + /** + * Set the value produced by worker thread + */ + private synchronized void setValue(Object x) { + value = x; + } + + /** + * Compute the value to be returned by the get method. + */ + public abstract Object construct(); + + /** + * Called on the event dispatching thread (not on the worker thread) + * after the construct method has returned. + */ + public void finished() { + } + + /** + * A new method that interrupts the worker thread. Call this method + * to force the worker to stop what it's doing. + */ + public void interrupt() { + Thread t = threadVar.get(); + if (t != null) { + t.interrupt(); + } + threadVar.clear(); + } + + /** + * Return the value created by the construct method. + * Returns null if either the constructing thread or the current + * thread was interrupted before a value was produced. + * + * @return the value created by the construct method + */ + public Object get() { + while (true) { + Thread t = threadVar.get(); + if (t == null) { + return getValue(); + } + try { + t.join(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); // propagate + return null; + } + } + } + + + /** + * Start a thread that will call the construct method + * and then exit. + */ + public SwingWorker() { + final Runnable doFinished = new Runnable() { + public void run() { finished(); } + }; + + Runnable doConstruct = new Runnable() { + public void run() { + try { + setValue(construct()); + } + finally { + threadVar.clear(); + } + + SwingUtilities.invokeLater(doFinished); + } + }; + + Thread t = new Thread(doConstruct); + threadVar = new ThreadVar(t); + } + + /** + * Start the worker thread. + */ + public void start() { + Thread t = threadVar.get(); + if (t != null) { + t.start(); + } + } +}